From e7f5dfec5fc4cff686340a935442342a9383aa34 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 21 Oct 2024 19:58:35 -0400 Subject: [PATCH] Add ability to update the Identity Metadata name field. (#956) This PR exposes the ability to update the `name` field within the Identity's Metadata. The reasoning behind exposing the specific field vs allowing the entire object to be updated is to prevent some pretty bad foot-guns. For more context: ```typescript export interface IdentityMetadata { name: string; tenant: string; uri: string; connectedDid?: string; } ``` The `uri` field should always be the DID's URI, so this should remain unchanged. The `tenant` field is set to the Agent's DID, changing this would be a foot-gun and might render the identity un-readable. The `connectedDid` field is used to denote whether a DID is a delegated/connected DID and which DID it is acting on behalf of (the connectedDID). Since none of these other values should be changed, we expose a method to update a single field. --- .changeset/shy-grapes-juggle.md | 8 ++ packages/agent/src/identity-api.ts | 33 +++++++ packages/agent/tests/identity-api.spec.ts | 109 ++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 .changeset/shy-grapes-juggle.md diff --git a/.changeset/shy-grapes-juggle.md b/.changeset/shy-grapes-juggle.md new file mode 100644 index 000000000..4cb022f44 --- /dev/null +++ b/.changeset/shy-grapes-juggle.md @@ -0,0 +1,8 @@ +--- +"@web5/agent": patch +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +--- + +Add ability to update the Identity Metadata name field. diff --git a/packages/agent/src/identity-api.ts b/packages/agent/src/identity-api.ts index f35bab22a..0b1bbd1cd 100644 --- a/packages/agent/src/identity-api.ts +++ b/packages/agent/src/identity-api.ts @@ -270,6 +270,39 @@ export class AgentIdentityApi { + if (!name) { + throw new Error('AgentIdentityApi: Failed to set metadata name due to missing name value.'); + } + + const identity = await this.get({ didUri }); + if (!identity) { + throw new Error(`AgentIdentityApi: Failed to set metadata name due to Identity not found: ${didUri}`); + } + + if (identity.metadata.name === name) { + throw new Error('AgentIdentityApi: No changes detected.'); + } + + // Update the name in the Identity's metadata and store it + await this._store.set({ + id : identity.did.uri, + data : { ...identity.metadata, name }, + agent : this.agent, + tenant : identity.metadata.tenant, + updateExisting : true, + useCache : true + }); + } + /** * Returns the connected Identity, if one is available. * diff --git a/packages/agent/tests/identity-api.spec.ts b/packages/agent/tests/identity-api.spec.ts index 3ce0bcff2..3c038aa1d 100644 --- a/packages/agent/tests/identity-api.spec.ts +++ b/packages/agent/tests/identity-api.spec.ts @@ -440,6 +440,115 @@ describe('AgentIdentityApi', () => { }); }); + describe('setMetadataName', () => { + it('should update the name of an Identity', async () => { + const identity = await testHarness.agent.identity.create({ + metadata : { name: 'Test Identity' }, + didMethod : 'jwk', + didOptions : { + verificationMethods: [{ + algorithm: 'Ed25519' + }] + } + }); + expect(identity.metadata.name).to.equal('Test Identity'); + + // sanity fetch the identity + let storedIdentity = await testHarness.agent.identity.get({ didUri: identity.did.uri }); + expect(storedIdentity).to.exist; + expect(storedIdentity?.metadata.name).to.equal('Test Identity'); + + // update the identity + await testHarness.agent.identity.setMetadataName({ didUri: identity.did.uri, name: 'Updated Identity' }); + + // fetch the updated identity + storedIdentity = await testHarness.agent.identity.get({ didUri: identity.did.uri }); + expect(storedIdentity).to.exist; + expect(storedIdentity?.metadata.name).to.equal('Updated Identity'); + }); + + it('should throw if identity does not exist', async () => { + try { + await testHarness.agent.identity.setMetadataName({ didUri: 'did:method:xyz123', name: 'Updated Identity' }); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('AgentIdentityApi: Failed to set metadata name due to Identity not found'); + } + }); + + it('should throw if name is missing or empty', async () => { + const storeSpy = sinon.spy(testHarness.agent.identity['_store'], 'set'); + const identity = await testHarness.agent.identity.create({ + metadata : { name: 'Test Identity' }, + didMethod : 'jwk', + didOptions : { + verificationMethods: [{ + algorithm: 'Ed25519' + }] + } + }); + + expect(storeSpy.callCount).to.equal(1); + + try { + await testHarness.agent.identity.setMetadataName({ didUri: identity.did.uri, name: '' }); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('Failed to set metadata name due to missing name value'); + } + + try { + await testHarness.agent.identity.setMetadataName({ didUri: identity.did.uri, name: undefined! }); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('Failed to set metadata name due to missing name value'); + } + + // call count should not have changed + expect(storeSpy.callCount).to.equal(1); + + // sanity confirm the name did not change + const storedIdentity = await testHarness.agent.identity.get({ didUri: identity.did.uri }); + expect(storedIdentity).to.exist; + expect(storedIdentity?.metadata.name).to.equal('Test Identity'); + }); + + it('should throw if the updated name is the same as the current name', async () => { + const identity = await testHarness.agent.identity.create({ + metadata : { name: 'Test Identity' }, + didMethod : 'jwk', + didOptions : { + verificationMethods: [{ + algorithm: 'Ed25519' + }] + } + }); + + const storeSpy = sinon.spy(testHarness.agent.identity['_store'], 'set'); + + try { + await testHarness.agent.identity.setMetadataName({ didUri: identity.did.uri, name: 'Test Identity' }); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('AgentIdentityApi: No changes detected'); + } + + // confirm set has not been called + expect(storeSpy.notCalled).to.be.true; + + // sanity update the name to something else + await testHarness.agent.identity.setMetadataName({ didUri: identity.did.uri, name: 'Updated Identity' }); + + // confirm set has been called + expect(storeSpy.calledOnce).to.be.true; + + // confirm the name was updated + const storedIdentity = await testHarness.agent.identity.get({ didUri: identity.did.uri }); + expect(storedIdentity).to.exist; + expect(storedIdentity?.metadata.name).to.equal('Updated Identity'); + }); + }); + describe('connectedIdentity', () => { it('returns a connected Identity', async () => { // create multiple identities, some that are connected, and some that are not