From 87c7fb27f4fc48f1e2517ef3e648c72676476ed1 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 17 Jul 2024 18:15:43 -0400 Subject: [PATCH 1/5] DWN Registration --- packages/agent/src/index.ts | 1 + packages/api/src/web5.ts | 44 +++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 8f63aee6c..4c5220cef 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -10,6 +10,7 @@ export * from './bearer-identity.js'; export * from './crypto-api.js'; export * from './did-api.js'; export * from './dwn-api.js'; +export * from './dwn-registrar.js'; export * from './hd-identity-vault.js'; export * from './identity-api.js'; export * from './local-key-manager.js'; diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index f9f566cbc..baf23f5d8 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -2,7 +2,7 @@ import type { BearerIdentity, HdIdentityVault, Web5Agent } from '@web5/agent'; import { DidApi } from './did-api.js'; import { DwnApi } from './dwn-api.js'; -import { DwnRecordsPermissionScope, DwnProtocolDefinition } from '@web5/agent'; +import { DwnRecordsPermissionScope, DwnProtocolDefinition, DwnRegistrar } from '@web5/agent'; import { VcApi } from './vc-api.js'; import { Web5UserAgent } from '@web5/user-agent'; @@ -147,6 +147,17 @@ export type Web5ConnectOptions = { * See {@link DidCreateOptions} for available options. */ didCreateOptions?: DidCreateOptions; + + /** + * If the `registration` option is provided, the agent DID and the connected DID will be registered with the DWN endpoints provided by `techPreview` or `didCreateOptions`. + * + * If registration fails, the `onFailure` callback will be called with the error. + * If registration is successful, the `onSuccess` callback will be called. + */ + registration? : { + onSuccess: () => void; + onFailure: (error: any) => void; + } } /** @@ -224,7 +235,7 @@ export class Web5 { * @returns A promise that resolves to a {@link Web5} instance and the connected DID. */ static async connect({ - agent, agentVault, connectedDid, password, recoveryPhrase, sync, techPreview, didCreateOptions + agent, agentVault, connectedDid, password, recoveryPhrase, sync, techPreview, didCreateOptions, registration }: Web5ConnectOptions = {}): Promise { if (agent === undefined) { // A custom Web5Agent implementation was not specified, so use default managed user agent. @@ -313,6 +324,35 @@ export class Web5 { connectedDid = identity.did.uri; } + if (registration !== undefined) { + // If a registration object is passed, we attempt to register the AgentDID and the ConnectedDID with the DWN endpoints provided + + const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints; + for (const dwnEndpoint of serviceEndpointNodes) { + try { + + // check if endpoint needs registration + const serverInfo = await userAgent.rpc.getServerInfo(dwnEndpoint); + if (serverInfo.registrationRequirements.length === 0) { + // no registration required + continue; + } + + // register the agent DID + await DwnRegistrar.registerTenant(dwnEndpoint, agent.agentDid.uri); + + // register the connected Identity DID + await DwnRegistrar.registerTenant(dwnEndpoint, connectedDid); + + // if no failure occurs, call the onSuccess callback + registration.onSuccess(); + } catch(error) { + // for any failure, call the onFailure callback with the error + registration.onFailure(error); + } + } + } + // Enable sync, unless explicitly disabled. if (sync !== 'off') { // First, register the user identity for sync. From 81226d2940a138b859c8f895c6938519aa2057d0 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 17 Jul 2024 19:02:57 -0400 Subject: [PATCH 2/5] DWN Registration flow --- packages/api/src/web5.ts | 23 +++-- packages/api/tests/web5.spec.ts | 145 +++++++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 9 deletions(-) diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index baf23f5d8..d632ad9c0 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -151,7 +151,7 @@ export type Web5ConnectOptions = { /** * If the `registration` option is provided, the agent DID and the connected DID will be registered with the DWN endpoints provided by `techPreview` or `didCreateOptions`. * - * If registration fails, the `onFailure` callback will be called with the error. + * If registration fails, the `onFailure` callback will be called with the error. * If registration is successful, the `onSuccess` callback will be called. */ registration? : { @@ -326,11 +326,13 @@ export class Web5 { if (registration !== undefined) { // If a registration object is passed, we attempt to register the AgentDID and the ConnectedDID with the DWN endpoints provided - const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints; - for (const dwnEndpoint of serviceEndpointNodes) { - try { + // We only want to return the success callback if we successfully register with all DWN endpoints + // So we keep track of whether we attempted registration at all + let registrationAttempt = false; + try { + for (const dwnEndpoint of serviceEndpointNodes) { // check if endpoint needs registration const serverInfo = await userAgent.rpc.getServerInfo(dwnEndpoint); if (serverInfo.registrationRequirements.length === 0) { @@ -338,18 +340,23 @@ export class Web5 { continue; } + registrationAttempt = true; + // register the agent DID await DwnRegistrar.registerTenant(dwnEndpoint, agent.agentDid.uri); // register the connected Identity DID await DwnRegistrar.registerTenant(dwnEndpoint, connectedDid); + } - // if no failure occurs, call the onSuccess callback + if (registrationAttempt) { + // If there was a registration attempt and no errors were thrown, call the onSuccess callback registration.onSuccess(); - } catch(error) { - // for any failure, call the onFailure callback with the error - registration.onFailure(error); } + + } catch(error) { + // for any failure, call the onFailure callback with the error + registration.onFailure(error); } } diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index f31335099..5e818e1b5 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -3,7 +3,7 @@ import sinon from 'sinon'; import { MemoryStore } from '@web5/common'; import { Web5UserAgent } from '@web5/user-agent'; -import { AgentIdentityApi, HdIdentityVault, PlatformAgentTestHarness } from '@web5/agent'; +import { AgentIdentityApi, DwnRegistrar, HdIdentityVault, PlatformAgentTestHarness } from '@web5/agent'; import { Web5 } from '../src/web5.js'; @@ -204,5 +204,148 @@ describe('Web5', () => { const serviceEndpoints = (identityApiSpy.firstCall.args[0].didOptions as any).services[0].serviceEndpoint; expect(serviceEndpoints).to.deep.equal(['https://dwn.tbddev.org/beta']); }); + + describe('registration', () => { + it('should call onSuccess if registration is successful', async () => { + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + const serverInfoStub = sinon.stub(testHarness.agent.rpc, 'getServerInfo').resolves({ + registrationRequirements : ['terms-of-service'], + maxFileSize : 10000, + webSocketSupport : true, + }); + + // stub a successful registration + const registerStub = sinon.stub(DwnRegistrar, 'registerTenant').resolves(); + + const registration = { + onSuccess : () => {}, + onFailure : () => {} + }; + + const registerSuccessSpy = sinon.spy(registration, 'onSuccess'); + const registerFailureSpy = sinon.spy(registration, 'onFailure'); + + const { web5, did } = await Web5.connect({ registration, didCreateOptions: { dwnEndpoints: [ + 'https://dwn.example.com', + 'https://dwn.production.com/' + ] } }); + expect(web5).to.exist; + expect(did).to.exist; + + // Success should be called, and failure should not + expect(registerFailureSpy.notCalled, 'onFailure not called').to.be.true; + expect(registerSuccessSpy.calledOnce, 'onSuccess called').to.be.true; + + // Expect getServerInfo and registerTenant to be called. + expect(serverInfoStub.calledTwice, 'getServerInfo called').to.be.true; // once per dwnEndpoint + expect(registerStub.callCount, 'registerTenant called').to.equal(4); // called twice for each dwnEndpoint + }); + + it('should call onFailure if the registration attempts fail', async () => { + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + const serverInfoStub = sinon.stub(testHarness.agent.rpc, 'getServerInfo').resolves({ + registrationRequirements : ['terms-of-service'], + maxFileSize : 10000, + webSocketSupport : true, + }); + + // stub a successful registration + const registerStub = sinon.stub(DwnRegistrar, 'registerTenant').rejects(); + + const registration = { + onSuccess : () => {}, + onFailure : () => {} + }; + + const registerSuccessSpy = sinon.spy(registration, 'onSuccess'); + const registerFailureSpy = sinon.spy(registration, 'onFailure'); + + const { web5, did } = await Web5.connect({ registration, didCreateOptions: { dwnEndpoints: [ + 'https://dwn.example.com', + 'https://dwn.production.com/' + ] } }); + expect(web5).to.exist; + expect(did).to.exist; + + // failure should be called, and success should not + expect(registerSuccessSpy.notCalled, 'onSuccess not called').to.be.true; + expect(registerFailureSpy.calledOnce, 'onFailure called').to.be.true; + + // Expect getServerInfo and registerTenant to be called. + expect(serverInfoStub.calledOnce, 'getServerInfo called').to.be.true; // only called once before registration fails + expect(registerStub.callCount, 'registerTenant called').to.equal(1); // called once and fails + }); + + it('should not attempt registration if the server does not require it', async () => { + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + const serverInfoStub = sinon.stub(testHarness.agent.rpc, 'getServerInfo').resolves({ + registrationRequirements : [], // no registration requirements + maxFileSize : 10000, + webSocketSupport : true, + }); + + // stub a successful registration + const registerStub = sinon.stub(DwnRegistrar, 'registerTenant').resolves(); + + const registration = { + onSuccess : () => {}, + onFailure : () => {} + }; + + const registerSuccessSpy = sinon.spy(registration, 'onSuccess'); + const registerFailureSpy = sinon.spy(registration, 'onFailure'); + + const { web5, did } = await Web5.connect({ registration, didCreateOptions: { dwnEndpoints: [ + 'https://dwn.example.com', + 'https://dwn.production.com/' + ] } }); + expect(web5).to.exist; + expect(did).to.exist; + + // should ot call either success or failure + expect(registerFailureSpy.notCalled, 'onFailure not called').to.be.true; + expect(registerSuccessSpy.notCalled, 'onSuccess called').to.be.true; + + // Expect getServerInfo to be called but not registerTenant + expect(serverInfoStub.calledTwice, 'getServerInfo called').to.be.true; // once per dwnEndpoint + expect(registerStub.notCalled, 'registerTenant not called').to.be.true; // not called + }); + + it('techPreview.dwnEndpoints should take precedence over didCreateOptions.dwnEndpoints', async () => { + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + const serverInfoStub = sinon.stub(testHarness.agent.rpc, 'getServerInfo').resolves({ + registrationRequirements : ['terms-of-service'], + maxFileSize : 10000, + webSocketSupport : true, + }); + + // stub a successful registration + const registerStub = sinon.stub(DwnRegistrar, 'registerTenant').resolves(); + + const registration = { + onSuccess : () => {}, + onFailure : () => {} + }; + + const registerSuccessSpy = sinon.spy(registration, 'onSuccess'); + const registerFailureSpy = sinon.spy(registration, 'onFailure'); + + const { web5, did } = await Web5.connect({ registration, + didCreateOptions : { dwnEndpoints: [ 'https://dwn.example.com', 'https://dwn.production.com/' ] }, // two endpoints, + techPreview : { dwnEndpoints: [ 'https://dwn.production.com/' ] }, // one endpoint + }); + expect(web5).to.exist; + expect(did).to.exist; + + // Success should be called, and failure should not + expect(registerFailureSpy.notCalled, 'onFailure not called').to.be.true; + expect(registerSuccessSpy.calledOnce, 'onSuccess called').to.be.true; + + // Expect getServerInfo and registerTenant to be called. + expect(serverInfoStub.calledOnce, 'getServerInfo called').to.be.true; // Should only be called once for `techPreview` endpoint + expect(registerStub.callCount, 'registerTenant called').to.equal(2); // called twice, once for Agent DID once for Identity DID + }); + }); + }); }); \ No newline at end of file From 966d6b71bd8b229b7c50749b99487ab0650d6b0d Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 17 Jul 2024 19:05:12 -0400 Subject: [PATCH 3/5] docs --- packages/api/src/web5.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index d632ad9c0..fd63ce294 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -155,7 +155,9 @@ export type Web5ConnectOptions = { * If registration is successful, the `onSuccess` callback will be called. */ registration? : { + /** Called when all of the DWN registrations are successful */ onSuccess: () => void; + /** Called when any of the DWN registrations fail */ onFailure: (error: any) => void; } } From 9c29d52ec6ae3dc890df6307c39b6b142c9fe8e4 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 17 Jul 2024 19:12:32 -0400 Subject: [PATCH 4/5] add changeset --- .changeset/bright-plums-buy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-plums-buy.md diff --git a/.changeset/bright-plums-buy.md b/.changeset/bright-plums-buy.md new file mode 100644 index 000000000..1fe658669 --- /dev/null +++ b/.changeset/bright-plums-buy.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +Add DWN Tenent Registration to `Web5.connect()` From cf7b4dc9409a42829cf81b48c484a77d146c0d6d Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 17 Jul 2024 19:36:31 -0400 Subject: [PATCH 5/5] call onsuccess as long as failures dont occur --- packages/api/src/web5.ts | 12 ++---------- packages/api/tests/web5.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index fd63ce294..243f250aa 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -330,9 +330,6 @@ export class Web5 { // If a registration object is passed, we attempt to register the AgentDID and the ConnectedDID with the DWN endpoints provided const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints; - // We only want to return the success callback if we successfully register with all DWN endpoints - // So we keep track of whether we attempted registration at all - let registrationAttempt = false; try { for (const dwnEndpoint of serviceEndpointNodes) { // check if endpoint needs registration @@ -342,8 +339,6 @@ export class Web5 { continue; } - registrationAttempt = true; - // register the agent DID await DwnRegistrar.registerTenant(dwnEndpoint, agent.agentDid.uri); @@ -351,11 +346,8 @@ export class Web5 { await DwnRegistrar.registerTenant(dwnEndpoint, connectedDid); } - if (registrationAttempt) { - // If there was a registration attempt and no errors were thrown, call the onSuccess callback - registration.onSuccess(); - } - + // If no failures occurred, call the onSuccess callback + registration.onSuccess(); } catch(error) { // for any failure, call the onFailure callback with the error registration.onFailure(error); diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index 5e818e1b5..c5afee939 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -302,9 +302,9 @@ describe('Web5', () => { expect(web5).to.exist; expect(did).to.exist; - // should ot call either success or failure + // should call onSuccess and not onFailure + expect(registerSuccessSpy.calledOnce, 'onSuccess called').to.be.true; expect(registerFailureSpy.notCalled, 'onFailure not called').to.be.true; - expect(registerSuccessSpy.notCalled, 'onSuccess called').to.be.true; // Expect getServerInfo to be called but not registerTenant expect(serverInfoStub.calledTwice, 'getServerInfo called').to.be.true; // once per dwnEndpoint