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()` 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..243f250aa 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,19 @@ 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? : { + /** Called when all of the DWN registrations are successful */ + onSuccess: () => void; + /** Called when any of the DWN registrations fail */ + onFailure: (error: any) => void; + } } /** @@ -224,7 +237,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 +326,34 @@ 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; + + try { + for (const dwnEndpoint of serviceEndpointNodes) { + // 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 failures occurred, 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. diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index f31335099..c5afee939 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 call onSuccess and not onFailure + expect(registerSuccessSpy.calledOnce, 'onSuccess called').to.be.true; + expect(registerFailureSpy.notCalled, 'onFailure not 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