From 750aa1c2c52515104c7ff4e36945297b58d68356 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Tue, 16 Jul 2024 17:40:45 -0400 Subject: [PATCH] DWN Endpoint during `Web5.connect()` DID Creation (#750) * remove selection for DWN endpoints during Connect DID creation, instead provide an option or a single default to `https://dwn.tbddev.org/beta` --- .changeset/two-crabs-taste.md | 5 + packages/api/src/index.ts | 1 - packages/api/src/tech-preview.ts | 57 -------- packages/api/src/web5.ts | 19 ++- packages/api/tests/tech-preview.spec.ts | 186 ------------------------ packages/api/tests/web5.spec.ts | 61 +++++++- 6 files changed, 80 insertions(+), 249 deletions(-) create mode 100644 .changeset/two-crabs-taste.md delete mode 100644 packages/api/src/tech-preview.ts delete mode 100644 packages/api/tests/tech-preview.spec.ts diff --git a/.changeset/two-crabs-taste.md b/.changeset/two-crabs-taste.md new file mode 100644 index 000000000..3c2338f08 --- /dev/null +++ b/.changeset/two-crabs-taste.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +default DWN Server selection updated to select only 1 defaulting to the `beta` TBD hosted version diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 30e46b9c1..8d2f52e15 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -27,7 +27,6 @@ export * from './protocol.js'; export * from './record.js'; export * from './vc-api.js'; export * from './web5.js'; -export * from './tech-preview.js'; export * from './web-features.js'; import * as utils from './utils.js'; diff --git a/packages/api/src/tech-preview.ts b/packages/api/src/tech-preview.ts deleted file mode 100644 index 1bc86a9b9..000000000 --- a/packages/api/src/tech-preview.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { utils as didUtils } from '@web5/dids'; - -/** - * Dynamically selects up to 2 DWN endpoints that are provided - * by default during the Tech Preview period. - * - * @beta - */ -export async function getTechPreviewDwnEndpoints(): Promise { - let response: Response; - try { - response = await fetch('https://dwn.tbddev.org/.well-known/did.json'); - if (!response.ok) { - throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); - } - } catch(error: any) { - console.warn('failed to get tech preview dwn endpoints:', error.message); - return []; - } - - const didDocument = await response.json(); - const [ dwnService ] = didUtils.getServices({ didDocument, id: '#dwn', type: 'DecentralizedWebNode' }); - - // allocate up to 2 nodes for a user. - const techPreviewEndpoints = new Set(); - - if ('serviceEndpoint' in dwnService - && !Array.isArray(dwnService.serviceEndpoint) - && typeof dwnService.serviceEndpoint !== 'string' - && Array.isArray(dwnService.serviceEndpoint.nodes)) { - const dwnUrls = dwnService.serviceEndpoint.nodes; - - const numNodesToAllocate = Math.min(dwnUrls.length, 2); - - for (let attempts = 0; attempts < dwnUrls.length && techPreviewEndpoints.size < numNodesToAllocate; attempts += 1) { - const nodeIdx = getRandomInt(0, dwnUrls.length); - const dwnUrl = dwnUrls[nodeIdx]; - - try { - const healthCheck = await fetch(`${dwnUrl}/health`); - if (healthCheck.ok) { - techPreviewEndpoints.add(dwnUrl); - } - } catch(error: unknown) { - // Ignore healthcheck failures and try the next node. - } - } - } - - return Array.from(techPreviewEndpoints); -} - -function getRandomInt(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min)) + min; -} \ No newline at end of file diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index 1a398dd78..f9f566cbc 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -3,7 +3,6 @@ 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 { getTechPreviewDwnEndpoints } from './tech-preview.js'; import { VcApi } from './vc-api.js'; import { Web5UserAgent } from '@web5/user-agent'; @@ -13,6 +12,12 @@ export type TechPreviewOptions = { dwnEndpoints?: string[]; } +/** Override defaults for DID creation. */ +export type DidCreateOptions = { + /** Override default dwnEndpoints provided during DID creation. */ + dwnEndpoints?: string[]; +} + /** * Options to provide when the initiating app wants to import a delegated identity/DID from an external wallet. */ @@ -136,6 +141,12 @@ export type Web5ConnectOptions = { * See {@link TechPreviewOptions} for available options. */ techPreview?: TechPreviewOptions; + + /** + * Override defaults configured options for creating a DID during connect. + * See {@link DidCreateOptions} for available options. + */ + didCreateOptions?: DidCreateOptions; } /** @@ -213,7 +224,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 + agent, agentVault, connectedDid, password, recoveryPhrase, sync, techPreview, didCreateOptions }: Web5ConnectOptions = {}): Promise { if (agent === undefined) { // A custom Web5Agent implementation was not specified, so use default managed user agent. @@ -254,8 +265,8 @@ export class Web5 { // If an existing identity is not found found, create a new one. const existingIdentityCount = identities.length; if (existingIdentityCount === 0) { - // Use the specified DWN endpoints or get default tech preview hosted nodes. - const serviceEndpointNodes = techPreview?.dwnEndpoints ?? await getTechPreviewDwnEndpoints(); + // Use the specified DWN endpoints or the latest TBD hosted DWN + const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints ?? ['https://dwn.tbddev.org/beta']; // Generate a new Identity for the end-user. identity = await userAgent.identity.create({ diff --git a/packages/api/tests/tech-preview.spec.ts b/packages/api/tests/tech-preview.spec.ts deleted file mode 100644 index 602b935bc..000000000 --- a/packages/api/tests/tech-preview.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import sinon from 'sinon'; -import chai, { expect } from 'chai'; - -import { chaiUrl } from './utils/chai-plugins.js'; -import { getTechPreviewDwnEndpoints } from '../src/tech-preview.js'; - -chai.use(chaiUrl); - -describe('Tech Preview', () => { - describe('getTechPreviewDwnEndpoints()', () => { - let fetchStub: sinon.SinonStub; - let mockDwnEndpoints: Array; - - let tbdWellKnownOkResponse = { - status : 200, - statusText : 'OK', - ok : true, - json : async () => Promise.resolve({ - id : 'did:web:dwn.tbddev.org', - service : [ - { - id : '#dwn', - serviceEndpoint : { - nodes: mockDwnEndpoints - }, - type: 'DecentralizedWebNode' - } - ] - }) - }; - - let tbdWellKnownBadResponse = { - status : 400, - statusText : 'Bad Request', - ok : false - }; - - let dwnServerHealthOkResponse = { - status : 200, - statusText : 'OK', - ok : true, - json : async () => Promise.resolve({ok: true}) - }; - - let dwnServerHealthBadResponse = { - status : 400, - statusText : 'Bad Request', - ok : false - }; - - beforeEach(() => { - mockDwnEndpoints = [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1', - 'https://dwn.tbddev.test/dwn2', - 'https://dwn.tbddev.test/dwn3', - 'https://dwn.tbddev.test/dwn4', - 'https://dwn.tbddev.test/dwn5', - 'https://dwn.tbddev.test/dwn6' - ]; - - fetchStub = sinon.stub(globalThis as any, 'fetch'); - - fetchStub.callsFake((url) => { - if (url === 'https://dwn.tbddev.org/.well-known/did.json') { - return Promise.resolve(tbdWellKnownOkResponse); - } else if (url.endsWith('/health')) { - return Promise.resolve(dwnServerHealthOkResponse); - } - }); - }); - - afterEach(() => { - fetchStub.restore(); - }); - - it('returns an array', async () => { - const dwnEndpoints = await getTechPreviewDwnEndpoints(); - - expect(dwnEndpoints).to.be.an('array'); - }); - - it('returns valid DWN endpoint URLs', async () => { - const dwnEndpoints = await getTechPreviewDwnEndpoints(); - - // There must be at one URL to check or else this test always passes. - expect(dwnEndpoints).to.have.length.greaterThan(0); - - dwnEndpoints.forEach(endpoint => { - expect(endpoint).to.be.a.url; - expect(mockDwnEndpoints).to.include(endpoint); - }); - }); - - it('returns 2 DWN endpoints if at least 2 are healthy', async () => { - const promises = Array(50).fill(0).map(() => getTechPreviewDwnEndpoints()); - - const results = await Promise.all(promises); - - results.forEach(result => { - expect(result).to.be.an('array').that.has.length(2); - }); - }); - - it('returns 1 DWN endpoints if only 1 is healthy', async () => { - mockDwnEndpoints = [ - 'https://dwn.tbddev.test/dwn0' - ]; - - const promises = Array(50).fill(0).map(() => getTechPreviewDwnEndpoints()); - - const results = await Promise.all(promises); - - results.forEach(result => { - expect(result).to.be.an('array').that.has.length(1); - }); - }); - - it('ignores health check failures and tries next endpooint', async () => { - mockDwnEndpoints = [ - 'https://dwn.tbddev.test/dwn0', - 'https://dwn.tbddev.test/dwn1', - ]; - - // Stub fetch to simulate dwn.tbddev.org responding but all of the hosted DWN Server reporting not healthy. - fetchStub.restore(); - fetchStub = sinon.stub(globalThis as any, 'fetch'); - fetchStub.callsFake((url) => { - if (url === 'https://dwn.tbddev.org/.well-known/did.json') { - return Promise.resolve(tbdWellKnownOkResponse); - } else if (url === 'https://dwn.tbddev.test/dwn0/health') { - return Promise.reject(dwnServerHealthOkResponse); - } else if (url === 'https://dwn.tbddev.test/dwn1/health') { - return Promise.resolve(dwnServerHealthBadResponse); - } - }); - - const dwnEndpoints = await getTechPreviewDwnEndpoints(); - - expect(dwnEndpoints).to.be.an('array').that.has.length(0); - }); - - it('returns 0 DWN endpoints if none are healthy', async () => { - // Stub fetch to simulate dwn.tbddev.org responding but all of the hosted DWN Server reporting not healthy. - fetchStub.restore(); - fetchStub = sinon.stub(globalThis as any, 'fetch'); - fetchStub.callsFake((url) => { - if (url === 'https://dwn.tbddev.org/.well-known/did.json') { - return Promise.resolve(tbdWellKnownOkResponse); - } else if (url.endsWith('/health')) { - return Promise.resolve(dwnServerHealthBadResponse); - } - }); - - const dwnEndpoints = await getTechPreviewDwnEndpoints(); - - expect(dwnEndpoints).to.be.an('array').that.has.length(0); - }); - - it('returns 0 DWN endpoints if dwn.tbddev.org is not responding', async () => { - // Stub fetch to simulate dwn.tbddev.org responding but all of the hosted DWN Server reporting not healthy. - fetchStub.restore(); - fetchStub = sinon.stub(globalThis as any, 'fetch'); - fetchStub.callsFake((url) => { - if (url === 'https://dwn.tbddev.org/.well-known/did.json') { - return Promise.resolve(tbdWellKnownBadResponse); - } - }); - - const dwnEndpoints = await getTechPreviewDwnEndpoints(); - - expect(dwnEndpoints).to.be.an('array').that.has.length(0); - }); - - it('returns 0 DWN endpoints if fetching dwn.tbddev.org throws an exception', async () => { - // Stub fetch to simulate fetching dwn.tbddev.org throwing an exception. - fetchStub.restore(); - fetchStub = sinon.stub(globalThis as any, 'fetch'); - fetchStub.withArgs('https://dwn.tbddev.org/.well-known/did.json').rejects(new Error('Network error')); - - const dwnEndpoints = await getTechPreviewDwnEndpoints(); - - expect(dwnEndpoints).to.be.an('array').that.has.length(0); - }); - }); -}); \ No newline at end of file diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index c38a8b4cb..f31335099 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -1,7 +1,9 @@ import { expect } from 'chai'; +import sinon from 'sinon'; + import { MemoryStore } from '@web5/common'; import { Web5UserAgent } from '@web5/user-agent'; -import { HdIdentityVault, PlatformAgentTestHarness } from '@web5/agent'; +import { AgentIdentityApi, HdIdentityVault, PlatformAgentTestHarness } from '@web5/agent'; import { Web5 } from '../src/web5.js'; @@ -131,6 +133,27 @@ describe('Web5', () => { }); describe('connect()', () => { + let testHarness: PlatformAgentTestHarness; + + before(async () => { + testHarness = await PlatformAgentTestHarness.setup({ + agentClass : Web5UserAgent, + agentStores : 'memory' + }); + }); + + beforeEach(async () => { + sinon.restore(); + await testHarness.clearStorage(); + await testHarness.createAgentDid(); + }); + + after(async () => { + sinon.restore(); + await testHarness.clearStorage(); + await testHarness.closeStorage(); + }); + it('uses Web5UserAgent, by default', async () => { // Create an in-memory identity vault store to speed up tests. const agentVault = new HdIdentityVault({ @@ -145,5 +168,41 @@ describe('Web5', () => { expect(recoveryPhrase).to.be.a('string'); expect(recoveryPhrase.split(' ')).to.have.lengthOf(12); }); + + it('creates an identity using the provided techPreview dwnEndpoints', async () => { + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + const identityApiSpy = sinon.spy(AgentIdentityApi.prototype, 'create'); + const { web5, did } = await Web5.connect({ techPreview: { dwnEndpoints: ['https://dwn.example.com/preview'] }}); + expect(web5).to.exist; + expect(did).to.exist; + + expect(identityApiSpy.calledOnce, 'identityApiSpy called').to.be.true; + const serviceEndpoints = (identityApiSpy.firstCall.args[0].didOptions as any).services[0].serviceEndpoint; + expect(serviceEndpoints).to.deep.equal(['https://dwn.example.com/preview']); + }); + + it('creates an identity using the provided didCreateOptions dwnEndpoints', async () => { + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + const identityApiSpy = sinon.spy(AgentIdentityApi.prototype, 'create'); + const { web5, did } = await Web5.connect({ didCreateOptions: { dwnEndpoints: ['https://dwn.example.com'] }}); + expect(web5).to.exist; + expect(did).to.exist; + + expect(identityApiSpy.calledOnce, 'identityApiSpy called').to.be.true; + const serviceEndpoints = (identityApiSpy.firstCall.args[0].didOptions as any).services[0].serviceEndpoint; + expect(serviceEndpoints).to.deep.equal(['https://dwn.example.com']); + }); + + it('defaults to `https://dwn.tbddev.org/beta` as the single DWN Service endpoint if non is provided', async () => { + sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); + const identityApiSpy = sinon.spy(AgentIdentityApi.prototype, 'create'); + const { web5, did } = await Web5.connect(); + expect(web5).to.exist; + expect(did).to.exist; + + expect(identityApiSpy.calledOnce, 'identityApiSpy called').to.be.true; + const serviceEndpoints = (identityApiSpy.firstCall.args[0].didOptions as any).services[0].serviceEndpoint; + expect(serviceEndpoints).to.deep.equal(['https://dwn.tbddev.org/beta']); + }); }); }); \ No newline at end of file