From 90aaf9d571f5bce20c96c4cb16372ae0646dbe92 Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Tue, 29 Oct 2024 11:10:24 -0700 Subject: [PATCH 1/4] Added CLI tests --- package.json | 4 +- src/cli.ts | 343 +++++++++++++++++++++---------- src/resolver.ts | 53 ++++- src/routes/.well-known/did.jsonl | 2 +- src/routes/did.ts | 5 +- src/utils.ts | 69 ++++--- test/cli-e2e.test.ts | 323 +++++++++++++++++++++++++++++ test/features.test.ts | 1 - test/fixtures/not-authorized.log | 4 +- test/witness.test.ts | 7 +- 10 files changed, 659 insertions(+), 152 deletions(-) create mode 100644 test/cli-e2e.test.ts diff --git a/package.json b/package.json index 4dff70f..c6e600c 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "dev": "bun --watch --inspect-wait ./src/resolver.ts", "server": "bun --watch ./src/resolver.ts", "test": "bun test", - "test:watch": "bun test --watch witness", + "test:watch": "bun test --watch", "test:bail": "bun test --watch --bail --verbose", "test:log": "mkdir -p ./test/logs && LOG_RESOLVES=true bun test &> ./test/logs/test-run.txt", - "cli": "bun run src/cli.ts" + "cli": "bun src/cli.ts" }, "devDependencies": { "bun-bagel": "^1.1.0", diff --git a/src/cli.ts b/src/cli.ts index 3121cce..a3f721a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,8 @@ -import { createDID, resolveDID, updateDID, deactivateDID, resolveDIDFromLog } from './method'; +import { createDID, updateDID, deactivateDID, resolveDIDFromLog } from './method'; import { createSigner, generateEd25519VerificationMethod } from './cryptography'; -import { fetchLogFromIdentifier, getFileUrl, readLogFromDisk, writeLogToDisk, writeVerificationMethodToEnv } from './utils'; +import { fetchLogFromIdentifier, readLogFromDisk, writeLogToDisk, writeVerificationMethodToEnv } from './utils'; +import { dirname } from 'path'; +import fs from 'fs'; const usage = ` Usage: bun run cli [command] [options] @@ -22,6 +24,7 @@ Options: --service [service] Add a service (format: type,endpoint) (can be used multiple times) --add-vm [type] Add a verification method (type can be authentication, assertionMethod, keyAgreement, capabilityInvocation, capabilityDelegation) --also-known-as [alias] Add an alsoKnownAs alias (can be used multiple times) + --next-key-hash [hash] Add a nextKeyHash (can be used multiple times) Examples: bun run cli create --domain example.com --portable --witness did:example:witness1 --witness did:example:witness2 @@ -30,41 +33,13 @@ Examples: bun run cli deactivate --log ./did.jsonl --output ./deactivated-did.jsonl `; -async function main() { - const args = Bun.argv.slice(2); - const command = args[0]; - - if (!command) { - console.log(usage); - process.exit(1); - } - - try { - switch (command) { - case 'create': - await handleCreate(args.slice(1)); - break; - case 'resolve': - await handleResolve(args.slice(1)); - break; - case 'update': - await handleUpdate(args.slice(1)); - break; - case 'deactivate': - await handleDeactivate(args.slice(1)); - break; - default: - console.log(`Unknown command: ${command}`); - console.log(usage); - process.exit(1); - } - } catch (error) { - console.error('An error occurred:', error); - process.exit(1); - } +// Add this function at the top with the other constants +function showHelp() { + console.log(usage); } -async function handleCreate(args: string[]) { +// Export the handler functions for testing +export async function handleCreate(args: string[]) { const options = parseOptions(args); const domain = options['domain'] as string; const output = options['output'] as string | undefined; @@ -72,60 +47,103 @@ async function handleCreate(args: string[]) { const prerotation = options['prerotation'] !== undefined; const witnesses = options['witness'] as string[] | undefined; const witnessThreshold = options['witness-threshold'] ? parseInt(options['witness-threshold'] as string) : witnesses?.length ?? 0; + const nextKeyHashes = options['next-key-hash'] as string[] | undefined; if (!domain) { console.error('Domain is required for create command'); process.exit(1); } - const authKey = await generateEd25519VerificationMethod(); - const { did, doc, meta, log } = await createDID({ - domain, - signer: createSigner(authKey), - updateKeys: [authKey.publicKeyMultibase!], - verificationMethods: [authKey], - portable, - prerotation, - witnesses, - witnessThreshold, - }); + if (prerotation && !nextKeyHashes) { + console.error('next-key-hash is required when prerotation is enabled'); + process.exit(1); + } + + try { + // Create DID + const authKey = await generateEd25519VerificationMethod(); + const { did, doc, meta, log } = await createDID({ + domain, + signer: createSigner(authKey), + updateKeys: [authKey.publicKeyMultibase!], + verificationMethods: [authKey], + portable, + prerotation, + witnesses, + witnessThreshold, + nextKeyHashes, + }); + + console.log('Created DID:', did); + + if (output) { + // Ensure output directory exists + const outputDir = dirname(output); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } - console.log('Created DID:', did); - // console.log('DID Document:', JSON.stringify(doc, null, 2)); - // console.log('Meta:', JSON.stringify(meta, null, 2)); - // console.log('DID Log:', JSON.stringify(log, null, 2)); + // Write log to file + writeLogToDisk(output, log); + console.log(`DID log written to ${output}`); + + // Save verification method to env + writeVerificationMethodToEnv({ + ...authKey, + controller: did, + id: `${did}#${authKey.publicKeyMultibase?.slice(-8)}` + }); + console.log(`DID verification method saved to env`); + + // Write DID document for reference + const docPath = output.replace('.jsonl', '.json'); + fs.writeFileSync(docPath, JSON.stringify(doc, null, 2)); + console.log(`DID document written to ${docPath}`); + } else { + // If no output specified, print to console + console.log('DID Document:', JSON.stringify(doc, null, 2)); + console.log('DID Log:', JSON.stringify(log, null, 2)); + } - if (output) { - writeLogToDisk(output, log); - console.log(`DID log written to ${output}`); - writeVerificationMethodToEnv({...authKey, controller: did, id: `${did}#${authKey.publicKeyMultibase?.slice(-8)}`}); - console.log(`DID verification method saved to env`); + return { did, doc, meta, log }; + } catch (error) { + console.error('Error creating DID:', error); + process.exit(1); } } -async function handleResolve(args: string[]) { +export async function handleResolve(args: string[]) { const options = parseOptions(args); const didIdentifier = options['did'] as string; + const logFile = options['log'] as string; - if (!didIdentifier) { - console.error('DID identifier is required for resolve command'); + if (!didIdentifier && !logFile) { + console.error('Either --did or --log is required for resolve command'); process.exit(1); } try { - const log = await fetchLogFromIdentifier(didIdentifier); + let log: DIDLog; + if (logFile) { + log = readLogFromDisk(logFile); + } else { + log = await fetchLogFromIdentifier(didIdentifier); + } + const { did, doc, meta } = await resolveDIDFromLog(log); console.log('Resolved DID:', did); console.log('DID Document:', JSON.stringify(doc, null, 2)); console.log('Metadata:', meta); + + return { did, doc, meta }; } catch (error) { console.error('Error resolving DID:', error); process.exit(1); } } -async function handleUpdate(args: string[]) { +export async function handleUpdate(args: string[]) { const options = parseOptions(args); const logFile = options['log'] as string; const output = options['output'] as string | undefined; @@ -133,48 +151,98 @@ async function handleUpdate(args: string[]) { const witnesses = options['witness'] as string[] | undefined; const witnessThreshold = options['witness-threshold'] ? parseInt(options['witness-threshold'] as string) : undefined; const services = options['service'] ? parseServices(options['service'] as string[]) : undefined; - const addVm = options['add-vm'] as VerificationMethodType[] | undefined; + const addVm = options['add-vm'] as string[] | undefined; const alsoKnownAs = options['also-known-as'] as string[] | undefined; + const updateKey = options['update-key'] as string | undefined; if (!logFile) { console.error('Log file is required for update command'); process.exit(1); } - const log = readLogFromDisk(logFile); - const authKey = await generateEd25519VerificationMethod(); - - const verificationMethods: VerificationMethod[] = [ - authKey, - ...(addVm?.map(type => ({ - type: "Multikey", - publicKeyMultibase: authKey.publicKeyMultibase, - } as VerificationMethod)) || []) - ]; - - const { did, doc, meta, log: updatedLog } = await updateDID({ - log, - signer: createSigner(authKey), - updateKeys: [authKey.publicKeyMultibase!], - verificationMethods, - prerotation, - witnesses, - witnessThreshold, - services, - alsoKnownAs, - }); + try { + const log = readLogFromDisk(logFile); + const { did, meta } = await resolveDIDFromLog(log); + console.log('\nCurrent DID:', did); + console.log('Current meta:', meta); + + // Get the verification method from environment + const envVMs = JSON.parse(Buffer.from(process.env.DID_VERIFICATION_METHODS || 'W10=', 'base64').toString('utf8')); + + const vm = envVMs.find((vm: any) => vm.controller === did); + console.log('\nFound VM:', vm); + + if (!vm) { + throw new Error('No matching verification method found for DID'); + } + + // Only generate a new auth key if update-key wasn't provided + const authKey = updateKey ? { + type: "Multikey" as const, + publicKeyMultibase: updateKey, + secretKeyMultibase: vm.secretKeyMultibase, + controller: did, + id: `${did}#${updateKey.slice(-8)}` + } : await generateEd25519VerificationMethod(); + + console.log('\nNew auth key:', authKey); + + // Create verification methods array + const verificationMethods: VerificationMethod[] = []; + + // If we're adding VMs, create a VM for each type + if (addVm && addVm.length > 0) { + const vmId = `${did}#${authKey.publicKeyMultibase!.slice(-8)}`; + + // Add a verification method for each type + for (const vmType of addVm) { + const newVM: VerificationMethod = { + id: vmId, + type: "Multikey", + controller: did, + publicKeyMultibase: authKey.publicKeyMultibase, + secretKeyMultibase: authKey.secretKeyMultibase, + purpose: vmType as VerificationMethodType + }; + verificationMethods.push(newVM); + } + } else { + // For non-VM updates (services, alsoKnownAs), still need a VM with purpose + verificationMethods.push({ + id: `${did}#${authKey.publicKeyMultibase!.slice(-8)}`, + type: "Multikey", + controller: did, + publicKeyMultibase: authKey.publicKeyMultibase, + secretKeyMultibase: authKey.secretKeyMultibase, + purpose: "assertionMethod" + }); + } - console.log('Updated DID:', did); - console.log('Updated DID Document:', JSON.stringify(doc, null, 2)); - console.log('Updated Metadata:', meta); + const result = await updateDID({ + log, + signer: createSigner(authKey), + updateKeys: [authKey.publicKeyMultibase!], + verificationMethods, + prerotation, + witnesses, + witnessThreshold, + services, + alsoKnownAs + }); + + if (output) { + writeLogToDisk(output, result.log); + console.log(`Updated DID log written to ${output}`); + } - if (output) { - writeLogToDisk(output, updatedLog); - console.log(`Updated DID log written to ${output}`); + return result; + } catch (error) { + console.error('Error updating DID:', error); + process.exit(1); } } -async function handleDeactivate(args: string[]) { +export async function handleDeactivate(args: string[]) { const options = parseOptions(args); const logFile = options['log'] as string; const output = options['output'] as string | undefined; @@ -184,20 +252,43 @@ async function handleDeactivate(args: string[]) { process.exit(1); } - const log = readLogFromDisk(logFile); - const authKey = await generateEd25519VerificationMethod(); - const { did, doc, meta, log: deactivatedLog } = await deactivateDID({ - log, - signer: createSigner(authKey), - }); + try { + // Read the current log to get the latest state + const log = readLogFromDisk(logFile); + const { did, meta } = await resolveDIDFromLog(log); + console.log('Current DID:', did); + console.log('Current meta:', meta); + + // Get the verification method from environment + const envContent = fs.readFileSync('.env', 'utf8'); + const vmMatch = envContent.match(/DID_VERIFICATION_METHODS=(.+)/); + if (!vmMatch) { + throw new Error('No verification method found in .env file'); + } + + // Parse the VM from env + const vm = JSON.parse(Buffer.from(vmMatch[1], 'base64').toString('utf8'))[0]; + if (!vm) { + throw new Error('No verification method found in environment'); + } + + // Use the current authorized key from meta + vm.publicKeyMultibase = meta.updateKeys[0]; - console.log('Deactivated DID:', did); - console.log('Deactivated DID Document:', JSON.stringify(doc, null, 2)); - console.log('Deactivated Metadata:', meta); + const result = await deactivateDID({ + log, + signer: createSigner(vm) + }); - if (output) { - writeLogToDisk(output, deactivatedLog); - console.log(`Deactivated DID log written to ${output}`); + if (output) { + writeLogToDisk(output, result.log); + console.log(`Deactivated DID log written to ${output}`); + } + + return result; + } catch (error) { + console.error('Error deactivating DID:', error); + process.exit(1); } } @@ -209,7 +300,7 @@ function parseOptions(args: string[]): Record { + console.error('Fatal error:', error); + process.exit(1); + }); +} + +// Export main for testing if needed +export { main }; diff --git a/src/resolver.ts b/src/resolver.ts index bdba151..ebac620 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -6,12 +6,17 @@ const app = new Elysia() .get('/health', 'ok') .get('/.well-known/did.jsonl', () => getLogFileForBase()) .post('/witness', async ({body}) => { - const result = await createWitnessProof((body as any).log); - console.log(`Signed with VM`, (result as any).proof.verificationMethod) - if ('error' in result) { - return { error: result.error }; + try { + const result = await createWitnessProof((body as any).log); + if ('error' in result) { + throw new Error(result.error); + } + console.log(`Signed with VM`, (result as any).proof.verificationMethod) + return { proof: result.proof }; + } catch (error) { + console.error('Error creating witness proof:', error); + return new Response(JSON.stringify({ error }), { status: 400 }); } - return { proof: result.proof }; }) .group('/:id', app => { return app @@ -24,7 +29,41 @@ const app = new Elysia() }) .get('/', ({params}) => getLatestDIDDoc({params})) }) - .listen(8000) +const port = process.env.PORT || 8000; -console.log(`🔍 Resolver is running at on port ${app.server?.port}...`) +// Parse the DID_VERIFICATION_METHODS environment variable +const verificationMethods = JSON.parse(Buffer.from(process.env.DID_VERIFICATION_METHODS || 'W10=', 'base64').toString('utf8')); + +// Function to get all active DIDs from verification methods +async function getActiveDIDs(): Promise { + const activeDIDs: string[] = []; + + try { + // Get unique DIDs from verification methods + for (const vm of verificationMethods) { + const did = vm.controller || vm.id.split('#')[0]; + activeDIDs.push(did); + } + } catch (error) { + console.error('Error processing verification methods:', error); + } + + return activeDIDs; +} + +// Log active DIDs when server starts +app.onStart(async () => { + console.log('\n=== Active DIDs ==='); + const activeDIDs = await getActiveDIDs(); + + if (activeDIDs.length === 0) { + console.log('No active DIDs found'); + } else { + activeDIDs.forEach(did => console.log(did)); + } + console.log('=================\n'); +}); + +console.log(`🔍 Resolver is running at http://localhost:${port}`); +app.listen(port); diff --git a/src/routes/.well-known/did.jsonl b/src/routes/.well-known/did.jsonl index 0561f56..18b35f6 100644 --- a/src/routes/.well-known/did.jsonl +++ b/src/routes/.well-known/did.jsonl @@ -1 +1 @@ -{"versionId":"1-QmVo8HTg988dwKhyBooHcCGHUpk23wrND9n8KAY97KQA3B","versionTime":"2024-10-16T19:49:10Z","parameters":{"method":"did:tdw:0.4","scid":"QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj","updateKeys":["z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000","controller":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000","authentication":["did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000#7vs27kdp"],"verificationMethod":[{"id":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000#7vs27kdp","controller":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000","type":"Multikey","publicKeyMultibase":"z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp#z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp","created":"2024-10-16T19:49:10Z","proofPurpose":"authentication","proofValue":"z2dWto83Fc8iSsmpp8qKkbdJBBu82GmJGx4qrF3KjVRKtvQYEbrHKASo1JDbxXbTuoN9PnqZbY8uvDv758csXGiM6"}]} +{"versionId":"1-QmayWDYf5yveXwGRCDAQs5Nsv3jx14Q3owAL58HYe4uTtJ","versionTime":"2024-10-29T16:06:58Z","parameters":{"method":"did:tdw:0.4","scid":"QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H","updateKeys":["z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1"],"portable":true,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000","controller":"did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000","assertionMethod":["did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000#ABisKwp1"],"verificationMethod":[{"id":"did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000#ABisKwp1","controller":"did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000","type":"Multikey","publicKeyMultibase":"z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1#z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1","created":"2024-10-29T16:06:58Z","proofPurpose":"assertionMethod","proofValue":"z3QDMmuuLYrpdh13zj9UrqjFzrRATdyGwyYdWT9pgts52uiRZ56jPcXiHEHqfRXLPjzrxd2UAJYyFWj3aK9oLNtf6"}]} diff --git a/src/routes/did.ts b/src/routes/did.ts index 76fc5c5..8506e0b 100644 --- a/src/routes/did.ts +++ b/src/routes/did.ts @@ -1,12 +1,13 @@ -import { resolveDID } from '../method'; +import { resolveDIDFromLog } from '../method'; import { getFileUrl } from '../utils'; export const getLatestDIDDoc = async ({params: {id}}: {params: {id: string;};}) => { try { + console.log(`Resolving DID ${id}`); const url = getFileUrl(id); const didLog = await (await fetch(url)).text(); const logEntries: DIDLog = didLog.trim().split('\n').map(l => JSON.parse(l)); - const {did, doc, meta} = await resolveDID(logEntries); + const {did, doc, meta} = await resolveDIDFromLog(logEntries); return {doc, meta}; } catch (e) { console.error(e) diff --git a/src/utils.ts b/src/utils.ts index fe61b4d..bec8c51 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,13 +7,25 @@ import { resolveDIDFromLog } from './method'; import { join } from 'path'; export const readLogFromDisk = (path: string): DIDLog => { - return fs.readFileSync(path, 'utf8').trim().split('\n').map(l => JSON.parse(l)); + return readLogFromString(fs.readFileSync(path, 'utf8')); +} + +export const readLogFromString = (str: string): DIDLog => { + return str.trim().split('\n').map(l => JSON.parse(l)); } export const writeLogToDisk = (path: string, log: DIDLog) => { - fs.writeFileSync(path, JSON.stringify(log.shift()) + '\n'); - for (const entry of log) { - fs.appendFileSync(path, JSON.stringify(entry) + '\n'); + try { + // Write first entry + fs.writeFileSync(path, JSON.stringify(log[0]) + '\n'); + + // Append remaining entries + for (let i = 1; i < log.length; i++) { + fs.appendFileSync(path, JSON.stringify(log[i]) + '\n'); + } + } catch (error) { + console.error('Error writing log to disk:', error); + throw error; } } @@ -179,10 +191,6 @@ export const normalizeVMs = (verificationMethod: VerificationMethod[] | undefine export const collectWitnessProofs = async (witnesses: string[], log: DIDLog): Promise => { const proofs: DataIntegrityProof[] = []; - const timeout = (ms: number) => new Promise((_, reject) => - setTimeout(() => reject(new Error('Request timed out')), ms) - ); - const collectProof = async (witness: string): Promise => { const parts = witness.split(':'); if (parts.length < 4) { @@ -191,38 +199,37 @@ export const collectWitnessProofs = async (witnesses: string[], log: DIDLog): Pr const witnessUrl = getBaseUrl(witness) + '/witness'; try { - const response: any = await Promise.race([ - fetch(witnessUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ log }), - }), - timeout(10000) // 10 second timeout - ]); + const response = await fetch(witnessUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ log }), + }); - if (response.ok) { - const data = await response.json(); - if (data.proof) { - proofs.push(data.proof); - } else { - console.warn(`Witness ${witnessUrl} did not provide a valid proof`); - } - } else { + if (!response.ok) { console.warn(`Witness ${witnessUrl} responded with status: ${response.status}`); + return; } - } catch (error: any) { - if (error.message === 'Request timed out') { - console.error(`Request to witness ${witnessUrl} timed out`); + + const data = await response.json() as any; + if (data.proof) { + proofs.push(data.proof); + } else if (data.data?.proof) { + proofs.push(data.data.proof); } else { - console.error(`Error collecting proof from witness ${witnessUrl}:`, error); + console.warn(`Witness ${witnessUrl} did not provide a valid proof`); } + } catch (error: any) { + console.error(`Error collecting proof from witness ${witnessUrl}:`, error.message); } }; - // Collect proofs from all witnesses concurrently await Promise.all(witnesses.map(collectProof)); + + if (proofs.length === 0) { + console.warn('No witness proofs were collected'); + } return proofs; }; diff --git a/test/cli-e2e.test.ts b/test/cli-e2e.test.ts new file mode 100644 index 0000000..3c7a7dc --- /dev/null +++ b/test/cli-e2e.test.ts @@ -0,0 +1,323 @@ +import { describe, expect, test, beforeAll, afterAll, it } from "bun:test"; +import fs from 'node:fs'; +import { join } from 'path'; +import { readLogFromDisk, readLogFromString } from "../src/utils"; +import { $ } from "bun"; +import { resolveDIDFromLog } from "../src/method"; + +describe("CLI End-to-End Tests", () => { + const TEST_DIR = './test/temp-cli-e2e'; + const TEST_LOG_FILE = join(TEST_DIR, 'did.jsonl'); + const WITNESS_SERVER_URL = "http://localhost:8000"; + let currentDID: string; + + beforeAll(() => { + // Create test directory if it doesn't exist + if (!fs.existsSync(TEST_DIR)) { + fs.mkdirSync(TEST_DIR, { recursive: true }); + } + }); + + afterAll(() => { + // Clean up test files + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true }); + } + }); + + test("Create DID using CLI", async () => { + // Run the CLI create command + const proc = await $`bun run cli create --domain example.com --output ${TEST_LOG_FILE} --portable`.quiet(); + + expect(proc.exitCode).toBe(0); + + // Verify the log file was created + expect(fs.existsSync(TEST_LOG_FILE)).toBe(true); + + // Read and verify the log content + const log = readLogFromDisk(TEST_LOG_FILE); + expect(log).toHaveLength(1); + expect(log[0].parameters.portable).toBe(true); + expect(log[0].parameters.method).toBe('did:tdw:0.4'); + + // Get the DID from the log + const { did, meta } = await resolveDIDFromLog(log); + currentDID = did; + + // Read the verification method directly from .env file + const envContent = fs.readFileSync('.env', 'utf8'); + const vmMatch = envContent.match(/DID_VERIFICATION_METHODS=(.+)/); + if (!vmMatch) { + throw new Error('No verification method found in .env file'); + } + + // Parse and set the VM in the current process + const vm = JSON.parse(Buffer.from(vmMatch[1], 'base64').toString('utf8'))[0]; + process.env.DID_VERIFICATION_METHODS = vmMatch[1]; + }); + + test("Update DID using CLI", async () => { + // Read the current log to get the latest state + const currentLog = readLogFromDisk(TEST_LOG_FILE); + const { meta } = await resolveDIDFromLog(currentLog); + + // Get the authorized key from meta + const authorizedKey = meta.updateKeys[0]; + + // Run the CLI update command to add a service, using the authorized key + const proc = await $`bun run cli update --log ${TEST_LOG_FILE} --output ${TEST_LOG_FILE} --service LinkedDomains,https://example.com --update-key ${authorizedKey}`.quiet(); + + expect(proc.exitCode).toBe(0); + + // Verify the update + const log = readLogFromDisk(TEST_LOG_FILE); + expect(log).toHaveLength(2); + + // Check if service was added + const lastEntry = log[log.length - 1]; + expect(lastEntry.state?.service).toBeDefined(); + if (lastEntry.state?.service) { + expect(lastEntry.state.service[0].type).toBe('LinkedDomains'); + } + }); + + test("Second Update DID using CLI", async () => { + // Read the current log to get the latest state + const currentLog = readLogFromDisk(TEST_LOG_FILE); + const { meta } = await resolveDIDFromLog(currentLog); + + // Get the authorized key from meta + const authorizedKey = meta.updateKeys[0]; + + // Run the CLI update command to add another service, using the authorized key + const proc = await $`bun run cli update --log ${TEST_LOG_FILE} --output ${TEST_LOG_FILE} --service NewService,https://newservice.example.com --update-key ${authorizedKey}`.quiet(); + + expect(proc.exitCode).toBe(0); + + // Verify the update + const log = readLogFromDisk(TEST_LOG_FILE); + expect(log).toHaveLength(3); + + // Check if new service was added + const lastEntry = log[log.length - 1]; + expect(lastEntry.state?.service).toBeDefined(); + if (lastEntry.state?.service) { + expect(lastEntry.state.service[0].type).toBe('NewService'); + } + }); + + test("Deactivate DID using CLI", async () => { + // Read the current log to get the latest state + const currentLog = readLogFromDisk(TEST_LOG_FILE); + const { meta } = await resolveDIDFromLog(currentLog); + + // Read the current verification method from env + const envContent = fs.readFileSync('.env', 'utf8'); + const vmMatch = envContent.match(/DID_VERIFICATION_METHODS=(.+)/); + if (!vmMatch) { + throw new Error('No verification method found in .env file'); + } + + // Parse and update the VM with the current authorized key + const vm = JSON.parse(Buffer.from(vmMatch[1], 'base64').toString('utf8'))[0]; + vm.publicKeyMultibase = meta.updateKeys[0]; + process.env.DID_VERIFICATION_METHODS = Buffer.from(JSON.stringify([vm])).toString('base64'); + + // Run the CLI deactivate command + const proc = await $`bun run cli deactivate --log ${TEST_LOG_FILE} --output ${TEST_LOG_FILE}`.quiet(); + expect(proc.exitCode).toBe(0); + + // Verify the deactivation + const log = readLogFromDisk(TEST_LOG_FILE); + const lastEntry = log[log.length - 1]; + expect(lastEntry.parameters.deactivated).toBe(true); + }); + + test("Create DID with witnesses using CLI", async () => { + const witnessLogFile = join(TEST_DIR, 'did-witness.jsonl'); + + try { + // First, fetch the witness's DID log directly + const witnessProc = await $`curl http://localhost:8000/.well-known/did.jsonl`.quiet(); + if (witnessProc.exitCode !== 0) { + console.error('Error fetching witness DID:', witnessProc.stderr.toString()); + throw new Error('Failed to fetch witness DID'); + } + + // Parse the witness DID log + const witnessLogStr = witnessProc.stdout.toString(); + + // Parse the witness log and get the DID from the state + const witnessLog = readLogFromString(witnessLogStr); + const witnessDID = witnessLog[0].state.id; + + // Run the CLI create command with witness + const proc = await $`bun run cli create --domain localhost:8000 --output ${witnessLogFile} --witness ${witnessDID} --witness-threshold 1`.quiet(); + + expect(proc.exitCode).toBe(0); + + // Verify the witness configuration + const log = readLogFromDisk(witnessLogFile); + + // Add null checks for TypeScript + if (!log[0]?.parameters?.witnesses) { + throw new Error('Missing witnesses in parameters'); + } + + expect(log[0].parameters.witnesses).toHaveLength(1); + expect(log[0].parameters.witnesses[0]).toBe(witnessDID!); + expect(log[0].parameters.witnessThreshold).toBe(1); + expect(log[0].proof).toHaveLength(2); // Controller proof + witness proof + } catch (error) { + console.error('Error in witness test:', error); + throw error; + } + }); + + test("Create DID with prerotation", async () => { + const prerotationLogFile = join(TEST_DIR, 'did-prerotation.jsonl'); + + // First create a DID with prerotation and next key hashes + const nextKeyHash1 = "z6MkgYGF3thn8k1Qz9P4c3mKthZXNhUgkdwBwE5hbWFJktGH"; + const nextKeyHash2 = "z6MkrCD1Qr8TQ4SQNzpkwx8qRLFQkUg7oKc8rjhYoV6DpHXx"; + + const createProc = await $`bun run cli create --domain example.com --output ${prerotationLogFile} --portable --prerotation --next-key-hash ${nextKeyHash1} --next-key-hash ${nextKeyHash2}`.quiet(); + expect(createProc.exitCode).toBe(0); + + // Wait a moment for the .env file to be written + await new Promise(resolve => setTimeout(resolve, 100)); + + // Get the current authorized key and DID + const currentLog = readLogFromDisk(prerotationLogFile); + const { did, meta } = await resolveDIDFromLog(currentLog); + const authorizedKey = meta.updateKeys[0]; + + // Read and parse the VM from env + const envContent = fs.readFileSync('.env', 'utf8'); + const vmMatch = envContent.match(/DID_VERIFICATION_METHODS=(.+)/); + if (!vmMatch) { + throw new Error('No verification method found in .env file'); + } + + // Parse and update the VM with the current authorized key and controller + const vm = JSON.parse(Buffer.from(vmMatch[1], 'base64').toString('utf8'))[0]; + vm.publicKeyMultibase = authorizedKey; + vm.controller = did; + process.env.DID_VERIFICATION_METHODS = Buffer.from(JSON.stringify([vm])).toString('base64'); + + // Verify prerotation setup + expect(currentLog[0].parameters.prerotation).toBe(true); + expect(currentLog[0].parameters.nextKeyHashes).toHaveLength(2); + expect(currentLog[0].parameters.nextKeyHashes).toContain(nextKeyHash1); + expect(currentLog[0].parameters.nextKeyHashes).toContain(nextKeyHash2); + }); + + test("Update DID with verification methods", async () => { + const vmLogFile = join(TEST_DIR, 'did-vm.jsonl'); + + // First create a DID + const createProc = await $`bun run cli create --domain example.com --output ${vmLogFile} --portable`.quiet(); + expect(createProc.exitCode).toBe(0); + + // Wait a moment for the .env file to be written + await new Promise(resolve => setTimeout(resolve, 100)); + + // Get the current authorized key and DID + const currentLog = readLogFromDisk(vmLogFile); + const { did, meta } = await resolveDIDFromLog(currentLog); + const authorizedKey = meta.updateKeys[0]; + + // Read and parse the VM from env + const envContent = fs.readFileSync('.env', 'utf8'); + const vmMatch = envContent.match(/DID_VERIFICATION_METHODS=(.+)/); + if (!vmMatch) { + throw new Error('No verification method found in .env file'); + } + + // Parse and update the VM with the current authorized key + const vm = JSON.parse(Buffer.from(vmMatch[1], 'base64').toString('utf8'))[0]; + vm.publicKeyMultibase = authorizedKey; + vm.controller = did; + vm.id = `${did}#${authorizedKey.slice(-8)}`; + process.env.DID_VERIFICATION_METHODS = Buffer.from(JSON.stringify([vm])).toString('base64'); + + // Add all VM types in a single update + const proc = await $`bun run cli update --log ${vmLogFile} --output ${vmLogFile} --add-vm authentication --add-vm assertionMethod --add-vm keyAgreement --add-vm capabilityInvocation --add-vm capabilityDelegation --update-key ${authorizedKey}`.quiet(); + expect(proc.exitCode).toBe(0); + + // Verify all VM types were added + const finalLog = readLogFromDisk(vmLogFile); + const finalEntry = finalLog[finalLog.length - 1]; + + const vmTypes = ['authentication', 'assertionMethod', 'keyAgreement', 'capabilityInvocation', 'capabilityDelegation'] as const; + const vmId = `${did}#${authorizedKey.slice(-8)}`; + + for (const vmType of vmTypes) { + expect(finalEntry.state[vmType]).toBeDefined(); + expect(Array.isArray(finalEntry.state[vmType])).toBe(true); + expect(finalEntry.state[vmType]).toContain(vmId); + } + }); + + test("Update DID with alsoKnownAs", async () => { + const akLogFile = join(TEST_DIR, 'did-aka.jsonl'); + + // First create a DID + const createProc = await $`bun run cli create --domain example.com --output ${akLogFile} --portable`.quiet(); + expect(createProc.exitCode).toBe(0); + + // Wait a moment for the .env file to be written + await new Promise(resolve => setTimeout(resolve, 100)); + + // Get the current authorized key and DID + const currentLog = readLogFromDisk(akLogFile); + const { did, meta } = await resolveDIDFromLog(currentLog); + const authorizedKey = meta.updateKeys[0]; + + // Read and parse the VM from env + const envContent = fs.readFileSync('.env', 'utf8'); + const vmMatch = envContent.match(/DID_VERIFICATION_METHODS=(.+)/); + if (!vmMatch) { + throw new Error('No verification method found in .env file'); + } + + // Parse and update the VM with the current authorized key + const vm = JSON.parse(Buffer.from(vmMatch[1], 'base64').toString('utf8'))[0]; + vm.publicKeyMultibase = authorizedKey; + vm.controller = did; + process.env.DID_VERIFICATION_METHODS = Buffer.from(JSON.stringify([vm])).toString('base64'); + + // Update with alsoKnownAs + const alias = 'https://example.com/users/123'; + const proc = await $`bun run cli update --log ${akLogFile} --output ${akLogFile} --also-known-as ${alias} --update-key ${authorizedKey}`.quiet(); + expect(proc.exitCode).toBe(0); + + // Verify alsoKnownAs was added + const finalLog = readLogFromDisk(akLogFile); + const lastEntry = finalLog[finalLog.length - 1]; + expect(lastEntry.state.alsoKnownAs).toBeDefined(); + expect(Array.isArray(lastEntry.state.alsoKnownAs)).toBe(true); + expect(lastEntry.state.alsoKnownAs).toContain(alias); + }); + + test("Resolve DID command", async () => { + // First create a DID + const resolveLogFile = join(TEST_DIR, 'did-resolve.jsonl'); + const createProc = await $`bun run cli create --domain example.com --output ${resolveLogFile} --portable`.quiet(); + expect(createProc.exitCode).toBe(0); + + // Get the DID from the log + const log = readLogFromDisk(resolveLogFile); + const { did } = await resolveDIDFromLog(log); + + // Test resolve command with log file instead of DID + const proc = await $`bun run cli resolve --log ${resolveLogFile}`.quiet(); + expect(proc.exitCode).toBe(0); + + // Verify resolve output contains expected fields + const output = proc.stdout.toString(); + expect(output).toContain('Resolved DID'); + expect(output).toContain('DID Document'); + expect(output).toContain('Metadata'); + }); +}); \ No newline at end of file diff --git a/test/features.test.ts b/test/features.test.ts index b81c949..49bab9f 100644 --- a/test/features.test.ts +++ b/test/features.test.ts @@ -77,7 +77,6 @@ beforeAll(async () => { }); test("Resolve DID at time (first)", async () => { - console.error('log', log) const resolved = await resolveDIDFromLog(log, {versionTime: new Date('2021-01-15T08:32:55Z')}); expect(resolved.meta.versionId.split('-')[0]).toBe('1'); }); diff --git a/test/fixtures/not-authorized.log b/test/fixtures/not-authorized.log index a3e3dcb..537ebef 100644 --- a/test/fixtures/not-authorized.log +++ b/test/fixtures/not-authorized.log @@ -1,2 +1,2 @@ -{"versionId":"1-QmbHUDLxTJCeKX3oACyabxT41b59dBeyVmGFUbYHheyXzZ","versionTime":"2024-10-25T18:45:30Z","parameters":{"method":"did:tdw:0.4","scid":"QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q","updateKeys":["z6MkmGhgCBEntnVBDJxRuzmiQegAW9CjxiNBghfQkuXz1KAL"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com","controller":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com","assertionMethod":["did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#kuXz1KAL","did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#zZHqtfrA"],"verificationMethod":[{"id":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#kuXz1KAL","controller":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com","type":"Multikey","publicKeyMultibase":"z6MkmGhgCBEntnVBDJxRuzmiQegAW9CjxiNBghfQkuXz1KAL"},{"id":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#zZHqtfrA","controller":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com","type":"Multikey","publicKeyMultibase":"z6MkoQuMZHARUzhqbFyz8R4S4XZtrZdXDNGvsPoMzZHqtfrA"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkmGhgCBEntnVBDJxRuzmiQegAW9CjxiNBghfQkuXz1KAL#z6MkmGhgCBEntnVBDJxRuzmiQegAW9CjxiNBghfQkuXz1KAL","created":"2024-10-25T18:45:30Z","proofPurpose":"assertionMethod","proofValue":"z4gL79eB3tnnHm8UPnxRPf4Q6P2E8t3YSxbF76MX2TG3acDDY8ZdjFo8XdbVPPqt49j5KacX5zVbawNmqBZb1rrdK"}]} -{"versionId":"2-Qmdxp9ccKBcsCvWS1J4CZJLSLb7xwbVDnbrVK9vvVdk8hk","versionTime":"2024-10-25T18:45:30Z","parameters":{"witnesses":[],"witnessThreshold":0},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com","controller":["did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com"],"assertionMethod":["did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#t9eqqUNt","did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#aKfrG3z9"],"verificationMethod":[{"id":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#t9eqqUNt","controller":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com","type":"Multikey","publicKeyMultibase":"z6MknCCJKC4F9JkRPpCiAVq7zJjU68yuvvj2x9ght9eqqUNt"},{"id":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com#aKfrG3z9","controller":"did:tdw:QmZnNM9Fzy5DU61AXBxtHbReZFMoEuS636s9HsxfJZbF9q:example.com","type":"Multikey","publicKeyMultibase":"z6Mkpec8gRmSA83WzQj36Tuhaa4mQLXuyuB76QipaKfrG3z9"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkoQuMZHARUzhqbFyz8R4S4XZtrZdXDNGvsPoMzZHqtfrA#z6MkoQuMZHARUzhqbFyz8R4S4XZtrZdXDNGvsPoMzZHqtfrA","created":"2024-10-25T18:45:30Z","proofPurpose":"assertionMethod","proofValue":"z3yyXeh9y4bibGaJWkrWBi7A3fG2TnRDtuoWUzvZFo91yUBqs8vuZEEqRTT27FjMpusJU1VWf8WWJu7N3LBLphtyZ"}]} +{"versionId":"1-QmVAR3nCjqo3EMtMoneARLsQyGPzGnzCLaffEiMB82EnhR","versionTime":"2024-10-29T18:09:38Z","parameters":{"method":"did:tdw:0.4","scid":"QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m","updateKeys":["z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","assertionMethod":["did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#95h4AW9z","did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#53DwaRB9"],"verificationMethod":[{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#95h4AW9z","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z"},{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#53DwaRB9","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6MkjbVgHJM3tiKdbNFVEwKMudBiWHAPMkedMN8753DwaRB9"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z#z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z","created":"2024-10-29T18:09:38Z","proofPurpose":"assertionMethod","proofValue":"zVZaCEz6PwmSZuU9vbXGA8ZAMvBKB9qDhac3CtGeyGyY2AmuRiMNxxoEdfw4dxfKEAWdHpHSoKLeMsMv2wTor1Uc"}]} +{"versionId":"2-QmPda74e1FZtwv9QgpRzSDMGqywtdUVRTMnHsRcwDumgWH","versionTime":"2024-10-29T18:09:38Z","parameters":{"witnesses":[],"witnessThreshold":0},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","controller":["did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com"],"assertionMethod":["did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#mfG6KeH9","did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#nVdR2AUv"],"verificationMethod":[{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#mfG6KeH9","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6MkiHEEvrBizECqja9Ews96qkaozd7tPiWHVHVymfG6KeH9"},{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#nVdR2AUv","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6Mkea9JYHgb9mwCVBA5wpxPYUE2fsVJ9tVsb3iunVdR2AUv"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjbVgHJM3tiKdbNFVEwKMudBiWHAPMkedMN8753DwaRB9#z6MkjbVgHJM3tiKdbNFVEwKMudBiWHAPMkedMN8753DwaRB9","created":"2024-10-29T18:09:38Z","proofPurpose":"assertionMethod","proofValue":"z4rEJBnJrBMTmU645eGMwH2AVC6VC2muvokTSXkCPqhowCvSnvcJGtu7s9HEnKUukaD3o8YB3unGpFcTcoGHWMcFy"}]} diff --git a/test/witness.test.ts b/test/witness.test.ts index b4677f0..9db1522 100644 --- a/test/witness.test.ts +++ b/test/witness.test.ts @@ -34,8 +34,12 @@ const getWitnessDIDLog = async () => { const isWitnessServerRunning = async () => { try { const response = await fetch(`${WITNESS_SERVER_URL}/health`); - return response.ok; + if (response.ok) { + return true; + } + return false; } catch (error) { + console.error('Witness server is not running'); return false; } }; @@ -61,7 +65,6 @@ const runWitnessTests = async () => { const didLog = await getWitnessDIDLog(); const {did, meta} = await resolveDIDFromLog(didLog as DIDLog); WITNESS_SCID = meta.scid; - console.log(`Witness DID ${did} found`); }); test("Create DID with witness", async () => { From 68fa54d19dbd8a5d23f870ebce8aaef4bb91a6e9 Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Tue, 29 Oct 2024 14:04:03 -0700 Subject: [PATCH 2/4] Update readme --- README.md | 147 ++++++++++++++++++++++----------------------------- bin/tdw | 2 + package.json | 3 ++ 3 files changed, 68 insertions(+), 84 deletions(-) create mode 100755 bin/tdw diff --git a/README.md b/README.md index 700ed6c..cf8bc4d 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ The following commands are defined in the `package.json` file: ``` This command runs: `bun test` -4. `test:watch`: Run tests in watch mode, focusing on witness tests. +4. `test:watch`: Run tests in watch mode. ```bash bun run test:watch ``` - This command runs: `bun test --watch witness` + This command runs: `bun test --watch` 5. `test:bail`: Run tests in watch mode, stopping on the first failure with verbose output. ```bash @@ -78,102 +78,81 @@ The following commands are defined in the `package.json` file: ``` This command runs: `bun run src/cli.ts --` -## CLI Documentation +## CLI Usage Guide -``` -The CLI is Experimental, buggy and beta software -- use at your own risk! -``` - -The trustdidweb-ts package provides a Command Line Interface (CLI) for managing Decentralized Identifiers (DIDs) using the `did:tdw` method. - - -### Usage - -The general syntax for using the CLI is: +> ⚠️ **Warning**: The CLI is experimental beta software - use at your own risk! +### Basic Syntax ```bash bun run cli [command] [options] ``` -To output the help using the CLI: +### Available Commands + +#### 1. Create a DID +Create a new DID with various configuration options: ```bash -bun run cli help +bun run cli create \ + --domain example.com \ + --output ./did.jsonl \ + --portable \ + --witness did:tdw:witness1:example.com \ + --witness-threshold 1 ``` -### Commands - -1. **Create a DID** - - ```bash - bun run cli create [options] - ``` - - Options: - - `--domain [domain]`: (Required) Domain for the DID - - `--output [file]`: (Optional) Path to save the DID log - - `--portable`: (Optional) Make the DID portable - - `--prerotation`: (Optional) Enable pre-rotation - - `--witness [witness]`: (Optional) Add a witness (can be used multiple times) - - `--witness-threshold [n]`: (Optional) Set witness threshold - - Example: - ```bash - bun run cli create --domain example.com --portable --witness did:tdw:QmWitness1:example.com --witness did:tdw:QmWitness2...:example.com - ``` - -2. **Resolve a DID** - - ```bash - bun run cli resolve --did [did] - ``` - - Example: - ```bash - bun run cli resolve --did did:tdw:Qm...:example.com - ``` - -3. **Update a DID** - - ```bash - bun run cli update [options] - ``` +**Key Options:** +- `--domain`: (Required) Host domain for the DID +- `--output`: Save location for DID log +- `--portable`: Enable domain portability +- `--prerotation`: Enable key pre-rotation security +- `--witness`: Add witness DIDs (repeatable) +- `--witness-threshold`: Set minimum witness count +- `--next-key-hash`: Add pre-rotation key hashes (required with --prerotation) - Options: - - `--log [file]`: (Required) Path to the DID log file - - `--output [file]`: (Optional) Path to save the updated DID log - - `--prerotation`: (Optional) Enable pre-rotation - - `--witness [witness]`: (Optional) Add a witness (can be used multiple times) - - `--witness-threshold [n]`: (Optional) Set witness threshold - - `--service [service]`: (Optional) Add a service (format: type,endpoint) - - `--add-vm [type]`: (Optional) Add a verification method - - `--also-known-as [alias]`: (Optional) Add an alsoKnownAs alias - - Example: - ```bash - bun run cli update --log ./did.jsonl --output ./updated-did.jsonl --add-vm keyAgreement --service LinkedDomains,https://example.com - ``` +#### 2. Resolve a DID +View the current state of a DID: -4. **Deactivate a DID** +```bash +# From DID identifier +bun run cli resolve --did did:tdw:123456:example.com - ```bash - bun run cli deactivate [options] - ``` +# From local log file +bun run cli resolve --log ./did.jsonl +``` - Options: - - `--log [file]`: (Required) Path to the DID log file - - `--output [file]`: (Optional) Path to save the deactivated DID log +#### 3. Update a DID +Modify an existing DID's properties: - Example: - ```bash - bun run cli deactivate --log ./did.jsonl --output ./deactivated-did.jsonl - ``` +```bash +bun run cli update \ + --log ./did.jsonl \ + --output ./updated.jsonl \ + --add-vm keyAgreement \ + --service LinkedDomains,https://example.com \ + --also-known-as did:web:example.com +``` -### Additional Notes +**Update Options:** +- `--log`: (Required) Current DID log path +- `--output`: Updated log save location +- `--add-vm`: Add verification methods: + - authentication + - assertionMethod + - keyAgreement + - capabilityInvocation + - capabilityDelegation +- `--service`: Add services (format: type,endpoint) +- `--also-known-as`: Add alternative identifiers +- `--prerotation`: Enable/update key pre-rotation +- `--witness`: Update witness list +- `--witness-threshold`: Update witness requirements + +#### 4. Deactivate a DID +Permanently deactivate a DID: -- The CLI automatically generates new authentication keys when creating or updating a DID. -- The `--portable` option in the create command allows the DID to be moved to a different domain later. -- The `--prerotation` option enables key pre-rotation, which helps prevent loss of control if an active private key is compromised. -- Witness functionality allows for third-party attestation of DID operations. -- The CLI saves the DID log to a file when the `--output` option is provided. -- For the update and deactivate commands, the existing DID log must be provided using the `--log` option. +```bash +bun run cli deactivate \ + --log ./did.jsonl \ + --output ./deactivated.jsonl +``` diff --git a/bin/tdw b/bin/tdw new file mode 100755 index 0000000..21f618a --- /dev/null +++ b/bin/tdw @@ -0,0 +1,2 @@ +#!/usr/bin/env bun +import "../src/cli.ts"; \ No newline at end of file diff --git a/package.json b/package.json index c6e600c..40bda1c 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ "json-canonicalize": "^1.0.6", "multiformats": "^13.1.0", "nanoid": "^5.0.6" + }, + "bin": { + "tdw": "./bin/tdw" } } From 91f4a9295fb8c89e6fa9a5aa46bc8f0a3fd5f122 Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Tue, 29 Oct 2024 21:05:12 -0700 Subject: [PATCH 3/4] Updated cli binary --- .gitignore | 3 ++- bin/tdw | 8 +++++++- package.json | 9 +++++---- src/cli.ts | 18 ++++++++++-------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 22972d2..1686d49 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ test/logs node_modules bun.lockb claude_chats -*.jsonl \ No newline at end of file +*.jsonl +did.json \ No newline at end of file diff --git a/bin/tdw b/bin/tdw index 21f618a..2de43ee 100755 --- a/bin/tdw +++ b/bin/tdw @@ -1,2 +1,8 @@ #!/usr/bin/env bun -import "../src/cli.ts"; \ No newline at end of file +import { main } from "../src/cli.ts"; + +// Execute the main CLI function +main().catch(error => { + console.error('Fatal error:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/package.json b/package.json index 40bda1c..48c3c73 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "trustdidweb-ts", "module": "src/index.ts", "type": "module", + "version": "0.0.3", "scripts": { "dev": "bun --watch --inspect-wait ./src/resolver.ts", "server": "bun --watch ./src/resolver.ts", @@ -16,16 +17,16 @@ "bun-types": "latest" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.4.5" }, "dependencies": { "@interop/base58-universal": "^1.0.0", - "@noble/curves": "^1.4.2", + "@noble/curves": "^1.6.0", "@noble/ed25519": "^2.1.0", "elysia": "^0.8.17", "json-canonicalize": "^1.0.6", - "multiformats": "^13.1.0", - "nanoid": "^5.0.6" + "multiformats": "^13.3.1", + "nanoid": "^5.0.8" }, "bin": { "tdw": "./bin/tdw" diff --git a/src/cli.ts b/src/cli.ts index a3f721a..31b845a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -97,8 +97,8 @@ export async function handleCreate(args: string[]) { // Write DID document for reference const docPath = output.replace('.jsonl', '.json'); - fs.writeFileSync(docPath, JSON.stringify(doc, null, 2)); - console.log(`DID document written to ${docPath}`); + fs.writeFileSync(docPath, JSON.stringify(doc, null, 2).replace(/did:tdw:([^:]+)/g, 'did:web')); + console.log(`DID WEB document written to ${docPath}`); } else { // If no output specified, print to console console.log('DID Document:', JSON.stringify(doc, null, 2)); @@ -233,6 +233,11 @@ export async function handleUpdate(args: string[]) { if (output) { writeLogToDisk(output, result.log); console.log(`Updated DID log written to ${output}`); + + // Write DID document for reference + const docPath = output.replace('.jsonl', '.json'); + fs.writeFileSync(docPath, JSON.stringify(result.doc, null, 2).replace(/did:tdw:([^:]+)/g, 'did:web')); + console.log(`DID WEB document written to ${docPath}`); } return result; @@ -335,8 +340,8 @@ function parseServices(services: string[]): ServiceEndpoint[] { }); } -// Update the main function to use the handlers -async function main() { +// Update the main function to be exported +export async function main() { const [command, ...args] = process.argv.slice(2); console.log('Command:', command); console.log('Args:', args); @@ -370,13 +375,10 @@ async function main() { } } -// Move the main function to the bottom and only run it if this file is being executed directly +// Only run main if this file is being executed directly if (process.argv[1] === import.meta.path) { main().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } - -// Export main for testing if needed -export { main }; From 8b8f764f747df10e51fa8145d14279feaac48a05 Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Tue, 29 Oct 2024 22:08:11 -0700 Subject: [PATCH 4/4] CLI won't overwrite env file --- package.json | 2 +- src/utils.ts | 33 +++++++++++++++++++++++++++----- test.json | 19 ++++++++++++++++++ test/fixtures/not-authorized.log | 4 ++-- 4 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 test.json diff --git a/package.json b/package.json index 48c3c73..356f58e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trustdidweb-ts", "module": "src/index.ts", "type": "module", - "version": "0.0.3", + "version": "0.0.4", "scripts": { "dev": "bun --watch --inspect-wait ./src/resolver.ts", "server": "bun --watch ./src/resolver.ts", diff --git a/src/utils.ts b/src/utils.ts index bec8c51..76ac9d4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,24 +41,47 @@ export const writeVerificationMethodToEnv = (verificationMethod: VerificationMet }; try { + // Read existing .env content + let envContent = ''; let existingData: any[] = []; + if (fs.existsSync(envFilePath)) { - const envContent = fs.readFileSync(envFilePath, 'utf8'); + envContent = fs.readFileSync(envFilePath, 'utf8'); const match = envContent.match(/DID_VERIFICATION_METHODS=(.*)/); if (match && match[1]) { const decodedData = Buffer.from(match[1], 'base64').toString('utf8'); existingData = JSON.parse(decodedData); + + // Check if verification method with same ID already exists + const existingIndex = existingData.findIndex(vm => vm.id === vmData.id); + if (existingIndex !== -1) { + // Update existing verification method + existingData[existingIndex] = vmData; + } else { + // Add new verification method + existingData.push(vmData); + } + } else { + // No existing verification methods, create new array + existingData = [vmData]; } + } else { + // No .env file exists, create new array + existingData = [vmData]; } - - existingData.push(vmData); const jsonData = JSON.stringify(existingData); const encodedData = Buffer.from(jsonData).toString('base64'); - const envContent = `DID_VERIFICATION_METHODS=${encodedData}\n`; + // If DID_VERIFICATION_METHODS already exists, replace it + if (envContent.includes('DID_VERIFICATION_METHODS=')) { + envContent = envContent.replace(/DID_VERIFICATION_METHODS=.*\n?/, `DID_VERIFICATION_METHODS=${encodedData}\n`); + } else { + // Otherwise append it + envContent += `DID_VERIFICATION_METHODS=${encodedData}\n`; + } - fs.writeFileSync(envFilePath, envContent); + fs.writeFileSync(envFilePath, envContent.trim() + '\n'); console.log('Verification method written to .env file successfully.'); } catch (error) { console.error('Error writing verification method to .env file:', error); diff --git a/test.json b/test.json new file mode 100644 index 0000000..2ef4c8a --- /dev/null +++ b/test.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1" + ], + "id": "did:web:example.com", + "controller": "did:web:example.com", + "assertionMethod": [ + "did:web:example.com#kXMjmXxS" + ], + "verificationMethod": [ + { + "id": "did:web:example.com#kXMjmXxS", + "controller": "did:web:example.com", + "type": "Multikey", + "publicKeyMultibase": "z6MkkpR5RBSDQ6UQbBua6McJ9XTysJgbC3HfSNGRkXMjmXxS" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/not-authorized.log b/test/fixtures/not-authorized.log index 537ebef..165ba24 100644 --- a/test/fixtures/not-authorized.log +++ b/test/fixtures/not-authorized.log @@ -1,2 +1,2 @@ -{"versionId":"1-QmVAR3nCjqo3EMtMoneARLsQyGPzGnzCLaffEiMB82EnhR","versionTime":"2024-10-29T18:09:38Z","parameters":{"method":"did:tdw:0.4","scid":"QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m","updateKeys":["z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","assertionMethod":["did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#95h4AW9z","did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#53DwaRB9"],"verificationMethod":[{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#95h4AW9z","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z"},{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#53DwaRB9","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6MkjbVgHJM3tiKdbNFVEwKMudBiWHAPMkedMN8753DwaRB9"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z#z6MkgrX82PNSda1w1vnTdhwB3WJGs1W4xF5ZpFdD95h4AW9z","created":"2024-10-29T18:09:38Z","proofPurpose":"assertionMethod","proofValue":"zVZaCEz6PwmSZuU9vbXGA8ZAMvBKB9qDhac3CtGeyGyY2AmuRiMNxxoEdfw4dxfKEAWdHpHSoKLeMsMv2wTor1Uc"}]} -{"versionId":"2-QmPda74e1FZtwv9QgpRzSDMGqywtdUVRTMnHsRcwDumgWH","versionTime":"2024-10-29T18:09:38Z","parameters":{"witnesses":[],"witnessThreshold":0},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","controller":["did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com"],"assertionMethod":["did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#mfG6KeH9","did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#nVdR2AUv"],"verificationMethod":[{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#mfG6KeH9","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6MkiHEEvrBizECqja9Ews96qkaozd7tPiWHVHVymfG6KeH9"},{"id":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com#nVdR2AUv","controller":"did:tdw:QmfAHZbY7SraPR1DUBX5seVaZqudr5Gs2WZ4YYLAGB1v6m:example.com","type":"Multikey","publicKeyMultibase":"z6Mkea9JYHgb9mwCVBA5wpxPYUE2fsVJ9tVsb3iunVdR2AUv"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjbVgHJM3tiKdbNFVEwKMudBiWHAPMkedMN8753DwaRB9#z6MkjbVgHJM3tiKdbNFVEwKMudBiWHAPMkedMN8753DwaRB9","created":"2024-10-29T18:09:38Z","proofPurpose":"assertionMethod","proofValue":"z4rEJBnJrBMTmU645eGMwH2AVC6VC2muvokTSXkCPqhowCvSnvcJGtu7s9HEnKUukaD3o8YB3unGpFcTcoGHWMcFy"}]} +{"versionId":"1-QmQgTR8Z2WKMDT9DtZumv3pqgqrJvztQCpQbPyoJjmeN9U","versionTime":"2024-10-30T05:06:52Z","parameters":{"method":"did:tdw:0.4","scid":"QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV","updateKeys":["z6MknEocAB9dkGC86HNQq1ArrmB8vHTBRJvCkdJHDK69RYC2"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com","controller":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com","assertionMethod":["did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#DK69RYC2","did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#f9zpvwPW"],"verificationMethod":[{"id":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#DK69RYC2","controller":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com","type":"Multikey","publicKeyMultibase":"z6MknEocAB9dkGC86HNQq1ArrmB8vHTBRJvCkdJHDK69RYC2"},{"id":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#f9zpvwPW","controller":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com","type":"Multikey","publicKeyMultibase":"z6MkfHw8qiYm1KkUKRPBARH3At9Ht9J9rG15U3vtf9zpvwPW"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MknEocAB9dkGC86HNQq1ArrmB8vHTBRJvCkdJHDK69RYC2#z6MknEocAB9dkGC86HNQq1ArrmB8vHTBRJvCkdJHDK69RYC2","created":"2024-10-30T05:06:52Z","proofPurpose":"assertionMethod","proofValue":"z3bkYzMncn8iYsRyXVBUXfQVzcvHjK8R8EQTXpbSBdUWkt3iYN9bbNRKD1gvkcx9rpkVSLm3jwAyHHvEAj5mrgNsN"}]} +{"versionId":"2-QmfHZjH9odDmfJtHP61yQrMnuErgjqxWhh2zBHh7BMWBaw","versionTime":"2024-10-30T05:06:52Z","parameters":{"witnesses":[],"witnessThreshold":0},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com","controller":["did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com"],"assertionMethod":["did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#ThrPAi95","did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#1BdKKEBW"],"verificationMethod":[{"id":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#ThrPAi95","controller":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com","type":"Multikey","publicKeyMultibase":"z6MkhPk8FcJNCWKDrCL62zDHWpgNCNirfBNww8GVThrPAi95"},{"id":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com#1BdKKEBW","controller":"did:tdw:QmcFHtZkEyBYP8d5a9KGcu1XHvYkit5bWCb7zAsVz3R2aV:example.com","type":"Multikey","publicKeyMultibase":"z6MktScN41PcKVWPPELxWRo6b1FV8sN41AY88wbe1BdKKEBW"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkfHw8qiYm1KkUKRPBARH3At9Ht9J9rG15U3vtf9zpvwPW#z6MkfHw8qiYm1KkUKRPBARH3At9Ht9J9rG15U3vtf9zpvwPW","created":"2024-10-30T05:06:52Z","proofPurpose":"assertionMethod","proofValue":"z2pCUG5fLkTU4Vcibf4aR4SLt7ZuCfAJVKLgGezWcsfdUXv42z7zUQfG1GptVxvp4chaSd1zDh6P5cP96Co8SfVAm"}]}