Skip to content

Commit

Permalink
DWN Registration (#769)
Browse files Browse the repository at this point in the history
* DWN Registration
  • Loading branch information
LiranCohen authored Jul 17, 2024
1 parent 6e1a7ec commit d18aa6b
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-plums-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/api": patch
---

Add DWN Tenent Registration to `Web5.connect()`
1 change: 1 addition & 0 deletions packages/agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
45 changes: 43 additions & 2 deletions packages/api/src/web5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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<Web5ConnectResult> {
if (agent === undefined) {
// A custom Web5Agent implementation was not specified, so use default managed user agent.
Expand Down Expand Up @@ -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.
Expand Down
145 changes: 144 additions & 1 deletion packages/api/tests/web5.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
});
});

});
});

0 comments on commit d18aa6b

Please sign in to comment.