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 318 kB
{"version":3,"sources":["../../package.json","../../src/cli/index.ts","../../src/cli/args.ts","../../src/cli-lib/config.ts","../../src/cli-lib/types.ts","../../src/cli-lib/deeplink.ts","../../src/multibase/base.ts","../../src/errors/IdentityKitError.ts","../../src/utils/bytes.ts","../../src/types/crypto.ts","../../src/multibase/key.ts","../../src/multibase/did.ts","../../src/deeplink/shared.ts","../../src/cli-lib/authHeader.ts","../../src/auth/v1/index.ts","../../src/auth/v1/utils.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/crypto/EcdsaR1PublicKey.ts","../../src/auth/v1/nonceStore.ts","../../src/auth/index.ts","../../src/cli-lib/http.ts","../../src/cli-lib/keys.ts","../../src/utils/did.ts","../../src/utils/DebugLogger.ts","../../src/vdr/abstractVDR.ts","../../src/vdr/keyVDR.ts","../../src/vdr/roochVDR.ts","../../src/signers/types.ts","../../src/signers/didAccountSigner.ts","../../src/vdr/roochVDRTypes.ts","../../src/utils/sessionScopes.ts","../../src/cache/InMemoryLRUDIDDocumentCache.ts","../../src/vdr/VDRRegistry.ts","../../src/vdr/index.ts","../../src/cli-lib/verify.ts"],"sourcesContent":["{\n \"name\": \"@nuwa-ai/identity-kit\",\n \"version\": \"0.8.1\",\n \"description\": \"SDK for NIP-1 Agent Single DID Multi-Key Model and NIP-3 CADOP (Custodian-Assisted DID Onboarding Protocol)\",\n \"type\": \"module\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"bin\": {\n \"nuwa-id\": \"./dist/cli/index.cjs\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"./web\": {\n \"types\": \"./dist/web/index.d.ts\",\n \"import\": \"./dist/web/index.js\",\n \"require\": \"./dist/web/index.cjs\"\n },\n \"./testHelpers\": {\n \"types\": \"./dist/testHelpers/index.d.ts\",\n \"import\": \"./dist/testHelpers/index.js\",\n \"require\": \"./dist/testHelpers/index.cjs\"\n }\n },\n \"scripts\": {\n \"dev\": \"tsup --watch\",\n \"build\": \"tsup && chmod +x dist/cli/index.cjs dist/cli/index.js\",\n \"test\": \"jest --config=jest.config.json\",\n \"test:coverage\": \"jest --config=jest.config.json --coverage\",\n \"test:watch\": \"jest --config=jest.config.json --watch\",\n \"clean\": \"rimraf dist\",\n \"lint\": \"eslint . --ext .ts\",\n \"lint:fix\": \"eslint . --ext .ts --fix\",\n \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n \"format:check\": \"prettier --check \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n \"prepublishOnly\": \"pnpm run clean && pnpm run build\"\n },\n \"files\": [\n \"dist/**/*\"\n ],\n \"keywords\": [\n \"did\",\n \"identity\",\n \"web3\",\n \"nuwa\",\n \"nip-1\",\n \"nip-3\",\n \"cadop\",\n \"custodian\",\n \"onboarding\",\n \"web\",\n \"browser\"\n ],\n \"peerDependencies\": {\n \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n }\n },\n \"author\": \"Nuwa Community\",\n \"license\": \"Apache-2.0\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/nuwa-protocol/nuwa.git\",\n \"directory\": \"nuwa-kit/typescript/packages/identity-kit\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/nuwa-protocol/nuwa/issues\"\n },\n \"homepage\": \"https://github.com/nuwa-protocol/nuwa/tree/main/nuwa-kit/typescript/packages/identity-kit#readme\",\n \"devDependencies\": {\n \"@jest/globals\": \"^29.7.0\",\n \"@types/jest\": \"^29.5.14\",\n \"@types/node\": \"^20.10.0\",\n \"@types/react\": \"^18.0.0\",\n \"@typescript-eslint/eslint-plugin\": \"^7.3.1\",\n \"@typescript-eslint/parser\": \"^7.3.1\",\n \"eslint\": \"^8.57.0\",\n \"eslint-config-prettier\": \"^9.1.0\",\n \"eslint-plugin-prettier\": \"^5.1.3\",\n \"jest\": \"^29.7.0\",\n \"jest-environment-jsdom\": \"^30.0.5\",\n \"jsdom\": \"^26.1.0\",\n \"prettier\": \"^3.2.5\",\n \"rimraf\": \"^5.0.5\",\n \"ts-jest\": \"29.1.1\",\n \"tsup\": \"^8.2.3\",\n \"typescript\": \"^5.3.2\"\n },\n \"dependencies\": {\n \"@noble/curves\": \"^1.9.1\",\n \"@noble/hashes\": \"^1.8.0\",\n \"@roochnetwork/rooch-sdk\": \"^0.4.0\",\n \"multiformats\": \"^9.9.0\"\n },\n \"tsup\": {\n \"entry\": [\n \"src/index.ts\",\n \"src/web/index.ts\",\n \"src/testHelpers/index.ts\",\n \"src/cli/index.ts\"\n ],\n \"format\": [\n \"esm\",\n \"cjs\"\n ],\n \"dts\": true,\n \"splitting\": false,\n \"sourcemap\": true,\n \"clean\": true,\n \"external\": [\n \"react\"\n ]\n }\n}\n","#!/usr/bin/env node\n\nimport { stat } from 'fs/promises';\nimport path from 'path';\nimport { parseArgs, type ParsedArgs } from './args';\nimport {\n ensureCliDir,\n getActiveProfile,\n getCliPaths,\n keyExists,\n loadConfig,\n loadKeyMaterial,\n saveConfig,\n saveKeyMaterial,\n saveKeyMaterialWithRelativePath,\n updateActiveProfile,\n} from '../cli-lib/config';\nimport { buildAddKeyDeepLink } from '../cli-lib/deeplink';\nimport { createDidAuthHeader } from '../cli-lib/authHeader';\nimport { sendDidAuthRequest } from '../cli-lib/http';\nimport { createAgentKeyMaterial } from '../cli-lib/keys';\nimport { verifyDidKeyBinding } from '../cli-lib/verify';\nimport { makeDefaultConfig } from '../cli-lib/types';\n\nfunction resolveCliVersion(): string {\n const envVersion = process.env.npm_package_version;\n if (envVersion) return envVersion;\n\n try {\n // Resolve from installed package root when running CJS bin (`dist/cli/index.cjs`).\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const pkg = require('../../package.json') as { version?: string };\n if (typeof pkg.version === 'string' && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fallback below.\n }\n\n return 'unknown';\n}\n\nconst cliVersion = resolveCliVersion();\n\nasync function main(): Promise<void> {\n try {\n const parsed = parseArgs(process.argv.slice(2));\n const command = parsed.command;\n\n // Handle version flags: -v, --version\n if (getBool(parsed.options.version) || command === 'version') {\n printVersion();\n return;\n }\n\n // Handle help flags: -h, --help, help\n if (!command || command === 'help' || getBool(parsed.options.help)) {\n printHelp();\n return;\n }\n\n switch (command) {\n case 'init':\n await runInit(parsed.options);\n return;\n case 'set-did':\n await runSetDid(parsed.options);\n return;\n case 'status':\n await runStatus(parsed.options);\n return;\n case 'profile:list':\n await runProfileList(parsed.options);\n return;\n case 'profile:use':\n await runProfileUse(parsed.options);\n return;\n case 'profile:create':\n await runProfileCreate(parsed.options);\n return;\n case 'link':\n await runLink(parsed.options);\n return;\n case 'verify':\n await runVerify(parsed.options);\n return;\n case 'auth-header':\n await runAuthHeader(parsed.options);\n return;\n case 'curl':\n await runCurl(parsed.options);\n return;\n default:\n throw new Error(`Unknown command: ${command}. Run 'nuwa-id --help' for usage information.`);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`error: ${message}`);\n process.exit(1);\n }\n}\n\nasync function runInit(options: ParsedArgs['options']): Promise<void> {\n await ensureCliDir();\n const config = await loadConfig();\n const active = getActiveProfile(config);\n const exists = await keyExists();\n const force = getBool(options.force);\n if (exists && !force) {\n throw new Error('key already exists, use --force to overwrite');\n }\n\n const keyFragment = getString(options['key-fragment']) || makeDefaultKeyFragment();\n const network = parseNetwork(getString(options.network) || active.profile.network);\n const roochRpcUrl = getString(options['rpc-url']) || active.profile.roochRpcUrl;\n const cadopDomain = getString(options['cadop-domain']) || active.profile.cadopDomain;\n\n const key = await createAgentKeyMaterial(keyFragment);\n await saveKeyMaterial(key);\n await saveConfig(\n updateActiveProfile(config, profile => ({\n ...profile,\n network,\n roochRpcUrl,\n cadopDomain,\n keyFragment,\n }))\n );\n\n console.log('initialized did-auth agent config');\n console.log(`publicKeyMultibase=${key.publicKeyMultibase}`);\n console.log(`keyFragment=${keyFragment}`);\n console.log(`activeProfile=${active.name}`);\n console.log(`configDir=${getCliPaths().dir}`);\n}\n\nasync function runLink(options: ParsedArgs['options']): Promise<void> {\n const config = await loadConfig();\n const active = getActiveProfile(config);\n const key = await loadKeyMaterial();\n const cadopDomain = getString(options['cadop-domain']) || active.profile.cadopDomain;\n const redirectUri = getString(options['redirect-uri']);\n assertKeyFragmentMatch(active.profile.keyFragment, key.keyFragment);\n\n const link = buildAddKeyDeepLink({\n key,\n keyFragment: active.profile.keyFragment,\n cadopDomain,\n redirectUri,\n });\n\n if (getBool(options.json)) {\n console.log(\n JSON.stringify(\n {\n deepLinkUrl: link.url,\n payload: link.payload,\n },\n null,\n 2\n )\n );\n return;\n }\n\n console.log(link.url);\n}\n\nasync function runSetDid(options: ParsedArgs['options']): Promise<void> {\n const did = requiredString(options.did, '--did is required');\n const config = await loadConfig();\n await saveConfig(updateActiveProfile(config, profile => ({ ...profile, did })));\n console.log(`saved did=${did} to ${getCliPaths().configFile}`);\n}\n\nasync function runStatus(options: ParsedArgs['options']): Promise<void> {\n const config = await loadConfig();\n const active = getActiveProfile(config);\n const cliPaths = getCliPaths();\n const keyFilePath = path.join(cliPaths.dir, active.profile.keyFile);\n\n // Check if key file exists\n let keyFileExists = false;\n try {\n await stat(keyFilePath);\n keyFileExists = true;\n } catch {\n keyFileExists = false;\n }\n\n const status = {\n activeProfile: active.name,\n did: active.profile.did || '',\n network: active.profile.network,\n cadopDomain: active.profile.cadopDomain,\n keyFragment: active.profile.keyFragment,\n keyFilePath,\n keyFileExists,\n };\n\n if (getBool(options.json)) {\n console.log(JSON.stringify(status, null, 2));\n return;\n }\n\n console.log(`Active Profile: ${status.activeProfile}`);\n console.log(`DID: ${status.did || '(not set)'}`);\n console.log(`Network: ${status.network}`);\n console.log(`Cadop Domain: ${status.cadopDomain}`);\n console.log(`Key Fragment: ${status.keyFragment}`);\n console.log(`Key File Path: ${status.keyFilePath}`);\n console.log(`Key File Exists: ${status.keyFileExists ? 'Yes' : 'No'}`);\n}\n\nasync function runProfileList(options: ParsedArgs['options']): Promise<void> {\n const config = await loadConfig();\n const entries = Object.entries(config.profiles).map(([name, profile]) => ({\n name,\n active: name === config.activeProfile,\n did: profile.did || '',\n network: profile.network,\n cadopDomain: profile.cadopDomain,\n keyFragment: profile.keyFragment,\n keyFile: profile.keyFile,\n }));\n\n if (getBool(options.json)) {\n console.log(JSON.stringify(entries, null, 2));\n return;\n }\n\n for (const entry of entries) {\n const prefix = entry.active ? '*' : ' ';\n console.log(\n `${prefix} ${entry.name} network=${entry.network} keyFragment=${entry.keyFragment} did=${entry.did || '-'}`\n );\n }\n}\n\nasync function runProfileUse(options: ParsedArgs['options']): Promise<void> {\n const name = requiredString(options.name, '--name is required');\n const config = await loadConfig();\n if (!config.profiles[name]) {\n throw new Error(`profile not found: ${name}`);\n }\n\n await saveConfig({\n ...config,\n activeProfile: name,\n });\n console.log(`activeProfile=${name}`);\n}\n\nasync function runProfileCreate(options: ParsedArgs['options']): Promise<void> {\n const name = requiredString(options.name, '--name is required');\n if (!/^[a-zA-Z0-9_-]+$/.test(name)) {\n throw new Error('invalid --name, allowed chars: a-z A-Z 0-9 _ -');\n }\n\n const config = await loadConfig();\n if (config.profiles[name]) {\n throw new Error(`profile already exists: ${name}`);\n }\n const profileKeyFile = `keys/${name}.json`;\n const force = getBool(options.force);\n try {\n await stat(path.join(getCliPaths().dir, profileKeyFile));\n if (!force) {\n throw new Error(`key already exists for profile \"${name}\", use --force to overwrite`);\n }\n } catch (error: unknown) {\n const err = error as NodeJS.ErrnoException;\n if (err?.code && err.code !== 'ENOENT') {\n throw error;\n }\n }\n\n const active = getActiveProfile(config);\n const keyFragment = getString(options['key-fragment']) || makeDefaultKeyFragment();\n const network = parseNetwork(getString(options.network) || active.profile.network);\n const roochRpcUrl = getString(options['rpc-url']) || active.profile.roochRpcUrl;\n const cadopDomain = getString(options['cadop-domain']) || active.profile.cadopDomain;\n const did = getString(options.did);\n\n const next = {\n ...config,\n profiles: {\n ...config.profiles,\n [name]: {\n did,\n network,\n roochRpcUrl,\n cadopDomain,\n keyFragment,\n keyFile: profileKeyFile,\n },\n },\n };\n\n const key = await createAgentKeyMaterial(keyFragment);\n await saveKeyMaterialWithRelativePath(key, profileKeyFile);\n await saveConfig(next);\n console.log(`created profile=${name}`);\n console.log(`publicKeyMultibase=${key.publicKeyMultibase}`);\n console.log(`keyFragment=${keyFragment}`);\n}\n\nasync function runVerify(options: ParsedArgs['options']): Promise<void> {\n const config = await loadConfig();\n const active = getActiveProfile(config);\n const did = active.profile.did;\n if (!did) throw new Error('did is not set; run `nuwa-id set-did --did DID`');\n const key = await loadKeyMaterial();\n assertKeyFragmentMatch(active.profile.keyFragment, key.keyFragment);\n const network = parseNetwork(getString(options.network) || active.profile.network || 'main');\n const rpcUrl = getString(options['rpc-url']) || active.profile.roochRpcUrl;\n const keyId = `${did}#${active.profile.keyFragment}`;\n\n const result = await verifyDidKeyBinding({\n did,\n keyId,\n network,\n rpcUrl,\n });\n\n if (result.verificationMethodFound && result.authenticationBound) {\n console.log(`verified: ${result.did} contains ${result.keyId} in authentication`);\n return;\n }\n\n const reasons: string[] = [];\n if (!result.verificationMethodFound) reasons.push('verificationMethod missing');\n if (!result.authenticationBound) reasons.push('authentication binding missing');\n throw new Error(`verify failed: ${reasons.join(', ')}`);\n}\n\nasync function runAuthHeader(options: ParsedArgs['options']): Promise<void> {\n const config = await loadConfig();\n const active = getActiveProfile(config);\n const did = active.profile.did;\n if (!did) throw new Error('did is not set; run `nuwa-id set-did --did DID`');\n const method = requiredString(options.method, '--method is required');\n const url = requiredString(options.url, '--url is required');\n const body = getString(options.body) || '';\n const audience = getString(options.audience);\n\n const key = await loadKeyMaterial();\n assertKeyFragmentMatch(active.profile.keyFragment, key.keyFragment);\n\n const header = await createDidAuthHeader({\n did,\n key,\n method,\n url,\n body,\n audience,\n });\n console.log(header);\n}\n\nasync function runCurl(options: ParsedArgs['options']): Promise<void> {\n const config = await loadConfig();\n const active = getActiveProfile(config);\n const did = active.profile.did;\n if (!did) throw new Error('did is not set; run `nuwa-id set-did --did DID`');\n const method = requiredString(options.method, '--method is required');\n const url = requiredString(options.url, '--url is required');\n const body = getString(options.body) || '';\n const audience = getString(options.audience);\n\n const key = await loadKeyMaterial();\n assertKeyFragmentMatch(active.profile.keyFragment, key.keyFragment);\n const headers = parseHeaders(getStringArray(options.header));\n\n const response = await sendDidAuthRequest({\n did,\n key,\n method,\n url,\n body,\n audience,\n headers,\n });\n\n console.log(`HTTP ${response.status} ${response.statusText}`);\n console.log(response.body);\n}\n\nfunction getBool(value: unknown): boolean {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') return value === 'true';\n return false;\n}\n\nfunction getString(value: unknown): string | undefined {\n if (typeof value === 'string') return value;\n if (Array.isArray(value) && value.length > 0) {\n return typeof value[value.length - 1] === 'string'\n ? (value[value.length - 1] as string)\n : undefined;\n }\n return undefined;\n}\n\nfunction getStringArray(value: unknown): string[] {\n if (Array.isArray(value)) return value.filter(v => typeof v === 'string') as string[];\n if (typeof value === 'string') return [value];\n return [];\n}\n\nfunction requiredString(value: unknown, message: string): string {\n const result = getString(value);\n if (!result) throw new Error(message);\n return result;\n}\n\nfunction parseHeaders(entries: string[]): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const entry of entries) {\n const sep = entry.indexOf(':');\n if (sep <= 0) {\n throw new Error(`invalid --header value: ${entry}`);\n }\n const name = entry.slice(0, sep).trim();\n const value = entry.slice(sep + 1).trim();\n headers[name] = value;\n }\n return headers;\n}\n\nfunction parseNetwork(input: string): 'main' | 'test' {\n if (input !== 'main' && input !== 'test') {\n throw new Error(`invalid network \"${input}\", allowed values: main | test`);\n }\n return input;\n}\n\nfunction makeDefaultKeyFragment(now = new Date()): string {\n const year = now.getUTCFullYear();\n const month = String(now.getUTCMonth() + 1).padStart(2, '0');\n const day = String(now.getUTCDate()).padStart(2, '0');\n const hour = String(now.getUTCHours()).padStart(2, '0');\n const minute = String(now.getUTCMinutes()).padStart(2, '0');\n const second = String(now.getUTCSeconds()).padStart(2, '0');\n return `agent-auth-${year}${month}${day}${hour}${minute}${second}`;\n}\n\nfunction assertKeyFragmentMatch(expected: string, actual: string): void {\n if (expected === actual) return;\n throw new Error(\n `keyFragment mismatch: config has \"${expected}\" but key file has \"${actual}\". Update active profile keyFragment to \"${actual}\" in ~/.config/nuwa-did/config.json, then retry.`\n );\n}\n\nfunction printHelp(): void {\n const defaults = makeDefaultConfig();\n const active = defaults.profiles[defaults.activeProfile];\n const lines = [\n 'nuwa-id - DIDAuth helper CLI for remote agents',\n '',\n 'Usage:',\n ' nuwa-id [command] [options]',\n '',\n 'Commands:',\n ' init Initialize DIDAuth agent config',\n ' set-did Set DID for active profile',\n ' status Show current profile status',\n ' profile list List all profiles',\n ' profile use Switch active profile',\n ' profile create Create a new profile',\n ' link Generate deep link for adding key',\n ' verify Verify DID key binding',\n ' auth-header Generate DID auth header',\n ' curl Send signed HTTP request',\n ' version Show CLI version',\n ' help Show this help message',\n '',\n 'Options:',\n ' -h, --help Show help message',\n ' -v, --version Show version',\n ' --json Output in JSON format (for list, status)',\n '',\n 'Examples:',\n ' nuwa-id init',\n ' nuwa-id status',\n ' nuwa-id status --json',\n ' nuwa-id profile list --json',\n '',\n `Defaults: network=${active.network}, cadop-domain=${active.cadopDomain}, profile=${defaults.activeProfile}`,\n ];\n console.log(lines.join('\\n'));\n}\n\nfunction printVersion(): void {\n console.log(cliVersion);\n}\n\nvoid main();\n\n// Export for testing\nexport { parseArgs };\nexport type { ParsedArgs };\n","export type ParsedArgs = {\n command?: string;\n options: Record<string, string | boolean | string[]>;\n};\n\nexport function parseArgs(argv: string[]): ParsedArgs {\n if (argv.length === 0) return { options: {} };\n const [first, ...tail] = argv;\n\n // Handle short flags (-h, -v)\n if (first === '-h' || first === '--help') {\n return { command: 'help', options: {} };\n }\n if (first === '-v' || first === '--version') {\n return { command: 'version', options: {} };\n }\n if (first.startsWith('-')) {\n throw new Error(`Unknown flag: ${first}`);\n }\n\n let command = first;\n let rest = tail;\n if (first === 'profile') {\n const action = tail[0];\n if (!action || action.startsWith('--')) {\n throw new Error('profile command requires subcommand: list | use | create');\n }\n command = `profile:${action}`;\n rest = tail.slice(1);\n }\n const options: ParsedArgs['options'] = {};\n\n for (let i = 0; i < rest.length; i++) {\n const token = rest[i];\n if (!token.startsWith('-')) continue;\n\n // Handle short flags\n if (token.startsWith('-') && !token.startsWith('--')) {\n const flag = token.slice(1);\n if (flag === 'h') {\n addOption(options, 'help', true);\n continue;\n }\n if (flag === 'v') {\n addOption(options, 'version', true);\n continue;\n }\n // Unknown short flag\n throw new Error(`Unknown flag: -${flag}`);\n }\n\n const [rawKey, inlineValue] = token.slice(2).split('=', 2);\n if (inlineValue !== undefined) {\n addOption(options, rawKey, inlineValue);\n continue;\n }\n\n const next = rest[i + 1];\n if (!next || next.startsWith('--')) {\n addOption(options, rawKey, true);\n continue;\n }\n\n addOption(options, rawKey, next);\n i += 1;\n }\n\n return { command, options };\n}\n\nfunction addOption(\n options: ParsedArgs['options'],\n key: string,\n value: string | boolean\n): void {\n const existing = options[key];\n if (existing === undefined) {\n options[key] = value;\n return;\n }\n if (Array.isArray(existing)) {\n existing.push(String(value));\n options[key] = existing;\n return;\n }\n options[key] = [String(existing), String(value)];\n}\n","import { mkdir, readFile, stat, writeFile } from 'fs/promises';\nimport os from 'os';\nimport path from 'path';\nimport {\n ActiveProfile,\n AgentKeyMaterial,\n DidCliConfig,\n DidCliProfile,\n makeDefaultConfig,\n} from './types';\n\nfunction resolveCliDir(): string {\n return path.join(os.homedir(), '.config', 'nuwa-did');\n}\n\nexport interface CliPaths {\n dir: string;\n configFile: string;\n keysDir: string;\n}\n\nexport function getCliPaths(): CliPaths {\n const dir = resolveCliDir();\n return {\n dir,\n configFile: path.join(dir, 'config.json'),\n keysDir: path.join(dir, 'keys'),\n };\n}\n\nexport async function ensureCliDir(): Promise<CliPaths> {\n const paths = getCliPaths();\n await mkdir(paths.dir, { recursive: true, mode: 0o700 });\n await mkdir(paths.keysDir, { recursive: true, mode: 0o700 });\n return paths;\n}\n\nexport async function loadConfig(): Promise<DidCliConfig> {\n const paths = await ensureCliDir();\n try {\n const raw = await readFile(paths.configFile, 'utf8');\n try {\n return normalizeConfig(JSON.parse(raw) as Partial<DidCliConfig>);\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `invalid nuwa-id config at ${paths.configFile}: ${message}. Delete the file and run \\`nuwa-id init\\` to recreate it.`\n );\n }\n } catch (error: unknown) {\n const err = error as NodeJS.ErrnoException;\n if (!err?.code || err.code !== 'ENOENT') {\n throw error;\n }\n return makeDefaultConfig();\n }\n}\n\nexport async function saveConfig(config: DidCliConfig): Promise<void> {\n const paths = await ensureCliDir();\n await writeFile(paths.configFile, JSON.stringify(config, null, 2), {\n encoding: 'utf8',\n mode: 0o600,\n });\n}\n\nexport async function keyExists(): Promise<boolean> {\n const config = await loadConfig();\n const keyFile = resolveProfileKeyFile(config, config.activeProfile);\n try {\n await stat(keyFile);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function loadKeyMaterial(profileName?: string): Promise<AgentKeyMaterial> {\n const config = await loadConfig();\n const keyFile = resolveProfileKeyFile(config, profileName || config.activeProfile);\n let raw: string;\n try {\n raw = await readFile(keyFile, 'utf8');\n } catch (error: unknown) {\n const err = error as NodeJS.ErrnoException;\n if (err?.code === 'ENOENT') {\n throw new Error('agent key not initialized; run `nuwa-id init`');\n }\n throw error;\n }\n\n try {\n return JSON.parse(raw) as AgentKeyMaterial;\n } catch {\n throw new Error(`invalid key file at ${keyFile}`);\n }\n}\n\nexport async function saveKeyMaterial(key: AgentKeyMaterial, profileName?: string): Promise<void> {\n const config = await loadConfig();\n const keyFile = resolveProfileKeyFile(config, profileName || config.activeProfile);\n await saveKeyMaterialToPath(key, keyFile);\n}\n\nexport async function saveKeyMaterialWithRelativePath(\n key: AgentKeyMaterial,\n keyFileRelativePath: string\n): Promise<void> {\n const keyFile = resolveSafeKeyPath(keyFileRelativePath);\n await saveKeyMaterialToPath(key, keyFile);\n}\n\nasync function saveKeyMaterialToPath(key: AgentKeyMaterial, keyFile: string): Promise<void> {\n const paths = await ensureCliDir();\n await mkdir(path.dirname(keyFile), { recursive: true, mode: 0o700 });\n await writeFile(keyFile, JSON.stringify(key, null, 2), {\n encoding: 'utf8',\n mode: 0o600,\n });\n}\n\nexport function getActiveProfile(config: DidCliConfig): ActiveProfile {\n const profile = config.profiles[config.activeProfile];\n if (!profile) {\n throw new Error(`active profile not found: ${config.activeProfile}`);\n }\n return {\n name: config.activeProfile,\n profile,\n };\n}\n\nexport function updateActiveProfile(\n config: DidCliConfig,\n updater: (profile: DidCliProfile) => DidCliProfile\n): DidCliConfig {\n const active = getActiveProfile(config);\n return {\n ...config,\n profiles: {\n ...config.profiles,\n [active.name]: updater(active.profile),\n },\n };\n}\n\nfunction normalizeConfig(input: Partial<DidCliConfig>): DidCliConfig {\n if (input.version !== 2) {\n throw new Error('invalid config version');\n }\n\n const activeProfile = input.activeProfile;\n if (!activeProfile || typeof activeProfile !== 'string') {\n throw new Error('invalid config: activeProfile missing');\n }\n\n const profiles = input.profiles;\n if (!profiles || typeof profiles !== 'object') {\n throw new Error('invalid config: profiles missing');\n }\n\n const active = (profiles as Record<string, Partial<DidCliProfile>>)[activeProfile];\n if (!active) {\n throw new Error(`invalid config: active profile \"${activeProfile}\" missing`);\n }\n\n return {\n version: 2,\n activeProfile,\n profiles: profiles as Record<string, DidCliProfile>,\n };\n}\n\nfunction resolveProfileKeyFile(config: DidCliConfig, profileName: string): string {\n const profile = config.profiles[profileName];\n if (!profile) {\n throw new Error(`profile not found: ${profileName}`);\n }\n\n return resolveSafeKeyPath(profile.keyFile, profileName);\n}\n\nfunction resolveSafeKeyPath(keyFile: string, profileName?: string): string {\n if (path.isAbsolute(keyFile) || keyFile.includes('..')) {\n if (profileName) {\n throw new Error(`invalid keyFile in config for profile \"${profileName}\"`);\n }\n throw new Error('invalid keyFile path');\n }\n\n return path.join(getCliPaths().dir, keyFile);\n}\n","import { KeyType } from '../types/crypto';\n\nexport type CliNetwork = 'main' | 'test';\n\nexport interface DidCliProfile {\n did?: string;\n network: CliNetwork;\n roochRpcUrl?: string;\n cadopDomain: string;\n keyFragment: string;\n keyFile: string;\n}\n\nexport interface DidCliConfig {\n version: 2;\n activeProfile: string;\n profiles: Record<string, DidCliProfile>;\n}\n\nexport interface ActiveProfile {\n name: string;\n profile: DidCliProfile;\n}\n\nexport interface NewProfileInput {\n name: string;\n network: CliNetwork;\n roochRpcUrl?: string;\n cadopDomain: string;\n keyFragment: string;\n did?: string;\n}\n\nexport interface AgentKeyMaterial {\n keyType: KeyType;\n publicKeyMultibase: string;\n privateKeyMultibase: string;\n keyFragment: string;\n createdAt: string;\n}\n\nexport const DEFAULT_PROFILE_NAME = 'default';\n\nexport function makeDefaultConfig(): DidCliConfig {\n return makeSingleProfileConfig({\n name: DEFAULT_PROFILE_NAME,\n network: 'main',\n cadopDomain: 'https://id.nuwa.dev',\n keyFragment: 'agent-auth-1',\n });\n}\n\nexport function makeSingleProfileConfig(input: NewProfileInput): DidCliConfig {\n return {\n version: 2,\n activeProfile: input.name,\n profiles: {\n [input.name]: {\n did: input.did,\n network: input.network,\n roochRpcUrl: input.roochRpcUrl,\n cadopDomain: input.cadopDomain,\n keyFragment: input.keyFragment,\n keyFile: `keys/${input.name}.json`,\n },\n },\n };\n}\n","import { randomUUID } from 'crypto';\nimport { AddKeyRequestPayloadV1 } from '../types/deeplink';\nimport { AgentKeyMaterial } from './types';\nimport {\n buildAddKeyDeepLink as buildAddKeyDeepLinkUrl,\n buildAddKeyPayload,\n normalizeCadopDomain,\n} from '../deeplink/shared';\n\nexport interface BuildDeepLinkInput {\n key: AgentKeyMaterial;\n cadopDomain: string;\n keyFragment: string;\n redirectUri?: string;\n}\n\nexport interface BuildDeepLinkOutput {\n url: string;\n payload: AddKeyRequestPayloadV1;\n}\n\nexport function buildAddKeyDeepLink(input: BuildDeepLinkInput): BuildDeepLinkOutput {\n const domain = normalizeCadopDomain(input.cadopDomain);\n const payload: AddKeyRequestPayloadV1 = buildAddKeyPayload({\n keyType: input.key.keyType,\n publicKeyMultibase: input.key.publicKeyMultibase,\n keyFragment: input.keyFragment,\n relationships: ['authentication'],\n redirectUri: input.redirectUri || `${domain}/close`,\n state: randomUUID(),\n });\n return {\n url: buildAddKeyDeepLinkUrl({ cadopDomain: domain, payload }),\n payload,\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';\nimport { IdentityKitErrorCode, createMultibaseError } from '../errors';\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 createMultibaseError(\n IdentityKitErrorCode.MULTIBASE_ENCODE_FAILED,\n `Unsupported multibase: ${base}`,\n { base, supportedBases: Object.keys(ENCODER_MAP) }\n );\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 createMultibaseError(\n IdentityKitErrorCode.MULTIBASE_DECODE_FAILED,\n `Unsupported multibase prefix: ${prefix}`,\n { prefix, supportedPrefixes: ['z', 'M', 'm', 'u', 'U', 'f'] }\n );\n }\n }\n}\n","/**\n * Unified error handling for IdentityKit\n */\n\n/**\n * Error codes for IdentityKit operations\n */\nexport enum IdentityKitErrorCode {\n // Configuration and initialization\n INVALID_CONFIGURATION = 'INVALID_CONFIGURATION',\n ENVIRONMENT_NOT_SUPPORTED = 'ENVIRONMENT_NOT_SUPPORTED',\n INITIALIZATION_FAILED = 'INITIALIZATION_FAILED',\n\n // DID operations\n DID_NOT_FOUND = 'DID_NOT_FOUND',\n DID_INVALID_FORMAT = 'DID_INVALID_FORMAT',\n DID_METHOD_NOT_SUPPORTED = 'DID_METHOD_NOT_SUPPORTED',\n DID_CREATION_FAILED = 'DID_CREATION_FAILED',\n DID_RESOLUTION_FAILED = 'DID_RESOLUTION_FAILED',\n DID_NOT_SET = 'DID_NOT_SET',\n DID_SERVICE_NOT_FOUND = 'DID_SERVICE_NOT_FOUND',\n DID_VERIFICATION_METHOD_NOT_FOUND = 'DID_VERIFICATION_METHOD_NOT_FOUND',\n\n // VDR (Verifiable Data Registry) operations\n VDR_NOT_AVAILABLE = 'VDR_NOT_AVAILABLE',\n VDR_OPERATION_FAILED = 'VDR_OPERATION_FAILED',\n VDR_NETWORK_ERROR = 'VDR_NETWORK_ERROR',\n VDR_INVALID_RESPONSE = 'VDR_INVALID_RESPONSE',\n\n // Key management\n KEY_NOT_FOUND = 'KEY_NOT_FOUND',\n KEY_INVALID_FORMAT = 'KEY_INVALID_FORMAT',\n KEY_GENERATION_FAILED = 'KEY_GENERATION_FAILED',\n KEY_IMPORT_FAILED = 'KEY_IMPORT_FAILED',\n KEY_STORAGE_ERROR = 'KEY_STORAGE_ERROR',\n KEY_PERMISSION_DENIED = 'KEY_PERMISSION_DENIED',\n KEY_VALIDATION_FAILED = 'KEY_VALIDATION_FAILED',\n KEY_PRIVATE_KEY_NOT_AVAILABLE = 'KEY_PRIVATE_KEY_NOT_AVAILABLE',\n KEY_DID_MISMATCH = 'KEY_DID_MISMATCH',\n KEY_ID_MISMATCH = 'KEY_ID_MISMATCH',\n KEY_TYPE_NOT_SUPPORTED = 'KEY_TYPE_NOT_SUPPORTED',\n KEY_ALREADY_EXISTS = 'KEY_ALREADY_EXISTS',\n\n // Signing operations\n SIGNING_FAILED = 'SIGNING_FAILED',\n SIGNATURE_VERIFICATION_FAILED = 'SIGNATURE_VERIFICATION_FAILED',\n SIGNER_NOT_AVAILABLE = 'SIGNER_NOT_AVAILABLE',\n SIGNER_INVALID_DID = 'SIGNER_INVALID_DID',\n SIGNER_NO_KEYS = 'SIGNER_NO_KEYS',\n\n // Authentication (from existing AuthErrorCode)\n AUTH_INVALID_HEADER = 'AUTH_INVALID_HEADER',\n AUTH_INVALID_BASE64 = 'AUTH_INVALID_BASE64',\n AUTH_INVALID_JSON = 'AUTH_INVALID_JSON',\n AUTH_MISSING_SIGNATURE = 'AUTH_MISSING_SIGNATURE',\n AUTH_TIMESTAMP_OUT_OF_WINDOW = 'AUTH_TIMESTAMP_OUT_OF_WINDOW',\n AUTH_NONCE_REPLAYED = 'AUTH_NONCE_REPLAYED',\n AUTH_DID_DOCUMENT_NOT_FOUND = 'AUTH_DID_DOCUMENT_NOT_FOUND',\n AUTH_VERIFICATION_METHOD_NOT_FOUND = 'AUTH_VERIFICATION_METHOD_NOT_FOUND',\n AUTH_INVALID_PUBLIC_KEY = 'AUTH_INVALID_PUBLIC_KEY',\n AUTH_DID_MISMATCH = 'AUTH_DID_MISMATCH',\n\n // Web-specific operations\n WEB_BROWSER_NOT_SUPPORTED = 'WEB_BROWSER_NOT_SUPPORTED',\n WEB_STORAGE_NOT_AVAILABLE = 'WEB_STORAGE_NOT_AVAILABLE',\n WEB_DEEPLINK_FAILED = 'WEB_DEEPLINK_FAILED',\n WEB_CADOP_CONNECTION_FAILED = 'WEB_CADOP_CONNECTION_FAILED',\n WEB_OAUTH_CALLBACK_FAILED = 'WEB_OAUTH_CALLBACK_FAILED',\n WEB_NOT_CONNECTED = 'WEB_NOT_CONNECTED',\n WEB_CALLBACK_FAILED = 'WEB_CALLBACK_FAILED',\n\n // React-specific operations\n REACT_NOT_AVAILABLE = 'REACT_NOT_AVAILABLE',\n REACT_HOOK_MISUSE = 'REACT_HOOK_MISUSE',\n\n // Crypto operations\n CRYPTO_PROVIDER_NOT_FOUND = 'CRYPTO_PROVIDER_NOT_FOUND',\n CRYPTO_OPERATION_FAILED = 'CRYPTO_OPERATION_FAILED',\n CRYPTO_KEY_DERIVATION_FAILED = 'CRYPTO_KEY_DERIVATION_FAILED',\n\n // Multibase operations\n MULTIBASE_DECODE_FAILED = 'MULTIBASE_DECODE_FAILED',\n MULTIBASE_ENCODE_FAILED = 'MULTIBASE_ENCODE_FAILED',\n MULTIBASE_INVALID_FORMAT = 'MULTIBASE_INVALID_FORMAT',\n\n // Validation operations\n SCOPE_VALIDATION_FAILED = 'SCOPE_VALIDATION_FAILED',\n SCOPE_INVALID_FORMAT = 'SCOPE_INVALID_FORMAT',\n VALIDATION_FAILED = 'VALIDATION_FAILED',\n INVALID_INPUT_FORMAT = 'INVALID_INPUT_FORMAT',\n\n // DeepLink operations\n DEEPLINK_INVALID_STATE = 'DEEPLINK_INVALID_STATE',\n DEEPLINK_CALLBACK_FAILED = 'DEEPLINK_CALLBACK_FAILED',\n\n // Storage operations\n STORAGE_QUOTA_EXCEEDED = 'STORAGE_QUOTA_EXCEEDED',\n STORAGE_OPERATION_FAILED = 'STORAGE_OPERATION_FAILED',\n\n // Signer operations\n SIGNER_CONVERSION_FAILED = 'SIGNER_CONVERSION_FAILED',\n SIGNER_OPERATION_FAILED = 'SIGNER_OPERATION_FAILED',\n\n // Permission operations\n PERMISSION_DENIED = 'PERMISSION_DENIED',\n OPERATION_NOT_PERMITTED = 'OPERATION_NOT_PERMITTED',\n\n // Generic errors\n OPERATION_NOT_SUPPORTED = 'OPERATION_NOT_SUPPORTED',\n INVALID_PARAMETER = 'INVALID_PARAMETER',\n INTERNAL_ERROR = 'INTERNAL_ERROR',\n NETWORK_ERROR = 'NETWORK_ERROR',\n TIMEOUT_ERROR = 'TIMEOUT_ERROR',\n}\n\n/**\n * Base error class for all IdentityKit errors\n */\nexport class IdentityKitError extends Error {\n public readonly code: IdentityKitErrorCode;\n public readonly category: string;\n public readonly details?: unknown;\n public readonly cause?: Error;\n\n constructor(\n code: IdentityKitErrorCode,\n message: string,\n options?: {\n category?: string;\n details?: unknown;\n cause?: Error;\n }\n ) {\n super(message);\n this.name = 'IdentityKitError';\n this.code = code;\n this.category = options?.category || this.inferCategory(code);\n this.details = options?.details;\n this.cause = options?.cause;\n\n // Maintain proper stack trace\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n\n // Chain the original error stack if available\n if (options?.cause) {\n this.stack = `${this.stack}\\nCaused by: ${options.cause.stack}`;\n }\n }\n\n /**\n * Infer error category from error code\n */\n private inferCategory(code: IdentityKitErrorCode): string {\n if (code.startsWith('AUTH_')) return 'authentication';\n if (code.startsWith('DID_')) return 'did';\n if (code.startsWith('VDR_')) return 'vdr';\n if (code.startsWith('KEY_')) return 'key-management';\n if (code.startsWith('WEB_')) return 'web';\n if (code.startsWith('REACT_')) return 'react';\n if (code.startsWith('CRYPTO_')) return 'crypto';\n if (code.startsWith('MULTIBASE_')) return 'multibase';\n if (code.startsWith('SCOPE_') || code.startsWith('VALIDATION_')) return 'validation';\n if (code.startsWith('DEEPLINK_')) return 'deeplink';\n if (code.startsWith('STORAGE_')) return 'storage';\n if (code.startsWith('SIGNER_')) return 'signer';\n if (code.includes('SIGNING') || code.includes('SIGNATURE')) return 'signing';\n if (code.includes('NETWORK')) return 'network';\n if (code.includes('PERMISSION')) return 'permission';\n return 'general';\n }\n\n /**\n * Convert to a plain object for serialization\n */\n toJSON(): {\n name: string;\n code: string;\n category: string;\n message: string;\n details?: unknown;\n stack?: string;\n } {\n return {\n name: this.name,\n code: this.code,\n category: this.category,\n message: this.message,\n details: this.details,\n stack: this.stack,\n };\n }\n\n /**\n * Get a user-friendly error message with suggestions\n */\n getUserMessage(): string {\n const suggestions = this.getSuggestions();\n let message = this.message;\n\n if (suggestions.length > 0) {\n message += '\\n\\nSuggestions:\\n' + suggestions.map(s => `• ${s}`).join('\\n');\n }\n\n return message;\n }\n\n /**\n * Get contextual suggestions based on error code\n */\n private getSuggestions(): string[] {\n switch (this.code) {\n case IdentityKitErrorCode.WEB_BROWSER_NOT_SUPPORTED:\n return [\n 'Use a modern browser that supports required Web APIs',\n \"Check if you're running in a browser environment\",\n ];\n\n case IdentityKitErrorCode.WEB_STORAGE_NOT_AVAILABLE:\n return [\n 'Enable localStorage or IndexedDB in your browser',\n \"Check if you're in private/incognito mode\",\n 'Consider using memory storage as fallback',\n ];\n\n case IdentityKitErrorCode.DID_METHOD_NOT_SUPPORTED:\n return [\n 'Check if the DID method is registered with VDRRegistry',\n 'Verify the DID format is correct',\n ];\n\n case IdentityKitErrorCode.VDR_NETWORK_ERROR:\n return [\n 'Check your network connection',\n 'Verify the RPC URL is correct and accessible',\n 'Check if the VDR service is running',\n ];\n\n case IdentityKitErrorCode.KEY_STORAGE_ERROR:\n return [\n 'Check browser storage permissions',\n 'Verify storage quota is not exceeded',\n 'Try clearing browser storage and retry',\n ];\n\n case IdentityKitErrorCode.REACT_NOT_AVAILABLE:\n return [\n 'Ensure React is properly installed and imported',\n \"Check if you're using the hook in a React component\",\n ];\n\n case IdentityKitErrorCode.CRYPTO_PROVIDER_NOT_FOUND:\n return [\n 'Check if the key type is supported',\n 'Verify the crypto provider factory configuration',\n ];\n\n case IdentityKitErrorCode.MULTIBASE_DECODE_FAILED:\n return [\n 'Verify the encoded string format is correct',\n 'Check if the multibase prefix is valid',\n 'Ensure the input is not corrupted',\n ];\n\n case IdentityKitErrorCode.SCOPE_VALIDATION_FAILED:\n return [\n 'Check the scope format: address::module::function',\n 'Verify the address format is valid',\n 'Ensure module and function names are correct',\n ];\n\n case IdentityKitErrorCode.DEEPLINK_INVALID_STATE:\n return [\n 'Check if the state parameter matches the stored value',\n 'Verify the callback URL parameters are correct',\n 'Ensure the session storage is available',\n ];\n\n case IdentityKitErrorCode.STORAGE_QUOTA_EXCEEDED:\n return [\n 'Clear unused data from browser storage',\n 'Check available storage quota',\n 'Consider using a different storage strategy',\n ];\n\n case IdentityKitErrorCode.SIGNER_CONVERSION_FAILED:\n return [\n 'Verify the signer implements the required interface',\n 'Check if the key ID is available in the signer',\n 'Ensure the DID account signer is properly configured',\n ];\n\n default:\n return [];\n }\n }\n}\n\n/**\n * Factory functions for creating specific error types\n */\n\nexport function createConfigurationError(\n message: string,\n details?: unknown,\n cause?: Error\n): IdentityKitError {\n return new IdentityKitError(IdentityKitErrorCode.INVALID_CONFIGURATION, message, {\n category: 'configuration',\n details,\n cause,\n });\n}\n\nexport function createDIDError(\n code: IdentityKitErrorCode,\n message: string,\n details?: unknown,\n cause?: Error\n): IdentityKitError {\n return new IdentityKitError(code, message, {\n category: 'did',\n details,\n cause,\n });\n}\n\nexport function createVDRError(\n code: IdentityKitErrorCode,\n message: string,\n details?: unknown,\n cause?: Error\n): IdentityKitError {\n return new IdentityKitError(code, message, {\n category: 'vdr',\n details,\n cause,\n });\n}\n\nexport function createKeyManagementError(\n code: IdentityKitErrorCode,\n message: string,\n details?: unknown,\n cause?: Error\n): IdentityKitError {\n return new IdentityKitError(code, message, {\n category: 'key-management',\n details,\n cause,\n });\n}\n\nexport function createAuthenticationError(\n code: IdentityKitErrorCode,\n message: string,\n details?: unknown,\n cause?: Error\n): IdentityKitError {\n return new IdentityKitError(code, message, {\n category: 'authentication',\n details,\n cause,\n });\n}\n\nexport function createWebError(\n code: IdentityKitErrorCode,\n message: string,\n details?: unknown,\n cause?: Error\n): IdentityKitError {\n return new IdentityKi