From 91d99bcdb9ee309b8e20f510dff3e40c65231e0a Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 16 Oct 2024 15:36:01 -0400 Subject: [PATCH 1/6] get and set dwn endpoints added to identity api --- packages/agent/src/identity-api.ts | 41 ++++++ packages/agent/src/utils.ts | 5 +- packages/agent/tests/identity-api.spec.ts | 155 ++++++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/identity-api.ts b/packages/agent/src/identity-api.ts index 113f35e9c..c807d4548 100644 --- a/packages/agent/src/identity-api.ts +++ b/packages/agent/src/identity-api.ts @@ -9,6 +9,9 @@ import type { IdentityMetadata, PortableIdentity } from './types/identity.js'; import { BearerIdentity } from './bearer-identity.js'; import { isPortableDid } from './prototyping/dids/utils.js'; import { InMemoryIdentityStore } from './store-identity.js'; +import { getDwnServiceEndpointUrls } from './utils.js'; +import { canonicalize } from '@web5/crypto'; +import { PortableDid } from '@web5/dids'; export interface IdentityApiParams { agent?: Web5PlatformAgent; @@ -216,6 +219,44 @@ export class AgentIdentityApi { + return getDwnServiceEndpointUrls(didUri, this.agent.did); + } + + public async setDwnEndpoints({ didUri, endpoints }: { didUri: string; endpoints: string[] }): Promise { + const bearerDid = await this.agent.did.get({ didUri }); + if (!bearerDid) { + throw new Error(`AgentIdentityApi: Failed to set DWN endpoints due to DID not found: ${didUri}`); + } + + const portableDid = JSON.parse(JSON.stringify(await bearerDid.export())) as PortableDid; + const dwnService = portableDid.document.service?.find(service => service.id.endsWith('dwn')); + if (dwnService) { + // Update the existing DWN Service with the provided endpoints + dwnService.serviceEndpoint = endpoints; + } else { + + // create a DWN Service to add to the DID document + const newDwnService = { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : endpoints, + enc : '#enc', + sig : '#sig' + }; + + // if no other services exist, create a new array with the DWN service + if (!portableDid.document.service) { + portableDid.document.service = [newDwnService]; + } else { + // otherwise, push the new DWN service to the existing services + portableDid.document.service.push(newDwnService); + } + } + + await this.agent.did.update({ portableDid, tenant: this.agent.agentDid.uri }); + } + /** * Returns the connected Identity, if one is available. * diff --git a/packages/agent/src/utils.ts b/packages/agent/src/utils.ts index f0d1824aa..11a34065a 100644 --- a/packages/agent/src/utils.ts +++ b/packages/agent/src/utils.ts @@ -4,7 +4,7 @@ import { PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tb import { Readable } from '@web5/common'; import { utils as didUtils } from '@web5/dids'; import { ReadableWebToNodeStream } from 'readable-web-to-node-stream'; -import { DateSort, DwnInterfaceName, DwnMethodName, Message, Records, RecordsWrite } from '@tbd54566975/dwn-sdk-js'; +import { DateSort, DwnInterfaceName, DwnMethodName, Message, RecordsWrite } from '@tbd54566975/dwn-sdk-js'; export function blobToIsomorphicNodeReadable(blob: Blob): Readable { return webReadableToIsomorphicNodeReadable(blob.stream() as ReadableStream); @@ -38,6 +38,9 @@ export async function getDwnServiceEndpointUrls(didUri: string, dereferencer: Di return []; } +export async function setDwnServiceEndpointUrls(didUri: string, serviceEndpointUrls: string[], dereferencer: DidUrlDereferencer) { +} + export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { return Message.getAuthor(record); } diff --git a/packages/agent/tests/identity-api.spec.ts b/packages/agent/tests/identity-api.spec.ts index b143a17db..d1df11d6f 100644 --- a/packages/agent/tests/identity-api.spec.ts +++ b/packages/agent/tests/identity-api.spec.ts @@ -5,6 +5,7 @@ import { TestAgent } from './utils/test-agent.js'; import { AgentIdentityApi } from '../src/identity-api.js'; import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { PortableIdentity } from '../src/index.js'; +import { BearerDid, PortableDid } from '@web5/dids'; describe('AgentIdentityApi', () => { @@ -220,6 +221,160 @@ describe('AgentIdentityApi', () => { }); }); + describe('setDwnEndpoints()', () => { + it('should set the DWN endpoints for a DID', async () => { + const initialEndpoints = ['https://example.com/dwn']; + // create a new identity + const identity = await testHarness.agent.identity.create({ + didMethod : 'dht', + didOptions : { + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : initialEndpoints, + enc : '#enc', + sig : '#sig', + } + ], + verificationMethods: [ + { + algorithm : 'Ed25519', + id : 'sig', + purposes : ['assertionMethod', 'authentication'] + }, + { + algorithm : 'secp256k1', + id : 'enc', + purposes : ['keyAgreement'] + } + ] + }, + metadata: { name: 'Alice' }, + }); + + // control: get the service endpoints of the created DID + const initialDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + expect(initialDwnEndpoints).to.deep.equal(initialEndpoints); + + // set new endpoints + const newEndpoints = ['https://example.com/dwn2']; + await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: newEndpoints }); + + // get the service endpoints of the updated DID + const updatedDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + expect(updatedDwnEndpoints).to.deep.equal(newEndpoints); + }); + + it('should throw an error if the service endpoints remain unchanged', async () => { + const initialEndpoints = ['https://example.com/dwn']; + // create a new identity + const identity = await testHarness.agent.identity.create({ + didMethod : 'dht', + didOptions : { + services: [ + { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : initialEndpoints, + enc : '#enc', + sig : '#sig', + } + ], + verificationMethods: [ + { + algorithm : 'Ed25519', + id : 'sig', + purposes : ['assertionMethod', 'authentication'] + }, + { + algorithm : 'secp256k1', + id : 'enc', + purposes : ['keyAgreement'] + } + ] + }, + metadata: { name: 'Alice' }, + }); + + // control: get the service endpoints of the created DID + const initialDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + expect(initialDwnEndpoints).to.deep.equal(initialEndpoints); + + // set the same endpoints + try { + await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: initialEndpoints }); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('AgentDidApi: No changes detected'); + } + }); + + it('should throw an error if the DID is not found', async () => { + try { + await testHarness.agent.identity.setDwnEndpoints({ didUri: 'did:method:xyz123', endpoints: ['https://example.com/dwn'] }); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('AgentIdentityApi: Failed to set DWN endpoints due to DID not found'); + } + }); + + it('should add a DWN service if no services exist', async () => { + // create a new identity without any DWN endpoints or services + const identity = await testHarness.agent.identity.create({ + didMethod : 'dht', + metadata : { name: 'Alice' }, + }); + + // control: get the service endpoints of the created DID, should fail + try { + await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + expect.fail('should have thrown an error'); + } catch(error: any) { + expect(error.message).to.include('Failed to dereference'); + } + + // set new endpoints + const newEndpoints = ['https://example.com/dwn2']; + await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: newEndpoints }); + + // get the service endpoints of the updated DID + const updatedDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + expect(updatedDwnEndpoints).to.deep.equal(newEndpoints); + }); + + it('should add a DWN service if one does not exist in the services list', async () => { + // create a new identity without a DWN service + const identity = await testHarness.agent.identity.create({ + didMethod : 'dht', + didOptions : { + services: [{ + id : 'some-service', // non DWN service + type : 'SomeService', + serviceEndpoint : ['https://example.com/some-service'], + }] + }, + metadata: { name: 'Alice' }, + }); + + // control: get the service endpoints of the created DID, should fail + try { + await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + expect.fail('should have thrown an error'); + } catch(error: any) { + expect(error.message).to.include('Failed to dereference'); + } + + // set new endpoints + const newEndpoints = ['https://example.com/dwn2']; + await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: newEndpoints }); + + // get the service endpoints of the updated DID + const updatedDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + expect(updatedDwnEndpoints).to.deep.equal(newEndpoints); + }); + }); + describe('connectedIdentity', () => { it('returns a connected Identity', async () => { // create multiple identities, some that are connected, and some that are not From e353b8d961727bcd9d36a3c4bdf0b2adc86ab17c Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 16 Oct 2024 16:07:47 -0400 Subject: [PATCH 2/6] remove unused function --- packages/agent/src/utils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/agent/src/utils.ts b/packages/agent/src/utils.ts index 11a34065a..2ab38757f 100644 --- a/packages/agent/src/utils.ts +++ b/packages/agent/src/utils.ts @@ -38,9 +38,6 @@ export async function getDwnServiceEndpointUrls(didUri: string, dereferencer: Di return []; } -export async function setDwnServiceEndpointUrls(didUri: string, serviceEndpointUrls: string[], dereferencer: DidUrlDereferencer) { -} - export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { return Message.getAuthor(record); } From c3f971ffb0267e658e8340b461181d8c9d2ba0db Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 16 Oct 2024 16:42:05 -0400 Subject: [PATCH 3/6] modify tests to avoid creating new did dht dids each run --- packages/agent/tests/identity-api.spec.ts | 263 ++++++++++++++-------- 1 file changed, 164 insertions(+), 99 deletions(-) diff --git a/packages/agent/tests/identity-api.spec.ts b/packages/agent/tests/identity-api.spec.ts index d1df11d6f..3ce0bcff2 100644 --- a/packages/agent/tests/identity-api.spec.ts +++ b/packages/agent/tests/identity-api.spec.ts @@ -5,7 +5,7 @@ import { TestAgent } from './utils/test-agent.js'; import { AgentIdentityApi } from '../src/identity-api.js'; import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { PortableIdentity } from '../src/index.js'; -import { BearerDid, PortableDid } from '@web5/dids'; +import { BearerDid, PortableDid, UniversalResolver } from '@web5/dids'; describe('AgentIdentityApi', () => { @@ -222,88 +222,143 @@ describe('AgentIdentityApi', () => { }); describe('setDwnEndpoints()', () => { - it('should set the DWN endpoints for a DID', async () => { - const initialEndpoints = ['https://example.com/dwn']; - // create a new identity - const identity = await testHarness.agent.identity.create({ - didMethod : 'dht', - didOptions : { - services: [ - { - id : 'dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : initialEndpoints, - enc : '#enc', - sig : '#sig', + const testPortableDid: PortableDid = { + uri : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy', + document : { + id : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy', + verificationMethod : [ + { + id : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#0', + type : 'JsonWebKey', + controller : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'H2XEz9RKJ7T0m7BmlyphVEdpKDFFT1WpJ9_STXKd7wY', + kid : '-2bXX6F3hvTHV5EBFX6oyKq11s7gtJdzUjjwdeUyBVA', + alg : 'EdDSA' + } + }, + { + id : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#sig', + type : 'JsonWebKey', + controller : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'T2rdfCxGubY_zta8Gy6SVxypcchfmZKJhbXB9Ia9xlg', + kid : 'Ogpmsy5VR3SET9WC0WZD9r5p1WAKdCt1fxT0GNSLE5c', + alg : 'EdDSA' } - ], - verificationMethods: [ - { - algorithm : 'Ed25519', - id : 'sig', - purposes : ['assertionMethod', 'authentication'] - }, - { - algorithm : 'secp256k1', - id : 'enc', - purposes : ['keyAgreement'] + }, + { + id : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#enc', + type : 'JsonWebKey', + controller : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy', + publicKeyJwk : { + kty : 'EC', + crv : 'secp256k1', + x : 'oTPWtNfN7e48p3n-VsoSp07kcHfCszSrJ1-qFx3diiI', + y : '5KSDrAkg91yK19zxD6ESRPAI8v91F-QRXPbivZ-v-Ac', + kid : 'K0CBI00sEmYE6Av4PHqiwPNMzrBRA9dyIlzh1a9A2H8', + alg : 'ES256K' } - ] + } + ], + authentication: [ + 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#0', + 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#sig' + ], + assertionMethod: [ + 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#0', + 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#sig' + ], + capabilityDelegation: [ + 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#0' + ], + capabilityInvocation: [ + 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#0' + ], + keyAgreement: [ + 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#enc' + ], + service: [ + { + id : 'did:dht:d71hju6wjeu5j7r5sbujqkubktds1kbtei8imkj859jr4hw77hdy#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : [ + 'https://example.com/dwn' + ], + enc : '#enc', + sig : '#sig' + } + ] + }, + metadata: { + published : true, + versionId : '1729109527' + }, + privateKeys: [ + { + crv : 'Ed25519', + d : '7vRkinnXFRb2GkNVeY5yQ6TCnYwbtq9gJcbdqnzFR2o', + kty : 'OKP', + x : 'H2XEz9RKJ7T0m7BmlyphVEdpKDFFT1WpJ9_STXKd7wY', + kid : '-2bXX6F3hvTHV5EBFX6oyKq11s7gtJdzUjjwdeUyBVA', + alg : 'EdDSA' }, - metadata: { name: 'Alice' }, - }); + { + crv : 'Ed25519', + d : 'YM-0lQkMc9mNr2NrBVMojpCG2MMAnYk6-4dwxlFeiuw', + kty : 'OKP', + x : 'T2rdfCxGubY_zta8Gy6SVxypcchfmZKJhbXB9Ia9xlg', + kid : 'Ogpmsy5VR3SET9WC0WZD9r5p1WAKdCt1fxT0GNSLE5c', + alg : 'EdDSA' + }, + { + kty : 'EC', + crv : 'secp256k1', + d : 'f4BngIzc_N-YDf04vXD5Ya-HdiVWB8Egk4QoSHKKJPg', + x : 'oTPWtNfN7e48p3n-VsoSp07kcHfCszSrJ1-qFx3diiI', + y : '5KSDrAkg91yK19zxD6ESRPAI8v91F-QRXPbivZ-v-Ac', + kid : 'K0CBI00sEmYE6Av4PHqiwPNMzrBRA9dyIlzh1a9A2H8', + alg : 'ES256K' + } + ] + }; - // control: get the service endpoints of the created DID - const initialDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); - expect(initialDwnEndpoints).to.deep.equal(initialEndpoints); + beforeEach(async () => { + // import the keys for the test portable DID + await BearerDid.import({ keyManager: testHarness.agent.keyManager, portableDid: testPortableDid }); + }); + + it('should set the DWN endpoints for a DID', async () => { + // stub did.get to return the test DID + sinon.stub(testHarness.agent.did, 'get').resolves(new BearerDid({ ...testPortableDid, keyManager: testHarness.agent.keyManager })); + const updateSpy = sinon.stub(testHarness.agent.did, 'update').resolves(); // set new endpoints const newEndpoints = ['https://example.com/dwn2']; - await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: newEndpoints }); - - // get the service endpoints of the updated DID - const updatedDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); - expect(updatedDwnEndpoints).to.deep.equal(newEndpoints); + await testHarness.agent.identity.setDwnEndpoints({ didUri: testPortableDid.uri, endpoints: newEndpoints }); + + expect(updateSpy.calledOnce).to.be.true; + // expect the updated DID to have the new DWN service + expect(updateSpy.firstCall.args[0].portableDid.document.service).to.deep.equal([{ + id : `${testPortableDid.uri}#dwn`, + type : 'DecentralizedWebNode', + serviceEndpoint : newEndpoints, + enc : '#enc', + sig : '#sig' + }]); }); it('should throw an error if the service endpoints remain unchanged', async () => { - const initialEndpoints = ['https://example.com/dwn']; - // create a new identity - const identity = await testHarness.agent.identity.create({ - didMethod : 'dht', - didOptions : { - services: [ - { - id : 'dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : initialEndpoints, - enc : '#enc', - sig : '#sig', - } - ], - verificationMethods: [ - { - algorithm : 'Ed25519', - id : 'sig', - purposes : ['assertionMethod', 'authentication'] - }, - { - algorithm : 'secp256k1', - id : 'enc', - purposes : ['keyAgreement'] - } - ] - }, - metadata: { name: 'Alice' }, - }); - - // control: get the service endpoints of the created DID - const initialDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); - expect(initialDwnEndpoints).to.deep.equal(initialEndpoints); + // stub did.get to return the test DID + sinon.stub(testHarness.agent.did, 'get').resolves(new BearerDid({ ...testPortableDid, keyManager: testHarness.agent.keyManager })); // set the same endpoints try { - await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: initialEndpoints }); + await testHarness.agent.identity.setDwnEndpoints({ didUri: testPortableDid.uri, endpoints: ['https://example.com/dwn'] }); expect.fail('Expected an error to be thrown'); } catch (error: any) { expect(error.message).to.include('AgentDidApi: No changes detected'); @@ -320,15 +375,15 @@ describe('AgentIdentityApi', () => { }); it('should add a DWN service if no services exist', async () => { - // create a new identity without any DWN endpoints or services - const identity = await testHarness.agent.identity.create({ - didMethod : 'dht', - metadata : { name: 'Alice' }, - }); + // stub the did.get to return a DID without any services + const testPortableDidWithoutServices = { ...testPortableDid, document: { ...testPortableDid.document, service: undefined } }; + sinon.stub(testHarness.agent.did, 'get').resolves(new BearerDid({ ...testPortableDidWithoutServices, keyManager: testHarness.agent.keyManager })); + sinon.stub(UniversalResolver.prototype, 'resolve').withArgs(testPortableDid.uri).resolves({ didDocument: testPortableDidWithoutServices.document, didDocumentMetadata: {}, didResolutionMetadata: {} }); + const updateSpy = sinon.stub(testHarness.agent.did, 'update').resolves(); // control: get the service endpoints of the created DID, should fail try { - await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + await testHarness.agent.identity.getDwnEndpoints({ didUri: testPortableDid.uri }); expect.fail('should have thrown an error'); } catch(error: any) { expect(error.message).to.include('Failed to dereference'); @@ -336,30 +391,30 @@ describe('AgentIdentityApi', () => { // set new endpoints const newEndpoints = ['https://example.com/dwn2']; - await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: newEndpoints }); - - // get the service endpoints of the updated DID - const updatedDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); - expect(updatedDwnEndpoints).to.deep.equal(newEndpoints); + await testHarness.agent.identity.setDwnEndpoints({ didUri: testPortableDid.uri, endpoints: newEndpoints }); + + expect(updateSpy.calledOnce).to.be.true; + + // expect the updated DID to have the new DWN service + expect(updateSpy.firstCall.args[0].portableDid.document.service).to.deep.equal([{ + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : newEndpoints, + enc : '#enc', + sig : '#sig' + }]); }); it('should add a DWN service if one does not exist in the services list', async () => { - // create a new identity without a DWN service - const identity = await testHarness.agent.identity.create({ - didMethod : 'dht', - didOptions : { - services: [{ - id : 'some-service', // non DWN service - type : 'SomeService', - serviceEndpoint : ['https://example.com/some-service'], - }] - }, - metadata: { name: 'Alice' }, - }); + // stub the did.get and resolver to return a DID with a different service + const testPortableDidWithDifferentService = { ...testPortableDid, document: { ...testPortableDid.document, service: [{ id: 'other', type: 'Other', serviceEndpoint: ['https://example.com/other'] }] } }; + sinon.stub(testHarness.agent.did, 'get').resolves(new BearerDid({ ...testPortableDidWithDifferentService, keyManager: testHarness.agent.keyManager })); + sinon.stub(UniversalResolver.prototype, 'resolve').withArgs(testPortableDid.uri).resolves({ didDocument: testPortableDidWithDifferentService.document, didDocumentMetadata: {}, didResolutionMetadata: {} }); + const updateSpy = sinon.stub(testHarness.agent.did, 'update').resolves(); // control: get the service endpoints of the created DID, should fail try { - await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); + await testHarness.agent.identity.getDwnEndpoints({ didUri: testPortableDidWithDifferentService.uri }); expect.fail('should have thrown an error'); } catch(error: any) { expect(error.message).to.include('Failed to dereference'); @@ -367,11 +422,21 @@ describe('AgentIdentityApi', () => { // set new endpoints const newEndpoints = ['https://example.com/dwn2']; - await testHarness.agent.identity.setDwnEndpoints({ didUri: identity.did.uri, endpoints: newEndpoints }); - - // get the service endpoints of the updated DID - const updatedDwnEndpoints = await testHarness.agent.identity.getDwnEndpoints({ didUri: identity.did.uri }); - expect(updatedDwnEndpoints).to.deep.equal(newEndpoints); + await testHarness.agent.identity.setDwnEndpoints({ didUri: testPortableDidWithDifferentService.uri, endpoints: newEndpoints }); + + // expect the updated DID to have the new DWN service as well as the existing service + expect(updateSpy.calledOnce).to.be.true; + expect(updateSpy.firstCall.args[0].portableDid.document.service).to.deep.equal([{ + id : 'other', + type : 'Other', + serviceEndpoint : ['https://example.com/other'] + }, { + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : newEndpoints, + enc : '#enc', + sig : '#sig' + }]); }); }); From 227b7e3dffd31e6f6658682f04a62c1c665fb6ce Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 16 Oct 2024 16:58:48 -0400 Subject: [PATCH 4/6] return a copy for did export --- packages/agent/src/bearer-identity.ts | 2 +- packages/agent/src/identity-api.ts | 3 +-- packages/dids/src/bearer-did.ts | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/agent/src/bearer-identity.ts b/packages/agent/src/bearer-identity.ts index 8221f3035..41949909d 100644 --- a/packages/agent/src/bearer-identity.ts +++ b/packages/agent/src/bearer-identity.ts @@ -36,7 +36,7 @@ export class BearerIdentity { public async export(): Promise { return { portableDid : await this.did.export(), - metadata : this.metadata + metadata : { ...this.metadata }, }; } } \ No newline at end of file diff --git a/packages/agent/src/identity-api.ts b/packages/agent/src/identity-api.ts index c807d4548..1e51cb85c 100644 --- a/packages/agent/src/identity-api.ts +++ b/packages/agent/src/identity-api.ts @@ -10,7 +10,6 @@ import { BearerIdentity } from './bearer-identity.js'; import { isPortableDid } from './prototyping/dids/utils.js'; import { InMemoryIdentityStore } from './store-identity.js'; import { getDwnServiceEndpointUrls } from './utils.js'; -import { canonicalize } from '@web5/crypto'; import { PortableDid } from '@web5/dids'; export interface IdentityApiParams { @@ -229,7 +228,7 @@ export class AgentIdentityApi service.id.endsWith('dwn')); if (dwnService) { // Update the existing DWN Service with the provided endpoints diff --git a/packages/dids/src/bearer-did.ts b/packages/dids/src/bearer-did.ts index cdecee834..4ed3dbd58 100644 --- a/packages/dids/src/bearer-did.ts +++ b/packages/dids/src/bearer-did.ts @@ -128,12 +128,12 @@ export class BearerDid { throw new Error(`DID document for '${this.uri}' is missing verification methods`); } - // Create a new `PortableDid` object to store the exported data. - let portableDid: PortableDid = { + // Create a new `PortableDid` copy object to store the exported data. + let portableDid: PortableDid = JSON.parse(JSON.stringify({ uri : this.uri, document : this.document, metadata : this.metadata - }; + })); // If the BearerDid's key manager supports exporting private keys, add them to the portable DID. if ('exportKey' in this.keyManager && typeof this.keyManager.exportKey === 'function') { From 800e71e4331ab2ceba8f573f9279c2c922ba3188 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 16 Oct 2024 17:18:56 -0400 Subject: [PATCH 5/6] add changeset --- .changeset/fair-pillows-notice.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/fair-pillows-notice.md diff --git a/.changeset/fair-pillows-notice.md b/.changeset/fair-pillows-notice.md new file mode 100644 index 000000000..796372911 --- /dev/null +++ b/.changeset/fair-pillows-notice.md @@ -0,0 +1,9 @@ +--- +"@web5/agent": patch +"@web5/dids": patch +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +--- + +Add ability to update DWN Endpoints From 8b03387f58a042bb7ccc6b7d01de1ddf2f1ddd99 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 16 Oct 2024 17:28:36 -0400 Subject: [PATCH 6/6] add doc comments --- packages/agent/src/identity-api.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/agent/src/identity-api.ts b/packages/agent/src/identity-api.ts index 1e51cb85c..f35bab22a 100644 --- a/packages/agent/src/identity-api.ts +++ b/packages/agent/src/identity-api.ts @@ -218,10 +218,24 @@ export class AgentIdentityApi { return getDwnServiceEndpointUrls(didUri, this.agent.did); } + /** + * Sets the DWN endpoints for the given DID. + * + * @param didUri - The DID URI to set the DWN endpoints for. + * @param endpoints - The array of DWN endpoints to set. + * @throws An error if the DID is not found, or if an update cannot be performed. + */ public async setDwnEndpoints({ didUri, endpoints }: { didUri: string; endpoints: string[] }): Promise { const bearerDid = await this.agent.did.get({ didUri }); if (!bearerDid) {