@web5/agent
Version:
429 lines (354 loc) • 15.2 kB
text/typescript
import type {
DidDocument,
DidMetadata,
PortableDid,
DidMethodApi,
DidDhtCreateOptions,
DidJwkCreateOptions,
DidResolutionResult,
DidResolutionOptions,
DidVerificationMethod,
DidResolverCache,
} from '@web5/dids';
import { BearerDid, Did, DidDht, UniversalResolver } from '@web5/dids';
import type { AgentDataStore } from './store-data.js';
import type { AgentKeyManager } from './types/key-manager.js';
import type { ResponseStatus, Web5PlatformAgent } from './types/agent.js';
import { InMemoryDidStore } from './store-did.js';
import { AgentDidResolverCache } from './agent-did-resolver-cache.js';
import { canonicalize } from '@web5/crypto';
export enum DidInterface {
Create = 'Create',
// Deactivate = 'Deactivate',
Resolve = 'Resolve',
// Update = 'Update'
}
export interface DidMessageParams {
[DidInterface.Create]: DidCreateParams;
// [DidInterface.Deactivate]: DidDeactivateParams;
[DidInterface.Resolve]: DidResolveParams;
// [DidInterface.Update]: DidUpdateParams;
}
export interface DidMessageResult {
[DidInterface.Create]: DidCreateResult;
// [DidInterface.Deactivate]: DidDeactivateResult;
[DidInterface.Resolve]: DidResolveResult;
// [DidInterface.Update]: DidUpdateResult;
}
export type DidCreateResult = {
uri: string;
document: DidDocument;
metadata: DidMetadata;
}
export type DidResolveResult = DidResolutionResult
export type DidRequest<T extends DidInterface> = {
messageType: T;
messageParams: DidMessageParams[T];
}
export type DidResolveParams = {
didUri: string;
options?: DidResolutionOptions;
}
export type DidResponse<T extends DidInterface> = ResponseStatus & {
result?: DidMessageResult[T];
};
export interface DidCreateParams<
TKeyManager = AgentKeyManager,
TMethod extends keyof DidMethodCreateOptions<TKeyManager> = keyof DidMethodCreateOptions<TKeyManager>
> {
method: TMethod;
options?: DidMethodCreateOptions<TKeyManager>[TMethod];
store?: boolean;
tenant?: string;
}
export interface DidMethodCreateOptions<TKeyManager> {
dht: DidDhtCreateOptions<TKeyManager>;
jwk: DidJwkCreateOptions<TKeyManager>;
}
export interface DidApiParams {
didMethods: DidMethodApi[];
agent?: Web5PlatformAgent;
/**
* An optional `DidResolverCache` instance used for caching resolved DID documents.
*
* Providing a cache implementation can significantly enhance resolution performance by avoiding
* redundant resolutions for previously resolved DIDs. If omitted, the default is an instance of `AgentDidResolverCache`.
*
* `AgentDidResolverCache` keeps a stale copy of the Agent's managed Identity DIDs and only refreshes upon a successful resolution.
* This allows for quick and offline access to the internal DIDs used by the agent.
*/
resolverCache?: DidResolverCache;
store?: AgentDataStore<PortableDid>;
}
export function isDidRequest<T extends DidInterface>(
didRequest: DidRequest<DidInterface>, messageType: T
): didRequest is DidRequest<T> {
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
* the `AgentDidApi`. 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;
private _didMethods: Map<string, DidMethodApi> = new Map();
private _store: AgentDataStore<PortableDid>;
constructor({ agent, didMethods, resolverCache, store }: DidApiParams) {
if (!didMethods) {
throw new TypeError(`AgentDidApi: Required parameter missing: 'didMethods'`);
}
// Initialize the DID resolver with the given DID methods and resolver cache, or use a default
// AgentDidResolverCache if none is provided.
super({
didResolvers : didMethods,
cache : resolverCache ?? new AgentDidResolverCache({ agent, location: 'DATA/AGENT/DID_CACHE' })
});
this._agent = agent;
// If `store` is not given, use an in-memory store by default.
this._store = store ?? new InMemoryDidStore();
for (const didMethod of didMethods) {
this._didMethods.set(didMethod.methodName, didMethod);
}
}
/**
* 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 {
if (this._agent === undefined) {
throw new Error('AgentDidApi: Unable to determine agent execution context.');
}
return this._agent;
}
set agent(agent: Web5PlatformAgent) {
this._agent = agent;
// AgentDidResolverCache should set the agent if it is the type of cache being used
if ('agent' in this.cache) {
this.cache.agent = agent;
}
}
public async create({
method, tenant, options, store
}: DidCreateParams<TKeyManager>): Promise<BearerDid> {
// Get the DID method implementation, which also verifies the method is supported.
const didMethod = this.getMethod(method);
// 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.
const { uri, document, metadata } = bearerDid;
const portableDid: PortableDid = { uri, document, metadata };
// 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.
await this._store.set({
id : portableDid.uri,
data : portableDid,
agent : this.agent,
tenant : tenant ?? portableDid.uri,
preventDuplicates : false,
useCache : true
});
}
return bearerDid;
}
public async export({ didUri, tenant }: {
didUri: string;
tenant?: string;
}): Promise<PortableDid> {
// Attempt to retrieve the DID from the agent's DID store.
const bearerDid = await this.get({ didUri, tenant });
if (!bearerDid) {
throw new Error(`AgentDidApi: Failed to export due to DID not found: ${didUri}`);
}
// If the DID was found, return the DID in a portable format, and if supported by the Agent's
// key manager, the private key material.
const portableDid = await bearerDid.export();
return portableDid;
}
public async get({ didUri, tenant }: {
didUri: string,
tenant?: string
}): Promise<BearerDid | undefined> {
const portableDid = await this._store.get({ id: didUri, agent: this.agent, tenant, useCache: true });
if (!portableDid) return undefined;
const bearerDid = await BearerDid.import({ portableDid, keyManager: this.agent.keyManager });
return bearerDid;
}
public async getSigningMethod({ didUri, methodId }: {
didUri: string;
methodId?: string;
}): Promise<DidVerificationMethod> {
// Verify the DID method is supported.
const parsedDid = Did.parse(didUri);
if (!parsedDid) {
throw new Error(`Invalid DID URI: ${didUri}`);
}
// Get the DID method implementation, which also verifies the method is supported.
const didMethod = this.getMethod(parsedDid.method);
// Resolve the DID document.
const { didDocument, didResolutionMetadata } = await this.resolve(didUri);
if (!didDocument) {
throw new Error(`DID resolution failed for '${didUri}': ${JSON.stringify(didResolutionMetadata)}`);
}
// Retrieve the method-specific verification method to be used for signing operations.
const verificationMethod = await didMethod.getSigningMethod({ didDocument, methodId });
return verificationMethod;
}
public async update({ tenant, portableDid, publish = true }: {
tenant?: string;
portableDid: PortableDid;
publish?: boolean;
}): Promise<BearerDid> {
// Check if the DID exists in the store.
const existingDid = await this.get({ didUri: portableDid.uri, tenant: tenant ?? portableDid.uri });
if (!existingDid) {
throw new Error(`AgentDidApi: Could not update, DID not found: ${portableDid.uri}`);
}
// If the document has not changed, abort the update.
if (canonicalize(portableDid.document) === canonicalize(existingDid.document)) {
throw new Error('AgentDidApi: No changes detected, update aborted');
}
// If private keys are present in the PortableDid, import the key material into the Agent's key
// manager. Validate that the key material for every verification method in the DID document is
// present in the key manager. If no keys are present, this will fail.
// NOTE: We currently do not delete the previous keys from the document.
// TODO: Add support for deleting the keys no longer present in the document.
const bearerDid = await BearerDid.import({ keyManager: this.agent.keyManager, portableDid });
// Only the DID URI, document, and metadata are stored in the Agent's DID store.
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 });
await this._store.set({
id : uri,
data : portableDidWithoutKeys,
agent : this.agent,
tenant : tenant ?? uri,
updateExisting : true,
useCache : true
});
if (publish) {
const parsedDid = Did.parse(uri);
// currently only supporting DHT as a publishable method.
// TODO: abstract this into the didMethod class so that other publishable methods can be supported.
if (parsedDid && parsedDid.method === 'dht') {
await DidDht.publish({ did: bearerDid });
}
}
return bearerDid;
}
public async import({ portableDid, tenant }: {
portableDid: PortableDid;
tenant?: string;
}): Promise<BearerDid> {
// If private keys are present in the PortableDid, import the key material into the Agent's key
// manager. Validate that the key material for every verification method in the DID document is
// present in the key manager.
const bearerDid = await BearerDid.import({ keyManager: this.agent.keyManager, portableDid });
// Only the DID URI, document, and metadata are stored in the Agent's DID store.
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.
await this._store.set({
id : portableDidWithoutKeys.uri,
data : portableDidWithoutKeys,
agent : this.agent,
tenant : tenant ?? portableDidWithoutKeys.uri,
preventDuplicates : true,
useCache : true
});
return bearerDid;
}
public async delete({ didUri, tenant, deleteKey = true }: {
didUri: string;
tenant?: string;
deleteKey?: boolean;
}): Promise<void> {
const portableDid = await this._store.get({ id: didUri, agent: this.agent, tenant, useCache: false });
if(!portableDid) {
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 });
if (deleteKey) {
// Delete the keys associated with the DID
// TODO: this could be made a static method on `BearerDid` class
await this.deleteKeys({ portableDid });
}
}
public async deleteKeys({ portableDid }: {
portableDid: PortableDid;
}): Promise<void> {
for (const verificationMethod of portableDid.document.verificationMethod || []) {
if (!verificationMethod.publicKeyJwk) {
continue;
}
// Compute the key URI of the verification method's public key.
const keyUri = await this.agent.keyManager.getKeyUri({ key: verificationMethod.publicKeyJwk });
await this.agent.keyManager.deleteKey({ keyUri });
}
}
public async processRequest<T extends DidInterface>(
request: DidRequest<T>
): Promise<DidResponse<T>> {
// Process Create DID request.
if (isDidRequest(request, DidInterface.Create)) {
try {
const bearerDid = await this.create({ ...request.messageParams });
const response: DidResponse<typeof request.messageType> = {
result: {
uri : bearerDid.uri,
document : bearerDid.document,
metadata : bearerDid.metadata,
},
ok : true,
status : { code: 201, message: 'Created' }
};
return response;
} catch (error: any) {
return {
ok : false,
status : { code: 500, message: error.message ?? 'Unknown error occurred' }
};
}
}
// Process Resolve DID request.
if (isDidRequest(request, DidInterface.Resolve)) {
const { didUri, options } = request.messageParams;
const resolutionResult = await this.resolve(didUri, options);
const response: DidResponse<typeof request.messageType> = {
result : resolutionResult,
ok : true,
status : { code: 200, message: 'OK' }
};
return response;
}
throw new Error(`AgentDidApi: Unsupported request type: ${request.messageType}`);
}
private getMethod(methodName: string): DidMethodApi {
const didMethodApi = this._didMethods.get(methodName);
if (didMethodApi === undefined) {
throw new Error(`DID Method not supported: ${methodName}`);
}
return didMethodApi;
}
}