diff --git a/packages/agent/src/dwn-manager.ts b/packages/agent/src/dwn-manager.ts index d610cd84c..46eb11683 100644 --- a/packages/agent/src/dwn-manager.ts +++ b/packages/agent/src/dwn-manager.ts @@ -6,6 +6,7 @@ import { RecordsWriteMessage, RecordsWriteOptions, Signer, + PermissionsGrant, } from '@tbd54566975/dwn-sdk-js'; import { Jose } from '@web5/crypto'; @@ -69,6 +70,9 @@ const dwnMessageCreators = { [DwnInterfaceName.Records + DwnMethodName.Query] : RecordsQuery, [DwnInterfaceName.Records + DwnMethodName.Write] : RecordsWrite, [DwnInterfaceName.Records + DwnMethodName.Delete] : RecordsDelete, + [DwnInterfaceName.Permissions + DwnMethodName.Grant] : PermissionsGrant, + [DwnInterfaceName.Permissions + DwnMethodName.Request] : PermissionsGrant, + [DwnInterfaceName.Permissions + DwnMethodName.Revoke] : PermissionsGrant, [DwnInterfaceName.Protocols + DwnMethodName.Query] : ProtocolsQuery, [DwnInterfaceName.Protocols + DwnMethodName.Configure] : ProtocolsConfigure, }; @@ -184,8 +188,11 @@ export class DwnManager { }); dwnRpcRequest.message = message; messageData = data; - + } else if ('message' in request) { + dwnRpcRequest.message = request.message; + messageData = request.dataStream; } else { + // construct message from messageOptions const { message } = await this.constructDwnMessage({ request }); dwnRpcRequest.message = message; messageData = request.dataStream; @@ -224,7 +231,7 @@ export class DwnManager { dwnReply = await this.agent.rpcClient.sendDwnRequest(dwnRpcRequest as DwnRpcRequest); break; } catch(error: unknown) { - const message = (error instanceof Error) ? error.message : 'Uknown error'; + const message = (error instanceof Error) ? error.message : 'Unknown error'; errorMessages.push({ url: dwnUrl, message }); } } @@ -405,7 +412,7 @@ export class DwnManager { author: string, messageOptions: unknown, messageType: string - }): Promise { + }): Promise> { const { author, messageOptions, messageType } = options; const dwnAuthorizationSigner = await this.constructDwnAuthorizationSigner(author); diff --git a/packages/agent/src/types/agent.ts b/packages/agent/src/types/agent.ts index 5ce8f05ee..bbf0e605e 100644 --- a/packages/agent/src/types/agent.ts +++ b/packages/agent/src/types/agent.ts @@ -8,6 +8,7 @@ import type { RecordsDeleteMessage, ProtocolsQueryMessage, ProtocolsConfigureMessage, + GenericMessage, } from '@tbd54566975/dwn-sdk-js'; import { DidResolver } from '@web5/dids'; @@ -57,7 +58,9 @@ export type ProcessDwnRequest = DwnRequest & { store?: boolean; }; -export type SendDwnRequest = DwnRequest & (ProcessDwnRequest | { messageCid: string }) +export type SendDwnRequest = DwnRequest & ( + ProcessDwnRequest | { messageCid: string } | { message: GenericMessage, dataStream?: Blob | ReadableStream | Readable } +); /** * TODO: add JSDoc diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 11bccc120..9df7265bb 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -1,6 +1,9 @@ import type { DwnResponse, Web5Agent } from '@web5/agent'; -import type { +import { UnionMessageReply, + PermissionsGrant, + PermissionsGrantMessage, + PermissionsGrantOptions, RecordsReadOptions, RecordsQueryOptions, RecordsWriteMessage, @@ -11,6 +14,7 @@ import type { ProtocolsConfigureMessage, ProtocolsConfigureOptions, ProtocolsConfigureDescriptor, + Message, } from '@tbd54566975/dwn-sdk-js'; import { isEmptyObject } from '@web5/common'; @@ -20,6 +24,17 @@ import { Record } from './record.js'; import { Protocol } from './protocol.js'; import { dataToBlob } from './utils.js'; +export type PermissionsGrantRequest = { + target?: string; + message: Omit; +} + +export type PermissionsGrantResponse = { + permissionsGrant: PermissionsGrant | undefined; + permissionsGrantId: string | undefined; + status: UnionMessageReply['status'] +}; + export type ProtocolsConfigureRequest = { message: Omit; } @@ -377,4 +392,64 @@ export class DwnApi { }, }; } + + get permissions() { + return { + /** + * Create and store a PermissionsGrant DWN message + * @param request.target The DID whose DWN the PermissionsGrant message will be sent to. If undefined, + * the message will be stored in the local DWN of the connectedDid. + * @param request.message The message options used to create the PermissionsGrant messsage. + * @returns {PermissionsGrantResponse} + */ + grant: async (request: PermissionsGrantRequest): Promise => { + const agentRequest = { + author : this.connectedDid, + messageOptions : request.message, + messageType : DwnInterfaceName.Permissions + DwnMethodName.Grant, + target : request.target || this.connectedDid + }; + + let agentResponse: DwnResponse; + + if (request.target) { + agentResponse = await this.agent.sendDwnRequest(agentRequest); + } else { + agentResponse = await this.agent.processDwnRequest(agentRequest); + } + + const { message, reply: { status } } = agentResponse; + + let permissionsGrant: PermissionsGrant | undefined; + let permissionsGrantId: string | undefined; + if (200 <= status.code && status.code <= 299) { + permissionsGrant = await PermissionsGrant.parse(message as PermissionsGrantMessage); + permissionsGrantId = await Message.getCid(permissionsGrant.message); + } + + return { + permissionsGrant, + permissionsGrantId, + status, + }; + }, + + /** + * Send an existing PermissionsGrant message to a remote DWN. + * @param target DID whose remote DWN the Permissions message will be sent to. + * @param message The PermissionsGrant message that will be sent. + * @returns {UnionMessageReply['status']} + */ + send: async (target: string, message: PermissionsGrant): Promise<{ status: UnionMessageReply['status'] }> => { + const { reply: { status } } = await this.agent.sendDwnRequest({ + messageType : message.message.descriptor.interface + message.message.descriptor.method, + author : message.author, + target : target, + message : message.message, + }); + + return { status }; + }, + }; + } } \ No newline at end of file diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 2374a7899..99cd89400 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -1,5 +1,6 @@ import type { PortableDid } from '@web5/dids'; import type { ManagedIdentity } from '@web5/agent'; +import { Temporal } from '@js-temporal/polyfill'; import { expect } from 'chai'; import { TestManagedAgent } from '@web5/agent'; @@ -8,6 +9,7 @@ import { DwnApi } from '../src/dwn-api.js'; import { testDwnUrl } from './test-config.js'; import { TestUserAgent } from './utils/test-user-agent.js'; import emailProtocolDefinition from './fixtures/protocol-definitions/email.json' assert { type: 'json' }; +import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; let testDwnUrls: string[] = [testDwnUrl]; @@ -569,4 +571,57 @@ describe('DwnApi', () => { }); }); }); + + describe('permissions.grant()', () => { + describe('agent', () => { + it('returns a PermissionsGrant that can be invoked', async () => { + const { did: bobDid } = await testAgent.createIdentity({ testDwnUrls }); + + const { status, permissionsGrant, permissionsGrantId } = await dwn.permissions.grant({ + message: { + dateExpires : Temporal.Now.instant().toString({ smallestUnit: 'microseconds' }), + grantedBy : aliceDid.did, + grantedFor : aliceDid.did, + grantedTo : bobDid.did, + scope : { + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + } + }, + }); + + expect(status.code).to.eq(202); + expect(permissionsGrant).not.to.be.undefined; + expect(permissionsGrantId).not.to.be.undefined; + + // send to Alice's remote DWN + const { status: statusSend } = await dwn.permissions.send(aliceDid.did, permissionsGrant); + + expect(statusSend.code).to.eq(202); + }); + }); + + describe('target: did', async () => { + it('returns a PermissionsGrant that can be invoked', async () => { + const { did: bobDid } = await testAgent.createIdentity({ testDwnUrls }); + + const { status, permissionsGrant } = await dwn.permissions.grant({ + target : bobDid.did, + message : { + dateExpires : Temporal.Now.instant().toString({ smallestUnit: 'microseconds' }), + grantedBy : aliceDid.did, + grantedFor : aliceDid.did, + grantedTo : bobDid.did, + scope : { + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + } + }, + }); + + expect(status.code).to.eq(202); + expect(permissionsGrant).not.to.be.undefined; + }); + }); + }); }); \ No newline at end of file