UNPKG

@nuwa-ai/identity-kit

Version:

SDK for NIP-1 Agent Single DID Multi-Key Model and NIP-3 CADOP (Custodian-Assisted DID Onboarding Protocol)

1 lines 286 kB
{"version":3,"sources":["../src/index.ts","../src/types/crypto.ts","../src/cache/InMemoryLRUDIDDocumentCache.ts","../src/vdr/VDRRegistry.ts","../src/multibase/base.ts","../src/utils/bytes.ts","../src/multibase/key.ts","../src/multibase/did.ts","../src/utils/did.ts","../src/utils/DebugLogger.ts","../src/vdr/abstractVDR.ts","../src/vdr/keyVDR.ts","../src/vdr/roochVDR.ts","../src/crypto/providers/ed25519.ts","../src/crypto/providers/secp256k1.ts","../src/crypto/providers/ecdsa_r1.ts","../src/crypto/factory.ts","../src/crypto/utils.ts","../src/signers/keyStoreUtils.ts","../src/signers/KeyStoreSigner.ts","../src/signers/didAccountSigner.ts","../src/vdr/roochVDRTypes.ts","../src/utils/sessionScopes.ts","../src/vdr/index.ts","../src/keys/KeyStore.ts","../src/keys/StoredKeyCodec.ts","../src/keys/KeyManager.ts","../src/IdentityEnv.ts","../src/IdentityKit.ts","../src/CadopIdentityKit.ts","../src/auth/v1/index.ts","../src/auth/v1/utils.ts","../src/auth/v1/nonceStore.ts","../src/auth/index.ts","../src/testHelpers/env.ts","../src/testHelpers/rooch.ts","../src/testHelpers/didFactory.ts"],"sourcesContent":["export * from './types';\nexport * from './IdentityKit';\nexport * from './CadopIdentityKit';\nexport * from './vdr';\nexport * from './multibase';\nexport * from './crypto';\nexport * from './auth';\nexport { initRoochVDR } from './vdr';\nexport * from './keys';\nexport * from './signers';\nexport * from './cache';\nexport { DebugLogger } from './utils/DebugLogger';\nexport * from './IdentityEnv';\nexport * from './utils/bytes';\nexport * from './utils/did';\nexport * from './utils/sessionScopes';\nexport * from './testHelpers';\n","import { SignatureScheme } from '@roochnetwork/rooch-sdk';\n\n// Cryptographic types and constants\n\n/**\n * Readable enum for supported verification key suites.\n *\n * NOTE: Replaces the old `KEY_TYPE` const object. Use this in new code to\n * improve clarity. Values keep the same long-form strings to avoid breaking\n * existing behavior when serialized into DID Documents.\n */\nexport enum KeyType {\n ED25519 = 'Ed25519VerificationKey2020',\n SECP256K1 = 'EcdsaSecp256k1VerificationKey2019',\n ECDSAR1 = 'EcdsaSecp256r1VerificationKey2019',\n}\n\n/**\n * @deprecated Will be removed in the next major version. Use `KeyType` enum instead.\n */\nexport const KEY_TYPE = KeyType;\n\n/**\n * Type guard to check if a string is a valid KeyType\n */\nexport function isKeyType(value: string): value is KeyType {\n return Object.values(KeyType).includes(value as KeyType);\n}\n\n/**\n * Convert a string to KeyType, with runtime validation\n * @throws Error if the string is not a valid KeyType\n */\nexport function toKeyType(value: string): KeyType {\n if (isKeyType(value)) {\n return value;\n }\n throw new Error(`Invalid key type: ${value}`);\n}\n\nexport function roochSignatureSchemeToKeyType(scheme: SignatureScheme): KeyType {\n if (scheme === 'Secp256k1') {\n return KeyType.SECP256K1;\n } else if (scheme === 'ED25519') {\n return KeyType.ED25519;\n } else if (scheme === 'EcdsaR1') {\n return KeyType.ECDSAR1;\n }\n throw new Error(`Unsupported Rooch signature scheme: ${scheme}`);\n}\n\nexport function keyTypeToRoochSignatureScheme(keyType: KeyType): SignatureScheme {\n if (keyType === KeyType.SECP256K1) {\n return 'Secp256k1';\n } else if (keyType === KeyType.ED25519) {\n return 'ED25519';\n } else if (keyType === KeyType.ECDSAR1) {\n return 'EcdsaR1';\n }\n throw new Error(`Unsupported key type: ${keyType}`);\n}\n\n/**\n * https://www.w3.org/TR/webauthn-2/#typedefdef-cosealgorithmidentifier\n * Convert a WebAuthn public key algorithm to KeyType, with runtime validation\n * @throws Error if the string is not a valid KeyType\n */\nexport function algorithmToKeyType(algorithm: number): KeyType | undefined {\n switch (algorithm) {\n case -8:\n return KeyType.ED25519;\n case -7:\n return KeyType.ECDSAR1;\n default:\n return undefined;\n }\n}\n\n/**\n * Convert a KeyType to WebAuthn algorithm identifier\n */\nexport function keyTypeToAlgorithm(keyType: KeyType): number | undefined {\n switch (keyType) {\n case KeyType.ED25519:\n return -8;\n case KeyType.ECDSAR1:\n return -7;\n default:\n return undefined;\n }\n}\n\n/**\n * Get list of supported WebAuthn algorithms\n */\nexport function getSupportedAlgorithms(): number[] {\n return [-8, -7];\n}\n\n/**\n * Type that represents either a KeyType or a string\n * Useful for functions that need to accept both strict KeyType and general string values\n */\nexport type KeyTypeInput = KeyType | string;\n\n/**\n * Represents the information needed to create a new operational key.\n */\nexport interface OperationalKeyInfo {\n idFragment?: string; // Optional fragment for the key id (e.g., 'key-2'). If not provided, one might be generated.\n type: string; // Cryptographic suite of the key, e.g., Ed25519VerificationKey2020\n publicKeyMaterial: Uint8Array | JsonWebKey; // The public key material\n controller?: string; // Defaults to the master DID if not provided\n}\n","import { DIDDocument } from '../types';\nimport { DIDDocumentCache } from './index';\n\n/**\n * A lightweight in-memory LRU cache implementation for DID Documents.\n * It is intentionally dependency-free so that the SDK does not pull in\n * additional packages by default. You can replace it with your own\n * implementation (Redis, IndexedDB, etc.) by implementing the\n * `DIDDocumentCache` interface and providing it to `VDRRegistry.setCache()`.\n */\nexport class InMemoryLRUDIDDocumentCache implements DIDDocumentCache {\n private readonly capacity: number;\n private readonly map: Map<string, DIDDocument | null>;\n\n constructor(maxEntries = 1000) {\n this.capacity = maxEntries;\n this.map = new Map<string, DIDDocument | null>();\n }\n\n get(did: string): DIDDocument | null | undefined {\n if (!this.map.has(did)) return undefined;\n const value = this.map.get(did) ?? null;\n // Refresh the recently used key to the end.\n this.map.delete(did);\n this.map.set(did, value);\n return value;\n }\n\n set(did: string, doc: DIDDocument | null): void {\n if (this.map.has(did)) {\n this.map.delete(did);\n } else if (this.map.size >= this.capacity) {\n // Evict the least-recently-used entry (Map iteration order is insertion order).\n const lruKey = this.map.keys().next().value;\n if (lruKey !== undefined) {\n this.map.delete(lruKey);\n }\n }\n this.map.set(did, doc);\n }\n\n has(did: string): boolean {\n return this.map.has(did);\n }\n\n delete(did: string): void {\n this.map.delete(did);\n }\n\n clear(): void {\n this.map.clear();\n }\n}\n","import { DIDDocument, DIDResolver } from '../types';\nimport { DIDDocumentCache } from '../cache';\nimport { VDRInterface, DIDCreationRequest, DIDCreationResult, CADOPCreationRequest } from './types';\n\nimport { InMemoryLRUDIDDocumentCache } from '../cache/InMemoryLRUDIDDocumentCache';\n\n/**\n * Global registry for VDR (Verifiable Data Registry) implementations.\n * This singleton manages all registered VDRs and maintains a DID Document cache.\n */\nexport class VDRRegistry implements DIDResolver {\n private static instance: VDRRegistry;\n private vdrs: Map<string, VDRInterface> = new Map();\n\n private cache: DIDDocumentCache;\n\n private constructor() {\n // Use the default in-memory cache unless overridden by the developer.\n this.cache = new InMemoryLRUDIDDocumentCache();\n }\n\n static getInstance(): VDRRegistry {\n if (!this.instance) {\n this.instance = new VDRRegistry();\n }\n return this.instance;\n }\n\n /** Register a VDR implementation for its DID method (e.g., 'key', 'rooch'). */\n registerVDR(vdr: VDRInterface) {\n this.vdrs.set(vdr.getMethod(), vdr);\n }\n\n /** Retrieve a previously registered VDR implementation by its method. */\n getVDR(method: string): VDRInterface | undefined {\n return this.vdrs.get(method);\n }\n\n /**\n * Override the default cache implementation.\n * This allows developers to provide their own cache (e.g., Redis, browser storage).\n */\n setCache(cache: DIDDocumentCache) {\n this.cache = cache;\n }\n\n /** Returns the currently configured cache instance. */\n getCache(): DIDDocumentCache {\n return this.cache;\n }\n\n async resolveDID(did: string, options?: { forceRefresh?: boolean }): Promise<DIDDocument | null> {\n const method = did.split(':')[1];\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n\n // Attempt to serve from cache if allowed.\n if (!options?.forceRefresh) {\n const cached = this.cache.get(did);\n if (cached !== undefined) {\n return cached;\n }\n }\n\n const resolved = await vdr.resolve(did);\n // Cache the resolution result (including null for negative caching).\n this.cache.set(did, resolved);\n return resolved;\n }\n\n async createDID(\n method: string,\n creationRequest: DIDCreationRequest,\n options?: Record<string, any>\n ): Promise<DIDCreationResult> {\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n const result = await vdr.create(creationRequest, options);\n if (result.success && result.didDocument) {\n this.cache.set(result.didDocument.id, result.didDocument);\n }\n return result;\n }\n\n async createDIDViaCADOP(\n method: string,\n creationRequest: CADOPCreationRequest,\n options?: Record<string, any>\n ): Promise<DIDCreationResult> {\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n const result = await vdr.createViaCADOP(creationRequest, options);\n if (result.success && result.didDocument) {\n this.cache.set(result.didDocument.id, result.didDocument);\n }\n return result;\n }\n\n async exists(did: string): Promise<boolean> {\n const method = did.split(':')[1];\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n\n // If we have a positive cache entry, short-circuit the call.\n if (this.cache.has(did)) {\n const doc = this.cache.get(did);\n return doc !== null;\n }\n\n const exists = await vdr.exists(did);\n // We don't cache the existence check result here to avoid stale data.\n return exists;\n }\n}\n","import { base58btc } from 'multiformats/bases/base58';\nimport { base64pad, base64, base64url, base64urlpad } from 'multiformats/bases/base64';\nimport { base16 } from 'multiformats/bases/base16';\nimport { bytesToString, stringToBytes } from '../utils/bytes';\n\n/**\n * Supported multibase names – use these with the generic `encode()` API.\n */\nexport type MultibaseName =\n | 'base58btc'\n | 'base64pad'\n | 'base64'\n | 'base64url'\n | 'base64urlpad'\n | 'base16';\n\ntype MultibaseCodecImpl = {\n encode: (bytes: Uint8Array) => string;\n decode: (encoded: string) => Uint8Array;\n};\n\nconst ENCODER_MAP: Record<MultibaseName, MultibaseCodecImpl> = {\n base58btc,\n base64pad,\n base64,\n base64url,\n base64urlpad,\n base16,\n};\n\n/**\n * Base multibase codec implementation\n * Provides basic encoding/decoding functionality without key type awareness\n */\nexport class MultibaseCodec {\n /**\n * Generic encode\n * Example: `MultibaseCodec.encode(bytes, 'base64url')`\n */\n static encode(data: Uint8Array | string, base: MultibaseName): string {\n const encoder = ENCODER_MAP[base];\n if (!encoder) {\n throw new Error(`Unsupported multibase: ${base}`);\n }\n const bytes = typeof data === 'string' ? stringToBytes(data) : data;\n return encoder.encode(bytes);\n }\n\n /**\n * Encode bytes to base58btc format\n * @param bytes The bytes to encode\n * @returns base58btc encoded string with 'z' prefix\n */\n static encodeBase58btc(bytes: Uint8Array): string {\n return this.encode(bytes, 'base58btc');\n }\n\n /**\n * Encode bytes to base64pad format\n * @param bytes The bytes to encode\n * @returns base64pad encoded string with 'M' prefix\n */\n static encodeBase64pad(data: Uint8Array | string): string {\n return this.encode(data, 'base64pad');\n }\n\n /**\n * Encode bytes to base16 (hex) format\n * @param bytes The bytes to encode\n * @returns base16 encoded string with 'f' prefix\n */\n static encodeBase16(bytes: Uint8Array): string {\n return this.encode(bytes, 'base16');\n }\n\n /**\n * Encode bytes to base64 format\n * @param bytes The bytes to encode\n * @returns base64 encoded string\n */\n static encodeBase64(data: Uint8Array | string): string {\n return this.encode(data, 'base64');\n }\n\n /**\n * Encode bytes to base64url format (RFC4648 URL-safe, no padding)\n * @param bytes The bytes to encode\n * @returns base64url encoded string with 'u' prefix\n */\n static encodeBase64url(data: Uint8Array | string): string {\n return this.encode(data, 'base64url');\n }\n\n /**\n * Encode bytes to base64urlpad format (URL-safe with padding)\n * @param bytes The bytes to encode\n * @returns base64urlpad encoded string with 'U' prefix\n */\n static encodeBase64urlpad(data: Uint8Array | string): string {\n return this.encode(data, 'base64urlpad');\n }\n\n /**\n * Decode base58btc string to bytes\n * @param encoded The base58btc encoded string\n * @returns decoded bytes\n */\n static decodeBase58btc(encoded: string): Uint8Array {\n return base58btc.decode(encoded);\n }\n\n /**\n * Decode base64pad string to bytes\n * @param encoded The base64pad encoded string\n * @returns decoded bytes\n */\n static decodeBase64pad(encoded: string): Uint8Array {\n return base64pad.decode(encoded);\n }\n\n /**\n * Decode base16 string to bytes\n * @param encoded The base16 encoded string\n * @returns decoded bytes\n */\n static decodeBase16(encoded: string): Uint8Array {\n return base16.decode(encoded);\n }\n\n /**\n * Decode base64 string to bytes\n * @param encoded The base64 encoded string\n * @returns decoded bytes\n */\n static decodeBase64(encoded: string): Uint8Array {\n return base64.decode(encoded);\n }\n\n /**\n * Decode base64url string to bytes\n * @param encoded The base64url encoded string\n * @returns decoded bytes\n */\n static decodeBase64url(encoded: string): Uint8Array {\n return base64url.decode(encoded);\n }\n\n /**\n * Decode base64url string to string\n * @param encoded The base64url encoded string\n * @returns decoded string\n */\n static decodeBase64urlToString(encoded: string): string {\n return bytesToString(this.decodeBase64url(encoded));\n }\n\n /**\n * Decode base64urlpad string to bytes\n * @param encoded The base64urlpad encoded string\n * @returns decoded bytes\n */\n static decodeBase64urlpad(encoded: string): Uint8Array {\n return base64urlpad.decode(encoded);\n }\n\n /**\n * Decode base64urlpad string to string\n * @param encoded The base64urlpad encoded string\n * @returns decoded string\n */\n static decodeBase64urlpadToString(encoded: string): string {\n return bytesToString(this.decodeBase64urlpad(encoded));\n }\n\n /**\n * Decode multibase encoded string to bytes\n * After multiformats v9, there is no longer a single \"universal base\" object;\n * the official recommendation is to manually dispatch prefixes between the few *.decoder objects you use.\n * @param encoded The multibase encoded string\n * @returns decoded bytes\n */\n static decode(encoded: string): Uint8Array {\n // Multibase prefix is always the first character\n const prefix = encoded[0];\n switch (prefix) {\n case 'z': // base58btc\n return base58btc.decode(encoded);\n case 'M': // base64pad (RFC4648 with padding)\n return base64pad.decode(encoded);\n case 'f': // base16 (hex, lowercase)\n return base16.decode(encoded);\n case 'm': // base64 (no padding)\n return base64.decode(encoded);\n case 'u': // base64url (no padding)\n return base64url.decode(encoded);\n case 'U': // base64urlpad (with padding)\n return base64urlpad.decode(encoded);\n default:\n throw new Error(`Unsupported multibase prefix: ${prefix}`);\n }\n }\n}\n","export function stringToBytes(str: string): Uint8Array {\n if (typeof TextEncoder !== 'undefined') {\n return new TextEncoder().encode(str);\n }\n // Node.js < 16 fallback using Buffer\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\n return Uint8Array.from(Buffer.from(str, 'utf-8'));\n }\n throw new Error('No TextEncoder or Buffer available in this environment.');\n}\n\nexport function bytesToString(bytes: Uint8Array): string {\n if (typeof TextDecoder !== 'undefined') {\n return new TextDecoder('utf-8').decode(bytes);\n }\n // Node.js < 16 fallback using Buffer\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\n return Buffer.from(bytes).toString('utf-8');\n }\n throw new Error('No TextDecoder or Buffer available in this environment.');\n}\n\nexport function base64urlToBytes(base64url: string): Uint8Array {\n // Add padding if needed\n const padding = base64url.length % 4;\n const paddedBase64url = base64url + '='.repeat(padding === 0 ? 0 : 4 - padding);\n\n // Convert base64url to base64\n const base64 = paddedBase64url.replace(/-/g, '+').replace(/_/g, '/');\n\n // Decode to bytes\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes;\n}\n\nexport const Bytes = {\n stringToBytes,\n bytesToString,\n base64urlToBytes,\n};\n\nexport default Bytes;\n","import { KeyType, KEY_TYPE } from '../types/crypto';\nimport { MultibaseCodec } from './base';\n\n/**\n * Key multibase codec implementation\n * Handles encoding/decoding of cryptographic keys with type information\n */\nexport class KeyMultibaseCodec {\n private static readonly ED25519_PREFIX = new Uint8Array([0xed, 0x01]);\n private static readonly SECP256K1_PREFIX = new Uint8Array([0xe7, 0x01]);\n private static readonly ECDSA_R1_PREFIX = new Uint8Array([0x12, 0x00]);\n private static readonly ED25519_KEY_LENGTH = 32;\n private static readonly SECP256K1_KEY_LENGTH = 33;\n private static readonly ECDSA_R1_KEY_LENGTH = 33;\n\n /**\n * Encode public key with multicodec prefix\n * @param bytes The public key bytes\n * @param keyType The key type\n * @returns multibase encoded string\n */\n static encodeWithType(bytes: Uint8Array, keyType: KeyType): string {\n // Validate key length\n const expectedLength = this.getExpectedKeyLength(keyType);\n if (bytes.length !== expectedLength) {\n throw new Error(\n `Invalid key length for ${keyType}. Expected ${expectedLength} bytes, got ${bytes.length}`\n );\n }\n\n const prefix = this.getMulticodecPrefix(keyType);\n const prefixedKey = this.concatenateBytes(prefix, bytes);\n return MultibaseCodec.encodeBase58btc(prefixedKey);\n }\n\n /**\n * Decode multibase encoded key\n * @param encoded The multibase encoded string\n * @returns The key type and public key bytes\n */\n static decodeWithType(encoded: string): { keyType: KeyType; bytes: Uint8Array } {\n try {\n const decoded = MultibaseCodec.decodeBase58btc(encoded);\n if (decoded.length < 2) {\n throw new Error('Invalid key format: too short');\n }\n\n const keyType = this.extractKeyType(decoded);\n const bytes = this.extractBytes(decoded);\n\n // Validate key length\n const expectedLength = this.getExpectedKeyLength(keyType);\n if (bytes.length !== expectedLength) {\n throw new Error(\n `Invalid key length for ${keyType}. Expected ${expectedLength} bytes, got ${bytes.length}`\n );\n }\n\n return { keyType, bytes };\n } catch (error) {\n if (error instanceof Error && error.message === 'Non-base58btc character') {\n throw new Error('Invalid multibase format');\n }\n throw error;\n }\n }\n\n private static getMulticodecPrefix(keyType: KeyType): Uint8Array {\n switch (keyType) {\n case KEY_TYPE.ED25519:\n return this.ED25519_PREFIX;\n case KEY_TYPE.SECP256K1:\n return this.SECP256K1_PREFIX;\n case KEY_TYPE.ECDSAR1:\n return this.ECDSA_R1_PREFIX;\n default:\n throw new Error(`Unsupported key type: ${keyType}`);\n }\n }\n\n private static concatenateBytes(a: Uint8Array, b: Uint8Array): Uint8Array {\n const result = new Uint8Array(a.length + b.length);\n result.set(a);\n result.set(b, a.length);\n return result;\n }\n\n private static extractKeyType(prefixedBytes: Uint8Array): KeyType {\n if (prefixedBytes[0] === 0xed && prefixedBytes[1] === 0x01) {\n return KEY_TYPE.ED25519;\n } else if (prefixedBytes[0] === 0xe7 && prefixedBytes[1] === 0x01) {\n return KEY_TYPE.SECP256K1;\n } else if (prefixedBytes[0] === 0x12 && prefixedBytes[1] === 0x00) {\n return KEY_TYPE.ECDSAR1;\n }\n throw new Error('Unknown key type prefix');\n }\n\n private static extractBytes(prefixedBytes: Uint8Array): Uint8Array {\n return prefixedBytes.slice(2);\n }\n\n private static getExpectedKeyLength(keyType: KeyType): number {\n switch (keyType) {\n case KEY_TYPE.ED25519:\n return this.ED25519_KEY_LENGTH;\n case KEY_TYPE.SECP256K1:\n return this.SECP256K1_KEY_LENGTH;\n case KEY_TYPE.ECDSAR1:\n return this.ECDSA_R1_KEY_LENGTH;\n default:\n throw new Error(`Unsupported key type: ${keyType}`);\n }\n }\n}\n","import { KeyType } from '../types/crypto';\nimport { MultibaseCodec } from './base';\nimport { KeyMultibaseCodec } from './key';\n\n/**\n * DID key codec implementation\n * Handles encoding/decoding of did:key identifiers\n */\nexport class DidKeyCodec {\n /**\n * Generate did:key from public key\n * @param publicKey The public key bytes\n * @param keyType The key type\n * @returns did:key identifier\n */\n static generateDidKey(publicKey: Uint8Array, keyType: KeyType): string {\n // KeyMultibaseCodec.encodeWithType will validate key length\n const multibase = KeyMultibaseCodec.encodeWithType(publicKey, keyType);\n return `did:key:${multibase}`;\n }\n\n /**\n * Parse did:key to get key type and public key\n * @param didKey The did:key identifier\n * @returns The key type and public key bytes\n */\n static parseDidKey(didKey: string): { keyType: KeyType; publicKey: Uint8Array } {\n if (!didKey.startsWith('did:key:')) {\n throw new Error('Invalid did:key format');\n }\n const multibase = didKey.substring(8);\n const { keyType, bytes } = KeyMultibaseCodec.decodeWithType(multibase);\n return { keyType, publicKey: bytes };\n }\n}\n","/**\n * DID utility helpers (method, identifier & fragment parsing)\n * Used across SDK layers (VDR, Signer, KeyManager, etc.)\n */\n\n/**\n * Parsed DID parts\n */\nexport interface ParsedDID {\n /** DID method, e.g. 'key', 'rooch' */\n method: string;\n /** Unique identifier part (method-specific id, without fragment) */\n identifier: string;\n /** Optional fragment (ver. method / service id) */\n fragment?: string;\n}\n\n/**\n * Parse a DID or DID-URL into its components.\n *\n * @param did Full DID string: `did:<method>:<identifier>[#fragment]`\n * @throws Error if input does not start with `did:` or lacks method / identifier parts.\n */\nexport function parseDid(did: string): ParsedDID {\n if (!did.startsWith('did:')) {\n throw new Error(`Invalid DID: ${did}`);\n }\n\n // Strip leading `did:` and split once by ':'\n const afterPrefix = did.slice(4);\n const methodEnd = afterPrefix.indexOf(':');\n if (methodEnd === -1) {\n throw new Error(`Invalid DID – missing method/identifier separator: ${did}`);\n }\n const method = afterPrefix.slice(0, methodEnd);\n const idPlusFrag = afterPrefix.slice(methodEnd + 1);\n if (!method || !idPlusFrag) {\n throw new Error(`Invalid DID: ${did}`);\n }\n\n const hashIdx = idPlusFrag.indexOf('#');\n return hashIdx === -1\n ? { method, identifier: idPlusFrag }\n : {\n method,\n identifier: idPlusFrag.slice(0, hashIdx),\n fragment: idPlusFrag.slice(hashIdx + 1),\n };\n}\n\n/** Get DID method string */\nexport function extractMethod(did: string): string {\n return parseDid(did).method;\n}\n\n/** Get method-specific identifier (without fragment) */\nexport function extractIdentifier(did: string): string {\n return parseDid(did).identifier;\n}\n\n/**\n * Extract the fragment from a DID URL or any string containing `#`.\n * Throws an error if no fragment present.\n */\nexport function extractFragment(idOrDid: string): string {\n const idx = idOrDid.indexOf('#');\n if (idx === -1) {\n throw new Error(`No fragment found in ${idOrDid}`);\n }\n return idOrDid.slice(idx + 1);\n}\n\n/** Alias kept for back-compat with existing imports */\nexport const extractFragmentFromId = extractFragment;\n\n/** Build a canonical DID string from method & identifier */\nexport function buildDid(method: string, identifier: string): string {\n return `did:${method}:${identifier}`;\n}\n\n/**\n * Compare two DIDs ignoring their fragments.\n */\nexport function sameDid(a: string, b: string): boolean {\n const pa = parseDid(a);\n const pb = parseDid(b);\n return pa.method === pb.method && pa.identifier === pb.identifier;\n}\n\n/**\n * Return the canonical DID (strip any `#fragment`).\n */\nexport function getDidWithoutFragment(did: string): string {\n const { method, identifier } = parseDid(did);\n return buildDid(method, identifier);\n}\n","/* eslint-disable no-console */\n/*\n * Lightweight environment-agnostic debug logger.\n *\n * ‑ Works in both Node.js and browser.\n * ‑ Supports level filtering (debug | info | warn | error | silent).\n * ‑ Namespaced: each module/class can request its own logger via DebugLogger.get(\"MyModule\").\n * ‑ Global level can be controlled at runtime via DebugLogger.setGlobalLevel() *or*\n * environment variable NUWA_LOG_LEVEL (node) / window.__NUWA_LOG_LEVEL__ (browser).\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: 50,\n};\n\nfunction detectInitialGlobalLevel(): LogLevel {\n // Node.js: use process.env if available\n if (typeof process !== 'undefined' && (process as any).env) {\n const envLevel = (process as any).env.NUWA_LOG_LEVEL as string | undefined;\n if (envLevel && envLevel in LEVEL_ORDER) return envLevel as LogLevel;\n }\n // Browser: allow runtime override via global variable\n if (typeof window !== 'undefined' && (window as any).__NUWA_LOG_LEVEL__) {\n const envLevel = (window as any).__NUWA_LOG_LEVEL__ as string;\n if (envLevel && envLevel in LEVEL_ORDER) return envLevel as LogLevel;\n }\n return 'info';\n}\n\nexport class DebugLogger {\n // ---------------------------------------------------------------------------\n // Static section\n // ---------------------------------------------------------------------------\n private static globalLevel: LogLevel = detectInitialGlobalLevel();\n private static loggers = new Map<string, DebugLogger>();\n private static defaultNamespace = 'global';\n\n /** Acquire (or create) a logger for the given namespace. */\n static get(namespace: string): DebugLogger {\n if (!DebugLogger.loggers.has(namespace)) {\n DebugLogger.loggers.set(namespace, new DebugLogger(namespace));\n }\n return DebugLogger.loggers.get(namespace)!;\n }\n\n /** Override global log level at runtime. */\n static setGlobalLevel(level: LogLevel): void {\n DebugLogger.globalLevel = level;\n // Propagate to existing instances unless they explicitly override.\n for (const logger of DebugLogger.loggers.values()) {\n if (!logger.levelOverridden) {\n logger.level = level;\n }\n }\n }\n\n /** Read current global level. */\n static getGlobalLevel(): LogLevel {\n return DebugLogger.globalLevel;\n }\n\n /** Set default namespace used by static convenience methods. */\n static setDefaultNamespace(namespace: string): void {\n DebugLogger.defaultNamespace = namespace;\n }\n\n // ---------------------------------------------------------------------------\n // Static convenience logging methods\n // ---------------------------------------------------------------------------\n /**\n * Log using the default namespace. Useful when callers don't need per-module loggers.\n * Example: DebugLogger.debug('hello')\n */\n static debug(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).debug(...args);\n }\n\n static info(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).info(...args);\n }\n\n static warn(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).warn(...args);\n }\n\n static error(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).error(...args);\n }\n\n // ---------------------------------------------------------------------------\n // Instance section\n // ---------------------------------------------------------------------------\n private level: LogLevel;\n private levelOverridden = false;\n\n private constructor(private namespace: string) {\n this.level = DebugLogger.globalLevel;\n }\n\n /** Override level for this logger only. */\n setLevel(level: LogLevel): void {\n this.level = level;\n this.levelOverridden = true;\n }\n\n // -------------------------------------------------------\n // Logging helpers\n // -------------------------------------------------------\n debug(...args: unknown[]): void {\n this._log('debug', args);\n }\n\n info(...args: unknown[]): void {\n this._log('info', args);\n }\n\n warn(...args: unknown[]): void {\n this._log('warn', args);\n }\n\n error(...args: unknown[]): void {\n this._log('error', args);\n }\n\n // prettier-ignore\n private _log(level: LogLevel, args: unknown[]): void {\n if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) {\n return; // filtered out\n }\n\n const prefix = `[${this.namespace}]`;\n\n // Colorize in browser / modern terminal if desired; keep simple for now.\n switch (level) {\n case 'debug':\n console.debug(prefix, ...args);\n break;\n case 'info':\n console.info(prefix, ...args);\n break;\n case 'warn':\n console.warn(prefix, ...args);\n break;\n case 'error':\n console.error(prefix, ...args);\n break;\n }\n }\n}\n","import {\n DIDDocument,\n VerificationMethod,\n VerificationRelationship,\n ServiceEndpoint,\n} from '../types/did';\nimport { DIDCreationRequest, DIDCreationResult, CADOPCreationRequest, VDRInterface } from './types';\nimport { parseDid } from '../utils/did';\nimport { DebugLogger } from '../utils/DebugLogger';\n\n// Unified logger for AbstractVDR\nconst logger = DebugLogger.get('AbstractVDR');\n\n/**\n * Abstract base class for implementing Verifiable Data Registry functionality\n * Provides common utility methods and enforces the VDRInterface contract\n */\nexport abstract class AbstractVDR implements VDRInterface {\n // The DID method this VDR handles (e.g., 'key', 'web')\n protected readonly method: string;\n\n /**\n * Creates a new AbstractVDR instance\n *\n * @param method The DID method this VDR handles\n */\n constructor(method: string) {\n this.method = method;\n }\n\n /**\n * Gets the DID method this VDR handles\n *\n * @returns The DID method string\n */\n getMethod(): string {\n return this.method;\n }\n\n /**\n * Validates that a given DID matches the method this VDR handles\n *\n * @param did The DID to validate\n * @throws Error if the DID doesn't match this VDR's method\n */\n protected validateDIDMethod(did: string): void {\n const { method } = parseDid(did);\n if (method !== this.method) {\n throw new Error(`DID ${did} is not a valid did:${this.method} identifier`);\n }\n }\n\n /**\n * Validates a DID document's basic structure\n *\n * @param document The DID document to validate\n * @returns true if valid, throws an error otherwise\n */\n protected validateDocument(document: DIDDocument): boolean {\n if (!document.id) {\n throw new Error('DID document must have an id');\n }\n\n this.validateDIDMethod(document.id);\n\n if (!document['@context']) {\n throw new Error('DID document must have a @context property');\n }\n\n if (!document.verificationMethod || document.verificationMethod.length === 0) {\n throw new Error('DID document must have at least one verification method');\n }\n\n return true;\n }\n\n /**\n * Check if a key has a specific verification relationship in a DID document\n *\n * @param didDocument The DID document to check\n * @param keyId The ID of the verification method\n * @param relationship The verification relationship to check\n * @returns True if the key has the relationship\n */\n protected hasVerificationRelationship(\n didDocument: DIDDocument,\n keyId: string,\n relationship: VerificationRelationship\n ): boolean {\n const relationshipArray = didDocument[relationship];\n if (!relationshipArray) return false;\n\n return relationshipArray.some(item => {\n if (typeof item === 'string') {\n return item === keyId;\n } else if (typeof item === 'object' && item.id) {\n return item.id === keyId;\n }\n return false;\n });\n }\n\n /**\n * Validates if a key has permission to perform an operation\n *\n * @param didDocument The DID document\n * @param keyId The ID of the key\n * @param requiredRelationship The required verification relationship\n * @returns True if the key has permission\n */\n protected validateKeyPermission(\n didDocument: DIDDocument,\n keyId: string,\n requiredRelationship: VerificationRelationship\n ): boolean {\n const keyExists = didDocument.verificationMethod?.some(vm => vm.id === keyId);\n if (!keyExists) {\n logger.error(`Key ${keyId} not found in DID document`);\n return false;\n }\n\n const isPrimaryKey = didDocument.verificationMethod?.[0]?.id === keyId;\n if (isPrimaryKey) {\n return true;\n }\n\n const hasPermission = didDocument[requiredRelationship]?.includes(keyId);\n\n if (!hasPermission) {\n logger.error(`Key ${keyId} does not have ${requiredRelationship} permission`);\n return false;\n }\n\n return true;\n }\n\n /**\n * Default create implementation - throws not implemented error for base class\n * Subclasses must override this method to provide actual implementation\n */\n async create(request: DIDCreationRequest, options?: any): Promise<DIDCreationResult> {\n throw new Error(`create method not implemented for ${this.method} VDR`);\n }\n\n /**\n * Default CADOP implementation - throws not implemented error\n */\n async createViaCADOP(request: CADOPCreationRequest, options?: any): Promise<DIDCreationResult> {\n throw new Error(`createViaCADOP not implemented for ${this.method} VDR`);\n }\n\n /**\n * Build DID Document from creation request\n */\n protected buildDIDDocumentFromRequest(request: DIDCreationRequest): DIDDocument {\n const did = request.preferredDID!;\n\n // Extract the first controller for the verification method (which only accepts string)\n const controllerForVM = Array.isArray(request.controller)\n ? request.controller[0]\n : request.controller || did;\n\n const verificationMethod: VerificationMethod = {\n id: `${did}#account-key`,\n type: request.keyType || 'EcdsaSecp256k1VerificationKey2019',\n controller: controllerForVM,\n publicKeyMultibase: request.publicKeyMultibase,\n };\n\n const didDocument: DIDDocument = {\n '@context': ['https://www.w3.org/ns/did/v1'],\n id: did,\n controller: request.controller\n ? Array.isArray(request.controller)\n ? request.controller\n : [request.controller]\n : [did],\n verificationMethod: [verificationMethod, ...(request.additionalVerificationMethods || [])],\n service: request.initialServices || [],\n };\n\n // Set initial relationships\n const relationships = request.initialRelationships || [\n 'authentication',\n 'assertionMethod',\n 'capabilityInvocation',\n 'capabilityDelegation',\n ];\n\n const vmId = verificationMethod.id;\n relationships.forEach(rel => {\n if (!didDocument[rel]) {\n didDocument[rel] = [];\n }\n (didDocument[rel] as string[]).push(vmId);\n });\n\n return didDocument;\n }\n\n /**\n * Resolves a DID to its corresponding DID document\n * Implementations must provide this functionality\n */\n abstract resolve(did: string): Promise<DIDDocument | null>;\n\n /**\n * Checks if a DID exists in the registry\n * Default implementation tries to resolve and checks if result is not null\n */\n async exists(did: string): Promise<boolean> {\n try {\n const doc = await this.resolve(did);\n return doc !== null;\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Add a verification method to a DID document\n * Default implementation that should be overridden by specific VDR implementations\n */\n async addVerificationMethod(\n did: string,\n verificationMethod: VerificationMethod,\n relationships?: VerificationRelationship[],\n options?: any\n ): Promise<boolean> {\n throw new Error(`addVerificationMethod not implemented for ${this.method} VDR`);\n }\n\n /**\n * Remove a verification method from a DID document\n * Default implementation that should be overridden by specific VDR implementations\n */\n async removeVerificationMethod(did: string, id: string, options?: any): Promise<boolean> {\n throw new Error(`removeVerificationMethod not implemented for ${this.method} VDR`);\n }\n\n /**\n * Add a service to a DID document\n * Default implementation that should be overridden by specific VDR implementations\n */\n async addService(did: string, service: ServiceEndpoint, options?: any): Promise<boolean> {\n throw new Error(`addService not implemented for ${this.method} VDR`);\n }\n\n /**\n * Remove a service from a DID document\n * Default implementation that should be overridden by specific VDR implementations\n */\n async removeService(did: string, id: string, options?: any): Promise<boolean> {\n throw new Error(`removeService not implemented for ${this.method} VDR`);\n }\n\n /**\n * Update verification relationships for a verification method\n * Default implementation that should be overridden by specific VDR implementations\n */\n async updateRelationships(\n did: string,\n id: string,\n add: VerificationRelationship[],\n remove: VerificationRelationship[],\n options?: any\n ): Promise<boolean> {\n throw new Error(`updateRelationships not implemented for ${this.method} VDR`);\n }\n\n /**\n * Update the controller of a DID document\n * Default implementation that should be overridden by specific VDR implementations\n */\n async updateController(\n did: string,\n controller: string | string[],\n options?: any\n ): Promise<boolean> {\n throw new Error(`updateController not implemented for ${this.method} VDR`);\n }\n\n /**\n * Validates options for update operations and ensures proper permissions\n *\n * @param did The DID being operated on\n * @param document The resolved DID document\n * @param keyId The key ID used for signing\n * @param requiredRelationship The required verification relationship for this operation\n * @throws Error if validation fails\n */\n protected async validateUpdateOperation(\n did: string,\n document: DIDDocument | null,\n keyId: string,\n requiredRelationship: VerificationRelationship\n ): Promise<DIDDocument> {\n // Validate DID method\n this.validateDIDMethod(did);\n\n // Check if document exists\n if (!document) {\n throw new Error(`DID document ${did} not found`);\n }\n\n // Check permission\n if (!this.validateKeyPermission(document, keyId, requiredRelationship)) {\n throw new Error(\n `Key ${keyId} does not have ${requiredRelationship} permission required for this operation`\n );\n }\n\n return document;\n }\n\n /**\n * Validates that inputs to addVerificationMethod are correct\n *\n * @param did The DID being operated on\n * @param verificationMethod The verification method to validate\n * @param document The current DID document\n * @throws Error if validation fails\n */\n protected validateVerificationMethod(\n did: string,\n verificationMethod: VerificationMethod,\n document: DIDDocument\n ): void {\n // Ensure ID starts with the DID\n if (!verificationMethod.id.startsWith(did)) {\n throw new Error(`Verification method ID ${verificationMethod.id} must start with DID ${did}`);\n }\n\n // Check if method already exists\n if (document.verificationMethod?.some(vm => vm.id === verificationMethod.id)) {\n throw new Error(`Verification method ${verificationMethod.id} already exists`);\n }\n\n // Validate required fields\n if (!verificationMethod.type) {\n throw new Error('Verification method must have a type');\n }\n\n if (!verificationMethod.controller) {\n throw new Error('Verification method must have a controller');\n }\n\n // Check that at least one key material format is present\n if (!verificationMethod.publicKeyMultibase && !verificationMethod.publicKeyJwk) {\n throw new Error('Verification method must have at least one form of public key material');\n }\n }\n\n /**\n * Validates that inputs to addService are correct\n *\n * @param did The DID being operated on\n * @param service The service to validate\n * @param document The current DID document\n * @throws Error if validation fails\n */\n protected validateService(did: string, service: ServiceEndpoint, document: DIDDocument): void {\n // Ensure ID starts with the DID\n if (!service.id.startsWith(did)) {\n throw new Error(`Service ID ${service.id} must start with DID ${did}`);\n }\n\n // Check if service already exists\n if (document.service?.some(s => s.id === service.id)) {\n throw new Error(`Service ${service.id} already exists`);\n }\n\n // Validate required fields\n if (!service.type) {\n throw new Error('Service must have a type');\n }\n\n if (!service.serviceEndpoint) {\n throw new Error('Service must have a serviceEndpoint');\n }\n }\n\n /**\n * Makes a deep copy of a DID document for modification\n *\n * @param document The DID document to copy\n * @returns A deep copy of the document\n */\n protected copyDocument(document: DIDDocument): DIDDocument {\n return JSON.parse(JSON.stringify(document));\n }\n}\n","import {\n DIDDocument,\n VerificationMethod,\n VerificationRelationship,\n ServiceEndpoint,\n} from '../types/did';\nimport { DIDCreationRequest, DIDCreationResult, CADOPCreationRequest } from './types';\nimport { AbstractVDR } from './abstractVDR';\nimport { MultibaseCodec, DidKeyCodec } from '../multibase';\nimport { DebugLogger } from '../utils/DebugLogger';\n\n/**\n * KeyVDR handles did:key DIDs\n *\n * did:key DIDs are self-resolving as they contain the public key material\n * embedded in the identifier. This implementation follows the did:key method\n * specification.\n *\n * Example did:key: did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK\n *\n * Reference: https://w3c-ccg.github.io/did-method-key/\n */\n\n// Unified logger for KeyVDR\nconst logger = DebugLogger.get('KeyVDR');\n\nexport class KeyVDR extends AbstractVDR {\n // In-memory cache of documents, shared across all instances\n private static documentCache: Map<string, DIDDocument> = new Map();\n\n constructor() {\n super('key');\n }\n\n /**\n * Resets the document cache - primarily for testing purposes\n * to ensure tests don't interfere with each other.\n */\n public reset(): void {\n KeyVDR.documentCache.clear();\n }\n\n /**\n * Override resolve to handle test mode\n */\n async resolve(did: string): Promise<DIDDocument | null> {\n try {\n // Check the cache first\n if (KeyVDR.documentCache.has(did)) {\n return KeyVDR.documentCache.get(did)!;\n }\n\n return null;\n } catch (error) {\n logger.error(`Error resolving ${did}:`, error);\n return null;\n }\n }\n\n /**\n * Add a verification method to a did:key document\n * For did:key, this is mostly a simulation as the document is derived from the key\n * This operation will update the local cache but not the actual structure of the did:key\n *\n * @param did The DID to update\n * @param verificationMethod The verification method to add\n * @param relationships Optional relationships to add the verification method to\n * @param options Additional options like keyId for signing\n * @returns Promise resolving to true if successful in updating the cache\n */\n async addVerificationMethod(\n did: string,\n verificationMethod: VerificationMethod,\n relationships: VerificationRelationship[] = [],\n options?: any\n ): Promise<boolean> {\n try {\n const originalDocument = await this.resolve(did);\n if (!originalDocument) {\n throw new Error(`DID ${did} not found`);\n }\n\n // Use parent class validation methods\n await this.validateUpdateOperation(\n did,\n originalDocument,\n options?.keyId,\n 'capabilityDelegation'\n );\n this.validateVerificationMethod(did, verificationMethod, originalDocument);\n\n // Check for duplicate verification method ID\n if (originalDocument.verificationMethod?.some(vm => vm.id === verificationMethod.id)) {\n throw new Error(`Verification method ${verificationMethod.id} already exists`);\n }\n\n if (!originalDocument.verificationMethod) {\n originalDocument.verificationMethod = [];\n }\n\n // Add the verification method\n originalDocument.verificationMethod.push(verificationMethod);\n\n // Add relationships without duplicates\n relationships.forEach(relationship => {\n if (!originalDocument[relationship]) {\n originalDocument[relationship] = [];\n }\n if (!originalDocument[relationship]!.includes(verificationMethod.id)) {\n originalDocument[relationship]!.push(verificationMethod.id);\n }\n });\n\n // Update the cache\n KeyVDR.documentCache.set(did, originalDocument);\n return true;\n } catch (error) {\n logger.error(`Error adding verification method to ${did}:`, error);\n throw error;\n }\n }\n\n /**\n * Remove a verification method from a did:key document\n * For did:key, this is mostly a simulation as the document is derived from the key\n * This operation will update the local cache but not the actual structure of the did:key\n *\n * @param did The DID to update\n * @param keyId The ID of the verification method to remove\n * @param options Additional options\n * @returns Promise resolving to true if successful in updating the cache\n */\n async removeVerificationMethod(did: string, keyId: string, options?: any): Promise<boolean> {\n try {\n const originalDocument = await this.resolve(did);\n if (!originalDocument) {\n throw new Error(`DID ${did} not found`);\n }\n\n // Use parent class validation method\n await this.validateUpdateOperation(\n did,\n originalDocument,\n options?.keyId,\n 'capabilityDelegation'\n );\n\n const verificationMethods = originalDocument.verificationMethod || [];\n const vmIndex = verificationMethods.findIndex(vm => vm.id === keyId);\n if (vmIndex === -1) {\n // Verification method not found, silently succeed\n return true;\n }\n\n const isPrimaryKey = vmIndex === 0;\n if (isPrimaryKey) {\n throw new Error(`Cannot remove the primary key ${keyId} from did:key document`);\n }\n\n originalDocument.verificationMethod = verificationMethods.filter(vm => vm.id !== keyId);\n\n const relationships: VerificationRelationship[] = [\n 'authentication',\n 'assertionMethod',\n 'capabilityInvocation',\n 'capabilityDelegation',\n ];\n relationships.forEach(relationship => {\n if (originalDocument[relationship]) {\n originalDocument[relationship] = originalDocument[relationship]!.filter(\n id => id !== keyId\n );\n }\n });\n\n KeyVDR.documentCache.set(did, originalDocument);\n return true;\n } catch (error) {\n logger.error(`Error removing verification method from ${did}:`, error);\n throw error;\n }\n }\n\n /**\n * Add a service to a did:key document\n * For did:key, this is mostly a simulation as the document is derived from the key\n * This operation will update the local cache but not the actual structure of the did:key\n *\n