From e7cb25a06ac5c521943bd0bb1cac55770c2ea82c Mon Sep 17 00:00:00 2001 From: Liran Cohen <c.liran.c@gmail.com> Date: Wed, 11 Sep 2024 20:35:24 -0400 Subject: [PATCH] Protocol Query with or without grant, Configure with grant. (#894) - protocol query with regular permission grant - if grant is not found, author query as delegate did to get any public protocols - protocol configure with delegate grant - `Permission` can now include `configure` which represents `ProtocolsConfigure` of a particular protocol - `createPermissionRequestForProtocol` now includes a grant for `ProtocolsQuery` for the protocol. --- .changeset/eighty-bikes-join.md | 5 + .changeset/slimy-bulldogs-kiss.md | 8 + package.json | 2 +- packages/agent/package.json | 2 +- packages/agent/src/connect.ts | 20 +- packages/agent/src/oidc.ts | 21 +- packages/agent/src/permissions-api.ts | 18 +- packages/agent/src/utils.ts | 2 +- packages/agent/tests/connect.spec.ts | 15 +- packages/api/package.json | 2 +- packages/api/src/dwn-api.ts | 57 +- packages/api/src/web5.ts | 2 +- packages/api/tests/dwn-api.spec.ts | 1020 ++++++++++++++----------- packages/dev-env/docker-compose.yaml | 2 +- pnpm-lock.yaml | 36 +- 15 files changed, 711 insertions(+), 501 deletions(-) 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 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/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 884c15b96..68aff74e9 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, @@ -261,6 +270,13 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco method : DwnMethodName.Subscribe, }); break; + case 'configure': + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Configure, + }); + break; } } diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 3443aabe5..e56e9eb1f 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,20 @@ function encryptAuthResponse({ return compactJwe; } +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; +} + /** * Creates the permission grants that assign to the selectedDid the level of * permissions that the web app requested in the {@link Web5ConnectAuthRequest} @@ -615,9 +630,8 @@ 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, 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 +640,6 @@ async function createPermissionGrants( dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional author : selectedDid, }); - }) ); 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/agent/src/utils.ts b/packages/agent/src/utils.ts index 936d95980..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 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/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/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index b7405958a..464b1bb00 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,32 @@ export class DwnApi { * Configure method, used to setup a new protocol (or update) with the passed definitions */ configure: async (request: ProtocolsConfigureRequest): Promise<ProtocolsConfigureResponse> => { - const agentResponse = await this.agent.processDwnRequest({ + + const agentRequest:ProcessDwnRequest<DwnInterface.ProtocolsConfigure> = { author : this.connectedDid, messageParams : request.message, messageType : DwnInterface.ProtocolsConfigure, target : this.connectedDid - }); + }; + + if (this.delegateDid) { + 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); const { message, messageCid, reply: { status }} = agentResponse; const response: ProtocolsConfigureResponse = { status }; @@ -457,6 +476,30 @@ export class DwnApi { target : request.from || this.connectedDid }; + 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 { + const { grant: { id: permissionGrantId } } = await this.permissionsApi.getPermissionForRequest({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + protocol : request.message.filter.protocol, + cached : true, + messageType : agentRequest.messageType + }); + + agentRequest.messageParams = { + ...agentRequest.messageParams, + permissionGrantId + }; + agentRequest.granteeDid = this.delegateDid; + } 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; + } + } + let agentResponse: DwnResponse<DwnInterface.ProtocolsQuery>; if (request.from) { @@ -616,8 +659,8 @@ export class DwnApi { delegatedGrant }; agentRequest.granteeDid = this.delegateDid; - } catch(error:any) { - // set the author of the request to the delegate did + } 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; } } @@ -708,7 +751,7 @@ export class DwnApi { }; agentRequest.granteeDid = this.delegateDid; } catch(_error:any) { - // set the author of the request to the delegate did + // if a grant is not found, we should author the request as the delegated DID to get public records agentRequest.author = this.delegateDid; } } @@ -811,7 +854,7 @@ export class DwnApi { }; agentRequest.granteeDid = this.delegateDid; } catch(_error:any) { - // set the author of the request to the delegate did + // 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/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'; 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<string, Record> = 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<string, Record> = 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<string, Record> = 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<string, Record> = 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<string, Record> = 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<string, Record> = 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<string, Record> = 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<string, Record> = 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); }); }); }); 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 860e902e7..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 @@ -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 @@ -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,12 +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==} + '@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': @@ -2511,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': @@ -7369,7 +7369,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tbd54566975/dwn-sdk-js@0.4.6': + '@tbd54566975/dwn-sdk-js@0.4.7': dependencies: '@ipld/dag-cbor': 9.0.3 '@js-temporal/polyfill': 0.4.4 @@ -7402,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 @@ -8355,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