@interop/did-method-webvh 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ import { concatBuffers } from './utils/buffer.js';
2
+ import { canonicalizeStrict } from './utils/canonicalize.js';
3
+ import { createHash } from './utils/crypto.js';
4
+ import { multibaseDecode } from './utils/multiformats.js';
5
+ import { fetchWitnessProofs, parseDidKeyDid, parseDidKeyVerificationMethod, resolveVM } from './utils.js';
6
+ function createWitnessProofSigner(signer) {
7
+ return async (document, proofTemplate) => {
8
+ if (!proofTemplate) {
9
+ throw new Error('Witness proof template is required');
10
+ }
11
+ const signed = await signer.sign({
12
+ document,
13
+ proof: proofTemplate,
14
+ });
15
+ return {
16
+ proof: {
17
+ verificationMethod: proofTemplate.verificationMethod,
18
+ proofValue: signed.proofValue,
19
+ },
20
+ };
21
+ };
22
+ }
23
+ /**
24
+ * Creates a single witness DataIntegrityProof for one `versionId`.
25
+ *
26
+ * @param signer Proof signer callback.
27
+ * @param versionId Target DID log version id.
28
+ * @param verificationMethod Witness verification method DID URL.
29
+ * @param created Optional proof creation time in ISO format.
30
+ * @returns A complete DataIntegrityProof for did-witness processing.
31
+ */
32
+ export async function createWitnessProof(signer, versionId, verificationMethod, created = new Date().toISOString()) {
33
+ const proofTemplate = {
34
+ type: 'DataIntegrityProof',
35
+ cryptosuite: 'eddsa-jcs-2022',
36
+ verificationMethod,
37
+ created,
38
+ proofPurpose: 'assertionMethod',
39
+ };
40
+ const signedData = await signer({ versionId }, proofTemplate);
41
+ const mergedProof = {
42
+ ...proofTemplate,
43
+ ...signedData.proof,
44
+ };
45
+ // Strip undefined fields to keep the proof JSON-compatible.
46
+ const sanitizedProof = JSON.parse(JSON.stringify(mergedProof));
47
+ if (!sanitizedProof.verificationMethod) {
48
+ throw new Error('Witness proof is missing verificationMethod');
49
+ }
50
+ if (!sanitizedProof.proofValue) {
51
+ throw new Error('Witness proof is missing proofValue');
52
+ }
53
+ return {
54
+ id: sanitizedProof.id,
55
+ type: sanitizedProof.type ?? proofTemplate.type,
56
+ cryptosuite: sanitizedProof.cryptosuite ?? proofTemplate.cryptosuite,
57
+ verificationMethod: sanitizedProof.verificationMethod,
58
+ created: sanitizedProof.created ?? proofTemplate.created,
59
+ proofValue: sanitizedProof.proofValue,
60
+ proofPurpose: sanitizedProof.proofPurpose ?? proofTemplate.proofPurpose,
61
+ };
62
+ }
63
+ /**
64
+ * Signs one did-witness proof entry for a single target `versionId`.
65
+ *
66
+ * The signer map is keyed by witness DID (`did:key:...`).
67
+ *
68
+ * @param options Witness signing options for one target version.
69
+ * @returns A witness proof file entry for the target version.
70
+ */
71
+ export async function signWitnessProofEntry(options) {
72
+ if (!options.versionId) {
73
+ throw new Error('versionId is required');
74
+ }
75
+ const witnessCount = options.witnesses.length;
76
+ if (witnessCount === 0) {
77
+ throw new Error('Witness list cannot be empty');
78
+ }
79
+ const proofs = await Promise.all(options.witnesses.map(async (witness) => {
80
+ const { did } = parseDidKeyDid(witness.id);
81
+ const signer = options.witnessSignersByDid[did];
82
+ if (!signer) {
83
+ throw new Error(`Missing witness signer for ${did}`);
84
+ }
85
+ const verificationMethod = signer.getVerificationMethodId();
86
+ const parsedVerificationMethod = parseDidKeyVerificationMethod(verificationMethod);
87
+ if (parsedVerificationMethod.did !== did) {
88
+ throw new Error(`Witness signer verificationMethod DID does not match witness id: ${did}`);
89
+ }
90
+ return createWitnessProof(createWitnessProofSigner(signer), options.versionId, verificationMethod, options.created);
91
+ }));
92
+ return {
93
+ versionId: options.versionId,
94
+ proof: proofs,
95
+ };
96
+ }
97
+ /**
98
+ * Signs did-witness proof entries for multiple target `versionId`s.
99
+ *
100
+ * @param versionIds Target DID log version ids.
101
+ * @param witnesses Witness DID entries used to sign.
102
+ * @param witnessSignersByDid Signer map keyed by witness did:key DID.
103
+ * @param created Optional proof creation time in ISO format.
104
+ * @returns A witness proof file entry per version id.
105
+ */
106
+ export async function signWitnessProofEntries(versionIds, witnesses, witnessSignersByDid, created) {
107
+ return Promise.all(versionIds.map((versionId) => signWitnessProofEntry({
108
+ versionId,
109
+ witnesses,
110
+ witnessSignersByDid,
111
+ created,
112
+ })));
113
+ }
114
+ export function validateWitnessParameter(witness) {
115
+ if (!witness.witnesses || !Array.isArray(witness.witnesses) || witness.witnesses.length === 0) {
116
+ throw new Error('Witness list cannot be empty');
117
+ }
118
+ if (!witness.threshold ||
119
+ parseInt(witness.threshold.toString(), 10) < 1 ||
120
+ parseInt(witness.threshold.toString(), 10) > witness.witnesses.length) {
121
+ throw new Error('Witness threshold must be between 1 and the number of witnesses');
122
+ }
123
+ const ids = new Set();
124
+ for (const w of witness.witnesses) {
125
+ const parsedDid = (() => {
126
+ try {
127
+ return parseDidKeyDid(w.id);
128
+ }
129
+ catch {
130
+ throw new Error('Witness DIDs must be did:key format');
131
+ }
132
+ })();
133
+ if (ids.has(parsedDid.did)) {
134
+ throw new Error(`Duplicate witness id: ${w.id}`);
135
+ }
136
+ ids.add(parsedDid.did);
137
+ }
138
+ }
139
+ export function countWitnessApprovals(proofs, witnesses) {
140
+ const processed = new Set();
141
+ const witnessesByDid = new Map(witnesses.map((witness) => {
142
+ const parsedDid = parseDidKeyDid(witness.id);
143
+ return [parsedDid.did, witness];
144
+ }));
145
+ for (const proof of proofs) {
146
+ const parsedVerificationMethod = parseDidKeyVerificationMethod(proof.verificationMethod);
147
+ const witness = witnessesByDid.get(parsedVerificationMethod.did);
148
+ if (witness) {
149
+ if (proof.cryptosuite !== 'eddsa-jcs-2022') {
150
+ throw new Error('Invalid witness proof cryptosuite');
151
+ }
152
+ processed.add(witness.id);
153
+ }
154
+ }
155
+ return processed.size;
156
+ }
157
+ export async function countVerifiedWitnessApprovals(logEntry, witnessProofs, currentWitness, verifier) {
158
+ if (!verifier) {
159
+ throw new Error('Verifier implementation is required');
160
+ }
161
+ let approvals = 0;
162
+ const processedWitnesses = new Set();
163
+ const witnessesByDid = new Map((currentWitness.witnesses ?? []).map((witness) => {
164
+ const parsedDid = parseDidKeyDid(witness.id);
165
+ return [parsedDid.did, witness];
166
+ }));
167
+ for (const proofSet of witnessProofs) {
168
+ for (const proof of proofSet.proof) {
169
+ try {
170
+ if (proof.type !== 'DataIntegrityProof') {
171
+ throw new Error('Invalid witness proof type');
172
+ }
173
+ if (proof.proofPurpose !== 'assertionMethod') {
174
+ throw new Error('Invalid witness proof purpose');
175
+ }
176
+ if (proof.cryptosuite !== 'eddsa-jcs-2022') {
177
+ throw new Error('Invalid witness proof cryptosuite');
178
+ }
179
+ const parsedVerificationMethod = parseDidKeyVerificationMethod(proof.verificationMethod);
180
+ const witness = witnessesByDid.get(parsedVerificationMethod.did);
181
+ if (!witness || processedWitnesses.has(witness.id)) {
182
+ continue;
183
+ }
184
+ const vm = await resolveVM(proof.verificationMethod);
185
+ if (!vm?.publicKeyMultibase) {
186
+ throw new Error(`Verification Method ${proof.verificationMethod} not found`);
187
+ }
188
+ const publicKey = multibaseDecode(vm.publicKeyMultibase).bytes;
189
+ if (publicKey.length !== 34) {
190
+ throw new Error(`Invalid public key length ${publicKey.length} (should be 34 bytes)`);
191
+ }
192
+ const { proofValue, ...proofWithoutValue } = proof;
193
+ // Create hashes
194
+ const canonicalizedData = canonicalizeStrict({ versionId: logEntry.versionId });
195
+ const canonicalizedProof = canonicalizeStrict(proofWithoutValue);
196
+ const dataHash = await createHash(canonicalizedData);
197
+ const proofHash = await createHash(canonicalizedProof);
198
+ const input = concatBuffers(proofHash, dataHash);
199
+ const signature = multibaseDecode(proofValue).bytes;
200
+ const verified = await verifier.verify(signature, input, publicKey.slice(2));
201
+ if (!verified) {
202
+ throw new Error('Invalid witness proof signature');
203
+ }
204
+ approvals++;
205
+ processedWitnesses.add(witness.id);
206
+ }
207
+ catch (error) {
208
+ const message = error instanceof Error ? error.message : String(error);
209
+ console.warn(`Ignoring invalid witness proof for version ${proofSet.versionId} ` +
210
+ `(verificationMethod: ${proof.verificationMethod}): ${message}`);
211
+ }
212
+ }
213
+ }
214
+ return approvals;
215
+ }
216
+ export { fetchWitnessProofs };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@interop/did-method-webvh",
3
+ "type": "module",
4
+ "version": "3.0.0",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/interop-alliance/did-method-webvh.git"
9
+ },
10
+ "module": "dist/index.js",
11
+ "browser": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "files": [
14
+ "dist/**/*",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
20
+ "packageManager": "pnpm@11.3.0",
21
+ "scripts": {
22
+ "dev": "tsx watch examples/express-resolver.ts",
23
+ "lint": "biome check .",
24
+ "lint:fix": "biome check --write .",
25
+ "format": "biome format .",
26
+ "format:fix": "biome format --write .",
27
+ "test": "rm -rf ./test/logs && NODE_ENV=test vitest run",
28
+ "test:watch": "rm -rf ./test/logs && NODE_ENV=test vitest",
29
+ "test:bail": "rm -rf ./test/logs && NODE_ENV=test vitest --bail=1",
30
+ "test:coverage": "rm -rf ./test/logs && NODE_ENV=test vitest run --coverage",
31
+ "test:log": "mkdir -p ./test/logs && rm -rf ./test/logs/* && LOG_RESOLVES=true NODE_ENV=test vitest run > ./test/logs/test-run.txt 2>&1",
32
+ "cli": "tsx src/cli.ts",
33
+ "build": "npm run build:clean && tsc",
34
+ "build:clean": "rm -rf dist",
35
+ "check": "tsc --noEmit --project tsconfig.dev.json",
36
+ "prepublishOnly": "npm run build",
37
+ "example:signer": "tsx examples/signer.ts"
38
+ },
39
+ "devDependencies": {
40
+ "@biomejs/biome": "^2.3.6",
41
+ "@types/node": "^22.10.0",
42
+ "@vitest/coverage-v8": "^4.1.8",
43
+ "tsx": "^4.19.0",
44
+ "typescript": "^5.7.0",
45
+ "vitest": "^4.0.0"
46
+ },
47
+ "dependencies": {
48
+ "@noble/hashes": "^2.2.0",
49
+ "@stablelib/ed25519": "^2.1.0",
50
+ "json-canonicalize": "^2.0.0"
51
+ },
52
+ "bin": {
53
+ "didwebvh": "./dist/cli.js"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "exports": {
59
+ ".": {
60
+ "types": "./dist/index.d.ts",
61
+ "react-native": "./dist/index.js",
62
+ "import": "./dist/index.js",
63
+ "default": "./dist/index.js"
64
+ },
65
+ "./types": {
66
+ "types": "./dist/types.d.ts",
67
+ "default": "./dist/types.js"
68
+ }
69
+ }
70
+ }