Skip to content

Commit

Permalink
Add ability to update the Identity Metadata name field. (#956)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
LiranCohen authored Oct 21, 2024
1 parent 1334fae commit e7f5dfe
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .changeset/shy-grapes-juggle.md
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 33 additions & 0 deletions packages/agent/src/identity-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,39 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
await this.agent.did.update({ portableDid, tenant: this.agent.agentDid.uri });
}

/**
* Updates the Identity's metadata name field.
*
* @param didUri - The DID URI of the Identity to update.
* @param name - The new name to set for the Identity.
*
* @throws An error if the Identity is not found, name is not provided, or no changes are detected.
*/
public async setMetadataName({ didUri, name }: { didUri: string; name: string }): Promise<void> {
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.
*
Expand Down
109 changes: 109 additions & 0 deletions packages/agent/tests/identity-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e7f5dfe

Please sign in to comment.