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