From 32291950f7c049eaf8983cd4c850e686e684e0c2 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Mon, 30 Oct 2023 15:33:20 -0700 Subject: [PATCH 1/3] Expose DWN PermissionsGrant Message --- packages/agent/src/dwn-manager.ts | 13 +++++-- packages/agent/src/types/agent.ts | 5 ++- packages/api/src/dwn-api.ts | 59 +++++++++++++++++++++++++++++- packages/api/tests/dwn-api.spec.ts | 35 ++++++++++++++++++ 4 files changed, 107 insertions(+), 5 deletions(-) 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..358714b8e 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -1,5 +1,5 @@ import type { DwnResponse, Web5Agent } from '@web5/agent'; -import type { +import { UnionMessageReply, RecordsReadOptions, RecordsQueryOptions, @@ -11,6 +11,7 @@ import type { ProtocolsConfigureMessage, ProtocolsConfigureOptions, ProtocolsConfigureDescriptor, + Message, } from '@tbd54566975/dwn-sdk-js'; import { isEmptyObject } from '@web5/common'; @@ -19,6 +20,14 @@ import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; import { Record } from './record.js'; import { Protocol } from './protocol.js'; import { dataToBlob } from './utils.js'; +import { PermissionsGrant } from '@tbd54566975/dwn-sdk-js'; +import { PermissionsGrantMessage } from '@tbd54566975/dwn-sdk-js'; +import { PermissionsGrantOptions } from '@tbd54566975/dwn-sdk-js'; + +export type PermissionsGrantRequest = { + target?: string; + message: Omit; +} export type ProtocolsConfigureRequest = { message: Omit; @@ -91,6 +100,10 @@ export type RecordsWriteRequest = { store?: boolean; } +export type PermissionGrantRequest = { + message?: Omit, 'authorizationSigner'>; +} + export type RecordsWriteResponse = { status: UnionMessageReply['status']; record?: Record @@ -377,4 +390,48 @@ export class DwnApi { }, }; } + + get permissions() { + return { + grant: async (request: PermissionsGrantRequest): Promise<{ permissionsGrant: PermissionsGrant, status: UnionMessageReply['status'] }> => { + 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; + if (200 <= status.code && status.code <= 299) { + permissionsGrant = await PermissionsGrant.parse(message as PermissionsGrantMessage); + } + + return { + permissionsGrant, + 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..9e22398fd 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,37 @@ 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 } = 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.Read, + } + }, + }); + + expect(status.code).to.eq(202); + expect(permissionsGrant).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', () => { + + }); + }); }); \ No newline at end of file From a7e015bd490ea890a95cb928584470b2360b1f9f Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Wed, 1 Nov 2023 15:00:31 -0700 Subject: [PATCH 2/3] Test dwn send PermissionsGrant --- packages/api/src/dwn-api.ts | 25 ++++++++++++++++++++++++- packages/api/tests/dwn-api.spec.ts | 26 +++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 358714b8e..71a202ce7 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -29,6 +29,12 @@ export type PermissionsGrantRequest = { message: Omit; } +export type PermissionsGrantResponse = { + permissionsGrant: PermissionsGrant | undefined; + permissionsGrantId: string | undefined; + status: UnionMessageReply['status'] +}; + export type ProtocolsConfigureRequest = { message: Omit; } @@ -101,6 +107,7 @@ export type RecordsWriteRequest = { } export type PermissionGrantRequest = { + target?: string; message?: Omit, 'authorizationSigner'>; } @@ -393,7 +400,14 @@ export class DwnApi { get permissions() { return { - grant: async (request: PermissionsGrantRequest): Promise<{ permissionsGrant: PermissionsGrant, status: UnionMessageReply['status'] }> => { + /** + * 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, @@ -412,16 +426,25 @@ export class DwnApi { 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, diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 9e22398fd..99cd89400 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -577,7 +577,7 @@ describe('DwnApi', () => { it('returns a PermissionsGrant that can be invoked', async () => { const { did: bobDid } = await testAgent.createIdentity({ testDwnUrls }); - const { status, permissionsGrant } = await dwn.permissions.grant({ + const { status, permissionsGrant, permissionsGrantId } = await dwn.permissions.grant({ message: { dateExpires : Temporal.Now.instant().toString({ smallestUnit: 'microseconds' }), grantedBy : aliceDid.did, @@ -585,13 +585,14 @@ describe('DwnApi', () => { grantedTo : bobDid.did, scope : { interface : DwnInterfaceName.Records, - method : DwnMethodName.Read, + 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); @@ -600,8 +601,27 @@ describe('DwnApi', () => { }); }); - describe('target: did', () => { + 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 From c1b83d77df4a7ff1bfbf2e7839098df41e704cce Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Thu, 2 Nov 2023 13:48:14 -0700 Subject: [PATCH 3/3] Consolidate imports --- packages/api/src/dwn-api.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 71a202ce7..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 { UnionMessageReply, + PermissionsGrant, + PermissionsGrantMessage, + PermissionsGrantOptions, RecordsReadOptions, RecordsQueryOptions, RecordsWriteMessage, @@ -20,9 +23,6 @@ import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; import { Record } from './record.js'; import { Protocol } from './protocol.js'; import { dataToBlob } from './utils.js'; -import { PermissionsGrant } from '@tbd54566975/dwn-sdk-js'; -import { PermissionsGrantMessage } from '@tbd54566975/dwn-sdk-js'; -import { PermissionsGrantOptions } from '@tbd54566975/dwn-sdk-js'; export type PermissionsGrantRequest = { target?: string; @@ -106,11 +106,6 @@ export type RecordsWriteRequest = { store?: boolean; } -export type PermissionGrantRequest = { - target?: string; - message?: Omit, 'authorizationSigner'>; -} - export type RecordsWriteResponse = { status: UnionMessageReply['status']; record?: Record