@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.
package/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # `@interop/did-method-webvh`
2
+
3
+ [![CI](https://github.com/interop-alliance/did-method-webvh/actions/workflows/ci.yml/badge.svg)](https://github.com/interop-alliance/did-method-webvh/actions/workflows/ci.yml)
4
+
5
+ `@interop/did-method-webvh` provides developers with a comprehensive library for working with
6
+ Decentralized Identifiers (DIDs) following the `did:webvh` method specification.
7
+ This Typescript-based toolkit is designed to facilitate the integration and
8
+ management of DIDs within web applications, enabling secure identity
9
+ verification and authentication processes. It includes functions for creating,
10
+ resolving, updating and deactivating DIDs by managing DID documents. The package
11
+ is built to ensure compatibility with the latest web development standards,
12
+ offering a straightforward API that makes it easy to implement DID-based
13
+ features in a variety of projects.
14
+
15
+ ## Summary
16
+
17
+ The `@interop/did-method-webvh` implementation of the [
18
+ `did:webvh`]('https://identity.foundation/didwebvh/') specification aims to be
19
+ compatible with the `did:webvh` v1.0 specification.
20
+
21
+ ## Examples
22
+
23
+ The `examples` directory contains sample code demonstrating how to use the
24
+ library:
25
+
26
+ - **Resolver Example**: `examples/express-resolver.ts` (`npm run dev`)
27
+ demonstrates how to implement a DID resolver with Express.js. See
28
+ the [Examples README](./examples/README.md) for more information.
29
+ - **Signer Example**: The `examples/signer.ts` (`npm run example:signer`) file
30
+ demonstrates how to implement a custom signer using `AbstractCrypto`.
31
+
32
+ ## Prerequisites
33
+
34
+ Node.js >= 20.19
35
+
36
+ ## Install dependencies
37
+
38
+ ```bash
39
+ npm install
40
+ ```
41
+
42
+ ## Local development setup
43
+
44
+ When running the examples from the source checkout, the `@interop/did-method-webvh` package
45
+ name resolves to your local build output via the `file:..` dependency in
46
+ `examples/package.json`. Run the following once per clone:
47
+
48
+ ```bash
49
+ npm run build # generate the dist/ artifacts
50
+ cd examples
51
+ npm install # link the examples to the local build
52
+ cd ..
53
+ ```
54
+
55
+ After that, you can start the resolver example:
56
+
57
+ ```bash
58
+ npm run dev
59
+ ```
60
+
61
+ If you ever need to refresh the build (for example after local code changes),
62
+ rerun `npm run build`.
63
+
64
+ ## Available Commands
65
+
66
+ The following commands are defined in the `package.json` file:
67
+
68
+ 1. `dev`: Run the Express resolver example in watch mode for development.
69
+
70
+ ```bash
71
+ npm run dev
72
+ ```
73
+
74
+ This command runs: `tsx watch examples/express-resolver.ts`
75
+
76
+ 1. `test`: Run all tests.
77
+
78
+ ```bash
79
+ npm test
80
+ ```
81
+
82
+ 2. `test:watch`: Run tests in watch mode.
83
+
84
+ ```bash
85
+ npm run test:watch
86
+ ```
87
+
88
+ 3. `test:bail`: Run tests, stopping on the first failure.
89
+
90
+ ```bash
91
+ npm run test:bail
92
+ ```
93
+
94
+ 4. `test:log`: Run tests and save logs to a file.
95
+
96
+ ```bash
97
+ npm run test:log
98
+ ```
99
+
100
+ 5. `cli`: Run the CLI tool.
101
+
102
+ ```bash
103
+ npm run cli
104
+ ```
105
+
106
+ The CLI accepts a `--watcher` option during create and update operations to
107
+ specify one or more watcher URLs.
108
+
109
+ 6. `build`: Build the package.
110
+
111
+ ```bash
112
+ npm run build
113
+ ```
114
+
115
+ 7. `build:clean`: Clean the build directory.
116
+
117
+ ```bash
118
+ npm run build:clean
119
+ ```
120
+
121
+ ## Releasing
122
+
123
+ Publishing is **fully automated** and happens **only** when a maintainer
124
+ publishes a GitHub Release.
125
+
126
+ - **Who can publish**: GitHub users with **write**, **maintain**, or **admin**
127
+ permission on this repo.
128
+ - **Required tag format**: `vMAJOR.MINOR.PATCH` (for example `v2.7.5`).
129
+ - **Required semver bump**: the tag must be a **single** major/minor/patch
130
+ increment over the latest existing `v*` tag.
131
+
132
+ ### How to cut a release
133
+
134
+ 1. In GitHub, go to **Releases** → **Draft a new release**
135
+ 2. Set **Tag** to the next version, e.g. `v2.7.5`
136
+ 3. Choose the target branch/commit (typically `main`)
137
+ 4. Click **Publish release**
138
+
139
+ That will trigger the publish workflow, which will:
140
+
141
+ - validate the tag + your repo permission
142
+ - set `package.json` version from the tag (without the leading `v`)
143
+ - run `npm test` and `npm run build`
144
+ - publish to npm
145
+
146
+ ### npm authentication
147
+
148
+ Publishing
149
+ uses [npm OIDC trusted publishing](https://docs.npmjs.com/trusted-publishers) —
150
+ the workflow exchanges its GitHub Actions OIDC token for a short-lived npm
151
+ publish token at publish time. No static `NPM_TOKEN` is required.
152
+
153
+ For this to work, the `@interop/did-method-webvh` package on npmjs.com must have a Trusted
154
+ Publisher configured pointing at this repository and the
155
+ `.github/workflows/publish.yml` workflow.
156
+
157
+ ### Troubleshooting
158
+
159
+ - **Tag rejected**: make sure it matches `vX.Y.Z` and is exactly one
160
+ major/minor/patch bump over the latest `v*` tag.
161
+ - **Permission rejected**: ensure the releasing user has write/maintain/admin
162
+ permission on the GitHub repo.
163
+ - **`EOTP` / OTP required at publish**: the npm token path is being used instead
164
+ of OIDC. Make sure no `NODE_AUTH_TOKEN` is set on the publish step and that
165
+ the workflow has `id-token: write` permission.
166
+ - **OIDC exchange failed**: confirm the Trusted Publisher config on npmjs.com
167
+ matches this repo's owner, name, and workflow file path (
168
+ `.github/workflows/publish.yml`).
169
+
170
+ ## Creating a DID Resolver
171
+
172
+ The `@interop/did-method-webvh` library provides the core functionality for resolving DIDs,
173
+ but it does not include a built-in HTTP resolver. You can create your own
174
+ resolver using your preferred web framework by following these steps:
175
+
176
+ 1. Import the `resolveDID` function from the `@interop/did-method-webvh` library:
177
+
178
+ ```typescript
179
+ import { resolveDID } from '@interop/did-method-webvh';
180
+ ```
181
+
182
+ 2. Create endpoints for resolving DIDs:
183
+
184
+ ```typescript
185
+ // Example using Express
186
+ app.get('/resolve/:id', async (req, res) => {
187
+ try {
188
+ const result = await resolveDID(req.params.id);
189
+ res.json(result);
190
+ } catch (error) {
191
+ res.status(400).json({
192
+ error: 'Resolution failed',
193
+ details: error.message
194
+ });
195
+ }
196
+ });
197
+ ```
198
+
199
+ 3. Implement file retrieval logic for DID documents and associated resources.
200
+
201
+ For complete examples, see the [examples](./examples/) directory.
202
+
203
+ ### Resolution metadata notes (v1.0)
204
+
205
+ For `did:webvh:1.0` resolution flows, resolver failures that invalidate the DID
206
+ are surfaced using:
207
+
208
+ - `meta.error = "invalidDid"`
209
+ - `meta.problemDetails` populated with RFC9457-style fields (`type`, `title`,
210
+ `detail`)
211
+
212
+ Absence cases (for example missing DID log or missing DID URL resource) use:
213
+
214
+ - `meta.error = "notFound"`
215
+
216
+ When resolving a requested earlier version (for example with `versionId`,
217
+ `versionNumber`, or `versionTime`), the resolver may return a valid earlier
218
+ document while still reporting `meta.error = "invalidDid"` if a later log entry
219
+ fails verification.
220
+
221
+ ## API Reference
222
+
223
+ ### Core Functions
224
+
225
+ -
226
+ `resolveDID(did: string, options?: ResolutionOptions): Promise<{did: string, doc: any, meta: DIDResolutionMeta, controlled: boolean}>`
227
+ Resolves a DID to its DID document.
228
+ For `v1.0`, `options.fastResolve` is an opt-in mode defaulting to `false` for
229
+ full log parsing.
230
+
231
+ -
232
+ `resolveDIDFromLog(log: DIDLog, options?: ResolutionOptions & { witnessProofs?: WitnessProofFileEntry[] }): Promise<{did: string, doc: any, meta: DIDResolutionMeta}>`
233
+ Resolves directly from an in-memory DID log.
234
+ For `v1.0`, `options.fastResolve` is an opt-in mode defaulting to `false` for
235
+ full log parsing.
236
+
237
+ -
238
+ `createDID(options: CreateDIDInterface): Promise<{did: string, doc: any, meta: DIDResolutionMeta, log: DIDLog, webDoc?: DIDDoc}>`
239
+ Creates a new DID.
240
+ Accepts `address` (`host`, `host:port`, `https://...`, or `did:webvh:...`) or
241
+ legacy `domain`.
242
+ Resolver URL mapping uses `http://localhost` for local testing and `https://`
243
+ for non-local hosts.
244
+ If `alsoKnownAsWeb: true` is supplied, the result also includes `webDoc`, the
245
+ parallel `did:web` DID document to publish as `did.json`.
246
+
247
+ -
248
+ `updateDID(options: UpdateDIDInterface): Promise<{did: string, doc: any, meta: DIDResolutionMeta, log: DIDLog, webDoc?: DIDDoc}>`
249
+ Updates an existing DID.
250
+ Returns `webDoc` when the updated DID document carries a `did:web:` alias in
251
+ `alsoKnownAs`.
252
+
253
+ -
254
+ `deactivateDID(options: DeactivateDIDInterface): Promise<{did: string, doc: any, meta: DIDResolutionMeta, log: DIDLog}>`
255
+ Deactivates an existing DID.
256
+
257
+ - `generateParallelDidWeb(didwebvhDid: string, didwebvhDoc: DIDDoc): DIDDoc`
258
+ Generates the parallel `did:web` document defined by did:webvh v1.0 §3.7.10.
259
+
260
+ ### Witness Functions
261
+
262
+ -
263
+ `createWitnessProof(signer, versionId, verificationMethod, created?): Promise<DataIntegrityProof>`
264
+ Creates and signs one witness proof for a specific `versionId`.
265
+
266
+ -
267
+ `signWitnessProofEntry(options: WitnessSigningOptions): Promise<WitnessSigningResult>`
268
+ Signs one did-witness proof entry (`{ versionId, proof[] }`) for a single target
269
+ version.
270
+
271
+ -
272
+ `signWitnessProofEntries(versionIds: string[], witnesses: WitnessEntry[], witnessSignersByDid: Record<string, WitnessSigner>, created?: string): Promise<WitnessSigningResult[]>`
273
+ Signs did-witness proof entries for multiple target versions.
274
+
275
+ ### Cryptography Functions
276
+
277
+ - `createDocumentSigner(options: SignerOptions): Signer`
278
+ Creates a signer for signing DID documents.
279
+
280
+ - `prepareDataForSigning(data: any): Uint8Array`
281
+ Prepares data for signing.
282
+
283
+ - `createProof(options: SigningInput): Promise<SigningOutput>`
284
+ Creates a proof for a DID document.
285
+
286
+ - `createSigner(options: SignerOptions): Signer`
287
+ Creates a signer for signing data.
288
+
289
+ - `AbstractCrypto`
290
+ An abstract class for implementing custom signers.
291
+
292
+ ## License
293
+
294
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,5 @@
1
+ import type { DIDLogEntry, Verifier, WitnessParameterResolution } from './interfaces.js';
2
+ export declare const documentStateIsValid: (doc: DIDLogEntry, updateKeys: string[], witness: WitnessParameterResolution | undefined | null, skipWitnessVerification?: boolean, verifier?: Verifier) => Promise<boolean>;
3
+ export declare const hashChainValid: (derivedHash: string, logEntryHash: string) => boolean;
4
+ export declare const newKeysAreInNextKeys: (updateKeys: string[], previousNextKeyHashes: string[]) => Promise<boolean>;
5
+ export declare const scidIsFromHash: (scid: string, hash: string) => Promise<boolean>;
@@ -0,0 +1,82 @@
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 { createSCID, deriveNextKeyHash, parseDidKeyVerificationMethod, resolveVM } from './utils.js';
6
+ import { validateWitnessParameter } from './witness.js';
7
+ const isKeyAuthorized = (verificationMethod, updateKeys) => {
8
+ const parsedVerificationMethod = parseDidKeyVerificationMethod(verificationMethod);
9
+ return updateKeys.some((updateKey) => {
10
+ return updateKey === parsedVerificationMethod.keyMultibase;
11
+ });
12
+ };
13
+ export const documentStateIsValid = async (doc, updateKeys, witness, skipWitnessVerification, verifier) => {
14
+ if (!verifier) {
15
+ throw new Error('Verifier implementation is required');
16
+ }
17
+ let { proof: proofs, ...rest } = doc;
18
+ if (!proofs) {
19
+ throw new Error('Missing proof in DID log entry');
20
+ }
21
+ if (!Array.isArray(proofs)) {
22
+ proofs = [proofs];
23
+ }
24
+ if (witness?.witnesses && witness.witnesses.length > 0) {
25
+ if (!skipWitnessVerification) {
26
+ validateWitnessParameter(witness);
27
+ }
28
+ }
29
+ for (let i = 0; i < proofs.length; i++) {
30
+ const proof = proofs[i];
31
+ if (!proof.verificationMethod.startsWith('did:key:')) {
32
+ throw new Error(`Unsupported verification method for DID log entry authorization: ${proof.verificationMethod}`);
33
+ }
34
+ if (!isKeyAuthorized(proof.verificationMethod, updateKeys)) {
35
+ throw new Error(`Key ${proof.verificationMethod} is not authorized to update.`);
36
+ }
37
+ if (proof.type !== 'DataIntegrityProof') {
38
+ throw new Error(`Unknown proof type ${proof.type}`);
39
+ }
40
+ if (proof.proofPurpose !== 'assertionMethod') {
41
+ throw new Error(`Invalid proof purpose '${proof.proofPurpose}' for DID log entry proof. Expected 'assertionMethod'.`);
42
+ }
43
+ if (proof.cryptosuite !== 'eddsa-jcs-2022') {
44
+ throw new Error(`Unknown cryptosuite ${proof.cryptosuite}`);
45
+ }
46
+ const vm = await resolveVM(proof.verificationMethod);
47
+ if (!vm?.publicKeyMultibase) {
48
+ throw new Error(`Verification Method ${proof.verificationMethod} not found`);
49
+ }
50
+ const publicKey = multibaseDecode(vm.publicKeyMultibase).bytes;
51
+ if (publicKey[0] !== 0xed || publicKey[1] !== 0x01) {
52
+ throw new Error(`multiKey doesn't include ed25519 header (0xed01)`);
53
+ }
54
+ const { proofValue, ...restProof } = proof;
55
+ const signature = multibaseDecode(proofValue).bytes;
56
+ const dataHash = await createHash(canonicalizeStrict(rest));
57
+ const proofHash = await createHash(canonicalizeStrict(restProof));
58
+ const input = concatBuffers(proofHash, dataHash);
59
+ const verified = await verifier.verify(signature, input, publicKey.slice(2));
60
+ if (!verified) {
61
+ throw new Error(`Proof ${i} failed verification (proofValue: ${proofValue})`);
62
+ }
63
+ }
64
+ return true;
65
+ };
66
+ export const hashChainValid = (derivedHash, logEntryHash) => {
67
+ return derivedHash === logEntryHash;
68
+ };
69
+ export const newKeysAreInNextKeys = async (updateKeys, previousNextKeyHashes) => {
70
+ if (previousNextKeyHashes.length > 0) {
71
+ for (const key of updateKeys) {
72
+ const keyHash = await deriveNextKeyHash(key);
73
+ if (!previousNextKeyHashes.includes(keyHash)) {
74
+ throw new Error(`Invalid update key ${keyHash}. Not found in nextKeyHashes ${previousNextKeyHashes}`);
75
+ }
76
+ }
77
+ }
78
+ return true;
79
+ };
80
+ export const scidIsFromHash = async (scid, hash) => {
81
+ return scid === (await createSCID(hash));
82
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import type { DIDLog } from './interfaces.js';
3
+ export declare function handleCreate(args: string[]): Promise<{
4
+ did: string;
5
+ doc: import("./interfaces.js").DIDDoc;
6
+ meta: import("./interfaces.js").DIDResolutionMeta;
7
+ log: DIDLog;
8
+ }>;
9
+ export declare function handleResolve(args: string[]): Promise<{
10
+ did: string;
11
+ doc: any;
12
+ meta: import("./interfaces.js").DIDResolutionMeta;
13
+ }>;
14
+ export declare function handleUpdate(args: string[]): Promise<import("./interfaces.js").UpdateDIDResult>;
15
+ export declare function handleDeactivate(args: string[]): Promise<{
16
+ did: string;
17
+ doc: any;
18
+ meta: import("./interfaces.js").DIDResolutionMeta;
19
+ log: DIDLog;
20
+ }>;
21
+ export declare function main(): Promise<void>;