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 { Convert, NodeStream } from '@web5/common'; import { utils as cryptoUtils } from '@web5/crypto'; import { DidDht, DidJwk, DidResolverCacheLevel, UniversalResolver } from '@web5/dids'; import { Cid, DataStoreLevel, Dwn, DwnMethodName, EventLogLevel, Message, MessageStoreLevel } from '@tbd54566975/dwn-sdk-js'; 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 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 }) { 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` }))); return yield Dwn.create({ dataStore, didResolver, eventLog, eventStream, messageStore, tenantGate }); }); } 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* () { const rawMessage = request.rawMessage; let readableStream; // TODO: Consider refactoring to move data transformations imposed by fetch() limitations to the HTTP transport-related methods. 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']); } } } // Determine the signer for the message. const signer = yield this.getSigner(request.author); const dwnMessageConstructor = dwnMessageConstructors[request.messageType]; const dwnMessage = rawMessage ? yield dwnMessageConstructor.parse(rawMessage) : yield dwnMessageConstructor.create(Object.assign(Object.assign({}, request.messageParams), { signer })); if (isRecordsWrite(dwnMessage) && request.signAsOwner) { yield dwnMessage.signAsOwner(signer); } return { message: dwnMessage.message, dataStream: readableStream }; }); } 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 MessagesGet message to fetch the message. const messagesGet = yield dwnMessageConstructors[DwnInterface.MessagesGet].create({ messageCids: [messageCid], signer }); const result = yield this._dwn.processMessage(author, messagesGet.message); if (!(result.entries && result.entries.length === 1)) { throw new Error('AgentDwnApi: Expected 1 message entry in the MessagesGet response but received none or more than one.'); } const [messageEntry] = result.entries; const message = messageEntry.message; if (!message) { throw new Error(`AgentDwnApi: Message not found with CID: ${messageCid}`); } let dwnMessageWithBlob = { message }; // isRecordsWrite(message) && (dwnMessage.data = await this.getDataForRecordsWrite({ author, message, messageEntry, messageType, signer })); // If the message is a RecordsWrite, either data will be present, // OR we have to fetch it using a RecordsRead. if (isRecordsWrite(messageEntry)) { if (messageEntry.encodedData) { const dataBytes = Convert.base64Url(messageEntry.encodedData).toUint8Array(); // TODO: test adding the messageEntry.message.descriptor.dataFormat to the Blob constructor. dwnMessageWithBlob.data = new Blob([dataBytes]); } else { const recordsRead = yield dwnMessageConstructors[DwnInterface.RecordsRead].create({ filter: { recordId: messageEntry.message.recordId }, signer }); const reply = yield this._dwn.processMessage(author, recordsRead.message); if (reply.status.code >= 400) { const { status: { code, detail } } = reply; throw new Error(`AgentDwnApi: (${code}) Failed to read data associated with record ${messageEntry.message.recordId}. ${detail}}`); } else if (reply.record) { const dataBytes = yield NodeStream.consumeToBytes({ readable: reply.record.data }); dwnMessageWithBlob.data = new Blob([dataBytes]); } } } return dwnMessageWithBlob; }); } /** * TODO: Refactor this to consolidate logic in AgentDwnApi and SyncEngineLevel. * ADDED TO GET SYNC WORKING * - createMessage() * - processMessage() */ createMessage({ author, messageParams, messageType }) { return __awaiter(this, void 0, void 0, function* () { // Determine the signer for the message. const signer = yield this.getSigner(author); const dwnMessageConstructor = dwnMessageConstructors[messageType]; const dwnMessage = yield dwnMessageConstructor.create(Object.assign(Object.assign({}, messageParams), { signer })); return dwnMessage; }); } processMessage({ dataStream, message, targetDid }) { return __awaiter(this, void 0, void 0, function* () { return yield this._dwn.processMessage(targetDid, message, { dataStream }); }); } } //# sourceMappingURL=dwn-api.js.map