UNPKG

@web5/agent

Version:
324 lines (270 loc) 11.9 kB
import type { RequireOnly } from '@web5/common'; import type { AgentDataStore } from './store-data.js'; import type { Web5PlatformAgent } from './types/agent.js'; import type { DidMethodCreateOptions } from './did-api.js'; import type { AgentKeyManager } from './types/key-manager.js'; 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 { PortableDid } from '@web5/dids'; export interface IdentityApiParams<TKeyManager extends AgentKeyManager> { agent?: Web5PlatformAgent<TKeyManager>; store?: AgentDataStore<IdentityMetadata>; } export interface IdentityCreateParams< TKeyManager = AgentKeyManager, TMethod extends keyof DidMethodCreateOptions<TKeyManager> = keyof DidMethodCreateOptions<TKeyManager> > { metadata: RequireOnly<IdentityMetadata, 'name'>; didMethod?: TMethod; didOptions?: DidMethodCreateOptions<TKeyManager>[TMethod]; store?: boolean; } export function isPortableIdentity(obj: unknown): obj is PortableIdentity { // Validate that the given value is an object that has the necessary properties of PortableIdentity. return !(!obj || typeof obj !== 'object' || obj === null) && 'did' in obj && 'metadata' in obj && 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 * the `AgentIdentityApi`. This agent is used to interact with other Web5 agent components. It's * vital to ensure this instance is set to correctly contextualize operations within the broader * Web5 Agent framework. */ private _agent?: Web5PlatformAgent<TKeyManager>; private _store: AgentDataStore<IdentityMetadata>; constructor({ agent, store }: IdentityApiParams<TKeyManager> = {}) { this._agent = agent; // If `store` is not given, use an in-memory store by default. this._store = store ?? new InMemoryIdentityStore(); } /** * Retrieves the `Web5PlatformAgent` execution context. * * @returns The `Web5PlatformAgent` instance that represents the current execution context. * @throws Will throw an error if the `agent` instance property is undefined. */ get agent(): Web5PlatformAgent<TKeyManager> { if (this._agent === undefined) { throw new Error('AgentIdentityApi: Unable to determine agent execution context.'); } return this._agent; } set agent(agent: Web5PlatformAgent<TKeyManager>) { this._agent = agent; } 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> { const bearerDid = await this.agent.did.create({ method : didMethod, options : didOptions, tenant : this.tenant, store, }); // Create the BearerIdentity object. const identity = new BearerIdentity({ did : bearerDid, metadata : { ...metadata, uri: bearerDid.uri, tenant: this.tenant } }); // Persist the Identity to the store, by default, unless the `store` option is set to false. if (store ?? true) { await this._store.set({ id : identity.did.uri, data : identity.metadata, agent : this.agent, tenant : identity.metadata.tenant, preventDuplicates : false, useCache : true }); } return identity; } public async export({ didUri }: { didUri: string; }): Promise<PortableIdentity> { const bearerIdentity = await this.get({ didUri }); if (!bearerIdentity) { throw new Error(`AgentIdentityApi: Failed to export due to Identity not found: ${didUri}`); } // If the Identity was found, return the Identity in a portable format, and if supported by the // Agent's key manager, the private key material. const portableIdentity = await bearerIdentity.export(); return portableIdentity; } public async get({ didUri }: { didUri: string; }): Promise<BearerIdentity | undefined> { 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; // Retrieve the DID from the Agent's DID store using the tenant value from the stored // Identity's metadata. const storedDid = await this.agent.did.get({ didUri, tenant: storedIdentity.tenant }); // If the Identity is present but the DID is not found, throw an error. if (!storedDid) { throw new Error(`AgentIdentityApi: Identity is present in the store but DID is missing: ${didUri}`); } // Create the BearerIdentity object. const identity = new BearerIdentity({ did: storedDid, metadata: storedIdentity }); return identity; } 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, tenant : portableIdentity.metadata.tenant }); // Verify the DID is present in the Agent's DID store. if (!storedDid) { throw new Error(`AgentIdentityApi: Failed to import 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, tenant : identity.metadata.tenant, preventDuplicates : true, useCache : true }); return identity; } public async list({ tenant }: { tenant?: string; } = {}): Promise<BearerIdentity[]> { // 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(metadata => this.get({ didUri: metadata.uri }))); return identities.filter(identity => typeof identity !== 'undefined') as BearerIdentity[]; } public async delete({ didUri }:{ didUri: string; }): Promise<void> { 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 }); } /** * Returns the DWN endpoints for the given DID. * * @param didUri - The DID URI to get the DWN endpoints for. * @returns An array of DWN endpoints. * @throws An error if the DID is not found, or no DWN service exists. */ public getDwnEndpoints({ didUri }: { didUri: string; }): Promise<string[]> { 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<void> { 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 = await bearerDid.export(); 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 }); } /** * 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. * * Accepts optional `connectedDid` parameter to filter the a specific connected identity, * if none is provided the first connected identity is returned. */ public async connectedIdentity({ connectedDid }:{ connectedDid?: string } = {}): Promise<BearerIdentity | undefined> { const identities = await this.list(); if (identities.length < 1) { return undefined; } // If a specific connected DID is provided, return the first identity that matches it. // Otherwise, return the first connected identity. return connectedDid ? identities.find(identity => identity.metadata.connectedDid === connectedDid) : identities.find(identity => identity.metadata.connectedDid !== undefined); } }