UNPKG

@cheqd/sdk

Version:

A TypeScript SDK built with CosmJS to interact with the cheqd network ledger

948 lines 57.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DIDModule = exports.setupDidExtension = exports.typeUrlMsgDeactivateDidDocResponse = exports.typeUrlMsgDeactivateDidDoc = exports.typeUrlMsgUpdateDidDocResponse = exports.typeUrlMsgUpdateDidDoc = exports.typeUrlMsgCreateDidDocResponse = exports.typeUrlMsgCreateDidDoc = exports.protobufLiterals = exports.contexts = exports.defaultDidExtensionKey = void 0; exports.isMsgCreateDidDocEncodeObject = isMsgCreateDidDocEncodeObject; exports.isMsgUpdateDidDocEncodeObject = isMsgUpdateDidDocEncodeObject; exports.isMsgDeactivateDidDocEncodeObject = isMsgDeactivateDidDocEncodeObject; exports.MsgCreateDidDocResponseEncodeObject = MsgCreateDidDocResponseEncodeObject; exports.MsgUpdateDidDocEncodeObject = MsgUpdateDidDocEncodeObject; exports.MsgUpdateDidDocResponseEncodeObject = MsgUpdateDidDocResponseEncodeObject; exports.MsgDeactivateDidDocEncodeObject = MsgDeactivateDidDocEncodeObject; exports.MsgDeactiveDidDocResponseEncodeObject = MsgDeactiveDidDocResponseEncodeObject; const stargate_cjs_1 = require("@cosmjs/stargate-cjs"); const _1 = require("./_"); const types_1 = require("../types"); const index_1 = require("@cheqd/ts-proto-cjs/cheqd/did/v2/index"); const uuid_cjs_1 = require("uuid-cjs"); const utils_cjs_1 = require("@cosmjs/utils-cjs"); const utils_1 = require("../utils"); /** Default extension key for DID-related query operations */ exports.defaultDidExtensionKey = 'did'; /** * Standard W3C and DID-related context URIs used in DID documents. * These contexts define the semantic meaning of properties in DID documents. */ exports.contexts = { /** W3C DID Core v1 context */ W3CDIDv1: 'https://www.w3.org/ns/did/v1', /** Ed25519 Signature Suite 2020 context */ W3CSuiteEd255192020: 'https://w3id.org/security/suites/ed25519-2020/v1', /** Ed25519 Signature Suite 2018 context */ W3CSuiteEd255192018: 'https://w3id.org/security/suites/ed25519-2018/v1', /** JSON Web Signature Suite 2020 context */ W3CSuiteJws2020: 'https://w3id.org/security/suites/jws-2020/v1', /** Linked Domains context for domain verification */ LinkedDomainsContext: 'https://identity.foundation/.well-known/did-configuration/v1', }; /** * Protobuf message type literals for DID operations. * Used for consistent message type identification across the module. */ exports.protobufLiterals = { /** Create DID document message type */ MsgCreateDidDoc: 'MsgCreateDidDoc', /** Create DID document response message type */ MsgCreateDidDocResponse: 'MsgCreateDidDocResponse', /** Update DID document message type */ MsgUpdateDidDoc: 'MsgUpdateDidDoc', /** Update DID document response message type */ MsgUpdateDidDocResponse: 'MsgUpdateDidDocResponse', /** Deactivate DID document message type */ MsgDeactivateDidDoc: 'MsgDeactivateDidDoc', /** Deactivate DID document response message type */ MsgDeactivateDidDocResponse: 'MsgDeactivateDidDocResponse', }; /** Type URL for MsgCreateDidDoc messages */ exports.typeUrlMsgCreateDidDoc = `/${index_1.protobufPackage}.${exports.protobufLiterals.MsgCreateDidDoc}`; /** Type URL for MsgCreateDidDocResponse messages */ exports.typeUrlMsgCreateDidDocResponse = `/${index_1.protobufPackage}.${exports.protobufLiterals.MsgCreateDidDocResponse}`; /** Type URL for MsgUpdateDidDoc messages */ exports.typeUrlMsgUpdateDidDoc = `/${index_1.protobufPackage}.${exports.protobufLiterals.MsgUpdateDidDoc}`; /** Type URL for MsgUpdateDidDocResponse messages */ exports.typeUrlMsgUpdateDidDocResponse = `/${index_1.protobufPackage}.${exports.protobufLiterals.MsgUpdateDidDocResponse}`; /** Type URL for MsgDeactivateDidDoc messages */ exports.typeUrlMsgDeactivateDidDoc = `/${index_1.protobufPackage}.${exports.protobufLiterals.MsgDeactivateDidDoc}`; /** Type URL for MsgDeactivateDidDocResponse messages */ exports.typeUrlMsgDeactivateDidDocResponse = `/${index_1.protobufPackage}.${exports.protobufLiterals.MsgDeactivateDidDocResponse}`; /** * Type guard function to check if an object is a MsgCreateDidDocEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgCreateDidDocEncodeObject */ function isMsgCreateDidDocEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgCreateDidDoc; } /** * Type guard function to check if an object is a MsgUpdateDidDocEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgUpdateDidDocEncodeObject */ function isMsgUpdateDidDocEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgUpdateDidDoc; } /** * Type guard function to check if an object is a MsgDeactivateDidDocEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgDeactivateDidDocEncodeObject */ function isMsgDeactivateDidDocEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgDeactivateDidDoc; } /** * Type guard function to check if an object is a MsgCreateDidDocResponseEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgCreateDidDocResponseEncodeObject */ function MsgCreateDidDocResponseEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgCreateDidDocResponse; } /** * Type guard function to check if an object is a MsgUpdateDidDocEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgUpdateDidDocEncodeObject */ function MsgUpdateDidDocEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgUpdateDidDoc; } /** * Type guard function to check if an object is a MsgUpdateDidDocResponseEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgUpdateDidDocResponseEncodeObject */ function MsgUpdateDidDocResponseEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgUpdateDidDocResponse; } /** * Type guard function to check if an object is a MsgDeactivateDidDocEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgDeactivateDidDocEncodeObject */ function MsgDeactivateDidDocEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgDeactivateDidDoc; } /** * Type guard function to check if an object is a MsgDeactivateDidDocResponseEncodeObject. * * @param obj - EncodeObject to check * @returns True if the object is a MsgDeactivateDidDocResponseEncodeObject */ function MsgDeactiveDidDocResponseEncodeObject(obj) { return obj.typeUrl === exports.typeUrlMsgUpdateDidDocResponse; } /** * Sets up the DID extension for the querier client. * Creates and configures the DID-specific query methods. * * @param base - Base QueryClient to extend * @returns Configured DID extension with query methods */ const setupDidExtension = (base) => { const rpc = (0, stargate_cjs_1.createProtobufRpcClient)(base); const queryService = new index_1.QueryClientImpl(rpc); return { [exports.defaultDidExtensionKey]: { didDoc: async (id) => { const { value } = await queryService.DidDoc({ id }); (0, utils_cjs_1.assert)(value); return value; }, didDocVersion: async (id, versionId) => { const { value } = await queryService.DidDocVersion({ id, version: versionId, }); (0, utils_cjs_1.assert)(value); return value; }, allDidDocVersionsMetadata: async (id, paginationKey) => { const response = await queryService.AllDidDocVersionsMetadata({ id, pagination: (0, stargate_cjs_1.createPagination)(paginationKey), }); return response; }, }, }; }; exports.setupDidExtension = setupDidExtension; /** * DID Module class providing comprehensive DID document management functionality. * Handles creation, updates, deactivation, and querying of DID documents on the Cheqd blockchain. */ class DIDModule extends _1.AbstractCheqdSDKModule { //@ts-expect-error the underlying type is intentionally wider static registryTypes = [ [exports.typeUrlMsgCreateDidDoc, index_1.MsgCreateDidDoc], [exports.typeUrlMsgCreateDidDocResponse, index_1.MsgCreateDidDocResponse], [exports.typeUrlMsgUpdateDidDoc, index_1.MsgUpdateDidDoc], [exports.typeUrlMsgUpdateDidDocResponse, index_1.MsgUpdateDidDocResponse], [exports.typeUrlMsgDeactivateDidDoc, index_1.MsgDeactivateDidDoc], [exports.typeUrlMsgDeactivateDidDocResponse, index_1.MsgDeactivateDidDocResponse], ]; /** Base denomination for Cheqd network transactions */ static baseMinimalDenom = 'ncheq'; /** * Standard fee amounts for DID operations. * These represent the default costs for different DID document operations. */ static fees = { /** Default fee for creating a new DID document */ DefaultCreateDidDocFee: { amount: '50000000000', denom: DIDModule.baseMinimalDenom, }, /** Default fee for updating an existing DID document */ DefaultUpdateDidDocFee: { amount: '25000000000', denom: DIDModule.baseMinimalDenom, }, /** Default fee for deactivating a DID document */ DefaultDeactivateDidDocFee: { amount: '10000000000', denom: DIDModule.baseMinimalDenom, }, }; /** Querier extension setup function for DID operations */ static querierExtensionSetup = exports.setupDidExtension; /** Querier instance with DID extension capabilities */ querier; /** * Constructs a new DID module instance. * * @param signer - Signing client for blockchain transactions * @param querier - Querier client with DID extension for data retrieval */ constructor(signer, querier) { super(signer, querier); this.querier = querier; this.methods = { createDidDocTx: this.createDidDocTx.bind(this), updateDidDocTx: this.updateDidDocTx.bind(this), deactivateDidDocTx: this.deactivateDidDocTx.bind(this), queryDidDoc: this.queryDidDoc.bind(this), queryDidDocVersion: this.queryDidDocVersion.bind(this), queryAllDidDocVersionsMetadata: this.queryAllDidDocVersionsMetadata.bind(this), }; } /** * Gets the registry types for DID message encoding/decoding. * * @returns Iterable of [typeUrl, GeneratedType] pairs for the registry */ getRegistryTypes() { return DIDModule.registryTypes; } /** * Gets the querier extension setup for DID operations. * * @returns Query extension setup function for DID functionality */ getQuerierExtensionSetup() { return DIDModule.querierExtensionSetup; } /** * Creates a new DID document transaction on the blockchain. * Validates the DID payload and authentication before submission. * * @param signInputs - Signing inputs or pre-computed signatures for the transaction * @param didPayload - DID document payload to create * @param address - Address of the account submitting the transaction * @param fee - Transaction fee configuration or 'auto' for automatic calculation * @param memo - Optional transaction memo * @param versionId - Optional version identifier for the DID document * @param context - Optional SDK context for accessing clients * @returns Promise resolving to the transaction response * @throws Error if DID payload is not spec compliant or authentication is invalid */ async createDidDocTx(signInputs, didPayload, address, fee, memo, versionId, context) { if (!this._signer) { this._signer = context.sdk.signer; } if (!this.querier) { this.querier = context.sdk.querier; } if (!versionId || versionId === '') { versionId = (0, uuid_cjs_1.v4)(); } const { valid, error, protobufVerificationMethod, protobufService } = await DIDModule.validateSpecCompliantPayload(didPayload); if (!valid) { throw new Error(`DID payload is not spec compliant: ${error}`); } const { valid: authenticationValid, error: authenticationError } = await DIDModule.validateAuthenticationAgainstSignatures(didPayload, signInputs, this.querier); if (!authenticationValid) { throw new Error(`DID authentication is not valid: ${authenticationError}`); } const payload = index_1.MsgCreateDidDocPayload.fromPartial({ context: didPayload?.['@context'], id: didPayload.id, controller: didPayload.controller, verificationMethod: protobufVerificationMethod, authentication: didPayload.authentication, assertionMethod: didPayload.assertionMethod, capabilityInvocation: didPayload.capabilityInvocation, capabilityDelegation: didPayload.capabilityDelegation, keyAgreement: didPayload.keyAgreement, service: protobufService, alsoKnownAs: didPayload.alsoKnownAs, versionId: versionId, }); let signatures; if (types_1.ISignInputs.isSignInput(signInputs)) { signatures = await this._signer.signCreateDidDocTx(signInputs, payload); } else { signatures = signInputs; } const value = { payload, signatures, }; const createDidMsg = { typeUrl: exports.typeUrlMsgCreateDidDoc, value, }; if (address === '') { address = (await context.sdk.options.wallet.getAccounts())[0].address; } if (!fee) { fee = await DIDModule.generateCreateDidDocFees(address); } return this._signer.signAndBroadcast(address, [createDidMsg], fee, memo); } /** * Updates an existing DID document transaction on the blockchain. * Validates the updated DID payload and handles key rotation scenarios. * * @param signInputs - Signing inputs or pre-computed signatures for the transaction * @param didPayload - Updated DID document payload * @param address - Address of the account submitting the transaction * @param fee - Transaction fee configuration or 'auto' for automatic calculation * @param memo - Optional transaction memo * @param versionId - Optional version identifier for the updated DID document * @param context - Optional SDK context for accessing clients * @returns Promise resolving to the transaction response * @throws Error if DID payload is not spec compliant or authentication is invalid */ async updateDidDocTx(signInputs, didPayload, address, fee, memo, versionId, context) { if (!this._signer) { this._signer = context.sdk.signer; } if (!this.querier) { this.querier = context.sdk.querier; } if (!versionId || versionId === '') { versionId = (0, uuid_cjs_1.v4)(); } const { valid, error, protobufVerificationMethod, protobufService } = await DIDModule.validateSpecCompliantPayload(didPayload); if (!valid) { throw new Error(`DID payload is not spec compliant: ${error}`); } const { valid: authenticationValid, error: authenticationError, externalControllersDocuments, previousDidDocument, } = await DIDModule.validateAuthenticationAgainstSignaturesKeyRotation(didPayload, signInputs, this.querier); if (!authenticationValid) { throw new Error(`DID authentication is not valid: ${authenticationError}`); } const payload = index_1.MsgUpdateDidDocPayload.fromPartial({ context: didPayload?.['@context'], id: didPayload.id, controller: didPayload.controller, verificationMethod: protobufVerificationMethod, authentication: didPayload.authentication, assertionMethod: didPayload.assertionMethod, capabilityInvocation: didPayload.capabilityInvocation, capabilityDelegation: didPayload.capabilityDelegation, keyAgreement: didPayload.keyAgreement, service: protobufService, alsoKnownAs: didPayload.alsoKnownAs, versionId: versionId, }); let signatures; if (types_1.ISignInputs.isSignInput(signInputs)) { signatures = await this._signer.signUpdateDidDocTx(signInputs, payload, externalControllersDocuments, previousDidDocument); } else { signatures = signInputs; } const value = { payload, signatures, }; const updateDidMsg = { typeUrl: exports.typeUrlMsgUpdateDidDoc, value, }; if (address === '') { address = (await context.sdk.options.wallet.getAccounts())[0].address; } if (!fee) { fee = await DIDModule.generateUpdateDidDocFees(address); } return this._signer.signAndBroadcast(address, [updateDidMsg], fee, memo); } /** * Deactivates an existing DID document transaction on the blockchain. * Validates authentication and creates a deactivation transaction. * * @param signInputs - Signing inputs or pre-computed signatures for the transaction * @param didPayload - DID document payload containing the ID to deactivate * @param address - Address of the account submitting the transaction * @param fee - Transaction fee configuration or 'auto' for automatic calculation * @param memo - Optional transaction memo * @param versionId - Optional version identifier for the deactivation * @param context - Optional SDK context for accessing clients * @returns Promise resolving to the transaction response * @throws Error if DID payload is not spec compliant or authentication is invalid */ async deactivateDidDocTx(signInputs, didPayload, address, fee, memo, versionId, context) { if (!this._signer) { this._signer = context.sdk.signer; } if (!versionId || versionId === '') { versionId = (0, uuid_cjs_1.v4)(); } const { valid, error, protobufVerificationMethod } = await DIDModule.validateSpecCompliantPayload(didPayload); if (!valid) { throw new Error(`DID payload is not spec compliant: ${error}`); } const { valid: authenticationValid, error: authenticationError } = await DIDModule.validateAuthenticationAgainstSignatures(didPayload, signInputs, this.querier); if (!authenticationValid) { throw new Error(`DID authentication is not valid: ${authenticationError}`); } const payload = index_1.MsgDeactivateDidDocPayload.fromPartial({ id: didPayload.id, versionId: versionId, }); let signatures; if (types_1.ISignInputs.isSignInput(signInputs)) { signatures = await this._signer.signDeactivateDidDocTx(signInputs, payload, protobufVerificationMethod); } else { signatures = signInputs; } const value = { payload, signatures, }; const deactivateDidMsg = { typeUrl: exports.typeUrlMsgDeactivateDidDoc, value, }; if (address === '') { address = (await context.sdk.options.wallet.getAccounts())[0].address; } if (!fee) { fee = await DIDModule.generateDeactivateDidDocFees(address); } return this._signer.signAndBroadcast(address, [deactivateDidMsg], fee, memo); } /** * Queries a DID document by its identifier. * Retrieves the latest version of the DID document with metadata. * * @param id - DID identifier to query * @param context - Optional SDK context for accessing clients * @returns Promise resolving to the DID document with metadata */ async queryDidDoc(id, context) { if (!this.querier) { this.querier = context.sdk.querier; } const { didDoc, metadata } = await this.querier[exports.defaultDidExtensionKey].didDoc(id); return { didDocument: await DIDModule.toSpecCompliantPayload(didDoc), didDocumentMetadata: await DIDModule.toSpecCompliantMetadata(metadata), }; } /** * Queries a specific version of a DID document by its identifier and version ID. * * @param id - DID identifier to query * @param versionId - Specific version identifier to retrieve * @param context - Optional SDK context for accessing clients * @returns Promise resolving to the DID document version with metadata */ async queryDidDocVersion(id, versionId, context) { if (!this.querier) { this.querier = context.sdk.querier; } const { didDoc, metadata } = await this.querier[exports.defaultDidExtensionKey].didDocVersion(id, versionId); return { didDocument: await DIDModule.toSpecCompliantPayload(didDoc), didDocumentMetadata: await DIDModule.toSpecCompliantMetadata(metadata), }; } /** * Queries metadata for all versions of a DID document. * Retrieves version history information for a specific DID. * * @param id - DID identifier to query version metadata for * @param context - Optional SDK context for accessing clients * @returns Promise resolving to array of version metadata and pagination info */ async queryAllDidDocVersionsMetadata(id, context) { if (!this.querier) { this.querier = context.sdk.querier; } const { versions, pagination } = await this.querier[exports.defaultDidExtensionKey].allDidDocVersionsMetadata(id); return { didDocumentVersionsMetadata: await Promise.all(versions.map(async (m) => await DIDModule.toSpecCompliantMetadata(m))), pagination, }; } /** * Validates a DID document against the Cheqd specification requirements. * Ensures all required fields are present and verification methods are supported. * * @param didDocument - DID document to validate * @returns Promise resolving to validation result with protobuf conversion or error details */ static async validateSpecCompliantPayload(didDocument) { // id is required, validated on both compile and runtime if (!didDocument?.id) return { valid: false, error: 'id is required' }; // verificationMethod is required if (!didDocument?.verificationMethod) return { valid: false, error: 'verificationMethod is required' }; // verificationMethod must be an array if (!Array.isArray(didDocument?.verificationMethod)) return { valid: false, error: 'verificationMethod must be an array' }; // verificationMethod types must be supported const protoVerificationMethod = didDocument.verificationMethod.map((vm) => { switch (vm?.type) { case types_1.VerificationMethods.Ed255192020: if (!vm?.publicKeyMultibase) throw new Error('publicKeyMultibase is required'); return index_1.VerificationMethod.fromPartial({ id: vm.id, controller: vm.controller, verificationMethodType: types_1.VerificationMethods.Ed255192020, verificationMaterial: vm.publicKeyMultibase, }); case types_1.VerificationMethods.JWK: if (!vm?.publicKeyJwk) throw new Error('publicKeyJwk is required'); return index_1.VerificationMethod.fromPartial({ id: vm.id, controller: vm.controller, verificationMethodType: types_1.VerificationMethods.JWK, verificationMaterial: JSON.stringify(vm.publicKeyJwk), }); case types_1.VerificationMethods.Ed255192018: if (!vm?.publicKeyBase58) throw new Error('publicKeyBase58 is required'); return index_1.VerificationMethod.fromPartial({ id: vm.id, controller: vm.controller, verificationMethodType: types_1.VerificationMethods.Ed255192018, verificationMaterial: vm.publicKeyBase58, }); default: throw new Error('Unsupported verificationMethod type'); } }); const protoService = (0, utils_1.normalizeService)(didDocument); return { valid: true, protobufVerificationMethod: protoVerificationMethod, protobufService: protoService, }; } /** * Converts a protobuf DID document to a specification-compliant DID document format. * Handles context inclusion, verification method formatting, and service denormalization. * * @param protobufDidDocument - Protobuf DID document to convert * @returns Promise resolving to a spec-compliant DID document */ static async toSpecCompliantPayload(protobufDidDocument) { const verificationMethod = protobufDidDocument.verificationMethod.map((vm) => { switch (vm.verificationMethodType) { case types_1.VerificationMethods.Ed255192020: if (!protobufDidDocument.context.includes(exports.contexts.W3CSuiteEd255192020)) protobufDidDocument.context = [...protobufDidDocument.context, exports.contexts.W3CSuiteEd255192020]; return { id: vm.id, type: vm.verificationMethodType, controller: vm.controller, publicKeyMultibase: vm.verificationMaterial, }; case types_1.VerificationMethods.JWK: if (!protobufDidDocument.context.includes(exports.contexts.W3CSuiteJws2020)) protobufDidDocument.context = [...protobufDidDocument.context, exports.contexts.W3CSuiteJws2020]; return { id: vm.id, type: vm.verificationMethodType, controller: vm.controller, publicKeyJwk: JSON.parse(vm.verificationMaterial), }; case types_1.VerificationMethods.Ed255192018: if (!protobufDidDocument.context.includes(exports.contexts.W3CSuiteEd255192018)) protobufDidDocument.context = [...protobufDidDocument.context, exports.contexts.W3CSuiteEd255192018]; return { id: vm.id, type: vm.verificationMethodType, controller: vm.controller, publicKeyBase58: vm.verificationMaterial, }; default: throw new Error('Unsupported verificationMethod type'); // should never happen } }); const service = (0, utils_1.denormalizeService)(protobufDidDocument); const context = (function () { if (protobufDidDocument.context.includes(exports.contexts.W3CDIDv1)) return protobufDidDocument.context; return [exports.contexts.W3CDIDv1, ...protobufDidDocument.context]; })(); const assertionMethod = protobufDidDocument.assertionMethod.map((am) => { try { // Check if the assertionMethod is a DID URL if (!am.startsWith('did:cheqd:')) { // Parse once if it's a stringified JSON const parsedAm = JSON.parse(am); if (typeof parsedAm === 'string') { // Parse again only if necessary return JSON.parse(parsedAm); } return parsedAm; } return am; } catch (error) { throw new Error(`Unsupported assertionMethod type: ${am}`); } }); const specCompliant = { '@context': context, id: protobufDidDocument.id, controller: protobufDidDocument.controller, verificationMethod: verificationMethod, authentication: protobufDidDocument.authentication, assertionMethod: assertionMethod, capabilityInvocation: protobufDidDocument.capabilityInvocation, capabilityDelegation: protobufDidDocument.capabilityDelegation, keyAgreement: protobufDidDocument.keyAgreement, service: service, alsoKnownAs: protobufDidDocument.alsoKnownAs, }; if (!protobufDidDocument.authentication?.length) delete specCompliant.authentication; if (!protobufDidDocument.assertionMethod?.length) delete specCompliant.assertionMethod; if (!protobufDidDocument.capabilityInvocation?.length) delete specCompliant.capabilityInvocation; if (!protobufDidDocument.capabilityDelegation?.length) delete specCompliant.capabilityDelegation; if (!protobufDidDocument.keyAgreement?.length) delete specCompliant.keyAgreement; if (!protobufDidDocument.service?.length) delete specCompliant.service; if (!protobufDidDocument.alsoKnownAs?.length) delete specCompliant.alsoKnownAs; return specCompliant; } /** * Converts protobuf metadata to specification-compliant DID document metadata format. * Handles date formatting and optional field normalization. * * @param protobufDidDocument - Protobuf metadata to convert * @returns Promise resolving to spec-compliant DID document metadata */ static async toSpecCompliantMetadata(protobufDidDocument) { return { created: protobufDidDocument.created?.toISOString(), updated: protobufDidDocument.updated?.toISOString(), deactivated: protobufDidDocument.deactivated, versionId: protobufDidDocument.versionId, nextVersionId: protobufDidDocument?.nextVersionId, previousVersionId: protobufDidDocument?.previousVersionId, }; } /** * Validates that provided signatures match the authentication requirements in a DID document. * Checks signature count, authentication presence, and controller authorization. * * @param didDocument - DID document containing authentication requirements * @param signatures - Array of signatures to validate against authentication * @param querier - Optional querier for retrieving external controller documents * @param externalControllersDidDocuments - Optional pre-loaded external controller documents * @returns Promise resolving to validation result with error details if invalid */ static async validateAuthenticationAgainstSignatures(didDocument, signatures, querier, externalControllersDidDocuments) { // validate signatures - case: no signatures if (!signatures || !signatures.length) return { valid: false, error: 'signatures are required' }; // validate authentication - case: no authentication when at least one verificationMethod if ((!didDocument.authentication || !didDocument.authentication.length) && didDocument.verificationMethod?.length) return { valid: false, error: 'authentication is required' }; const normalizedAuthentication = (0, utils_1.normalizeAuthentication)(didDocument); // define unique authentication const uniqueAuthentication = new Set(normalizedAuthentication); // validate authentication - case: authentication contains duplicates if (uniqueAuthentication.size < normalizedAuthentication.length) return { valid: false, error: `authentication contains duplicate key references: duplicate key reference ${Array.from(uniqueAuthentication).find((a) => (0, utils_1.normalizeAuthentication)(didDocument).filter((aa) => aa === a).length > 1)}`, }; // define unique signatures - shallow, only verificationMethodId, no signature const uniqueSignatures = new Set(signatures.map((s) => s.verificationMethodId)); // validate signatures - case: signatures contain duplicates if (uniqueSignatures.size < signatures.length) return { valid: false, error: `signatures contain duplicates: duplicate signature for key reference ${Array.from(uniqueSignatures).find((s) => signatures.filter((ss) => ss.verificationMethodId === s).length > 1)}`, }; // validate authentication - case: authentication contains invalid key references if (!Array.from(uniqueAuthentication).every((a) => didDocument.verificationMethod?.some((vm) => vm.id === a))) return { valid: false, error: `authentication contains invalid key references: invalid key reference ${Array.from(uniqueAuthentication).find((a) => !didDocument.verificationMethod?.some((vm) => vm.id === a))}`, }; // define whether external controller or not const externalController = (0, utils_1.normalizeController)(didDocument); // validate authentication - case: authentication matches signatures, unique, if no external controller if (!Array.from(uniqueAuthentication).every((a) => uniqueSignatures.has(a)) && !externalController) return { valid: false, error: `authentication does not match signatures: signature from key ${Array.from(uniqueAuthentication).find((a) => !uniqueSignatures.has(a))} is missing`, }; // validate signatures - case: authentication matches signatures, unique, excessive signatures, no external controller if (!Array.from(uniqueSignatures).every((s) => uniqueAuthentication.has(s)) && !externalController) return { valid: false, error: `authentication does not match signatures: signature from key ${Array.from(uniqueSignatures).find((s) => !uniqueAuthentication.has(s))} is not required`, }; // return, if no external controller if (!externalController) return { valid: true }; // require querier if (!querier) throw new Error('querier is required for external controller validation'); // get external controllers const externalControllers = externalController.filter((c) => c !== didDocument.id); // get external controllers' documents const externalControllersDocuments = await Promise.all(externalControllers?.map(async (c) => { // compute index of external controller's document, if provided const externalControllerDocumentIndex = externalControllersDidDocuments?.findIndex((d) => d.id === c); // get external controller's document, if provided if (externalControllerDocumentIndex !== undefined && externalControllerDocumentIndex !== -1) return externalControllersDidDocuments?.[externalControllerDocumentIndex]; // fetch external controller's document const protobufDocument = await querier[exports.defaultDidExtensionKey].didDoc(c); // throw, if not found if (!protobufDocument || !protobufDocument.didDoc) throw new Error(`Document for controller ${c} not found`); // convert to spec compliant payload return await DIDModule.toSpecCompliantPayload(protobufDocument.didDoc); })); // define unique required signatures const uniqueRequiredSignatures = new Set(externalControllersDocuments.concat(didDocument).flatMap((d) => (d ? (0, utils_1.normalizeAuthentication)(d) : []))); // validate authentication - case: authentication matches signatures, unique, if external controller if (!Array.from(uniqueRequiredSignatures).every((a) => uniqueSignatures.has(a))) return { valid: false, error: `authentication does not match signatures: signature from key ${Array.from(uniqueRequiredSignatures).find((a) => !uniqueSignatures.has(a))} is missing`, }; // validate authentication - case: authentication matches signatures, unique, excessive signatures, if external controller if (uniqueRequiredSignatures.size < uniqueSignatures.size) return { valid: false, error: `authentication does not match signatures: signature from key ${Array.from(uniqueSignatures).find((s) => !uniqueRequiredSignatures.has(s))} is not required`, }; // return valid return { valid: true }; } /** * Validates authentication against signatures for key rotation scenarios. * Handles validation during DID document updates where keys may have changed. * * @param didDocument - Updated DID document to validate * @param signatures - Array of signatures to validate * @param querier - Querier for retrieving previous DID document and controllers * @param previousDidDocument - Optional previous version of the DID document * @param externalControllersDidDocuments - Optional pre-loaded external controller documents * @returns Promise resolving to validation result with controller documents and previous document */ static async validateAuthenticationAgainstSignaturesKeyRotation(didDocument, signatures, querier, previousDidDocument, externalControllersDidDocuments) { // validate signatures - case: no signatures if (!signatures || !signatures.length) return { valid: false, error: 'signatures are required' }; // validate authentication - case: no authentication when at least one verificationMethod if ((!didDocument.authentication || !didDocument.authentication.length) && didDocument.verificationMethod?.length) return { valid: false, error: 'authentication is required' }; // define unique authentication const authentication = (0, utils_1.normalizeAuthentication)(didDocument); const uniqueAuthentication = new Set(authentication); // validate authentication - case: authentication contains duplicates if (uniqueAuthentication.size < authentication.length) return { valid: false, error: `authentication contains duplicate key references: duplicate key reference ${Array.from(uniqueAuthentication).find((a) => (0, utils_1.normalizeAuthentication)(didDocument).filter((aa) => aa === a).length > 1)}`, }; // define unique signatures const uniqueSignatures = new Set(signatures.map((s) => s.verificationMethodId)); // validate authentication - case: authentication contains invalid key references if (!Array.from(uniqueAuthentication).every((a) => didDocument.verificationMethod?.some((vm) => vm.id === a))) return { valid: false, error: `authentication contains invalid key references: invalid key reference ${Array.from(uniqueAuthentication).find((a) => !didDocument.verificationMethod?.some((vm) => vm.id === a))}`, }; // lookup previous document if (!previousDidDocument) { // get previous document const previousDocument = await querier[exports.defaultDidExtensionKey].didDoc(didDocument.id); // throw, if not found if (!previousDocument || !previousDocument.didDoc) throw new Error('Previous did document not found'); previousDidDocument = await DIDModule.toSpecCompliantPayload(previousDocument.didDoc); } const controllers = (0, utils_1.normalizeController)(didDocument); const previousControllers = (0, utils_1.normalizeController)(previousDidDocument); // define whether external controller or not const externalController = controllers.concat(previousControllers).some((c) => c !== didDocument.id); // define whether key rotation or not (same ID, different material) const keyRotation = !!didDocument.verificationMethod?.some((vm) => previousDidDocument?.verificationMethod?.some((pvm) => pvm.id === vm.id && (pvm.publicKeyBase58 !== vm.publicKeyBase58 || pvm.publicKeyMultibase !== vm.publicKeyMultibase || pvm.publicKeyJwk?.x !== vm.publicKeyJwk?.x))); // define whether key replacement or not (different IDs in authentication) const currentAuthenticationIds = new Set((0, utils_1.normalizeAuthentication)(didDocument)); const previousAuthenticationIds = new Set((0, utils_1.normalizeAuthentication)(previousDidDocument)); const removedKeys = Array.from(previousAuthenticationIds).filter((id) => !currentAuthenticationIds.has(id)); const addedKeys = Array.from(currentAuthenticationIds).filter((id) => !previousAuthenticationIds.has(id)); const keyReplacement = removedKeys.length > 0 && addedKeys.length > 0; // define controller rotation const controllerRotation = !controllers.every((c) => previousControllers.includes(c)) || !previousControllers.every((c) => controllers.includes(c)); // define rotated controllers const rotatedControllers = controllerRotation ? previousControllers.filter((c) => !controllers.includes(c)) : []; // define unique union of authentication const previousAuthentication = (0, utils_1.normalizeAuthentication)(previousDidDocument); const uniqueUnionAuthentication = new Set([...uniqueAuthentication, ...previousAuthentication]); // validate authentication - case: authentication matches signatures, unique, if no external controller, no key rotation, no key replacement if (!Array.from(uniqueUnionAuthentication).every((a) => uniqueSignatures.has(a)) && !externalController && !keyRotation && !keyReplacement) return { valid: false, error: `authentication does not match signatures: signature from key ${Array.from(uniqueAuthentication).find((a) => !uniqueSignatures.has(a))} is missing`, }; // define rotated keys const rotatedKeys = keyRotation ? didDocument.verificationMethod?.filter((vm) => previousDidDocument?.verificationMethod?.some((pvm) => pvm.id === vm.id && (pvm.publicKeyBase58 !== vm.publicKeyBase58 || pvm.publicKeyMultibase !== vm.publicKeyMultibase || pvm.publicKeyJwk !== vm.publicKeyJwk))) : []; // define unique union of signatures required, including key replacement logic let uniqueUnionSignaturesRequired = new Set(); if (keyRotation && keyReplacement) { // Combined operation: Both key rotation AND key replacement happening // Need signatures from: // 1. All rotated keys (both old and new material for same ID) // 2. All added keys (new keys being added) // 3. All removed keys (old keys being removed) const rotatedKeySignatures = authentication .filter((a) => rotatedKeys?.find((rk) => a === rk.id)) .map((a) => `${a}(document0)`); const previousRotatedKeySignatures = previousAuthentication .filter((a) => rotatedKeys?.find((rk) => a === rk.id)) .map((a) => `${a}(document1)`); const newKeySignatures = addedKeys .filter((keyId) => !rotatedKeys?.find((rk) => keyId === rk.id)) .map((keyId) => `${keyId}(document0)`); const oldKeySignatures = removedKeys .filter((keyId) => previousAuthentication.includes(keyId)) .map((keyId) => `${keyId}(document1)`); uniqueUnionSignaturesRequired = new Set([ ...rotatedKeySignatures, ...previousRotatedKeySignatures, ...newKeySignatures, ...oldKeySignatures, ]); } else if (keyRotation) { // Key rotation only (same ID, different material) uniqueUnionSignaturesRequired = new Set([ ...authentication.filter((a) => rotatedKeys?.find((rk) => a === rk.id)).map((a) => `${a}(document0)`), ...previousAuthentication.map((a) => `${a}(document1)`), ]); } else if (keyReplacement) { // Key replacement only (different IDs in authentication) // For key replacement, we need signatures from: // 1. The new keys (from current document) // 2. The old keys that are being replaced (from previous document) const newKeySignatures = addedKeys.map((keyId) => `${keyId}(document0)`); const oldKeySignatures = removedKeys .filter((keyId) => previousAuthentication.includes(keyId)) // Only if they were in authentication .map((keyId) => `${keyId}(document1)`); uniqueUnionSignaturesRequired = new Set([...newKeySignatures, ...oldKeySignatures]); } else { // No rotation or replacement uniqueUnionSignaturesRequired = new Set([...authentication.map((a) => `${a}(document0)`)]); } // define frequency of unique union of signatures required const uniqueUnionSignaturesRequiredFrequency = new Map([...uniqueUnionSignaturesRequired].map((s) => [s.replace(new RegExp(/\(document\d+\)/), ''), 0])); // count frequency of unique union of signatures required uniqueUnionSignaturesRequired.forEach((s) => { // define key const key = s.replace(new RegExp(/\(document\d+\)/), ''); // increment frequency uniqueUnionSignaturesRequiredFrequency.set(key, uniqueUnionSignaturesRequiredFrequency.get(key) + 1); }); // define frequency of signatures provided const uniqueSignaturesFrequency = new Map(signatures.map((s) => [s.verificationMethodId, 0])); // count frequency of signatures provided signatures.forEach((s) => { // increment frequency uniqueSignaturesFrequency.set(s.verificationMethodId, uniqueSignaturesFrequency.get(s.verificationMethodId) + 1); }); // validate signatures - case: authentication matches signatures, unique, excessive signatures, no external controller if (Array.from(uniqueSignaturesFrequency).filter(([k, f]) => uniqueUnionSignaturesRequiredFrequency.get(k) === undefined || (uniqueUnionSignaturesRequiredFrequency.get(k) && uniqueUnionSignaturesRequiredFrequency.get(k) < f)).length && !externalController) return { valid: false, error: `authentication does not match signatures: signature from key ${Array.from(uniqueSignaturesFrequency).find(([k, f]) => uniqueUnionSignaturesRequiredFrequency.get(k) === undefined || uniqueUnionSignaturesRequiredFrequency.get(k) < f)?.[0]} is not required`, }; // validate signatures - case: authentication matches signatures, unique, missing signatures, no external controller if (Array.from(uniqueSignaturesFrequency).filter(([k, f]) => uniqueUnionSignaturesRequiredFrequency.get(k) && uniqueUnionSignaturesRequiredFrequency.get(k) > f).length && !externalController) return { valid: false, error: `authentication does not match signatures: signature from key ${Array.from(uniqueSignaturesFrequency).find(([k, f]) => uniqueUnionSignaturesRequiredFrequency.get(k) > f)?.[0]} is missing`, }; // validate signatures - case: all required keys must have signatures provided (check for completely missing keys) if (!externalController) { const missingKeys = Array.from(uniqueUnionSignaturesRequiredFrequency.keys()).filter((requiredKey) => !uniqueSignaturesFrequency.has(requiredKey)); if (missingKeys.length > 0) { return { valid: false, error: `authentication does not match signatures: signature from key ${missingKeys[0]} is missing`, }; } } // require querier if (!querier) throw new Error('querier is required for external controller validation'); // get external controllers // Only include rotated controllers if they are external (not the current DID itself) const externalRotatedControllers = rotatedControllers.filter((c) => c !== didDocument.id); const externalControllers = controllers?.filter((c) => c !== didDocument.id).concat(externalRotatedControllers); // get external controllers' documents const externalControllersDocuments = await Promise.all(externalControllers?.map(async (c) => { // compute index of external con