UNPKG

@web5/agent

Version:
336 lines 19.1 kB
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