Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor IdentityApi to be scoped to the agent as a tenant. #911

Merged
merged 11 commits into from
Sep 26, 2024
5 changes: 5 additions & 0 deletions .changeset/fifty-beers-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/api": patch
---

Update usage of new IdentityApi behavior internally.
8 changes: 8 additions & 0 deletions .changeset/itchy-mayflies-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@web5/identity-agent": minor
"@web5/proxy-agent": minor
"@web5/user-agent": minor
"@web5/agent": minor
---

Simplify IdentityApi to be agent-focused and storing both the DID and IdentityMetadata under the agent's tenant.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"body-parser@<1.20.3": ">=1.20.3",
"send@<0.19.0": ">=0.19.0",
"serve-static@<1.16.0": ">=1.16.0",
"express@<4.20.0": ">=4.20.0"
"express@<4.20.0": ">=4.20.0",
"rollup@>=4.0.0 <4.22.4": ">=4.22.4"
}
}
}
15 changes: 15 additions & 0 deletions packages/agent/src/did-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ export function isDidRequest<T extends DidInterface>(
return didRequest.messageType === messageType;
}

/**
* This API is used to manage and interact with DIDs within the Web5 Agent framework.
*
* If a DWN Data Store is used, the DID information is stored under DID's own tenant by default.
* If a tenant property is passed, that tenant will be used to store the DID information.
*/
export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager> extends UniversalResolver {
/**
* Holds the instance of a `Web5PlatformAgent` that represents the current execution context for
Expand Down Expand Up @@ -170,6 +176,9 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
// Create the DID and store the generated keys in the Agent's key manager.
const bearerDid = await didMethod.create({ keyManager: this.agent.keyManager, options });

// pre-populate the resolution cache with the document and metadata
await this.cache.set(bearerDid.uri, { didDocument: bearerDid.document, didResolutionMetadata: { }, didDocumentMetadata: bearerDid.metadata });

// Persist the DID to the store, by default, unless the `store` option is set to false.
if (store ?? true) {
// Data stored in the Agent's DID store must be in PortableDid format.
Expand Down Expand Up @@ -260,6 +269,9 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
const { uri, document, metadata } = bearerDid;
const portableDidWithoutKeys: PortableDid = { uri, document, metadata };

// pre-populate the resolution cache with the document and metadata
await this.cache.set(uri, { didDocument: document, didResolutionMetadata: { }, didDocumentMetadata: metadata });

// Store the DID in the agent's DID store.
// Unless an existing `tenant` is specified, a record that includes the DID's URI, document,
// and metadata will be stored under a new tenant controlled by the imported DID.
Expand All @@ -285,6 +297,9 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
throw new Error('AgentDidApi: Could not delete, DID not found');
}

// delete from the cache
await this.cache.delete(didUri);

// Delete the data before deleting the associated keys.
await this._store.delete({ id: didUri, agent: this.agent, tenant });

Expand Down
87 changes: 34 additions & 53 deletions packages/agent/src/identity-api.ts
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export interface IdentityCreateParams<
metadata: RequireOnly<IdentityMetadata, 'name'>;
didMethod?: TMethod;
didOptions?: DidMethodCreateOptions<TKeyManager>[TMethod];
tenant?: string;
store?: boolean;
}

Expand All @@ -35,6 +34,16 @@ export function isPortableIdentity(obj: unknown): obj is PortableIdentity {
&& isPortableDid(obj.did);
}

/**
* This API is used to manage and interact with Identities within the Web5 Agent framework.
* An Identity is a DID that is associated with metadata that describes the Identity.
* Metadata includes A name(label), and whether or not the Identity is connected (delegated to act on the behalf of another DID).
*
* A KeyManager is used to manage the cryptographic keys associated with the Identities.
*
* The `DidApi` is used internally to create, store, and manage DIDs.
* When a DWN Data Store is used, the Identity and DID information are stored under the Agent DID's tenant.
*/
export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyManager> {
/**
* Holds the instance of a `Web5PlatformAgent` that represents the current execution context for
Expand Down Expand Up @@ -71,22 +80,29 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
this._agent = agent;
}

public async create({ metadata, didMethod = 'dht', didOptions, store, tenant }:
get tenant(): string {
if (!this._agent) {
throw new Error('AgentIdentityApi: The agent must be set to perform tenant specific actions.');
}

return this._agent.agentDid.uri;
}

public async create({ metadata, didMethod = 'dht', didOptions, store }:
IdentityCreateParams<TKeyManager>
): Promise<BearerIdentity> {
// Unless an existing `tenant` is specified, a record that includes the DID's URI, document,
// and metadata will be stored under a new tenant controlled by the newly created DID.

const bearerDid = await this.agent.did.create({
method : didMethod,
options : didOptions,
tenant : this.tenant,
store,
tenant
});

// Create the BearerIdentity object.
const identity = new BearerIdentity({
did : bearerDid,
metadata : { ...metadata, uri: bearerDid.uri, tenant: tenant ?? bearerDid.uri }
metadata : { ...metadata, uri: bearerDid.uri, tenant: this.tenant }
});

// Persist the Identity to the store, by default, unless the `store` option is set to false.
Expand All @@ -104,12 +120,10 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
return identity;
}

public async export({ didUri, tenant }: {
public async export({ didUri }: {
didUri: string;
tenant?: string;
}): Promise<PortableIdentity> {
// Attempt to retrieve the Identity from the Agent's Identity store.
const bearerIdentity = await this.get({ didUri, tenant });
const bearerIdentity = await this.get({ didUri });

if (!bearerIdentity) {
throw new Error(`AgentIdentityApi: Failed to export due to Identity not found: ${didUri}`);
Expand All @@ -122,12 +136,10 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
return portableIdentity;
}

public async get({ didUri, tenant }: {
public async get({ didUri }: {
didUri: string;
tenant?: string;
}): Promise<BearerIdentity | undefined> {
// Attempt to retrieve the Identity from the Agent's Identity store.
const storedIdentity = await this._store.get({ id: didUri, agent: this.agent, tenant, useCache: true });
const storedIdentity = await this._store.get({ id: didUri, agent: this.agent, useCache: true });

// If the Identity is not found in the store, return undefined.
if (!storedIdentity) return undefined;
Expand All @@ -150,6 +162,10 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
public async import({ portableIdentity }: {
portableIdentity: PortableIdentity;
}): Promise<BearerIdentity> {

// set the tenant of the portable identity to the agent's tenant
portableIdentity.metadata.tenant = this.tenant;

// Import the PortableDid to the Agent's DID store.
const storedDid = await this.agent.did.import({
portableDid : portableIdentity.portableDid,
Expand Down Expand Up @@ -183,56 +199,21 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana
// Retrieve the list of Identities from the Agent's Identity store.
const storedIdentities = await this._store.list({ agent: this.agent, tenant });

const identities = await Promise.all(
storedIdentities.map(async metadata => {
return this.get({ didUri: metadata.uri, tenant: metadata.tenant });
})
);
const identities = await Promise.all(storedIdentities.map(metadata => this.get({ didUri: metadata.uri })));

return identities.filter(identity => typeof identity !== 'undefined') as BearerIdentity[];
}

public async manage({ portableIdentity }: {
portableIdentity: PortableIdentity;
}): Promise<BearerIdentity> {
// Retrieve the DID using the `tenant` stored in the given Identity's metadata.
const storedDid = await this.agent.did.get({
didUri : portableIdentity.metadata.uri,
tenant : portableIdentity.metadata.tenant
});

// Verify the DID is present in the DID store.
if (!storedDid) {
throw new Error(`AgentIdentityApi: Failed to manage Identity: ${portableIdentity.metadata.uri}`);
}

// Create the BearerIdentity object.
const identity = new BearerIdentity({ did: storedDid, metadata: portableIdentity.metadata });

// Store the Identity metadata in the Agent's Identity store.
await this._store.set({
id : identity.did.uri,
data : identity.metadata,
agent : this.agent,
preventDuplicates : true,
useCache : true
});

return identity;
}

public async delete({ didUri, tenant }:{
public async delete({ didUri }:{
didUri: string;
tenant?: string;
}): Promise<void> {
// Attempt to retrieve the Identity from the Agent's Identity store.
const storedIdentity = await this._store.get({ id: didUri, agent: this.agent, tenant, useCache: true });
const storedIdentity = await this._store.get({ id: didUri, agent: this.agent, useCache: true });
if (!storedIdentity) {
throw new Error(`AgentIdentityApi: Failed to purge due to Identity not found: ${didUri}`);
}

// Delete the Identity from the Agent's Identity store.
await this._store.delete({ id: didUri, agent: this.agent, tenant });
await this._store.delete({ id: didUri, agent: this.agent });
}

/**
Expand Down
7 changes: 0 additions & 7 deletions packages/agent/src/test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,6 @@ export class PlatformAgentTestHarness {
return bearerIdentity;
}

public async preloadResolverCache({ didUri, resolutionResult }: {
didUri: string;
resolutionResult: DidResolutionResult;
}): Promise<void> {
await this.didResolverCache.set(didUri, resolutionResult);
}

public static async setup({ agentClass, agentStores, testDataLocation }: {
agentClass: new (params: any) => Web5PlatformAgent<LocalKeyManager>
agentStores?: 'dwn' | 'memory';
Expand Down
20 changes: 0 additions & 20 deletions packages/agent/tests/dwn-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1082,15 +1082,6 @@ describe('AgentDwnApi', () => {
}
};

await testHarness.preloadResolverCache({
didUri : testPortableIdentity.portableDid.uri,
resolutionResult : {
didDocument : testPortableIdentity.portableDid.document,
didDocumentMetadata : testPortableIdentity.portableDid.metadata,
didResolutionMetadata : {}
}
});

alice = await testHarness.agent.identity.import({
portableIdentity: testPortableIdentity
});
Expand Down Expand Up @@ -1840,17 +1831,6 @@ describe('AgentDwnApi', () => {
store : false
});

// Since the DID DHT document wasn't published, add the DID DHT document to the resolver
// cache so that DID resolution will succeed during the dereferencing operation.
await testHarness.preloadResolverCache({
didUri : identity.did.uri,
resolutionResult : {
didDocument : identity.did.document,
didDocumentMetadata : identity.did.metadata,
didResolutionMetadata : {}
}
});

try {
await testHarness.agent.dwn.sendRequest({
author : identity.did.uri,
Expand Down
Loading
Loading