Add comprehensive installation and setup documentation

- Add GETTING_STARTED.md with quick start guide and development modes
- Add INSTALL.sh automated installation script
- Add INSTALLATION_CHECKLIST.md, INSTALLATION_SUCCESS.md, and INSTALLATION_SUMMARY.md
- Add QUICK_REFERENCE.md for common commands
- Add SETUP_GUIDE.md with detailed setup instructions
- Update README.md with improved project overview
- Add did-wallet app dependencies and node_modules
This commit is contained in:
Dorian
2026-01-27 17:18:21 +00:00
parent a81f655133
commit 0d073fa89e
22658 changed files with 4494151 additions and 6 deletions
+280
View File
@@ -0,0 +1,280 @@
import type {
Jwk,
Signer,
CryptoApi,
KeyIdentifier,
EnclosedSignParams,
KmsExportKeyParams,
KmsImportKeyParams,
KeyImporterExporter,
EnclosedVerifyParams,
} from '@web5/crypto';
import { LocalKeyManager, utils as cryptoUtils } from '@web5/crypto';
import type { DidDocument } from './types/did-core.js';
import type { DidMetadata, PortableDid } from './types/portable-did.js';
import { DidError, DidErrorCode } from './did-error.js';
import { extractDidFragment, getVerificationMethods } from './utils.js';
/**
* A `BearerDidSigner` extends the {@link Signer} interface to include specific properties for
* signing with a Decentralized Identifier (DID). It encapsulates the algorithm and key identifier,
* which are often needed when signing JWTs, JWSs, JWEs, and other data structures.
*
* Typically, the algorithm and key identifier are used to populate the `alg` and `kid` fields of a
* JWT or JWS header.
*/
export interface BearerDidSigner extends Signer {
/**
* The cryptographic algorithm identifier used for signing operations.
*
* Typically, this value is used to populate the `alg` field of a JWT or JWS header. The
* registered algorithm names are defined in the
* {@link https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms | IANA JSON Web Signature and Encryption Algorithms registry}.
*
* @example
* "ES256" // ECDSA using P-256 and SHA-256
*/
algorithm: string;
/**
* The unique identifier of the key within the DID document that is used for signing and
* verification operations.
*
* This identifier must be a DID URI with a fragment (e.g., did:method:123#key-0) that references
* a specific verification method in the DID document. It allows users of a `BearerDidSigner` to
* determine the DID and key that will be used for signing and verification operations.
*
* @example
* "did:dht:123#key-1" // A fragment identifier referring to a key in the DID document
*/
keyId: string;
}
/**
* Represents a Decentralized Identifier (DID) along with its DID document, key manager, metadata,
* and convenience functions.
*/
export class BearerDid {
/** {@inheritDoc Did#uri} */
uri: string;
/**
* The DID document associated with this DID.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, § DID Document}
*/
document: DidDocument;
/** {@inheritDoc DidMetadata} */
metadata: DidMetadata;
/**
* Key Management System (KMS) used to manage the DIDs keys and sign data.
*
* Each DID method requires at least one key be present in the provided `keyManager`.
*/
keyManager: CryptoApi;
constructor({ uri, document, metadata, keyManager }: {
uri: string,
document: DidDocument,
metadata: DidMetadata,
keyManager: CryptoApi
}) {
this.uri = uri;
this.document = document;
this.metadata = metadata;
this.keyManager = keyManager;
}
/**
* Converts a `BearerDid` object to a portable format containing the URI and verification methods
* associated with the DID.
*
* This method is useful when you need to represent the key material and metadata associated with
* a DID in format that can be used independently of the specific DID method implementation. It
* extracts both public and private keys from the DID's key manager and organizes them into a
* `PortableDid` structure.
*
* @remarks
* If the DID's key manager does not allow private keys to be exported, the `PortableDid` returned
* will not contain a `privateKeys` property. This enables the importing and exporting DIDs that
* use the same underlying KMS even if the KMS does not support exporting private keys. Examples
* include hardware security modules (HSMs) and cloud-based KMS services like AWS KMS.
*
* If the DID's key manager does support exporting private keys, the resulting `PortableDid` will
* include a `privateKeys` property which contains the same number of entries as there are
* verification methods as the DID document, each with its associated private key and the
* purpose(s) for which the key can be used (e.g., `authentication`, `assertionMethod`, etc.).
*
* @example
* ```ts
* // Assuming `did` is an instance of BearerDid
* const portableDid = await did.export();
* // portableDid now contains the DID URI, document, metadata, and optionally, private keys.
* ```
*
* @returns A `PortableDid` containing the URI, DID document, metadata, and optionally private
* keys associated with the `BearerDid`.
* @throws An error if the DID document does not contain any verification methods or the keys for
* any verification method are missing in the key manager.
*/
public async export(): Promise<PortableDid> {
// Verify the DID document contains at least one verification method.
if (!(Array.isArray(this.document.verificationMethod) && this.document.verificationMethod.length > 0)) {
throw new Error(`DID document for '${this.uri}' is missing verification methods`);
}
// Create a new `PortableDid` object to store the exported data.
let portableDid: PortableDid = {
uri : this.uri,
document : this.document,
metadata : this.metadata
};
// If the BearerDid's key manager supports exporting private keys, add them to the portable DID.
if ('exportKey' in this.keyManager && typeof this.keyManager.exportKey === 'function') {
const privateKeys: Jwk[] = [];
for (let vm of this.document.verificationMethod) {
if (!vm.publicKeyJwk) {
throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`);
}
// Compute the key URI of the verification method's public key.
const keyUri = await this.keyManager.getKeyUri({ key: vm.publicKeyJwk });
// Retrieve the private key from the key manager.
const privateKey = await this.keyManager.exportKey({ keyUri }) as Jwk;
// Add the verification method to the key set.
privateKeys.push({ ...privateKey });
}
portableDid.privateKeys = privateKeys;
}
return portableDid;
}
/**
* Return a {@link Signer} that can be used to sign messages, credentials, or arbitrary data.
*
* If given, the `methodId` parameter is used to select a key from the verification methods
* present in the DID Document.
*
* If `methodID` is not given, the first verification method intended for signing claims is used.
*
* @param params - The parameters for the `getSigner` operation.
* @param params.methodId - ID of the verification method key that will be used for sign and
* verify operations. Optional.
* @returns An instantiated {@link Signer} that can be used to sign and verify data.
*/
public async getSigner(params?: { methodId: string }): Promise<BearerDidSigner> {
// Attempt to find a verification method that matches the given method ID, or if not given,
// find the first verification method intended for signing claims.
const verificationMethod = this.document.verificationMethod?.find(
vm => extractDidFragment(vm.id) === (extractDidFragment(params?.methodId) ?? extractDidFragment(this.document.assertionMethod?.[0]))
);
if (!(verificationMethod && verificationMethod.publicKeyJwk)) {
throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document');
}
// Compute the expected key URI of the signing key.
const keyUri = await this.keyManager.getKeyUri({ key: verificationMethod.publicKeyJwk });
// Get the public key to be used for verify operations, which also verifies that the key is
// present in the key manager's store.
const publicKey = await this.keyManager.getPublicKey({ keyUri });
// Bind the DID's key manager to the signer.
const keyManager = this.keyManager;
// Determine the signing algorithm.
const algorithm = cryptoUtils.getJoseSignatureAlgorithmFromPublicKey(publicKey);
return {
algorithm : algorithm,
keyId : verificationMethod.id,
async sign({ data }: EnclosedSignParams): Promise<Uint8Array> {
const signature = await keyManager.sign({ data, keyUri: keyUri! }); // `keyUri` is guaranteed to be defined at this point.
return signature;
},
async verify({ data, signature }: EnclosedVerifyParams): Promise<boolean> {
const isValid = await keyManager.verify({ data, key: publicKey!, signature }); // `publicKey` is guaranteed to be defined at this point.
return isValid;
}
};
}
/**
* Instantiates a {@link BearerDid} object from a given {@link PortableDid}.
*
* This method allows for the creation of a `BearerDid` object using a previously created DID's
* key material, DID document, and metadata.
*
* @example
* ```ts
* // Export an existing BearerDid to PortableDid format.
* const portableDid = await did.export();
* // Reconstruct a BearerDid object from the PortableDid.
* const did = await BearerDid.import({ portableDid });
* ```
*
* @param params - The parameters for the import operation.
* @param params.portableDid - The PortableDid object to import.
* @param params.keyManager - Optionally specify an external Key Management System (KMS) used to
* generate keys and sign data. If not given, a new
* {@link LocalKeyManager} instance will be created and
* used.
* @returns A Promise resolving to a `BearerDid` object representing the DID formed from the
* provided PortableDid.
* @throws An error if the PortableDid document does not contain any verification methods or the
* keys for any verification method are missing in the key manager.
*/
public static async import({ portableDid, keyManager = new LocalKeyManager() }: {
keyManager?: CryptoApi & KeyImporterExporter<KmsImportKeyParams, KeyIdentifier, KmsExportKeyParams>;
portableDid: PortableDid;
}): Promise<BearerDid> {
// Get all verification methods from the given DID document, including embedded methods.
const verificationMethods = getVerificationMethods({ didDocument: portableDid.document });
// Validate that the DID document contains at least one verification method.
if (verificationMethods.length === 0) {
throw new DidError(DidErrorCode.InvalidDidDocument, `At least one verification method is required but 0 were given`);
}
// If given, import the private key material into the key manager.
for (let key of portableDid.privateKeys ?? []) {
await keyManager.importKey({ key });
}
// Validate that the key material for every verification method in the DID document is present
// in the key manager.
for (let vm of verificationMethods) {
if (!vm.publicKeyJwk) {
throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`);
}
// Compute the key URI of the verification method's public key.
const keyUri = await keyManager.getKeyUri({ key: vm.publicKeyJwk });
// Verify that the key is present in the key manager. If not, an error is thrown.
await keyManager.getPublicKey({ keyUri });
}
// Use the given PortableDid to construct the BearerDid object.
const did = new BearerDid({
uri : portableDid.uri,
document : portableDid.document,
metadata : portableDid.metadata,
keyManager
});
return did;
}
}
+75
View File
@@ -0,0 +1,75 @@
/**
* A custom error class for DID-related errors.
*/
export class DidError extends Error {
/**
* Constructs an instance of DidError, a custom error class for handling DID-related errors.
*
* @param code - A {@link DidErrorCode} representing the specific type of error encountered.
* @param message - A human-readable description of the error.
*/
constructor(public code: DidErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = 'DidError';
// Ensures that instanceof works properly, the correct prototype chain when using inheritance,
// and that V8 stack traces (like Chrome, Edge, and Node.js) are more readable and relevant.
Object.setPrototypeOf(this, new.target.prototype);
// Captures the stack trace in V8 engines (like Chrome, Edge, and Node.js).
// In non-V8 environments, the stack trace will still be captured.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, DidError);
}
}
}
/**
* An enumeration of possible DID error codes.
*/
export enum DidErrorCode {
/** The DID supplied does not conform to valid syntax. */
InvalidDid = 'invalidDid',
/** The supplied method name is not supported by the DID method and/or DID resolver implementation. */
MethodNotSupported = 'methodNotSupported',
/** An unexpected error occurred during the requested DID operation. */
InternalError = 'internalError',
/** The DID document supplied does not conform to valid syntax. */
InvalidDidDocument = 'invalidDidDocument',
/** The byte length of a DID document does not match the expected value. */
InvalidDidDocumentLength = 'invalidDidDocumentLength',
/** The DID URL supplied to the dereferencing function does not conform to valid syntax. */
InvalidDidUrl = 'invalidDidUrl',
/** The given proof of a previous DID is invalid */
InvalidPreviousDidProof = 'invalidPreviousDidProof',
/** An invalid public key is detected during a DID operation. */
InvalidPublicKey = 'invalidPublicKey',
/** The byte length of a public key does not match the expected value. */
InvalidPublicKeyLength = 'invalidPublicKeyLength',
/** An invalid public key type was detected during a DID operation. */
InvalidPublicKeyType = 'invalidPublicKeyType',
/** Verification of a signature failed during a DID operation. */
InvalidSignature = 'invalidSignature',
/** The DID resolver was unable to find the DID document resulting from the resolution request. */
NotFound = 'notFound',
/**
* The representation requested via the `accept` input metadata property is not supported by the
* DID method and/or DID resolver implementation.
*/
RepresentationNotSupported = 'representationNotSupported',
/** The type of a public key is not supported by the DID method and/or DID resolver implementation. */
UnsupportedPublicKeyType = 'unsupportedPublicKeyType',
}
+186
View File
@@ -0,0 +1,186 @@
/**
* The `Did` class represents a Decentralized Identifier (DID) Uniform Resource Identifier (URI).
*
* This class provides a method for parsing a DID URI string into its component parts, as well as a
* method for serializing a DID URI object into a string.
*
* A DID URI is composed of the following components:
* - scheme
* - method
* - id
* - path
* - query
* - fragment
* - params
*
* @see {@link https://www.w3.org/TR/did-core/#did-syntax | DID Core Specification, § DID Syntax}
*/
export class Did {
/** Regular expression pattern for matching the method component of a DID URI. */
static readonly METHOD_PATTERN = '([a-z0-9]+)';
/** Regular expression pattern for matching percent-encoded characters in a method identifier. */
static readonly PCT_ENCODED_PATTERN = '(?:%[0-9a-fA-F]{2})';
/** Regular expression pattern for matching the characters allowed in a method identifier. */
static readonly ID_CHAR_PATTERN = `(?:[a-zA-Z0-9._-]|${Did.PCT_ENCODED_PATTERN})`;
/** Regular expression pattern for matching the method identifier component of a DID URI. */
static readonly METHOD_ID_PATTERN = `((?:${Did.ID_CHAR_PATTERN}*:)*(${Did.ID_CHAR_PATTERN}+))`;
/** Regular expression pattern for matching the path component of a DID URI. */
static readonly PATH_PATTERN = `(/[^#?]*)?`;
/** Regular expression pattern for matching the query component of a DID URI. */
static readonly QUERY_PATTERN = `([?][^#]*)?`;
/** Regular expression pattern for matching the fragment component of a DID URI. */
static readonly FRAGMENT_PATTERN = `(#.*)?`;
/** Regular expression pattern for matching all of the components of a DID URI. */
static readonly DID_URI_PATTERN = new RegExp(
`^did:(?<method>${Did.METHOD_PATTERN}):(?<id>${Did.METHOD_ID_PATTERN})(?<path>${Did.PATH_PATTERN})(?<query>${Did.QUERY_PATTERN})(?<fragment>${Did.FRAGMENT_PATTERN})$`
);
/**
* A string representation of the DID.
*
* A DID is a URI composed of three parts: the scheme `did:`, a method identifier, and a unique,
* method-specific identifier specified by the DID method.
*
* @example
* did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o
*/
uri: string;
/**
* The name of the DID method.
*
* Examples of DID method names are `dht`, `jwk`, and `web`, among others.
*/
method: string;
/**
* The DID method identifier.
*
* @example
* h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o
*/
id: string;
/**
* Optional path component of the DID URI.
*
* @example
* did:web:tbd.website/path
*/
path?: string;
/**
* Optional query component of the DID URI.
*
* @example
* did:web:tbd.website?versionId=1
*/
query?: string;
/**
* Optional fragment component of the DID URI.
*
* @example
* did:web:tbd.website#key-1
*/
fragment?: string;
/**
* Optional query parameters in the DID URI.
*
* @example
* did:web:tbd.website?service=files&relativeRef=/whitepaper.pdf
*/
params?: Record<string, string>;
/**
* Constructs a new `Did` instance from individual components.
*
* @param params - An object containing the parameters to be included in the DID URI.
* @param params.method - The name of the DID method.
* @param params.id - The DID method identifier.
* @param params.path - Optional. The path component of the DID URI.
* @param params.query - Optional. The query component of the DID URI.
* @param params.fragment - Optional. The fragment component of the DID URI.
* @param params.params - Optional. The query parameters in the DID URI.
*/
constructor({ method, id, path, query, fragment, params }: {
method: string,
id: string,
path?: string,
query?: string,
fragment?: string,
params?: Record<string, string>
}) {
this.uri = `did:${method}:${id}`;
this.method = method;
this.id = id;
this.path = path;
this.query = query;
this.fragment = fragment;
this.params = params;
}
/**
* Parses a DID URI string into its individual components.
*
* @example
* ```ts
* const did = Did.parse('did:example:123?service=agent&relativeRef=/credentials#degree');
*
* console.log(did.uri) // Output: 'did:example:123'
* console.log(did.method) // Output: 'example'
* console.log(did.id) // Output: '123'
* console.log(did.query) // Output: 'service=agent&relativeRef=/credentials'
* console.log(did.fragment) // Output: 'degree'
* console.log(did.params) // Output: { service: 'agent', relativeRef: '/credentials' }
* ```
*
* @params didUri - The DID URI string to be parsed.
* @returns A `Did` object representing the parsed DID URI, or `null` if the input string is not a valid DID URI.
*/
static parse(didUri: string): Did | null {
// Return null if the input string is empty or not provided.
if (!didUri) return null;
// Execute the regex pattern on the input string to extract URI components.
const match = Did.DID_URI_PATTERN.exec(didUri);
// If the pattern does not match, or if the required groups are not found, return null.
if (!match || !match.groups) return null;
// Extract the method, id, params, path, query, and fragment from the regex match groups.
const { method, id, path, query, fragment } = match.groups;
// Initialize a new Did object with the uri, method and id.
const did: Did = {
uri: `did:${method}:${id}`,
method,
id,
};
// If path is present, add it to the Did object.
if (path) did.path = path;
// If query is present, add it to the Did object, removing the leading '?'.
if (query) did.query = query.slice(1);
// If fragment is present, add it to the Did object, removing the leading '#'.
if (fragment) did.fragment = fragment.slice(1);
// If query params are present, parse them into a key-value object and add to the Did object.
if (query) {
const parsedParams = {} as Record<string, string>;
// Split the query string by '&' to get individual parameter strings.
const paramPairs = query.slice(1).split('&');
for (const pair of paramPairs) {
// Split each parameter string by '=' to separate keys and values.
const [key, value] = pair.split('=');
parsedParams[key] = value;
}
did.params = parsedParams;
}
return did;
}
}
+21
View File
@@ -0,0 +1,21 @@
export * from './types/did-core.js';
export * from './types/did-resolution.js';
export type * from './types/multibase.js';
export type * from './types/portable-did.js';
export * from './did.js';
export * from './did-error.js';
export * from './bearer-did.js';
export * from './methods/did-dht.js';
export * from './methods/did-ion.js';
export * from './methods/did-jwk.js';
export * from './methods/did-key.js';
export * from './methods/did-method.js';
export * from './methods/did-web.js';
export * from './resolver/resolver-cache-level.js';
export * from './resolver/resolver-cache-noop.js';
export * from './resolver/universal-resolver.js';
export * as utils from './utils.js';
File diff suppressed because it is too large Load Diff
+887
View File
@@ -0,0 +1,887 @@
import type { CryptoApi, Jwk, KeyIdentifier, KeyImporterExporter, KmsExportKeyParams, KmsImportKeyParams } from '@web5/crypto';
import type {
JwkEs256k,
IonDocumentModel,
IonPublicKeyModel,
IonPublicKeyPurpose,
} from '@decentralized-identity/ion-sdk';
import { IonDid, IonRequest } from '@decentralized-identity/ion-sdk';
import { LocalKeyManager, computeJwkThumbprint } from '@web5/crypto';
import type { PortableDid } from '../types/portable-did.js';
import type { DidCreateOptions, DidCreateVerificationMethod, DidRegistrationResult } from '../methods/did-method.js';
import type {
DidService,
DidDocument,
DidResolutionResult,
DidResolutionOptions,
DidVerificationMethod,
DidVerificationRelationship,
} from '../types/did-core.js';
import { Did } from '../did.js';
import { BearerDid } from '../bearer-did.js';
import { DidMethod } from '../methods/did-method.js';
import { DidError, DidErrorCode } from '../did-error.js';
import { getVerificationRelationshipsById } from '../utils.js';
import { EMPTY_DID_RESOLUTION_RESULT } from '../types/did-resolution.js';
/**
* Options for creating a Decentralized Identifier (DID) using the DID ION method.
*/
export interface DidIonCreateOptions<TKms> extends DidCreateOptions<TKms> {
/**
* Optional. The URI of a server involved in executing DID method operations. In the context of
* DID creation, the endpoint is expected to be a Sidetree node. If not specified, a default
* gateway node is used.
*/
gatewayUri?: string;
/**
* Optional. Determines whether the created DID should be published to a Sidetree node.
*
* If set to `true` or omitted, the DID is publicly discoverable. If `false`, the DID is not
* published and cannot be resolved by others. By default, newly created DIDs are published.
*
* @see {@link https://identity.foundation/sidetree/spec/#create | Sidetree Protocol Specification, § Create}
*
* @example
* ```ts
* const did = await DidIon.create({
* options: {
* publish: false
* };
* ```
*/
publish?: boolean;
/**
* Optional. An array of service endpoints associated with the DID.
*
* Services are used in DID documents to express ways of communicating with the DID subject or
* associated entities. A service can be any type of service the DID subject wants to advertise,
* including decentralized identity management services for further discovery, authentication,
* authorization, or interaction.
*
* @see {@link https://www.w3.org/TR/did-core/#services | DID Core Specification, § Services}
*
* @example
* ```ts
* const did = await DidIon.create({
* options: {
* services: [
* {
* id: 'dwn',
* type: 'DecentralizedWebNode',
* serviceEndpoint: ['https://example.com/dwn1', 'https://example/dwn2']
* }
* ]
* };
* ```
*/
services?: DidService[];
/**
* Optional. An array of verification methods to be included in the DID document.
*
* By default, a newly created DID ION document will contain a single Ed25519 verification method.
* Additional verification methods can be added to the DID document using the
* `verificationMethods` property.
*
* @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods}
*
* @example
* ```ts
* const did = await DidIon.create({
* options: {
* verificationMethods: [
* {
* algorithm: 'Ed25519',
* purposes: ['authentication', 'assertionMethod']
* },
* {
* algorithm: 'Ed25519',
* id: 'dwn-sig',
* purposes: ['authentication', 'assertionMethod']
* }
* ]
* };
* ```
*/
verificationMethods?: DidCreateVerificationMethod<TKms>[];
}
/**
* Represents the request model for managing DID documents within the ION network, according to the
* Sidetree protocol specification.
*/
export interface DidIonCreateRequest {
/** The type of operation to perform, which is always 'create' for a Create Operation. */
type: 'create';
/** Contains properties related to the initial state of the DID document. */
suffixData: {
/** A hash of the `delta` object, representing the initial changes to the DID document. */
deltaHash: string;
/** A commitment value used for future recovery operations, hashed for security. */
recoveryCommitment: string;
};
/** Details the changes to be applied to the DID document in this operation. */
delta: {
/** A commitment value used for the next update operation, hashed for security. */
updateCommitment: string;
/** An array of patch objects specifying the modifications to apply to the DID document. */
patches: {
/** The type of modification to perform (e.g., adding or removing public keys or service
* endpoints). */
action: string;
/** The document state or partial state to apply with this patch. */
document: IonDocumentModel;
}[];
}
}
/**
* Represents a {@link DidVerificationMethod | DID verification method} in the context of DID ION
* create, update, deactivate, and resolve operations.
*
* Unlike the DID Core standard {@link DidVerificationMethod} interface, this type is specific to
* the ION method operations and only includes the `id`, `publicKeyJwk`, and `purposes` properties:
* - The `id` property is optional and specifies the identifier fragment of the verification method.
* - The `publicKeyJwk` property is required and represents the public key in JWK format.
* - The `purposes` property is required and specifies the purposes for which the verification
* method can be used.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* id : 'sig',
* publicKeyJwk : {
* kty : 'OKP',
* crv : 'Ed25519',
* x : 'o40shZrsco-CfEqk6mFsXfcP94ly3Az3gm84PzAUsXo',
* kid : 'BDp0xim82GswlxnPV8TPtBdUw80wkGIF8gjFbw1x5iQ',
* },
* purposes: ['authentication', 'assertionMethod']
* };
* ```
*/
export interface DidIonVerificationMethod {
/**
* Optionally specify the identifier fragment of the verification method.
*
* If not specified, the method's ID will be generated from the key's ID or thumbprint.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* id: 'sig',
* ...
* };
* ```
*/
id?: string;
/**
* A public key in JWK format.
*
* A JSON Web Key (JWK) that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* publicKeyJwk: {
* kty : "OKP",
* crv : "X25519",
* x : "7XdJtNmJ9pV_O_3mxWdn6YjiHJ-HhNkdYQARzVU_mwY",
* kid : "xtsuKULPh6VN9fuJMRwj66cDfQyLaxuXHkMlmAe_v6I"
* },
* ...
* };
* ```
*/
publicKeyJwk: Jwk;
/**
* Specify the purposes for which a verification method is intended to be used in a DID document.
*
* The `purposes` property defines the specific
* {@link DidVerificationRelationship | verification relationships} between the DID subject and
* the verification method. This enables the verification method to be utilized for distinct
* actions such as authentication, assertion, key agreement, capability delegation, and others. It
* is important for verifiers to recognize that a verification method must be associated with the
* relevant purpose in the DID document to be valid for that specific use case.
*
* @example
* ```ts
* const verificationMethod: DidIonVerificationMethod = {
* purposes: ['authentication', 'assertionMethod'],
* ...
* };
* ```
*/
purposes: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[];
}
/**
* `IonPortableDid` interface extends the {@link PortableDid} interface.
*
* It represents a Decentralized Identifier (DID) that is portable and can be used across different
* domains, including the ION specific recovery and update keys.
*/
export interface IonPortableDid extends PortableDid {
/** The JSON Web Key (JWK) used for recovery purposes. */
recoveryKey: Jwk;
/** The JSON Web Key (JWK) used for updating the DID. */
updateKey: Jwk;
}
/**
* Enumerates the types of keys that can be used in a DID ION document.
*
* The DID ION method supports various cryptographic key types. These key types are essential for
* the creation and management of DIDs and their associated cryptographic operations like signing
* and encryption.
*/
export enum DidIonRegisteredKeyType {
/**
* Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature
* Algorithm) and Curve25519.
*/
Ed25519 = 'Ed25519',
/**
* secp256k1: A cryptographic curve used for digital signatures in a range of decentralized
* systems.
*/
secp256k1 = 'secp256k1',
/**
* secp256r1: Also known as P-256 or prime256v1, this curve is used for cryptographic operations
* and is widely supported in various cryptographic libraries and standards.
*/
secp256r1 = 'secp256r1',
/**
* X25519: A Diffie-Hellman key exchange algorithm using Curve25519.
*/
X25519 = 'X25519'
}
/**
* Private helper that maps algorithm identifiers to their corresponding DID ION
* {@link DidIonRegisteredKeyType | registered key type}.
*/
const AlgorithmToKeyTypeMap = {
Ed25519 : DidIonRegisteredKeyType.Ed25519,
ES256K : DidIonRegisteredKeyType.secp256k1,
ES256 : DidIonRegisteredKeyType.secp256r1,
'P-256' : DidIonRegisteredKeyType.secp256r1,
secp256k1 : DidIonRegisteredKeyType.secp256k1,
secp256r1 : DidIonRegisteredKeyType.secp256r1
} as const;
/**
* The default node to use as a gateway to the Sidetree newtork when anchoring, updating, and
* resolving DID documents.
*/
const DEFAULT_GATEWAY_URI = 'https://ion.tbd.engineering';
/**
* The `DidIon` class provides an implementation of the `did:ion` DID method.
*
* Features:
* - DID Creation: Create new `did:ion` DIDs.
* - DID Key Management: Instantiate a DID object from an existing key in a Key Management System
* (KMS). If supported by the KMS, a DID's key can be exported to a portable
* DID format.
* - DID Resolution: Resolve a `did:ion` to its corresponding DID Document stored in the Sidetree
* network.
* - Signature Operations: Sign and verify messages using keys associated with a DID.
*
* @see {@link https://identity.foundation/sidetree/spec/ | Sidetree Protocol Specification}
* @see {@link https://github.com/decentralized-identity/ion/blob/master/docs/design.md | ION Design Document}
*
* @example
* ```ts
* // DID Creation
* const did = await DidIon.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidIon.create({ keyManager });
*
* // DID Resolution
* const resolutionResult = await DidIon.resolve({ did: did.uri });
*
* // Signature Operations
* const signer = await did.getSigner();
* const signature = await signer.sign({ data: new TextEncoder().encode('Message') });
* const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature });
*
* // Key Management
*
* // Instantiate a DID object for a published DID with existing keys in a KMS
* const did = await DidIon.fromKeyManager({
* didUri: 'did:ion:EiAzB7K-xDIKc1csXo5HX2eNBoemK9feNhL3cKwfukYOug',
* keyManager
* });
*
* // Convert a DID object to a portable format
* const portableDid = await DidIon.toKeys({ did });
* ```
*/
export class DidIon extends DidMethod {
/**
* Name of the DID method, as defined in the DID ION specification.
*/
public static methodName = 'ion';
/**
* Creates a new DID using the `did:ion` method formed from a newly generated key.
*
* Notes:
* - If no `options` are given, by default a new Ed25519 key will be generated.
*
* @example
* ```ts
* // DID Creation
* const did = await DidIon.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidIon.create({ keyManager });
* ```
*
* @param params - The parameters for the create operation.
* @param params.keyManager - Optionally specify a Key Management System (KMS) used to generate
* keys and sign data.
* @param params.options - Optional parameters that can be specified when creating a new DID.
* @returns A Promise resolving to a {@link BearerDid} object representing the new DID.
*/
public static async create<TKms extends CryptoApi | undefined = undefined>({
keyManager = new LocalKeyManager(),
options = {}
}: {
keyManager?: TKms;
options?: DidIonCreateOptions<TKms>;
} = {}): Promise<BearerDid> {
// Before processing the create operation, validate DID-method-specific requirements to prevent
// keys from being generated unnecessarily.
// Check 1: Validate that the algorithm for any given verification method is supported by the
// DID ION specification.
if (options.verificationMethods?.some(vm => !(vm.algorithm in AlgorithmToKeyTypeMap))) {
throw new Error('One or more verification method algorithms are not supported');
}
// Check 2: Validate that the ID for any given verification method is unique.
const methodIds = options.verificationMethods?.filter(vm => 'id' in vm).map(vm => vm.id);
if (methodIds && methodIds.length !== new Set(methodIds).size) {
throw new Error('One or more verification method IDs are not unique');
}
// Check 3: Validate that the required properties for any given services are present.
if (options.services?.some(s => !s.id || !s.type || !s.serviceEndpoint)) {
throw new Error('One or more services are missing required properties');
}
// If no verification methods were specified, generate a default Ed25519 verification method.
const defaultVerificationMethod: DidCreateVerificationMethod<TKms> = {
algorithm : 'Ed25519' as any,
purposes : ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation']
};
const verificationMethodsToAdd: DidIonVerificationMethod[] = [];
// Generate random key material for additional verification methods, if any.
for (const vm of options.verificationMethods ?? [defaultVerificationMethod]) {
// Generate a random key for the verification method.
const keyUri = await keyManager.generateKey({ algorithm: vm.algorithm });
const publicKey = await keyManager.getPublicKey({ keyUri });
// Add the verification method to the DID document.
verificationMethodsToAdd.push({
id : vm.id,
publicKeyJwk : publicKey,
purposes : vm.purposes ?? ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation']
});
}
// Generate a random key for the ION Recovery Key. Sidetree requires secp256k1 recovery keys.
const recoveryKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 });
const recoveryKey = await keyManager.getPublicKey({ keyUri: recoveryKeyUri });
// Generate a random key for the ION Update Key. Sidetree requires secp256k1 update keys.
const updateKeyUri = await keyManager.generateKey({ algorithm: DidIonRegisteredKeyType.secp256k1 });
const updateKey = await keyManager.getPublicKey({ keyUri: updateKeyUri });
// Compute the Long Form DID URI from the keys and services, if any.
const longFormDidUri = await DidIonUtils.computeLongFormDidUri({
recoveryKey,
updateKey,
services : options.services ?? [],
verificationMethods : verificationMethodsToAdd
});
// Expand the DID URI string to a DID document.
const { didDocument, didResolutionMetadata } = await DidIon.resolve(longFormDidUri, { gatewayUri: options.gatewayUri });
if (didDocument === null) {
throw new Error(`Unable to resolve DID during creation: ${didResolutionMetadata?.error}`);
}
// Create the BearerDid object, including the "Short Form" of the DID URI, the ION update and
// recovery keys, and specifying that the DID has not yet been published.
const did = new BearerDid({
uri : longFormDidUri,
document : didDocument,
metadata : {
published : false,
canonicalId : longFormDidUri.split(':', 3).join(':'),
recoveryKey,
updateKey
},
keyManager
});
// By default, publish the DID document to a Sidetree node unless explicitly disabled.
if (options.publish ?? true) {
const registrationResult = await DidIon.publish({ did, gatewayUri: options.gatewayUri });
did.metadata = registrationResult.didDocumentMetadata;
}
return did;
}
/**
* Given the W3C DID Document of a `did:ion` DID, return the verification method that will be used
* for signing messages and credentials. If given, the `methodId` parameter is used to select the
* verification method. If not given, the first verification method in the authentication property
* in the DID Document is used.
*
* @param params - The parameters for the `getSigningMethod` operation.
* @param params.didDocument - DID Document to get the verification method from.
* @param params.methodId - ID of the verification method to use for signing.
* @returns Verification method to use for signing.
*/
public static async getSigningMethod({ didDocument, methodId }: {
didDocument: DidDocument;
methodId?: string;
}): Promise<DidVerificationMethod> {
// Verify the DID method is supported.
const parsedDid = Did.parse(didDocument.id);
if (parsedDid && parsedDid.method !== this.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`);
}
// Get the verification method with either the specified ID or the first assertion method.
const verificationMethod = didDocument.verificationMethod?.find(
vm => vm.id === (methodId ?? didDocument.assertionMethod?.[0])
);
if (!(verificationMethod && verificationMethod.publicKeyJwk)) {
throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document');
}
return verificationMethod;
}
/**
* Instantiates a {@link BearerDid} object for the DID ION method from a given {@link PortableDid}.
*
* This method allows for the creation of a `BearerDid` object using a previously created DID's
* key material, DID document, and metadata.
*
* @example
* ```ts
* // Export an existing BearerDid to PortableDid format.
* const portableDid = await did.export();
* // Reconstruct a BearerDid object from the PortableDid.
* const did = await DidIon.import({ portableDid });
* ```
*
* @param params - The parameters for the import operation.
* @param params.portableDid - The PortableDid object to import.
* @param params.keyManager - Optionally specify an external Key Management System (KMS) used to
* generate keys and sign data. If not given, a new
* {@link LocalKeyManager} instance will be created and
* used.
* @returns A Promise resolving to a `BearerDid` object representing the DID formed from the
* provided PortableDid.
* @throws An error if the DID document does not contain any verification methods or the keys for
* any verification method are missing in the key manager.
*/
public static async import({ portableDid, keyManager = new LocalKeyManager() }: {
keyManager?: CryptoApi & KeyImporterExporter<KmsImportKeyParams, KeyIdentifier, KmsExportKeyParams>;
portableDid: PortableDid;
}): Promise<BearerDid> {
// Verify the DID method is supported.
const parsedDid = Did.parse(portableDid.uri);
if (parsedDid?.method !== DidIon.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`);
}
const did = await BearerDid.import({ portableDid, keyManager });
return did;
}
/**
* Publishes a DID to a Sidetree node, making it publicly discoverable and resolvable.
*
* This method handles the publication of a DID Document associated with a `did:ion` DID to a
* Sidetree node.
*
* @remarks
* - This method is typically invoked automatically during the creation of a new DID unless the
* `publish` option is set to `false`.
* - For existing, unpublished DIDs, it can be used to publish the DID Document to a Sidetree node.
* - The method relies on the specified Sidetree node to interface with the network.
*
* @param params - The parameters for the `publish` operation.
* @param params.did - The `BearerDid` object representing the DID to be published.
* @param params.gatewayUri - Optional. The URI of a server involved in executing DID
* method operations. In the context of publishing, the
* endpoint is expected to be a Sidetree node. If not
* specified, a default node is used.
* @returns A Promise resolving to a boolean indicating whether the publication was successful.
*
* @example
* ```ts
* // Generate a new DID and keys but explicitly disable publishing.
* const did = await DidIon.create({ options: { publish: false } });
* // Publish the DID to the Sidetree network.
* const isPublished = await DidIon.publish({ did });
* // `isPublished` is true if the DID was successfully published.
* ```
*/
public static async publish({ did, gatewayUri = DEFAULT_GATEWAY_URI }: {
did: BearerDid;
gatewayUri?: string;
}): Promise<DidRegistrationResult> {
// Construct an ION verification method made up of the id, public key, and purposes from each
// verification method in the DID document.
const verificationMethods: DidIonVerificationMethod[] = did.document.verificationMethod?.map(
vm => ({
id : vm.id,
publicKeyJwk : vm.publicKeyJwk!,
purposes : getVerificationRelationshipsById({ didDocument: did.document, methodId: vm.id })
})
) ?? [];
// Create the ION document.
const ionDocument = await DidIonUtils.createIonDocument({
services: did.document.service ?? [],
verificationMethods
});
// Construct the ION Create Operation request.
const createOperation = await DidIonUtils.constructCreateRequest({
ionDocument,
recoveryKey : did.metadata.recoveryKey,
updateKey : did.metadata.updateKey
});
try {
// Construct the URL of the SideTree node's operations endpoint.
const operationsUrl = DidIonUtils.appendPathToUrl({
baseUrl : gatewayUri,
path : `/operations`
});
// Submit the Create Operation to the operations endpoint.
const response = await fetch(operationsUrl, {
method : 'POST',
mode : 'cors',
headers : { 'Content-Type': 'application/json' },
body : JSON.stringify(createOperation)
});
// Return the result of processing the Create operation, including the updated DID metadata
// with the publishing result.
return {
didDocument : did.document,
didDocumentMetadata : {
...did.metadata,
published: response.ok,
},
didRegistrationMetadata: {}
};
} catch (error: any) {
return {
didDocument : null,
didDocumentMetadata : {
published: false,
},
didRegistrationMetadata: {
error : DidErrorCode.InternalError,
errorMessage : `Failed to publish DID document for: ${did.uri}`
}
};
}
}
/**
* Resolves a `did:ion` identifier to its corresponding DID document.
*
* This method performs the resolution of a `did:ion` DID, retrieving its DID Document from the
* Sidetree-based DID overlay network. The process involves querying a Sidetree node to retrieve
* the DID Document that corresponds to the given DID identifier.
*
* @remarks
* - If a `gatewayUri` option is not specified, a default node is used to access the Sidetree
* network.
* - It decodes the DID identifier and retrieves the associated DID Document and metadata.
* - In case of resolution failure, appropriate error information is returned.
*
* @example
* ```ts
* const resolutionResult = await DidIon.resolve('did:ion:example');
* ```
*
* @param didUri - The DID to be resolved.
* @param options - Optional parameters for resolving the DID. Unused by this DID method.
* @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution.
*/
public static async resolve(didUri: string, options: DidResolutionOptions = {}): Promise<DidResolutionResult> {
// Attempt to parse the DID URI.
const parsedDid = Did.parse(didUri);
// If parsing failed, the DID is invalid.
if (!parsedDid) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'invalidDid' }
};
}
// If the DID method is not "ion", return an error.
if (parsedDid.method !== DidIon.methodName) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'methodNotSupported' }
};
}
// To execute the read method operation, use the given gateway URI or a default Sidetree node.
const gatewayUri = options?.gatewayUri ?? DEFAULT_GATEWAY_URI;
try {
// Construct the URL to be used in the resolution request.
const resolutionUrl = DidIonUtils.appendPathToUrl({
baseUrl : gatewayUri,
path : `/identifiers/${didUri}`
});
// Attempt to retrieve the DID document and metadata from the Sidetree node.
const response = await fetch(resolutionUrl);
// If the DID document was not found, return an error.
if (!response.ok) {
throw new DidError(DidErrorCode.NotFound, `Unable to find DID document for: ${didUri}`);
}
// If the DID document was retrieved successfully, return it.
const { didDocument, didDocumentMetadata } = await response.json() as DidResolutionResult;
return {
...EMPTY_DID_RESOLUTION_RESULT,
...didDocument && { didDocument },
didDocumentMetadata: {
published: didDocumentMetadata?.method?.published,
...didDocumentMetadata
}
};
} catch (error: any) {
// Rethrow any unexpected errors that are not a `DidError`.
if (!(error instanceof DidError)) throw new Error(error);
// Return a DID Resolution Result with the appropriate error code.
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: {
error: error.code,
...error.message && { errorMessage: error.message }
}
};
}
}
}
/**
* The `DidIonUtils` class provides utility functions to support operations in the DID ION method.
*/
export class DidIonUtils {
/**
* Appends a specified path to a base URL, ensuring proper formatting of the resulting URL.
*
* This method is useful for constructing URLs for accessing various endpoints, such as Sidetree
* nodes in the ION network. It handles the nuances of URL path concatenation, including the
* addition or removal of leading/trailing slashes, to create a well-formed URL.
*
* @param params - The parameters for URL construction.
* @param params.baseUrl - The base URL to which the path will be appended.
* @param params.path - The path to append to the base URL.
* @returns The fully constructed URL string with the path appended to the base URL.
*/
public static appendPathToUrl({ baseUrl, path }: {
baseUrl: string;
path: string;
}): string {
const url = new URL(baseUrl);
url.pathname = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname += path.startsWith('/') ? path.substring(1) : path;
return url.toString();
}
/**
* Computes the Long Form DID URI given an ION DID's recovery key, update key, services, and
* verification methods.
*
* @param params - The parameters for computing the Long Form DID URI.
* @param params.recoveryKey - The ION Recovery Key.
* @param params.updateKey - The ION Update Key.
* @param params.services - An array of services associated with the DID.
* @param params.verificationMethods - An array of verification methods associated with the DID.
* @returns A Promise resolving to the Long Form DID URI.
*/
public static async computeLongFormDidUri({ recoveryKey, updateKey, services, verificationMethods }: {
recoveryKey: Jwk;
updateKey: Jwk;
services: DidService[];
verificationMethods: DidIonVerificationMethod[];
}): Promise<string> {
// Create the ION document.
const ionDocument = await DidIonUtils.createIonDocument({ services, verificationMethods });
// Normalize JWK to onnly include specific members and in lexicographic order.
const normalizedRecoveryKey = DidIonUtils.normalizeJwk(recoveryKey);
const normalizedUpdateKey = DidIonUtils.normalizeJwk(updateKey);
// Compute the Long Form DID URI.
const longFormDidUri = await IonDid.createLongFormDid({
document : ionDocument,
recoveryKey : normalizedRecoveryKey as JwkEs256k,
updateKey : normalizedUpdateKey as JwkEs256k
});
return longFormDidUri;
}
/**
* Constructs a Sidetree Create Operation request for a DID document within the ION network.
*
* This method prepares the necessary payload for submitting a Create Operation to a Sidetree
* node, encapsulating the details of the DID document, recovery key, and update key.
*
* @param params - Parameters required to construct the Create Operation request.
* @param params.ionDocument - The DID document model containing public keys and service endpoints.
* @param params.recoveryKey - The recovery public key in JWK format.
* @param params.updateKey - The update public key in JWK format.
* @returns A promise resolving to the ION Create Operation request model, ready for submission to a Sidetree node.
*/
public static async constructCreateRequest({ ionDocument, recoveryKey, updateKey }: {
ionDocument: IonDocumentModel,
recoveryKey: Jwk,
updateKey: Jwk
}): Promise<DidIonCreateRequest> {
// Create an ION DID create request operation.
const createRequest = await IonRequest.createCreateRequest({
document : ionDocument,
recoveryKey : DidIonUtils.normalizeJwk(recoveryKey) as JwkEs256k,
updateKey : DidIonUtils.normalizeJwk(updateKey) as JwkEs256k
}) as DidIonCreateRequest;
return createRequest;
}
/**
* Assembles an ION document model from provided services and verification methods
*
* This model serves as the foundation for a DID document in the ION network, facilitating the
* creation and management of decentralized identities. It translates service endpoints and
* public keys into a format compatible with the Sidetree protocol, ensuring the resulting DID
* document adheres to the required specifications for ION DIDs. This method is essential for
* constructing the payload needed to register or update DIDs within the ION network.
*
* @param params - The parameters containing the services and verification methods to include in the ION document.
* @param params.services - A list of service endpoints to be included in the DID document, specifying ways to interact with the DID subject.
* @param params.verificationMethods - A list of verification methods to be included, detailing the cryptographic keys and their intended uses within the DID document.
* @returns A Promise resolving to an `IonDocumentModel`, ready for use in Sidetree operations like DID creation and updates.
*/
public static async createIonDocument({ services, verificationMethods }: {
services: DidService[];
verificationMethods: DidIonVerificationMethod[]
}): Promise<IonDocumentModel> {
/**
* STEP 1: Convert verification methods to ION SDK format.
*/
const ionPublicKeys: IonPublicKeyModel[] = [];
for (const vm of verificationMethods) {
// Use the given ID, the key's ID, or the key's thumbprint as the verification method ID.
let methodId = vm.id ?? vm.publicKeyJwk.kid ?? await computeJwkThumbprint({ jwk: vm.publicKeyJwk });
methodId = `${methodId.split('#').pop()}`; // Remove fragment prefix, if any.
// Convert public key JWK to ION format.
const publicKey: IonPublicKeyModel = {
id : methodId,
publicKeyJwk : DidIonUtils.normalizeJwk(vm.publicKeyJwk),
purposes : vm.purposes as IonPublicKeyPurpose[],
type : 'JsonWebKey2020'
};
ionPublicKeys.push(publicKey);
}
/**
* STEP 2: Convert service entries, if any, to ION SDK format.
*/
const ionServices = services.map(service => ({
...service,
id: `${service.id.split('#').pop()}` // Remove fragment prefix, if any.
}));
/**
* STEP 3: Format as ION document.
*/
const ionDocumentModel: IonDocumentModel = {
publicKeys : ionPublicKeys,
services : ionServices
};
return ionDocumentModel;
}
/**
* Normalize the given JWK to include only specific members and in lexicographic order.
*
* @param jwk - The JWK to normalize.
* @returns The normalized JWK.
*/
private static normalizeJwk(jwk: Jwk): Jwk {
const keyType = jwk.kty;
let normalizedJwk: Jwk;
if (keyType === 'EC') {
normalizedJwk = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
} else if (keyType === 'oct') {
normalizedJwk = { k: jwk.k, kty: jwk.kty };
} else if (keyType === 'OKP') {
normalizedJwk = { crv: jwk.crv, kty: jwk.kty, x: jwk.x };
} else if (keyType === 'RSA') {
normalizedJwk = { e: jwk.e, kty: jwk.kty, n: jwk.n };
} else {
throw new Error(`Unsupported key type: ${keyType}`);
}
return normalizedJwk;
}
}
+410
View File
@@ -0,0 +1,410 @@
import type {
Jwk,
CryptoApi,
KeyIdentifier,
KmsExportKeyParams,
KmsImportKeyParams,
KeyImporterExporter,
InferKeyGeneratorAlgorithm,
} from '@web5/crypto';
import { Convert } from '@web5/common';
import { LocalKeyManager } from '@web5/crypto';
import type { PortableDid } from '../types/portable-did.js';
import type { DidCreateOptions, DidCreateVerificationMethod } from './did-method.js';
import type { DidDocument, DidResolutionOptions, DidResolutionResult, DidVerificationMethod } from '../types/did-core.js';
import { Did } from '../did.js';
import { DidMethod } from './did-method.js';
import { BearerDid } from '../bearer-did.js';
import { DidError, DidErrorCode } from '../did-error.js';
import { EMPTY_DID_RESOLUTION_RESULT } from '../types/did-resolution.js';
/**
* Defines the set of options available when creating a new Decentralized Identifier (DID) with the
* 'did:jwk' method.
*
* Either the `algorithm` or `verificationMethods` option can be specified, but not both.
* - A new key will be generated using the algorithm identifier specified in either the `algorithm`
* property or the `verificationMethods` object's `algorithm` property.
* - If `verificationMethods` is given, it must contain exactly one entry since DID JWK only
* supports a single verification method.
* - If neither is given, the default is to generate a new Ed25519 key.
*
* @example
* ```ts
* // DID Creation
*
* // By default, when no options are given, a new Ed25519 key will be generated.
* const did = await DidJwk.create();
*
* // The algorithm to use for key generation can be specified as a top-level option.
* const did = await DidJwk.create({
* options: { algorithm = 'ES256K' }
* });
*
* // Or, alternatively as a property of the verification method.
* const did = await DidJwk.create({
* options: {
* verificationMethods: [{ algorithm = 'ES256K' }]
* }
* });
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidJwk.create({ keyManager });
*
* // DID Resolution
* const resolutionResult = await DidJwk.resolve({ did: did.uri });
*
* // Signature Operations
* const signer = await did.getSigner();
* const signature = await signer.sign({ data: new TextEncoder().encode('Message') });
* const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature });
*
* // Import / Export
*
* // Export a BearerDid object to the PortableDid format.
* const portableDid = await did.export();
*
* // Reconstruct a BearerDid object from a PortableDid
* const did = await DidJwk.import(portableDid);
* ```
*/
export interface DidJwkCreateOptions<TKms> extends DidCreateOptions<TKms> {
/**
* Optionally specify the algorithm to be used for key generation.
*/
algorithm?: TKms extends CryptoApi
? InferKeyGeneratorAlgorithm<TKms>
: InferKeyGeneratorAlgorithm<LocalKeyManager>;
/**
* Alternatively, specify the algorithm to be used for key generation of the single verification
* method in the DID Document.
*/
verificationMethods?: DidCreateVerificationMethod<TKms>[];
}
/**
* The `DidJwk` class provides an implementation of the `did:jwk` DID method.
*
* Features:
* - DID Creation: Create new `did:jwk` DIDs.
* - DID Key Management: Instantiate a DID object from an existing verification method key set or
* or a key in a Key Management System (KMS). If supported by the KMS, a DID's
* key can be exported to a portable DID format.
* - DID Resolution: Resolve a `did:jwk` to its corresponding DID Document.
* - Signature Operations: Sign and verify messages using keys associated with a DID.
*
* @remarks
* The `did:jwk` DID method uses a single JSON Web Key (JWK) to generate a DID and does not rely
* on any external system such as a blockchain or centralized database. This characteristic makes
* it suitable for use cases where a assertions about a DID Subject can be self-verifiable by
* third parties.
*
* The DID URI is formed by Base64URL-encoding the JWK and prefixing with `did:jwk:`. The DID
* Document of a `did:jwk` DID contains a single verification method, which is the JWK used
* to generate the DID. The verification method is identified by the key ID `#0`.
*
* @see {@link https://github.com/quartzjer/did-jwk/blob/main/spec.md | DID JWK Specification}
*
* @example
* ```ts
* // DID Creation
* const did = await DidJwk.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidJwk.create({ keyManager });
*
* // DID Resolution
* const resolutionResult = await DidJwk.resolve({ did: did.uri });
*
* // Signature Operations
* const signer = await did.getSigner();
* const signature = await signer.sign({ data: new TextEncoder().encode('Message') });
* const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature });
*
* // Key Management
*
* // Instantiate a DID object from an existing key in a KMS
* const did = await DidJwk.fromKeyManager({
* didUri: 'did:jwk:eyJrIjoiT0tQIiwidCI6IkV1c2UyNTYifQ',
* keyManager
* });
*
* // Instantiate a DID object from an existing verification method key
* const did = await DidJwk.fromKeys({
* verificationMethods: [{
* publicKeyJwk: {
* kty: 'OKP',
* crv: 'Ed25519',
* x: 'cHs7YMLQ3gCWjkacMURBsnEJBcEsvlsE5DfnsfTNDP4'
* },
* privateKeyJwk: {
* kty: 'OKP',
* crv: 'Ed25519',
* x: 'cHs7YMLQ3gCWjkacMURBsnEJBcEsvlsE5DfnsfTNDP4',
* d: 'bdcGE4KzEaekOwoa-ee3gAm1a991WvNj_Eq3WKyqTnE'
* }
* }]
* });
*
* // Convert a DID object to a portable format
* const portableDid = await DidJwk.toKeys({ did });
*
* // Reconstruct a DID object from a portable format
* const did = await DidJwk.fromKeys(portableDid);
* ```
*/
export class DidJwk extends DidMethod {
/**
* Name of the DID method, as defined in the DID JWK specification.
*/
public static methodName = 'jwk';
/**
* Creates a new DID using the `did:jwk` method formed from a newly generated key.
*
* @remarks
* The DID URI is formed by Base64URL-encoding the JWK and prefixing with `did:jwk:`.
*
* Notes:
* - If no `options` are given, by default a new Ed25519 key will be generated.
* - The `algorithm` and `verificationMethods` options are mutually exclusive. If both are given,
* an error will be thrown.
*
* @example
* ```ts
* // DID Creation
* const did = await DidJwk.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidJwk.create({ keyManager });
* ```
*
* @param params - The parameters for the create operation.
* @param params.keyManager - Optionally specify a Key Management System (KMS) used to generate
* keys and sign data.
* @param params.options - Optional parameters that can be specified when creating a new DID.
* @returns A Promise resolving to a {@link BearerDid} object representing the new DID.
*/
public static async create<TKms extends CryptoApi | undefined = undefined>({
keyManager = new LocalKeyManager(),
options = {}
}: {
keyManager?: TKms;
options?: DidJwkCreateOptions<TKms>;
} = {}): Promise<BearerDid> {
// Before processing the create operation, validate DID-method-specific requirements to prevent
// keys from being generated unnecessarily.
// Check 1: Validate that `algorithm` or `verificationMethods` options are not both given.
if (options.algorithm && options.verificationMethods) {
throw new Error(`The 'algorithm' and 'verificationMethods' options are mutually exclusive`);
}
// Check 2: If `verificationMethods` is given, it must contain exactly one entry since DID JWK
// only supports a single verification method.
if (options.verificationMethods && options.verificationMethods.length !== 1) {
throw new Error(`The 'verificationMethods' option must contain exactly one entry`);
}
// Default to Ed25519 key generation if an algorithm is not given.
const algorithm = options.algorithm ?? options.verificationMethods?.[0]?.algorithm ?? 'Ed25519';
// Generate a new key using the specified `algorithm`.
const keyUri = await keyManager.generateKey({ algorithm });
const publicKey = await keyManager.getPublicKey({ keyUri });
// Compute the DID identifier from the public key by serializing the JWK to a UTF-8 string and
// encoding in Base64URL format.
const identifier = Convert.object(publicKey).toBase64Url();
// Attach the prefix `did:jwk` to form the complete DID URI.
const didUri = `did:${DidJwk.methodName}:${identifier}`;
// Expand the DID URI string to a DID document.
const didResolutionResult = await DidJwk.resolve(didUri);
const document = didResolutionResult.didDocument as DidDocument;
// Create the BearerDid object from the generated key material.
const did = new BearerDid({
uri : didUri,
document,
metadata : {},
keyManager
});
return did;
}
/**
* Given the W3C DID Document of a `did:jwk` DID, return the verification method that will be used
* for signing messages and credentials. If given, the `methodId` parameter is used to select the
* verification method. If not given, the first verification method in the DID Document is used.
*
* Note that for DID JWK, only one verification method can exist so specifying `methodId` could be
* considered redundant or unnecessary. The option is provided for consistency with other DID
* method implementations.
*
* @param params - The parameters for the `getSigningMethod` operation.
* @param params.didDocument - DID Document to get the verification method from.
* @param params.methodId - ID of the verification method to use for signing.
* @returns Verification method to use for signing.
*/
public static async getSigningMethod({ didDocument }: {
didDocument: DidDocument;
methodId?: string;
}): Promise<DidVerificationMethod> {
// Verify the DID method is supported.
const parsedDid = Did.parse(didDocument.id);
if (parsedDid && parsedDid.method !== this.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`);
}
// Attempt to find the verification method in the DID Document.
const [ verificationMethod ] = didDocument.verificationMethod ?? [];
if (!(verificationMethod && verificationMethod.publicKeyJwk)) {
throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document');
}
return verificationMethod;
}
/**
* Instantiates a {@link BearerDid} object for the DID JWK method from a given {@link PortableDid}.
*
* This method allows for the creation of a `BearerDid` object using a previously created DID's
* key material, DID document, and metadata.
*
* @remarks
* The `verificationMethod` array of the DID document must contain exactly one key since the
* `did:jwk` method only supports a single verification method.
*
* @example
* ```ts
* // Export an existing BearerDid to PortableDid format.
* const portableDid = await did.export();
* // Reconstruct a BearerDid object from the PortableDid.
* const did = await DidJwk.import({ portableDid });
* ```
*
* @param params - The parameters for the import operation.
* @param params.portableDid - The PortableDid object to import.
* @param params.keyManager - Optionally specify an external Key Management System (KMS) used to
* generate keys and sign data. If not given, a new
* {@link LocalKeyManager} instance will be created and
* used.
* @returns A Promise resolving to a `BearerDid` object representing the DID formed from the provided keys.
* @throws An error if the DID document does not contain exactly one verification method.
*/
public static async import({ portableDid, keyManager = new LocalKeyManager() }: {
keyManager?: CryptoApi & KeyImporterExporter<KmsImportKeyParams, KeyIdentifier, KmsExportKeyParams>;
portableDid: PortableDid;
}): Promise<BearerDid> {
// Verify the DID method is supported.
const parsedDid = Did.parse(portableDid.uri);
if (parsedDid?.method !== DidJwk.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`);
}
// Use the given PortableDid to construct the BearerDid object.
const did = await BearerDid.import({ portableDid, keyManager });
// Validate that the given DID document contains exactly one verification method.
// Note: The non-undefined assertion is necessary because the type system cannot infer that
// the `verificationMethod` property is defined -- which is checked by `BearerDid.import()`.
if (did.document.verificationMethod!.length !== 1) {
throw new DidError(DidErrorCode.InvalidDidDocument, `DID document must contain exactly one verification method`);
}
return did;
}
/**
* Resolves a `did:jwk` identifier to a DID Document.
*
* @param didUri - The DID to be resolved.
* @param _options - Optional parameters for resolving the DID. Unused by this DID method.
* @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution.
*/
public static async resolve(didUri: string, _options?: DidResolutionOptions): Promise<DidResolutionResult> {
// Attempt to parse the DID URI.
const parsedDid = Did.parse(didUri);
// Attempt to decode the Base64URL-encoded JWK.
let publicKey: Jwk | undefined;
try {
publicKey = Convert.base64Url(parsedDid!.id).toObject() as Jwk;
} catch { /* Consume the error so that a DID resolution error can be returned later. */ }
// If parsing or decoding failed, the DID is invalid.
if (!parsedDid || !publicKey) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'invalidDid' }
};
}
// If the DID method is not "jwk", return an error.
if (parsedDid.method !== DidJwk.methodName) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'methodNotSupported' }
};
}
const didDocument: DidDocument = {
'@context': [
'https://www.w3.org/ns/did/v1'
],
id: parsedDid.uri
};
const keyUri = `${didDocument.id}#0`;
// Set the Verification Method property.
didDocument.verificationMethod = [{
id : keyUri,
type : 'JsonWebKey',
controller : didDocument.id,
publicKeyJwk : publicKey
}];
// Set the Verification Relationship properties.
didDocument.authentication = [keyUri];
didDocument.assertionMethod = [keyUri];
didDocument.capabilityInvocation = [keyUri];
didDocument.capabilityDelegation = [keyUri];
didDocument.keyAgreement = [keyUri];
// If the JWK contains a `use` property with the value "sig" then the `keyAgreement` property
// is not included in the DID Document. If the `use` value is "enc" then only the `keyAgreement`
// property is included in the DID Document.
switch (publicKey.use) {
case 'sig': {
delete didDocument.keyAgreement;
break;
}
case 'enc': {
delete didDocument.authentication;
delete didDocument.assertionMethod;
delete didDocument.capabilityInvocation;
delete didDocument.capabilityDelegation;
break;
}
}
return {
...EMPTY_DID_RESOLUTION_RESULT,
didDocument,
};
}
}
File diff suppressed because it is too large Load Diff
+276
View File
@@ -0,0 +1,276 @@
import type {
CryptoApi,
LocalKeyManager,
InferKeyGeneratorAlgorithm,
} from '@web5/crypto';
import type { BearerDid } from '../bearer-did.js';
import type { DidMetadata } from '../types/portable-did.js';
import type {
DidDocument,
DidResolutionResult,
DidResolutionOptions,
DidVerificationMethod,
} from '../types/did-core.js';
import { DidVerificationRelationship } from '../types/did-core.js';
/**
* Represents options during the creation of a Decentralized Identifier (DID).
*
* Implementations of this interface may contain properties and methods that provide specific
* options or metadata during the DID creation processes following specific DID method
* specifications.
*/
export interface DidCreateOptions<TKms> {
/**
* Optional. An array of verification methods to be included in the DID document.
*/
verificationMethods?: DidCreateVerificationMethod<TKms>[];
}
/**
* Options for additional verification methods added to the DID Document during the creation of a
* new Decentralized Identifier (DID).
*/
export interface DidCreateVerificationMethod<TKms> extends Pick<Partial<DidVerificationMethod>, 'controller' | 'id' | 'type'> {
/**
* The name of the cryptographic algorithm to be used for key generation.
*
* Examples might include `Ed25519` and `ES256K` but will vary depending on the DID method
* specification and the key management system in use.
*
* @example
* ```ts
* const verificationMethod: DidCreateVerificationMethod = {
* algorithm: 'Ed25519'
* };
* ```
*/
algorithm: TKms extends CryptoApi
? InferKeyGeneratorAlgorithm<TKms>
: InferKeyGeneratorAlgorithm<LocalKeyManager>;
/**
* Optionally specify the purposes for which a verification method is intended to be used in a DID
* document.
*
* The `purposes` property defines the specific
* {@link DidVerificationRelationship | verification relationships} between the DID subject and
* the verification method. This enables the verification method to be utilized for distinct
* actions such as authentication, assertion, key agreement, capability delegation, and others. It
* is important for verifiers to recognize that a verification method must be associated with the
* relevant purpose in the DID document to be valid for that specific use case.
*
* @example
* ```ts
* const verificationMethod: DidCreateVerificationMethod = {
* algorithm: 'Ed25519',
* controller: 'did:example:1234',
* purposes: ['authentication', 'assertionMethod']
* };
* ```
*/
purposes?: (DidVerificationRelationship | keyof typeof DidVerificationRelationship)[];
}
/**
* Defines the API for a specific DID method. It includes functionalities for creating and resolving
* DIDs.
*
* @typeparam T - The type of the DID instance associated with this method.
* @typeparam O - The type of the options used for creating the DID.
*/
export interface DidMethodApi<
TKms extends CryptoApi | undefined = CryptoApi,
TDid extends BearerDid = BearerDid,
TOptions extends DidCreateOptions<TKms> = DidCreateOptions<TKms>
> extends DidMethodResolver {
/**
* The name of the DID method.
*
* For example, in the DID `did:example:123456`, "example" would be the method name.
*/
methodName: string;
new (): DidMethod;
/**
* Creates a new DID.
*
* This function should generate a new DID in accordance with the DID method specification being
* implemented, using the provided `keyManager`, and optionally, any provided `options`.
*
* @param params - The parameters used to create the DID.
* @param params.keyManager - Optional. The cryptographic API used for key management.
* @param params.options - Optional. The options used for creating the DID.
* @returns A promise that resolves to the newly created DID instance.
*/
create(params: {
keyManager?: TKms;
options?: TOptions;
}): Promise<TDid>;
/**
* Given a DID Document, return the verification method that will be used for signing messages and
* credentials.
*
* If given, the `methodId` parameter is used to select the verification method. If not given, a
* DID method specific approach is taken to selecting the verification method to return.
*
* @param params - The parameters for the `getSigningMethod` operation.
* @param params.didDocument - DID Document to get the verification method from.
* @param params.methodId - ID of the verification method to use for signing.
* @returns A promise that resolves to the erification method to use for signing.
*/
getSigningMethod(params: {
didDocument: DidDocument;
methodId?: string;
}): Promise<DidVerificationMethod>;
}
/**
* Defines the interface for resolving a DID using a specific DID method.
*
* A DID resolver takes a DID URI as input and returns a {@link DidResolutionResult} object.
*
* @property {string} methodName - The name of the DID method.
* @method resolve - Asynchronous method to resolve a DID URI. Takes the DID URI and optional resolution options.
*/
export interface DidMethodResolver {
/**
* The name of the DID method.
*
* For example, in the DID `did:example:123456`, "example" would be the method name.
*/
methodName: string;
new (): DidMethod;
/**
* Resolves a DID URI.
*
* This function should resolve the DID URI in accordance with the DID method specification being
* implemented, using the provided `options`.
*
* @param didUri - The DID URI to be resolved.
* @param options - Optional. The options used for resolving the DID.
* @returns A {@link DidResolutionResult} object containing the DID document and metadata or an error.
*/
resolve(didUri: string, options?: DidResolutionOptions): Promise<DidResolutionResult>;
}
/**
* Represents the result of a Decentralized Identifier (DID) registration operation.
*
* This type encapsulates the complete outcome of registering a DID, including the registration
* metadata, the DID document (if registration is successful), and metadata about the DID document.
*/
export interface DidRegistrationResult {
/**
* The DID document resulting from the registration process, if successful.
*
* If the registration operation was successful, this MUST contain a DID document
* corresponding to the DID. If the registration is unsuccessful, this value MUST be empty.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, § DID Document}
*/
didDocument: DidDocument | null;
/**
* Metadata about the DID Document.
*
* This structure contains information about the DID Document like creation and update timestamps,
* deactivation status, versioning information, and other details relevant to the DID Document.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-diddocumentmetadata | DID Core Specification, § DID Document Metadata}
*/
didDocumentMetadata: DidMetadata;
/**
* A metadata structure consisting of values relating to the results of the DID registration
* process.
*
* This structure is REQUIRED, and in the case of an error in the registration process,
* this MUST NOT be empty. If the registration is not successful, this structure MUST contain an
* `error` property describing the error.
*/
didRegistrationMetadata: DidRegistrationMetadata;
}
/**
* Represents metadata related to the result of a DID registration operation.
*
* This type includes fields that provide information about the outcome of a DID registration
* process (e.g., create, update, deactivate), including any errors that occurred.
*
* This metadata typically changes between invocations of the `create`, `update`, and `deactivate`
* functions, as it represents data about the registration process itself.
*/
export type DidRegistrationMetadata = {
/**
* An error code indicating issues encountered during the DID registration process.
*
* While the DID Core specification does not define a specific set of error codes for the result
* returned by the `create`, `update`, or `deactivate` functions, it is recommended to use the
* error codes defined in the DID Specification Registries for
* {@link https://www.w3.org/TR/did-spec-registries/#error | DID Resolution Metadata }.
*
* Recommended error codes include:
* - `internalError`: An unexpected error occurred during DID registration process.
* - `invalidDid`: The provided DID is invalid.
* - `invalidDidDocument`: The provided DID document does not conform to valid syntax.
* - `invalidDidDocumentLength`: The byte length of the provided DID document does not match the expected value.
* - `invalidSignature`: Verification of a signature failed.
* - `methodNotSupported`: The DID method specified is not supported.
* - Custom error codes can also be provided as strings.
*/
error?: string;
// Additional output metadata generated during DID registration.
[key: string]: any;
};
/**
* Base abstraction for all Decentralized Identifier (DID) method implementations.
*
* This base class serves as a foundational structure upon which specific DID methods
* can be implemented. Subclasses should furnish particular method and data models adherent
* to various DID methods, taking care to adhere to the
* {@link https://www.w3.org/TR/did-core/ | W3C DID Core specification} and the
* respective DID method specifications.
*/
export class DidMethod {
/**
* MUST be implemented by all DID method implementations that extend {@link DidMethod}.
*
* Given the W3C DID Document of a DID, return the verification method that will be used for
* signing messages and credentials. If given, the `methodId` parameter is used to select the
* verification method. If not given, each DID method implementation will select a default
* verification method from the DID Document.
*
* @param _params - The parameters for the `getSigningMethod` operation.
* @param _params.didDocument - DID Document to get the verification method from.
* @param _params.methodId - ID of the verification method to use for signing.
* @returns Verification method to use for signing.
*/
public static async getSigningMethod(_params: {
didDocument: DidDocument;
methodId?: string;
}): Promise<DidVerificationMethod | undefined> {
throw new Error(`Not implemented: Classes extending DidMethod must implement getSigningMethod()`);
}
/**
* MUST be implemented by all DID method implementations that extend {@link DidMethod}.
*
* Resolves a DID URI to a DID Document.
*
* @param _didUri - The DID to be resolved.
* @param _options - Optional parameters for resolving the DID.
* @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution.
*/
public static async resolve(_didUri: string, _options?: DidResolutionOptions): Promise<DidResolutionResult> {
throw new Error(`Not implemented: Classes extending DidMethod must implement resolve()`);
}
}
+96
View File
@@ -0,0 +1,96 @@
import type { DidDocument, DidResolutionOptions, DidResolutionResult } from '../types/did-core.js';
import { Did } from '../did.js';
import { DidMethod } from './did-method.js';
import { EMPTY_DID_RESOLUTION_RESULT } from '../types/did-resolution.js';
/**
* The `DidWeb` class provides an implementation of the `did:web` DID method.
*
* Features:
* - DID Resolution: Resolve a `did:web` to its corresponding DID Document.
*
* @remarks
* The `did:web` method uses a web domain's existing reputation and aims to integrate decentralized
* identities with the existing web infrastructure to drive adoption. It leverages familiar web
* security models and domain ownership to provide accessible, interoperable digital identity
* management.
*
* @see {@link https://w3c-ccg.github.io/did-method-web/ | DID Web Specification}
*
* @example
* ```ts
* // DID Resolution
* const resolutionResult = await DidWeb.resolve({ did: did.uri });
* ```
*/
export class DidWeb extends DidMethod {
/**
* Name of the DID method, as defined in the DID Web specification.
*/
public static methodName = 'web';
/**
* Resolves a `did:web` identifier to a DID Document.
*
* @param didUri - The DID to be resolved.
* @param _options - Optional parameters for resolving the DID. Unused by this DID method.
* @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of the resolution.
*/
public static async resolve(didUri: string, _options?: DidResolutionOptions): Promise<DidResolutionResult> {
// Attempt to parse the DID URI.
const parsedDid = Did.parse(didUri);
// If parsing failed, the DID is invalid.
if (!parsedDid) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'invalidDid' }
};
}
// If the DID method is not "web", return an error.
if (parsedDid.method !== DidWeb.methodName) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'methodNotSupported' }
};
}
// Replace ":" with "/" in the identifier and prepend "https://" to obtain the fully qualified
// domain name and optional path.
let baseUrl = `https://${parsedDid.id.replace(/:/g, '/')}`;
// If the domain contains a percent encoded port value, decode the colon.
baseUrl = decodeURIComponent(baseUrl);
// Append the expected location of the DID document depending on whether a path was specified.
const didDocumentUrl = parsedDid.id.includes(':') ?
`${baseUrl}/did.json` :
`${baseUrl}/.well-known/did.json`;
try {
// Perform an HTTP GET request to obtain the DID document.
const response = await fetch(didDocumentUrl);
// If the response status code is not 200, return an error.
if (!response.ok) throw new Error('HTTP error status code returned');
// Parse the DID document.
const didDocument = await response.json() as DidDocument;
return {
...EMPTY_DID_RESOLUTION_RESULT,
didDocument,
};
} catch (error: any) {
// If the DID document could not be retrieved, return an error.
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: { error: 'notFound' }
};
}
}
}
@@ -0,0 +1,163 @@
import type { AbstractLevel } from 'abstract-level';
import ms from 'ms';
import { Level } from 'level';
import type { DidResolutionResult } from '../types/did-core.js';
import type { DidResolverCache } from '../types/did-resolution.js';
/**
* Configuration parameters for creating a LevelDB-based cache for DID resolution results.
*
* Allows customization of the underlying database instance, storage location, and cache
* time-to-live (TTL) settings.
*/
export type DidResolverCacheLevelParams = {
/**
* Optional. An instance of `AbstractLevel` to use as the database. If not provided, a new
* LevelDB instance will be created at the specified `location`.
*/
db?: AbstractLevel<string | Buffer | Uint8Array, string, string>;
/**
* Optional. The file system path or IndexedDB name where the LevelDB store will be created.
* Defaults to 'DATA/DID_RESOLVERCACHE' if not specified.
*/
location?: string;
/**
* Optional. The time-to-live for cache entries, expressed as a string (e.g., '1h', '15m').
* Determines how long a cache entry should remain valid before being considered expired. Defaults
* to '15m' if not specified.
*/
ttl?: string;
}
/**
* Encapsulates a DID resolution result along with its expiration information for caching purposes.
*
* This type is used internally by the `DidResolverCacheLevel` to store DID resolution results
* with an associated time-to-live (TTL) value. The TTL is represented in milliseconds and
* determines when the cached entry is considered expired and eligible for removal.
*/
type CachedDidResolutionResult = {
/**
* The expiration time of the cache entry in milliseconds since the Unix epoch.
*
* This value is used to calculate whether the cached entry is still valid or has expired.
*/
ttlMillis: number;
/**
* The DID resolution result being cached.
*
* This object contains the resolved DID document and associated metadata.
*/
value: DidResolutionResult;
}
/**
* A Level-based cache implementation for storing and retrieving DID resolution results.
*
* This cache uses LevelDB for storage, allowing data persistence across process restarts or
* browser refreshes. It's suitable for both Node.js and browser environments.
*
* @remarks
* The LevelDB cache keeps data in memory for fast access and also writes to the filesystem in
* Node.js or indexedDB in browsers. Time-to-live (TTL) for cache entries is configurable.
*
* @example
* ```
* const cache = new DidResolverCacheLevel({ ttl: '15m' });
* ```
*/
export class DidResolverCacheLevel implements DidResolverCache {
/** The underlying LevelDB store used for caching. */
private cache;
/** The time-to-live for cache entries in milliseconds. */
private ttl: number;
constructor({
db,
location = 'DATA/DID_RESOLVERCACHE',
ttl = '15m'
}: DidResolverCacheLevelParams = {}) {
this.cache = db ?? new Level<string, string>(location);
this.ttl = ms(ttl);
}
/**
* Retrieves a DID resolution result from the cache.
*
* If the cached item has exceeded its TTL, it's scheduled for deletion and undefined is returned.
*
* @param did - The DID string used as the key for retrieving the cached result.
* @returns The cached DID resolution result or undefined if not found or expired.
*/
async get(did: string): Promise<DidResolutionResult | void> {
try {
const str = await this.cache.get(did);
const cachedDidResolutionResult: CachedDidResolutionResult = JSON.parse(str);
if (Date.now() >= cachedDidResolutionResult.ttlMillis) {
// defer deletion to be called in the next tick of the js event loop
this.cache.nextTick(() => this.cache.del(did));
return;
} else {
return cachedDidResolutionResult.value;
}
} catch(error: any) {
// Don't throw when a key wasn't found.
if (error.notFound) {
return;
}
throw error;
}
}
/**
* Stores a DID resolution result in the cache with a TTL.
*
* @param did - The DID string used as the key for storing the result.
* @param value - The DID resolution result to be cached.
* @returns A promise that resolves when the operation is complete.
*/
set(did: string, value: DidResolutionResult): Promise<void> {
const cachedDidResolutionResult: CachedDidResolutionResult = { ttlMillis: Date.now() + this.ttl, value };
const str = JSON.stringify(cachedDidResolutionResult);
return this.cache.put(did, str);
}
/**
* Deletes a DID resolution result from the cache.
*
* @param did - The DID string used as the key for deletion.
* @returns A promise that resolves when the operation is complete.
*/
delete(did: string): Promise<void> {
return this.cache.del(did);
}
/**
* Clears all entries from the cache.
*
* @returns A promise that resolves when the operation is complete.
*/
clear(): Promise<void> {
return this.cache.clear();
}
/**
* Closes the underlying LevelDB store.
*
* @returns A promise that resolves when the store is closed.
*/
close(): Promise<void> {
return this.cache.close();
}
}
@@ -0,0 +1,26 @@
import type { DidResolutionResult } from '../types/did-core.js';
import type { DidResolverCache } from '../types/did-resolution.js';
/**
* No-op cache that is used as the default cache for did-resolver.
*
* The motivation behind using a no-op cache as the default stems from the desire to maximize the
* potential for this library to be used in as many JS runtimes as possible.
*/
export const DidResolverCacheNoop: DidResolverCache = {
get: function (_key: string): Promise<DidResolutionResult> {
return null as any;
},
set: function (_key: string, _value: DidResolutionResult): Promise<void> {
return null as any;
},
delete: function (_key: string): Promise<void> {
return null as any;
},
clear: function (): Promise<void> {
return null as any;
},
close: function (): Promise<void> {
return null as any;
}
};
@@ -0,0 +1,238 @@
import type { DidMethodResolver } from '../methods/did-method.js';
import type { DidResolver, DidResolverCache, DidUrlDereferencer } from '../types/did-resolution.js';
import type { DidDereferencingOptions, DidDereferencingResult, DidResolutionOptions, DidResolutionResult, DidResource } from '../types/did-core.js';
import { Did } from '../did.js';
import { DidErrorCode } from '../did-error.js';
import { DidResolverCacheNoop } from './resolver-cache-noop.js';
import { EMPTY_DID_RESOLUTION_RESULT } from '../types/did-resolution.js';
/**
* Parameters for configuring the `UniversalResolver` class, which is responsible for resolving
* decentralized identifiers (DIDs) to their corresponding DID documents.
*
* This type specifies the essential components required by the `UniversalResolver` to perform
* DID resolution and dereferencing. It includes an array of `DidMethodResolver` instances,
* each capable of resolving DIDs for a specific method, and optionally, a cache for storing
* resolved DID documents to improve resolution efficiency.
*/
export type UniversalResolverParams = {
/**
* An array of `DidMethodResolver` instances.
*
* Each resolver in this array is designed to handle a specific DID method, enabling the
* `DidResolver` to support multiple DID methods simultaneously.
*/
didResolvers: DidMethodResolver[];
/**
* 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, a no-operation cache is used,
* which effectively disables caching.
*/
cache?: DidResolverCache;
}
/**
* The `DidResolver` class provides mechanisms for resolving Decentralized Identifiers (DIDs) to
* their corresponding DID documents.
*
* The class is designed to handle various DID methods by utilizing an array of `DidMethodResolver`
* instances, each responsible for a specific DID method.
*
* Providing a cache implementation can significantly enhance resolution performance by avoiding
* redundant resolutions for previously resolved DIDs. If omitted, a no-operation cache is used,
* which effectively disables caching.
*
* Usage:
* - Construct the `DidResolver` with an array of `DidMethodResolver` instances and an optional cache.
* - Use `resolve` to resolve a DID to its DID Resolution Result.
* - Use `dereference` to extract specific resources from a DID URL, like service endpoints or verification methods.
*
* @example
* ```ts
* const resolver = new DidResolver({
* didResolvers: [<array of DidMethodResolver instances>],
* cache: new DidResolverCacheNoop()
* });
*
* const resolutionResult = await resolver.resolve('did:example:123456');
* const dereferenceResult = await resolver.dereference({ didUri: 'did:example:123456#key-1' });
* ```
*/
export class UniversalResolver implements DidResolver, DidUrlDereferencer {
/**
* A cache for storing resolved DID documents.
*/
private cache: DidResolverCache;
/**
* A map to store method resolvers against method names.
*/
private didResolvers: Map<string, DidMethodResolver> = new Map();
/**
* Constructs a new `DidResolver`.
*
* @param params - The parameters for constructing the `DidResolver`.
*/
constructor({ cache, didResolvers }: UniversalResolverParams) {
this.cache = cache || DidResolverCacheNoop;
for (const resolver of didResolvers) {
this.didResolvers.set(resolver.methodName, resolver);
}
}
/**
* Resolves a DID to a DID Resolution Result.
*
* If the DID Resolution Result is present in the cache, it returns the cached result. Otherwise,
* it uses the appropriate method resolver to resolve the DID, stores the resolution result in the
* cache, and returns the resolultion result.
*
* @param didUri - The DID or DID URL to resolve.
* @returns A promise that resolves to the DID Resolution Result.
*/
public async resolve(didUri: string, options?: DidResolutionOptions): Promise<DidResolutionResult> {
const parsedDid = Did.parse(didUri);
if (!parsedDid) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: {
error : DidErrorCode.InvalidDid,
errorMessage : `Invalid DID URI: ${didUri}`
}
};
}
const resolver = this.didResolvers.get(parsedDid.method);
if (!resolver) {
return {
...EMPTY_DID_RESOLUTION_RESULT,
didResolutionMetadata: {
error : DidErrorCode.MethodNotSupported,
errorMessage : `Method not supported: ${parsedDid.method}`
}
};
}
const cachedResolutionResult = await this.cache.get(parsedDid.uri);
if (cachedResolutionResult) {
return cachedResolutionResult;
} else {
const resolutionResult = await resolver.resolve(parsedDid.uri, options);
if (!resolutionResult.didResolutionMetadata.error) {
// Cache the resolution result if it was successful.
await this.cache.set(parsedDid.uri, resolutionResult);
}
return resolutionResult;
}
}
/**
* Dereferences a DID (Decentralized Identifier) URL to a corresponding DID resource.
*
* This method interprets the DID URL's components, which include the DID method, method-specific
* identifier, path, query, and fragment, and retrieves the related resource as per the DID Core
* specifications.
*
* The dereferencing process involves resolving the DID contained in the DID URL to a DID document,
* and then extracting the specific part of the document identified by the fragment in the DID URL.
* If no fragment is specified, the entire DID document is returned.
*
* This method supports resolution of different components within a DID document such as service
* endpoints and verification methods, based on their IDs. It accommodates both full and
* DID URLs as specified in the DID Core specification.
*
* More information on DID URL dereferencing can be found in the
* {@link https://www.w3.org/TR/did-core/#did-url-dereferencing | DID Core specification}.
*
* TODO: This is a partial implementation and does not fully implement DID URL dereferencing. (https://github.com/TBD54566975/web5-js/issues/387)
*
* @param didUrl - The DID URL string to dereference.
* @param [_options] - Input options to the dereference function. Optional.
* @returns a {@link DidDereferencingResult}
*/
async dereference(
didUrl: string,
_options?: DidDereferencingOptions
): Promise<DidDereferencingResult> {
// Validate the given `didUrl` confirms to the DID URL syntax.
const parsedDidUrl = Did.parse(didUrl);
if (!parsedDidUrl) {
return {
dereferencingMetadata : { error: DidErrorCode.InvalidDidUrl },
contentStream : null,
contentMetadata : {}
};
}
// Obtain the DID document for the input DID by executing DID resolution.
const { didDocument, didResolutionMetadata, didDocumentMetadata } = await this.resolve(parsedDidUrl.uri);
if (!didDocument) {
return {
dereferencingMetadata : { error: didResolutionMetadata.error },
contentStream : null,
contentMetadata : {}
};
}
// Return the entire DID Document if no query or fragment is present on the DID URL.
if (!parsedDidUrl.fragment || parsedDidUrl.query) {
return {
dereferencingMetadata : { contentType: 'application/did+json' },
contentStream : didDocument,
contentMetadata : didDocumentMetadata
};
}
const { service = [], verificationMethod = [] } = didDocument;
// Create a set of possible id matches. The DID spec allows for an id to be the entire
// did#fragment or just #fragment.
// @see {@link }https://www.w3.org/TR/did-core/#relative-did-urls | Section 3.2.2, Relative DID URLs}.
// Using a Set for fast string comparison since some DID methods have long identifiers.
const idSet = new Set([didUrl, parsedDidUrl.fragment, `#${parsedDidUrl.fragment}`]);
let didResource: DidResource | undefined;
// Find the first matching verification method in the DID document.
for (let vm of verificationMethod) {
if (idSet.has(vm.id)) {
didResource = vm;
break;
}
}
// Find the first matching service in the DID document.
for (let svc of service) {
if (idSet.has(svc.id)) {
didResource = svc;
break;
}
}
if (didResource) {
return {
dereferencingMetadata : { contentType: 'application/did+json' },
contentStream : didResource,
contentMetadata : didResolutionMetadata
};
} else {
return {
dereferencingMetadata : { error: DidErrorCode.NotFound },
contentStream : null,
contentMetadata : {},
};
}
}
}
+580
View File
@@ -0,0 +1,580 @@
import { Jwk } from '@web5/crypto';
/**
* Represents metadata related to the process of DID dereferencing.
*
* This type includes fields that provide information about the outcome of a DID dereferencing operation,
* including the content type of the returned resource and any errors that occurred during the dereferencing process.
*
* @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-metadata | DID Core Specification, § DID URL Dereferencing Metadata}
*/
export type DidDereferencingMetadata = {
/**
* The Media Type of the returned contentStream SHOULD be expressed using this property if
* dereferencing is successful.
*/
contentType?: string;
/**
* The error code from the dereferencing process. This property is REQUIRED when there is an
* error in the dereferencing process. The value of this property MUST be a single keyword
* expressed as an ASCII string. The possible property values of this field SHOULD be registered
* in the {@link https://www.w3.org/TR/did-spec-registries/ | DID Specification Registries}.
* The DID Core specification defines the following common error values:
*
* - `invalidDidUrl`: The DID URL supplied to the DID URL dereferencing function does not conform
* to valid syntax.
* - `notFound`: The DID URL dereferencer was unable to find the `contentStream` resulting from
* this dereferencing request.
*
* @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-metadata | DID Core Specification, § DID URL Dereferencing Metadata}
*/
error?: string;
// Additional output metadata generated during DID Resolution.
[key: string]: any;
}
/**
* Represents the options that can be used during the process of DID dereferencing.
*
* This interface allows the caller to specify preferences and additional parameters for the DID
* dereferencing operation.
*
* @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing-options}
*/
export interface DidDereferencingOptions {
/** The Media Type that the caller prefers for contentStream. */
accept?: string;
/** Additional properties used during DID dereferencing. */
[key: string]: any;
}
/**
* Represents the result of a DID dereferencing operation.
*
* This type encapsulates the outcomes of the DID URL dereferencing process, including metadata
* about the dereferencing operation, the content stream retrieved (if any), and metadata about the
* content stream.
*
* @see {@link https://www.w3.org/TR/did-core/#did-url-dereferencing | DID Core Specification, § DID URL Dereferencing}
*/
export type DidDereferencingResult = {
/**
* A metadata structure consisting of values relating to the results of the DID URL dereferencing
* process. This structure is REQUIRED, and in the case of an error in the dereferencing process,
* this MUST NOT be empty. Properties defined by this specification are in 7.2.2 DID URL
* Dereferencing Metadata. If the dereferencing is not successful, this structure MUST contain an
* `error` property describing the error.
*/
dereferencingMetadata: DidDereferencingMetadata;
/**
* If the `dereferencing` function was called and successful, this MUST contain a resource
* corresponding to the DID URL. The contentStream MAY be a resource such as:
* - a DID document that is serializable in one of the conformant representations
* - a Verification Method
* - a service.
* - any other resource format that can be identified via a Media Type and obtained through the
* resolution process.
*
* If the dereferencing is unsuccessful, this value MUST be empty.
*/
contentStream: DidResource | null;
/**
* If the dereferencing is successful, this MUST be a metadata structure, but the structure MAY be
* empty. This structure contains metadata about the contentStream. If the contentStream is a DID
* document, this MUST be a didDocumentMetadata structure as described in DID Resolution. If the
* dereferencing is unsuccessful, this output MUST be an empty metadata structure.
*/
contentMetadata: DidDocumentMetadata;
}
/**
* A set of data describing the Decentralized Identifierr (DID) subject.
*
* A DID Document contains information associated with the DID, such as cryptographic public keys
* and service endpoints, enabling trustable interactions associated with the DID subject.
*
* - Cryptographic public keys - Used by the DID subject or a DID delegate to authenticate itself
* and prove its association with the DID.
* - Service endpoints - Used to communicate or interact with the DID subject or associated
* entities. Examples include discovery, agent, social networking, file
* storage, and verifiable credential repository services.
*
* A DID Document can be retrieved by resolving a DID, as described in
* {@link https://www.w3.org/TR/did-core/#did-resolution | DID Core Specification, § DID Resolution}.
*/
export interface DidDocument {
/**
* A JSON-LD context link, which provides a JSON-LD processor with the information necessary to
* interpret the DID document JSON. The default context URL is 'https://www.w3.org/ns/did/v1'.
*/
'@context'?: 'https://www.w3.org/ns/did/v1' | string | (string | Record<string, any>)[];
/**
* The DID Subject to which this DID Document pertains.
*
* The `id` property is REQUIRED and must be a valid DID.
*
* @see {@link https://www.w3.org/TR/did-core/#did-subject | DID Core Specification, § DID Subject}
*/
id: string;
/**
* A DID subject can have multiple identifiers for different purposes, or at different times.
* The assertion that two or more DIDs (or other types of URI) refer to the same DID subject can
* be made using the `alsoKnownAs` property.
*
* @see {@link https://www.w3.org/TR/did-core/#also-known-as | DID Core Specification, § Also Known As}
*/
alsoKnownAs?: string[];
/**
* A DID controller is an entity that is authorized to make changes to a DID document. Typically,
* only the DID Subject (i.e., the value of `id` property in the DID document) is authoritative.
* However, another DID can be specified as the DID controller, and when doing so, any
* verification methods contained in the DID document for the other DID should be accepted as
* authoritative. In other words, proofs created by the controller DID should be considered
* equivalent to proofs created by the DID Subject.
*
* @see {@link https://www.w3.org/TR/did-core/#did-controller | DID Core Specification, § DID Controller}
*/
controller?: string | string[];
/**
* A DID document can express verification methods, such as cryptographic public keys, which can
* be used to authenticate or authorize interactions with the DID subject or associated parties.
*
* @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods}
*/
verificationMethod?: DidVerificationMethod[];
/**
* The `assertionMethod` verification relationship is used to specify how the DID subject is
* expected to express claims, such as for the purposes of issuing a Verifiable Credential.
*
* @see {@link https://www.w3.org/TR/did-core/#assertion | DID Core Specification, § Assertion}
*/
assertionMethod?: (DidVerificationMethod | string)[];
/**
* The `authentication` verification relationship is used to specify how the DID subject is expected
* to be authenticated, for purposes such as logging into a website or engaging in any sort of
* challenge-response protocol.
* @see {@link https://www.w3.org/TR/did-core/#authentication | DID Core Specification, § Authentication}
*/
authentication?: (DidVerificationMethod | string)[];
/**
* The `keyAgreement` verification relationship is used to specify how an entity can generate
* encryption material in order to transmit confidential information intended for the DID
* subject, such as for the purposes of establishing a secure communication channel with the
* recipient.
*
* @see {@link https://www.w3.org/TR/did-core/#key-agreement | DID Core Specification, § Key Agreement}
*/
keyAgreement?: (DidVerificationMethod | string)[];
/**
* The `capabilityDelegation` verification relationship is used to specify a mechanism that might
* be used by the DID subject to delegate a cryptographic capability to another party, such as
* delegating the authority to access a specific HTTP API to a subordinate.
*
* @see {@link https://www.w3.org/TR/did-core/#capability-delegation | DID Core Specification, § Capability Delegation}
*/
capabilityDelegation?: (DidVerificationMethod | string)[];
/**
* The `capabilityInvocation` verification relationship is used to specify a verification method
* that might be used by the DID subject to invoke a cryptographic capability, such as the
* authorization to update the DID Document.
*/
capabilityInvocation?: (DidVerificationMethod | string)[];
/**
* Services are used in DID documents to express ways of communicating with the DID subject or
* associated entities. A service can be any type of service the DID subject wants to advertise,
* including decentralized identity management services for further discovery, authentication,
* authorization, or interaction.
*
* @see {@link https://www.w3.org/TR/did-core/#services | DID Core Specification, § Services}
*/
service?: DidService[];
}
/**
* Represents metadata about the DID document resulting from a DID resolution operation.
*
* This metadata typically does not change between invocations of the `resolve` and
* `resolveRepresentation` functions unless the DID document changes, as it represents metadata
* about the DID document.
*
* @see {@link https://www.w3.org/TR/did-core/#did-document-metadata | DID Core Specification, § DID Document Metadata}
*/
export interface DidDocumentMetadata {
/**
* Timestamp of the Create operation.
*
* The value of the property MUST be a string formatted as an XML Datetime normalized to
* UTC 00:00:00 and without sub-second decimal precision. For example: `2020-12-20T19:17:47Z`.
*/
created?: string;
/**
* Timestamp of the last Update operation for the document version which was resolved.
*
* The value of the property MUST follow the same formatting rules as the `created` property.
* The `updated` property is omitted if an Update operation has never been performed on the DID
* document. If an `updated` property exists, it can be the same value as the `created` property
* when the difference between the two timestamps is less than one second.
*/
updated?: string;
/**
* Whether the DID has been deactivated.
*
* If a DID has been deactivated, DID document metadata MUST include this property with the
* boolean value `true`. If a DID has not been deactivated, this properrty is OPTIONAL, but if
* present, MUST have the boolean value `false`.
*/
deactivated?: boolean;
/**
* Version ID of the last Update operation for the document version which was resolved.
*/
versionId?: string;
/**
* Timestamp of the next Update operation if the resolved document version is not the latest
* version of the document.
*
* The value of the property MUST follow the same formatting rules as the `created` property.
*/
nextUpdate?: string;
/**
* Version ID of the next Update operation if the resolved document version is not the latest
* version of the document.
*/
nextVersionId?: string;
/**
* A DID method can define different forms of a DID that are logically equivalent. An example is
* when a DID takes one form prior to registration in a verifiable data registry and another form
* after such registration. In this case, the DID method specification might need to express one
* or more DIDs that are logically equivalent to the resolved DID as a property of the DID
* document. This is the purpose of the `equivalentId` property.
*
* A requesting party is expected to retain the values from the id and equivalentId properties to
* ensure any subsequent interactions with any of the values they contain are correctly handled as
* logically equivalent (e.g., retain all variants in a database so an interaction with any one
* maps to the same underlying account).
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-equivalentid | DID Core Specification, § DID Document Metadata}
*/
equivalentId?: string[];
/**
* The `canonicalId` property is identical to the `equivalentId` property except:
* - it is associated with a single value rather than a set
* - the DID is defined to be the canonical ID for the DID subject within the scope of the
* containing DID document.
*
* A requesting party is expected to use the `canonicalId` value as its primary ID value for the
* DID subject and treat all other equivalent values as secondary aliases (e.g., update
* corresponding primary references in their systems to reflect the new canonical ID directive).
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-canonicalid | DID Core Specification, § DID Document Metadata}
*/
canonicalId?: string;
// Additional output metadata generated during DID Resolution.
[key: string]: any;
}
/**
* Represents metadata related to the result of a DID resolution operation.
*
* This type includes fields that provide information about the outcome of a DID resolution process,
* including the content type of the returned DID document and any errors that occurred during the
* resolution process.
*
* This metadata typically changes between invocations of the `resolve` and `resolveRepresentation`
* functions, as it represents data about the resolution process itself.
*
* @see {@link https://www.w3.org/TR/did-core/#did-resolution-metadata | DID Core Specification, § DID Resolution Metadata}
*/
export type DidResolutionMetadata = {
/**
* The Media Type of the returned `didDocumentStream`.
*
* This property is REQUIRED if resolution is successful and if the `resolveRepresentation`
* function was called. This property MUST NOT be present if the `resolve` function was called.
* The value of this property MUST be an ASCII string that is the Media Type of the conformant
* representations. The caller of the `resolveRepresentation` function MUST use this value when
* determining how to parse and process the `didDocumentStream` returned by this function into the
* data model.
*/
contentType?: string;
/**
* An error code indicating issues encountered during the DID Resolution or DID URL
* Dereferencing process.
*
* Defined error codes include:
* - `internalError`: An unexpected error occurred during DID Resolution or DID URL
* dereferencing process.
* - `invalidDid`: The provided DID is invalid.
* - `methodNotSupported`: The DID method specified is not supported.
* - `notFound`: The DID or DID URL does not exist.
* - `representationNotSupported`: The DID document representation is not supported.
* - Custom error codes can also be provided as strings.
*
* @see {@link https://www.w3.org/TR/did-core/#did-resolution-metadata | DID Core Specification, § DID Resolution Metadata}
* @see {@link https://www.w3.org/TR/did-spec-registries/#error | DID Specification Registries, § Error}
*/
error?: string;
// Additional output metadata generated during DID Resolution.
[key: string]: any;
};
/**
* DID Resolution input metadata.
*
* The DID Core specification defines the following common properties:
* - `accept`: The Media Type that the caller prefers for the returned representation of the DID
* Document.
*
* The possible properties within this structure and their possible values are registered in the
* {@link https://www.w3.org/TR/did-spec-registries/#did-resolution-options | DID Specification Registries}.
*
* @see {@link https://www.w3.org/TR/did-core/#did-resolution-options | DID Core Specification, § DID Resolution Options}
*/
export interface DidResolutionOptions {
/**
* The Media Type that the caller prefers for the returned representation of the DID Document.
*
* This property is REQUIRED if the `resolveRepresentation` function was called. This property
* MUST NOT be present if the `resolve` function was called.
*
* The value of this property MUST be an ASCII string that is the Media Type of the conformant
* representations. The caller of the `resolveRepresentation` function MUST use this value when
* determining how to parse and process the `didDocumentStream` returned by this function into the
* data model.
*
* @see {@link https://www.w3.org/TR/did-core/#did-resolution-options | DID Core Specification, § DID Resolution Options}
*/
accept?: string;
// Additional properties used during DID Resolution.
[key: string]: any;
}
/**
* Represents the result of a Decentralized Identifier (DID) resolution operation.
*
* This type encapsulates the complete outcome of resolving a DID, including the resolution metadata,
* the DID document (if resolution is successful), and metadata about the DID document.
*
* @see {@link https://www.w3.org/TR/did-core/#did-resolution | DID Core Specification, § DID Resolution}
*/
export type DidResolutionResult = {
/**
* A JSON-LD context link, which provides the JSON-LD processor with the information necessary to
* interpret the resolution result JSON. The default context URL is
* 'https://w3id.org/did-resolution/v1'.
*/
'@context'?: 'https://w3id.org/did-resolution/v1' | string | (string | Record<string, any>)[];
/**
* A metadata structure consisting of values relating to the results of the DID resolution
* process.
*
* This structure is REQUIRED, and in the case of an error in the resolution process,
* this MUST NOT be empty. If the resolution is not successful, this structure MUST contain an
* `error` property describing the error.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-didresolutionmetadata | DID Core Specification, § DID Resolution Metadata}
*/
didResolutionMetadata: DidResolutionMetadata;
/**
* The DID document resulting from the resolution process, if successful.
*
* If the `resolve` function was called and successful, this MUST contain a DID document
* corresponding to the DID. If the resolution is unsuccessful, this value MUST be empty.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, § DID Document}
*/
didDocument: DidDocument | null;
/**
* Metadata about the DID Document.
*
* This structure contains information about the DID Document like creation and update timestamps,
* deactivation status, versioning information, and other details relevant to the DID Document.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-diddocumentmetadata | DID Core Specification, § DID Document Metadata}
*/
didDocumentMetadata: DidDocumentMetadata;
};
/**
* A DID Resource is either a DID Document, a DID Verification method or a DID Service
*/
export type DidResource = DidDocument | DidService | DidVerificationMethod;
/**
* Services are used in DID documents to express ways of communicating with the DID subject or
* associated entities. A service can be any type of service the DID subject wants to advertise.
*
* @see {@link https://www.w3.org/TR/did-core/#services}
*/
export type DidService = {
/**
* Identifier of the service.
*
* The `id` property is REQUIRED. It MUST be a URI conforming to
* {@link https://datatracker.ietf.org/doc/html/rfc3986 | RFC3986} and MUST be unique within the
* DID document.
*/
id: string;
/**
* The type of service being described.
*
* The `type` property is REQUIRED. It MUST be a string. To maximize interoperability, the value
* SHOULD be registered in the
* {@link https://www.w3.org/TR/did-spec-registries/ | DID Specification Registries}. Examples of
* service types can be found in
* {@link https://www.w3.org/TR/did-spec-registries/#service-types | § Service Types}.
*/
type: string;
/**
* A URI that can be used to interact with the DID service.
*
* The value of the `serviceEndpoint` property MUST be a string, an object containing key/value
* pairs, or an array composed of strings or objects. All string values MUST be valid URIs
* conforming to {@link https://datatracker.ietf.org/doc/html/rfc3986 | RFC3986}.
*/
serviceEndpoint: DidServiceEndpoint | DidServiceEndpoint[];
// DID methods MAY include additional service properties.
[key: string]: any;
};
/**
* A service endpoint is a URI (Uniform Resource Identifier) that can be used to interact with the
* DID service.
*
* The value of the `serviceEndpoint` property MUST be a string or an object containing key/value
* pairs. All string values MUST be valid URIs conforming to
* {@link https://datatracker.ietf.org/doc/html/rfc3986 | RFC3986}.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-serviceendpoint | RFC3986, § 5.4 Services}
*/
export type DidServiceEndpoint = string | Record<string, any>;
/**
* Represents a verification method in the context of a DID document.
*
* A verification method is a mechanism by which a DID controller can cryptographically assert proof
* of ownership or control over a DID or DID document. This can include, but is not limited to,
* cryptographic public keys or other data that can be used to authenticate or authorize actions.
*
* @see {@link https://www.w3.org/TR/did-core/#verification-methods | DID Core Specification, § Verification Methods}
*/
export interface DidVerificationMethod {
/**
* The identifier of the verification method, which must be a URI.
*/
id: string;
/**
* The type of the verification method.
*
* To maximize interoperability this value SHOULD be one of the valid verification method types
* registered in the {@link https://www.w3.org/TR/did-spec-registries/#verification-method-types | DID Specification Registries}.
*/
type: string;
/**
* The DID of the entity that controls this verification method.
*/
controller: string;
/**
* (Optional) A public key in JWK format.
*
* A JSON Web Key (JWK) that conforms to {@link https://datatracker.ietf.org/doc/html/rfc7517 | RFC 7517}.
*/
publicKeyJwk?: Jwk;
/**
* (Optional) A public key in Multibase format.
*
* A multibase key that conforms to the draft
* {@link https://datatracker.ietf.org/doc/draft-multiformats-multibase/ | Multibase specification}.
*/
publicKeyMultibase?: string;
}
/**
* Represents the various verification relationships defined in a DID document.
*
* These verification relationships indicate the intended usage of verification methods within a DID
* document. Each relationship signifies a different purpose or context in which a verification
* method can be used, such as authentication, assertionMethod, keyAgreement, capabilityDelegation,
* and capabilityInvocation. The array provides a standardized set of relationship names for
* consistent referencing and implementation across different DID methods.
*
* @see {@link https://www.w3.org/TR/did-core/#verification-relationships | DID Core Specification, § Verification Relationships}
*/
export enum DidVerificationRelationship {
/**
* Specifies how the DID subject is expected to be authenticated. This is commonly used for
* purposes like logging into a website or participating in challenge-response protocols.
*
* @see {@link https://www.w3.org/TR/did-core/#authentication | DID Core Specification, § Authentication}
*/
authentication = 'authentication',
/**
* Specifies how the DID subject is expected to express claims, such as for issuing Verifiable
* Credentials. This relationship is typically used when the DID subject is the issuer of a
* credential.
*
* @see {@link https://www.w3.org/TR/did-core/#assertion | DID Core Specification, § Assertion}
*/
assertionMethod = 'assertionMethod',
/**
* Specifies how an entity can generate encryption material to communicate confidentially with the
* DID subject. Often used in scenarios requiring secure communication channels.
*
* @see {@link https://www.w3.org/TR/did-core/#key-agreement | DID Core Specification, § Key Agreement}
*/
keyAgreement = 'keyAgreement',
/**
* Specifies a verification method used by the DID subject to invoke a cryptographic capability.
* This is frequently associated with authorization actions, like updating the DID Document.
*
* @see {@link https://www.w3.org/TR/did-core/#capability-invocation | DID Core Specification, § Capability Invocation}
*/
capabilityInvocation = 'capabilityInvocation',
/**
* Specifies a mechanism used by the DID subject to delegate a cryptographic capability to another
* party. This can include delegating access to a specific resource or API.
*
* @see {@link https://www.w3.org/TR/did-core/#capability-delegation | DID Core Specification, § Capability Delegation}
*/
capabilityDelegation = 'capabilityDelegation',
}
+93
View File
@@ -0,0 +1,93 @@
import type { KeyValueStore } from '@web5/common';
import type { DidDereferencingOptions, DidDereferencingResult, DidResolutionOptions, DidResolutionResult } from './did-core.js';
/**
* Represents the interface for resolving a Decentralized Identifier (DID) to its corresponding DID
* document.
*
* The `DidResolver` interface defines a single method, `resolve`, which takes a DID URL as input
* and returns a `Promise` that resolves to a `DidResolutionResult`. This result contains the DID
* document associated with the given DID, along with metadata about the resolution process.
*
* Implementations of this interface are expected to support resolution of DIDs according to the
* specific rules and methods defined by the DID scheme in use.
*
* More information on DID URL dereferencing can be found in the
* {@link https://www.w3.org/TR/did-core/#did-resolution | DID Core specification}.
*
* @example
* ```typescript
* const resolutionResult = await didResolver.resolve('did:example:123456789abcdefghi');
* ```
*/
export interface DidResolver {
/**
* Resolves a DID URI to a DID document and associated metadata.
*
* This function should resolve the DID URI in accordance with the relevant DID method
* specification, using the provided `options`.
*
* @param didUri - The DID URI to be resolved.
* @param options - Optional. The options used for resolving the DID.
* @returns A {@link DidResolutionResult} object containing the DID document and metadata or an
* error.
*/
resolve(didUrl: string, options?: DidResolutionOptions): Promise<DidResolutionResult>;
}
/**
* Interface for cache implementations used by to store resolved DID documents.
*/
export interface DidResolverCache extends KeyValueStore<string, DidResolutionResult | void> {}
/**
* Represents the interface for dereferencing a DID URL to a specific resource within a DID
* document.
*
* The `DidUrlDereferencer` interface defines a single method, `dereference`, which takes a DID URL
* as input and returns a `Promise` that resolves to a `DidDereferencingResult`. This result
* includes the dereferenced resource (if found) and metadata about the dereferencing process.
*
* Dereferencing a DID URL involves parsing the URL to identify the specific part of the DID
* document being referenced, which could be a verification method, a service endpoint, or the
* entire document itself.
*
* Implementations of this interface must adhere to the dereferencing mechanisms defined in the DID
* Core specifications, handling various components of the DID URL including the DID itself, path,
* query, and fragment.
*
* More information on DID URL dereferencing can be found in the
* {@link https://www.w3.org/TR/did-core/#did-url-dereferencing | DID Core specification}.
*
* @example
* ```typescript
* const dereferenceResult = await didUrlDereferencer.dereference('did:example:123456789abcdefghi#keys-1');
* ```
*/
export interface DidUrlDereferencer {
/**
* Dereferences a DID (Decentralized Identifier) URL to a corresponding DID resource.
*
* This method interprets the DID URL's components, which include the DID method, method-specific
* identifier, path, query, and fragment, and retrieves the related resource as per the DID Core
* specifications.
*
* @param didUrl - The DID URL string to dereference.
* @param options - Input options to the dereference function. Optional.
* @returns a {@link DidDereferencingResult}
*/
dereference(didUrl: string, options?: DidDereferencingOptions): Promise<DidDereferencingResult>;
}
/**
* A constant representing an empty DID Resolution Result. This object is used as the basis for a
* result of DID resolution and is typically augmented with additional properties by the
* DID method resolver.
*/
export const EMPTY_DID_RESOLUTION_RESULT: DidResolutionResult = {
'@context' : 'https://w3id.org/did-resolution/v1',
didResolutionMetadata : {},
didDocument : null,
didDocumentMetadata : {},
};
+29
View File
@@ -0,0 +1,29 @@
/**
* Represents a cryptographic key with associated multicodec metadata.
*
* The `KeyWithMulticodec` type encapsulates a cryptographic key along with optional multicodec
* information. It is primarily used in functions that convert between cryptographic keys and their
* string representations, ensuring that the key's format and encoding are preserved and understood
* across different systems and applications.
*/
export type KeyWithMulticodec = {
/**
* A `Uint8Array` representing the raw bytes of the cryptographic key. This is the primary data of
* the type and is essential for cryptographic operations.
*/
keyBytes: Uint8Array,
/**
* An optional number representing the multicodec code. This code uniquely identifies the encoding
* format or protocol associated with the key. The presence of this code is crucial for decoding
* the key correctly in different contexts.
*/
multicodecCode?: number,
/**
* An optional string representing the human-readable name of the multicodec. This name provides
* an easier way to identify the encoding format or protocol of the key, especially when the
* numerical code is not immediately recognizable.
*/
multicodecName?: string
};
+64
View File
@@ -0,0 +1,64 @@
import type { Jwk } from '@web5/crypto';
import type { DidDocument, DidDocumentMetadata } from './did-core.js';
/**
* Represents metadata about a DID resulting from create, update, or deactivate operations.
*/
export interface DidMetadata extends DidDocumentMetadata {
/**
* For DID methods that support publishing, the `published` property indicates whether the DID
* document has been published to the respective network.
*
* A `true` value signifies that the DID document is publicly accessible on the network (e.g.,
* Mainline DHT), allowing it to be resolved by others. A `false` value implies the DID document
* is not published, limiting its visibility to public resolution. Absence of this property
* indicates that the DID method does not support publishing.
*/
published?: boolean;
}
/**
* Format to document a DID identifier, along with its associated data, which can be exported,
* saved to a file, or imported. The intent is bundle all of the necessary metadata to enable usage
* of the DID in different contexts.
*/
/**
* Format that documents the key material and metadata of a Decentralized Identifier (DID) to enable
* usage of the DID in different contexts.
*
* This format is useful for exporting, saving to a file, or importing a DID across process
* boundaries or between different DID method implementations.
*
* @example
* ```ts
* // Generate a new DID.
* const did = await DidExample.create();
*
* // Export to a PortableDid.
* const portableDid = await did.export();
*
* // Instantiate a BearerDid object from a PortableDid.
* const importedDid = await DidExample.import(portableDid);
* // The `importedDid` object should be equivalent to the original `did` object.
* ```
*/
export interface PortableDid {
/** {@inheritDoc Did#uri} */
uri: string;
/**
* The DID document associated with this DID.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, § DID Document}
*/
document: DidDocument;
/** {@inheritDoc DidMetadata} */
metadata: DidMetadata;
/**
* An optional array of private keys associated with the DID document's verification methods.
*/
privateKeys?: Jwk[];
}
+532
View File
@@ -0,0 +1,532 @@
import type { Jwk } from '@web5/crypto';
import type { RequireOnly } from '@web5/common';
import { Convert, Multicodec } from '@web5/common';
import { computeJwkThumbprint } from '@web5/crypto';
import type { KeyWithMulticodec } from './types/multibase.js';
import { DidError, DidErrorCode } from './did-error.js';
import {
DidService,
DidDocument,
DidVerificationMethod,
DidVerificationRelationship,
} from './types/did-core.js';
/**
* Represents a Decentralized Web Node (DWN) service in a DID Document.
*
* A DWN DID service is a specialized type of DID service with the `type` set to
* `DecentralizedWebNode`. It includes specific properties `enc` and `sig` that are used to identify
* the public keys that can be used to interact with the DID Subject. The values of these properties
* are strings or arrays of strings containing one or more verification method `id` values present in
* the same DID document. If the `enc` and/or `sig` properties are an array of strings, an entity
* interacting with the DID subject is expected to use the verification methods in the order they
* are listed.
*
* @example
* ```ts
* const service: DwnDidService = {
* id: 'did:example:123#dwn',
* type: 'DecentralizedWebNode',
* serviceEndpoint: 'https://dwn.tbddev.org/dwn0',
* enc: 'did:example:123#key-1',
* sig: 'did:example:123#key-2'
* }
* ```
*
* @see {@link https://identity.foundation/decentralized-web-node/spec/ | DIF Decentralized Web Node (DWN) Specification}
*/
export interface DwnDidService extends DidService {
/**
* One or more verification method `id` values that can be used to encrypt information
* intended for the DID subject.
*/
enc?: string | string[];
/**
* One or more verification method `id` values that will be used by the DID subject to sign data
* or by another entity to verify signatures created by the DID subject.
*/
sig: string | string[];
}
/**
* Extracts the fragment part of a Decentralized Identifier (DID) verification method identifier.
*
* This function takes any input and aims to return only the fragment of a DID identifier,
* which comes after the '#' symbol in a DID string. It's designed specifically for handling
* DID verification method identifiers. The function returns undefined for non-string inputs, inputs
* that do not contain a '#', or complex data structures like objects or arrays, ensuring that only
* the fragment part of a DID string is extracted when present.
*
* @example
* ```ts
* console.log(extractDidFragment("did:example:123#key-1")); // Output: "key-1"
* console.log(extractDidFragment("did:example:123")); // Output: undefined
* console.log(extractDidFragment({ id: "did:example:123#0", type: "JsonWebKey" })); // Output: undefined
* console.log(extractDidFragment(undefined)); // Output: undefined
* ```
*
* @param input - The input to be processed. Can be of any type, but the function is designed
* to work with strings that represent DID verification method identifiers.
* @returns The fragment part of the DID identifier if the input is a string containing a '#'.
* Returns an empty string for all other inputs, including non-string types, strings
* without a '#', and complex data structures.
*/
export function extractDidFragment(input: unknown): string | undefined {
if (typeof input !== 'string') return undefined;
if (input.length === 0) return undefined;
return input.split('#').pop();
}
/**
* Retrieves services from a given DID document, optionally filtered by `id` or `type`.
*
* If no `id` or `type` filters are provided, all defined services are returned.
*
* The given DID Document must adhere to the
* {@link https://www.w3.org/TR/did-core/ | W3C DID Core Specification}.
*
* @example
* ```ts
* const didDocument = { ... }; // W3C DID document
* const services = getServices({ didDocument, type: 'DecentralizedWebNode' });
* ```
*
* @param params - An object containing input parameters for retrieving services.
* @param params.didDocument - The DID document from which services are retrieved.
* @param params.id - Optional. A string representing the specific service ID to match. If provided, only the service with this ID will be returned.
* @param params.type - Optional. A string representing the specific service type to match. If provided, only the service(s) of this type will be returned.
* @returns An array of services. If no matching service is found, an empty array is returned.
*/
export function getServices({ didDocument, id, type }: {
didDocument: DidDocument;
id?: string;
type?: string;
}): DidService[] {
return didDocument?.service?.filter(service => {
if (id && service.id !== id) return false;
if (type && service.type !== type) return false;
return true;
}) ?? [];
}
/**
* Retrieves a verification method object from a DID document if there is a match for the given
* public key.
*
* This function searches the verification methods in a given DID document for a match with the
* provided public key (either in JWK or multibase format). If a matching verification method is
* found it is returned. If no match is found `null` is returned.
*
*
* @example
* ```ts
* const didDocument = {
* // ... contents of a DID document ...
* };
* const publicKeyJwk = { kty: 'OKP', crv: 'Ed25519', x: '...' };
*
* const verificationMethod = await getVerificationMethodByKey({
* didDocument,
* publicKeyJwk
* });
* ```
*
* @param params - An object containing input parameters for retrieving the verification method ID.
* @param params.didDocument - The DID document to search for the verification method.
* @param params.publicKeyJwk - The public key in JSON Web Key (JWK) format to match against the verification methods in the DID document.
* @param params.publicKeyMultibase - The public key as a multibase encoded string to match against the verification methods in the DID document.
* @returns A promise that resolves with the matching verification method, or `null` if no match is found.
* @throws Throws an `Error` if the `didDocument` parameter is missing or if the `didDocument` does not contain any verification methods.
*/
export async function getVerificationMethodByKey({ didDocument, publicKeyJwk, publicKeyMultibase }: {
didDocument: DidDocument;
publicKeyJwk?: Jwk;
publicKeyMultibase?: string;
}): Promise<DidVerificationMethod | null> {
// Collect all verification methods from the DID document.
const verificationMethods = getVerificationMethods({ didDocument });
for (let method of verificationMethods) {
if (publicKeyJwk && method.publicKeyJwk) {
const publicKeyThumbprint = await computeJwkThumbprint({ jwk: publicKeyJwk });
if (publicKeyThumbprint === await computeJwkThumbprint({ jwk: method.publicKeyJwk })) {
return method;
}
} else if (publicKeyMultibase && method.publicKeyMultibase) {
if (publicKeyMultibase === method.publicKeyMultibase) {
return method;
}
}
}
return null;
}
/**
* Retrieves all verification methods from a given DID document, including embedded methods.
*
* This function consolidates all verification methods into a single array for easy access and
* processing. It checks both the primary `verificationMethod` array and the individual verification
* relationship properties `authentication`, `assertionMethod`, `keyAgreement`,
* `capabilityInvocation`, and `capabilityDelegation` for embedded methods.
*
* The given DID Document must adhere to the
* {@link https://www.w3.org/TR/did-core/ | W3C DID Core Specification}.
*
* @example
* ```ts
* const didDocument = { ... }; // W3C DID document
* const verificationMethods = getVerificationMethods({ didDocument });
* ```
*
* @param params - An object containing input parameters for retrieving verification methods.
* @param params.didDocument - The DID document from which verification methods are retrieved.
* @returns An array of `DidVerificationMethod`. If no verification methods are found, an empty array is returned.
* @throws Throws an `TypeError` if the `didDocument` parameter is missing.
*/
export function getVerificationMethods({ didDocument }: {
didDocument: DidDocument;
}): DidVerificationMethod[] {
if (!didDocument) throw new TypeError(`Required parameter missing: 'didDocument'`);
const verificationMethods: DidVerificationMethod[] = [];
// Check the 'verificationMethod' array.
verificationMethods.push(...didDocument.verificationMethod?.filter(isDidVerificationMethod) ?? []);
// Check verification relationship properties for embedded verification methods.
Object.keys(DidVerificationRelationship).forEach((relationship) => {
verificationMethods.push(
...(didDocument[relationship as keyof DidDocument] as (string | DidVerificationMethod)[])
?.filter(isDidVerificationMethod) ?? []
);
});
return verificationMethods;
}
/**
* Retrieves all DID verification method types from a given DID document.
*
* The given DID Document must adhere to the
* {@link https://www.w3.org/TR/did-core/ | W3C DID Core Specification}.
*
* @example
* ```ts
* const didDocument = {
* verificationMethod: [
* {
* 'id' : 'did:example:123#key-0',
* 'type' : 'Ed25519VerificationKey2018',
* 'controller' : 'did:example:123',
* 'publicKeyBase58' : '3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J'
* },
* {
* 'id' : 'did:example:123#key-1',
* 'type' : 'X25519KeyAgreementKey2019',
* 'controller' : 'did:example:123',
* 'publicKeyBase58' : 'FbQWLPRhTH95MCkQUeFYdiSoQt8zMwetqfWoxqPgaq7x'
* },
* {
* 'id' : 'did:example:123#key-3',
* 'type' : 'JsonWebKey2020',
* 'controller' : 'did:example:123',
* 'publicKeyJwk' : {
* 'kty' : 'EC',
* 'crv' : 'P-256',
* 'x' : 'Er6KSSnAjI70ObRWhlaMgqyIOQYrDJTE94ej5hybQ2M',
* 'y' : 'pPVzCOTJwgikPjuUE6UebfZySqEJ0ZtsWFpj7YSPGEk'
* }
* }
* ]
* },
* const vmTypes = getVerificationMethodTypes({ didDocument });
* console.log(vmTypes);
* // Output: ['Ed25519VerificationKey2018', 'X25519KeyAgreementKey2019', 'JsonWebKey2020']
* ```
*
* @param params - An object containing input parameters for retrieving types.
* @param params.didDocument - The DID document from which types are retrieved.
* @returns An array of types. If no types were found, an empty array is returned.
*/
export function getVerificationMethodTypes({ didDocument }: {
didDocument: DidDocument;
}): string[] {
// Collect all verification methods from the DID document.
const verificationMethods = getVerificationMethods({ didDocument });
// Map to extract 'type' from each verification method.
const types = verificationMethods.map(method => method.type);
return [...new Set(types)]; // Return only unique types.
}
/**
* Retrieves a list of DID verification relationships by a specific method ID from a DID document.
*
* This function examines the specified DID document to identify any verification relationships
* (e.g., `authentication`, `assertionMethod`) that reference a verification method by its method ID
* or contain an embedded verification method matching the method ID. The method ID is typically a
* fragment of a DID (e.g., `did:example:123#key-1`) that uniquely identifies a verification method
* within the DID document.
*
* The search considers both direct references to verification methods by their IDs and verification
* methods embedded within the verification relationship arrays. It returns an array of
* `DidVerificationRelationship` enums corresponding to the verification relationships that contain
* the specified method ID.
*
* @param params - An object containing input parameters for retrieving verification relationships.
* @param params.didDocument - The DID document to search for verification relationships.
* @param params.methodId - The method ID to search for within the verification relationships.
* @returns An array of `DidVerificationRelationship` enums representing the types of verification
* relationships that reference the specified method ID.
*
* @example
* ```ts
* const didDocument: DidDocument = {
* // ...contents of a DID document...
* };
*
* const relationships = getVerificationRelationshipsById({
* didDocument,
* methodId: 'key-1'
* });
* console.log(relationships);
* // Output might include ['authentication', 'assertionMethod'] if those relationships
* // reference or contain the specified method ID.
* ```
*/
export function getVerificationRelationshipsById({ didDocument, methodId }: {
didDocument: DidDocument;
methodId: string;
}): DidVerificationRelationship[] {
const relationships: DidVerificationRelationship[] = [];
Object.keys(DidVerificationRelationship).forEach((relationship) => {
if (Array.isArray(didDocument[relationship as keyof DidDocument])) {
const relationshipMethods = didDocument[relationship as keyof DidDocument] as (string | DidVerificationMethod)[];
const methodIdFragment = extractDidFragment(methodId);
// Check if the verification relationship property contains a matching method ID either
// directly referenced or as an embedded verification method.
const containsMethodId = relationshipMethods.some(method => {
const isByReferenceMatch = extractDidFragment(method) === methodIdFragment;
const isEmbeddedMethodMatch = isDidVerificationMethod(method) && extractDidFragment(method.id) === methodIdFragment;
return isByReferenceMatch || isEmbeddedMethodMatch;
});
if (containsMethodId) {
relationships.push(relationship as DidVerificationRelationship);
}
}
});
return relationships;
}
/**
* Checks if a given object is a {@link DidService}.
*
* A {@link DidService} in the context of DID resources must include the properties `id`, `type`,
* and `serviceEndpoint`. The `serviceEndpoint` can be a `DidServiceEndpoint` or an array of
* `DidServiceEndpoint` objects.
*
* @example
* ```ts
* const service = {
* id: "did:example:123#service-1",
* type: "OidcService",
* serviceEndpoint: "https://example.com/oidc"
* };
*
* if (isDidService(service)) {
* console.log('The object is a DidService');
* } else {
* console.log('The object is not a DidService');
* }
* ```
*
* @param obj - The object to be checked.
* @returns `true` if `obj` is a `DidService`; otherwise, `false`.
*/
export function isDidService(obj: unknown): obj is DidService {
// Validate that the given value is an object.
if (!obj || typeof obj !== 'object' || obj === null) return false;
// Validate that the object has the necessary properties of DidService.
return 'id' in obj && 'type' in obj && 'serviceEndpoint' in obj;
}
/**
* Checks if a given object is a {@link DwnDidService}.
*
* A {@link DwnDidService} is defined as {@link DidService} object with a `type` of
* "DecentralizedWebNode" and `enc` and `sig` properties, where both properties are either strings
* or arrays of strings.
*
* @example
* ```ts
* const didDocument: DidDocument = {
* id: 'did:example:123',
* verificationMethod: [
* {
* id: 'did:example:123#key-1',
* type: 'JsonWebKey2020',
* controller: 'did:example:123',
* publicKeyJwk: { ... }
* },
* {
* id: 'did:example:123#key-2',
* type: 'JsonWebKey2020',
* controller: 'did:example:123',
* publicKeyJwk: { ... }
* }
* ],
* service: [
* {
* id: 'did:example:123#dwn',
* type: 'DecentralizedWebNode',
* serviceEndpoint: 'https://dwn.tbddev.org/dwn0',
* enc: 'did:example:123#key-1',
* sig: 'did:example:123#key-2'
* }
* ]
* };
*
* if (isDwnService(didDocument.service[0])) {
* console.log('The object is a DwnDidService');
* } else {
* console.log('The object is not a DwnDidService');
* }
* ```
*
* @see {@link https://identity.foundation/decentralized-web-node/spec/ | Decentralized Web Node (DWN) Specification}
*
* @param obj - The object to be checked.
* @returns `true` if `obj` is a DwnDidService; otherwise, `false`.
*/
export function isDwnDidService(obj: unknown): obj is DwnDidService {
// Validate that the given value is a {@link DidService}.
if (!isDidService(obj)) return false;
// Validate that the `type` property is `DecentralizedWebNode`.
if (obj.type !== 'DecentralizedWebNode') return false;
// Validate that the given object has the `enc` and `sig` properties.
if (!('enc' in obj && 'sig' in obj)) return false;
// Validate that the `enc` and `sig` properties are either strings or arrays of strings.
const isStringOrStringArray = (prop: any): boolean =>
typeof prop === 'string' || Array.isArray(prop) && prop.every(item => typeof item === 'string');
return (isStringOrStringArray(obj.enc)) && (isStringOrStringArray(obj.sig));
}
/**
* Checks if a given object is a DID Verification Method.
*
* A {@link DidVerificationMethod} in the context of DID resources must include the properties `id`,
* `type`, and `controller`.
*
* @example
* ```ts
* const resource = {
* id : "did:example:123#0",
* type : "JsonWebKey2020",
* controller : "did:example:123",
* publicKeyJwk : { ... }
* };
*
* if (isDidVerificationMethod(resource)) {
* console.log('The resource is a DidVerificationMethod');
* } else {
* console.log('The resource is not a DidVerificationMethod');
* }
* ```
*
* @param obj - The object to be checked.
* @returns `true` if `obj` is a `DidVerificationMethod`; otherwise, `false`.
*/
export function isDidVerificationMethod(obj: unknown): obj is DidVerificationMethod {
// Validate that the given value is an object.
if (!obj || typeof obj !== 'object' || obj === null) return false;
// Validate that the object has the necessary properties of a DidVerificationMethod.
if (!('id' in obj && 'type' in obj && 'controller' in obj)) return false;
if (typeof obj.id !== 'string') return false;
if (typeof obj.type !== 'string') return false;
if (typeof obj.controller !== 'string') return false;
return true;
}
/**
* Converts a cryptographic key to a multibase identifier.
*
* @remarks
* This method provides a way to represent a cryptographic key as a multibase identifier.
* It takes a `Uint8Array` representing the key, and either the multicodec code or multicodec name
* as input. The method first adds the multicodec prefix to the key, then encodes it into Base58
* format. Finally, it converts the Base58 encoded key into a multibase identifier.
*
* @example
* ```ts
* const key = new Uint8Array([...]); // Cryptographic key as Uint8Array
* const multibaseId = keyBytesToMultibaseId({ key, multicodecName: 'ed25519-pub' });
* ```
*
* @param params - The parameters for the conversion.
* @returns The multibase identifier as a string.
*/
export function keyBytesToMultibaseId({ keyBytes, multicodecCode, multicodecName }:
RequireOnly<KeyWithMulticodec, 'keyBytes'>
): string {
const prefixedKey = Multicodec.addPrefix({
code : multicodecCode,
data : keyBytes,
name : multicodecName
});
const prefixedKeyB58 = Convert.uint8Array(prefixedKey).toBase58Btc();
const multibaseKeyId = Convert.base58Btc(prefixedKeyB58).toMultibase();
return multibaseKeyId;
}
/**
* Converts a multibase identifier to a cryptographic key.
*
* @remarks
* This function decodes a multibase identifier back into a cryptographic key. It first decodes the
* identifier from multibase format into Base58 format, and then converts it into a `Uint8Array`.
* Afterward, it removes the multicodec prefix, extracting the raw key data along with the
* multicodec code and name.
*
* @example
* ```ts
* const multibaseKeyId = '...'; // Multibase identifier of the key
* const { key, multicodecCode, multicodecName } = multibaseIdToKey({ multibaseKeyId });
* ```
*
* @param params - The parameters for the conversion.
* @param params.multibaseKeyId - The multibase identifier string of the key.
* @returns An object containing the key as a `Uint8Array` and its multicodec code and name.
* @throws `DidError` if the multibase identifier is invalid.
*/
export function multibaseIdToKeyBytes({ multibaseKeyId }: {
multibaseKeyId: string
}): Required<KeyWithMulticodec> {
try {
const prefixedKeyB58 = Convert.multibase(multibaseKeyId).toBase58Btc();
const prefixedKey = Convert.base58Btc(prefixedKeyB58).toUint8Array();
const { code, data, name } = Multicodec.removePrefix({ prefixedData: prefixedKey });
return { keyBytes: data, multicodecCode: code, multicodecName: name };
} catch (error: any) {
throw new DidError(DidErrorCode.InvalidDid, `Invalid multibase identifier: ${multibaseKeyId}`);
}
}