@web5/agent
Version:
324 lines (270 loc) • 11.9 kB
text/typescript
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);
}
}