@dwk/vc 0.1.0-beta.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/LICENSE +15 -0
- package/README.md +143 -0
- package/dist/config.d.ts +97 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +62 -0
- package/dist/config.js.map +1 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +139 -0
- package/dist/credential.js.map +1 -0
- package/dist/data-integrity.d.ts +102 -0
- package/dist/data-integrity.d.ts.map +1 -0
- package/dist/data-integrity.js +253 -0
- package/dist/data-integrity.js.map +1 -0
- package/dist/datetime.d.ts +26 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +54 -0
- package/dist/datetime.js.map +1 -0
- package/dist/did-web.d.ts +93 -0
- package/dist/did-web.d.ts.map +1 -0
- package/dist/did-web.js +206 -0
- package/dist/did-web.js.map +1 -0
- package/dist/handler.d.ts +37 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +362 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/jcs.d.ts +31 -0
- package/dist/jcs.d.ts.map +1 -0
- package/dist/jcs.js +67 -0
- package/dist/jcs.js.map +1 -0
- package/dist/log.d.ts +34 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +32 -0
- package/dist/log.js.map +1 -0
- package/dist/multibase.d.ts +57 -0
- package/dist/multibase.d.ts.map +1 -0
- package/dist/multibase.js +165 -0
- package/dist/multibase.js.map +1 -0
- package/dist/status-list.d.ts +116 -0
- package/dist/status-list.d.ts.map +1 -0
- package/dist/status-list.js +241 -0
- package/dist/status-list.js.map +1 -0
- package/package.json +48 -0
- package/src/config.ts +158 -0
- package/src/credential.ts +188 -0
- package/src/data-integrity.ts +425 -0
- package/src/datetime.ts +57 -0
- package/src/did-web.ts +273 -0
- package/src/handler.ts +477 -0
- package/src/index.ts +133 -0
- package/src/jcs.ts +83 -0
- package/src/log.ts +35 -0
- package/src/multibase.ts +189 -0
- package/src/status-list.ts +356 -0
package/src/did-web.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `did:web` ↔ URL mapping, DID-document construction, and verification-method
|
|
3
|
+
* resolution.
|
|
4
|
+
*
|
|
5
|
+
* The DID document itself (`/.well-known/did.json`) is a static artifact a static
|
|
6
|
+
* host can serve — {@link buildDidDocument} produces it — so this package does
|
|
7
|
+
* not need a Worker to *resolve* a DID. The Worker side uses
|
|
8
|
+
* {@link createDidWebResolver} during VC verification to fetch a credential
|
|
9
|
+
* issuer's DID document and locate the verification key referenced by a proof.
|
|
10
|
+
*
|
|
11
|
+
* Method/URL mapping follows the did:web rules: the first colon-separated
|
|
12
|
+
* component is the host (with a `%3A`-encoded port), remaining components are
|
|
13
|
+
* path segments, and a bare host resolves under `/.well-known/`.
|
|
14
|
+
*
|
|
15
|
+
* @see https://w3c-ccg.github.io/did-method-web/
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { JsonObject, VerificationMethod } from "./data-integrity";
|
|
19
|
+
|
|
20
|
+
const DID_WEB_PREFIX = "did:web:";
|
|
21
|
+
|
|
22
|
+
/** The default did:web context documents emitted by {@link buildDidDocument}. */
|
|
23
|
+
export const DID_CONTEXT_V1 = "https://www.w3.org/ns/did/v1";
|
|
24
|
+
export const MULTIKEY_CONTEXT_V1 = "https://w3id.org/security/multikey/v1";
|
|
25
|
+
export const JWK_CONTEXT_V1 = "https://w3id.org/security/jwk/v1";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert a `did:web` identifier to the URL of its DID document. Throws if the
|
|
29
|
+
* input is not a `did:web` identifier.
|
|
30
|
+
*/
|
|
31
|
+
export function didWebToUrl(did: string): string {
|
|
32
|
+
if (!did.startsWith(DID_WEB_PREFIX)) {
|
|
33
|
+
throw new Error(`@dwk/vc: "${did}" is not a did:web identifier`);
|
|
34
|
+
}
|
|
35
|
+
const components = did.slice(DID_WEB_PREFIX.length).split(":");
|
|
36
|
+
const host = components[0];
|
|
37
|
+
if (host === undefined || host.length === 0) {
|
|
38
|
+
throw new Error(`@dwk/vc: did:web identifier "${did}" has no host`);
|
|
39
|
+
}
|
|
40
|
+
const authority = decodeURIComponent(host);
|
|
41
|
+
const segments = components.slice(1).map((segment) => {
|
|
42
|
+
if (segment.length === 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`@dwk/vc: did:web identifier "${did}" has an empty path segment`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const decoded = decodeURIComponent(segment);
|
|
48
|
+
// Reject `.`/`..` so a crafted identifier cannot traverse out of its path
|
|
49
|
+
// when the URL is built and fetched.
|
|
50
|
+
if (decoded === "." || decoded === "..") {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`@dwk/vc: did:web identifier "${did}" has an invalid path segment "${decoded}"`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return decoded;
|
|
56
|
+
});
|
|
57
|
+
const path =
|
|
58
|
+
segments.length === 0
|
|
59
|
+
? "/.well-known/did.json"
|
|
60
|
+
: `/${segments.join("/")}/did.json`;
|
|
61
|
+
return `https://${authority}${path}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Derive the `did:web` identifier a URL (or host) would publish. A bare origin
|
|
66
|
+
* (or one whose path is `/.well-known/did.json`) maps to `did:web:<host>`; a
|
|
67
|
+
* deeper path maps its segments to colon-separated components.
|
|
68
|
+
*/
|
|
69
|
+
export function urlToDidWeb(input: string): string {
|
|
70
|
+
const url = new URL(input.includes("://") ? input : `https://${input}`);
|
|
71
|
+
const host = url.host; // includes a non-default port
|
|
72
|
+
const encodedHost = host.replace(/:/g, "%3A");
|
|
73
|
+
|
|
74
|
+
let pathname = url.pathname;
|
|
75
|
+
if (pathname.endsWith("/did.json")) {
|
|
76
|
+
pathname = pathname.slice(0, -"/did.json".length);
|
|
77
|
+
}
|
|
78
|
+
const segments = pathname.split("/").filter((segment) => segment.length > 0);
|
|
79
|
+
if (
|
|
80
|
+
segments.length === 0 ||
|
|
81
|
+
(segments.length === 1 && segments[0] === ".well-known")
|
|
82
|
+
) {
|
|
83
|
+
return `${DID_WEB_PREFIX}${encodedHost}`;
|
|
84
|
+
}
|
|
85
|
+
const encoded = segments.map((segment) => encodeURIComponent(segment));
|
|
86
|
+
return `${DID_WEB_PREFIX}${encodedHost}:${encoded.join(":")}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Input describing one verification method to publish in a DID document. */
|
|
90
|
+
export interface VerificationMethodInput {
|
|
91
|
+
/** Method id. A bare fragment (`#key-0`) is resolved against the DID. */
|
|
92
|
+
readonly id: string;
|
|
93
|
+
/** Method type. Defaults to `"Multikey"` (or `"JsonWebKey"` for a JWK). */
|
|
94
|
+
readonly type?: string;
|
|
95
|
+
readonly publicKeyMultibase?: string;
|
|
96
|
+
readonly publicKeyJwk?: JsonWebKey;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Verification relationships a method can be referenced by. */
|
|
100
|
+
export interface VerificationRelationships {
|
|
101
|
+
/** Reference under `assertionMethod` (issuing credentials). Default `true`. */
|
|
102
|
+
readonly assertionMethod?: boolean;
|
|
103
|
+
/** Reference under `authentication`. Default `false`. */
|
|
104
|
+
readonly authentication?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Options for {@link buildDidDocument}. */
|
|
108
|
+
export interface BuildDidDocumentOptions {
|
|
109
|
+
readonly did: string;
|
|
110
|
+
readonly verificationMethods: readonly VerificationMethodInput[];
|
|
111
|
+
/** Relationships applied to every supplied method. */
|
|
112
|
+
readonly relationships?: VerificationRelationships;
|
|
113
|
+
/** Optional `alsoKnownAs` identifiers (e.g. an `https:` WebID). */
|
|
114
|
+
readonly alsoKnownAs?: readonly string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function absoluteId(did: string, id: string): string {
|
|
118
|
+
return id.startsWith("#") ? `${did}${id}` : id;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build a `did:web` DID document from public verification methods. Emits the
|
|
123
|
+
* DID, Multikey, and (when a JWK method is present) JWK context documents, the
|
|
124
|
+
* `verificationMethod` array, and the requested verification relationships
|
|
125
|
+
* (defaulting to `assertionMethod`). Suitable for serialization to
|
|
126
|
+
* `/.well-known/did.json`.
|
|
127
|
+
*/
|
|
128
|
+
export function buildDidDocument(options: BuildDidDocumentOptions): JsonObject {
|
|
129
|
+
const { did } = options;
|
|
130
|
+
if (!did.startsWith(DID_WEB_PREFIX)) {
|
|
131
|
+
throw new Error(`@dwk/vc: "${did}" is not a did:web identifier`);
|
|
132
|
+
}
|
|
133
|
+
if (options.verificationMethods.length === 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"@dwk/vc: a DID document needs at least one verification method",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let hasJwk = false;
|
|
140
|
+
const methods = options.verificationMethods.map((input) => {
|
|
141
|
+
const id = absoluteId(did, input.id);
|
|
142
|
+
const method: JsonObject = {
|
|
143
|
+
id,
|
|
144
|
+
controller: did,
|
|
145
|
+
};
|
|
146
|
+
if (input.publicKeyMultibase !== undefined) {
|
|
147
|
+
method.type = input.type ?? "Multikey";
|
|
148
|
+
method.publicKeyMultibase = input.publicKeyMultibase;
|
|
149
|
+
} else if (input.publicKeyJwk !== undefined) {
|
|
150
|
+
hasJwk = true;
|
|
151
|
+
method.type = input.type ?? "JsonWebKey";
|
|
152
|
+
method.publicKeyJwk = input.publicKeyJwk as unknown as JsonObject;
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`@dwk/vc: verification method "${id}" needs publicKeyMultibase or publicKeyJwk`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return { id, method };
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const context: string[] = [DID_CONTEXT_V1, MULTIKEY_CONTEXT_V1];
|
|
162
|
+
if (hasJwk) context.push(JWK_CONTEXT_V1);
|
|
163
|
+
|
|
164
|
+
const relationships = options.relationships ?? {};
|
|
165
|
+
const ids = methods.map((m) => m.id);
|
|
166
|
+
|
|
167
|
+
const document: JsonObject = {
|
|
168
|
+
"@context": context,
|
|
169
|
+
id: did,
|
|
170
|
+
verificationMethod: methods.map((m) => m.method),
|
|
171
|
+
};
|
|
172
|
+
if (relationships.assertionMethod ?? true) document.assertionMethod = ids;
|
|
173
|
+
if (relationships.authentication ?? false) document.authentication = ids;
|
|
174
|
+
if (options.alsoKnownAs !== undefined && options.alsoKnownAs.length > 0) {
|
|
175
|
+
document.alsoKnownAs = [...options.alsoKnownAs];
|
|
176
|
+
}
|
|
177
|
+
return document;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Locate a verification method in a DID document by its id. A method `id` that
|
|
182
|
+
* is a relative reference (`#key-0`) is resolved against the document's `id`
|
|
183
|
+
* before comparison, per DID Core — foreign documents commonly use relative ids.
|
|
184
|
+
*/
|
|
185
|
+
export function findVerificationMethod(
|
|
186
|
+
didDocument: JsonObject,
|
|
187
|
+
id: string,
|
|
188
|
+
): VerificationMethod | undefined {
|
|
189
|
+
const docId = typeof didDocument.id === "string" ? didDocument.id : "";
|
|
190
|
+
const methods = didDocument.verificationMethod;
|
|
191
|
+
if (!Array.isArray(methods)) return undefined;
|
|
192
|
+
for (const entry of methods) {
|
|
193
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const entryId = (entry as JsonObject).id;
|
|
197
|
+
if (typeof entryId !== "string") continue;
|
|
198
|
+
const resolved = entryId.startsWith("#") ? `${docId}${entryId}` : entryId;
|
|
199
|
+
if (resolved === id) {
|
|
200
|
+
return entry as unknown as VerificationMethod;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A minimal `fetch` used to retrieve DID documents. */
|
|
207
|
+
export type FetchLike = (
|
|
208
|
+
input: string,
|
|
209
|
+
init?: { headers?: Record<string, string> },
|
|
210
|
+
) => Promise<{ ok: boolean; status: number; json: () => Promise<unknown> }>;
|
|
211
|
+
|
|
212
|
+
/** Options for {@link createDidWebResolver}. */
|
|
213
|
+
export interface DidWebResolverOptions {
|
|
214
|
+
/** Override the fetch implementation (defaults to the global `fetch`). */
|
|
215
|
+
readonly fetch?: FetchLike;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build a {@link VerificationMethodResolver} that resolves a `did:web`
|
|
220
|
+
* verification-method id by fetching the controller's DID document over HTTPS
|
|
221
|
+
* and locating the referenced method. Returns `undefined` for non-`did:web`
|
|
222
|
+
* ids, fetch failures, and unknown methods — verification treats that as an
|
|
223
|
+
* unresolvable key rather than throwing.
|
|
224
|
+
*/
|
|
225
|
+
export function createDidWebResolver(
|
|
226
|
+
options: DidWebResolverOptions = {},
|
|
227
|
+
): (id: string) => Promise<VerificationMethod | undefined> {
|
|
228
|
+
const fetchImpl =
|
|
229
|
+
options.fetch ?? (globalThis.fetch as unknown as FetchLike | undefined);
|
|
230
|
+
if (fetchImpl === undefined) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
"@dwk/vc: no fetch implementation available for did:web resolution",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return async (id: string) => {
|
|
237
|
+
const hashIndex = id.indexOf("#");
|
|
238
|
+
const did = hashIndex === -1 ? id : id.slice(0, hashIndex);
|
|
239
|
+
if (!did.startsWith(DID_WEB_PREFIX)) return undefined;
|
|
240
|
+
|
|
241
|
+
let url: string;
|
|
242
|
+
try {
|
|
243
|
+
url = didWebToUrl(did);
|
|
244
|
+
} catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let response: Awaited<ReturnType<FetchLike>>;
|
|
249
|
+
try {
|
|
250
|
+
response = await fetchImpl(url, {
|
|
251
|
+
headers: { accept: "application/did+json, application/json" },
|
|
252
|
+
});
|
|
253
|
+
} catch {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
if (!response.ok) return undefined;
|
|
257
|
+
|
|
258
|
+
let document: unknown;
|
|
259
|
+
try {
|
|
260
|
+
document = await response.json();
|
|
261
|
+
} catch {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
document === null ||
|
|
266
|
+
typeof document !== "object" ||
|
|
267
|
+
Array.isArray(document)
|
|
268
|
+
) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
return findVerificationMethod(document as JsonObject, id);
|
|
272
|
+
};
|
|
273
|
+
}
|