From 75126a7a55a637a5180e5d22816de7fb26e0fcc6 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Sat, 7 Sep 2024 09:41:34 -0400 Subject: [PATCH 01/11] allow query/configure of protocols with grants, query without grant for public protocols --- packages/agent/src/connect.ts | 6 ++++ packages/api/src/dwn-api.ts | 52 +++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 884c15b96..d4bc4533f 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -223,6 +223,12 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco method : DwnMethodName.Subscribe, }); + // In order to allow the application the ability to query for installed protocols. + requests.push({ + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Query, + }); + // We also request any additional permissions the user has requested for this protocol for (const permission of permissions) { switch (permission) { diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index b7405958a..16342a073 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -7,7 +7,6 @@ import type { CreateGrantParams, CreateRequestParams, - DwnRecordsInterfaces, FetchPermissionRequestParams, FetchPermissionsParams } from '@web5/agent'; @@ -428,12 +427,38 @@ export class DwnApi { * Configure method, used to setup a new protocol (or update) with the passed definitions */ configure: async (request: ProtocolsConfigureRequest): Promise => { - const agentResponse = await this.agent.processDwnRequest({ + + const agentRequest:ProcessDwnRequest = { author : this.connectedDid, messageParams : request.message, messageType : DwnInterface.ProtocolsConfigure, target : this.connectedDid - }); + }; + + if (this.delegateDid) { + // NOTE: currently protocol configure only allows normal permission grants, not delegated grants. + // However, protocol grants should be used in a delegated scenario as they modify state. + // Additionally currently ProtocolConfigure does not scope to specific protocols, which it should. + // TODO: Add Delegate Grants to ProtocolConfigure https://github.com/TBD54566975/dwn-sdk-js/issues/801 + // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 + + const { grant: { id: permissionGrantId }} = await this.permissionsApi.getPermissionForRequest({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + delegate : true, + cached : true, + messageType : agentRequest.messageType + }); + + agentRequest.messageParams = { + ...agentRequest.messageParams, + permissionGrantId + }; + + agentRequest.granteeDid = this.delegateDid; + } + + const agentResponse = await this.agent.processDwnRequest(agentRequest); const { message, messageCid, reply: { status }} = agentResponse; const response: ProtocolsConfigureResponse = { status }; @@ -457,6 +482,27 @@ export class DwnApi { target : request.from || this.connectedDid }; + if (this.delegateDid) { + try { + const { grant: { id: permissionGrantId } } = await this.permissionsApi.getPermissionForRequest({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + delegate : true, + cached : true, + messageType : agentRequest.messageType + }); + + agentRequest.messageParams = { + ...agentRequest.messageParams, + permissionGrantId + }; + agentRequest.granteeDid = this.delegateDid; + } catch(error) { + // if a grant is not found, we should sign the request as the delegated DID to get public protocols + agentRequest.author = this.delegateDid; + } + } + let agentResponse: DwnResponse; if (request.from) { From 3c0f26d75e9cbbecb9effacc7761a08c3f96378b Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Sat, 7 Sep 2024 09:46:39 -0400 Subject: [PATCH 02/11] add comment/TODO --- packages/agent/src/connect.ts | 1 + packages/api/src/dwn-api.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index d4bc4533f..b269837d2 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -224,6 +224,7 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco }); // In order to allow the application the ability to query for installed protocols. + // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 requests.push({ interface : DwnInterfaceName.Protocols, method : DwnMethodName.Query, diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 16342a073..82e6d5120 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -483,7 +483,13 @@ export class DwnApi { }; if (this.delegateDid) { + // We attempt to get a grant within a try catch, if there is no grant we will still sign the query with the delegate DID's key + // If the protocol is public, the query should be successful. This allows the app to query for public protocols without having a grant. + try { + // NOTE: Currently protocol permissions are not scoped to specific protocols. + // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 + const { grant: { id: permissionGrantId } } = await this.permissionsApi.getPermissionForRequest({ connectedDid : this.connectedDid, delegateDid : this.delegateDid, From bc51b4e8d00a42ef9ac10c7ed98c28fe207ce36e Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Sat, 7 Sep 2024 09:51:54 -0400 Subject: [PATCH 03/11] records query/read/subscribe should attempt with delegate DID if no permission is found --- packages/api/src/dwn-api.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 82e6d5120..df3fec6a0 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -668,8 +668,8 @@ export class DwnApi { delegatedGrant }; agentRequest.granteeDid = this.delegateDid; - } catch(error:any) { - // set the author of the request to the delegate did + } catch(error) { + // if a grant is not found, we should sign the request as the delegated DID to get public records agentRequest.author = this.delegateDid; } } @@ -759,8 +759,8 @@ export class DwnApi { delegatedGrant }; agentRequest.granteeDid = this.delegateDid; - } catch(_error:any) { - // set the author of the request to the delegate did + } catch(error) { + // if a grant is not found, we should sign the request as the delegated DID to get public records agentRequest.author = this.delegateDid; } } @@ -862,8 +862,8 @@ export class DwnApi { delegatedGrant }; agentRequest.granteeDid = this.delegateDid; - } catch(_error:any) { - // set the author of the request to the delegate did + } catch(error) { + // if a grant is not found, we should sign the request as the delegated DID to get public records agentRequest.author = this.delegateDid; } }; From 29bcc2dd545e8547fe2a1331198745405c78b7a7 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Sat, 7 Sep 2024 10:08:43 -0400 Subject: [PATCH 04/11] remove request for protocols query, as it would create duplicates --- packages/agent/src/connect.ts | 6 +----- packages/api/src/web5.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index b269837d2..6eb7997b0 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -223,12 +223,8 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco method : DwnMethodName.Subscribe, }); - // In order to allow the application the ability to query for installed protocols. // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 - requests.push({ - interface : DwnInterfaceName.Protocols, - method : DwnMethodName.Query, - }); + // When this is done we should add a request to query the specific protocol. // We also request any additional permissions the user has requested for this protocol for (const permission of permissions) { diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index de33ad20a..bf07f732d 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -17,7 +17,7 @@ import type { } from '@web5/agent'; import { Web5UserAgent } from '@web5/user-agent'; -import { DwnRegistrar, WalletConnect } from '@web5/agent'; +import { DwnInterface, DwnRegistrar, WalletConnect } from '@web5/agent'; import { DidApi } from './did-api.js'; import { DwnApi } from './dwn-api.js'; From c2dfceed0b54d85b152ecc820346470d574e9e4f Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Sun, 8 Sep 2024 17:12:43 -0400 Subject: [PATCH 05/11] temporary change in protocols configure/query behavior --- packages/api/src/dwn-api.ts | 22 ++++------------------ packages/api/tests/web5.spec.ts | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index df3fec6a0..931dceeee 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -437,25 +437,9 @@ export class DwnApi { if (this.delegateDid) { // NOTE: currently protocol configure only allows normal permission grants, not delegated grants. - // However, protocol grants should be used in a delegated scenario as they modify state. - // Additionally currently ProtocolConfigure does not scope to specific protocols, which it should. // TODO: Add Delegate Grants to ProtocolConfigure https://github.com/TBD54566975/dwn-sdk-js/issues/801 // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 - - const { grant: { id: permissionGrantId }} = await this.permissionsApi.getPermissionForRequest({ - connectedDid : this.connectedDid, - delegateDid : this.delegateDid, - delegate : true, - cached : true, - messageType : agentRequest.messageType - }); - - agentRequest.messageParams = { - ...agentRequest.messageParams, - permissionGrantId - }; - - agentRequest.granteeDid = this.delegateDid; + throw new Error('Delegated grants are not yet supported for protocol configuration: https://github.com/TBD54566975/dwn-sdk-js/issues/801'); } const agentResponse = await this.agent.processDwnRequest(agentRequest); @@ -487,12 +471,14 @@ export class DwnApi { // If the protocol is public, the query should be successful. This allows the app to query for public protocols without having a grant. try { + const protocolFromRequest = request.message.filter?.protocol; + // NOTE: Currently protocol permissions are not scoped to specific protocols. // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 - const { grant: { id: permissionGrantId } } = await this.permissionsApi.getPermissionForRequest({ connectedDid : this.connectedDid, delegateDid : this.delegateDid, + protocol : protocolFromRequest, delegate : true, cached : true, messageType : agentRequest.messageType diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index be0016963..c3a004eac 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -553,6 +553,16 @@ describe('web5 api', () => { delegateDid, }); + // attempt to query using the grant + let queryResult = await web5.dwn.records.query({ + protocol : protocol.protocol, + message : { + filter: { protocol: protocol.protocol } + } + }); + expect(queryResult.status.code).to.equal(200); + expect(queryResult.records).to.have.lengthOf(1); + // attempt to delete using the grant const deleteResult = await web5.dwn.records.delete({ protocol : protocol.protocol, @@ -562,15 +572,15 @@ describe('web5 api', () => { }); expect(deleteResult.status.code).to.equal(202); - // attempt to query using the grant - const queryResult = await web5.dwn.records.query({ + // query again after the deletion + queryResult = await web5.dwn.records.query({ protocol : protocol.protocol, message : { filter: { protocol: protocol.protocol } } }); expect(queryResult.status.code).to.equal(200); - expect(queryResult.records).to.have.lengthOf(0); // record has been deleted + expect(queryResult.records).to.have.lengthOf(0); // deleted // connecting a 2nd time will return the same connectedDID and delegatedDID const { did: did2, delegateDid: delegateDid2 } = await Web5.connect(); From 544c593e73b7d8476779de96cf4866b9a547dbf3 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 11 Sep 2024 16:36:03 -0400 Subject: [PATCH 06/11] add ability to query for protocols with grants --- packages/agent/package.json | 2 +- packages/agent/src/connect.ts | 22 +++++++++++---- packages/agent/src/oidc.ts | 16 +++++++++-- packages/agent/src/utils.ts | 2 +- packages/agent/tests/connect.spec.ts | 15 ++++++---- packages/api/src/dwn-api.ts | 38 +++++++++++++++----------- packages/api/tests/web5.spec.ts | 16 ++--------- pnpm-lock.yaml | 41 ++++++++++++++++++++++++++-- 8 files changed, 105 insertions(+), 47 deletions(-) diff --git a/packages/agent/package.json b/packages/agent/package.json index 4bfec2ed7..4ef00ec5a 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -71,7 +71,7 @@ "dependencies": { "@noble/ciphers": "0.5.3", "@scure/bip39": "1.2.2", - "@tbd54566975/dwn-sdk-js": "0.4.6", + "@tbd54566975/dwn-sdk-js": "0.4.7", "@web5/common": "1.0.0", "@web5/crypto": "workspace:*", "@web5/dids": "workspace:*", diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 6eb7997b0..a01044318 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -188,7 +188,7 @@ export type ConnectPermissionRequest = { /** * Shorthand for the types of permissions that can be requested. */ -export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe'; +export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe' | 'configure'; /** * The options for creating a permission request for a given protocol. @@ -203,11 +203,20 @@ export type ProtocolPermissionOptions = { /** * Creates a set of Dwn Permission Scopes to request for a given protocol. - * If no permissions are provided, the default is to request all permissions (write, read, delete, query, subscribe). + * + * If no permissions are provided, the default is to request all relevant record permissions (write, read, delete, query, subscribe). + * 'configure' is not included by default, as this gives the application a lot of control over the protocol. */ function createPermissionRequestForProtocol({ definition, permissions }: ProtocolPermissionOptions): ConnectPermissionRequest { const requests: DwnPermissionScope[] = []; + // Add the ability to query for the specific protocol + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Query, + }); + // In order to enable sync, we must request permissions for `MessagesQuery`, `MessagesRead` and `MessagesSubscribe` requests.push({ protocol : definition.protocol, @@ -223,9 +232,6 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco method : DwnMethodName.Subscribe, }); - // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 - // When this is done we should add a request to query the specific protocol. - // We also request any additional permissions the user has requested for this protocol for (const permission of permissions) { switch (permission) { @@ -264,6 +270,12 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco method : DwnMethodName.Subscribe, }); break; + case 'configure': + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Configure, + }); } } diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 3443aabe5..515d775f9 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -16,6 +16,7 @@ import { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnPermissionScope, Dw import { AgentPermissionsApi } from './permissions-api.js'; import type { Web5Agent } from './types/agent.js'; import { isRecordPermissionScope } from './dwn-api.js'; +import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; /** * Sent to an OIDC server to authorize a client. Allows clients @@ -600,6 +601,16 @@ function encryptAuthResponse({ return compactJwe; } +function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean { + if (isRecordPermissionScope(scope)) { + return true; + } else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) { + return true; + } + + return false; +} + /** * Creates the permission grants that assign to the selectedDid the level of * permissions that the web app requested in the {@link Web5ConnectAuthRequest} @@ -616,8 +627,8 @@ async function createPermissionGrants( const permissionGrants = await Promise.all( scopes.map((scope) => { - // check if the scope is a records permission scope, if so it is a delegated permission - const delegated = isRecordPermissionScope(scope); + // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission. + const delegated = shouldUseDelegatePermission(scope); return permissionsApi.createGrant({ delegated, store : true, @@ -626,7 +637,6 @@ async function createPermissionGrants( dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional author : selectedDid, }); - }) ); diff --git a/packages/agent/src/utils.ts b/packages/agent/src/utils.ts index 936d95980..7bb6a16c3 100644 --- a/packages/agent/src/utils.ts +++ b/packages/agent/src/utils.ts @@ -39,7 +39,7 @@ export async function getDwnServiceEndpointUrls(didUri: string, dereferencer: Di } export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { - return Records.getAuthor(record); + return Message.getAuthor(record) } export function isRecordsWrite(obj: unknown): obj is RecordsWrite { diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 286f7234d..f6a1b87cb 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -827,10 +827,11 @@ describe('web5 connect', function () { }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); - expect(permissionRequests.permissionScopes.length).to.equal(3); // only includes the sync permissions + expect(permissionRequests.permissionScopes.length).to.equal(4); // only includes the sync permissions + protocol query permission expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined; expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined; expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; }); it('should add requested permissions to the request', async () => { @@ -854,13 +855,13 @@ describe('web5 connect', function () { expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); - // the 3 sync permissions plus the 2 requested permissions - expect(permissionRequests.permissionScopes.length).to.equal(5); + // the 3 sync permissions plus the 2 requested permissions, and a protocol query permission + expect(permissionRequests.permissionScopes.length).to.equal(6); expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; }); - it('supports requesting `read`, `write`, `delete`, `query` and `subscribe` permissions', async () => { + it('supports requesting `read`, `write`, `delete`, `query`, `subscribe` and `configure` permissions', async () => { const protocol:DwnProtocolDefinition = { published : true, protocol : 'https://exmaple.org/protocols/social', @@ -876,18 +877,20 @@ describe('web5 connect', function () { }; const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe'] + definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure'] }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); // the 3 sync permissions plus the 5 requested permissions - expect(permissionRequests.permissionScopes.length).to.equal(8); + expect(permissionRequests.permissionScopes.length).to.equal(10); expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined; expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined; expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure)).to.not.be.undefined; }); }); }); diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 931dceeee..66081734a 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -436,10 +436,20 @@ export class DwnApi { }; if (this.delegateDid) { - // NOTE: currently protocol configure only allows normal permission grants, not delegated grants. - // TODO: Add Delegate Grants to ProtocolConfigure https://github.com/TBD54566975/dwn-sdk-js/issues/801 - // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 - throw new Error('Delegated grants are not yet supported for protocol configuration: https://github.com/TBD54566975/dwn-sdk-js/issues/801'); + const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + protocol : request.message.definition.protocol, + delegate : true, + cached : true, + messageType : agentRequest.messageType + }); + + agentRequest.messageParams = { + ...agentRequest.messageParams, + delegatedGrant + }; + agentRequest.granteeDid = this.delegateDid; } const agentResponse = await this.agent.processDwnRequest(agentRequest); @@ -472,14 +482,10 @@ export class DwnApi { try { const protocolFromRequest = request.message.filter?.protocol; - - // NOTE: Currently protocol permissions are not scoped to specific protocols. - // TODO: Scope Protocol Permissions to a specific protocol. https://github.com/TBD54566975/dwn-sdk-js/issues/802 const { grant: { id: permissionGrantId } } = await this.permissionsApi.getPermissionForRequest({ connectedDid : this.connectedDid, delegateDid : this.delegateDid, protocol : protocolFromRequest, - delegate : true, cached : true, messageType : agentRequest.messageType }); @@ -489,8 +495,8 @@ export class DwnApi { permissionGrantId }; agentRequest.granteeDid = this.delegateDid; - } catch(error) { - // if a grant is not found, we should sign the request as the delegated DID to get public protocols + } catch(_error:any) { + // if a grant is not found, we should author the request as the delegated DID to get public protocols agentRequest.author = this.delegateDid; } } @@ -654,8 +660,8 @@ export class DwnApi { delegatedGrant }; agentRequest.granteeDid = this.delegateDid; - } catch(error) { - // if a grant is not found, we should sign the request as the delegated DID to get public records + } catch(_error:any) { + // if a grant is not found, we should author the request as the delegated DID to get public records agentRequest.author = this.delegateDid; } } @@ -745,8 +751,8 @@ export class DwnApi { delegatedGrant }; agentRequest.granteeDid = this.delegateDid; - } catch(error) { - // if a grant is not found, we should sign the request as the delegated DID to get public records + } catch(_error:any) { + // if a grant is not found, we should author the request as the delegated DID to get public records agentRequest.author = this.delegateDid; } } @@ -848,8 +854,8 @@ export class DwnApi { delegatedGrant }; agentRequest.granteeDid = this.delegateDid; - } catch(error) { - // if a grant is not found, we should sign the request as the delegated DID to get public records + } catch(_error:any) { + // if a grant is not found, we should author the request as the delegated DID to get public records agentRequest.author = this.delegateDid; } }; diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index c3a004eac..be0016963 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -553,16 +553,6 @@ describe('web5 api', () => { delegateDid, }); - // attempt to query using the grant - let queryResult = await web5.dwn.records.query({ - protocol : protocol.protocol, - message : { - filter: { protocol: protocol.protocol } - } - }); - expect(queryResult.status.code).to.equal(200); - expect(queryResult.records).to.have.lengthOf(1); - // attempt to delete using the grant const deleteResult = await web5.dwn.records.delete({ protocol : protocol.protocol, @@ -572,15 +562,15 @@ describe('web5 api', () => { }); expect(deleteResult.status.code).to.equal(202); - // query again after the deletion - queryResult = await web5.dwn.records.query({ + // attempt to query using the grant + const queryResult = await web5.dwn.records.query({ protocol : protocol.protocol, message : { filter: { protocol: protocol.protocol } } }); expect(queryResult.status.code).to.equal(200); - expect(queryResult.records).to.have.lengthOf(0); // deleted + expect(queryResult.records).to.have.lengthOf(0); // record has been deleted // connecting a 2nd time will return the same connectedDID and delegatedDID const { did: did2, delegateDid: delegateDid2 } = await Web5.connect(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 860e902e7..85dd87816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,8 +64,8 @@ importers: specifier: 1.2.2 version: 1.2.2 '@tbd54566975/dwn-sdk-js': - specifier: 0.4.6 - version: 0.4.6 + specifier: 0.4.7 + version: 0.4.7 '@web5/common': specifier: 1.0.0 version: 1.0.0 @@ -2112,6 +2112,10 @@ packages: resolution: {integrity: sha512-eTd9v2ioT+hYrmob28OgxyLgOPAqJosb8rIAHDpFzEjYlQZSxCEohIZysMrLgWIcSLljyViSFr06mDelRPgGPg==} engines: {node: '>= 18'} + '@tbd54566975/dwn-sdk-js@0.4.7': + resolution: {integrity: sha512-VYaLT4FKdHfVvUPZbicUpF77erkOSi1xBP/EVQIpnp0khPujp2lYcojbRcw4c4JR23CrRvLPy/iWXmEhdP8LqA==} + engines: {node: '>= 18'} + '@tbd54566975/dwn-sql-store@0.6.6': resolution: {integrity: sha512-LY8it9npYjI/Kx/aK94gR6/1AfptmRGagUuXOfprm/lUcK3uJ79EReOq8zk7CXyTK66+GAu+oGFzuCoo12EJ1g==} engines: {node: '>=18'} @@ -7402,6 +7406,39 @@ snapshots: - encoding - supports-color + '@tbd54566975/dwn-sdk-js@0.4.7': + dependencies: + '@ipld/dag-cbor': 9.0.3 + '@js-temporal/polyfill': 0.4.4 + '@noble/ciphers': 0.5.3 + '@noble/curves': 1.4.2 + '@noble/ed25519': 2.0.0 + '@noble/secp256k1': 2.0.0 + '@web5/dids': 1.1.3 + abstract-level: 1.0.3 + ajv: 8.12.0 + blockstore-core: 4.2.0 + cross-fetch: 4.0.0 + eciesjs: 0.4.5 + interface-blockstore: 5.2.3 + interface-store: 5.1.2 + ipfs-unixfs-exporter: 13.1.5 + ipfs-unixfs-importer: 15.1.5 + level: 8.0.0 + lodash: 4.17.21 + lru-cache: 9.1.2 + ms: 2.1.3 + multiformats: 11.0.2 + randombytes: 2.1.0 + readable-stream: 4.5.2 + uint8arrays: 5.1.0 + ulidx: 2.1.0 + uuid: 8.3.2 + varint: 6.0.0 + transitivePeerDependencies: + - encoding + - supports-color + '@tbd54566975/dwn-sql-store@0.6.6': dependencies: '@ipld/dag-cbor': 9.0.5 From 7a7b75adb57988fe88bd76ff8119881888bf0bfa Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 11 Sep 2024 17:19:52 -0400 Subject: [PATCH 07/11] upgrade dwn-server --- package.json | 2 +- packages/agent/src/connect.ts | 1 + packages/api/package.json | 2 +- packages/dev-env/docker-compose.yaml | 2 +- pnpm-lock.yaml | 63 ++++++---------------------- 5 files changed, 17 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index d5a5292b5..765a28f44 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@changesets/cli": "^2.27.5", "@npmcli/package-json": "5.0.0", "@typescript-eslint/eslint-plugin": "7.9.0", - "@web5/dwn-server": "0.4.9", + "@web5/dwn-server": "0.4.10", "audit-ci": "^7.0.1", "eslint-plugin-mocha": "10.4.3", "globals": "^13.24.0", diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index a01044318..68aff74e9 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -276,6 +276,7 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco interface : DwnInterfaceName.Protocols, method : DwnMethodName.Configure, }); + break; } } diff --git a/packages/api/package.json b/packages/api/package.json index 22e311d31..a5da39837 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -85,7 +85,7 @@ }, "devDependencies": { "@playwright/test": "1.45.3", - "@tbd54566975/dwn-sdk-js": "0.4.6", + "@tbd54566975/dwn-sdk-js": "0.4.7", "@types/chai": "4.3.6", "@types/eslint": "8.56.10", "@types/mocha": "10.0.1", diff --git a/packages/dev-env/docker-compose.yaml b/packages/dev-env/docker-compose.yaml index 116d52148..f4beb63af 100644 --- a/packages/dev-env/docker-compose.yaml +++ b/packages/dev-env/docker-compose.yaml @@ -3,6 +3,6 @@ version: "3.98" services: dwn-server: container_name: dwn-server - image: ghcr.io/tbd54566975/dwn-server:0.4.9 + image: ghcr.io/tbd54566975/dwn-server:0.4.10 ports: - "3000:3000" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85dd87816..d2af32d77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,8 +40,8 @@ importers: specifier: 7.9.0 version: 7.9.0(@typescript-eslint/parser@7.14.1(eslint@9.7.0)(typescript@5.5.4))(eslint@9.7.0)(typescript@5.5.4) '@web5/dwn-server': - specifier: 0.4.9 - version: 0.4.9 + specifier: 0.4.10 + version: 0.4.10 audit-ci: specifier: ^7.0.1 version: 7.1.0 @@ -192,8 +192,8 @@ importers: specifier: 1.45.3 version: 1.45.3 '@tbd54566975/dwn-sdk-js': - specifier: 0.4.6 - version: 0.4.6 + specifier: 0.4.7 + version: 0.4.7 '@types/chai': specifier: 4.3.6 version: 4.3.6 @@ -2108,16 +2108,12 @@ packages: '@sphereon/ssi-types@0.26.0': resolution: {integrity: sha512-r4JQIN7rnPunEv0HvCFC1ZCc9qlWcegYvhJbMJqSvyFE6VhmT5NNdH9jNV9QetgMa0yo5r3k+TnHNv3nH58Dmg==} - '@tbd54566975/dwn-sdk-js@0.4.6': - resolution: {integrity: sha512-eTd9v2ioT+hYrmob28OgxyLgOPAqJosb8rIAHDpFzEjYlQZSxCEohIZysMrLgWIcSLljyViSFr06mDelRPgGPg==} - engines: {node: '>= 18'} - '@tbd54566975/dwn-sdk-js@0.4.7': resolution: {integrity: sha512-VYaLT4FKdHfVvUPZbicUpF77erkOSi1xBP/EVQIpnp0khPujp2lYcojbRcw4c4JR23CrRvLPy/iWXmEhdP8LqA==} engines: {node: '>= 18'} - '@tbd54566975/dwn-sql-store@0.6.6': - resolution: {integrity: sha512-LY8it9npYjI/Kx/aK94gR6/1AfptmRGagUuXOfprm/lUcK3uJ79EReOq8zk7CXyTK66+GAu+oGFzuCoo12EJ1g==} + '@tbd54566975/dwn-sql-store@0.6.7': + resolution: {integrity: sha512-5v/BudrItBx8UUMEIH42nMBwykpM9ZyBpMERmWwJn06Xe47wv+ojkDhVX000Npuv4q+bsLv0lQhCaIAmKcMlaQ==} engines: {node: '>=18'} '@tootallnate/quickjs-emscripten@0.23.0': @@ -2515,8 +2511,8 @@ packages: resolution: {integrity: sha512-M9EfsEYcOtYuEvUQjow4vpxXbD0Sz5H8EuDXMtwuvP4UdYL0ATl+60F8+8HDmwPFeUy6M2wxuoixrLDwSRFwZA==} engines: {node: '>=18.0.0'} - '@web5/dwn-server@0.4.9': - resolution: {integrity: sha512-LCBu7gcmfWcT8i571LPK5bHsBqtF2b0gC1VjAqZTo7ESCjGPrL6byvntiGiYWfSfzl9zgxpb/dIdVu/Ia8xvFA==} + '@web5/dwn-server@0.4.10': + resolution: {integrity: sha512-gdXIDC4OkCS58+EG85SN82IeWynl3uqkpeoq79A6X9NCGWO9+5XM5pNKCjkPxxNdsGfz0sX+nYLkSqrRX5BcFA==} hasBin: true '@webassemblyjs/ast@1.12.1': @@ -7373,39 +7369,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@tbd54566975/dwn-sdk-js@0.4.6': - dependencies: - '@ipld/dag-cbor': 9.0.3 - '@js-temporal/polyfill': 0.4.4 - '@noble/ciphers': 0.5.3 - '@noble/curves': 1.4.2 - '@noble/ed25519': 2.0.0 - '@noble/secp256k1': 2.0.0 - '@web5/dids': 1.1.3 - abstract-level: 1.0.3 - ajv: 8.12.0 - blockstore-core: 4.2.0 - cross-fetch: 4.0.0 - eciesjs: 0.4.5 - interface-blockstore: 5.2.3 - interface-store: 5.1.2 - ipfs-unixfs-exporter: 13.1.5 - ipfs-unixfs-importer: 15.1.5 - level: 8.0.0 - lodash: 4.17.21 - lru-cache: 9.1.2 - ms: 2.1.3 - multiformats: 11.0.2 - randombytes: 2.1.0 - readable-stream: 4.5.2 - uint8arrays: 5.1.0 - ulidx: 2.1.0 - uuid: 8.3.2 - varint: 6.0.0 - transitivePeerDependencies: - - encoding - - supports-color - '@tbd54566975/dwn-sdk-js@0.4.7': dependencies: '@ipld/dag-cbor': 9.0.3 @@ -7439,10 +7402,10 @@ snapshots: - encoding - supports-color - '@tbd54566975/dwn-sql-store@0.6.6': + '@tbd54566975/dwn-sql-store@0.6.7': dependencies: '@ipld/dag-cbor': 9.0.5 - '@tbd54566975/dwn-sdk-js': 0.4.6 + '@tbd54566975/dwn-sdk-js': 0.4.7 kysely: 0.26.3 multiformats: 12.0.1 readable-stream: 4.4.2 @@ -8392,10 +8355,10 @@ snapshots: level: 8.0.1 ms: 2.1.3 - '@web5/dwn-server@0.4.9': + '@web5/dwn-server@0.4.10': dependencies: - '@tbd54566975/dwn-sdk-js': 0.4.6 - '@tbd54566975/dwn-sql-store': 0.6.6 + '@tbd54566975/dwn-sdk-js': 0.4.7 + '@tbd54566975/dwn-sql-store': 0.6.7 '@web5/crypto': 1.0.3 better-sqlite3: 8.7.0 body-parser: 1.20.3 From 6d292b8b700b28448a7a2b8d1bac590fa39c0d35 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 11 Sep 2024 17:23:13 -0400 Subject: [PATCH 08/11] lint fix --- packages/agent/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent/src/utils.ts b/packages/agent/src/utils.ts index 7bb6a16c3..f0d1824aa 100644 --- a/packages/agent/src/utils.ts +++ b/packages/agent/src/utils.ts @@ -39,7 +39,7 @@ export async function getDwnServiceEndpointUrls(didUri: string, dereferencer: Di } export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { - return Message.getAuthor(record) + return Message.getAuthor(record); } export function isRecordsWrite(obj: unknown): obj is RecordsWrite { From fe8d3cb234203db7672db8c6f0bd78ff57c30115 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 11 Sep 2024 19:11:14 -0400 Subject: [PATCH 09/11] test protocol query/configure permissions, scopes to protocol for protocol interface messages --- packages/agent/src/permissions-api.ts | 18 +- packages/api/tests/dwn-api.spec.ts | 1020 ++++++++++++++----------- 2 files changed, 580 insertions(+), 458 deletions(-) diff --git a/packages/agent/src/permissions-api.ts b/packages/agent/src/permissions-api.ts index 86ae5e343..6abf2fd5a 100644 --- a/packages/agent/src/permissions-api.ts +++ b/packages/agent/src/permissions-api.ts @@ -365,7 +365,7 @@ export class AgentPermissionsApi implements PermissionsApi { if (scopeMessageType === messageType) { if (isRecordsType(messageType)) { const recordScope = scope as DwnRecordsPermissionScope; - if (!this.matchesProtocol(recordScope, protocol)) { + if (recordScope.protocol !== protocol) { return false; } @@ -386,11 +386,12 @@ export class AgentPermissionsApi implements PermissionsApi { } } else { const messagesScope = scope as DwnMessagesPermissionScope | DwnProtocolPermissionScope; - if (this.protocolScopeUnrestricted(messagesScope)) { + // Checks for unrestricted protocol scope, if no protocol is defined in the scope it is unrestricted + if (messagesScope.protocol === undefined) { return true; } - if (!this.matchesProtocol(messagesScope, protocol)) { + if (messagesScope.protocol !== protocol) { return false; } @@ -401,17 +402,6 @@ export class AgentPermissionsApi implements PermissionsApi { return false; } - private static matchesProtocol(scope: DwnPermissionScope & { protocol?: string }, protocol?: string): boolean { - return scope.protocol !== undefined && scope.protocol === protocol; - } - - /** - * Checks if the scope is restricted to a specific protocol - */ - private static protocolScopeUnrestricted(scope: DwnPermissionScope & { protocol?: string }): boolean { - return scope.protocol === undefined; - } - private static isUnrestrictedProtocolScope(scope: DwnPermissionScope & { contextId?: string, protocolPath?: string }): boolean { return scope.contextId === undefined && scope.protocolPath === undefined; } diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 16cdc0613..ca3081664 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -169,511 +169,643 @@ describe('DwnApi', () => { } as any })); }); - it('should create a record with a delegated grant', async () => { - const { status, record } = await delegateDwn.records.create({ - data : 'Hello, world!', - message : { - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } + describe('records', () => { + it('should create a record with a delegated grant', async () => { + const { status, record } = await delegateDwn.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + + expect(status.code).to.equal(202); + expect(record).to.not.be.undefined; + + // alice is the author, but the signer is the delegateDid + expect(record.author).to.equal(aliceDid.uri); + const signerDid = Jws.getSignerDid(record.rawMessage.authorization.signature.signatures[0]); + expect(signerDid).to.equal(delegateDid.uri); + expect(record.rawMessage.authorization.authorDelegatedGrant).to.not.be.undefined; }); - expect(status.code).to.equal(202); - expect(record).to.not.be.undefined; + it('should read records with a delegated grant', async () => { + const { status: writeStatus, record } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); - // alice is the author, but the signer is the delegateDid - expect(record.author).to.equal(aliceDid.uri); - const signerDid = Jws.getSignerDid(record.rawMessage.authorization.signature.signatures[0]); - expect(signerDid).to.equal(delegateDid.uri); - expect(record.rawMessage.authorization.authorDelegatedGrant).to.not.be.undefined; - }); + expect(writeStatus.code).to.equal(202); + expect(record).to.not.be.undefined; + const { status: sendStatus } = await record.send(); + expect(sendStatus.code).to.equal(202); - it('should read records with a delegated grant', async () => { - const { status: writeStatus, record } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } + const { status: readStatus, record: readRecord } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + recordId: record.id + } + } + }); + + expect(readStatus.code).to.equal(200); + expect(readRecord).to.exist; + expect(readRecord.id).to.equal; }); - expect(writeStatus.code).to.equal(202); - expect(record).to.not.be.undefined; - const { status: sendStatus } = await record.send(); - expect(sendStatus.code).to.equal(202); + it('should query records with a delegated grant', async () => { + const { status: writeStatus, record } = await delegateDwn.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + + expect(writeStatus.code).to.equal(202); + expect(record).to.not.be.undefined; - const { status: readStatus, record: readRecord } = await delegateDwn.records.read({ - from : aliceDid.uri, - protocol : notesProtocol.protocol, - message : { - filter: { - recordId: record.id + const { status: queryStatus, records } = await delegateDwn.records.query({ + protocol : notesProtocol.protocol, + message : { + filter: { + protocol : notesProtocol.protocol, + protocolPath : 'note' + } } - } - }); + }); - expect(readStatus.code).to.equal(200); - expect(readRecord).to.exist; - expect(readRecord.id).to.equal; - }); + expect(queryStatus.code).to.equal(200); + expect(records).to.exist; + expect(records).to.have.lengthOf(1); - it('should query records with a delegated grant', async () => { - const { status: writeStatus, record } = await delegateDwn.records.create({ - data : 'Hello, world!', - message : { - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } + // alice is the author, but the signer is the delegateDid + expect(records![0].author).to.equal(aliceDid.uri); + const signerDid = Jws.getSignerDid(records![0].rawMessage.authorization.signature.signatures[0]); + expect(signerDid).to.equal(delegateDid.uri); + expect(records![0].rawMessage.authorization.authorDelegatedGrant).to.not.be.undefined; + + // the record should be the same + expect(records![0].id).to.equal(record!.id); }); - expect(writeStatus.code).to.equal(202); - expect(record).to.not.be.undefined; + it('should subscribe to records with a delegated grant', async () => { + // subscribe to all messages from the protocol + const records: Map = new Map(); + const subscriptionHandler = async (record: Record) => { + records.set(record.id, record); + }; - const { status: queryStatus, records } = await delegateDwn.records.query({ - protocol : notesProtocol.protocol, - message : { - filter: { + const subscribeResult = await delegateDwn.records.subscribe({ + protocol : notesProtocol.protocol, + message : { + filter: { + protocol: notesProtocol.protocol + } + }, + subscriptionHandler + }); + expect(subscribeResult.status.code).to.equal(200); + + // write a record + const writeResult = await delegateDwn.records.write({ + data : 'Hello, world!', + message : { + recipient : bobDid.uri, protocol : notesProtocol.protocol, - protocolPath : 'note' + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', } - } - }); - - expect(queryStatus.code).to.equal(200); - expect(records).to.exist; - expect(records).to.have.lengthOf(1); + }); + expect(writeResult.status.code).to.equal(202); - // alice is the author, but the signer is the delegateDid - expect(records![0].author).to.equal(aliceDid.uri); - const signerDid = Jws.getSignerDid(records![0].rawMessage.authorization.signature.signatures[0]); - expect(signerDid).to.equal(delegateDid.uri); - expect(records![0].rawMessage.authorization.authorDelegatedGrant).to.not.be.undefined; + // wait for the record to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(records.size).to.equal(1); + const record = records.get(writeResult.record.id); + expect(record.toJSON()).to.deep.equal(writeResult.record.toJSON()); + expect(record.deleted).to.be.false; + }); - // the record should be the same - expect(records![0].id).to.equal(record!.id); - }); + // delete the record using the original writeResult instance of it + const deleteResult = await writeResult.record.delete(); + expect(deleteResult.status.code).to.equal(202); - it('should subscribe to records with a delegated grant', async () => { - // subscribe to all messages from the protocol - const records: Map = new Map(); - const subscriptionHandler = async (record: Record) => { - records.set(record.id, record); - }; + // wait for the record state to be reflected as deleted + await Poller.pollUntilSuccessOrTimeout(async () => { + const record = records.get(writeResult.record.id); + expect(record).to.exist; + expect(record.deleted).to.be.true; + }); - const subscribeResult = await delegateDwn.records.subscribe({ - protocol : notesProtocol.protocol, - message : { - filter: { - protocol: notesProtocol.protocol + // write another record and delete the previous one, the state should be updated + const writeResult2 = await delegateDwn.records.write({ + data : 'Hello, world!', + message : { + recipient : bobDid.uri, + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', } - }, - subscriptionHandler - }); - expect(subscribeResult.status.code).to.equal(200); - - // write a record - const writeResult = await delegateDwn.records.write({ - data : 'Hello, world!', - message : { - recipient : bobDid.uri, - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeResult.status.code).to.equal(202); + }); + expect(writeResult2.status.code).to.equal(202); - // wait for the record to be received - await Poller.pollUntilSuccessOrTimeout(async () => { - expect(records.size).to.equal(1); - const record = records.get(writeResult.record.id); - expect(record.toJSON()).to.deep.equal(writeResult.record.toJSON()); - expect(record.deleted).to.be.false; + // wait for the record to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(records.size).to.equal(2); + const record = records.get(writeResult2.record.id); + expect(record.toJSON()).to.deep.equal(writeResult2.record.toJSON()); + expect(record.deleted).to.be.false; + + //check the deleted record + const deletedRecord = records.get(writeResult.record.id); + expect(deletedRecord).to.exist; + expect(deletedRecord.deleted).to.be.true; + }); }); - // delete the record using the original writeResult instance of it - const deleteResult = await writeResult.record.delete(); - expect(deleteResult.status.code).to.equal(202); + it('should read records as the delegate DID if no grant is found', async () => { + // alice installs some other protocol + const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { + ...notesProtocol, + protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` + }} }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); + expect(aliceOtherProtocolSend.code).to.equal(202); - // wait for the record state to be reflected as deleted - await Poller.pollUntilSuccessOrTimeout(async () => { - const record = records.get(writeResult.record.id); - expect(record).to.exist; - expect(record.deleted).to.be.true; - }); + // alice writes a note record to the permissioned protocol + const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus1.code).to.equal(202); + expect(allowedRecord).to.not.be.undefined; + const { status: allowedRecordSendStatus } = await allowedRecord.send(); + expect(allowedRecordSendStatus.code).to.equal(202); - // write another record and delete the previous one, the state should be updated - const writeResult2 = await delegateDwn.records.write({ - data : 'Hello, world!', - message : { - recipient : bobDid.uri, - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeResult2.status.code).to.equal(202); + // alice writes a public and private note to the other protocol + const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + published : true, + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus2.code).to.equal(202); + expect(publicRecord).to.not.be.undefined; + const { status: publicRecordSendStatus } = await publicRecord.send(); + expect(publicRecordSendStatus.code).to.equal(202); - // wait for the record to be received - await Poller.pollUntilSuccessOrTimeout(async () => { - expect(records.size).to.equal(2); - const record = records.get(writeResult2.record.id); - expect(record.toJSON()).to.deep.equal(writeResult2.record.toJSON()); - expect(record.deleted).to.be.false; + const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus3.code).to.equal(202); + expect(privateRecord).to.not.be.undefined; + const { status: privateRecordSendStatus } = await privateRecord.send(); + expect(privateRecordSendStatus.code).to.equal(202); - //check the deleted record - const deletedRecord = records.get(writeResult.record.id); - expect(deletedRecord).to.exist; - expect(deletedRecord.deleted).to.be.true; - }); - }); - it('should read records as the delegate DID if no grant is found', async () => { - // alice installs some other protocol - const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { - ...notesProtocol, - protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` - }} }); - expect(aliceConfigStatus.code).to.equal(202); - const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); - expect(aliceOtherProtocolSend.code).to.equal(202); - - // alice writes a note record to the permissioned protocol - const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeStatus1.code).to.equal(202); - expect(allowedRecord).to.not.be.undefined; - const { status: allowedRecordSendStatus } = await allowedRecord.send(); - expect(allowedRecordSendStatus.code).to.equal(202); - - // alice writes a public and private note to the other protocol - const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - published : true, - protocol : aliceOtherProtocol.definition.protocol, - protocolPath : 'note', - schema : aliceOtherProtocol.definition.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeStatus2.code).to.equal(202); - expect(publicRecord).to.not.be.undefined; - const { status: publicRecordSendStatus } = await publicRecord.send(); - expect(publicRecordSendStatus.code).to.equal(202); - - const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - protocol : aliceOtherProtocol.definition.protocol, - protocolPath : 'note', - schema : aliceOtherProtocol.definition.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeStatus3.code).to.equal(202); - expect(privateRecord).to.not.be.undefined; - const { status: privateRecordSendStatus } = await privateRecord.send(); - expect(privateRecordSendStatus.code).to.equal(202); + // sanity: delegateDwn reads from the allowed record from alice's DWN + const { status: readStatus1, record: allowedRecordReturned } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + recordId: allowedRecord.id + } + } + }); + expect(readStatus1.code).to.equal(200); + expect(allowedRecordReturned).to.exist; + expect(allowedRecordReturned.id).to.equal(allowedRecord.id); + // delegateDwn reads from the other protocol, which no permissions exist + // only the public record is successfully returned + const { status: readStatus2, record: publicRecordReturned } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + recordId: publicRecord.id + } + } + }); + expect(readStatus2.code).to.equal(200); + expect(publicRecordReturned).to.exist; + expect(publicRecordReturned.id).to.equal(publicRecord.id); - // sanity: delegateDwn reads from the allowed record from alice's DWN - const { status: readStatus1, record: allowedRecordReturned } = await delegateDwn.records.read({ - from : aliceDid.uri, - protocol : notesProtocol.protocol, - message : { - filter: { - recordId: allowedRecord.id + // attempt to read the private record, which should fail + const { status: readStatus3, record: privateRecordReturned } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + recordId: privateRecord.id + } } - } - }); - expect(readStatus1.code).to.equal(200); - expect(allowedRecordReturned).to.exist; - expect(allowedRecordReturned.id).to.equal(allowedRecord.id); + }); + expect(readStatus3.code).to.equal(401); + expect(privateRecordReturned).to.be.undefined; - // delegateDwn reads from the other protocol, which no permissions exist - // only the public record is successfully returned - const { status: readStatus2, record: publicRecordReturned } = await delegateDwn.records.read({ - from : aliceDid.uri, - protocol : aliceOtherProtocol.definition.protocol, - message : { - filter: { - recordId: publicRecord.id + // sanity: query as alice to get both records + const { status: readStatus4, record: privateRecordReturnedAlice } = await dwnAlice.records.read({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + recordId: privateRecord.id + } } - } + }); + expect(readStatus4.code).to.equal(200); + expect(privateRecordReturnedAlice).to.exist; + expect(privateRecordReturnedAlice.id).to.equal(privateRecord.id); }); - expect(readStatus2.code).to.equal(200); - expect(publicRecordReturned).to.exist; - expect(publicRecordReturned.id).to.equal(publicRecord.id); - // attempt to read the private record, which should fail - const { status: readStatus3, record: privateRecordReturned } = await delegateDwn.records.read({ - from : aliceDid.uri, - protocol : aliceOtherProtocol.definition.protocol, - message : { - filter: { - recordId: privateRecord.id + it('should query records as the delegate DID if no grant is found', async () => { + // alice installs some other protocol + const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { + ...notesProtocol, + protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` + }} }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); + expect(aliceOtherProtocolSend.code).to.equal(202); + + // alice writes a note record to the permissioned protocol + const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', } - } - }); - expect(readStatus3.code).to.equal(401); - expect(privateRecordReturned).to.be.undefined; + }); + expect(writeStatus1.code).to.equal(202); + expect(allowedRecord).to.not.be.undefined; + const { status: allowedRecordSendStatus } = await allowedRecord.send(); + expect(allowedRecordSendStatus.code).to.equal(202); - // sanity: query as alice to get both records - const { status: readStatus4, record: privateRecordReturnedAlice } = await dwnAlice.records.read({ - from : aliceDid.uri, - protocol : aliceOtherProtocol.definition.protocol, - message : { - filter: { - recordId: privateRecord.id + // alice writes a public and private note to the other protocol + const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + published : true, + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', } - } - }); - expect(readStatus4.code).to.equal(200); - expect(privateRecordReturnedAlice).to.exist; - expect(privateRecordReturnedAlice.id).to.equal(privateRecord.id); - }); + }); + expect(writeStatus2.code).to.equal(202); + expect(publicRecord).to.not.be.undefined; + const { status: publicRecordSendStatus } = await publicRecord.send(); + expect(publicRecordSendStatus.code).to.equal(202); - it('should query records as the delegate DID if no grant is found', async () => { - // alice installs some other protocol - const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { - ...notesProtocol, - protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` - }} }); - expect(aliceConfigStatus.code).to.equal(202); - const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); - expect(aliceOtherProtocolSend.code).to.equal(202); - - // alice writes a note record to the permissioned protocol - const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeStatus1.code).to.equal(202); - expect(allowedRecord).to.not.be.undefined; - const { status: allowedRecordSendStatus } = await allowedRecord.send(); - expect(allowedRecordSendStatus.code).to.equal(202); - - // alice writes a public and private note to the other protocol - const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - published : true, - protocol : aliceOtherProtocol.definition.protocol, - protocolPath : 'note', - schema : aliceOtherProtocol.definition.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeStatus2.code).to.equal(202); - expect(publicRecord).to.not.be.undefined; - const { status: publicRecordSendStatus } = await publicRecord.send(); - expect(publicRecordSendStatus.code).to.equal(202); - - const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - protocol : aliceOtherProtocol.definition.protocol, - protocolPath : 'note', - schema : aliceOtherProtocol.definition.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeStatus3.code).to.equal(202); - expect(privateRecord).to.not.be.undefined; - const { status: privateRecordSendStatus } = await privateRecord.send(); - expect(privateRecordSendStatus.code).to.equal(202); + const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus3.code).to.equal(202); + expect(privateRecord).to.not.be.undefined; + const { status: privateRecordSendStatus } = await privateRecord.send(); + expect(privateRecordSendStatus.code).to.equal(202); - // sanity: delegateDwn queries for the allowed record from alice's DWN - const { status: queryStatus1, records: allowedRecords } = await delegateDwn.records.query({ - from : aliceDid.uri, - protocol : notesProtocol.protocol, - message : { - filter: { - protocol: notesProtocol.protocol + // sanity: delegateDwn queries for the allowed record from alice's DWN + const { status: queryStatus1, records: allowedRecords } = await delegateDwn.records.query({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol: notesProtocol.protocol + } } - } - }); - expect(queryStatus1.code).to.equal(200); - expect(allowedRecords).to.exist; - expect(allowedRecords).to.have.lengthOf(1); + }); + expect(queryStatus1.code).to.equal(200); + expect(allowedRecords).to.exist; + expect(allowedRecords).to.have.lengthOf(1); - // delegateDwn queries for the other protocol, which no permissions exist - // only the public record is returned - const { status: queryStatus2, records: publicRecords } = await delegateDwn.records.query({ - from : aliceDid.uri, - protocol : aliceOtherProtocol.definition.protocol, - message : { - filter: { - protocol: aliceOtherProtocol.definition.protocol + // delegateDwn queries for the other protocol, which no permissions exist + // only the public record is returned + const { status: queryStatus2, records: publicRecords } = await delegateDwn.records.query({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } } - } - }); - expect(queryStatus2.code).to.equal(200); - expect(publicRecords).to.exist; - expect(publicRecords).to.have.lengthOf(1); - expect(publicRecords![0].id).to.equal(publicRecord.id); + }); + expect(queryStatus2.code).to.equal(200); + expect(publicRecords).to.exist; + expect(publicRecords).to.have.lengthOf(1); + expect(publicRecords![0].id).to.equal(publicRecord.id); - // sanity: query as alice to get both records - const { status: queryStatus3, records: allRecords } = await dwnAlice.records.query({ - from : aliceDid.uri, - protocol : aliceOtherProtocol.definition.protocol, - message : { - filter: { - protocol: aliceOtherProtocol.definition.protocol + // sanity: query as alice to get both records + const { status: queryStatus3, records: allRecords } = await dwnAlice.records.query({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } } - } + }); + expect(queryStatus3.code).to.equal(200); + expect(allRecords).to.exist; + expect(allRecords).to.have.lengthOf(2); + expect(allRecords.map(r => r.id)).to.have.members([publicRecord.id, privateRecord.id]); }); - expect(queryStatus3.code).to.equal(200); - expect(allRecords).to.exist; - expect(allRecords).to.have.lengthOf(2); - expect(allRecords.map(r => r.id)).to.have.members([publicRecord.id, privateRecord.id]); - }); - it('should subscribe to records as the delegate DID if no grant is found', async () => { - // alice installs some other protocol - const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { - ...notesProtocol, - protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` - }} }); - expect(aliceConfigStatus.code).to.equal(202); - const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); - expect(aliceOtherProtocolSend.code).to.equal(202); + it('should subscribe to records as the delegate DID if no grant is found', async () => { + // alice installs some other protocol + const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { + ...notesProtocol, + protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` + }} }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); + expect(aliceOtherProtocolSend.code).to.equal(202); - // delegatedDwn subscribes to both protocols - const permissionedNotesRecords: Map = new Map(); - const permissionedNotesSubscriptionHandler = async (record: Record) => { - permissionedNotesRecords.set(record.id, record); - }; - const permissionedNotesSubscribeResult = await delegateDwn.records.subscribe({ - from : aliceDid.uri, - protocol : notesProtocol.protocol, - message : { - filter: { - protocol: notesProtocol.protocol + // delegatedDwn subscribes to both protocols + const permissionedNotesRecords: Map = new Map(); + const permissionedNotesSubscriptionHandler = async (record: Record) => { + permissionedNotesRecords.set(record.id, record); + }; + const permissionedNotesSubscribeResult = await delegateDwn.records.subscribe({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol: notesProtocol.protocol + } + }, + subscriptionHandler: permissionedNotesSubscriptionHandler + }); + expect(permissionedNotesSubscribeResult.status.code).to.equal(200); + + const otherProtocolRecords: Map = new Map(); + const otherProtocolSubscriptionHandler = async (record: Record) => { + otherProtocolRecords.set(record.id, record); + }; + const otherProtocolSubscribeResult = await delegateDwn.records.subscribe({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } + }, + subscriptionHandler: otherProtocolSubscriptionHandler + }); + expect(otherProtocolSubscribeResult.status.code).to.equal(200); + + // alice subscribes to the other protocol as a sanity + const aliceOtherProtocolRecords: Map = new Map(); + const aliceOtherProtocolSubscriptionHandler = async (record: Record) => { + aliceOtherProtocolRecords.set(record.id, record); + }; + const aliceOtherProtocolSubscribeResult = await dwnAlice.records.subscribe({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } + }, + subscriptionHandler: aliceOtherProtocolSubscriptionHandler + }); + expect(aliceOtherProtocolSubscribeResult.status.code).to.equal(200); + + // NOTE: write the private record before the public so that it should be received first + // alice writes a public and private note to the other protocol + const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + published : true, + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', } - }, - subscriptionHandler: permissionedNotesSubscriptionHandler - }); - expect(permissionedNotesSubscribeResult.status.code).to.equal(200); + }); + expect(writeStatus2.code).to.equal(202); + expect(publicRecord).to.not.be.undefined; + const { status: publicRecordSendStatus } = await publicRecord.send(); + expect(publicRecordSendStatus.code).to.equal(202); - const otherProtocolRecords: Map = new Map(); - const otherProtocolSubscriptionHandler = async (record: Record) => { - otherProtocolRecords.set(record.id, record); - }; - const otherProtocolSubscribeResult = await delegateDwn.records.subscribe({ - from : aliceDid.uri, - protocol : aliceOtherProtocol.definition.protocol, - message : { - filter: { - protocol: aliceOtherProtocol.definition.protocol + // alice writes a note record to the permissioned protocol + const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', } - }, - subscriptionHandler: otherProtocolSubscriptionHandler - }); - expect(otherProtocolSubscribeResult.status.code).to.equal(200); + }); + expect(writeStatus1.code).to.equal(202); + expect(allowedRecord).to.not.be.undefined; + const { status: allowedRecordSendStatus } = await allowedRecord.send(); + expect(allowedRecordSendStatus.code).to.equal(202); - // alice subscribes to the other protocol as a sanity - const aliceOtherProtocolRecords: Map = new Map(); - const aliceOtherProtocolSubscriptionHandler = async (record: Record) => { - aliceOtherProtocolRecords.set(record.id, record); - }; - const aliceOtherProtocolSubscribeResult = await dwnAlice.records.subscribe({ - from : aliceDid.uri, - protocol : aliceOtherProtocol.definition.protocol, - message : { - filter: { - protocol: aliceOtherProtocol.definition.protocol + const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', } - }, - subscriptionHandler: aliceOtherProtocolSubscriptionHandler - }); - expect(aliceOtherProtocolSubscribeResult.status.code).to.equal(200); - - // NOTE: write the private record before the public so that it should be received first - // alice writes a public and private note to the other protocol - const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - published : true, - protocol : aliceOtherProtocol.definition.protocol, - protocolPath : 'note', - schema : aliceOtherProtocol.definition.types.note.schema, - dataFormat : 'text/plain', - } - }); - expect(writeStatus2.code).to.equal(202); - expect(publicRecord).to.not.be.undefined; - const { status: publicRecordSendStatus } = await publicRecord.send(); - expect(publicRecordSendStatus.code).to.equal(202); - - // alice writes a note record to the permissioned protocol - const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - protocol : notesProtocol.protocol, - protocolPath : 'note', - schema : notesProtocol.types.note.schema, - dataFormat : 'text/plain', - } + }); + expect(writeStatus3.code).to.equal(202); + expect(privateRecord).to.not.be.undefined; + const { status: privateRecordSendStatus } = await privateRecord.send(); + expect(privateRecordSendStatus.code).to.equal(202); + + // wait for the records to be received + // alice receives both the public and private records on her subscription + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(aliceOtherProtocolRecords.size).to.equal(2); + expect(aliceOtherProtocolRecords.get(publicRecord.id)).to.exist; + expect(aliceOtherProtocolRecords.get(privateRecord.id)).to.exist; + }); + + // delegated agent only receives the public record from the other protocol + await Poller.pollUntilSuccessOrTimeout(async () => { + // permissionedNotesRecords should have the allowedRecord + expect(permissionedNotesRecords.size).to.equal(1); + expect(permissionedNotesRecords.get(allowedRecord.id)).to.exist; + + // otherProtocolRecords should have only the publicRecord + expect(otherProtocolRecords.size).to.equal(1); + expect(otherProtocolRecords.get(publicRecord.id)).to.exist; + }); }); - expect(writeStatus1.code).to.equal(202); - expect(allowedRecord).to.not.be.undefined; - const { status: allowedRecordSendStatus } = await allowedRecord.send(); - expect(allowedRecordSendStatus.code).to.equal(202); - - const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ - data : 'Hello, world!', - message : { - protocol : aliceOtherProtocol.definition.protocol, - protocolPath : 'note', - schema : aliceOtherProtocol.definition.types.note.schema, - dataFormat : 'text/plain', + }); + + describe('protocols', () => { + it('should configure a protocol with a delegated grant', async () => { + const protocolUri = `http://protocol-configure.xyz/protocol/${TestDataGenerator.randomString(15)}`; + + // attempt to configure the protocol without a grant, it should fail + try { + await delegateDwn.protocols.configure({ + message: { + definition: { + ...notesProtocol, + protocol: protocolUri, + } + } + }); + expect.fail('Expected an error to be thrown.'); + } catch(error: any) { + expect(error.message).to.equal(`CachedPermissions: No permissions found for ProtocolsConfigure: ${protocolUri}`); } + + // create a grant for the protocol + const delegatedBearerDid = await delegateHarness.agent.did.get({ didUri: delegateDid.uri, tenant: delegateDid.uri }); + const grants = await Oidc.createPermissionGrants(aliceDid.uri, delegatedBearerDid, testHarness.agent, [{ + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Configure, + protocol : protocolUri + }]); + + await Web5.processConnectedGrants({ grants, delegateDid: delegateDid.uri, agent: delegateHarness.agent }); + + // now try again after processing the connected grant + const { status, protocol } = await delegateDwn.protocols.configure({ + message: { + definition: { + ...notesProtocol, + protocol: protocolUri, + } + } + }); + expect(status.code).to.equal(202); + expect(protocol).to.exist; + expect(protocol.definition.protocol).to.equal(protocolUri); }); - expect(writeStatus3.code).to.equal(202); - expect(privateRecord).to.not.be.undefined; - const { status: privateRecordSendStatus } = await privateRecord.send(); - expect(privateRecordSendStatus.code).to.equal(202); - // wait for the records to be received - // alice receives both the public and private records on her subscription - await Poller.pollUntilSuccessOrTimeout(async () => { - expect(aliceOtherProtocolRecords.size).to.equal(2); - expect(aliceOtherProtocolRecords.get(publicRecord.id)).to.exist; - expect(aliceOtherProtocolRecords.get(privateRecord.id)).to.exist; + it('should query for a protocol with a permission grant', async () => { + // configure a non public protocol + const nonPublicProtocol = { + ...notesProtocol, + protocol : `http://non-public-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}`, + published : false + }; + + const { status: nonPublicStatus, protocol: nonPublicProtocolResponse } = await dwnAlice.protocols.configure({ + message: { + definition: nonPublicProtocol + } + }); + expect(nonPublicStatus.code).to.equal(202); + expect(nonPublicProtocolResponse).to.exist; + const nonPublicProtocolSend = await nonPublicProtocolResponse.send(aliceDid.uri); + expect(nonPublicProtocolSend.status.code).to.equal(202); + + // attempt to query the protocol, should not return any results as there are no grants for it + const { status: nonPublicQueryStatus, protocols: nonPublicProtocols } = await delegateDwn.protocols.query({ + from : aliceDid.uri, + message : { + filter: { + protocol: nonPublicProtocol.protocol + } + } + }); + expect(nonPublicQueryStatus.code).to.equal(200); + expect(nonPublicProtocols).to.exist; + expect(nonPublicProtocols).to.have.lengthOf(0); + + // grant the delegate DID access to query the non-public protocol + const delegatedBearerDid = await delegateHarness.agent.did.get({ didUri: delegateDid.uri, tenant: delegateDid.uri }); + const grants = await Oidc.createPermissionGrants(aliceDid.uri, delegatedBearerDid, testHarness.agent, [{ + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Query, + protocol : nonPublicProtocol.protocol + }]); + await Web5.processConnectedGrants({ grants, delegateDid: delegateDid.uri, agent: delegateHarness.agent }); + + // now query for the non-public protocol, should return the protocol + const { status: nonPublicQueryStatus2, protocols: nonPublicProtocols2 } = await delegateDwn.protocols.query({ + from : aliceDid.uri, + message : { + filter: { + protocol: nonPublicProtocol.protocol + } + } + }); + expect(nonPublicQueryStatus2.code).to.equal(200); + expect(nonPublicProtocols2).to.exist; + expect(nonPublicProtocols2).to.have.lengthOf(1); }); - // delegated agent only receives the public record from the other protocol - await Poller.pollUntilSuccessOrTimeout(async () => { - // permissionedNotesRecords should have the allowedRecord - expect(permissionedNotesRecords.size).to.equal(1); - expect(permissionedNotesRecords.get(allowedRecord.id)).to.exist; + it('should query for a protocol as the delegate DID if no grant is found', async () => { + // configure a public protocol without any grants + const publicProtocol = { + ...notesProtocol, + protocol : `http://public-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}`, + published : true + }; + + const { status: publicStatus, protocol: publicProtocolResponse } = await dwnAlice.protocols.configure({ + message: { + definition: publicProtocol + } + }); + expect(publicStatus.code).to.equal(202); + expect(publicProtocolResponse).to.exist; + const publicProtocolSend = await publicProtocolResponse.send(aliceDid.uri); + expect(publicProtocolSend.status.code).to.equal(202); - // otherProtocolRecords should have only the publicRecord - expect(otherProtocolRecords.size).to.equal(1); - expect(otherProtocolRecords.get(publicRecord.id)).to.exist; + const { status: publicQueryStatus, protocols: publicProtocols } = await delegateDwn.protocols.query({ + from : aliceDid.uri, + message : { + filter: { + protocol: publicProtocol.protocol + } + } + }); + expect(publicQueryStatus.code).to.equal(200); + expect(publicProtocols).to.exist; + expect(publicProtocols).to.have.lengthOf(1); + expect(publicProtocols[0].definition.protocol).to.equal(publicProtocol.protocol); }); }); }); From 79173c0a51a51be2e70a65bdc442f0d3853426a6 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 11 Sep 2024 19:35:48 -0400 Subject: [PATCH 10/11] add comments --- packages/agent/src/oidc.ts | 5 ++++- packages/api/src/dwn-api.ts | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 515d775f9..e56e9eb1f 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -602,12 +602,16 @@ function encryptAuthResponse({ } function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean { + // Currently all record permissions are treated as delegated permissions + // In the future only methods that modify state will be delegated and the rest will be normal permissions if (isRecordPermissionScope(scope)) { return true; } else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) { + // ProtocolConfigure messages are also delegated, as they modify state return true; } + // All other permissions are not treated as delegated return false; } @@ -626,7 +630,6 @@ async function createPermissionGrants( // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849 const permissionGrants = await Promise.all( scopes.map((scope) => { - // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission. const delegated = shouldUseDelegatePermission(scope); return permissionsApi.createGrant({ diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 66081734a..464b1bb00 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -481,11 +481,10 @@ export class DwnApi { // If the protocol is public, the query should be successful. This allows the app to query for public protocols without having a grant. try { - const protocolFromRequest = request.message.filter?.protocol; const { grant: { id: permissionGrantId } } = await this.permissionsApi.getPermissionForRequest({ connectedDid : this.connectedDid, delegateDid : this.delegateDid, - protocol : protocolFromRequest, + protocol : request.message.filter.protocol, cached : true, messageType : agentRequest.messageType }); From 911aa9646d08feb08dba6133b91ea29c80368fd2 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 11 Sep 2024 19:40:03 -0400 Subject: [PATCH 11/11] add changeset --- .changeset/eighty-bikes-join.md | 5 +++++ .changeset/slimy-bulldogs-kiss.md | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/eighty-bikes-join.md create mode 100644 .changeset/slimy-bulldogs-kiss.md diff --git a/.changeset/eighty-bikes-join.md b/.changeset/eighty-bikes-join.md new file mode 100644 index 000000000..0c1aa988b --- /dev/null +++ b/.changeset/eighty-bikes-join.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +Enable Protocol Query/Configure with delegate Grant diff --git a/.changeset/slimy-bulldogs-kiss.md b/.changeset/slimy-bulldogs-kiss.md new file mode 100644 index 000000000..0c5e07900 --- /dev/null +++ b/.changeset/slimy-bulldogs-kiss.md @@ -0,0 +1,8 @@ +--- +"@web5/agent": patch +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +--- + +Enable ProtocolQuery/Configure with delegate grant