@web5/agent
Version:
336 lines • 19.1 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { Cid, DataStoreLevel, Dwn, DwnInterfaceName, DwnMethodName, EventEmitterStream, EventLogLevel, Message, MessageStoreLevel, ResumableTaskStoreLevel } from '@tbd54566975/dwn-sdk-js';
import { NodeStream } from '@web5/common';
import { CryptoUtils } from '@web5/crypto';
import { DidDht, DidJwk, DidResolverCacheLevel, UniversalResolver } from '@web5/dids';
import { DwnInterface, dwnMessageConstructors } from './types/dwn.js';
import { blobToIsomorphicNodeReadable, getDwnServiceEndpointUrls, isRecordsWrite, webReadableToIsomorphicNodeReadable } from './utils.js';
export function isDwnRequest(dwnRequest, messageType) {
return dwnRequest.messageType === messageType;
}
export function isDwnMessage(messageType, message) {
const incomingMessageInterfaceName = message.descriptor.interface + message.descriptor.method;
return incomingMessageInterfaceName === messageType;
}
export function isRecordsType(messageType) {
return messageType === DwnInterface.RecordsDelete ||
messageType === DwnInterface.RecordsQuery ||
messageType === DwnInterface.RecordsRead ||
messageType === DwnInterface.RecordsSubscribe ||
messageType === DwnInterface.RecordsWrite;
}
export function isRecordPermissionScope(scope) {
return scope.interface === DwnInterfaceName.Records;
}
export function isMessagesPermissionScope(scope) {
return scope.interface === DwnInterfaceName.Messages;
}
export class AgentDwnApi {
constructor({ agent, dwn }) {
// If an agent is provided, set it as the execution context for this API.
this._agent = agent;
// Set the DWN instance for this API.
this._dwn = dwn;
}
/**
* Retrieves the `Web5PlatformAgent` execution context.
*
* @returns The `Web5PlatformAgent` instance that represents the current execution context.
* @throws Will throw an error if the `agent` instance property is undefined.
*/
get agent() {
if (this._agent === undefined) {
throw new Error('AgentDwnApi: Unable to determine agent execution context.');
}
return this._agent;
}
set agent(agent) {
this._agent = agent;
}
/**
* Public getter for the DWN instance used by this API.
*
* Notes:
* - This getter is public to allow advanced developers to access the DWN instance directly.
* However, it is recommended to use the `processRequest` method to interact with the DWN
* instance to ensure that the DWN message is constructed correctly.
* - The getter is named `node` to avoid confusion with the `dwn` property of the
* `Web5PlatformAgent`. In other words, so that a developer can call `agent.dwn.node` to access
* the DWN instance and not `agent.dwn.dwn`.
*/
get node() {
return this._dwn;
}
static createDwn({ dataPath, dataStore, didResolver, eventLog, eventStream, messageStore, tenantGate, resumableTaskStore }) {
return __awaiter(this, void 0, void 0, function* () {
dataStore !== null && dataStore !== void 0 ? dataStore : (dataStore = new DataStoreLevel({ blockstoreLocation: `${dataPath}/DWN_DATASTORE` }));
didResolver !== null && didResolver !== void 0 ? didResolver : (didResolver = new UniversalResolver({
didResolvers: [DidDht, DidJwk],
cache: new DidResolverCacheLevel({ location: `${dataPath}/DID_RESOLVERCACHE` }),
}));
eventLog !== null && eventLog !== void 0 ? eventLog : (eventLog = new EventLogLevel({ location: `${dataPath}/DWN_EVENTLOG` }));
messageStore !== null && messageStore !== void 0 ? messageStore : (messageStore = new MessageStoreLevel(({
blockstoreLocation: `${dataPath}/DWN_MESSAGESTORE`,
indexLocation: `${dataPath}/DWN_MESSAGEINDEX`
})));
resumableTaskStore !== null && resumableTaskStore !== void 0 ? resumableTaskStore : (resumableTaskStore = new ResumableTaskStoreLevel({ location: `${dataPath}/DWN_RESUMABLETASKSTORE` }));
eventStream !== null && eventStream !== void 0 ? eventStream : (eventStream = new EventEmitterStream());
return yield Dwn.create({ dataStore, didResolver, eventLog, eventStream, messageStore, tenantGate, resumableTaskStore });
});
}
processRequest(request) {
return __awaiter(this, void 0, void 0, function* () {
// Constructs a DWN message. and if there is a data payload, transforms the data to a Node
// Readable stream.
const { message, dataStream } = yield this.constructDwnMessage({ request });
// Extracts the optional subscription handler from the request to pass into `processMessage.
const { subscriptionHandler } = request;
// Conditionally processes the message with the DWN instance:
// - If `store` is not explicitly set to false, it sends the message to the DWN node for
// processing, passing along the target DID, the message, and any associated data stream.
// - If `store` is set to false, it immediately returns a simulated 'accepted' status without
// storing the message/data in the DWN node.
const reply = (request.store !== false)
? yield this._dwn.processMessage(request.target, message, { dataStream, subscriptionHandler })
: { status: { code: 202, detail: 'Accepted' } };
// Returns an object containing the reply from processing the message, the original message,
// and the content identifier (CID) of the message.
return {
reply,
message,
messageCid: yield Message.getCid(message),
};
});
}
sendRequest(request) {
return __awaiter(this, void 0, void 0, function* () {
// First, confirm the target DID can be dereferenced and extract the DWN service endpoint URLs.
const dwnEndpointUrls = yield getDwnServiceEndpointUrls(request.target, this.agent.did);
if (dwnEndpointUrls.length === 0) {
throw new Error(`AgentDwnApi: DID Service is missing or malformed: ${request.target}#dwn`);
}
let messageCid;
let message;
let data;
let subscriptionHandler;
// If `messageCid` is given, retrieve message and data, if any.
if ('messageCid' in request) {
({ message, data } = yield this.getDwnMessage({
author: request.author,
messageCid: request.messageCid,
messageType: request.messageType
}));
messageCid = request.messageCid;
}
else {
// Otherwise, construct a new message.
({ message } = yield this.constructDwnMessage({ request }));
if (request.dataStream && !(request.dataStream instanceof Blob)) {
throw new Error('AgentDwnApi: DataStream must be provided as a Blob');
}
data = request.dataStream;
subscriptionHandler = request.subscriptionHandler;
}
// Send the RPC request to the target DID's DWN service endpoint using the Agent's RPC client.
const reply = yield this.sendDwnRpcRequest({
targetDid: request.target,
dwnEndpointUrls,
message,
data,
subscriptionHandler
});
// If the message CID was not given in the `request`, compute it.
messageCid !== null && messageCid !== void 0 ? messageCid : (messageCid = yield Message.getCid(message));
// Returns an object containing the reply from processing the message, the original message,
// and the content identifier (CID) of the message.
return { reply, message, messageCid };
});
}
sendDwnRpcRequest({ targetDid, dwnEndpointUrls, message, data, subscriptionHandler }) {
return __awaiter(this, void 0, void 0, function* () {
const errorMessages = [];
if (message.descriptor.method === DwnMethodName.Subscribe && subscriptionHandler === undefined) {
throw new Error('AgentDwnApi: Subscription handler is required for subscription requests.');
}
// Try sending to author's publicly addressable DWNs until the first request succeeds.
for (let dwnUrl of dwnEndpointUrls) {
try {
if (subscriptionHandler !== undefined) {
// we get the server info to check if the server supports WebSocket for subscription requests
const serverInfo = yield this.agent.rpc.getServerInfo(dwnUrl);
if (!serverInfo.webSocketSupport) {
// If the server does not support WebSocket, add an error message and continue to the next URL.
errorMessages.push({
url: dwnUrl,
message: 'WebSocket support is not enabled on the server.'
});
continue;
}
// If the server supports WebSocket, replace the subscription URL with a socket transport.
// For `http` we use the unsecured `ws` protocol, and for `https` we use the secured `wss` protocol.
const parsedUrl = new URL(dwnUrl);
parsedUrl.protocol = parsedUrl.protocol === 'http:' ? 'ws:' : 'wss:';
dwnUrl = parsedUrl.toString();
}
const dwnReply = yield this.agent.rpc.sendDwnRequest({
dwnUrl,
targetDid,
message,
data,
subscriptionHandler
});
return dwnReply;
}
catch (error) {
errorMessages.push({
url: dwnUrl,
message: (error instanceof Error) ? error.message : 'Unknown error',
});
}
}
throw new Error(`Failed to send DWN RPC request: ${JSON.stringify(errorMessages)}`);
});
}
constructDwnMessage({ request }) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// if the request has a granteeDid, ensure the messageParams include the proper grant parameters
if (request.granteeDid && !this.hasGrantParams(request.messageParams)) {
throw new Error('AgentDwnApi: Requested to sign with a permission but no grant messageParams were provided in the request');
}
const rawMessage = request.rawMessage;
let readableStream;
// TODO: Consider refactoring to move data transformations imposed by fetch() limitations to the HTTP transport-related methods.
// if the request is a RecordsWrite message, we need to handle the data stream and update the messageParams accordingly
if (isDwnRequest(request, DwnInterface.RecordsWrite)) {
const messageParams = request.messageParams;
if (request.dataStream && !(messageParams === null || messageParams === void 0 ? void 0 : messageParams.data)) {
const { dataStream } = request;
let isomorphicNodeReadable;
if (dataStream instanceof Blob) {
isomorphicNodeReadable = blobToIsomorphicNodeReadable(dataStream);
readableStream = blobToIsomorphicNodeReadable(dataStream);
}
else if (dataStream instanceof ReadableStream) {
const [forCid, forProcessMessage] = dataStream.tee();
isomorphicNodeReadable = webReadableToIsomorphicNodeReadable(forCid);
readableStream = webReadableToIsomorphicNodeReadable(forProcessMessage);
}
if (!rawMessage) {
// @ts-ignore
messageParams.dataCid = yield Cid.computeDagPbCidFromStream(isomorphicNodeReadable);
// @ts-ignore
(_a = messageParams.dataSize) !== null && _a !== void 0 ? _a : (messageParams.dataSize = isomorphicNodeReadable['bytesRead']);
}
}
}
let dwnMessage;
const dwnMessageConstructor = dwnMessageConstructors[request.messageType];
// if there is no raw message provided, we need to create the dwn message
if (!rawMessage) {
// If we need to sign as an author delegate or with permissions we need to get the grantee's signer
// The messageParams should include either a permissionGrantId, or a delegatedGrant message
const signer = request.granteeDid ?
yield this.getSigner(request.granteeDid) :
yield this.getSigner(request.author);
dwnMessage = yield dwnMessageConstructor.create(Object.assign(Object.assign({}, request.messageParams), { signer }));
}
else {
dwnMessage = yield dwnMessageConstructor.parse(rawMessage);
if (isRecordsWrite(dwnMessage) && request.signAsOwner) {
// if we are signing as owner, we use the author's signer
const signer = yield this.getSigner(request.author);
yield dwnMessage.signAsOwner(signer);
}
else if (request.granteeDid && isRecordsWrite(dwnMessage) && request.signAsOwnerDelegate) {
// if we are signing as owner delegate, we use the grantee's signer and the provided delegated grant
const signer = yield this.getSigner(request.granteeDid);
//if we have reached here, the presence of the grant params has already been checked
const messageParams = request.messageParams;
yield dwnMessage.signAsOwnerDelegate(signer, messageParams.delegatedGrant);
}
}
return { message: dwnMessage.message, dataStream: readableStream };
});
}
hasGrantParams(params) {
return params !== undefined &&
(('permissionGrantId' in params && params.permissionGrantId !== undefined) ||
('delegatedGrant' in params && params.delegatedGrant !== undefined));
}
getSigner(author) {
return __awaiter(this, void 0, void 0, function* () {
// If the author is the Agent's DID, use the Agent's signer.
if (author === this.agent.agentDid.uri) {
const signer = yield this.agent.agentDid.getSigner();
return {
algorithm: signer.algorithm,
keyId: signer.keyId,
sign: (data) => __awaiter(this, void 0, void 0, function* () {
return yield signer.sign({ data });
})
};
}
else {
// Otherwise, use the author's DID to determine the signing method.
try {
const signingMethod = yield this.agent.did.getSigningMethod({ didUri: author });
if (!signingMethod.publicKeyJwk) {
throw new Error(`Verification method '${signingMethod.id}' does not contain a public key in JWK format`);
}
// Compute the key URI of the verification method's public key.
const keyUri = yield this.agent.keyManager.getKeyUri({ key: signingMethod.publicKeyJwk });
// Verify that the key is present in the key manager. If not, an error is thrown.
const publicKey = yield this.agent.keyManager.getPublicKey({ keyUri });
// Bind the Agent's Key Manager to the signer.
const keyManager = this.agent.keyManager;
return {
algorithm: CryptoUtils.getJoseSignatureAlgorithmFromPublicKey(publicKey),
keyId: signingMethod.id,
sign: (data) => __awaiter(this, void 0, void 0, function* () {
return yield keyManager.sign({ data, keyUri: keyUri });
})
};
}
catch (error) {
throw new Error(`AgentDwnApi: Unable to get signer for author '${author}': ${error.message}`);
}
}
});
}
/**
* FURTHER REFACTORING NEEDED BELOW THIS LINE
*/
getDwnMessage({ author, messageCid }) {
return __awaiter(this, void 0, void 0, function* () {
const signer = yield this.getSigner(author);
// Construct a MessagesRead message to fetch the message.
const messagesRead = yield dwnMessageConstructors[DwnInterface.MessagesRead].create({
messageCid: messageCid,
signer
});
const result = yield this._dwn.processMessage(author, messagesRead.message);
if (result.status.code !== 200) {
throw new Error(`AgentDwnApi: Failed to read message, response status: ${result.status.code} - ${result.status.detail}`);
}
const messageEntry = result.entry;
const message = messageEntry.message;
let dwnMessageWithBlob = { message };
// If the message is a RecordsWrite, data will be present in the form of a stream
if (isRecordsWrite(messageEntry) && messageEntry.data) {
const dataBytes = yield NodeStream.consumeToBytes({ readable: messageEntry.data });
dwnMessageWithBlob.data = new Blob([dataBytes], { type: messageEntry.message.descriptor.dataFormat });
}
return dwnMessageWithBlob;
});
}
}
//# sourceMappingURL=dwn-api.js.map