UNPKG

@dwn-protocol/id-sdk

Version:

SDK for accessing the features and capabilities

512 lines (426 loc) 16.1 kB
import { GenericMessage, MessagesGetReply, PrivateKeySigner, RecordsWriteMessage, RecordsWriteOptions, Signer, UnionMessageReply, } from '@dwn-protocol/id'; import { Convert } from '../common/index.js'; import { Jose } from '../crypto/index.js'; import { DidResolver } from '../dids/index.js'; import { Readable } from 'readable-stream'; import * as didUtils from '../dids/utils.js'; import { Cid, Dwn, Message, EventsGet, DataStream, RecordsRead, MessagesGet, RecordsWrite, RecordsQuery, DwnMethodName, RecordsDelete, ProtocolsQuery, DwnInterfaceName, ProtocolsConfigure, EventLogLevel, DataStoreLevel, MessageStoreLevel, } from '@dwn-protocol/id'; import type { DwnRpcRequest } from './rpc-client.js'; import type { DwnResponse,ProcessDwnRequest, SendDwnRequest, IDManagedAgent } from './types/agent.js'; import { isManagedKeyPair } from './utils.js'; import { blobToIsomorphicNodeReadable, webReadableToIsomorphicNodeReadable } from './utils.js'; export type GeneralJws = { payload: string signatures: SignatureEntry[] }; export type SignatureEntry = { protected: string signature: string }; export type RecordsWriteAuthorizationPayload = { recordId: string; contextId?: string; descriptorCid: string; attestationCid?: string; encryptionCid?: string; }; type DwnMessage = { message: any; data?: Blob; } const dwnMessageCreators = { [DwnInterfaceName.Events + DwnMethodName.Get] : EventsGet, [DwnInterfaceName.Messages + DwnMethodName.Get] : MessagesGet, [DwnInterfaceName.Records + DwnMethodName.Read] : RecordsRead, [DwnInterfaceName.Records + DwnMethodName.Query] : RecordsQuery, [DwnInterfaceName.Records + DwnMethodName.Write] : RecordsWrite, [DwnInterfaceName.Records + DwnMethodName.Delete] : RecordsDelete, [DwnInterfaceName.Protocols + DwnMethodName.Query] : ProtocolsQuery, [DwnInterfaceName.Protocols + DwnMethodName.Configure] : ProtocolsConfigure, }; export type DwnManagerOptions = { agent?: IDManagedAgent; dwn: Dwn; } export type DwnManagerCreateOptions = { agent?: IDManagedAgent; dataPath?: string; didResolver?: DidResolver; dwn?: Dwn; } export class DwnManager { /** * Holds the instance of a `IDManagedAgent` that represents the current * execution context for the `KeyManager`. This agent is utilized * to interact with other agent components. It's vital * to ensure this instance is set to correctly contextualize * operations within the broader agent framework. */ private _agent?: IDManagedAgent; private _dwn: Dwn; constructor(options: DwnManagerOptions) { this._agent = options.agent; this._dwn = options.dwn; } /** * Constructs a Signer for the connected did. * * @param author - The DID. * @returns A promise that resolves to the result. */ async getSigner(author: string): Promise<Signer> { const signingKeyId = await this.getAuthorSigningKeyId({ did: author }); const parsedDid = didUtils.parseDid({ didUrl: signingKeyId }); if (!parsedDid) throw new Error(`DidIonMethod: Unable to parse DID: ${signingKeyId}`); const normalizedDid = parsedDid.did.split(':', 3).join(':'); const normalizedSigningKeyId = `${normalizedDid}#${parsedDid.fragment}`; const signingKey = await this.agent.keyManager.getKey({ keyRef: normalizedSigningKeyId }); if (!isManagedKeyPair(signingKey)) { throw new Error(`DwnManager: Signing key not found for author: '${author}'`); } const { alg } = Jose.webCryptoToJose(signingKey.privateKey.algorithm); if (alg === undefined) { throw Error(`No algorithm provided to sign with key ID ${signingKeyId}`); } return { keyId : signingKeyId, algorithm : alg, sign : async (content: Uint8Array): Promise<Uint8Array> => { return await this.agent.keyManager.sign({ algorithm : signingKey.privateKey.algorithm, data : content, keyRef : normalizedSigningKeyId }); }, }; } /** * Constructs a Private Key Signer for a did. * * @param author - The DID Object. * @returns A promise that resolves to the result. */ async getPrivateKeySigner(author: any) { const signingKeyId = await this.agent.didManager.getDefaultSigningKey({ did: author.did }); const signingKeyPair = author.keySet.verificationMethodKeys[0]; // const signingKeyPair = ionDid.keySet.verificationMethodKeys.find(keyPair => keyPair.publicKeyJwk.kid === "#dwn-sig"); const signingPrivateKeyJwk = signingKeyPair.privateKeyJwk; return [new PrivateKeySigner({ privateJwk : signingPrivateKeyJwk, algorithm : signingPrivateKeyJwk.alg, keyId : signingKeyId, })]; } /** * Retrieves the `IDManagedAgent` execution context. * If the `agent` instance proprety is undefined, it will throw an error. * * @returns The `IDManagedAgent` instance that represents the current execution * context. * * @throws Will throw an error if the `agent` instance property is undefined. */ get agent(): IDManagedAgent { if (this._agent === undefined) { throw new Error('DidManager: Unable to determine agent execution context.'); } return this._agent; } set agent(agent: IDManagedAgent) { this._agent = agent; } get dwn(): Dwn { return this._dwn; } public static async create(options?: DwnManagerCreateOptions) { let { agent, dataPath, didResolver, dwn } = options ?? { }; dataPath ??= 'data/AGENT'; if (dwn === undefined) { const dataStore = new DataStoreLevel({ blockstoreLocation: `${dataPath}/DWN_DATASTORE` }); const eventLog = new EventLogLevel({ location: `${dataPath}/DWN_EVENTLOG` }); const messageStore = new MessageStoreLevel(({ blockstoreLocation : `${dataPath}/DWN_MESSAGESTORE`, indexLocation : `${dataPath}/DWN_MESSAGEINDEX` })); dwn = await Dwn.create({ dataStore, //@ts-ignore didResolver, eventLog, messageStore, }); } return new DwnManager({ agent, dwn }); } public async processRequest(request: ProcessDwnRequest): Promise<DwnResponse> { const { message, dataStream } = await this.constructDwnMessage({ request }); let reply: UnionMessageReply; if (request.store !== false) { reply = await this._dwn.processMessage(request.target, message, dataStream); } else { reply = { status: { code: 202, detail: 'Accepted' }}; } return { reply, message : message, messageCid : await Message.getCid(message) }; } public async sendRequest(request: SendDwnRequest): Promise<DwnResponse> { const dwnRpcRequest: Partial<DwnRpcRequest> = { targetDid: request.target }; let messageData: Blob | Readable | ReadableStream | undefined; if ('messageCid' in request) { const { message, data } = await this.getDwnMessage({ author : request.author, messageCid : request.messageCid, messageType : request.messageType }); dwnRpcRequest.message = message; messageData = data; } else { const { message } = await this.constructDwnMessage({ request }); dwnRpcRequest.message = message; messageData = request.dataStream; } if (messageData) { dwnRpcRequest.data = messageData; } const { didDocument, didResolutionMetadata } = await this.agent.didResolver.resolve(request.target); if (!didDocument) { const errorCode = `${didResolutionMetadata?.error}: ` ?? ''; const defaultMessage = `Unable to resolve target DID: ${request.target}`; const errorMessage = didResolutionMetadata?.errorMessage ?? defaultMessage; throw new Error(`DwnManager: ${errorCode}${errorMessage}`); } const [ service ] = didUtils.getServices({ didDocument, id: '#dwn' }); if (!service) { throw new Error(`DwnManager: DID Document of '${request.target}' has no service endpoints with ID '#dwn'`); } if (!didUtils.isDwnServiceEndpoint(service.serviceEndpoint)) { throw new Error(`DwnManager: Malformed '#dwn' service endpoint. Expected array of node addresses.`); } const dwnEndpointUrls = service.serviceEndpoint.nodes; let dwnReply; let errorMessages = []; // try sending to author's publicly addressable dwn's until first request succeeds. for (let dwnUrl of dwnEndpointUrls) { dwnRpcRequest.dwnUrl = dwnUrl; try { dwnReply = await this.agent.rpcClient.sendDwnRequest(dwnRpcRequest as DwnRpcRequest); break; } catch(error: unknown) { const message = (error instanceof Error) ? error.message : 'Unknown error'; errorMessages.push({ url: dwnUrl, message }); } } if (!dwnReply) { throw new Error(JSON.stringify(errorMessages)); } return { message : dwnRpcRequest.message, messageCid : await Message.getCid(dwnRpcRequest.message), reply : dwnReply, }; } private async constructDwnMessage(options: { request: ProcessDwnRequest }) { const { request } = options; let readableStream: Readable | undefined; if (request.messageType === 'RecordsWrite') { const messageOptions = request.messageOptions as RecordsWriteOptions; if (request.dataStream && !messageOptions.data) { const { dataStream } = request; let isomorphicNodeReadable: Readable; 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); } // @ts-ignore messageOptions.dataCid = await Cid.computeDagPbCidFromStream(isomorphicNodeReadable); // @ts-ignore messageOptions.dataSize ??= isomorphicNodeReadable['bytesRead']; } } const dwnSigner = await this.constructDwnSigner(request.author); const messageCreator = dwnMessageCreators[request.messageType]; const dwnMessage = await messageCreator.create({ ...<any>request.messageOptions, signer: dwnSigner }); // return { message: dwnMessage.toJSON(), dataStream: readableStream }; return { message: dwnMessage.message, dataStream: readableStream }; } private async getAuthorSigningKeyId(options: { did: string }): Promise<string> { const { did } = options; // Get the method-specific default signing key. const signingKeyId = await this.agent.didManager.getDefaultSigningKey({ did }); if (!signingKeyId) { throw new Error (`DwnManager: Unable to determine signing key for author: '${did}'`); } return signingKeyId; } private async constructDwnSigner(author: string): Promise<Signer> { const signingKeyId = await this.getAuthorSigningKeyId({ did: author }); /** * DID keys stored in KeyManager use the canonicalId as an alias, so * normalize the signing key ID before attempting to retrieve the key. */ const parsedDid = didUtils.parseDid({ didUrl: signingKeyId }); if (!parsedDid) throw new Error(`DidIonMethod: Unable to parse DID: ${signingKeyId}`); const normalizedDid = parsedDid.did.split(':', 3).join(':'); const normalizedSigningKeyId = `${normalizedDid}#${parsedDid.fragment}`; const signingKey = await this.agent.keyManager.getKey({ keyRef: normalizedSigningKeyId }); if (!isManagedKeyPair(signingKey)) { throw new Error(`DwnManager: Signing key not found for author: '${author}'`); } const { alg } = Jose.webCryptoToJose(signingKey.privateKey.algorithm); if (alg === undefined) { throw Error(`No algorithm provided to sign with key ID ${signingKeyId}`); } return { keyId : signingKeyId, algorithm : alg, sign : async (content: Uint8Array): Promise<Uint8Array> => { return await this.agent.keyManager.sign({ algorithm : signingKey.privateKey.algorithm, data : content, keyRef : normalizedSigningKeyId }); } }; } private async getDwnMessage(options: { author: string, messageType: string, messageCid: string }): Promise<DwnMessage> { const { author, messageType, messageCid } = options; const dwnSigner = await this.constructDwnSigner(author); const messagesGet = await MessagesGet.create({ messageCids : [messageCid], signer : dwnSigner }); const result: MessagesGetReply = await this._dwn.processMessage(author, messagesGet.message); if (!(result.messages && result.messages.length === 1)) { throw new Error('TODO: message not found'); } const [ messageEntry ] = result.messages; let { message } = messageEntry; if (!message) { throw new Error('TODO: message not found'); } let dwnMessage: DwnMessage = { message }; /** If the message is a RecordsWrite, either data will be present, OR * we have to fetch it using a RecordsRead. */ if (messageType === 'RecordsWrite') { const { encodedData } = messageEntry; const writeMessage = message as RecordsWriteMessage; if (encodedData) { const dataBytes = Convert.base64Url(encodedData).toUint8Array(); dwnMessage.data = new Blob([dataBytes]); } else { const recordsRead = await RecordsRead.create({ filter: { recordId: writeMessage.recordId }, signer: dwnSigner }); const reply = await this._dwn.processMessage(author, recordsRead.message); if (reply.status.code >= 400) { const { status: { code, detail } } = reply; throw new Error(`(${code}) Failed to read data associated with record ${writeMessage.recordId}. ${detail}}`); } else if (reply.record) { const dataBytes = await DataStream.toBytes(reply.record.data); dwnMessage.data = new Blob([dataBytes]); } } } return dwnMessage; } /** * ADDED TO GET SYNC WORKING * - createMessage() * - processMessage() * - writePrunedRecord() */ public async createMessage(options: { author: string, messageOptions: unknown, messageType: string }): Promise<EventsGet | MessagesGet | RecordsRead | RecordsQuery | RecordsWrite | RecordsDelete | ProtocolsQuery | ProtocolsConfigure> { const { author, messageOptions, messageType } = options; const dwnSigner = await this.constructDwnSigner(author); const messageCreator = dwnMessageCreators[messageType]; const dwnMessage = await messageCreator.create({ ...<any>messageOptions, signer: dwnSigner }); return dwnMessage; } /** * Writes a pruned initial `RecordsWrite` to a DWN without needing to supply associated data. * Note: This method should ONLY be used by a {@link SyncManager} implementation. * * @param options.targetDid - DID of the DWN tenant to write the pruned RecordsWrite to. * @returns DWN reply containing the status of processing request. */ public async writePrunedRecord(options: { targetDid: string, message: RecordsWriteMessage }): Promise<GenericMessageReply> { const { targetDid, message } = options; return await this._dwn.synchronizePrunedInitialRecordsWrite(targetDid, message); } public async processMessage(options: { targetDid: string, message: GenericMessage, dataStream?: Readable }): Promise<UnionMessageReply> { const { dataStream, message, targetDid } = options; return await this._dwn.processMessage(targetDid, message, dataStream); } } type GenericMessageReply = { status: Status; }; type Status = { code: number detail: string };