From 4ff2316e28ad3f29f0336c69adde0a37840ebb33 Mon Sep 17 00:00:00 2001 From: Liran Cohen <c.liran.c@gmail.com> Date: Tue, 27 Aug 2024 14:44:06 -0400 Subject: [PATCH 1/7] Add requestPermissionsForProtocol helper method to connect module (#854) This PR adds a `requestPermissionsForProtocol` helper method to the Connect module. This helper method takes a protocol definition, as well as simple string representations of the permissions being requested, ie `write`, `read`, `delete`, `query` and `subscribe`. It will by default include the permissions also needed to sync a protocol's messages `MessagesRead`, `MessagesQuery` and `MessagesSubscribe`. #### Example: ```typescript // all permissions for each protocol const { delegateDid } = await Web5.connect({ walletConnectOptions: { walletUri: "web5://connect", connectServerUrl: "http://localhost:3000/connect", permissionRequests: [{ protocolDefinition: profileProtocol }], onWalletUriReady: generateQRCode, validatePin: async () => { goToPinScreen(); const pin = await waitForPin(); return pin; }, }, }); ``` ```typescript // specific permissions const { delegateDid } = await Web5.connect({ walletConnectOptions: { walletUri: "web5://connect", connectServerUrl: "http://localhost:3000/connect", permissionRequests: [{ definition: protocol1, permissions: ['read', 'write'] // creates read+write + sync grants },{ definition: protocol2, }], onWalletUriReady: generateQRCode, validatePin: async () => { goToPinScreen(); const pin = await waitForPin(); return pin; }, }, }); ``` --- This PR also makes `registerIdentity` options optional. If no options are provided all protocols are synced. --- .changeset/fresh-olives-dance.md | 5 + examples/wallet-connect.html | 25 +--- packages/agent/src/connect.ts | 84 ++++++++++- packages/agent/src/oidc.ts | 101 +++++-------- packages/agent/src/sync-api.ts | 2 +- packages/agent/src/sync-engine-level.ts | 5 +- packages/agent/src/types/sync.ts | 2 +- packages/agent/tests/connect.spec.ts | 99 +++++++++++-- packages/agent/tests/rpc-client.spec.ts | 12 ++ .../agent/tests/sync-engine-level.spec.ts | 112 +++++--------- packages/api/src/web5.ts | 23 ++- packages/api/tests/web5.spec.ts | 139 ++++++++++++++++++ 12 files changed, 422 insertions(+), 187 deletions(-) create mode 100644 .changeset/fresh-olives-dance.md diff --git a/.changeset/fresh-olives-dance.md b/.changeset/fresh-olives-dance.md new file mode 100644 index 000000000..e611f3362 --- /dev/null +++ b/.changeset/fresh-olives-dance.md @@ -0,0 +1,5 @@ +--- +"@web5/agent": patch +--- + +Add requestPermissionsForProtocol helper method to connect module diff --git a/examples/wallet-connect.html b/examples/wallet-connect.html index eb3e813ab..ccaa699e7 100644 --- a/examples/wallet-connect.html +++ b/examples/wallet-connect.html @@ -128,35 +128,12 @@ <h1>Success</h1> }, }; - const scopes = [ - { - interface: "Records", - method: "Write", - protocol: "http://profile-protocol.xyz", - }, - { - interface: "Records", - method: "Query", - protocol: "http://profile-protocol.xyz", - }, - { - interface: "Records", - method: "Read", - protocol: "http://profile-protocol.xyz", - }, - ]; - try { const { delegateDid } = await Web5.connect({ walletConnectOptions: { walletUri: "web5://connect", connectServerUrl: "http://localhost:3000/connect", - permissionRequests: [ - { - protocolDefinition: profileProtocol, - permissionScopes: scopes, - }, - ], + permissionRequests: [{ protocolDefinition: profileProtocol }], onWalletUriReady: generateQRCode, validatePin: async () => { goToPinScreen(); diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index c30d75908..fdaa35362 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -1,13 +1,16 @@ -import { CryptoUtils } from '@web5/crypto'; -import { DwnProtocolDefinition, DwnRecordsPermissionScope } from './index.js'; + +import type { PushedAuthResponse } from './oidc.js'; +import type { DwnPermissionScope, DwnProtocolDefinition, Web5ConnectAuthResponse } from './index.js'; + import { - Web5ConnectAuthResponse, Oidc, - type PushedAuthResponse, } from './oidc.js'; import { pollWithTtl } from './utils.js'; -import { DidJwk } from '@web5/dids'; + import { Convert } from '@web5/common'; +import { CryptoUtils } from '@web5/crypto'; +import { DidJwk } from '@web5/dids'; +import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; /** * Initiates the wallet connect process. Used when a client wants to obtain @@ -179,7 +182,74 @@ export type ConnectPermissionRequest = { protocolDefinition: DwnProtocolDefinition; /** The scope of the permissions being requested for the given protocol */ - permissionScopes: DwnRecordsPermissionScope[]; + permissionScopes: DwnPermissionScope[]; }; -export const WalletConnect = { initClient }; +export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe'; + +function createPermissionRequestForProtocol(definition: DwnProtocolDefinition, permissions: Permission[]): ConnectPermissionRequest { + const requests: DwnPermissionScope[] = []; + + // In order to enable sync, we must request permissions for `MessagesQuery`, `MessagesRead` and `MessagesSubscribe` + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Read, + }, { + protocol : definition.protocol, + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Query, + }, { + protocol : definition.protocol, + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Subscribe, + }); + + // We also request any additional permissions the user has requested for this protocol + for (const permission of permissions) { + switch (permission) { + case 'write': + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + }); + break; + case 'read': + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Read, + }); + break; + case 'delete': + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Delete, + }); + break; + case 'query': + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Query, + }); + break; + case 'subscribe': + requests.push({ + protocol : definition.protocol, + interface : DwnInterfaceName.Records, + method : DwnMethodName.Subscribe, + }); + break; + } + } + + return { + protocolDefinition : definition, + permissionScopes : requests, + }; +} + +export const WalletConnect = { initClient, createPermissionRequestForProtocol }; diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index d544c428b..a05873393 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -12,15 +12,13 @@ import { concatenateUrl } from './utils.js'; import { xchacha20poly1305 } from '@noble/ciphers/chacha'; import type { ConnectPermissionRequest } from './connect.js'; import { DidDocument, DidJwk, PortableDid, type BearerDid } from '@web5/dids'; -import { AgentDwnApi } from './dwn-api.js'; -import { - DwnInterfaceName, - DwnMethodName, - type PermissionScope, - type RecordsWriteMessage, +import type { + PermissionScope, + RecordsWriteMessage, } from '@tbd54566975/dwn-sdk-js'; import { DwnInterface, DwnProtocolDefinition } from './types/dwn.js'; import { AgentPermissionsApi } from './permissions-api.js'; +import type { Web5Agent } from './types/agent.js'; /** * Sent to an OIDC server to authorize a client. Allows clients @@ -616,14 +614,17 @@ function encryptAuthResponse({ async function createPermissionGrants( selectedDid: string, delegateBearerDid: BearerDid, - dwn: AgentDwnApi, - permissionsApi: AgentPermissionsApi, + agent: Web5Agent, scopes: PermissionScope[], - protocolUri: string ) { + + const permissionsApi = new AgentPermissionsApi({ agent }); + + // 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) => permissionsApi.createGrant({ + store : true, grantedTo : delegateBearerDid.uri, scope, dateExpires : '2040-06-25T16:09:16.693356Z', @@ -632,57 +633,23 @@ async function createPermissionGrants( ) ); - // Grant Messages Query and Messages Read for sync to work - permissionGrants.push( - await permissionsApi.createGrant({ - grantedTo : delegateBearerDid.uri, - scope : { - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Query, - protocol : protocolUri, - }, - dateExpires : '2040-06-25T16:09:16.693356Z', - author : selectedDid, - }) - ); - permissionGrants.push( - await permissionsApi.createGrant({ - grantedTo : delegateBearerDid.uri, - scope : { - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Read, - protocol : protocolUri, - }, - dateExpires : '2040-06-25T16:09:16.693356Z', - author : selectedDid, - }) - ); - const messagePromises = permissionGrants.map(async (grant) => { - // Quirk: we have to pull out encodedData out of the message the schema validator doesnt want it there + // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there const { encodedData, ...rawMessage } = grant.message; const data = Convert.base64Url(encodedData).toUint8Array(); - const params = { + const { reply } = await agent.sendDwnRequest({ author : selectedDid, target : selectedDid, messageType : DwnInterface.RecordsWrite, dataStream : new Blob([data]), rawMessage, - }; - - const message = await dwn.processRequest(params); - const sent = await dwn.sendRequest(params); + }); - // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849 - if (message.reply.status.code !== 202) { - throw new Error( - `Could not process the message. Error details: ${message.reply.status.detail}` - ); - } - if (sent.reply.status.code !== 202) { + // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync + if (reply.status.code !== 202 && reply.status.code !== 409) { throw new Error( - `Could not send the message. Error details: ${message.reply.status.detail}` + `Could not send the message. Error details: ${reply.status.detail}` ); } @@ -700,10 +667,10 @@ async function createPermissionGrants( */ async function prepareProtocols( selectedDid: string, - agentDwnApi: AgentDwnApi, + agent: Web5Agent, protocolDefinition: DwnProtocolDefinition ) { - const queryMessage = await agentDwnApi.processRequest({ + const queryMessage = await agent.processDwnRequest({ author : selectedDid, messageType : DwnInterface.ProtocolsQuery, target : selectedDid, @@ -711,7 +678,7 @@ async function prepareProtocols( }); if (queryMessage.reply.status.code === 404) { - const configureMessage = await agentDwnApi.processRequest({ + const configureMessage = await agent.processDwnRequest({ author : selectedDid, messageType : DwnInterface.ProtocolsConfigure, target : selectedDid, @@ -721,6 +688,19 @@ async function prepareProtocols( if (configureMessage.reply.status.code !== 202) { throw new Error(`Could not install protocol: ${configureMessage.reply.status.detail}`); } + + // send the configure message to the remote DWN so that the APP can immediately use it without waiting for a sync cycle from the wallet + const { reply: sendReply } = await agent.sendDwnRequest({ + author : selectedDid, + target : selectedDid, + messageType : DwnInterface.ProtocolsConfigure, + rawMessage : configureMessage.message, + }); + + // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync + if (sendReply.status.code !== 202 && sendReply.status.code !== 409) { + throw new Error(`Could not send protocol: ${sendReply.status.detail}`); + } } else if (queryMessage.reply.status.code !== 200) { throw new Error(`Could not fetch protcol: ${queryMessage.reply.status.detail}`); } @@ -739,24 +719,17 @@ async function submitAuthResponse( selectedDid: string, authRequest: Web5ConnectAuthRequest, randomPin: string, - agentDwnApi: AgentDwnApi, - agentPermissionsApi: AgentPermissionsApi + agent: Web5Agent, ) { const delegateBearerDid = await DidJwk.create(); const delegatePortableDid = await delegateBearerDid.export(); const delegateGrantPromises = authRequest.permissionRequests.map(async (permissionRequest) => { - await prepareProtocols(selectedDid, agentDwnApi, permissionRequest.protocolDefinition); - // TODO: validate to make sure the scopes and definition are assigned to the same protocol - const permissionGrants = await Oidc.createPermissionGrants( - selectedDid, - delegateBearerDid, - agentDwnApi, - agentPermissionsApi, - permissionRequest.permissionScopes, - permissionRequest.protocolDefinition.protocol - ); + const { protocolDefinition, permissionScopes } = permissionRequest; + + await prepareProtocols(selectedDid, agent, protocolDefinition); + const permissionGrants = await Oidc.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes); return permissionGrants; }); diff --git a/packages/agent/src/sync-api.ts b/packages/agent/src/sync-api.ts index 3c7ea1089..29763f6b8 100644 --- a/packages/agent/src/sync-api.ts +++ b/packages/agent/src/sync-api.ts @@ -41,7 +41,7 @@ export class AgentSyncApi implements SyncEngine { this._syncEngine.agent = agent; } - public async registerIdentity(params: { did: string; options: SyncIdentityOptions }): Promise<void> { + public async registerIdentity(params: { did: string; options?: SyncIdentityOptions }): Promise<void> { await this._syncEngine.registerIdentity(params); } diff --git a/packages/agent/src/sync-engine-level.ts b/packages/agent/src/sync-engine-level.ts index 259667cb8..5908fba49 100644 --- a/packages/agent/src/sync-engine-level.ts +++ b/packages/agent/src/sync-engine-level.ts @@ -250,10 +250,13 @@ export class SyncEngineLevel implements SyncEngine { await pushQueue.batch(deleteOperations as any); } - public async registerIdentity({ did, options }: { did: string; options: SyncIdentityOptions }): Promise<void> { + public async registerIdentity({ did, options }: { did: string; options?: SyncIdentityOptions }): Promise<void> { // Get a reference to the `registeredIdentities` sublevel. const registeredIdentities = this._db.sublevel('registeredIdentities'); + // if no options are provided, we default to no delegateDid and all protocols (empty array) + options ??= { protocols: [] }; + // Add (or overwrite, if present) the Identity's DID as a registered identity. await registeredIdentities.put(did, JSON.stringify(options)); } diff --git a/packages/agent/src/types/sync.ts b/packages/agent/src/types/sync.ts index ee4dd3182..1fc5666d3 100644 --- a/packages/agent/src/types/sync.ts +++ b/packages/agent/src/types/sync.ts @@ -6,7 +6,7 @@ export type SyncIdentityOptions = { } export interface SyncEngine { agent: Web5PlatformAgent; - registerIdentity(params: { did: string, options: SyncIdentityOptions }): Promise<void>; + registerIdentity(params: { did: string, options?: SyncIdentityOptions }): Promise<void>; sync(direction?: 'push' | 'pull'): Promise<void>; startSync(params: { interval: string }): Promise<void>; stopSync(): void; diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 3d6c0d20a..441f80bdd 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -4,7 +4,6 @@ import { CryptoUtils } from '@web5/crypto'; import { type BearerDid, DidDht, DidJwk, PortableDid } from '@web5/dids'; import { Convert } from '@web5/common'; import { - DelegateGrant, Oidc, type Web5ConnectAuthRequest, type Web5ConnectAuthResponse, @@ -12,8 +11,9 @@ import { import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { TestAgent } from './utils/test-agent.js'; import { testDwnUrl } from './utils/test-config.js'; -import { BearerIdentity, DwnProtocolDefinition, DwnProtocolPermissionScope, DwnResponse, WalletConnect } from '../src/index.js'; -import { RecordsPermissionScope, type PermissionScope } from '@tbd54566975/dwn-sdk-js'; +import { BearerIdentity, DwnProtocolDefinition, WalletConnect } from '../src/index.js'; +import { RecordsPermissionScope } from '@tbd54566975/dwn-sdk-js'; +import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; describe('web5 connect', function () { this.timeout(20000); @@ -197,6 +197,7 @@ describe('web5 connect', function () { }); after(async () => { + sinon.restore(); await testHarness.clearStorage(); await testHarness.closeStorage(); }); @@ -288,13 +289,11 @@ describe('web5 connect', function () { const results = await Oidc.createPermissionGrants( providerIdentity.did.uri, delegateBearerDid, - testHarness.agent.dwn, - testHarness.agent.permissions, - permissionScopes, - protocolDefinition.protocol + testHarness.agent, + permissionScopes ); - const scopesRequestedPlusTwoDefaultScopes = permissionScopes.length + 2; - expect(results).to.have.lengthOf(scopesRequestedPlusTwoDefaultScopes); + const scopesRequested = permissionScopes.length; + expect(results).to.have.lengthOf(scopesRequested); expect(results[0]).to.be.a('object'); }); @@ -392,8 +391,7 @@ describe('web5 connect', function () { selectedDid, authRequest, randomPin, - testHarness.agent.dwn, - testHarness.agent.permissions + testHarness.agent ); expect(fetchSpy.calledOnce).to.be.true; }); @@ -490,4 +488,83 @@ describe('web5 connect', function () { expect(results?.delegatePortableDid).to.be.an('object'); }); }); + + describe('createPermissionRequestForProtocol', () => { + it('should add sync permissions to all requests', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, []); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + expect(permissionRequests.permissionScopes.length).to.equal(3); // only includes the sync permissions + 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; + }); + + it('should add requested permissions to the request', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, ['write', 'read']); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + + // the 3 sync permissions plus the 2 requested permissions + expect(permissionRequests.permissionScopes.length).to.equal(5); + 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 () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, ['write', 'read', 'delete', 'query', 'subscribe']); + + 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.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; + }); + }); }); diff --git a/packages/agent/tests/rpc-client.spec.ts b/packages/agent/tests/rpc-client.spec.ts index 1d03fb534..8bb207c38 100644 --- a/packages/agent/tests/rpc-client.spec.ts +++ b/packages/agent/tests/rpc-client.spec.ts @@ -69,6 +69,10 @@ describe('RPC Clients', () => { alice = await TestDataGenerator.generateDidKeyPersona(); }); + after(() => { + sinon.restore(); + }); + it('returns available transports', async () => { const httpOnlyClient = new Web5RpcClient(); @@ -262,6 +266,10 @@ describe('RPC Clients', () => { let alice: Persona; let client: HttpWeb5RpcClient; + after(() => { + sinon.restore(); + }); + beforeEach(async () => { sinon.restore(); @@ -353,6 +361,10 @@ describe('RPC Clients', () => { dwnUrl.protocol = dwnUrl.protocol === 'http:' ? 'ws:' : 'wss:'; const socketDwnUrl = dwnUrl.toString(); + after(() => { + sinon.restore(); + }); + beforeEach(async () => { sinon.restore(); diff --git a/packages/agent/tests/sync-engine-level.spec.ts b/packages/agent/tests/sync-engine-level.spec.ts index 7365148a6..35fa68a08 100644 --- a/packages/agent/tests/sync-engine-level.spec.ts +++ b/packages/agent/tests/sync-engine-level.spec.ts @@ -12,6 +12,7 @@ import { testDwnUrl } from './utils/test-config.js'; import { SyncEngineLevel } from '../src/sync-engine-level.js'; import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { Convert } from '@web5/common'; +import { AbstractLevel } from 'abstract-level'; let testDwnUrls: string[] = [testDwnUrl]; @@ -162,6 +163,7 @@ describe('SyncEngineLevel', () => { }); after(async () => { + sinon.restore(); await testHarness.clearStorage(); await testHarness.closeStorage(); }); @@ -353,10 +355,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN. @@ -431,10 +430,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to push and pull all records from Alice's remote DWN to Alice's local DWN. @@ -471,10 +467,7 @@ describe('SyncEngineLevel', () => { it('throws if sync is attempted while an interval sync is running', async () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // start the sync engine with an interval of 10 seconds @@ -1057,6 +1050,21 @@ describe('SyncEngineLevel', () => { expect(localBarRecords.reply.status.code).to.equal(200); expect(localBarRecords.reply.entries).to.have.length(0); }); + + it('defaults to all protocols and undefined delegate if no options are provided', async () => { + // spy on AbstractLevel put + const abstractLevelPut = sinon.spy(AbstractLevel.prototype, 'put'); + + // register identity without any options + await testHarness.agent.sync.registerIdentity({ + did: alice.did.uri + }); + + const registerIdentitiesPutCall = abstractLevelPut.args[0]; + const options = JSON.parse(registerIdentitiesPutCall[1] as string); + // confirm that without options the options are set to an empty protocol array + expect(options).to.deep.equal({ protocols: [] }); + }); }); describe('pull()', () => { @@ -1112,10 +1120,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN. @@ -1208,10 +1213,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to pull all records from Alice's remote DWNs @@ -1337,10 +1339,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // spy on sendDwnRequest to the remote DWN @@ -1561,10 +1560,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN. @@ -1626,10 +1622,7 @@ describe('SyncEngineLevel', () => { // register alice await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // create a remote record @@ -1720,18 +1713,12 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Register Bob's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : bob.did.uri, - options : { - protocols: [] - } + did: bob.did.uri, }); // Execute Sync to pull all records from Alice's and Bob's remove DWNs to their local DWNs. @@ -1814,10 +1801,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN. @@ -1917,10 +1901,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to pull all records from Alice's remote DWNs @@ -1956,10 +1937,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // scenario: The messageCids returned from the local eventLog contains a Cid that already exists in the remote DWN. @@ -2251,10 +2229,7 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Execute Sync to push all records from Alice's local DWN to Alice's remote DWN. @@ -2315,10 +2290,7 @@ describe('SyncEngineLevel', () => { //register alice await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // create a local record @@ -2407,18 +2379,12 @@ describe('SyncEngineLevel', () => { // Register Alice's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); // Register Bob's DID to be synchronized. await testHarness.agent.sync.registerIdentity({ - did : bob.did.uri, - options : { - protocols: [] - } + did: bob.did.uri, }); // Execute Sync to push all records from Alice's and Bob's local DWNs to their remote DWNs. @@ -2451,10 +2417,7 @@ describe('SyncEngineLevel', () => { describe('startSync()', () => { it('calls sync() in each interval', async () => { await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); const syncSpy = sinon.stub(SyncEngineLevel.prototype, 'sync'); @@ -2473,10 +2436,7 @@ describe('SyncEngineLevel', () => { it('does not call sync() again until a sync round finishes', async () => { await testHarness.agent.sync.registerIdentity({ - did : alice.did.uri, - options : { - protocols: [] - } + did: alice.did.uri, }); const clock = sinon.useFakeTimers(); diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index 112579de1..6aba6ecef 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -9,8 +9,10 @@ import type { DelegateGrant, DwnDataEncodedRecordsWriteMessage, DwnMessagesPermissionScope, + DwnProtocolDefinition, DwnRecordsPermissionScope, HdIdentityVault, + Permission, WalletConnectOptions, Web5Agent, } from '@web5/agent'; @@ -35,6 +37,15 @@ export type DidCreateOptions = { dwnEndpoints?: string[]; } +export type ConnectPermissionRequest = { + protocolDefinition: DwnProtocolDefinition; + permissions?: Permission[]; +} + +export type ConnectOptions = Omit<WalletConnectOptions, 'permissionRequests'> & { + permissionRequests: ConnectPermissionRequest[]; +} + /** Optional overrides that can be provided when calling {@link Web5.connect}. */ export type Web5ConnectOptions = { /** @@ -42,7 +53,7 @@ export type Web5ConnectOptions = { * This param currently will not work in apps that are currently connected. * It must only be invoked at registration with a reset and empty DWN and agent. */ - walletConnectOptions?: WalletConnectOptions; + walletConnectOptions?: ConnectOptions; /** * Provide a {@link Web5Agent} implementation. Defaults to creating a local @@ -276,7 +287,15 @@ export class Web5 { // No connected identity found and connectOptions are provided, attempt to import a delegated DID from an external wallet try { - const { delegatePortableDid, connectedDid, delegateGrants: returnedGrants } = await WalletConnect.initClient(walletConnectOptions); + const { permissionRequests, ...connectOptions } = walletConnectOptions; + const walletPermissionRequests = permissionRequests.map(({ protocolDefinition, permissions }) => WalletConnect.createPermissionRequestForProtocol(protocolDefinition, permissions ?? [ + 'read', 'write', 'delete', 'query', 'subscribe' + ])); + + const { delegatePortableDid, connectedDid, delegateGrants: returnedGrants } = await WalletConnect.initClient({ + ...connectOptions, + permissionRequests: walletPermissionRequests, + }); delegateGrants = returnedGrants; // Import the delegated DID as an Identity in the User Agent. diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index ef1bdb522..ceb4791e9 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -793,6 +793,145 @@ describe('web5 api', () => { expect(startSyncSpy.args[0][0].interval).to.equal('1m'); }); + + + + it('should request all permissions for a protocol if no specific permissions are provided', async () => { + + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + + // spy on the WalletConnect createPermissionRequestForProtocol method + const requestPermissionsSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); + + // We throw and spy on the initClient method to avoid the actual WalletConnect initialization + // but to still be able to spy on the passed parameters + sinon.stub(WalletConnect, 'initClient').throws('Error'); + + // stub the cleanUpIdentity method to avoid actual cleanup + sinon.stub(Web5 as any, 'cleanUpIdentity').resolves(); + + const protocolDefinition: DwnProtocolDefinition = { + protocol : 'https://example.com/test-protocol', + published : true, + types : { + foo : {}, + bar : {} + }, + structure: { + foo: { + bar: {} + } + } + }; + + try { + + await Web5.connect({ + walletConnectOptions: { + connectServerUrl : 'https://connect.example.com', + walletUri : 'https://wallet.example.com', + validatePin : async () => { return '1234'; }, + onWalletUriReady : (_walletUri: string) => {}, + permissionRequests : [{ protocolDefinition }] + } + }); + + expect.fail('Should have thrown an error'); + } catch(error: any) { + // we expect an error because we stubbed the initClient method to throw it + expect(error.message).to.include('Sinon-provided Error'); + + // The `createPermissionRequestForProtocol` method should have been called once for the provided protocol + expect(requestPermissionsSpy.callCount).to.equal(1); + const call = requestPermissionsSpy.getCall(0); + + // since no explicit permissions were provided, all permissions should be requested + expect(call.args[1]).to.have.members([ + 'read', 'write', 'delete', 'query', 'subscribe' + ]); + } + }); + + it('should only request the specified permissions for a protocol', async () => { + + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + + // spy on the WalletConnect createPermissionRequestForProtocol method + const requestPermissionsSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); + + // We throw and spy on the initClient method to avoid the actual WalletConnect initialization + // but to still be able to spy on the passed parameters + sinon.stub(WalletConnect, 'initClient').throws('Error'); + + // stub the cleanUpIdentity method to avoid actual cleanup + sinon.stub(Web5 as any, 'cleanUpIdentity').resolves(); + + const protocol1Definition: DwnProtocolDefinition = { + protocol : 'https://example.com/test-protocol-1', + published : true, + types : { + foo : {}, + bar : {} + }, + structure: { + foo: { + bar: {} + } + } + }; + + const protocol2Definition: DwnProtocolDefinition = { + protocol : 'https://example.com/test-protocol-2', + published : true, + types : { + foo : {}, + bar : {} + }, + structure: { + foo: { + bar: {} + } + } + }; + + + try { + + await Web5.connect({ + walletConnectOptions: { + connectServerUrl : 'https://connect.example.com', + walletUri : 'https://wallet.example.com', + validatePin : async () => { return '1234'; }, + onWalletUriReady : (_walletUri: string) => {}, + permissionRequests : [ + { protocolDefinition: protocol1Definition }, // no permissions provided, expect all permissions to be requested + { protocolDefinition: protocol2Definition, permissions: ['read', 'write'] } // only read and write permissions provided + ] + } + }); + + expect.fail('Should have thrown an error'); + } catch(error: any) { + // we expect an error because we stubbed the initClient method to throw it + expect(error.message).to.include('Sinon-provided Error'); + + // The `createPermissionRequestForProtocol` method should have been called once for each provided request + expect(requestPermissionsSpy.callCount).to.equal(2); + const call1 = requestPermissionsSpy.getCall(0); + + // since no explicit permissions were provided for the first protocol, all permissions should be requested + expect(call1.args[1]).to.have.members([ + 'read', 'write', 'delete', 'query', 'subscribe' + ]); + + const call2 = requestPermissionsSpy.getCall(1); + + // only the provided permissions should be requested for the second protocol + expect(call2.args[1]).to.have.members([ + 'read', 'write' + ]); + } + }); }); describe('registration', () => { From 734743884f21491efce9392e129d612735e85282 Mon Sep 17 00:00:00 2001 From: Liran Cohen <c.liran.c@gmail.com> Date: Tue, 27 Aug 2024 16:54:45 -0400 Subject: [PATCH 2/7] Upgrade dwn-server and dwn-sdk-js (#853) - upgrade `dwn-sdk-js` to `v0.4.6` - upgrade `dwn-server` to `v0.4.8` - added `sinon.restore()` in some places it may have been missing to eliminate cause of intermittent browser failures. --- .changeset/gold-tools-yell.md | 5 +++ .changeset/warm-starfishes-beam.md | 8 +++++ package.json | 2 +- packages/agent/package.json | 2 +- packages/api/package.json | 2 +- packages/credentials/tests/jwt.spec.ts | 5 +++ .../credentials/tests/ssi-validator.spec.ts | 1 + .../tests/verifiable-credential.spec.ts | 4 +++ packages/crypto-aws-kms/tests/utils.spec.ts | 4 +++ .../crypto/tests/local-key-manager.spec.ts | 4 +++ packages/dev-env/docker-compose.yaml | 2 +- packages/dids/tests/bearer-did.spec.ts | 4 +++ packages/dids/tests/methods/did-dht.spec.ts | 1 + packages/dids/tests/methods/did-ion.spec.ts | 1 + packages/dids/tests/methods/did-web.spec.ts | 4 +++ .../resolver/resolver-cache-level.spec.ts | 1 + pnpm-lock.yaml | 36 +++++++++---------- 17 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 .changeset/gold-tools-yell.md create mode 100644 .changeset/warm-starfishes-beam.md diff --git a/.changeset/gold-tools-yell.md b/.changeset/gold-tools-yell.md new file mode 100644 index 000000000..146303c84 --- /dev/null +++ b/.changeset/gold-tools-yell.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +Upgrade `agent` to refelcy the newest `dwn-sdk-js` and `dwn-server` diff --git a/.changeset/warm-starfishes-beam.md b/.changeset/warm-starfishes-beam.md new file mode 100644 index 000000000..de8f4fd84 --- /dev/null +++ b/.changeset/warm-starfishes-beam.md @@ -0,0 +1,8 @@ +--- +"@web5/agent": patch +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +--- + +Upgrade `dwn-sdk-js` version and `dwn-server` dependency diff --git a/package.json b/package.json index 83b76c01a..12c283353 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.7", + "@web5/dwn-server": "0.4.8", "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 8d44de2be..eb67702d6 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.5", + "@tbd54566975/dwn-sdk-js": "0.4.6", "@web5/common": "1.0.0", "@web5/crypto": "workspace:*", "@web5/dids": "1.1.0", diff --git a/packages/api/package.json b/packages/api/package.json index da1048f58..22e311d31 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.5", + "@tbd54566975/dwn-sdk-js": "0.4.6", "@types/chai": "4.3.6", "@types/eslint": "8.56.10", "@types/mocha": "10.0.1", diff --git a/packages/credentials/tests/jwt.spec.ts b/packages/credentials/tests/jwt.spec.ts index 8e718a0e2..0fd423038 100644 --- a/packages/credentials/tests/jwt.spec.ts +++ b/packages/credentials/tests/jwt.spec.ts @@ -12,6 +12,11 @@ import { VerifiableCredential } from '../src/verifiable-credential.js'; import sinon from 'sinon'; describe('Jwt', () => { + + after(() => { + sinon.restore(); + }); + describe('parse()', () => { it('throws error if JWT doesnt contain 3 parts', async () => { expect(() => diff --git a/packages/credentials/tests/ssi-validator.spec.ts b/packages/credentials/tests/ssi-validator.spec.ts index 17ed40f61..20d7a3d13 100644 --- a/packages/credentials/tests/ssi-validator.spec.ts +++ b/packages/credentials/tests/ssi-validator.spec.ts @@ -89,6 +89,7 @@ describe('SsiValidator', () => { afterEach(() => { fetchStub.restore(); + sinon.restore(); }); it('should throw an error if credential schema is missing', async () => { diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts index 9218afab2..3fd606704 100644 --- a/packages/credentials/tests/verifiable-credential.spec.ts +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -19,6 +19,10 @@ describe('Verifiable Credential Tests', () => { ) {} } + after(() => { + sinon.restore(); + }); + beforeEach(async () => { issuerDid = await DidJwk.create(); }); diff --git a/packages/crypto-aws-kms/tests/utils.spec.ts b/packages/crypto-aws-kms/tests/utils.spec.ts index 59d3732c6..4790fa571 100644 --- a/packages/crypto-aws-kms/tests/utils.spec.ts +++ b/packages/crypto-aws-kms/tests/utils.spec.ts @@ -7,6 +7,10 @@ import { Convert } from '@web5/common'; import { mockEcdsaSecp256k1 } from './fixtures/mock-ecdsa-secp256k1.js'; describe('AWS KMS Utils', () => { + after(() => { + sinon.restore(); + }); + describe('convertSpkiToPublicKey()', () => { it('converts DER-encoded SPKI public key to JWK', () => { // Setup. diff --git a/packages/crypto/tests/local-key-manager.spec.ts b/packages/crypto/tests/local-key-manager.spec.ts index c3f434802..e8032b196 100644 --- a/packages/crypto/tests/local-key-manager.spec.ts +++ b/packages/crypto/tests/local-key-manager.spec.ts @@ -11,6 +11,10 @@ import { LocalKeyManager } from '../src/local-key-manager.js'; describe('LocalKeyManager', () => { let keyManager: LocalKeyManager; + after(() => { + sinon.restore(); + }); + beforeEach(() => { keyManager = new LocalKeyManager(); }); diff --git a/packages/dev-env/docker-compose.yaml b/packages/dev-env/docker-compose.yaml index 541bc2415..5b4e854ad 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.6 + image: ghcr.io/tbd54566975/dwn-server:0.4.8 ports: - "3000:3000" diff --git a/packages/dids/tests/bearer-did.spec.ts b/packages/dids/tests/bearer-did.spec.ts index 01b914ee0..db0790bb8 100644 --- a/packages/dids/tests/bearer-did.spec.ts +++ b/packages/dids/tests/bearer-did.spec.ts @@ -11,6 +11,10 @@ import { BearerDid } from '../src/bearer-did.js'; describe('BearerDid', () => { let portableDid: PortableDid; + after(() => { + sinon.restore(); + }); + beforeEach(() => { portableDid = { uri : 'did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im80MHNoWnJzY28tQ2ZFcWs2bUZzWGZjUDk0bHkzQXozZ204NFB6QVVzWG8iLCJraWQiOiJCRHAweGltODJHc3dseG5QVjhUUHRCZFV3ODB3a0dJRjhnakZidzF4NWlRIiwiYWxnIjoiRWREU0EifQ', diff --git a/packages/dids/tests/methods/did-dht.spec.ts b/packages/dids/tests/methods/did-dht.spec.ts index 5e2e39af9..110bf325d 100644 --- a/packages/dids/tests/methods/did-dht.spec.ts +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -45,6 +45,7 @@ describe('DidDht', () => { afterEach(() => { fetchStub.restore(); + sinon.restore(); }); describe('create()', () => { diff --git a/packages/dids/tests/methods/did-ion.spec.ts b/packages/dids/tests/methods/did-ion.spec.ts index 2e64d5f87..9e8a1a9cd 100644 --- a/packages/dids/tests/methods/did-ion.spec.ts +++ b/packages/dids/tests/methods/did-ion.spec.ts @@ -40,6 +40,7 @@ describe('DidIon', () => { afterEach(() => { fetchStub.restore(); + sinon.restore(); }); describe('create', () => { diff --git a/packages/dids/tests/methods/did-web.spec.ts b/packages/dids/tests/methods/did-web.spec.ts index 24f1681f2..b33b05485 100644 --- a/packages/dids/tests/methods/did-web.spec.ts +++ b/packages/dids/tests/methods/did-web.spec.ts @@ -23,6 +23,10 @@ const fetchOkResponse = (response: any) => ({ }); describe('DidWeb', () => { + after(() => { + sinon.restore(); + }); + describe('resolve()', () => { it(`returns a 'notFound' error if the HTTP GET response is not status code 200`, async () => { // Setup stub so that a mocked response is returned rather than calling over the network. diff --git a/packages/dids/tests/resolver/resolver-cache-level.spec.ts b/packages/dids/tests/resolver/resolver-cache-level.spec.ts index 623535749..beeadc694 100644 --- a/packages/dids/tests/resolver/resolver-cache-level.spec.ts +++ b/packages/dids/tests/resolver/resolver-cache-level.spec.ts @@ -23,6 +23,7 @@ describe('DidResolverCacheLevel', () => { after(() => { clock.restore(); + sinon.restore(); }); describe('constructor', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebf0fabad..bdf87df58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,8 +32,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.7 - version: 0.4.7 + specifier: 0.4.8 + version: 0.4.8 audit-ci: specifier: ^7.0.1 version: 7.1.0 @@ -56,8 +56,8 @@ importers: specifier: 1.2.2 version: 1.2.2 '@tbd54566975/dwn-sdk-js': - specifier: 0.4.5 - version: 0.4.5 + specifier: 0.4.6 + version: 0.4.6 '@web5/common': specifier: 1.0.0 version: 1.0.0 @@ -184,8 +184,8 @@ importers: specifier: 1.45.3 version: 1.45.3 '@tbd54566975/dwn-sdk-js': - specifier: 0.4.5 - version: 0.4.5 + specifier: 0.4.6 + version: 0.4.6 '@types/chai': specifier: 4.3.6 version: 4.3.6 @@ -2100,12 +2100,12 @@ packages: '@sphereon/ssi-types@0.26.0': resolution: {integrity: sha512-r4JQIN7rnPunEv0HvCFC1ZCc9qlWcegYvhJbMJqSvyFE6VhmT5NNdH9jNV9QetgMa0yo5r3k+TnHNv3nH58Dmg==} - '@tbd54566975/dwn-sdk-js@0.4.5': - resolution: {integrity: sha512-UGcq9PX32oQ3sB9LfQms82Ce5secNJXUUe+W163Am2vOAAjJ8AyFG9CaIrXO8HMyEO1yZ7l3bBv57tYM9Zf70A==} + '@tbd54566975/dwn-sdk-js@0.4.6': + resolution: {integrity: sha512-eTd9v2ioT+hYrmob28OgxyLgOPAqJosb8rIAHDpFzEjYlQZSxCEohIZysMrLgWIcSLljyViSFr06mDelRPgGPg==} engines: {node: '>= 18'} - '@tbd54566975/dwn-sql-store@0.6.5': - resolution: {integrity: sha512-ZPdz7Ck7NMNCIOuZv+oxfxrw5lZJI/SukcLVd7PMseWZvT28D/gRcHgt0MfizeE9jLv9+ONI+0k8uc/O2NILyw==} + '@tbd54566975/dwn-sql-store@0.6.6': + resolution: {integrity: sha512-LY8it9npYjI/Kx/aK94gR6/1AfptmRGagUuXOfprm/lUcK3uJ79EReOq8zk7CXyTK66+GAu+oGFzuCoo12EJ1g==} engines: {node: '>=18'} '@tootallnate/quickjs-emscripten@0.23.0': @@ -2503,8 +2503,8 @@ packages: resolution: {integrity: sha512-M9EfsEYcOtYuEvUQjow4vpxXbD0Sz5H8EuDXMtwuvP4UdYL0ATl+60F8+8HDmwPFeUy6M2wxuoixrLDwSRFwZA==} engines: {node: '>=18.0.0'} - '@web5/dwn-server@0.4.7': - resolution: {integrity: sha512-ZCs1Ztc3wiywMEeINAIED/u5CA0qfuH8sr8gDeot1lBvu1viXMu1Ua8YqKc6CABPLXtWFmy5vuXIsf72TD5KHw==} + '@web5/dwn-server@0.4.8': + resolution: {integrity: sha512-Mr+Oq8XTZN133gnQYjhN07sbjVkfdlhsigQhDqThX9ghf2Kk3kiakb+5tlwYgsFCyj8O6sW+UM07443xZS3qLA==} hasBin: true '@webassemblyjs/ast@1.12.1': @@ -7356,7 +7356,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tbd54566975/dwn-sdk-js@0.4.5': + '@tbd54566975/dwn-sdk-js@0.4.6': dependencies: '@ipld/dag-cbor': 9.0.3 '@js-temporal/polyfill': 0.4.4 @@ -7389,10 +7389,10 @@ snapshots: - encoding - supports-color - '@tbd54566975/dwn-sql-store@0.6.5': + '@tbd54566975/dwn-sql-store@0.6.6': dependencies: '@ipld/dag-cbor': 9.0.5 - '@tbd54566975/dwn-sdk-js': 0.4.5 + '@tbd54566975/dwn-sdk-js': 0.4.6 kysely: 0.26.3 multiformats: 12.0.1 readable-stream: 4.4.2 @@ -8342,10 +8342,10 @@ snapshots: level: 8.0.1 ms: 2.1.3 - '@web5/dwn-server@0.4.7': + '@web5/dwn-server@0.4.8': dependencies: - '@tbd54566975/dwn-sdk-js': 0.4.5 - '@tbd54566975/dwn-sql-store': 0.6.5 + '@tbd54566975/dwn-sdk-js': 0.4.6 + '@tbd54566975/dwn-sql-store': 0.6.6 '@web5/crypto': 1.0.3 better-sqlite3: 8.7.0 body-parser: 1.20.2 From 5ac4fe51a2a87a266e95c3db8215059ac2d38e77 Mon Sep 17 00:00:00 2001 From: Liran Cohen <c.liran.c@gmail.com> Date: Wed, 28 Aug 2024 10:50:51 -0400 Subject: [PATCH 3/7] agent did api resolver cache (#855) In some cases, specifically when more than one identity exists, the `connect()` method can take over a minute to load. This is because DHT resolution takes places for each identity in the system when calling `identities.list()`. I won't go into the details of why resolution happens, but it's part of getting the signer to the DWN tenant for each identity. This PR implements @csuwildcat's cache implementation on the `AgentDidApi` class which takes into account which DIDs are managed by the agent (or the agent's own DID). These DIDs are never evicted from the cache, only updated versions are inserted after a successful resolution occurs. Other DIDs are evicted as usual. In addition to this I have increased the `recordId` reference storage cache to 21 days instead of 2 hours. These values don't change, so we should cache them for as long as possible. The max items is 1,000 and it will evict items based on last seen. 21 days was chosen because the maximum allowed timeout in Node is 24 days, I reduced it as a buffer. This should speed up `connect()` time and all signing of messages throughout the system. --- .changeset/kind-deers-burn.md | 9 ++ .github/workflows/alpha-npm.yml | 17 ++- audit-ci.json | 3 +- package.json | 3 +- packages/agent/package.json | 2 +- .../agent/src/agent-did-resolver-cache.ts | 72 +++++++++ packages/agent/src/did-api.ts | 19 ++- packages/agent/src/identity-api.ts | 13 +- packages/agent/src/index.ts | 1 + packages/agent/src/store-data.ts | 10 +- packages/agent/src/test-harness.ts | 5 +- .../tests/agent-did-resolver-cach.spec.ts | 142 ++++++++++++++++++ .../dids/src/resolver/resolver-cache-level.ts | 4 +- .../dids/src/resolver/universal-resolver.ts | 2 +- packages/user-agent/src/user-agent.ts | 3 +- pnpm-lock.yaml | 11 +- 16 files changed, 281 insertions(+), 35 deletions(-) create mode 100644 .changeset/kind-deers-burn.md create mode 100644 packages/agent/src/agent-did-resolver-cache.ts create mode 100644 packages/agent/tests/agent-did-resolver-cach.spec.ts diff --git a/.changeset/kind-deers-burn.md b/.changeset/kind-deers-burn.md new file mode 100644 index 000000000..f70eb6549 --- /dev/null +++ b/.changeset/kind-deers-burn.md @@ -0,0 +1,9 @@ +--- +"@web5/user-agent": patch +"@web5/agent": patch +"@web5/dids": patch +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +--- + +Implement DidResolverCache thats specific to Agent usage diff --git a/.github/workflows/alpha-npm.yml b/.github/workflows/alpha-npm.yml index 1235f307c..a722ea5fd 100644 --- a/.github/workflows/alpha-npm.yml +++ b/.github/workflows/alpha-npm.yml @@ -21,7 +21,8 @@ jobs: env: # Packages not listed here will be excluded from publishing - PACKAGES: "agent api common credentials crypto crypto-aws-kms dids identity-agent proxy-agent user-agent" + # These are currently in a specific order due to dependency requirements + PACKAGES: "crypto crypto-aws-kms common dids credentials agent identity-agent proxy-agent user-agent api" steps: - name: Checkout source @@ -29,7 +30,7 @@ jobs: # https://cashapp.github.io/hermit/usage/ci/ - name: Init Hermit - uses: cashapp/activate-hermit@31ce88b17a84941bb1b782f1b7b317856addf286 #v1.1.0 + uses: cashapp/activate-hermit@v1 with: cache: "true" @@ -63,11 +64,17 @@ jobs: node ./scripts/bump-workspace.mjs --prerelease=$ALPHA_PRERELEASE shell: bash - - name: Build all workspace packages + - name: Build all workspace packages sequentially env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} - run: pnpm --recursive --stream build - + run: | + for package in $PACKAGES; do + cd packages/$package + pnpm build + cd ../.. + done + shell: bash + - name: Publish selected @web5/* packages env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/audit-ci.json b/audit-ci.json index 0311fe9e4..12d9d2333 100644 --- a/audit-ci.json +++ b/audit-ci.json @@ -6,6 +6,7 @@ "mysql2", "braces", "GHSA-rv95-896h-c2vc", - "GHSA-952p-6rrq-rcjv" + "GHSA-952p-6rrq-rcjv", + "GHSA-4vvj-4cpr-p986" ] } \ No newline at end of file diff --git a/package.json b/package.json index 12c283353..db1067aaa 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "elliptic@>=4.0.0 <=6.5.6": ">=6.5.7", "elliptic@>=2.0.0 <=6.5.6": ">=6.5.7", "elliptic@>=5.2.1 <=6.5.6": ">=6.5.7", - "micromatch@<4.0.8": ">=4.0.8" + "micromatch@<4.0.8": ">=4.0.8", + "webpack@<5.94.0": ">=5.94.0" } } } diff --git a/packages/agent/package.json b/packages/agent/package.json index eb67702d6..3c04ac5c2 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -74,7 +74,7 @@ "@tbd54566975/dwn-sdk-js": "0.4.6", "@web5/common": "1.0.0", "@web5/crypto": "workspace:*", - "@web5/dids": "1.1.0", + "@web5/dids": "workspace:*", "abstract-level": "1.0.4", "ed25519-keygen": "0.4.11", "isomorphic-ws": "^5.0.0", diff --git a/packages/agent/src/agent-did-resolver-cache.ts b/packages/agent/src/agent-did-resolver-cache.ts new file mode 100644 index 000000000..ecf35b76b --- /dev/null +++ b/packages/agent/src/agent-did-resolver-cache.ts @@ -0,0 +1,72 @@ +import { DidResolutionResult, DidResolverCache, DidResolverCacheLevel, DidResolverCacheLevelParams } from '@web5/dids'; +import { Web5PlatformAgent } from './types/agent.js'; + + +/** + * AgentDidResolverCache keeps a stale copy of the Agent's managed Identity DIDs and only evicts and refreshes upon a successful resolution. + * This allows for quick and offline access to the internal DIDs used by the agent. + */ +export class AgentDidResolverCache extends DidResolverCacheLevel implements DidResolverCache { + + /** + * Holds the instance of a `Web5PlatformAgent` that represents the current execution context for + * the `AgentDidApi`. This agent is used to interact with other Web5 agent components. It's vital + * to ensure this instance is set to correctly contextualize operations within the broader Web5 + * Agent framework. + */ + private _agent?: Web5PlatformAgent; + + /** A map of DIDs that are currently in-flight. This helps avoid going into an infinite loop */ + private _resolving: Map<string, boolean> = new Map(); + + constructor({ agent, db, location, ttl }: DidResolverCacheLevelParams & { agent?: Web5PlatformAgent }) { + super ({ db, location, ttl }); + this._agent = agent; + } + + get agent() { + if (!this._agent) { + throw new Error('Agent not initialized'); + } + return this._agent; + } + + set agent(agent: Web5PlatformAgent) { + this._agent = agent; + } + + /** + * Get the DID resolution result from the cache for the given DID. + * + * If the DID is managed by the agent, or is the agent's own DID, it will not evict it from the cache until a new resolution is successful. + * This is done to achieve quick and offline access to the agent's own managed DIDs. + */ + async get(did: string): Promise<DidResolutionResult | void> { + try { + const str = await this.cache.get(did); + const cachedResult = JSON.parse(str); + if (!this._resolving.has(did) && Date.now() >= cachedResult.ttlMillis) { + this._resolving.set(did, true); + if (this.agent.agentDid.uri === did || 'undefined' !== typeof await this.agent.identity.get({ didUri: did })) { + try { + const result = await this.agent.did.resolve(did); + if (!result.didResolutionMetadata.error) { + this.set(did, result); + } + } finally { + this._resolving.delete(did); + } + } else { + this._resolving.delete(did); + this.cache.nextTick(() => this.cache.del(did)); + } + } + return cachedResult.value; + } catch(error: any) { + if (error.notFound) { + return; + } + throw error; + } + } +} \ No newline at end of file diff --git a/packages/agent/src/did-api.ts b/packages/agent/src/did-api.ts index 8b533b635..4a17d57af 100644 --- a/packages/agent/src/did-api.ts +++ b/packages/agent/src/did-api.ts @@ -3,12 +3,12 @@ import type { DidMetadata, PortableDid, DidMethodApi, - DidResolverCache, DidDhtCreateOptions, DidJwkCreateOptions, DidResolutionResult, DidResolutionOptions, DidVerificationMethod, + DidResolverCache, } from '@web5/dids'; import { BearerDid, Did, UniversalResolver } from '@web5/dids'; @@ -18,7 +18,7 @@ import type { AgentKeyManager } from './types/key-manager.js'; import type { ResponseStatus, Web5PlatformAgent } from './types/agent.js'; import { InMemoryDidStore } from './store-did.js'; -import { DidResolverCacheMemory } from './prototyping/dids/resolver-cache-memory.js'; +import { AgentDidResolverCache } from './agent-did-resolver-cache.js'; export enum DidInterface { Create = 'Create', @@ -87,8 +87,10 @@ export interface DidApiParams { * An optional `DidResolverCache` instance used for caching resolved DID documents. * * Providing a cache implementation can significantly enhance resolution performance by avoiding - * redundant resolutions for previously resolved DIDs. If omitted, a no-operation cache is used, - * which effectively disables caching. + * redundant resolutions for previously resolved DIDs. If omitted, the default is an instance of `AgentDidResolverCache`. + * + * `AgentDidResolverCache` keeps a stale copy of the Agent's managed Identity DIDs and only refreshes upon a successful resolution. + * This allows for quick and offline access to the internal DIDs used by the agent. */ resolverCache?: DidResolverCache; @@ -120,10 +122,10 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager> } // Initialize the DID resolver with the given DID methods and resolver cache, or use a default - // in-memory cache if none is provided. + // AgentDidResolverCache if none is provided. super({ didResolvers : didMethods, - cache : resolverCache ?? new DidResolverCacheMemory() + cache : resolverCache ?? new AgentDidResolverCache({ agent, location: 'DATA/AGENT/DID_CACHE' }) }); this._agent = agent; @@ -152,6 +154,11 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager> set agent(agent: Web5PlatformAgent) { this._agent = agent; + + // AgentDidResolverCache should set the agent if it is the type of cache being used + if ('agent' in this.cache) { + this.cache.agent = agent; + } } public async create({ diff --git a/packages/agent/src/identity-api.ts b/packages/agent/src/identity-api.ts index c64ef46ef..b857a67e6 100644 --- a/packages/agent/src/identity-api.ts +++ b/packages/agent/src/identity-api.ts @@ -183,14 +183,13 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana // Retrieve the list of Identities from the Agent's Identity store. const storedIdentities = await this._store.list({ agent: this.agent, tenant }); - const identities: BearerIdentity[] = []; + const identities = await Promise.all( + storedIdentities.map(async metadata => { + return this.get({ didUri: metadata.uri, tenant: metadata.tenant }); + }) + ); - for (const metadata of storedIdentities) { - const identity = await this.get({ didUri: metadata.uri, tenant: metadata.tenant }); - identities.push(identity!); - } - - return identities; + return identities.filter(identity => typeof identity !== 'undefined') as BearerIdentity[]; } public async manage({ portableIdentity }: { diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 5ed8caa62..7f7457575 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -7,6 +7,7 @@ export type * from './types/permissions.js'; export type * from './types/sync.js'; export type * from './types/vc.js'; +export * from './agent-did-resolver-cache.js'; export * from './bearer-identity.js'; export * from './cached-permissions.js'; export * from './crypto-api.js'; diff --git a/packages/agent/src/store-data.ts b/packages/agent/src/store-data.ts index 6873eaa0e..7d5699022 100644 --- a/packages/agent/src/store-data.ts +++ b/packages/agent/src/store-data.ts @@ -55,16 +55,20 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem /** * Index for mappings from Store Identifier to DWN record ID. + * Since these values don't change, we can use a long TTL. * - * Up to 1,000 entries are retained for 2 hours. + * Up to 1,000 entries are retained for 21 days. + * NOTE: The maximum number for the ttl is 2^31 - 1 milliseconds (24.8 days), setting to 21 days to be safe. */ - protected _index = new TtlCache<string, string>({ ttl: ms('2 hours'), max: 1000 }); + protected _index = new TtlCache<string, string>({ ttl: ms('21 days'), max: 1000 }); /** * Cache of tenant DIDs that have been initialized with the protocol. * This is used to avoid redundant protocol initialization requests. + * + * Since these are default protocols and unlikely to change, we can use a long TTL. */ - protected _protocolInitializedCache: TtlCache<string, boolean> = new TtlCache({ ttl: ms('1 hour'), max: 1000 }); + protected _protocolInitializedCache: TtlCache<string, boolean> = new TtlCache({ ttl: ms('21 days'), max: 1000 }); /** * The protocol assigned to this storage instance. diff --git a/packages/agent/src/test-harness.ts b/packages/agent/src/test-harness.ts index e17dc8df0..ec2e89d4d 100644 --- a/packages/agent/src/test-harness.ts +++ b/packages/agent/src/test-harness.ts @@ -4,11 +4,12 @@ import type { AbstractLevel } from 'abstract-level'; import { Level } from 'level'; import { LevelStore, MemoryStore } from '@web5/common'; import { DataStoreLevel, Dwn, EventEmitterStream, EventLogLevel, MessageStoreLevel, ResumableTaskStoreLevel } from '@tbd54566975/dwn-sdk-js'; -import { DidDht, DidJwk, DidResolutionResult, DidResolverCache, DidResolverCacheLevel } from '@web5/dids'; +import { DidDht, DidJwk, DidResolutionResult, DidResolverCache } from '@web5/dids'; import type { Web5PlatformAgent } from './types/agent.js'; import { AgentDidApi } from './did-api.js'; +import { AgentDidResolverCache } from './agent-did-resolver-cache.js'; import { AgentDwnApi } from './dwn-api.js'; import { AgentSyncApi } from './sync-api.js'; import { Web5RpcClient } from './rpc-client.js'; @@ -287,7 +288,7 @@ export class PlatformAgentTestHarness { const { didStore, identityStore, keyStore } = stores; // Setup DID Resolver Cache - const didResolverCache = new DidResolverCacheLevel({ + const didResolverCache = new AgentDidResolverCache({ location: testDataPath('DID_RESOLVERCACHE') }); diff --git a/packages/agent/tests/agent-did-resolver-cach.spec.ts b/packages/agent/tests/agent-did-resolver-cach.spec.ts new file mode 100644 index 000000000..37b7536b0 --- /dev/null +++ b/packages/agent/tests/agent-did-resolver-cach.spec.ts @@ -0,0 +1,142 @@ +import { AgentDidResolverCache } from '../src/agent-did-resolver-cache.js'; +import { PlatformAgentTestHarness } from '../src/test-harness.js'; +import { TestAgent } from './utils/test-agent.js'; + +import sinon from 'sinon'; +import { expect } from 'chai'; +import { DidJwk } from '@web5/dids'; +import { BearerIdentity } from '../src/bearer-identity.js'; + +describe('AgentDidResolverCache', () => { + let resolverCache: AgentDidResolverCache; + let testHarness: PlatformAgentTestHarness; + + before(async () => { + testHarness = await PlatformAgentTestHarness.setup({ + agentClass : TestAgent, + agentStores : 'dwn' + }); + + resolverCache = new AgentDidResolverCache({ agent: testHarness.agent, location: '__TESTDATA__/did_cache' }); + }); + + after(async () => { + sinon.restore(); + await testHarness.clearStorage(); + await testHarness.closeStorage(); + }); + + beforeEach(async () => { + sinon.restore(); + await testHarness.clearStorage(); + await testHarness.createAgentDid(); + }); + + it('does not attempt to resolve a DID that is already resolving', async () => { + const did = testHarness.agent.agentDid.uri; + const getStub = sinon.stub(resolverCache['cache'], 'get').resolves(JSON.stringify({ ttlMillis: Date.now() - 1000, value: { didDocument: { id: did } } })); + const resolveSpy = sinon.spy(testHarness.agent.did, 'resolve'); + + await Promise.all([ + resolverCache.get(did), + resolverCache.get(did) + ]); + + // get should be called twice, but resolve should only be called once + // because the second call should be blocked by the _resolving Map + expect(getStub.callCount).to.equal(2); + expect(resolveSpy.callCount).to.equal(1); + }); + + it('should not resolve a DID if the ttl has not elapsed', async () => { + const did = testHarness.agent.agentDid.uri; + const getStub = sinon.stub(resolverCache['cache'], 'get').resolves(JSON.stringify({ ttlMillis: Date.now() + 1000, value: { didDocument: { id: did } } })); + const resolveSpy = sinon.spy(testHarness.agent.did, 'resolve'); + + await resolverCache.get(did); + + // get should be called once, but resolve should not be called + expect(getStub.callCount).to.equal(1); + expect(resolveSpy.callCount).to.equal(0); + }); + + it('should not call resolve if the DID is not the agent DID or exists as an identity in the agent', async () => { + const did = await DidJwk.create({}); + const getStub = sinon.stub(resolverCache['cache'], 'get').resolves(JSON.stringify({ ttlMillis: Date.now() - 1000, value: { didDocument: { id: did.uri } } })); + const resolveSpy = sinon.spy(testHarness.agent.did, 'resolve'); + const nextTickSpy = sinon.stub(resolverCache['cache'], 'nextTick').resolves(); + sinon.stub(testHarness.agent.identity, 'get').resolves(undefined); + + await resolverCache.get(did.uri), + + // get should be called once, but we do not resolve even though the TTL is expired + expect(getStub.callCount).to.equal(1); + expect(resolveSpy.callCount).to.equal(0); + + // we expect the nextTick of the cache to be called to trigger a delete of the cache item after returning as it's expired + expect(nextTickSpy.callCount).to.equal(1); + }); + + it('should resolve if the DID is managed by the agent', async () => { + const did = await DidJwk.create({}); + const getStub = sinon.stub(resolverCache['cache'], 'get').resolves(JSON.stringify({ ttlMillis: Date.now() - 1000, value: { didDocument: { id: did.uri } } })); + const resolveSpy = sinon.spy(testHarness.agent.did, 'resolve'); + const nextTickSpy = sinon.stub(resolverCache['cache'], 'nextTick').resolves(); + sinon.stub(testHarness.agent.identity, 'get').resolves(new BearerIdentity({ + metadata: { name: 'Some Name', uri: did.uri, tenant: did.uri }, + did, + })); + + await resolverCache.get(did.uri), + + // get should be called once, and we also resolve the DId as it's returned by the identity.get method + expect(getStub.callCount).to.equal(1); + expect(resolveSpy.callCount).to.equal(1); + }); + + it('does not cache notFound records', async () => { + const did = testHarness.agent.agentDid.uri; + const getStub = sinon.stub(resolverCache['cache'], 'get').rejects({ notFound: true }); + + const result = await resolverCache.get(did); + + // get should be called once, and resolve should be called once + expect(getStub.callCount).to.equal(1); + expect(result).to.equal(undefined); + }); + + it('throws if the error is anything other than a notFound error', async () => { + const did = testHarness.agent.agentDid.uri; + const getStub = sinon.stub(resolverCache['cache'], 'get').rejects(new Error('Some Error')); + + try { + await resolverCache.get(did); + expect.fail('Should have thrown'); + } catch (error: any) { + expect(error.message).to.equal('Some Error'); + } + }); + + it('throws if the agent is not initialized', async () => { + // close existing DB + await resolverCache['cache'].close(); + + // set resolver cache without an agent + resolverCache = new AgentDidResolverCache({ location: '__TESTDATA__/did_cache' }); + + try { + // attempt to access the agent property + resolverCache.agent; + + expect.fail('Should have thrown'); + } catch (error: any) { + expect(error.message).to.equal('Agent not initialized'); + } + + // set the agent property + resolverCache.agent = testHarness.agent; + + // should not throw + resolverCache.agent; + }); +}); \ No newline at end of file diff --git a/packages/dids/src/resolver/resolver-cache-level.ts b/packages/dids/src/resolver/resolver-cache-level.ts index 69481e143..c4d2f2f80 100644 --- a/packages/dids/src/resolver/resolver-cache-level.ts +++ b/packages/dids/src/resolver/resolver-cache-level.ts @@ -73,10 +73,10 @@ type CachedDidResolutionResult = { */ export class DidResolverCacheLevel implements DidResolverCache { /** The underlying LevelDB store used for caching. */ - private cache; + protected cache; /** The time-to-live for cache entries in milliseconds. */ - private ttl: number; + protected ttl: number; constructor({ db, diff --git a/packages/dids/src/resolver/universal-resolver.ts b/packages/dids/src/resolver/universal-resolver.ts index 93ff11fb8..e938613ff 100644 --- a/packages/dids/src/resolver/universal-resolver.ts +++ b/packages/dids/src/resolver/universal-resolver.ts @@ -66,7 +66,7 @@ export class UniversalResolver implements DidResolver, DidUrlDereferencer { /** * A cache for storing resolved DID documents. */ - private cache: DidResolverCache; + protected cache: DidResolverCache; /** * A map to store method resolvers against method names. diff --git a/packages/user-agent/src/user-agent.ts b/packages/user-agent/src/user-agent.ts index 426a0668d..9568959fb 100644 --- a/packages/user-agent/src/user-agent.ts +++ b/packages/user-agent/src/user-agent.ts @@ -12,6 +12,7 @@ import { ProcessDwnRequest, Web5PlatformAgent, AgentPermissionsApi, + AgentDidResolverCache, } from '@web5/agent'; import { LevelStore } from '@web5/common'; @@ -152,7 +153,7 @@ export class Web5UserAgent<TKeyManager extends AgentKeyManager = LocalKeyManager didApi ??= new AgentDidApi({ didMethods : [DidDht, DidJwk], - resolverCache : new DidResolverCacheLevel({ location: `${dataPath}/DID_RESOLVERCACHE` }), + resolverCache : new AgentDidResolverCache({ location: `${dataPath}/DID_RESOLVERCACHE` }), store : new DwnDidStore() }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdf87df58..f79114ca2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,7 @@ overrides: elliptic@>=2.0.0 <=6.5.6: '>=6.5.7' elliptic@>=5.2.1 <=6.5.6: '>=6.5.7' micromatch@<4.0.8: '>=4.0.8' + webpack@<5.94.0: '>=5.94.0' importers: @@ -65,8 +66,8 @@ importers: specifier: workspace:* version: link:../crypto '@web5/dids': - specifier: 1.1.0 - version: 1.1.0 + specifier: workspace:* + version: link:../dids abstract-level: specifier: 1.0.4 version: 1.0.4 @@ -5323,13 +5324,13 @@ packages: resolution: {integrity: sha512-oYwAqCuL0OZhBoSgmdrLa7mv9MjommVMiQIWgcztf+eS4+8BfcUee6nenFnDhKOhzAVnk5gpZdfnz1iiBv+5sg==} engines: {node: '>= 14.15.0'} peerDependencies: - webpack: ^5.72.1 + webpack: '>=5.94.0' source-map-loader@5.0.0: resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==} engines: {node: '>= 18.12.0'} peerDependencies: - webpack: ^5.72.1 + webpack: '>=5.94.0' source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -5507,7 +5508,7 @@ packages: '@swc/core': '*' esbuild: '*' uglify-js: '*' - webpack: ^5.1.0 + webpack: '>=5.94.0' peerDependenciesMeta: '@swc/core': optional: true From d5acb5639687b99ac21f60eb0906af4a3d3707bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 11:01:11 -0400 Subject: [PATCH 4/7] Version Packages (#848) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- .changeset/blue-roses-cough.md | 8 -------- .changeset/fresh-olives-dance.md | 5 ----- .changeset/friendly-carrots-return.md | 12 ------------ .changeset/kind-deers-burn.md | 9 --------- .changeset/polite-days-wash.md | 8 -------- .changeset/silly-poets-sing.md | 8 -------- .changeset/spicy-forks-attack.md | 5 ----- .changeset/warm-starfishes-beam.md | 8 -------- packages/agent/CHANGELOG.md | 26 ++++++++++++++++++++++++++ packages/agent/package.json | 2 +- packages/credentials/CHANGELOG.md | 10 ++++++++++ packages/credentials/package.json | 2 +- packages/crypto-aws-kms/CHANGELOG.md | 9 +++++++++ packages/crypto-aws-kms/package.json | 2 +- packages/crypto/CHANGELOG.md | 6 ++++++ packages/crypto/package.json | 2 +- packages/dids/CHANGELOG.md | 11 +++++++++++ packages/dids/package.json | 2 +- packages/identity-agent/CHANGELOG.md | 23 +++++++++++++++++++++++ packages/identity-agent/package.json | 2 +- packages/proxy-agent/CHANGELOG.md | 23 +++++++++++++++++++++++ packages/proxy-agent/package.json | 2 +- packages/user-agent/CHANGELOG.md | 23 +++++++++++++++++++++++ packages/user-agent/package.json | 2 +- 24 files changed, 139 insertions(+), 71 deletions(-) delete mode 100644 .changeset/blue-roses-cough.md delete mode 100644 .changeset/fresh-olives-dance.md delete mode 100644 .changeset/friendly-carrots-return.md delete mode 100644 .changeset/kind-deers-burn.md delete mode 100644 .changeset/polite-days-wash.md delete mode 100644 .changeset/silly-poets-sing.md delete mode 100644 .changeset/spicy-forks-attack.md delete mode 100644 .changeset/warm-starfishes-beam.md diff --git a/.changeset/blue-roses-cough.md b/.changeset/blue-roses-cough.md deleted file mode 100644 index 60f3b5668..000000000 --- a/.changeset/blue-roses-cough.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@web5/agent": minor -"@web5/identity-agent": minor -"@web5/proxy-agent": minor -"@web5/user-agent": minor ---- - -Add ability to Sync a subset of protocols as a delegate diff --git a/.changeset/fresh-olives-dance.md b/.changeset/fresh-olives-dance.md deleted file mode 100644 index e611f3362..000000000 --- a/.changeset/fresh-olives-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@web5/agent": patch ---- - -Add requestPermissionsForProtocol helper method to connect module diff --git a/.changeset/friendly-carrots-return.md b/.changeset/friendly-carrots-return.md deleted file mode 100644 index 708482b2d..000000000 --- a/.changeset/friendly-carrots-return.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@web5/crypto-aws-kms": patch -"@web5/identity-agent": patch -"@web5/credentials": patch -"@web5/proxy-agent": patch -"@web5/user-agent": patch -"@web5/crypto": patch -"@web5/agent": patch -"@web5/dids": patch ---- - -cleanup crypto utils diff --git a/.changeset/kind-deers-burn.md b/.changeset/kind-deers-burn.md deleted file mode 100644 index f70eb6549..000000000 --- a/.changeset/kind-deers-burn.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@web5/user-agent": patch -"@web5/agent": patch -"@web5/dids": patch -"@web5/identity-agent": patch -"@web5/proxy-agent": patch ---- - -Implement DidResolverCache thats specific to Agent usage diff --git a/.changeset/polite-days-wash.md b/.changeset/polite-days-wash.md deleted file mode 100644 index 0ea7caf24..000000000 --- a/.changeset/polite-days-wash.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@web5/identity-agent": patch -"@web5/proxy-agent": patch -"@web5/user-agent": patch -"@web5/agent": patch ---- - -Introduce a `PermissionsApi` for Web5Agents diff --git a/.changeset/silly-poets-sing.md b/.changeset/silly-poets-sing.md deleted file mode 100644 index 16cdbf118..000000000 --- a/.changeset/silly-poets-sing.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@web5/agent": minor -"@web5/identity-agent": minor -"@web5/proxy-agent": minor -"@web5/user-agent": minor ---- - -Simplify support for Permission Grant logic within agent. diff --git a/.changeset/spicy-forks-attack.md b/.changeset/spicy-forks-attack.md deleted file mode 100644 index bddd07abb..000000000 --- a/.changeset/spicy-forks-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@web5/agent": patch ---- - -integrate dwn grants into connect flow diff --git a/.changeset/warm-starfishes-beam.md b/.changeset/warm-starfishes-beam.md deleted file mode 100644 index de8f4fd84..000000000 --- a/.changeset/warm-starfishes-beam.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@web5/agent": patch -"@web5/identity-agent": patch -"@web5/proxy-agent": patch -"@web5/user-agent": patch ---- - -Upgrade `dwn-sdk-js` version and `dwn-server` dependency diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index f5c3382b0..453270db9 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,31 @@ # @web5/agent +## 0.6.0 + +### Minor Changes + +- [#836](https://github.com/TBD54566975/web5-js/pull/836) [`3d1f825`](https://github.com/TBD54566975/web5-js/commit/3d1f8258465e1177d6d5fe17c2e1ea786baa331d) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Add ability to Sync a subset of protocols as a delegate + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Simplify support for Permission Grant logic within agent. + +### Patch Changes + +- [#854](https://github.com/TBD54566975/web5-js/pull/854) [`4ff2316`](https://github.com/TBD54566975/web5-js/commit/4ff2316e28ad3f29f0336c69adde0a37840ebb33) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Add requestPermissionsForProtocol helper method to connect module + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + +- [#855](https://github.com/TBD54566975/web5-js/pull/855) [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Implement DidResolverCache thats specific to Agent usage + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Introduce a `PermissionsApi` for Web5Agents + +- [#850](https://github.com/TBD54566975/web5-js/pull/850) [`fea0535`](https://github.com/TBD54566975/web5-js/commit/fea0535876d5b00ab5e0abea695e52aeaad9d78b) Thanks [@shamilovtim](https://github.com/shamilovtim)! - integrate dwn grants into connect flow + +- [#853](https://github.com/TBD54566975/web5-js/pull/853) [`7347438`](https://github.com/TBD54566975/web5-js/commit/734743884f21491efce9392e129d612735e85282) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Upgrade `dwn-sdk-js` version and `dwn-server` dependency + +- Updated dependencies [[`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8), [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77)]: + - @web5/crypto@1.0.4 + - @web5/dids@1.1.4 + ## 0.5.1 ### Patch Changes diff --git a/packages/agent/package.json b/packages/agent/package.json index 3c04ac5c2..3de0b2c93 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/agent", - "version": "0.5.1", + "version": "0.6.0", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/credentials/CHANGELOG.md b/packages/credentials/CHANGELOG.md index 9eff6091f..7441cf2a1 100644 --- a/packages/credentials/CHANGELOG.md +++ b/packages/credentials/CHANGELOG.md @@ -1,5 +1,15 @@ # @web5/credentials +## 1.1.1 + +### Patch Changes + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + +- Updated dependencies [[`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8), [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77)]: + - @web5/crypto@1.0.4 + - @web5/dids@1.1.4 + ## 1.1.0 ### Minor Changes diff --git a/packages/credentials/package.json b/packages/credentials/package.json index 0b18c1e7a..9cb28426a 100644 --- a/packages/credentials/package.json +++ b/packages/credentials/package.json @@ -1,6 +1,6 @@ { "name": "@web5/credentials", - "version": "1.1.0", + "version": "1.1.1", "description": "Verifiable Credentials", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/crypto-aws-kms/CHANGELOG.md b/packages/crypto-aws-kms/CHANGELOG.md index 4048d660a..4224f24fa 100644 --- a/packages/crypto-aws-kms/CHANGELOG.md +++ b/packages/crypto-aws-kms/CHANGELOG.md @@ -1,5 +1,14 @@ # @web5/crypto-aws-kms +## 1.0.4 + +### Patch Changes + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + +- Updated dependencies [[`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8)]: + - @web5/crypto@1.0.4 + ## 1.0.3 ### Patch Changes diff --git a/packages/crypto-aws-kms/package.json b/packages/crypto-aws-kms/package.json index 6585db339..c494db7b1 100644 --- a/packages/crypto-aws-kms/package.json +++ b/packages/crypto-aws-kms/package.json @@ -1,6 +1,6 @@ { "name": "@web5/crypto-aws-kms", - "version": "1.0.3", + "version": "1.0.4", "description": "Web5 cryptographic library using AWS KMS", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/crypto/CHANGELOG.md b/packages/crypto/CHANGELOG.md index 6ccd7d773..e946a2383 100644 --- a/packages/crypto/CHANGELOG.md +++ b/packages/crypto/CHANGELOG.md @@ -1,5 +1,11 @@ # @web5/crypto +## 1.0.4 + +### Patch Changes + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + ## 1.0.3 ### Patch Changes diff --git a/packages/crypto/package.json b/packages/crypto/package.json index f2ebecf10..309ef1ae0 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@web5/crypto", - "version": "1.0.3", + "version": "1.0.4", "description": "Web5 cryptographic library", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/dids/CHANGELOG.md b/packages/dids/CHANGELOG.md index c2e92457b..c598555db 100644 --- a/packages/dids/CHANGELOG.md +++ b/packages/dids/CHANGELOG.md @@ -1,5 +1,16 @@ # @web5/dids +## 1.1.4 + +### Patch Changes + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + +- [#855](https://github.com/TBD54566975/web5-js/pull/855) [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Implement DidResolverCache thats specific to Agent usage + +- Updated dependencies [[`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8)]: + - @web5/crypto@1.0.4 + ## 1.1.3 ### Patch Changes diff --git a/packages/dids/package.json b/packages/dids/package.json index 5188fe13b..88cab6639 100644 --- a/packages/dids/package.json +++ b/packages/dids/package.json @@ -1,6 +1,6 @@ { "name": "@web5/dids", - "version": "1.1.3", + "version": "1.1.4", "description": "TBD DIDs library", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/identity-agent/CHANGELOG.md b/packages/identity-agent/CHANGELOG.md index 6fba270d5..f83be6445 100644 --- a/packages/identity-agent/CHANGELOG.md +++ b/packages/identity-agent/CHANGELOG.md @@ -1,5 +1,28 @@ # @web5/identity-agent +## 0.5.0 + +### Minor Changes + +- [#836](https://github.com/TBD54566975/web5-js/pull/836) [`3d1f825`](https://github.com/TBD54566975/web5-js/commit/3d1f8258465e1177d6d5fe17c2e1ea786baa331d) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Add ability to Sync a subset of protocols as a delegate + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Simplify support for Permission Grant logic within agent. + +### Patch Changes + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + +- [#855](https://github.com/TBD54566975/web5-js/pull/855) [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Implement DidResolverCache thats specific to Agent usage + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Introduce a `PermissionsApi` for Web5Agents + +- [#853](https://github.com/TBD54566975/web5-js/pull/853) [`7347438`](https://github.com/TBD54566975/web5-js/commit/734743884f21491efce9392e129d612735e85282) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Upgrade `dwn-sdk-js` version and `dwn-server` dependency + +- Updated dependencies [[`3d1f825`](https://github.com/TBD54566975/web5-js/commit/3d1f8258465e1177d6d5fe17c2e1ea786baa331d), [`4ff2316`](https://github.com/TBD54566975/web5-js/commit/4ff2316e28ad3f29f0336c69adde0a37840ebb33), [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8), [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77), [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079), [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079), [`fea0535`](https://github.com/TBD54566975/web5-js/commit/fea0535876d5b00ab5e0abea695e52aeaad9d78b), [`7347438`](https://github.com/TBD54566975/web5-js/commit/734743884f21491efce9392e129d612735e85282)]: + - @web5/agent@0.6.0 + - @web5/crypto@1.0.4 + - @web5/dids@1.1.4 + ## 0.4.3 ### Patch Changes diff --git a/packages/identity-agent/package.json b/packages/identity-agent/package.json index dc5cdd4b1..17230bdf3 100644 --- a/packages/identity-agent/package.json +++ b/packages/identity-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/identity-agent", - "version": "0.4.3", + "version": "0.5.0", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/proxy-agent/CHANGELOG.md b/packages/proxy-agent/CHANGELOG.md index 4fb1cdb65..d35ae97f1 100644 --- a/packages/proxy-agent/CHANGELOG.md +++ b/packages/proxy-agent/CHANGELOG.md @@ -1,5 +1,28 @@ # @web5/proxy-agent +## 0.5.0 + +### Minor Changes + +- [#836](https://github.com/TBD54566975/web5-js/pull/836) [`3d1f825`](https://github.com/TBD54566975/web5-js/commit/3d1f8258465e1177d6d5fe17c2e1ea786baa331d) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Add ability to Sync a subset of protocols as a delegate + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Simplify support for Permission Grant logic within agent. + +### Patch Changes + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + +- [#855](https://github.com/TBD54566975/web5-js/pull/855) [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Implement DidResolverCache thats specific to Agent usage + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Introduce a `PermissionsApi` for Web5Agents + +- [#853](https://github.com/TBD54566975/web5-js/pull/853) [`7347438`](https://github.com/TBD54566975/web5-js/commit/734743884f21491efce9392e129d612735e85282) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Upgrade `dwn-sdk-js` version and `dwn-server` dependency + +- Updated dependencies [[`3d1f825`](https://github.com/TBD54566975/web5-js/commit/3d1f8258465e1177d6d5fe17c2e1ea786baa331d), [`4ff2316`](https://github.com/TBD54566975/web5-js/commit/4ff2316e28ad3f29f0336c69adde0a37840ebb33), [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8), [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77), [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079), [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079), [`fea0535`](https://github.com/TBD54566975/web5-js/commit/fea0535876d5b00ab5e0abea695e52aeaad9d78b), [`7347438`](https://github.com/TBD54566975/web5-js/commit/734743884f21491efce9392e129d612735e85282)]: + - @web5/agent@0.6.0 + - @web5/crypto@1.0.4 + - @web5/dids@1.1.4 + ## 0.4.3 ### Patch Changes diff --git a/packages/proxy-agent/package.json b/packages/proxy-agent/package.json index 2ab8aa134..150488ac6 100644 --- a/packages/proxy-agent/package.json +++ b/packages/proxy-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/proxy-agent", - "version": "0.4.3", + "version": "0.5.0", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/user-agent/CHANGELOG.md b/packages/user-agent/CHANGELOG.md index 6c8040f71..a5d793a7b 100644 --- a/packages/user-agent/CHANGELOG.md +++ b/packages/user-agent/CHANGELOG.md @@ -1,5 +1,28 @@ # @web5/user-agent +## 0.5.0 + +### Minor Changes + +- [#836](https://github.com/TBD54566975/web5-js/pull/836) [`3d1f825`](https://github.com/TBD54566975/web5-js/commit/3d1f8258465e1177d6d5fe17c2e1ea786baa331d) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Add ability to Sync a subset of protocols as a delegate + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Simplify support for Permission Grant logic within agent. + +### Patch Changes + +- [#838](https://github.com/TBD54566975/web5-js/pull/838) [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8) Thanks [@shamilovtim](https://github.com/shamilovtim)! - cleanup crypto utils + +- [#855](https://github.com/TBD54566975/web5-js/pull/855) [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Implement DidResolverCache thats specific to Agent usage + +- [#824](https://github.com/TBD54566975/web5-js/pull/824) [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Introduce a `PermissionsApi` for Web5Agents + +- [#853](https://github.com/TBD54566975/web5-js/pull/853) [`7347438`](https://github.com/TBD54566975/web5-js/commit/734743884f21491efce9392e129d612735e85282) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Upgrade `dwn-sdk-js` version and `dwn-server` dependency + +- Updated dependencies [[`3d1f825`](https://github.com/TBD54566975/web5-js/commit/3d1f8258465e1177d6d5fe17c2e1ea786baa331d), [`4ff2316`](https://github.com/TBD54566975/web5-js/commit/4ff2316e28ad3f29f0336c69adde0a37840ebb33), [`2d0b423`](https://github.com/TBD54566975/web5-js/commit/2d0b423b90546c8d28735b600328c8a5937b57c8), [`5ac4fe5`](https://github.com/TBD54566975/web5-js/commit/5ac4fe51a2a87a266e95c3db8215059ac2d38e77), [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079), [`0862ffc`](https://github.com/TBD54566975/web5-js/commit/0862ffc1f23550406ff592e8393cb2eb5a769079), [`fea0535`](https://github.com/TBD54566975/web5-js/commit/fea0535876d5b00ab5e0abea695e52aeaad9d78b), [`7347438`](https://github.com/TBD54566975/web5-js/commit/734743884f21491efce9392e129d612735e85282)]: + - @web5/agent@0.6.0 + - @web5/crypto@1.0.4 + - @web5/dids@1.1.4 + ## 0.4.3 ### Patch Changes diff --git a/packages/user-agent/package.json b/packages/user-agent/package.json index 7e57243ff..851b89507 100644 --- a/packages/user-agent/package.json +++ b/packages/user-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/user-agent", - "version": "0.4.3", + "version": "0.5.0", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From e3408c7ef0b9e2a2fa87d4b7768a6e25bf5ff8fe Mon Sep 17 00:00:00 2001 From: Liran Cohen <c.liran.c@gmail.com> Date: Thu, 29 Aug 2024 13:09:20 -0400 Subject: [PATCH 5/7] Update connect flow (#856) There were a few different issues with our sync implementation. 1) Records permissions were not issued as `delegate` grants. In the future the OIDC flow can accept actual `PermissionRequests` that also signal if the user wants a delegate grant or not. Currently the system is opinionated in that `Records` grants MUST be delegated, and `Messages` grants must not. 2) There are a few different scenarios the user can land at with regards to configuring protocols. Handle the various scenarios and make sure the remote node also has the configured protocol available as the connecting user will need it immediately to use. Added method to fail if a scope requests a different protocol than expected. Increase test coverage. --- examples/wallet-connect.html | 38 +++- packages/agent/src/connect.ts | 2 +- packages/agent/src/oidc.ts | 109 +++++---- packages/agent/tests/connect.spec.ts | 319 ++++++++++++++++++++++++++- packages/api/src/web5.ts | 5 +- 5 files changed, 420 insertions(+), 53 deletions(-) diff --git a/examples/wallet-connect.html b/examples/wallet-connect.html index ccaa699e7..f6f2e0cf8 100644 --- a/examples/wallet-connect.html +++ b/examples/wallet-connect.html @@ -121,19 +121,34 @@ <h1>Success</h1> $actions: [ { who: "anyone", - can: ["create", "update"], + can: ["read"], }, ], }, }, }; + const fooProtocol = { + protocol: "http://foo-protocol.xyz", + published: true, + types: { + foo: { + schema: "http://foo-protocol.xyz/schema/foo", + dataFormats: ["application/json"], + }, + }, + structure: { + foo: {}, + }, + }; + try { - const { delegateDid } = await Web5.connect({ + + const { delegateDid, web5 } = await Web5.connect({ walletConnectOptions: { walletUri: "web5://connect", connectServerUrl: "http://localhost:3000/connect", - permissionRequests: [{ protocolDefinition: profileProtocol }], + permissionRequests: [{ protocolDefinition: profileProtocol }, { protocolDefinition: fooProtocol }], onWalletUriReady: generateQRCode, validatePin: async () => { goToPinScreen(); @@ -144,7 +159,18 @@ <h1>Success</h1> }, }); - goToEndScreen(delegateDid); + // attempt to write to the foo protocol + const { record, status } = await web5.dwn.records.create({ + data: { fooData: 'Some Foo Data' }, + message: { + protocol: fooProtocol.protocol, + protocolPath: 'foo', + schema: fooProtocol.types.foo.schema, + dataFormat: fooProtocol.types.foo.dataFormats[0], + } + }); + + goToEndScreen(delegateDid, record, status); } catch (e) { document.getElementById( "errorMessage" @@ -187,12 +213,12 @@ <h1>Success</h1> document.getElementById("pinScreen").style.display = "block"; } - function goToEndScreen(delegateDid) { + function goToEndScreen(delegateDid, record, status) { document.getElementById( "didInformation" ).innerText = `delegateDid\n:${JSON.stringify( delegateDid - )}`; + )}\n\n\nRecordsWrite Status:${JSON.stringify(status)}\nRecord:${JSON.stringify(record, null, 2)}`; document.getElementById("pinScreen").style.display = "none"; document.getElementById("endScreen").style.display = "block"; diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index fdaa35362..24baf15cc 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -1,6 +1,6 @@ import type { PushedAuthResponse } from './oidc.js'; -import type { DwnPermissionScope, DwnProtocolDefinition, Web5ConnectAuthResponse } from './index.js'; +import type { DwnPermissionScope, DwnProtocolDefinition, Web5Agent, Web5ConnectAuthResponse } from './index.js'; import { Oidc, diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index a05873393..3443aabe5 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -12,13 +12,10 @@ import { concatenateUrl } from './utils.js'; import { xchacha20poly1305 } from '@noble/ciphers/chacha'; import type { ConnectPermissionRequest } from './connect.js'; import { DidDocument, DidJwk, PortableDid, type BearerDid } from '@web5/dids'; -import type { - PermissionScope, - RecordsWriteMessage, -} from '@tbd54566975/dwn-sdk-js'; -import { DwnInterface, DwnProtocolDefinition } from './types/dwn.js'; +import { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnPermissionScope, DwnProtocolDefinition } from './types/dwn.js'; import { AgentPermissionsApi } from './permissions-api.js'; import type { Web5Agent } from './types/agent.js'; +import { isRecordPermissionScope } from './dwn-api.js'; /** * Sent to an OIDC server to authorize a client. Allows clients @@ -161,14 +158,10 @@ export type SIOPv2AuthResponse = { /** An auth response that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 */ export type Web5ConnectAuthResponse = { - delegateGrants: DelegateGrant[]; + delegateGrants: DwnDataEncodedRecordsWriteMessage[]; delegatePortableDid: PortableDid; } & SIOPv2AuthResponse; -export type DelegateGrant = (RecordsWriteMessage & { - encodedData: string; -}) - /** Represents the different OIDC endpoint types. * 1. `pushedAuthorizationRequest`: client sends {@link PushedAuthRequest} receives {@link PushedAuthResponse} * 2. `authorize`: provider gets the {@link Web5ConnectAuthRequest} JWT that was stored by the PAR @@ -615,22 +608,26 @@ async function createPermissionGrants( selectedDid: string, delegateBearerDid: BearerDid, agent: Web5Agent, - scopes: PermissionScope[], + scopes: DwnPermissionScope[], ) { - const permissionsApi = new AgentPermissionsApi({ agent }); // 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) => - permissionsApi.createGrant({ + scopes.map((scope) => { + + // check if the scope is a records permission scope, if so it is a delegated permission + const delegated = isRecordPermissionScope(scope); + return permissionsApi.createGrant({ + delegated, store : true, grantedTo : delegateBearerDid.uri, scope, - dateExpires : '2040-06-25T16:09:16.693356Z', + dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional author : selectedDid, - }) - ) + }); + + }) ); const messagePromises = permissionGrants.map(async (grant) => { @@ -638,7 +635,7 @@ async function createPermissionGrants( const { encodedData, ...rawMessage } = grant.message; const data = Convert.base64Url(encodedData).toUint8Array(); - const { reply } = await agent.sendDwnRequest({ + const { reply } = await agent.sendDwnRequest({ author : selectedDid, target : selectedDid, messageType : DwnInterface.RecordsWrite, @@ -662,14 +659,14 @@ async function createPermissionGrants( } /** -* Installs the protocols required by the Client on the Provider -* if they don't already exist. -*/ -async function prepareProtocols( + * Installs the protocol required by the Client on the Provider if it doesn't already exist. + */ +async function prepareProtocol( selectedDid: string, agent: Web5Agent, protocolDefinition: DwnProtocolDefinition -) { +): Promise<void> { + const queryMessage = await agent.processDwnRequest({ author : selectedDid, messageType : DwnInterface.ProtocolsQuery, @@ -677,32 +674,48 @@ async function prepareProtocols( messageParams : { filter: { protocol: protocolDefinition.protocol } }, }); - if (queryMessage.reply.status.code === 404) { - const configureMessage = await agent.processDwnRequest({ + if ( queryMessage.reply.status.code !== 200) { + // if the query failed, throw an error + throw new Error( + `Could not fetch protocol: ${queryMessage.reply.status.detail}` + ); + } else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) { + + // send the protocol definition to the remote DWN first, if it passes we can process it locally + const { reply: sendReply, message: configureMessage } = await agent.sendDwnRequest({ author : selectedDid, - messageType : DwnInterface.ProtocolsConfigure, target : selectedDid, + messageType : DwnInterface.ProtocolsConfigure, messageParams : { definition: protocolDefinition }, }); - if (configureMessage.reply.status.code !== 202) { - throw new Error(`Could not install protocol: ${configureMessage.reply.status.detail}`); + // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync + if (sendReply.status.code !== 202 && sendReply.status.code !== 409) { + throw new Error(`Could not send protocol: ${sendReply.status.detail}`); } - // send the configure message to the remote DWN so that the APP can immediately use it without waiting for a sync cycle from the wallet + // process the protocol locally, we don't have to check if it exists as this is just a convenience over waiting for sync. + await agent.processDwnRequest({ + author : selectedDid, + target : selectedDid, + messageType : DwnInterface.ProtocolsConfigure, + rawMessage : configureMessage + }); + + } else { + + // the protocol already exists, let's make sure it exists on the remote DWN as the requesting app will need it + const configureMessage = queryMessage.reply.entries![0]; const { reply: sendReply } = await agent.sendDwnRequest({ author : selectedDid, target : selectedDid, messageType : DwnInterface.ProtocolsConfigure, - rawMessage : configureMessage.message, + rawMessage : configureMessage, }); - // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync if (sendReply.status.code !== 202 && sendReply.status.code !== 409) { throw new Error(`Could not send protocol: ${sendReply.status.detail}`); } - } else if (queryMessage.reply.status.code !== 200) { - throw new Error(`Could not fetch protcol: ${queryMessage.reply.status.detail}`); } } @@ -719,20 +732,34 @@ async function submitAuthResponse( selectedDid: string, authRequest: Web5ConnectAuthRequest, randomPin: string, - agent: Web5Agent, + agent: Web5Agent ) { const delegateBearerDid = await DidJwk.create(); const delegatePortableDid = await delegateBearerDid.export(); - const delegateGrantPromises = authRequest.permissionRequests.map(async (permissionRequest) => { - // TODO: validate to make sure the scopes and definition are assigned to the same protocol - const { protocolDefinition, permissionScopes } = permissionRequest; + // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this. + const delegateGrantPromises = authRequest.permissionRequests.map( + async (permissionRequest) => { + const { protocolDefinition, permissionScopes } = permissionRequest; - await prepareProtocols(selectedDid, agent, protocolDefinition); - const permissionGrants = await Oidc.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes); + // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with. + const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol); + if (!grantsMatchProtocolUri) { + throw new Error('All permission scopes must match the protocol uri they are provided with.'); + } - return permissionGrants; - }); + await prepareProtocol(selectedDid, agent, protocolDefinition); + + const permissionGrants = await Oidc.createPermissionGrants( + selectedDid, + delegateBearerDid, + agent, + permissionScopes + ); + + return permissionGrants; + } + ); const delegateGrants = (await Promise.all(delegateGrantPromises)).flat(); diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 441f80bdd..c56dc8720 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -11,7 +11,7 @@ import { import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { TestAgent } from './utils/test-agent.js'; import { testDwnUrl } from './utils/test-config.js'; -import { BearerIdentity, DwnProtocolDefinition, WalletConnect } from '../src/index.js'; +import { BearerIdentity, DwnInterface, DwnMessage, DwnProtocolDefinition, WalletConnect } from '../src/index.js'; import { RecordsPermissionScope } from '@tbd54566975/dwn-sdk-js'; import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; @@ -489,6 +489,323 @@ describe('web5 connect', function () { }); }); + describe('submitAuthResponse', () => { + it('should not attempt to configure the protocol if it already exists', async () => { + // scenario: the wallet gets a request for a protocol that it already has configured + // the wallet should not attempt to re-configure, but instead ensure that the protocol is + // sent to the remote DWN for the requesting client to be able to sync it down later + + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const options = { + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); + + // stub the processDwnRequest method to return a protocol entry + const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + messageCid : '', + reply : { status: { code: 202, detail: 'OK' } } + }); + + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ]} }); + + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + // expect the process request to only be called once for ProtocolsQuery + expect(processDwnRequestStub.callCount).to.equal(1); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); + + // send request should be called once as a ProtocolsConfigure + expect(sendRequestSpy.callCount).to.equal(1); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + }); + + it('should configure the protocol if it does not exist', async () => { + // scenario: the wallet gets a request for a protocol that it does not have configured + // the wallet should attempt to configure the protocol and then send the protocol to the remote DWN + + // looks for a response of 404, empty entries array or missing entries array + + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const options = { + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + messageCid : '', + reply : { status: { code: 202, detail: 'OK' } } + }); + + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ ] } }); + + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + // expect the process request to be called for query and configure + expect(processDwnRequestStub.callCount).to.equal(2); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); + expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + + // send request should be called once as a ProtocolsConfigure + expect(sendRequestSpy.callCount).to.equal(1); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + + // reset the spys + processDwnRequestStub.resetHistory(); + sendRequestSpy.resetHistory(); + + // processDwnRequestStub should resolve a 200 with no entires + processDwnRequestStub.resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); + + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + // expect the process request to be called for query and configure + expect(processDwnRequestStub.callCount).to.equal(2); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); + expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + + // send request should be called once as a ProtocolsConfigure + expect(sendRequestSpy.callCount).to.equal(1); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + }); + + it('should fail if the send request fails for newly configured protocol', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const options = { + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '' + }); + + // return without any entries + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); + + try { + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + expect(sendRequestSpy.callCount).to.equal(1); + } + }); + + it('should fail if the send request fails for existing protocol', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const options = { + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); + + // stub the processDwnRequest method to return a protocol entry + const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '' + }); + + // mock returning the protocol entry + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ] } }); + + try { + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + expect(processDwnRequestStub.callCount).to.equal(1); + expect(sendRequestSpy.callCount).to.equal(1); + } + }); + + it('should throw if protocol could not be fetched at all', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const options = { + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '' + }); + + // mock returning the protocol entry + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 500, detail: 'Some Error'}, } }); + + try { + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('Could not fetch protocol: Some Error'); + expect(processDwnRequestStub.callCount).to.equal(1); + expect(sendRequestSpy.callCount).to.equal(0); + } + }); + + it('should throw if a grant that is included in the request does not match the protocol definition', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const mismatchedScopes = [...permissionScopes]; + mismatchedScopes[0].protocol = 'http://profile-protocol.xyz/other'; + + const options = { + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); + + try { + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); + } + }); + }); + describe('createPermissionRequestForProtocol', () => { it('should add sync permissions to all requests', async () => { const protocol:DwnProtocolDefinition = { diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index 6aba6ecef..75468f6d6 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -6,7 +6,6 @@ import type { BearerIdentity, - DelegateGrant, DwnDataEncodedRecordsWriteMessage, DwnMessagesPermissionScope, DwnProtocolDefinition, @@ -242,7 +241,6 @@ export class Web5 { walletConnectOptions, }: Web5ConnectOptions = {}): Promise<Web5ConnectResult> { let delegateDid: string | undefined; - let delegateGrants: DelegateGrant[]; if (agent === undefined) { let registerSync = false; // A custom Web5Agent implementation was not specified, so use default managed user agent. @@ -292,11 +290,10 @@ export class Web5 { 'read', 'write', 'delete', 'query', 'subscribe' ])); - const { delegatePortableDid, connectedDid, delegateGrants: returnedGrants } = await WalletConnect.initClient({ + const { delegatePortableDid, connectedDid, delegateGrants } = await WalletConnect.initClient({ ...connectOptions, permissionRequests: walletPermissionRequests, }); - delegateGrants = returnedGrants; // Import the delegated DID as an Identity in the User Agent. // Setting the connectedDID in the metadata applies a relationship between the signer identity and the one it is impersonating. From 226455a7d6029292c810e67d80e129e2e20f83da Mon Sep 17 00:00:00 2001 From: Liran Cohen <c.liran.c@gmail.com> Date: Thu, 29 Aug 2024 15:08:08 -0400 Subject: [PATCH 6/7] Conflicting Sync error. (#857) When I added the one-shot `sync()` method I put in a guard to prevent it from being called while an interval was running. This caused a bug in the interval, so I've modified the code to account for it. Also updated some type docs to get rid of the warning. --- .changeset/tall-birds-dress.md | 8 +++++ packages/agent/src/connect.ts | 20 +++++++++++- packages/agent/src/sync-engine-level.ts | 3 +- packages/agent/tests/connect.spec.ts | 12 +++++-- .../agent/tests/sync-engine-level.spec.ts | 31 +++++++++++++------ packages/api/src/web5.ts | 25 +++++++++++++-- packages/api/tests/web5.spec.ts | 6 ++-- 7 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 .changeset/tall-birds-dress.md diff --git a/.changeset/tall-birds-dress.md b/.changeset/tall-birds-dress.md new file mode 100644 index 000000000..6eaf78707 --- /dev/null +++ b/.changeset/tall-birds-dress.md @@ -0,0 +1,8 @@ +--- +"@web5/agent": patch +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +--- + +Sync vs StartSync conflicting error. diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 24baf15cc..884c15b96 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -185,9 +185,27 @@ export type ConnectPermissionRequest = { permissionScopes: DwnPermissionScope[]; }; +/** + * Shorthand for the types of permissions that can be requested. + */ export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe'; -function createPermissionRequestForProtocol(definition: DwnProtocolDefinition, permissions: Permission[]): ConnectPermissionRequest { +/** + * The options for creating a permission request for a given protocol. + */ +export type ProtocolPermissionOptions = { + /** The protocol definition for the protocol being requested */ + definition: DwnProtocolDefinition; + + /** The permissions being requested for the protocol */ + permissions: Permission[]; +}; + +/** + * 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). + */ +function createPermissionRequestForProtocol({ definition, permissions }: ProtocolPermissionOptions): ConnectPermissionRequest { const requests: DwnPermissionScope[] = []; // In order to enable sync, we must request permissions for `MessagesQuery`, `MessagesRead` and `MessagesSubscribe` diff --git a/packages/agent/src/sync-engine-level.ts b/packages/agent/src/sync-engine-level.ts index 5908fba49..72336b3b3 100644 --- a/packages/agent/src/sync-engine-level.ts +++ b/packages/agent/src/sync-engine-level.ts @@ -288,7 +288,8 @@ export class SyncEngineLevel implements SyncEngine { } try { - await this.sync(); + await this.push(); + await this.pull(); } catch (error: any) { this.stopSync(); reject(error); diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index c56dc8720..286f7234d 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -822,7 +822,9 @@ describe('web5 connect', function () { } }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, []); + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: [] + }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); expect(permissionRequests.permissionScopes.length).to.equal(3); // only includes the sync permissions @@ -846,7 +848,9 @@ describe('web5 connect', function () { } }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, ['write', 'read']); + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: ['write', 'read'] + }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); @@ -871,7 +875,9 @@ describe('web5 connect', function () { } }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, ['write', 'read', 'delete', 'query', 'subscribe']); + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe'] + }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); diff --git a/packages/agent/tests/sync-engine-level.spec.ts b/packages/agent/tests/sync-engine-level.spec.ts index 35fa68a08..85003f6ec 100644 --- a/packages/agent/tests/sync-engine-level.spec.ts +++ b/packages/agent/tests/sync-engine-level.spec.ts @@ -2415,23 +2415,28 @@ describe('SyncEngineLevel', () => { }); describe('startSync()', () => { - it('calls sync() in each interval', async () => { + it('calls pull() and push() in each interval', async () => { await testHarness.agent.sync.registerIdentity({ did: alice.did.uri, }); - const syncSpy = sinon.stub(SyncEngineLevel.prototype, 'sync'); - syncSpy.resolves(); + const pullSpy = sinon.stub(SyncEngineLevel.prototype as any, 'pull'); + pullSpy.resolves(); + + const pushSpy = sinon.stub(SyncEngineLevel.prototype as any, 'push'); + pushSpy.resolves(); const clock = sinon.useFakeTimers(); testHarness.agent.sync.startSync({ interval: '500ms' }); await clock.tickAsync(1_400); // just under 3 intervals - syncSpy.restore(); + pullSpy.restore(); + pushSpy.restore(); clock.restore(); - expect(syncSpy.callCount).to.equal(2, 'push'); + expect(pullSpy.callCount).to.equal(2, 'push'); + expect(pushSpy.callCount).to.equal(2, 'pull'); }); it('does not call sync() again until a sync round finishes', async () => { @@ -2441,24 +2446,30 @@ describe('SyncEngineLevel', () => { const clock = sinon.useFakeTimers(); - const syncSpy = sinon.stub(SyncEngineLevel.prototype, 'sync'); - syncSpy.returns(new Promise((resolve) => { + const pullSpy = sinon.stub(SyncEngineLevel.prototype as any, 'pull'); + pullSpy.returns(new Promise<void>((resolve) => { clock.setTimeout(() => { resolve(); }, 1_500); // more than the interval })); + const pushSpy = sinon.stub(SyncEngineLevel.prototype as any, 'push'); + pushSpy.resolves(); + testHarness.agent.sync.startSync({ interval: '500ms' }); await clock.tickAsync(1_400); // less time than the push - expect(syncSpy.callCount).to.equal(1, 'sync'); + expect(pullSpy.callCount).to.equal(1, 'pull'); + expect(pullSpy.callCount).to.equal(1, 'push'); await clock.tickAsync(600); //remaining time for a 2nd sync - expect(syncSpy.callCount).to.equal(2, 'sync'); + expect(pullSpy.callCount).to.equal(2, 'pull'); + expect(pushSpy.callCount).to.equal(2, 'push'); - syncSpy.restore(); + pullSpy.restore(); + pushSpy.restore(); clock.restore(); }); }); diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index 75468f6d6..b93e857bf 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -36,12 +36,28 @@ export type DidCreateOptions = { dwnEndpoints?: string[]; } +/** + * Represents a permission request for a protocol definition. + */ export type ConnectPermissionRequest = { + /** + * The protocol definition for the protocol being requested. + */ protocolDefinition: DwnProtocolDefinition; + /** + * The permissions being requested for the protocol. If none are provided, the default is to request all permissions. + */ permissions?: Permission[]; } +/** + * Options for connecting to a Web5 agent. This includes the ability to connect to an external wallet + */ export type ConnectOptions = Omit<WalletConnectOptions, 'permissionRequests'> & { + /** + * The permissions that are being requested for the connected DID. + * This is used to create the {@link ConnectPermissionRequest} for the wallet connect flow. + */ permissionRequests: ConnectPermissionRequest[]; } @@ -286,9 +302,12 @@ export class Web5 { // No connected identity found and connectOptions are provided, attempt to import a delegated DID from an external wallet try { const { permissionRequests, ...connectOptions } = walletConnectOptions; - const walletPermissionRequests = permissionRequests.map(({ protocolDefinition, permissions }) => WalletConnect.createPermissionRequestForProtocol(protocolDefinition, permissions ?? [ - 'read', 'write', 'delete', 'query', 'subscribe' - ])); + const walletPermissionRequests = permissionRequests.map(({ protocolDefinition, permissions }) => WalletConnect.createPermissionRequestForProtocol({ + definition : protocolDefinition, + permissions : permissions ?? [ + 'read', 'write', 'delete', 'query', 'subscribe' + ]} + )); const { delegatePortableDid, connectedDid, delegateGrants } = await WalletConnect.initClient({ ...connectOptions, diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index ceb4791e9..5f8981d4d 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -846,7 +846,7 @@ describe('web5 api', () => { const call = requestPermissionsSpy.getCall(0); // since no explicit permissions were provided, all permissions should be requested - expect(call.args[1]).to.have.members([ + expect(call.args[0].permissions).to.have.members([ 'read', 'write', 'delete', 'query', 'subscribe' ]); } @@ -920,14 +920,14 @@ describe('web5 api', () => { const call1 = requestPermissionsSpy.getCall(0); // since no explicit permissions were provided for the first protocol, all permissions should be requested - expect(call1.args[1]).to.have.members([ + expect(call1.args[0].permissions).to.have.members([ 'read', 'write', 'delete', 'query', 'subscribe' ]); const call2 = requestPermissionsSpy.getCall(1); // only the provided permissions should be requested for the second protocol - expect(call2.args[1]).to.have.members([ + expect(call2.args[0].permissions).to.have.members([ 'read', 'write' ]); } From 9f08161a24155b12e26fc066c8c0b7118ca8299b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:18:54 -0400 Subject: [PATCH 7/7] Version Packages (#858) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> --- .changeset/tall-birds-dress.md | 8 -------- packages/agent/CHANGELOG.md | 6 ++++++ packages/agent/package.json | 2 +- packages/identity-agent/CHANGELOG.md | 9 +++++++++ packages/identity-agent/package.json | 2 +- packages/proxy-agent/CHANGELOG.md | 9 +++++++++ packages/proxy-agent/package.json | 2 +- packages/user-agent/CHANGELOG.md | 9 +++++++++ packages/user-agent/package.json | 2 +- 9 files changed, 37 insertions(+), 12 deletions(-) delete mode 100644 .changeset/tall-birds-dress.md diff --git a/.changeset/tall-birds-dress.md b/.changeset/tall-birds-dress.md deleted file mode 100644 index 6eaf78707..000000000 --- a/.changeset/tall-birds-dress.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@web5/agent": patch -"@web5/identity-agent": patch -"@web5/proxy-agent": patch -"@web5/user-agent": patch ---- - -Sync vs StartSync conflicting error. diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 453270db9..87cd9b04f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,11 @@ # @web5/agent +## 0.6.1 + +### Patch Changes + +- [#857](https://github.com/TBD54566975/web5-js/pull/857) [`226455a`](https://github.com/TBD54566975/web5-js/commit/226455a7d6029292c810e67d80e129e2e20f83da) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Sync vs StartSync conflicting error. + ## 0.6.0 ### Minor Changes diff --git a/packages/agent/package.json b/packages/agent/package.json index 3de0b2c93..7c8e7a6cc 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/agent", - "version": "0.6.0", + "version": "0.6.1", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/identity-agent/CHANGELOG.md b/packages/identity-agent/CHANGELOG.md index f83be6445..24d4e5916 100644 --- a/packages/identity-agent/CHANGELOG.md +++ b/packages/identity-agent/CHANGELOG.md @@ -1,5 +1,14 @@ # @web5/identity-agent +## 0.5.1 + +### Patch Changes + +- [#857](https://github.com/TBD54566975/web5-js/pull/857) [`226455a`](https://github.com/TBD54566975/web5-js/commit/226455a7d6029292c810e67d80e129e2e20f83da) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Sync vs StartSync conflicting error. + +- Updated dependencies [[`226455a`](https://github.com/TBD54566975/web5-js/commit/226455a7d6029292c810e67d80e129e2e20f83da)]: + - @web5/agent@0.6.1 + ## 0.5.0 ### Minor Changes diff --git a/packages/identity-agent/package.json b/packages/identity-agent/package.json index 17230bdf3..78a3ef5c0 100644 --- a/packages/identity-agent/package.json +++ b/packages/identity-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/identity-agent", - "version": "0.5.0", + "version": "0.5.1", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/proxy-agent/CHANGELOG.md b/packages/proxy-agent/CHANGELOG.md index d35ae97f1..59056e815 100644 --- a/packages/proxy-agent/CHANGELOG.md +++ b/packages/proxy-agent/CHANGELOG.md @@ -1,5 +1,14 @@ # @web5/proxy-agent +## 0.5.1 + +### Patch Changes + +- [#857](https://github.com/TBD54566975/web5-js/pull/857) [`226455a`](https://github.com/TBD54566975/web5-js/commit/226455a7d6029292c810e67d80e129e2e20f83da) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Sync vs StartSync conflicting error. + +- Updated dependencies [[`226455a`](https://github.com/TBD54566975/web5-js/commit/226455a7d6029292c810e67d80e129e2e20f83da)]: + - @web5/agent@0.6.1 + ## 0.5.0 ### Minor Changes diff --git a/packages/proxy-agent/package.json b/packages/proxy-agent/package.json index 150488ac6..e3769fefe 100644 --- a/packages/proxy-agent/package.json +++ b/packages/proxy-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/proxy-agent", - "version": "0.5.0", + "version": "0.5.1", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/user-agent/CHANGELOG.md b/packages/user-agent/CHANGELOG.md index a5d793a7b..9f108b12f 100644 --- a/packages/user-agent/CHANGELOG.md +++ b/packages/user-agent/CHANGELOG.md @@ -1,5 +1,14 @@ # @web5/user-agent +## 0.5.1 + +### Patch Changes + +- [#857](https://github.com/TBD54566975/web5-js/pull/857) [`226455a`](https://github.com/TBD54566975/web5-js/commit/226455a7d6029292c810e67d80e129e2e20f83da) Thanks [@LiranCohen](https://github.com/LiranCohen)! - Sync vs StartSync conflicting error. + +- Updated dependencies [[`226455a`](https://github.com/TBD54566975/web5-js/commit/226455a7d6029292c810e67d80e129e2e20f83da)]: + - @web5/agent@0.6.1 + ## 0.5.0 ### Minor Changes diff --git a/packages/user-agent/package.json b/packages/user-agent/package.json index 851b89507..7082dda86 100644 --- a/packages/user-agent/package.json +++ b/packages/user-agent/package.json @@ -1,6 +1,6 @@ { "name": "@web5/user-agent", - "version": "0.5.0", + "version": "0.5.1", "type": "module", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js",