@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-list.d.ts","sourceRoot":"","sources":["../src/status-list.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitstring Status List support: the encoded-list codec, credential/entry
|
|
3
|
+
* builders, and a D1-backed authority for flipping a credential's status.
|
|
4
|
+
*
|
|
5
|
+
* A status list is a GZIP-compressed bitstring, multibase base64url-encoded,
|
|
6
|
+
* published inside a `BitstringStatusListCredential`. A credential opts in by
|
|
7
|
+
* carrying a `BitstringStatusListEntry` that points at a list and an index;
|
|
8
|
+
* verifiers read the bit at that index. Flipping a bit (revoking, suspending) is
|
|
9
|
+
* stateful and security-sensitive, so the authoritative bits live in **D1** — a
|
|
10
|
+
* strongly-consistent store — never KV (see `spec/non-functional-requirements.md`).
|
|
11
|
+
*
|
|
12
|
+
* The codec and builders are pure (GZIP via `CompressionStream`, available under
|
|
13
|
+
* both Node and workerd); only {@link createVcStatusStore} touches a binding.
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.w3.org/TR/vc-bitstring-status-list/
|
|
16
|
+
*/
|
|
17
|
+
import { toXsdDateTime } from "./datetime";
|
|
18
|
+
import { decodeMultibase, encodeMultibaseBase64url } from "./multibase";
|
|
19
|
+
/**
|
|
20
|
+
* The minimum bitstring length the spec mandates (131,072 bits / 16 KB), chosen
|
|
21
|
+
* so an individual entry's index does not leak which credential it refers to.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_STATUS_LIST_LENGTH = 131072;
|
|
24
|
+
export const BITSTRING_STATUS_LIST_CREDENTIAL_TYPE = "BitstringStatusListCredential";
|
|
25
|
+
export const BITSTRING_STATUS_LIST_ENTRY_TYPE = "BitstringStatusListEntry";
|
|
26
|
+
export const BITSTRING_STATUS_LIST_SUBJECT_TYPE = "BitstringStatusList";
|
|
27
|
+
/** Emit a `statusPurpose` as plain data (a mutable array when one was given). */
|
|
28
|
+
function statusPurposeValue(purpose) {
|
|
29
|
+
return Array.isArray(purpose) ? [...purpose] : purpose;
|
|
30
|
+
}
|
|
31
|
+
/** Whether `purpose` (a `statusPurpose` field) covers `wanted`. */
|
|
32
|
+
function statusPurposeMatches(purpose, wanted) {
|
|
33
|
+
return Array.isArray(purpose) ? purpose.includes(wanted) : purpose === wanted;
|
|
34
|
+
}
|
|
35
|
+
/** Get the bit at `index` in a most-significant-bit-first bitstring. */
|
|
36
|
+
export function getBit(bits, index) {
|
|
37
|
+
if (index < 0)
|
|
38
|
+
return false;
|
|
39
|
+
const byteIndex = index >> 3;
|
|
40
|
+
if (byteIndex >= bits.length)
|
|
41
|
+
return false;
|
|
42
|
+
const bit = 7 - (index & 7);
|
|
43
|
+
return ((bits[byteIndex] >> bit) & 1) === 1;
|
|
44
|
+
}
|
|
45
|
+
/** Set the bit at `index` in a most-significant-bit-first bitstring. */
|
|
46
|
+
export function setBit(bits, index, value) {
|
|
47
|
+
// A negative index shifts to a negative byteIndex, which would slip past the
|
|
48
|
+
// upper-bound check, so guard both ends explicitly.
|
|
49
|
+
const byteIndex = index >> 3;
|
|
50
|
+
if (index < 0 || byteIndex >= bits.length) {
|
|
51
|
+
throw new Error(`@dwk/vc: status index ${index} is out of range`);
|
|
52
|
+
}
|
|
53
|
+
const bit = 7 - (index & 7);
|
|
54
|
+
if (value) {
|
|
55
|
+
bits[byteIndex] |= 1 << bit;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
bits[byteIndex] &= ~(1 << bit);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function gzip(input) {
|
|
62
|
+
const stream = new Response(input).body.pipeThrough(new CompressionStream("gzip"));
|
|
63
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
64
|
+
}
|
|
65
|
+
async function gunzip(input) {
|
|
66
|
+
const stream = new Response(input).body.pipeThrough(new DecompressionStream("gzip"));
|
|
67
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
68
|
+
}
|
|
69
|
+
/** GZIP-compress a bitstring and multibase base64url-encode it (`encodedList`). */
|
|
70
|
+
export async function encodeBitstring(bits) {
|
|
71
|
+
return encodeMultibaseBase64url(await gzip(bits));
|
|
72
|
+
}
|
|
73
|
+
/** Decode an `encodedList` (multibase base64url + GZIP) back to its bitstring. */
|
|
74
|
+
export async function decodeBitstring(encodedList) {
|
|
75
|
+
return gunzip(decodeMultibase(encodedList));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build an `encodedList` of `length` bits with the given indices set to 1.
|
|
79
|
+
* `length` is the bit count (rounded up to whole bytes).
|
|
80
|
+
*/
|
|
81
|
+
export async function buildEncodedList(setIndices, length = DEFAULT_STATUS_LIST_LENGTH) {
|
|
82
|
+
const bits = new Uint8Array(Math.ceil(length / 8));
|
|
83
|
+
for (const index of setIndices)
|
|
84
|
+
setBit(bits, index, true);
|
|
85
|
+
return encodeBitstring(bits);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Assemble an **unsigned** `BitstringStatusListCredential`. The caller signs it
|
|
89
|
+
* with {@link ./data-integrity.addProof} before publishing.
|
|
90
|
+
*
|
|
91
|
+
* Per the Bitstring Status List spec, `validFrom`/`validUntil`/`ttl` bound how
|
|
92
|
+
* long the published status may be cached; `validUntil` and `ttl` are emitted on
|
|
93
|
+
* the credential (and `ttl` also on the subject) when supplied.
|
|
94
|
+
*/
|
|
95
|
+
export function buildStatusListCredential(options) {
|
|
96
|
+
const subject = {
|
|
97
|
+
id: `${options.id}#list`,
|
|
98
|
+
type: BITSTRING_STATUS_LIST_SUBJECT_TYPE,
|
|
99
|
+
statusPurpose: statusPurposeValue(options.statusPurpose),
|
|
100
|
+
encodedList: options.encodedList,
|
|
101
|
+
};
|
|
102
|
+
if (options.ttl !== undefined)
|
|
103
|
+
subject.ttl = options.ttl;
|
|
104
|
+
const credential = {
|
|
105
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
106
|
+
id: options.id,
|
|
107
|
+
type: ["VerifiableCredential", BITSTRING_STATUS_LIST_CREDENTIAL_TYPE],
|
|
108
|
+
issuer: options.issuer,
|
|
109
|
+
validFrom: toXsdDateTime(options.validFrom ?? new Date()),
|
|
110
|
+
credentialSubject: subject,
|
|
111
|
+
};
|
|
112
|
+
if (options.validUntil !== undefined) {
|
|
113
|
+
credential.validUntil = toXsdDateTime(options.validUntil);
|
|
114
|
+
}
|
|
115
|
+
if (options.ttl !== undefined)
|
|
116
|
+
credential.ttl = options.ttl;
|
|
117
|
+
return credential;
|
|
118
|
+
}
|
|
119
|
+
/** Build a `credentialStatus` entry referencing a list index. */
|
|
120
|
+
export function buildStatusEntry(options) {
|
|
121
|
+
return {
|
|
122
|
+
id: options.id ??
|
|
123
|
+
`${options.statusListCredential}#${options.statusListIndex}`,
|
|
124
|
+
type: BITSTRING_STATUS_LIST_ENTRY_TYPE,
|
|
125
|
+
statusPurpose: statusPurposeValue(options.statusPurpose),
|
|
126
|
+
statusListIndex: String(options.statusListIndex),
|
|
127
|
+
statusListCredential: options.statusListCredential,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Read a `statusListIndex` from a credential's `credentialStatus` entry. */
|
|
131
|
+
export function statusEntryIndex(entry) {
|
|
132
|
+
const raw = entry.statusListIndex;
|
|
133
|
+
const value = typeof raw === "string" ? Number(raw) : raw;
|
|
134
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0
|
|
135
|
+
? value
|
|
136
|
+
: undefined;
|
|
137
|
+
}
|
|
138
|
+
/** Locate a `credentialStatus` entry for a purpose on a credential. */
|
|
139
|
+
export function findStatusEntry(credential, statusPurpose) {
|
|
140
|
+
const status = credential.credentialStatus;
|
|
141
|
+
const entries = Array.isArray(status)
|
|
142
|
+
? status
|
|
143
|
+
: status === undefined
|
|
144
|
+
? []
|
|
145
|
+
: [status];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (entry !== null &&
|
|
148
|
+
typeof entry === "object" &&
|
|
149
|
+
!Array.isArray(entry) &&
|
|
150
|
+
statusPurposeMatches(entry.statusPurpose, statusPurpose)) {
|
|
151
|
+
return entry;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const SCHEMA = [
|
|
157
|
+
`CREATE TABLE IF NOT EXISTS status_entries (
|
|
158
|
+
list_id TEXT NOT NULL,
|
|
159
|
+
status_purpose TEXT NOT NULL,
|
|
160
|
+
idx INTEGER NOT NULL,
|
|
161
|
+
value INTEGER NOT NULL DEFAULT 0,
|
|
162
|
+
PRIMARY KEY (list_id, status_purpose, idx)
|
|
163
|
+
)`,
|
|
164
|
+
`CREATE TABLE IF NOT EXISTS status_allocations (
|
|
165
|
+
list_id TEXT NOT NULL,
|
|
166
|
+
status_purpose TEXT NOT NULL,
|
|
167
|
+
next_index INTEGER NOT NULL,
|
|
168
|
+
PRIMARY KEY (list_id, status_purpose)
|
|
169
|
+
)`,
|
|
170
|
+
];
|
|
171
|
+
/**
|
|
172
|
+
* Create the D1-backed {@link VcStatusStore}. Fails loudly if the required
|
|
173
|
+
* `VC_STATUS_DB` binding is missing — no silent degradation (composition
|
|
174
|
+
* contract).
|
|
175
|
+
*/
|
|
176
|
+
export function createVcStatusStore(env) {
|
|
177
|
+
if (!env.VC_STATUS_DB) {
|
|
178
|
+
throw new Error("@dwk/vc: missing required D1 binding `VC_STATUS_DB`");
|
|
179
|
+
}
|
|
180
|
+
const db = env.VC_STATUS_DB;
|
|
181
|
+
return {
|
|
182
|
+
async init() {
|
|
183
|
+
for (const ddl of SCHEMA)
|
|
184
|
+
await db.prepare(ddl).run();
|
|
185
|
+
},
|
|
186
|
+
async allocateIndex(listId, statusPurpose) {
|
|
187
|
+
// Atomically bump the per-list counter and read it back, so concurrent
|
|
188
|
+
// allocations never hand out the same index.
|
|
189
|
+
const row = await db
|
|
190
|
+
.prepare(`INSERT INTO status_allocations (list_id, status_purpose, next_index)
|
|
191
|
+
VALUES (?, ?, 1)
|
|
192
|
+
ON CONFLICT (list_id, status_purpose)
|
|
193
|
+
DO UPDATE SET next_index = next_index + 1
|
|
194
|
+
RETURNING next_index`)
|
|
195
|
+
.bind(listId, statusPurpose)
|
|
196
|
+
.first();
|
|
197
|
+
// next_index now points past the allocated slot; the slot is one below.
|
|
198
|
+
return (row?.next_index ?? 1) - 1;
|
|
199
|
+
},
|
|
200
|
+
async setStatus(listId, statusPurpose, index, value) {
|
|
201
|
+
// Only set (value-1) bits are stored; clearing a bit deletes the row so the
|
|
202
|
+
// table stays sparse (one row per set bit) rather than accumulating
|
|
203
|
+
// value-0 tombstones. `getStatus`/`setIndices` already treat a missing row
|
|
204
|
+
// as unset.
|
|
205
|
+
if (value) {
|
|
206
|
+
await db
|
|
207
|
+
.prepare(`INSERT INTO status_entries (list_id, status_purpose, idx, value)
|
|
208
|
+
VALUES (?, ?, ?, 1)
|
|
209
|
+
ON CONFLICT (list_id, status_purpose, idx)
|
|
210
|
+
DO UPDATE SET value = 1`)
|
|
211
|
+
.bind(listId, statusPurpose, index)
|
|
212
|
+
.run();
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
await db
|
|
216
|
+
.prepare(`DELETE FROM status_entries
|
|
217
|
+
WHERE list_id = ? AND status_purpose = ? AND idx = ?`)
|
|
218
|
+
.bind(listId, statusPurpose, index)
|
|
219
|
+
.run();
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
async getStatus(listId, statusPurpose, index) {
|
|
223
|
+
const row = await db
|
|
224
|
+
.prepare(`SELECT value FROM status_entries
|
|
225
|
+
WHERE list_id = ? AND status_purpose = ? AND idx = ?`)
|
|
226
|
+
.bind(listId, statusPurpose, index)
|
|
227
|
+
.first();
|
|
228
|
+
return row?.value === 1;
|
|
229
|
+
},
|
|
230
|
+
async setIndices(listId, statusPurpose) {
|
|
231
|
+
const result = await db
|
|
232
|
+
.prepare(`SELECT idx FROM status_entries
|
|
233
|
+
WHERE list_id = ? AND status_purpose = ? AND value = 1
|
|
234
|
+
ORDER BY idx ASC`)
|
|
235
|
+
.bind(listId, statusPurpose)
|
|
236
|
+
.all();
|
|
237
|
+
return (result.results ?? []).map((r) => r.idx);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=status-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-list.js","sourceRoot":"","sources":["../src/status-list.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dwk/vc",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "did:web identity plus Verifiable Credential (VCDM 2.0) issuance, verification, and Bitstring Status List revocation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"verifiable-credentials",
|
|
7
|
+
"vc",
|
|
8
|
+
"did",
|
|
9
|
+
"did-web",
|
|
10
|
+
"data-integrity",
|
|
11
|
+
"decentralized-identity",
|
|
12
|
+
"cloudflare-workers"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"author": "David W. Keith <me@dwk.io>",
|
|
17
|
+
"homepage": "https://github.com/davidwkeith/workers/tree/main/packages/vc#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/davidwkeith/workers.git",
|
|
21
|
+
"directory": "packages/vc"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"!src/**/*.test.ts"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@dwk/log": "0.1.0-beta.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.build.json",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for {@link ./handler.createVc}: the issuer DID and verification
|
|
3
|
+
* method, the dynamic endpoint URLs, the (optional) status-list policy, and the
|
|
4
|
+
* delegated authorization and DID-resolution hooks. Per the composition contract
|
|
5
|
+
* the package never reads the global environment — every tunable arrives here and
|
|
6
|
+
* signing-key material arrives via a secret binding — so a handler can be
|
|
7
|
+
* instantiated multiple times and tested in isolation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { noopLogger, noopMetrics, type Logger, type Metrics } from "@dwk/log";
|
|
11
|
+
|
|
12
|
+
import type { VerificationMethod } from "./data-integrity";
|
|
13
|
+
import { urlToDidWeb } from "./did-web";
|
|
14
|
+
import { DEFAULT_STATUS_LIST_LENGTH, type StatusPurpose } from "./status-list";
|
|
15
|
+
|
|
16
|
+
/** A mutating operation an {@link AuthorizeOperation} hook can gate. */
|
|
17
|
+
export type VcOperation = "issue" | "status";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Authorization hook for the mutating endpoints (issue, status). Return `true`
|
|
21
|
+
* to allow. When omitted, requests are allowed through — the composing Worker is
|
|
22
|
+
* the front door and owns edge token validation (see `spec/architecture.md`);
|
|
23
|
+
* supply this to enforce authorization inside the package as well.
|
|
24
|
+
*/
|
|
25
|
+
export type AuthorizeOperation = (
|
|
26
|
+
operation: VcOperation,
|
|
27
|
+
request: Request,
|
|
28
|
+
) => boolean | Promise<boolean>;
|
|
29
|
+
|
|
30
|
+
/** Resolve a verification-method id to its key document, or `undefined`. */
|
|
31
|
+
export type DidResolver = (
|
|
32
|
+
id: string,
|
|
33
|
+
) => VerificationMethod | undefined | Promise<VerificationMethod | undefined>;
|
|
34
|
+
|
|
35
|
+
/** Status-list policy. */
|
|
36
|
+
export interface StatusConfig {
|
|
37
|
+
/** Enable the status endpoints and `credentialStatus` issuance. */
|
|
38
|
+
readonly enabled: boolean;
|
|
39
|
+
/** The status purpose lists track. Defaults to `"revocation"`. */
|
|
40
|
+
readonly statusPurpose?: StatusPurpose;
|
|
41
|
+
/** Bit length of each status list. Defaults to {@link DEFAULT_STATUS_LIST_LENGTH}. */
|
|
42
|
+
readonly listLength?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Configuration passed to {@link ./handler.createVc}. */
|
|
46
|
+
export interface VcConfig {
|
|
47
|
+
/** The identity root / base URL (e.g. `https://example.com`). */
|
|
48
|
+
readonly baseUrl: string;
|
|
49
|
+
/** The issuer DID. Defaults to the `did:web` derived from `baseUrl`. */
|
|
50
|
+
readonly did?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The verification method id used to sign credentials (e.g.
|
|
53
|
+
* `did:web:example.com#key-0`). Defaults to `${did}#key-0`.
|
|
54
|
+
*/
|
|
55
|
+
readonly verificationMethod?: string;
|
|
56
|
+
/** Absolute issuance endpoint. Defaults to `${baseUrl}/credentials/issue`. */
|
|
57
|
+
readonly issueEndpoint?: string;
|
|
58
|
+
/** Absolute verification endpoint. Defaults to `${baseUrl}/credentials/verify`. */
|
|
59
|
+
readonly verifyEndpoint?: string;
|
|
60
|
+
/** Absolute status-update endpoint. Defaults to `${baseUrl}/credentials/status`. */
|
|
61
|
+
readonly statusEndpoint?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Absolute base URL under which signed status list credentials are served. A
|
|
64
|
+
* request to `${statusListEndpoint}/<listId>` returns that list. Defaults to
|
|
65
|
+
* `${baseUrl}/credentials/status-lists`.
|
|
66
|
+
*/
|
|
67
|
+
readonly statusListEndpoint?: string;
|
|
68
|
+
/** Status-list policy. Disabled by default. */
|
|
69
|
+
readonly status?: StatusConfig;
|
|
70
|
+
/** Authorization hook for issue/status (see {@link AuthorizeOperation}). */
|
|
71
|
+
readonly authorize?: AuthorizeOperation;
|
|
72
|
+
/**
|
|
73
|
+
* Verification-method resolver used during verification. Defaults to a
|
|
74
|
+
* `did:web` resolver over the global `fetch`.
|
|
75
|
+
*/
|
|
76
|
+
readonly resolveDid?: DidResolver;
|
|
77
|
+
/** Logger; defaults to a no-op. */
|
|
78
|
+
readonly logger?: Logger;
|
|
79
|
+
/** Metrics sink; defaults to a no-op. */
|
|
80
|
+
readonly metrics?: Metrics;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Fully resolved configuration with defaults applied and URLs parsed. */
|
|
84
|
+
export interface ResolvedVcConfig {
|
|
85
|
+
readonly did: string;
|
|
86
|
+
readonly verificationMethod: string;
|
|
87
|
+
readonly issueEndpoint: string;
|
|
88
|
+
readonly verifyEndpoint: string;
|
|
89
|
+
readonly statusEndpoint: string;
|
|
90
|
+
readonly statusListEndpoint: string;
|
|
91
|
+
readonly issuePath: string;
|
|
92
|
+
readonly verifyPath: string;
|
|
93
|
+
readonly statusPath: string;
|
|
94
|
+
readonly statusListPath: string;
|
|
95
|
+
readonly statusEnabled: boolean;
|
|
96
|
+
readonly statusPurpose: StatusPurpose;
|
|
97
|
+
readonly statusListLength: number;
|
|
98
|
+
readonly authorize: AuthorizeOperation;
|
|
99
|
+
readonly resolveDid?: DidResolver;
|
|
100
|
+
readonly logger: Logger;
|
|
101
|
+
readonly metrics: Metrics;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pathOf(absoluteUrl: string, label: string): string {
|
|
105
|
+
try {
|
|
106
|
+
return new URL(absoluteUrl).pathname;
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error(`@dwk/vc: ${label} is not a valid URL`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve user config into a {@link ResolvedVcConfig}: derive the issuer DID and
|
|
114
|
+
* verification method from `baseUrl` when omitted, default each endpoint URL, and
|
|
115
|
+
* pre-compute pathnames for routing. Throws if `baseUrl` (or any explicitly
|
|
116
|
+
* supplied endpoint URL) is not a valid URL, or if status is enabled without a
|
|
117
|
+
* status-list endpoint resolvable from `baseUrl`.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveConfig(config: VcConfig): ResolvedVcConfig {
|
|
120
|
+
let base: URL;
|
|
121
|
+
try {
|
|
122
|
+
base = new URL(config.baseUrl);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error("@dwk/vc: `baseUrl` is not a valid URL");
|
|
125
|
+
}
|
|
126
|
+
const origin = base.origin;
|
|
127
|
+
|
|
128
|
+
const did = config.did ?? urlToDidWeb(config.baseUrl);
|
|
129
|
+
const verificationMethod = config.verificationMethod ?? `${did}#key-0`;
|
|
130
|
+
|
|
131
|
+
const issueEndpoint = config.issueEndpoint ?? `${origin}/credentials/issue`;
|
|
132
|
+
const verifyEndpoint =
|
|
133
|
+
config.verifyEndpoint ?? `${origin}/credentials/verify`;
|
|
134
|
+
const statusEndpoint =
|
|
135
|
+
config.statusEndpoint ?? `${origin}/credentials/status`;
|
|
136
|
+
const statusListEndpoint =
|
|
137
|
+
config.statusListEndpoint ?? `${origin}/credentials/status-lists`;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
did,
|
|
141
|
+
verificationMethod,
|
|
142
|
+
issueEndpoint,
|
|
143
|
+
verifyEndpoint,
|
|
144
|
+
statusEndpoint,
|
|
145
|
+
statusListEndpoint,
|
|
146
|
+
issuePath: pathOf(issueEndpoint, "issueEndpoint"),
|
|
147
|
+
verifyPath: pathOf(verifyEndpoint, "verifyEndpoint"),
|
|
148
|
+
statusPath: pathOf(statusEndpoint, "statusEndpoint"),
|
|
149
|
+
statusListPath: pathOf(statusListEndpoint, "statusListEndpoint"),
|
|
150
|
+
statusEnabled: config.status?.enabled ?? false,
|
|
151
|
+
statusPurpose: config.status?.statusPurpose ?? "revocation",
|
|
152
|
+
statusListLength: config.status?.listLength ?? DEFAULT_STATUS_LIST_LENGTH,
|
|
153
|
+
authorize: config.authorize ?? (() => true),
|
|
154
|
+
resolveDid: config.resolveDid,
|
|
155
|
+
logger: config.logger ?? noopLogger,
|
|
156
|
+
metrics: config.metrics ?? noopMetrics,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifiable Credential Data Model 2.0 shapes and structural validation.
|
|
3
|
+
*
|
|
4
|
+
* Plain-data helpers: assemble an unsigned credential, validate the required VCDM
|
|
5
|
+
* 2.0 members before signing or after verifying, and evaluate the validity
|
|
6
|
+
* window. No Web Crypto, no runtime — the proof lives in {@link ./data-integrity}.
|
|
7
|
+
*
|
|
8
|
+
* @see https://www.w3.org/TR/vc-data-model-2.0/
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isValidXsdDateTimeStamp, toXsdDateTime } from "./datetime";
|
|
12
|
+
import type { JcsValue } from "./jcs";
|
|
13
|
+
import type { JsonObject } from "./data-integrity";
|
|
14
|
+
|
|
15
|
+
/** The base VCDM 2.0 context, which MUST be the first `@context` entry. */
|
|
16
|
+
export const VC_CONTEXT_V2 = "https://www.w3.org/ns/credentials/v2";
|
|
17
|
+
|
|
18
|
+
/** The base credential type every verifiable credential carries. */
|
|
19
|
+
export const VERIFIABLE_CREDENTIAL_TYPE = "VerifiableCredential";
|
|
20
|
+
|
|
21
|
+
/** An issuer reference: a URL string or an object with an `id`. */
|
|
22
|
+
export type Issuer = string | (JsonObject & { id: string });
|
|
23
|
+
|
|
24
|
+
/** A credential prior to having a proof attached. */
|
|
25
|
+
export interface UnsignedCredential extends JsonObject {
|
|
26
|
+
"@context": JcsValue;
|
|
27
|
+
type: JcsValue;
|
|
28
|
+
issuer: Issuer;
|
|
29
|
+
credentialSubject: JcsValue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Inputs to {@link buildCredential}. */
|
|
33
|
+
export interface BuildCredentialOptions {
|
|
34
|
+
/** Extra credential types beyond `VerifiableCredential`. */
|
|
35
|
+
readonly type?: string | readonly string[];
|
|
36
|
+
/** Extra `@context` entries beyond the base VCDM 2.0 context. */
|
|
37
|
+
readonly context?: string | readonly string[];
|
|
38
|
+
/** The credential id (a URL), if any. */
|
|
39
|
+
readonly id?: string;
|
|
40
|
+
readonly issuer: Issuer;
|
|
41
|
+
readonly credentialSubject: JcsValue;
|
|
42
|
+
/** `validFrom` (XSD dateTime). Defaults to now when omitted. */
|
|
43
|
+
readonly validFrom?: Date | string;
|
|
44
|
+
/** `validUntil` (XSD dateTime). */
|
|
45
|
+
readonly validUntil?: Date | string;
|
|
46
|
+
/** A `credentialStatus` entry (e.g. a Bitstring Status List reference). */
|
|
47
|
+
readonly credentialStatus?: JsonObject | readonly JsonObject[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function dedupePrepend(
|
|
51
|
+
base: string,
|
|
52
|
+
extra: string | readonly string[] | undefined,
|
|
53
|
+
): string[] {
|
|
54
|
+
const out = [base];
|
|
55
|
+
if (extra === undefined) return out;
|
|
56
|
+
const list = typeof extra === "string" ? [extra] : extra;
|
|
57
|
+
for (const item of list) {
|
|
58
|
+
if (!out.includes(item)) out.push(item);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Assemble an unsigned VCDM 2.0 credential, guaranteeing the base context comes
|
|
65
|
+
* first and the base `VerifiableCredential` type is present. The result is ready
|
|
66
|
+
* for {@link ./data-integrity.addProof}.
|
|
67
|
+
*/
|
|
68
|
+
export function buildCredential(
|
|
69
|
+
options: BuildCredentialOptions,
|
|
70
|
+
): UnsignedCredential {
|
|
71
|
+
const credential: UnsignedCredential = {
|
|
72
|
+
"@context": dedupePrepend(VC_CONTEXT_V2, options.context),
|
|
73
|
+
type: dedupePrepend(VERIFIABLE_CREDENTIAL_TYPE, options.type),
|
|
74
|
+
issuer: options.issuer,
|
|
75
|
+
credentialSubject: options.credentialSubject,
|
|
76
|
+
};
|
|
77
|
+
if (options.id !== undefined) credential.id = options.id;
|
|
78
|
+
credential.validFrom = toXsdDateTime(options.validFrom ?? new Date());
|
|
79
|
+
if (options.validUntil !== undefined) {
|
|
80
|
+
credential.validUntil = toXsdDateTime(options.validUntil);
|
|
81
|
+
}
|
|
82
|
+
if (options.credentialStatus !== undefined) {
|
|
83
|
+
credential.credentialStatus = Array.isArray(options.credentialStatus)
|
|
84
|
+
? [...options.credentialStatus]
|
|
85
|
+
: (options.credentialStatus as JsonObject);
|
|
86
|
+
}
|
|
87
|
+
return credential;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasType(type: JcsValue | undefined, expected: string): boolean {
|
|
91
|
+
if (type === expected) return true;
|
|
92
|
+
return Array.isArray(type) && type.includes(expected);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function firstContext(context: JcsValue | undefined): JcsValue | undefined {
|
|
96
|
+
if (Array.isArray(context)) return context[0];
|
|
97
|
+
return context;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate the structural VCDM 2.0 requirements of a credential, returning a
|
|
102
|
+
* list of problems (empty when valid). Checks the base context ordering, the
|
|
103
|
+
* `VerifiableCredential` type, and the presence/typing of `issuer` and
|
|
104
|
+
* `credentialSubject`. Does not check the proof — that is
|
|
105
|
+
* {@link ./data-integrity.verifyProof}.
|
|
106
|
+
*/
|
|
107
|
+
export function validateCredential(credential: JsonObject): string[] {
|
|
108
|
+
const errors: string[] = [];
|
|
109
|
+
|
|
110
|
+
if (firstContext(credential["@context"]) !== VC_CONTEXT_V2) {
|
|
111
|
+
errors.push(`@context must begin with "${VC_CONTEXT_V2}"`);
|
|
112
|
+
}
|
|
113
|
+
if (!hasType(credential.type, VERIFIABLE_CREDENTIAL_TYPE)) {
|
|
114
|
+
errors.push(`type must include "${VERIFIABLE_CREDENTIAL_TYPE}"`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const issuer = credential.issuer;
|
|
118
|
+
const issuerOk =
|
|
119
|
+
typeof issuer === "string"
|
|
120
|
+
? issuer.length > 0
|
|
121
|
+
: issuer !== null &&
|
|
122
|
+
typeof issuer === "object" &&
|
|
123
|
+
!Array.isArray(issuer) &&
|
|
124
|
+
typeof (issuer as JsonObject).id === "string";
|
|
125
|
+
if (!issuerOk) {
|
|
126
|
+
errors.push("issuer must be a URL string or an object with a string id");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const subject = credential.credentialSubject;
|
|
130
|
+
const subjectOk =
|
|
131
|
+
subject !== null &&
|
|
132
|
+
subject !== undefined &&
|
|
133
|
+
typeof subject === "object" &&
|
|
134
|
+
(!Array.isArray(subject) || subject.length > 0);
|
|
135
|
+
if (!subjectOk) {
|
|
136
|
+
errors.push("credentialSubject must be an object or a non-empty array");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (credential.id !== undefined && typeof credential.id !== "string") {
|
|
140
|
+
errors.push("id, when present, must be a string");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return errors;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The issuer id of a credential, whether `issuer` is a string or an object. */
|
|
147
|
+
export function issuerId(credential: JsonObject): string | undefined {
|
|
148
|
+
const issuer = credential.issuer;
|
|
149
|
+
if (typeof issuer === "string") return issuer;
|
|
150
|
+
if (issuer !== null && typeof issuer === "object" && !Array.isArray(issuer)) {
|
|
151
|
+
const id = (issuer as JsonObject).id;
|
|
152
|
+
return typeof id === "string" ? id : undefined;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Evaluate a credential's validity window against `now` (epoch ms). Returns a
|
|
159
|
+
* reason when the credential is not yet valid or has expired, else `null`.
|
|
160
|
+
*
|
|
161
|
+
* A present-but-malformed bound fails **closed**: an unparseable `validFrom` is
|
|
162
|
+
* treated as not-yet-valid and an unparseable `validUntil` as expired, rather
|
|
163
|
+
* than silently dropping the bound (which would let a credential with a garbled
|
|
164
|
+
* expiry verify as if it never expires).
|
|
165
|
+
*/
|
|
166
|
+
export function checkValidityPeriod(
|
|
167
|
+
credential: JsonObject,
|
|
168
|
+
now: number = Date.now(),
|
|
169
|
+
): "not_yet_valid" | "expired" | null {
|
|
170
|
+
const validFrom = credential.validFrom;
|
|
171
|
+
if (validFrom !== undefined) {
|
|
172
|
+
if (typeof validFrom !== "string" || !isValidXsdDateTimeStamp(validFrom)) {
|
|
173
|
+
return "not_yet_valid";
|
|
174
|
+
}
|
|
175
|
+
if (now < Date.parse(validFrom)) return "not_yet_valid";
|
|
176
|
+
}
|
|
177
|
+
const validUntil = credential.validUntil;
|
|
178
|
+
if (validUntil !== undefined) {
|
|
179
|
+
if (
|
|
180
|
+
typeof validUntil !== "string" ||
|
|
181
|
+
!isValidXsdDateTimeStamp(validUntil)
|
|
182
|
+
) {
|
|
183
|
+
return "expired";
|
|
184
|
+
}
|
|
185
|
+
if (now > Date.parse(validUntil)) return "expired";
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|