@noy-db/attestation 0.2.0-pre.2
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 +21 -0
- package/README.md +3 -0
- package/dist/index.cjs +347 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +299 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vLannaAi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
b64urlToBytes: () => b64urlToBytes,
|
|
24
|
+
bytesToB64url: () => bytesToB64url,
|
|
25
|
+
bytesToHex: () => bytesToHex,
|
|
26
|
+
canonicalJson: () => canonicalJson,
|
|
27
|
+
computeFieldHashes: () => computeFieldHashes,
|
|
28
|
+
decodeQr: () => decodeQr,
|
|
29
|
+
ed25519Sign: () => ed25519Sign,
|
|
30
|
+
ed25519Verify: () => ed25519Verify,
|
|
31
|
+
encodeQr: () => encodeQr,
|
|
32
|
+
generateDocSigningKeyPair: () => generateDocSigningKeyPair,
|
|
33
|
+
getPath: () => getPath,
|
|
34
|
+
isRevoked: () => isRevoked,
|
|
35
|
+
keyIdFor: () => keyIdFor,
|
|
36
|
+
normalizeField: () => normalizeField,
|
|
37
|
+
sha256Bytes: () => sha256Bytes,
|
|
38
|
+
sha256Hex: () => sha256Hex,
|
|
39
|
+
signPayloadCore: () => signPayloadCore,
|
|
40
|
+
signRevocationList: () => signRevocationList,
|
|
41
|
+
utf8: () => utf8,
|
|
42
|
+
validateFieldSchema: () => validateFieldSchema,
|
|
43
|
+
verifyAttestation: () => verifyAttestation,
|
|
44
|
+
verifyRevocationList: () => verifyRevocationList
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(index_exports);
|
|
47
|
+
|
|
48
|
+
// src/encoding.ts
|
|
49
|
+
function utf8(s) {
|
|
50
|
+
return new TextEncoder().encode(s);
|
|
51
|
+
}
|
|
52
|
+
function bytesToHex(bytes) {
|
|
53
|
+
let out = "";
|
|
54
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function bytesToB64url(bytes) {
|
|
58
|
+
let s = "";
|
|
59
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
60
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
61
|
+
}
|
|
62
|
+
function b64urlToBytes(s) {
|
|
63
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
64
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
65
|
+
const bin = atob(b64);
|
|
66
|
+
const out = new Uint8Array(bin.length);
|
|
67
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function canonicalJson(value) {
|
|
71
|
+
if (value === null) return "null";
|
|
72
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
73
|
+
if (typeof value === "number") {
|
|
74
|
+
if (!Number.isFinite(value)) {
|
|
75
|
+
throw new Error(`canonicalJson: refusing to encode non-finite number ${String(value)}`);
|
|
76
|
+
}
|
|
77
|
+
return JSON.stringify(value);
|
|
78
|
+
}
|
|
79
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
80
|
+
if (typeof value === "bigint") throw new Error("canonicalJson: BigInt is not JSON-serializable");
|
|
81
|
+
if (typeof value === "undefined" || typeof value === "function") {
|
|
82
|
+
throw new Error(`canonicalJson: refusing to encode ${typeof value} \u2014 include all fields explicitly`);
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) return "[" + value.map((v) => canonicalJson(v)).join(",") + "]";
|
|
85
|
+
if (typeof value === "object") {
|
|
86
|
+
const obj = value;
|
|
87
|
+
const keys = Object.keys(obj).sort();
|
|
88
|
+
const parts = [];
|
|
89
|
+
for (const key of keys) parts.push(JSON.stringify(key) + ":" + canonicalJson(obj[key]));
|
|
90
|
+
return "{" + parts.join(",") + "}";
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`canonicalJson: unexpected value type: ${typeof value}`);
|
|
93
|
+
}
|
|
94
|
+
async function sha256Bytes(input) {
|
|
95
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8(input));
|
|
96
|
+
return new Uint8Array(digest);
|
|
97
|
+
}
|
|
98
|
+
async function sha256Hex(input) {
|
|
99
|
+
return bytesToHex(await sha256Bytes(input));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/normalize.ts
|
|
103
|
+
var NORMALIZERS = /* @__PURE__ */ new Set([
|
|
104
|
+
"trim",
|
|
105
|
+
"lower",
|
|
106
|
+
"upper",
|
|
107
|
+
"alnum-upper",
|
|
108
|
+
"digits",
|
|
109
|
+
"cents",
|
|
110
|
+
"iso-date"
|
|
111
|
+
]);
|
|
112
|
+
function getPath(obj, path) {
|
|
113
|
+
return path.split(".").reduce(
|
|
114
|
+
(o, k) => o == null ? void 0 : o[k],
|
|
115
|
+
obj
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
function normalizeField(value, n) {
|
|
119
|
+
switch (n) {
|
|
120
|
+
case "trim":
|
|
121
|
+
return String(value).trim();
|
|
122
|
+
case "lower":
|
|
123
|
+
return String(value).trim().toLowerCase();
|
|
124
|
+
case "upper":
|
|
125
|
+
return String(value).trim().toUpperCase();
|
|
126
|
+
case "alnum-upper":
|
|
127
|
+
return String(value).replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
128
|
+
case "digits":
|
|
129
|
+
return String(value).replace(/[^0-9]/g, "");
|
|
130
|
+
case "cents": {
|
|
131
|
+
if (typeof value === "number") {
|
|
132
|
+
if (!Number.isFinite(value)) {
|
|
133
|
+
throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`);
|
|
134
|
+
}
|
|
135
|
+
return String(Math.round(value * 100));
|
|
136
|
+
}
|
|
137
|
+
const stripped = String(value).replace(/[^0-9.-]/g, "");
|
|
138
|
+
if (!/[0-9]/.test(stripped)) {
|
|
139
|
+
throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`);
|
|
140
|
+
}
|
|
141
|
+
const num = Number(stripped);
|
|
142
|
+
if (!Number.isFinite(num)) {
|
|
143
|
+
throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`);
|
|
144
|
+
}
|
|
145
|
+
return String(Math.round(num * 100));
|
|
146
|
+
}
|
|
147
|
+
case "iso-date": {
|
|
148
|
+
const d = value instanceof Date ? value : new Date(String(value));
|
|
149
|
+
if (Number.isNaN(d.getTime())) {
|
|
150
|
+
throw new Error(`normalizeField(iso-date): unparseable date: ${String(value)}`);
|
|
151
|
+
}
|
|
152
|
+
return d.toISOString().slice(0, 10);
|
|
153
|
+
}
|
|
154
|
+
default: {
|
|
155
|
+
const exhaustive = n;
|
|
156
|
+
throw new Error(`normalizeField: unknown normalizer ${String(exhaustive)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function validateFieldSchema(schema) {
|
|
161
|
+
if (!schema.fields || schema.fields.length === 0) {
|
|
162
|
+
throw new Error("validateFieldSchema: schema must declare at least one field");
|
|
163
|
+
}
|
|
164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
165
|
+
for (const f of schema.fields) {
|
|
166
|
+
if (!NORMALIZERS.has(f.normalize)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`validateFieldSchema: unknown normalizer '${String(f.normalize)}' for path '${f.path}'`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (seen.has(f.path)) {
|
|
172
|
+
throw new Error(`validateFieldSchema: duplicate path '${f.path}'`);
|
|
173
|
+
}
|
|
174
|
+
seen.add(f.path);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/hashing.ts
|
|
179
|
+
async function computeFieldHashes(saltB64, schema, values) {
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const f of schema.fields) {
|
|
182
|
+
const raw = getPath(values, f.path);
|
|
183
|
+
if (raw === void 0 || raw === null) {
|
|
184
|
+
throw new Error(`computeFieldHashes: missing value at declared path '${f.path}'`);
|
|
185
|
+
}
|
|
186
|
+
const norm = normalizeField(raw, f.normalize);
|
|
187
|
+
const digest = await sha256Bytes(canonicalJson([saltB64, f.path, norm]));
|
|
188
|
+
out.push(bytesToB64url(digest));
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/ed25519.ts
|
|
194
|
+
var ALG = "Ed25519";
|
|
195
|
+
async function keyIdFor(publicKeyB64) {
|
|
196
|
+
return (await sha256Hex(publicKeyB64)).slice(0, 16);
|
|
197
|
+
}
|
|
198
|
+
async function generateDocSigningKeyPair() {
|
|
199
|
+
const kp = await globalThis.crypto.subtle.generateKey(ALG, true, ["sign", "verify"]);
|
|
200
|
+
const rawPub = new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", kp.publicKey));
|
|
201
|
+
const pkcs8 = new Uint8Array(await globalThis.crypto.subtle.exportKey("pkcs8", kp.privateKey));
|
|
202
|
+
const publicKeyB64 = bytesToB64url(rawPub);
|
|
203
|
+
return { keyId: await keyIdFor(publicKeyB64), publicKeyB64, privateKeyPkcs8B64: bytesToB64url(pkcs8) };
|
|
204
|
+
}
|
|
205
|
+
async function ed25519Sign(privateKeyPkcs8B64, message) {
|
|
206
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
207
|
+
"pkcs8",
|
|
208
|
+
b64urlToBytes(privateKeyPkcs8B64),
|
|
209
|
+
ALG,
|
|
210
|
+
false,
|
|
211
|
+
["sign"]
|
|
212
|
+
);
|
|
213
|
+
const sig = new Uint8Array(await globalThis.crypto.subtle.sign(ALG, key, message));
|
|
214
|
+
return bytesToB64url(sig);
|
|
215
|
+
}
|
|
216
|
+
async function ed25519Verify(publicKeyB64, sigB64url, message) {
|
|
217
|
+
try {
|
|
218
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
219
|
+
"raw",
|
|
220
|
+
b64urlToBytes(publicKeyB64),
|
|
221
|
+
ALG,
|
|
222
|
+
false,
|
|
223
|
+
["verify"]
|
|
224
|
+
);
|
|
225
|
+
return await globalThis.crypto.subtle.verify(
|
|
226
|
+
ALG,
|
|
227
|
+
key,
|
|
228
|
+
b64urlToBytes(sigB64url),
|
|
229
|
+
message
|
|
230
|
+
);
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/qr.ts
|
|
237
|
+
function encodeQr(p) {
|
|
238
|
+
return bytesToB64url(utf8(JSON.stringify(p)));
|
|
239
|
+
}
|
|
240
|
+
function decodeQr(s) {
|
|
241
|
+
let parsed;
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(new TextDecoder().decode(b64urlToBytes(s)));
|
|
244
|
+
} catch {
|
|
245
|
+
throw new Error("decodeQr: invalid base64url-encoded JSON payload");
|
|
246
|
+
}
|
|
247
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
248
|
+
throw new Error("decodeQr: invalid payload \u2014 expected a JSON object");
|
|
249
|
+
}
|
|
250
|
+
const p = parsed;
|
|
251
|
+
if (p["v"] !== 1) throw new Error(`decodeQr: unsupported version ${String(p["v"])} (expected 1)`);
|
|
252
|
+
if (typeof p["docId"] !== "string" || typeof p["salt"] !== "string" || p["alg"] !== "ed25519" || typeof p["keyId"] !== "string" || typeof p["sig"] !== "string" || !Array.isArray(p["fieldHashes"]) || !p["fieldHashes"].every((h) => typeof h === "string")) {
|
|
253
|
+
throw new Error("decodeQr: invalid payload shape");
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
v: 1,
|
|
257
|
+
docId: p["docId"],
|
|
258
|
+
salt: p["salt"],
|
|
259
|
+
alg: "ed25519",
|
|
260
|
+
keyId: p["keyId"],
|
|
261
|
+
fieldHashes: p["fieldHashes"],
|
|
262
|
+
sig: p["sig"]
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/revocation.ts
|
|
267
|
+
function listCore(revokedDocIds, asOf, keyId) {
|
|
268
|
+
return utf8(canonicalJson({ v: 1, revokedDocIds: [...revokedDocIds].sort(), asOf, keyId }));
|
|
269
|
+
}
|
|
270
|
+
function isRevoked(docId, list) {
|
|
271
|
+
if (!Array.isArray(list?.revokedDocIds)) return false;
|
|
272
|
+
return list.revokedDocIds.includes(docId);
|
|
273
|
+
}
|
|
274
|
+
async function signRevocationList(revokedDocIds, asOf, keyId, privateKeyPkcs8B64) {
|
|
275
|
+
const sorted = [...revokedDocIds].sort();
|
|
276
|
+
const sig = await ed25519Sign(privateKeyPkcs8B64, listCore(sorted, asOf, keyId));
|
|
277
|
+
return { v: 1, revokedDocIds: sorted, asOf, keyId, sig };
|
|
278
|
+
}
|
|
279
|
+
async function verifyRevocationList(list, publicKeyB64) {
|
|
280
|
+
if (list?.v !== 1 || !Array.isArray(list.revokedDocIds) || typeof list.asOf !== "string" || typeof list.keyId !== "string" || typeof list.sig !== "string") {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
return ed25519Verify(publicKeyB64, list.sig, listCore(list.revokedDocIds, list.asOf, list.keyId));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/verify.ts
|
|
287
|
+
function signedCore(core) {
|
|
288
|
+
return utf8(canonicalJson({ v: core.v, docId: core.docId, salt: core.salt, keyId: core.keyId, fieldHashes: core.fieldHashes }));
|
|
289
|
+
}
|
|
290
|
+
async function signPayloadCore(core, privateKeyPkcs8B64) {
|
|
291
|
+
return ed25519Sign(privateKeyPkcs8B64, signedCore(core));
|
|
292
|
+
}
|
|
293
|
+
async function verifyAttestation(input) {
|
|
294
|
+
const p = decodeQr(input.qr);
|
|
295
|
+
const pub = input.publicKeys[p.keyId];
|
|
296
|
+
const signatureValid = pub ? await ed25519Verify(pub, p.sig, signedCore({ v: p.v, docId: p.docId, salt: p.salt, keyId: p.keyId, fieldHashes: p.fieldHashes })) : false;
|
|
297
|
+
const schema = input.fieldSchema;
|
|
298
|
+
const perField = [];
|
|
299
|
+
let allMatch = true;
|
|
300
|
+
let countMismatch = false;
|
|
301
|
+
if (schema.fields.length !== p.fieldHashes.length) {
|
|
302
|
+
countMismatch = true;
|
|
303
|
+
allMatch = false;
|
|
304
|
+
for (const f of schema.fields) perField.push({ path: f.path, match: false });
|
|
305
|
+
} else {
|
|
306
|
+
const recomputed = await computeFieldHashes(p.salt, schema, input.claimedFields);
|
|
307
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
308
|
+
const match = recomputed[i] === p.fieldHashes[i];
|
|
309
|
+
perField.push({ path: schema.fields[i].path, match });
|
|
310
|
+
if (!match) allMatch = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const revoked = input.revocation ? isRevoked(p.docId, input.revocation.list) : null;
|
|
314
|
+
const valid = signatureValid && allMatch && revoked !== true;
|
|
315
|
+
let reason;
|
|
316
|
+
if (!signatureValid) reason = pub ? "signature invalid" : "unknown keyId";
|
|
317
|
+
else if (countMismatch) reason = "schema/payload field-count mismatch";
|
|
318
|
+
else if (!allMatch) reason = "field mismatch";
|
|
319
|
+
else if (revoked === true) reason = "revoked";
|
|
320
|
+
return reason !== void 0 ? { valid, signatureValid, perField, revoked, reason } : { valid, signatureValid, perField, revoked };
|
|
321
|
+
}
|
|
322
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
323
|
+
0 && (module.exports = {
|
|
324
|
+
b64urlToBytes,
|
|
325
|
+
bytesToB64url,
|
|
326
|
+
bytesToHex,
|
|
327
|
+
canonicalJson,
|
|
328
|
+
computeFieldHashes,
|
|
329
|
+
decodeQr,
|
|
330
|
+
ed25519Sign,
|
|
331
|
+
ed25519Verify,
|
|
332
|
+
encodeQr,
|
|
333
|
+
generateDocSigningKeyPair,
|
|
334
|
+
getPath,
|
|
335
|
+
isRevoked,
|
|
336
|
+
keyIdFor,
|
|
337
|
+
normalizeField,
|
|
338
|
+
sha256Bytes,
|
|
339
|
+
sha256Hex,
|
|
340
|
+
signPayloadCore,
|
|
341
|
+
signRevocationList,
|
|
342
|
+
utf8,
|
|
343
|
+
validateFieldSchema,
|
|
344
|
+
verifyAttestation,
|
|
345
|
+
verifyRevocationList
|
|
346
|
+
});
|
|
347
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/encoding.ts","../src/normalize.ts","../src/hashing.ts","../src/ed25519.ts","../src/qr.ts","../src/revocation.ts","../src/verify.ts"],"sourcesContent":["/**\n * @noy-db/attestation — pure document-attestation primitive.\n * @packageDocumentation\n */\nexport type { Normalizer, AttestationFieldSpec, AttestationFieldSchema } from './types.js'\nexport type { QrPayload } from './qr.js'\nexport type { RevocationList } from './revocation.js'\nexport type { VerifyInput, VerifyResult } from './verify.js'\n\nexport { canonicalJson, sha256Hex, sha256Bytes, bytesToHex, bytesToB64url, b64urlToBytes, utf8 } from './encoding.js'\nexport { normalizeField, validateFieldSchema, getPath } from './normalize.js'\nexport { computeFieldHashes } from './hashing.js'\nexport { generateDocSigningKeyPair, ed25519Sign, ed25519Verify, keyIdFor } from './ed25519.js'\nexport { encodeQr, decodeQr } from './qr.js'\nexport { signPayloadCore, verifyAttestation } from './verify.js'\nexport { isRevoked, verifyRevocationList, signRevocationList } from './revocation.js'\n","/**\n * Pure encoding + hashing primitives. Zero deps; WebCrypto only.\n *\n * `canonicalJson` and `sha256Hex` are intentionally byte-identical to\n * hub's `history/ledger/entry.ts` implementations. They are REPLICATED\n * here (not imported) because this package is upstream of hub — importing\n * from hub would invert the dependency. The conformance test pins the\n * shared contract via fixed vectors.\n */\n\nexport function utf8(s: string): Uint8Array {\n return new TextEncoder().encode(s)\n}\n\nexport function bytesToHex(bytes: Uint8Array): string {\n let out = ''\n for (const b of bytes) out += b.toString(16).padStart(2, '0')\n return out\n}\n\nexport function bytesToB64url(bytes: Uint8Array): string {\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nexport function b64urlToBytes(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4))\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad\n const bin = atob(b64)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\nexport function canonicalJson(value: unknown): string {\n if (value === null) return 'null'\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`canonicalJson: refusing to encode non-finite number ${String(value)}`)\n }\n return JSON.stringify(value)\n }\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'bigint') throw new Error('canonicalJson: BigInt is not JSON-serializable')\n if (typeof value === 'undefined' || typeof value === 'function') {\n throw new Error(`canonicalJson: refusing to encode ${typeof value} — include all fields explicitly`)\n }\n if (Array.isArray(value)) return '[' + value.map((v) => canonicalJson(v)).join(',') + ']'\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n const parts: string[] = []\n for (const key of keys) parts.push(JSON.stringify(key) + ':' + canonicalJson(obj[key]))\n return '{' + parts.join(',') + '}'\n }\n throw new Error(`canonicalJson: unexpected value type: ${typeof value}`)\n}\n\nexport async function sha256Bytes(input: string): Promise<Uint8Array> {\n const digest = await globalThis.crypto.subtle.digest('SHA-256', utf8(input) as BufferSource)\n return new Uint8Array(digest)\n}\n\nexport async function sha256Hex(input: string): Promise<string> {\n return bytesToHex(await sha256Bytes(input))\n}\n","import type { AttestationFieldSchema, Normalizer } from './types.js'\n\nconst NORMALIZERS: ReadonlySet<string> = new Set<Normalizer>([\n 'trim', 'lower', 'upper', 'alnum-upper', 'digits', 'cents', 'iso-date',\n])\n\nexport function getPath(obj: unknown, path: string): unknown {\n return path.split('.').reduce<unknown>(\n (o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]),\n obj,\n )\n}\n\n/**\n * Normalize a field value to a canonical string for hashing.\n *\n * `iso-date` returns the **UTC** calendar date (`toISOString().slice(0,10)`),\n * so a local-time `Date` near midnight can shift by ±1 day in non-UTC\n * environments — prefer passing an ISO date string (e.g. `'2026-05-29'`)\n * over a local `new Date(2026, 4, 29)`. Issue and verify use this same\n * function, so a value passed identically on both sides always round-trips.\n */\nexport function normalizeField(value: unknown, n: Normalizer): string {\n switch (n) {\n case 'trim':\n return String(value).trim()\n case 'lower':\n return String(value).trim().toLowerCase()\n case 'upper':\n return String(value).trim().toUpperCase()\n case 'alnum-upper':\n return String(value).replace(/[^A-Za-z0-9]/g, '').toUpperCase()\n case 'digits':\n return String(value).replace(/[^0-9]/g, '')\n case 'cents': {\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`)\n }\n return String(Math.round(value * 100))\n }\n // For string values: strip currency symbols / spaces / commas but keep digits, dot, minus\n const stripped = String(value).replace(/[^0-9.-]/g, '')\n // A valid numeric string must have at least one digit\n if (!/[0-9]/.test(stripped)) {\n throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`)\n }\n const num = Number(stripped)\n if (!Number.isFinite(num)) {\n throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`)\n }\n return String(Math.round(num * 100))\n }\n case 'iso-date': {\n const d = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(d.getTime())) {\n throw new Error(`normalizeField(iso-date): unparseable date: ${String(value)}`)\n }\n return d.toISOString().slice(0, 10)\n }\n default: {\n const exhaustive: never = n\n throw new Error(`normalizeField: unknown normalizer ${String(exhaustive)}`)\n }\n }\n}\n\nexport function validateFieldSchema(schema: AttestationFieldSchema): void {\n if (!schema.fields || schema.fields.length === 0) {\n throw new Error('validateFieldSchema: schema must declare at least one field')\n }\n const seen = new Set<string>()\n for (const f of schema.fields) {\n if (!NORMALIZERS.has(f.normalize)) {\n throw new Error(\n `validateFieldSchema: unknown normalizer '${String(f.normalize)}' for path '${f.path}'`,\n )\n }\n if (seen.has(f.path)) {\n throw new Error(`validateFieldSchema: duplicate path '${f.path}'`)\n }\n seen.add(f.path)\n }\n}\n","import type { AttestationFieldSchema } from './types.js'\nimport { canonicalJson, sha256Bytes, bytesToB64url } from './encoding.js'\nimport { getPath, normalizeField } from './normalize.js'\n\n/**\n * One salted, domain-separated hash per declared field, in schema order:\n * fieldHash[i] = base64url( sha256( canonicalJson([salt, path, normalizedValue]) ) )\n * Per-document salt defeats brute-force of low-entropy fields and cross-\n * document correlation; the path in the input domain-separates fields\n * that happen to share a value.\n */\nexport async function computeFieldHashes(\n saltB64: string,\n schema: AttestationFieldSchema,\n values: Record<string, unknown>,\n): Promise<string[]> {\n const out: string[] = []\n for (const f of schema.fields) {\n const raw = getPath(values, f.path)\n if (raw === undefined || raw === null) {\n throw new Error(`computeFieldHashes: missing value at declared path '${f.path}'`)\n }\n const norm = normalizeField(raw, f.normalize)\n const digest = await sha256Bytes(canonicalJson([saltB64, f.path, norm]))\n out.push(bytesToB64url(digest))\n }\n return out\n}\n","import { bytesToB64url, b64urlToBytes, sha256Hex } from './encoding.js'\n\nconst ALG = 'Ed25519'\n\n/** Stable key identifier: first 16 hex chars of sha256(publicKeyB64). */\nexport async function keyIdFor(publicKeyB64: string): Promise<string> {\n return (await sha256Hex(publicKeyB64)).slice(0, 16)\n}\n\nexport async function generateDocSigningKeyPair(): Promise<{\n keyId: string\n publicKeyB64: string // base64url raw (32 bytes) — non-secret, publishable\n privateKeyPkcs8B64: string // base64url pkcs8 — secret, wrap before persisting\n}> {\n const kp = await globalThis.crypto.subtle.generateKey(ALG, true, ['sign', 'verify'])\n const rawPub = new Uint8Array(await globalThis.crypto.subtle.exportKey('raw', kp.publicKey))\n const pkcs8 = new Uint8Array(await globalThis.crypto.subtle.exportKey('pkcs8', kp.privateKey))\n const publicKeyB64 = bytesToB64url(rawPub)\n return { keyId: await keyIdFor(publicKeyB64), publicKeyB64, privateKeyPkcs8B64: bytesToB64url(pkcs8) }\n}\n\nexport async function ed25519Sign(privateKeyPkcs8B64: string, message: Uint8Array): Promise<string> {\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n b64urlToBytes(privateKeyPkcs8B64) as BufferSource,\n ALG,\n false,\n ['sign'],\n )\n const sig = new Uint8Array(await globalThis.crypto.subtle.sign(ALG, key, message as BufferSource))\n return bytesToB64url(sig)\n}\n\nexport async function ed25519Verify(publicKeyB64: string, sigB64url: string, message: Uint8Array): Promise<boolean> {\n try {\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n b64urlToBytes(publicKeyB64) as BufferSource,\n ALG,\n false,\n ['verify'],\n )\n return await globalThis.crypto.subtle.verify(\n ALG,\n key,\n b64urlToBytes(sigB64url) as BufferSource,\n message as BufferSource,\n )\n } catch {\n return false\n }\n}\n","import { bytesToB64url, b64urlToBytes, utf8 } from './encoding.js'\n\nexport interface QrPayload {\n readonly v: 1\n readonly docId: string\n readonly salt: string\n readonly alg: 'ed25519'\n readonly keyId: string\n readonly fieldHashes: readonly string[]\n readonly sig: string\n}\n\n/** Compact JSON → base64url. (CBOR + base45 density optimisation deferred.) */\nexport function encodeQr(p: QrPayload): string {\n return bytesToB64url(utf8(JSON.stringify(p)))\n}\n\nexport function decodeQr(s: string): QrPayload {\n let parsed: unknown\n try {\n parsed = JSON.parse(new TextDecoder().decode(b64urlToBytes(s)))\n } catch {\n throw new Error('decodeQr: invalid base64url-encoded JSON payload')\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('decodeQr: invalid payload — expected a JSON object')\n }\n const p = parsed as Record<string, unknown>\n if (p['v'] !== 1) throw new Error(`decodeQr: unsupported version ${String(p['v'])} (expected 1)`)\n if (typeof p['docId'] !== 'string' || typeof p['salt'] !== 'string' || p['alg'] !== 'ed25519'\n || typeof p['keyId'] !== 'string' || typeof p['sig'] !== 'string'\n || !Array.isArray(p['fieldHashes']) || !p['fieldHashes'].every((h) => typeof h === 'string')) {\n throw new Error('decodeQr: invalid payload shape')\n }\n return {\n v: 1, docId: p['docId'], salt: p['salt'], alg: 'ed25519',\n keyId: p['keyId'], fieldHashes: p['fieldHashes'], sig: p['sig'],\n }\n}\n","import { canonicalJson, utf8 } from './encoding.js'\nimport { ed25519Sign, ed25519Verify } from './ed25519.js'\n\nexport interface RevocationList {\n readonly v: 1\n readonly revokedDocIds: readonly string[]\n readonly asOf: string\n readonly keyId: string\n readonly sig: string\n}\n\nfunction listCore(revokedDocIds: readonly string[], asOf: string, keyId: string): Uint8Array {\n return utf8(canonicalJson({ v: 1, revokedDocIds: [...revokedDocIds].sort(), asOf, keyId }))\n}\n\nexport function isRevoked(docId: string, list: RevocationList): boolean {\n // The list is untrusted (typically network-fetched). Fail closed on a\n // malformed shape rather than throwing a raw TypeError.\n if (!Array.isArray(list?.revokedDocIds)) return false\n return list.revokedDocIds.includes(docId)\n}\n\nexport async function signRevocationList(\n revokedDocIds: readonly string[], asOf: string, keyId: string, privateKeyPkcs8B64: string,\n): Promise<RevocationList> {\n const sorted = [...revokedDocIds].sort()\n const sig = await ed25519Sign(privateKeyPkcs8B64, listCore(sorted, asOf, keyId))\n return { v: 1, revokedDocIds: sorted, asOf, keyId, sig }\n}\n\nexport async function verifyRevocationList(list: RevocationList, publicKeyB64: string): Promise<boolean> {\n // Untrusted input — validate the shape before touching it (no raw TypeError).\n if (list?.v !== 1 || !Array.isArray(list.revokedDocIds)\n || typeof list.asOf !== 'string' || typeof list.keyId !== 'string' || typeof list.sig !== 'string') {\n return false\n }\n return ed25519Verify(publicKeyB64, list.sig, listCore(list.revokedDocIds, list.asOf, list.keyId))\n}\n","import type { AttestationFieldSchema } from './types.js'\nimport type { QrPayload } from './qr.js'\nimport type { RevocationList } from './revocation.js'\nimport { canonicalJson, utf8 } from './encoding.js'\nimport { ed25519Sign, ed25519Verify } from './ed25519.js'\nimport { computeFieldHashes } from './hashing.js'\nimport { decodeQr } from './qr.js'\nimport { isRevoked } from './revocation.js'\n\nexport interface VerifyInput {\n readonly qr: string\n readonly claimedFields: Record<string, unknown>\n readonly fieldSchema: AttestationFieldSchema\n readonly publicKeys: Readonly<Record<string, string>>\n readonly revocation?: { list: RevocationList }\n}\nexport interface VerifyResult {\n readonly valid: boolean\n readonly signatureValid: boolean\n readonly perField: ReadonlyArray<{ path: string; match: boolean }>\n readonly revoked: boolean | null\n readonly reason?: string\n}\n\n/**\n * The bytes the signature covers: canonicalJson of the payload minus `alg`/`sig`.\n * Excludes `alg` by design — v1 has exactly one algorithm (`ed25519`). If a `v:2`\n * ever introduces a second algorithm, `alg` MUST be added here to prevent a\n * downgrade attack.\n */\nfunction signedCore(core: { v: 1; docId: string; salt: string; keyId: string; fieldHashes: readonly string[] }): Uint8Array {\n return utf8(canonicalJson({ v: core.v, docId: core.docId, salt: core.salt, keyId: core.keyId, fieldHashes: core.fieldHashes }))\n}\n\nexport async function signPayloadCore(\n core: { v: 1; docId: string; salt: string; keyId: string; fieldHashes: readonly string[] },\n privateKeyPkcs8B64: string,\n): Promise<string> {\n return ed25519Sign(privateKeyPkcs8B64, signedCore(core))\n}\n\nexport async function verifyAttestation(input: VerifyInput): Promise<VerifyResult> {\n const p: QrPayload = decodeQr(input.qr)\n const pub = input.publicKeys[p.keyId]\n const signatureValid = pub\n ? await ed25519Verify(pub, p.sig, signedCore({ v: p.v, docId: p.docId, salt: p.salt, keyId: p.keyId, fieldHashes: p.fieldHashes }))\n : false\n\n const schema = input.fieldSchema\n const perField: Array<{ path: string; match: boolean }> = []\n let allMatch = true\n let countMismatch = false\n if (schema.fields.length !== p.fieldHashes.length) {\n countMismatch = true\n allMatch = false\n for (const f of schema.fields) perField.push({ path: f.path, match: false })\n } else {\n const recomputed = await computeFieldHashes(p.salt, schema, input.claimedFields)\n for (let i = 0; i < schema.fields.length; i++) {\n const match = recomputed[i] === p.fieldHashes[i]\n perField.push({ path: schema.fields[i]!.path, match })\n if (!match) allMatch = false\n }\n }\n\n // Membership check only — the CALLER must have verified the list's own\n // signature with `verifyRevocationList` before passing it here. A list that\n // omits a genuinely-revoked id lets that doc pass; that risk is the caller's.\n const revoked = input.revocation ? isRevoked(p.docId, input.revocation.list) : null\n const valid = signatureValid && allMatch && revoked !== true\n\n let reason: string | undefined\n if (!signatureValid) reason = pub ? 'signature invalid' : 'unknown keyId'\n else if (countMismatch) reason = 'schema/payload field-count mismatch'\n else if (!allMatch) reason = 'field mismatch'\n else if (revoked === true) reason = 'revoked'\n\n return reason !== undefined\n ? { valid, signatureValid, perField, revoked, reason }\n : { valid, signatureValid, perField, revoked }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACUO,SAAS,KAAK,GAAuB;AAC1C,SAAO,IAAI,YAAY,EAAE,OAAO,CAAC;AACnC;AAEO,SAAS,WAAW,OAA2B;AACpD,MAAI,MAAM;AACV,aAAW,KAAK,MAAO,QAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC5D,SAAO;AACT;AAEO,SAAS,cAAc,OAA2B;AACvD,MAAI,IAAI;AACR,aAAW,KAAK,MAAO,MAAK,OAAO,aAAa,CAAC;AACjD,SAAO,KAAK,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC1E;AAEO,SAAS,cAAc,GAAuB;AACnD,QAAM,MAAM,EAAE,SAAS,MAAM,IAAI,KAAK,IAAI,OAAO,IAAK,EAAE,SAAS,CAAE;AACnE,QAAM,MAAM,EAAE,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,IAAI;AACtD,QAAM,MAAM,KAAK,GAAG;AACpB,QAAM,MAAM,IAAI,WAAW,IAAI,MAAM;AACrC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,KAAI,CAAC,IAAI,IAAI,WAAW,CAAC;AAC9D,SAAO;AACT;AAEO,SAAS,cAAc,OAAwB;AACpD,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,OAAO,UAAU,UAAW,QAAO,QAAQ,SAAS;AACxD,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,YAAM,IAAI,MAAM,uDAAuD,OAAO,KAAK,CAAC,EAAE;AAAA,IACxF;AACA,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AACA,MAAI,OAAO,UAAU,SAAU,QAAO,KAAK,UAAU,KAAK;AAC1D,MAAI,OAAO,UAAU,SAAU,OAAM,IAAI,MAAM,gDAAgD;AAC/F,MAAI,OAAO,UAAU,eAAe,OAAO,UAAU,YAAY;AAC/D,UAAM,IAAI,MAAM,qCAAqC,OAAO,KAAK,uCAAkC;AAAA,EACrG;AACA,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,MAAM,IAAI,CAAC,MAAM,cAAc,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI;AACtF,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,MAAM;AACZ,UAAM,OAAO,OAAO,KAAK,GAAG,EAAE,KAAK;AACnC,UAAM,QAAkB,CAAC;AACzB,eAAW,OAAO,KAAM,OAAM,KAAK,KAAK,UAAU,GAAG,IAAI,MAAM,cAAc,IAAI,GAAG,CAAC,CAAC;AACtF,WAAO,MAAM,MAAM,KAAK,GAAG,IAAI;AAAA,EACjC;AACA,QAAM,IAAI,MAAM,yCAAyC,OAAO,KAAK,EAAE;AACzE;AAEA,eAAsB,YAAY,OAAoC;AACpE,QAAM,SAAS,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,KAAK,KAAK,CAAiB;AAC3F,SAAO,IAAI,WAAW,MAAM;AAC9B;AAEA,eAAsB,UAAU,OAAgC;AAC9D,SAAO,WAAW,MAAM,YAAY,KAAK,CAAC;AAC5C;;;ACjEA,IAAM,cAAmC,oBAAI,IAAgB;AAAA,EAC3D;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAS;AAAA,EAAe;AAAA,EAAU;AAAA,EAAS;AAC9D,CAAC;AAEM,SAAS,QAAQ,KAAc,MAAuB;AAC3D,SAAO,KAAK,MAAM,GAAG,EAAE;AAAA,IACrB,CAAC,GAAG,MAAO,KAAK,OAAO,SAAa,EAA8B,CAAC;AAAA,IACnE;AAAA,EACF;AACF;AAWO,SAAS,eAAe,OAAgB,GAAuB;AACpE,UAAQ,GAAG;AAAA,IACT,KAAK;AACH,aAAO,OAAO,KAAK,EAAE,KAAK;AAAA,IAC5B,KAAK;AACH,aAAO,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY;AAAA,IAC1C,KAAK;AACH,aAAO,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY;AAAA,IAC1C,KAAK;AACH,aAAO,OAAO,KAAK,EAAE,QAAQ,iBAAiB,EAAE,EAAE,YAAY;AAAA,IAChE,KAAK;AACH,aAAO,OAAO,KAAK,EAAE,QAAQ,WAAW,EAAE;AAAA,IAC5C,KAAK,SAAS;AACZ,UAAI,OAAO,UAAU,UAAU;AAC7B,YAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,gBAAM,IAAI,MAAM,+CAA+C,OAAO,KAAK,CAAC,EAAE;AAAA,QAChF;AACA,eAAO,OAAO,KAAK,MAAM,QAAQ,GAAG,CAAC;AAAA,MACvC;AAEA,YAAM,WAAW,OAAO,KAAK,EAAE,QAAQ,aAAa,EAAE;AAEtD,UAAI,CAAC,QAAQ,KAAK,QAAQ,GAAG;AAC3B,cAAM,IAAI,MAAM,+CAA+C,OAAO,KAAK,CAAC,EAAE;AAAA,MAChF;AACA,YAAM,MAAM,OAAO,QAAQ;AAC3B,UAAI,CAAC,OAAO,SAAS,GAAG,GAAG;AACzB,cAAM,IAAI,MAAM,+CAA+C,OAAO,KAAK,CAAC,EAAE;AAAA,MAChF;AACA,aAAO,OAAO,KAAK,MAAM,MAAM,GAAG,CAAC;AAAA,IACrC;AAAA,IACA,KAAK,YAAY;AACf,YAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,OAAO,KAAK,CAAC;AAChE,UAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,GAAG;AAC7B,cAAM,IAAI,MAAM,+CAA+C,OAAO,KAAK,CAAC,EAAE;AAAA,MAChF;AACA,aAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAAA,IACpC;AAAA,IACA,SAAS;AACP,YAAM,aAAoB;AAC1B,YAAM,IAAI,MAAM,sCAAsC,OAAO,UAAU,CAAC,EAAE;AAAA,IAC5E;AAAA,EACF;AACF;AAEO,SAAS,oBAAoB,QAAsC;AACxE,MAAI,CAAC,OAAO,UAAU,OAAO,OAAO,WAAW,GAAG;AAChD,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACA,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,OAAO,QAAQ;AAC7B,QAAI,CAAC,YAAY,IAAI,EAAE,SAAS,GAAG;AACjC,YAAM,IAAI;AAAA,QACR,4CAA4C,OAAO,EAAE,SAAS,CAAC,eAAe,EAAE,IAAI;AAAA,MACtF;AAAA,IACF;AACA,QAAI,KAAK,IAAI,EAAE,IAAI,GAAG;AACpB,YAAM,IAAI,MAAM,wCAAwC,EAAE,IAAI,GAAG;AAAA,IACnE;AACA,SAAK,IAAI,EAAE,IAAI;AAAA,EACjB;AACF;;;ACxEA,eAAsB,mBACpB,SACA,QACA,QACmB;AACnB,QAAM,MAAgB,CAAC;AACvB,aAAW,KAAK,OAAO,QAAQ;AAC7B,UAAM,MAAM,QAAQ,QAAQ,EAAE,IAAI;AAClC,QAAI,QAAQ,UAAa,QAAQ,MAAM;AACrC,YAAM,IAAI,MAAM,uDAAuD,EAAE,IAAI,GAAG;AAAA,IAClF;AACA,UAAM,OAAO,eAAe,KAAK,EAAE,SAAS;AAC5C,UAAM,SAAS,MAAM,YAAY,cAAc,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC,CAAC;AACvE,QAAI,KAAK,cAAc,MAAM,CAAC;AAAA,EAChC;AACA,SAAO;AACT;;;ACzBA,IAAM,MAAM;AAGZ,eAAsB,SAAS,cAAuC;AACpE,UAAQ,MAAM,UAAU,YAAY,GAAG,MAAM,GAAG,EAAE;AACpD;AAEA,eAAsB,4BAInB;AACD,QAAM,KAAK,MAAM,WAAW,OAAO,OAAO,YAAY,KAAK,MAAM,CAAC,QAAQ,QAAQ,CAAC;AACnF,QAAM,SAAS,IAAI,WAAW,MAAM,WAAW,OAAO,OAAO,UAAU,OAAO,GAAG,SAAS,CAAC;AAC3F,QAAM,QAAQ,IAAI,WAAW,MAAM,WAAW,OAAO,OAAO,UAAU,SAAS,GAAG,UAAU,CAAC;AAC7F,QAAM,eAAe,cAAc,MAAM;AACzC,SAAO,EAAE,OAAO,MAAM,SAAS,YAAY,GAAG,cAAc,oBAAoB,cAAc,KAAK,EAAE;AACvG;AAEA,eAAsB,YAAY,oBAA4B,SAAsC;AAClG,QAAM,MAAM,MAAM,WAAW,OAAO,OAAO;AAAA,IACzC;AAAA,IACA,cAAc,kBAAkB;AAAA,IAChC;AAAA,IACA;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,QAAM,MAAM,IAAI,WAAW,MAAM,WAAW,OAAO,OAAO,KAAK,KAAK,KAAK,OAAuB,CAAC;AACjG,SAAO,cAAc,GAAG;AAC1B;AAEA,eAAsB,cAAc,cAAsB,WAAmB,SAAuC;AAClH,MAAI;AACF,UAAM,MAAM,MAAM,WAAW,OAAO,OAAO;AAAA,MACzC;AAAA,MACA,cAAc,YAAY;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,CAAC,QAAQ;AAAA,IACX;AACA,WAAO,MAAM,WAAW,OAAO,OAAO;AAAA,MACpC;AAAA,MACA;AAAA,MACA,cAAc,SAAS;AAAA,MACvB;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACtCO,SAAS,SAAS,GAAsB;AAC7C,SAAO,cAAc,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC;AAC9C;AAEO,SAAS,SAAS,GAAsB;AAC7C,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,cAAc,CAAC,CAAC,CAAC;AAAA,EAChE,QAAQ;AACN,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,MAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAC1E,UAAM,IAAI,MAAM,yDAAoD;AAAA,EACtE;AACA,QAAM,IAAI;AACV,MAAI,EAAE,GAAG,MAAM,EAAG,OAAM,IAAI,MAAM,iCAAiC,OAAO,EAAE,GAAG,CAAC,CAAC,eAAe;AAChG,MAAI,OAAO,EAAE,OAAO,MAAM,YAAY,OAAO,EAAE,MAAM,MAAM,YAAY,EAAE,KAAK,MAAM,aAC7E,OAAO,EAAE,OAAO,MAAM,YAAY,OAAO,EAAE,KAAK,MAAM,YACtD,CAAC,MAAM,QAAQ,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ,GAAG;AAChG,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACnD;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IAAG,OAAO,EAAE,OAAO;AAAA,IAAG,MAAM,EAAE,MAAM;AAAA,IAAG,KAAK;AAAA,IAC/C,OAAO,EAAE,OAAO;AAAA,IAAG,aAAa,EAAE,aAAa;AAAA,IAAG,KAAK,EAAE,KAAK;AAAA,EAChE;AACF;;;AC3BA,SAAS,SAAS,eAAkC,MAAc,OAA2B;AAC3F,SAAO,KAAK,cAAc,EAAE,GAAG,GAAG,eAAe,CAAC,GAAG,aAAa,EAAE,KAAK,GAAG,MAAM,MAAM,CAAC,CAAC;AAC5F;AAEO,SAAS,UAAU,OAAe,MAA+B;AAGtE,MAAI,CAAC,MAAM,QAAQ,MAAM,aAAa,EAAG,QAAO;AAChD,SAAO,KAAK,cAAc,SAAS,KAAK;AAC1C;AAEA,eAAsB,mBACpB,eAAkC,MAAc,OAAe,oBACtC;AACzB,QAAM,SAAS,CAAC,GAAG,aAAa,EAAE,KAAK;AACvC,QAAM,MAAM,MAAM,YAAY,oBAAoB,SAAS,QAAQ,MAAM,KAAK,CAAC;AAC/E,SAAO,EAAE,GAAG,GAAG,eAAe,QAAQ,MAAM,OAAO,IAAI;AACzD;AAEA,eAAsB,qBAAqB,MAAsB,cAAwC;AAEvG,MAAI,MAAM,MAAM,KAAK,CAAC,MAAM,QAAQ,KAAK,aAAa,KAC/C,OAAO,KAAK,SAAS,YAAY,OAAO,KAAK,UAAU,YAAY,OAAO,KAAK,QAAQ,UAAU;AACtG,WAAO;AAAA,EACT;AACA,SAAO,cAAc,cAAc,KAAK,KAAK,SAAS,KAAK,eAAe,KAAK,MAAM,KAAK,KAAK,CAAC;AAClG;;;ACPA,SAAS,WAAW,MAAwG;AAC1H,SAAO,KAAK,cAAc,EAAE,GAAG,KAAK,GAAG,OAAO,KAAK,OAAO,MAAM,KAAK,MAAM,OAAO,KAAK,OAAO,aAAa,KAAK,YAAY,CAAC,CAAC;AAChI;AAEA,eAAsB,gBACpB,MACA,oBACiB;AACjB,SAAO,YAAY,oBAAoB,WAAW,IAAI,CAAC;AACzD;AAEA,eAAsB,kBAAkB,OAA2C;AACjF,QAAM,IAAe,SAAS,MAAM,EAAE;AACtC,QAAM,MAAM,MAAM,WAAW,EAAE,KAAK;AACpC,QAAM,iBAAiB,MACnB,MAAM,cAAc,KAAK,EAAE,KAAK,WAAW,EAAE,GAAG,EAAE,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,aAAa,EAAE,YAAY,CAAC,CAAC,IAChI;AAEJ,QAAM,SAAS,MAAM;AACrB,QAAM,WAAoD,CAAC;AAC3D,MAAI,WAAW;AACf,MAAI,gBAAgB;AACpB,MAAI,OAAO,OAAO,WAAW,EAAE,YAAY,QAAQ;AACjD,oBAAgB;AAChB,eAAW;AACX,eAAW,KAAK,OAAO,OAAQ,UAAS,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,MAAM,CAAC;AAAA,EAC7E,OAAO;AACL,UAAM,aAAa,MAAM,mBAAmB,EAAE,MAAM,QAAQ,MAAM,aAAa;AAC/E,aAAS,IAAI,GAAG,IAAI,OAAO,OAAO,QAAQ,KAAK;AAC7C,YAAM,QAAQ,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC;AAC/C,eAAS,KAAK,EAAE,MAAM,OAAO,OAAO,CAAC,EAAG,MAAM,MAAM,CAAC;AACrD,UAAI,CAAC,MAAO,YAAW;AAAA,IACzB;AAAA,EACF;AAKA,QAAM,UAAU,MAAM,aAAa,UAAU,EAAE,OAAO,MAAM,WAAW,IAAI,IAAI;AAC/E,QAAM,QAAQ,kBAAkB,YAAY,YAAY;AAExD,MAAI;AACJ,MAAI,CAAC,eAAgB,UAAS,MAAM,sBAAsB;AAAA,WACjD,cAAe,UAAS;AAAA,WACxB,CAAC,SAAU,UAAS;AAAA,WACpB,YAAY,KAAM,UAAS;AAEpC,SAAO,WAAW,SACd,EAAE,OAAO,gBAAgB,UAAU,SAAS,OAAO,IACnD,EAAE,OAAO,gBAAgB,UAAU,QAAQ;AACjD;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
type Normalizer = 'trim' | 'lower' | 'upper' | 'alnum-upper' | 'digits' | 'cents' | 'iso-date';
|
|
2
|
+
interface AttestationFieldSpec {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly normalize: Normalizer;
|
|
5
|
+
}
|
|
6
|
+
interface AttestationFieldSchema {
|
|
7
|
+
readonly fields: readonly AttestationFieldSpec[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface QrPayload {
|
|
11
|
+
readonly v: 1;
|
|
12
|
+
readonly docId: string;
|
|
13
|
+
readonly salt: string;
|
|
14
|
+
readonly alg: 'ed25519';
|
|
15
|
+
readonly keyId: string;
|
|
16
|
+
readonly fieldHashes: readonly string[];
|
|
17
|
+
readonly sig: string;
|
|
18
|
+
}
|
|
19
|
+
/** Compact JSON → base64url. (CBOR + base45 density optimisation deferred.) */
|
|
20
|
+
declare function encodeQr(p: QrPayload): string;
|
|
21
|
+
declare function decodeQr(s: string): QrPayload;
|
|
22
|
+
|
|
23
|
+
interface RevocationList {
|
|
24
|
+
readonly v: 1;
|
|
25
|
+
readonly revokedDocIds: readonly string[];
|
|
26
|
+
readonly asOf: string;
|
|
27
|
+
readonly keyId: string;
|
|
28
|
+
readonly sig: string;
|
|
29
|
+
}
|
|
30
|
+
declare function isRevoked(docId: string, list: RevocationList): boolean;
|
|
31
|
+
declare function signRevocationList(revokedDocIds: readonly string[], asOf: string, keyId: string, privateKeyPkcs8B64: string): Promise<RevocationList>;
|
|
32
|
+
declare function verifyRevocationList(list: RevocationList, publicKeyB64: string): Promise<boolean>;
|
|
33
|
+
|
|
34
|
+
interface VerifyInput {
|
|
35
|
+
readonly qr: string;
|
|
36
|
+
readonly claimedFields: Record<string, unknown>;
|
|
37
|
+
readonly fieldSchema: AttestationFieldSchema;
|
|
38
|
+
readonly publicKeys: Readonly<Record<string, string>>;
|
|
39
|
+
readonly revocation?: {
|
|
40
|
+
list: RevocationList;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
interface VerifyResult {
|
|
44
|
+
readonly valid: boolean;
|
|
45
|
+
readonly signatureValid: boolean;
|
|
46
|
+
readonly perField: ReadonlyArray<{
|
|
47
|
+
path: string;
|
|
48
|
+
match: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
readonly revoked: boolean | null;
|
|
51
|
+
readonly reason?: string;
|
|
52
|
+
}
|
|
53
|
+
declare function signPayloadCore(core: {
|
|
54
|
+
v: 1;
|
|
55
|
+
docId: string;
|
|
56
|
+
salt: string;
|
|
57
|
+
keyId: string;
|
|
58
|
+
fieldHashes: readonly string[];
|
|
59
|
+
}, privateKeyPkcs8B64: string): Promise<string>;
|
|
60
|
+
declare function verifyAttestation(input: VerifyInput): Promise<VerifyResult>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pure encoding + hashing primitives. Zero deps; WebCrypto only.
|
|
64
|
+
*
|
|
65
|
+
* `canonicalJson` and `sha256Hex` are intentionally byte-identical to
|
|
66
|
+
* hub's `history/ledger/entry.ts` implementations. They are REPLICATED
|
|
67
|
+
* here (not imported) because this package is upstream of hub — importing
|
|
68
|
+
* from hub would invert the dependency. The conformance test pins the
|
|
69
|
+
* shared contract via fixed vectors.
|
|
70
|
+
*/
|
|
71
|
+
declare function utf8(s: string): Uint8Array;
|
|
72
|
+
declare function bytesToHex(bytes: Uint8Array): string;
|
|
73
|
+
declare function bytesToB64url(bytes: Uint8Array): string;
|
|
74
|
+
declare function b64urlToBytes(s: string): Uint8Array;
|
|
75
|
+
declare function canonicalJson(value: unknown): string;
|
|
76
|
+
declare function sha256Bytes(input: string): Promise<Uint8Array>;
|
|
77
|
+
declare function sha256Hex(input: string): Promise<string>;
|
|
78
|
+
|
|
79
|
+
declare function getPath(obj: unknown, path: string): unknown;
|
|
80
|
+
/**
|
|
81
|
+
* Normalize a field value to a canonical string for hashing.
|
|
82
|
+
*
|
|
83
|
+
* `iso-date` returns the **UTC** calendar date (`toISOString().slice(0,10)`),
|
|
84
|
+
* so a local-time `Date` near midnight can shift by ±1 day in non-UTC
|
|
85
|
+
* environments — prefer passing an ISO date string (e.g. `'2026-05-29'`)
|
|
86
|
+
* over a local `new Date(2026, 4, 29)`. Issue and verify use this same
|
|
87
|
+
* function, so a value passed identically on both sides always round-trips.
|
|
88
|
+
*/
|
|
89
|
+
declare function normalizeField(value: unknown, n: Normalizer): string;
|
|
90
|
+
declare function validateFieldSchema(schema: AttestationFieldSchema): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One salted, domain-separated hash per declared field, in schema order:
|
|
94
|
+
* fieldHash[i] = base64url( sha256( canonicalJson([salt, path, normalizedValue]) ) )
|
|
95
|
+
* Per-document salt defeats brute-force of low-entropy fields and cross-
|
|
96
|
+
* document correlation; the path in the input domain-separates fields
|
|
97
|
+
* that happen to share a value.
|
|
98
|
+
*/
|
|
99
|
+
declare function computeFieldHashes(saltB64: string, schema: AttestationFieldSchema, values: Record<string, unknown>): Promise<string[]>;
|
|
100
|
+
|
|
101
|
+
/** Stable key identifier: first 16 hex chars of sha256(publicKeyB64). */
|
|
102
|
+
declare function keyIdFor(publicKeyB64: string): Promise<string>;
|
|
103
|
+
declare function generateDocSigningKeyPair(): Promise<{
|
|
104
|
+
keyId: string;
|
|
105
|
+
publicKeyB64: string;
|
|
106
|
+
privateKeyPkcs8B64: string;
|
|
107
|
+
}>;
|
|
108
|
+
declare function ed25519Sign(privateKeyPkcs8B64: string, message: Uint8Array): Promise<string>;
|
|
109
|
+
declare function ed25519Verify(publicKeyB64: string, sigB64url: string, message: Uint8Array): Promise<boolean>;
|
|
110
|
+
|
|
111
|
+
export { type AttestationFieldSchema, type AttestationFieldSpec, type Normalizer, type QrPayload, type RevocationList, type VerifyInput, type VerifyResult, b64urlToBytes, bytesToB64url, bytesToHex, canonicalJson, computeFieldHashes, decodeQr, ed25519Sign, ed25519Verify, encodeQr, generateDocSigningKeyPair, getPath, isRevoked, keyIdFor, normalizeField, sha256Bytes, sha256Hex, signPayloadCore, signRevocationList, utf8, validateFieldSchema, verifyAttestation, verifyRevocationList };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
type Normalizer = 'trim' | 'lower' | 'upper' | 'alnum-upper' | 'digits' | 'cents' | 'iso-date';
|
|
2
|
+
interface AttestationFieldSpec {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly normalize: Normalizer;
|
|
5
|
+
}
|
|
6
|
+
interface AttestationFieldSchema {
|
|
7
|
+
readonly fields: readonly AttestationFieldSpec[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface QrPayload {
|
|
11
|
+
readonly v: 1;
|
|
12
|
+
readonly docId: string;
|
|
13
|
+
readonly salt: string;
|
|
14
|
+
readonly alg: 'ed25519';
|
|
15
|
+
readonly keyId: string;
|
|
16
|
+
readonly fieldHashes: readonly string[];
|
|
17
|
+
readonly sig: string;
|
|
18
|
+
}
|
|
19
|
+
/** Compact JSON → base64url. (CBOR + base45 density optimisation deferred.) */
|
|
20
|
+
declare function encodeQr(p: QrPayload): string;
|
|
21
|
+
declare function decodeQr(s: string): QrPayload;
|
|
22
|
+
|
|
23
|
+
interface RevocationList {
|
|
24
|
+
readonly v: 1;
|
|
25
|
+
readonly revokedDocIds: readonly string[];
|
|
26
|
+
readonly asOf: string;
|
|
27
|
+
readonly keyId: string;
|
|
28
|
+
readonly sig: string;
|
|
29
|
+
}
|
|
30
|
+
declare function isRevoked(docId: string, list: RevocationList): boolean;
|
|
31
|
+
declare function signRevocationList(revokedDocIds: readonly string[], asOf: string, keyId: string, privateKeyPkcs8B64: string): Promise<RevocationList>;
|
|
32
|
+
declare function verifyRevocationList(list: RevocationList, publicKeyB64: string): Promise<boolean>;
|
|
33
|
+
|
|
34
|
+
interface VerifyInput {
|
|
35
|
+
readonly qr: string;
|
|
36
|
+
readonly claimedFields: Record<string, unknown>;
|
|
37
|
+
readonly fieldSchema: AttestationFieldSchema;
|
|
38
|
+
readonly publicKeys: Readonly<Record<string, string>>;
|
|
39
|
+
readonly revocation?: {
|
|
40
|
+
list: RevocationList;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
interface VerifyResult {
|
|
44
|
+
readonly valid: boolean;
|
|
45
|
+
readonly signatureValid: boolean;
|
|
46
|
+
readonly perField: ReadonlyArray<{
|
|
47
|
+
path: string;
|
|
48
|
+
match: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
readonly revoked: boolean | null;
|
|
51
|
+
readonly reason?: string;
|
|
52
|
+
}
|
|
53
|
+
declare function signPayloadCore(core: {
|
|
54
|
+
v: 1;
|
|
55
|
+
docId: string;
|
|
56
|
+
salt: string;
|
|
57
|
+
keyId: string;
|
|
58
|
+
fieldHashes: readonly string[];
|
|
59
|
+
}, privateKeyPkcs8B64: string): Promise<string>;
|
|
60
|
+
declare function verifyAttestation(input: VerifyInput): Promise<VerifyResult>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pure encoding + hashing primitives. Zero deps; WebCrypto only.
|
|
64
|
+
*
|
|
65
|
+
* `canonicalJson` and `sha256Hex` are intentionally byte-identical to
|
|
66
|
+
* hub's `history/ledger/entry.ts` implementations. They are REPLICATED
|
|
67
|
+
* here (not imported) because this package is upstream of hub — importing
|
|
68
|
+
* from hub would invert the dependency. The conformance test pins the
|
|
69
|
+
* shared contract via fixed vectors.
|
|
70
|
+
*/
|
|
71
|
+
declare function utf8(s: string): Uint8Array;
|
|
72
|
+
declare function bytesToHex(bytes: Uint8Array): string;
|
|
73
|
+
declare function bytesToB64url(bytes: Uint8Array): string;
|
|
74
|
+
declare function b64urlToBytes(s: string): Uint8Array;
|
|
75
|
+
declare function canonicalJson(value: unknown): string;
|
|
76
|
+
declare function sha256Bytes(input: string): Promise<Uint8Array>;
|
|
77
|
+
declare function sha256Hex(input: string): Promise<string>;
|
|
78
|
+
|
|
79
|
+
declare function getPath(obj: unknown, path: string): unknown;
|
|
80
|
+
/**
|
|
81
|
+
* Normalize a field value to a canonical string for hashing.
|
|
82
|
+
*
|
|
83
|
+
* `iso-date` returns the **UTC** calendar date (`toISOString().slice(0,10)`),
|
|
84
|
+
* so a local-time `Date` near midnight can shift by ±1 day in non-UTC
|
|
85
|
+
* environments — prefer passing an ISO date string (e.g. `'2026-05-29'`)
|
|
86
|
+
* over a local `new Date(2026, 4, 29)`. Issue and verify use this same
|
|
87
|
+
* function, so a value passed identically on both sides always round-trips.
|
|
88
|
+
*/
|
|
89
|
+
declare function normalizeField(value: unknown, n: Normalizer): string;
|
|
90
|
+
declare function validateFieldSchema(schema: AttestationFieldSchema): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One salted, domain-separated hash per declared field, in schema order:
|
|
94
|
+
* fieldHash[i] = base64url( sha256( canonicalJson([salt, path, normalizedValue]) ) )
|
|
95
|
+
* Per-document salt defeats brute-force of low-entropy fields and cross-
|
|
96
|
+
* document correlation; the path in the input domain-separates fields
|
|
97
|
+
* that happen to share a value.
|
|
98
|
+
*/
|
|
99
|
+
declare function computeFieldHashes(saltB64: string, schema: AttestationFieldSchema, values: Record<string, unknown>): Promise<string[]>;
|
|
100
|
+
|
|
101
|
+
/** Stable key identifier: first 16 hex chars of sha256(publicKeyB64). */
|
|
102
|
+
declare function keyIdFor(publicKeyB64: string): Promise<string>;
|
|
103
|
+
declare function generateDocSigningKeyPair(): Promise<{
|
|
104
|
+
keyId: string;
|
|
105
|
+
publicKeyB64: string;
|
|
106
|
+
privateKeyPkcs8B64: string;
|
|
107
|
+
}>;
|
|
108
|
+
declare function ed25519Sign(privateKeyPkcs8B64: string, message: Uint8Array): Promise<string>;
|
|
109
|
+
declare function ed25519Verify(publicKeyB64: string, sigB64url: string, message: Uint8Array): Promise<boolean>;
|
|
110
|
+
|
|
111
|
+
export { type AttestationFieldSchema, type AttestationFieldSpec, type Normalizer, type QrPayload, type RevocationList, type VerifyInput, type VerifyResult, b64urlToBytes, bytesToB64url, bytesToHex, canonicalJson, computeFieldHashes, decodeQr, ed25519Sign, ed25519Verify, encodeQr, generateDocSigningKeyPair, getPath, isRevoked, keyIdFor, normalizeField, sha256Bytes, sha256Hex, signPayloadCore, signRevocationList, utf8, validateFieldSchema, verifyAttestation, verifyRevocationList };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// src/encoding.ts
|
|
2
|
+
function utf8(s) {
|
|
3
|
+
return new TextEncoder().encode(s);
|
|
4
|
+
}
|
|
5
|
+
function bytesToHex(bytes) {
|
|
6
|
+
let out = "";
|
|
7
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
function bytesToB64url(bytes) {
|
|
11
|
+
let s = "";
|
|
12
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
13
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
14
|
+
}
|
|
15
|
+
function b64urlToBytes(s) {
|
|
16
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
17
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
18
|
+
const bin = atob(b64);
|
|
19
|
+
const out = new Uint8Array(bin.length);
|
|
20
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function canonicalJson(value) {
|
|
24
|
+
if (value === null) return "null";
|
|
25
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
26
|
+
if (typeof value === "number") {
|
|
27
|
+
if (!Number.isFinite(value)) {
|
|
28
|
+
throw new Error(`canonicalJson: refusing to encode non-finite number ${String(value)}`);
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
33
|
+
if (typeof value === "bigint") throw new Error("canonicalJson: BigInt is not JSON-serializable");
|
|
34
|
+
if (typeof value === "undefined" || typeof value === "function") {
|
|
35
|
+
throw new Error(`canonicalJson: refusing to encode ${typeof value} \u2014 include all fields explicitly`);
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value)) return "[" + value.map((v) => canonicalJson(v)).join(",") + "]";
|
|
38
|
+
if (typeof value === "object") {
|
|
39
|
+
const obj = value;
|
|
40
|
+
const keys = Object.keys(obj).sort();
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (const key of keys) parts.push(JSON.stringify(key) + ":" + canonicalJson(obj[key]));
|
|
43
|
+
return "{" + parts.join(",") + "}";
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`canonicalJson: unexpected value type: ${typeof value}`);
|
|
46
|
+
}
|
|
47
|
+
async function sha256Bytes(input) {
|
|
48
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8(input));
|
|
49
|
+
return new Uint8Array(digest);
|
|
50
|
+
}
|
|
51
|
+
async function sha256Hex(input) {
|
|
52
|
+
return bytesToHex(await sha256Bytes(input));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/normalize.ts
|
|
56
|
+
var NORMALIZERS = /* @__PURE__ */ new Set([
|
|
57
|
+
"trim",
|
|
58
|
+
"lower",
|
|
59
|
+
"upper",
|
|
60
|
+
"alnum-upper",
|
|
61
|
+
"digits",
|
|
62
|
+
"cents",
|
|
63
|
+
"iso-date"
|
|
64
|
+
]);
|
|
65
|
+
function getPath(obj, path) {
|
|
66
|
+
return path.split(".").reduce(
|
|
67
|
+
(o, k) => o == null ? void 0 : o[k],
|
|
68
|
+
obj
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function normalizeField(value, n) {
|
|
72
|
+
switch (n) {
|
|
73
|
+
case "trim":
|
|
74
|
+
return String(value).trim();
|
|
75
|
+
case "lower":
|
|
76
|
+
return String(value).trim().toLowerCase();
|
|
77
|
+
case "upper":
|
|
78
|
+
return String(value).trim().toUpperCase();
|
|
79
|
+
case "alnum-upper":
|
|
80
|
+
return String(value).replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
81
|
+
case "digits":
|
|
82
|
+
return String(value).replace(/[^0-9]/g, "");
|
|
83
|
+
case "cents": {
|
|
84
|
+
if (typeof value === "number") {
|
|
85
|
+
if (!Number.isFinite(value)) {
|
|
86
|
+
throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`);
|
|
87
|
+
}
|
|
88
|
+
return String(Math.round(value * 100));
|
|
89
|
+
}
|
|
90
|
+
const stripped = String(value).replace(/[^0-9.-]/g, "");
|
|
91
|
+
if (!/[0-9]/.test(stripped)) {
|
|
92
|
+
throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`);
|
|
93
|
+
}
|
|
94
|
+
const num = Number(stripped);
|
|
95
|
+
if (!Number.isFinite(num)) {
|
|
96
|
+
throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`);
|
|
97
|
+
}
|
|
98
|
+
return String(Math.round(num * 100));
|
|
99
|
+
}
|
|
100
|
+
case "iso-date": {
|
|
101
|
+
const d = value instanceof Date ? value : new Date(String(value));
|
|
102
|
+
if (Number.isNaN(d.getTime())) {
|
|
103
|
+
throw new Error(`normalizeField(iso-date): unparseable date: ${String(value)}`);
|
|
104
|
+
}
|
|
105
|
+
return d.toISOString().slice(0, 10);
|
|
106
|
+
}
|
|
107
|
+
default: {
|
|
108
|
+
const exhaustive = n;
|
|
109
|
+
throw new Error(`normalizeField: unknown normalizer ${String(exhaustive)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function validateFieldSchema(schema) {
|
|
114
|
+
if (!schema.fields || schema.fields.length === 0) {
|
|
115
|
+
throw new Error("validateFieldSchema: schema must declare at least one field");
|
|
116
|
+
}
|
|
117
|
+
const seen = /* @__PURE__ */ new Set();
|
|
118
|
+
for (const f of schema.fields) {
|
|
119
|
+
if (!NORMALIZERS.has(f.normalize)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`validateFieldSchema: unknown normalizer '${String(f.normalize)}' for path '${f.path}'`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (seen.has(f.path)) {
|
|
125
|
+
throw new Error(`validateFieldSchema: duplicate path '${f.path}'`);
|
|
126
|
+
}
|
|
127
|
+
seen.add(f.path);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/hashing.ts
|
|
132
|
+
async function computeFieldHashes(saltB64, schema, values) {
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const f of schema.fields) {
|
|
135
|
+
const raw = getPath(values, f.path);
|
|
136
|
+
if (raw === void 0 || raw === null) {
|
|
137
|
+
throw new Error(`computeFieldHashes: missing value at declared path '${f.path}'`);
|
|
138
|
+
}
|
|
139
|
+
const norm = normalizeField(raw, f.normalize);
|
|
140
|
+
const digest = await sha256Bytes(canonicalJson([saltB64, f.path, norm]));
|
|
141
|
+
out.push(bytesToB64url(digest));
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/ed25519.ts
|
|
147
|
+
var ALG = "Ed25519";
|
|
148
|
+
async function keyIdFor(publicKeyB64) {
|
|
149
|
+
return (await sha256Hex(publicKeyB64)).slice(0, 16);
|
|
150
|
+
}
|
|
151
|
+
async function generateDocSigningKeyPair() {
|
|
152
|
+
const kp = await globalThis.crypto.subtle.generateKey(ALG, true, ["sign", "verify"]);
|
|
153
|
+
const rawPub = new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", kp.publicKey));
|
|
154
|
+
const pkcs8 = new Uint8Array(await globalThis.crypto.subtle.exportKey("pkcs8", kp.privateKey));
|
|
155
|
+
const publicKeyB64 = bytesToB64url(rawPub);
|
|
156
|
+
return { keyId: await keyIdFor(publicKeyB64), publicKeyB64, privateKeyPkcs8B64: bytesToB64url(pkcs8) };
|
|
157
|
+
}
|
|
158
|
+
async function ed25519Sign(privateKeyPkcs8B64, message) {
|
|
159
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
160
|
+
"pkcs8",
|
|
161
|
+
b64urlToBytes(privateKeyPkcs8B64),
|
|
162
|
+
ALG,
|
|
163
|
+
false,
|
|
164
|
+
["sign"]
|
|
165
|
+
);
|
|
166
|
+
const sig = new Uint8Array(await globalThis.crypto.subtle.sign(ALG, key, message));
|
|
167
|
+
return bytesToB64url(sig);
|
|
168
|
+
}
|
|
169
|
+
async function ed25519Verify(publicKeyB64, sigB64url, message) {
|
|
170
|
+
try {
|
|
171
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
172
|
+
"raw",
|
|
173
|
+
b64urlToBytes(publicKeyB64),
|
|
174
|
+
ALG,
|
|
175
|
+
false,
|
|
176
|
+
["verify"]
|
|
177
|
+
);
|
|
178
|
+
return await globalThis.crypto.subtle.verify(
|
|
179
|
+
ALG,
|
|
180
|
+
key,
|
|
181
|
+
b64urlToBytes(sigB64url),
|
|
182
|
+
message
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/qr.ts
|
|
190
|
+
function encodeQr(p) {
|
|
191
|
+
return bytesToB64url(utf8(JSON.stringify(p)));
|
|
192
|
+
}
|
|
193
|
+
function decodeQr(s) {
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(new TextDecoder().decode(b64urlToBytes(s)));
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error("decodeQr: invalid base64url-encoded JSON payload");
|
|
199
|
+
}
|
|
200
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
201
|
+
throw new Error("decodeQr: invalid payload \u2014 expected a JSON object");
|
|
202
|
+
}
|
|
203
|
+
const p = parsed;
|
|
204
|
+
if (p["v"] !== 1) throw new Error(`decodeQr: unsupported version ${String(p["v"])} (expected 1)`);
|
|
205
|
+
if (typeof p["docId"] !== "string" || typeof p["salt"] !== "string" || p["alg"] !== "ed25519" || typeof p["keyId"] !== "string" || typeof p["sig"] !== "string" || !Array.isArray(p["fieldHashes"]) || !p["fieldHashes"].every((h) => typeof h === "string")) {
|
|
206
|
+
throw new Error("decodeQr: invalid payload shape");
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
v: 1,
|
|
210
|
+
docId: p["docId"],
|
|
211
|
+
salt: p["salt"],
|
|
212
|
+
alg: "ed25519",
|
|
213
|
+
keyId: p["keyId"],
|
|
214
|
+
fieldHashes: p["fieldHashes"],
|
|
215
|
+
sig: p["sig"]
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/revocation.ts
|
|
220
|
+
function listCore(revokedDocIds, asOf, keyId) {
|
|
221
|
+
return utf8(canonicalJson({ v: 1, revokedDocIds: [...revokedDocIds].sort(), asOf, keyId }));
|
|
222
|
+
}
|
|
223
|
+
function isRevoked(docId, list) {
|
|
224
|
+
if (!Array.isArray(list?.revokedDocIds)) return false;
|
|
225
|
+
return list.revokedDocIds.includes(docId);
|
|
226
|
+
}
|
|
227
|
+
async function signRevocationList(revokedDocIds, asOf, keyId, privateKeyPkcs8B64) {
|
|
228
|
+
const sorted = [...revokedDocIds].sort();
|
|
229
|
+
const sig = await ed25519Sign(privateKeyPkcs8B64, listCore(sorted, asOf, keyId));
|
|
230
|
+
return { v: 1, revokedDocIds: sorted, asOf, keyId, sig };
|
|
231
|
+
}
|
|
232
|
+
async function verifyRevocationList(list, publicKeyB64) {
|
|
233
|
+
if (list?.v !== 1 || !Array.isArray(list.revokedDocIds) || typeof list.asOf !== "string" || typeof list.keyId !== "string" || typeof list.sig !== "string") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return ed25519Verify(publicKeyB64, list.sig, listCore(list.revokedDocIds, list.asOf, list.keyId));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/verify.ts
|
|
240
|
+
function signedCore(core) {
|
|
241
|
+
return utf8(canonicalJson({ v: core.v, docId: core.docId, salt: core.salt, keyId: core.keyId, fieldHashes: core.fieldHashes }));
|
|
242
|
+
}
|
|
243
|
+
async function signPayloadCore(core, privateKeyPkcs8B64) {
|
|
244
|
+
return ed25519Sign(privateKeyPkcs8B64, signedCore(core));
|
|
245
|
+
}
|
|
246
|
+
async function verifyAttestation(input) {
|
|
247
|
+
const p = decodeQr(input.qr);
|
|
248
|
+
const pub = input.publicKeys[p.keyId];
|
|
249
|
+
const signatureValid = pub ? await ed25519Verify(pub, p.sig, signedCore({ v: p.v, docId: p.docId, salt: p.salt, keyId: p.keyId, fieldHashes: p.fieldHashes })) : false;
|
|
250
|
+
const schema = input.fieldSchema;
|
|
251
|
+
const perField = [];
|
|
252
|
+
let allMatch = true;
|
|
253
|
+
let countMismatch = false;
|
|
254
|
+
if (schema.fields.length !== p.fieldHashes.length) {
|
|
255
|
+
countMismatch = true;
|
|
256
|
+
allMatch = false;
|
|
257
|
+
for (const f of schema.fields) perField.push({ path: f.path, match: false });
|
|
258
|
+
} else {
|
|
259
|
+
const recomputed = await computeFieldHashes(p.salt, schema, input.claimedFields);
|
|
260
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
261
|
+
const match = recomputed[i] === p.fieldHashes[i];
|
|
262
|
+
perField.push({ path: schema.fields[i].path, match });
|
|
263
|
+
if (!match) allMatch = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const revoked = input.revocation ? isRevoked(p.docId, input.revocation.list) : null;
|
|
267
|
+
const valid = signatureValid && allMatch && revoked !== true;
|
|
268
|
+
let reason;
|
|
269
|
+
if (!signatureValid) reason = pub ? "signature invalid" : "unknown keyId";
|
|
270
|
+
else if (countMismatch) reason = "schema/payload field-count mismatch";
|
|
271
|
+
else if (!allMatch) reason = "field mismatch";
|
|
272
|
+
else if (revoked === true) reason = "revoked";
|
|
273
|
+
return reason !== void 0 ? { valid, signatureValid, perField, revoked, reason } : { valid, signatureValid, perField, revoked };
|
|
274
|
+
}
|
|
275
|
+
export {
|
|
276
|
+
b64urlToBytes,
|
|
277
|
+
bytesToB64url,
|
|
278
|
+
bytesToHex,
|
|
279
|
+
canonicalJson,
|
|
280
|
+
computeFieldHashes,
|
|
281
|
+
decodeQr,
|
|
282
|
+
ed25519Sign,
|
|
283
|
+
ed25519Verify,
|
|
284
|
+
encodeQr,
|
|
285
|
+
generateDocSigningKeyPair,
|
|
286
|
+
getPath,
|
|
287
|
+
isRevoked,
|
|
288
|
+
keyIdFor,
|
|
289
|
+
normalizeField,
|
|
290
|
+
sha256Bytes,
|
|
291
|
+
sha256Hex,
|
|
292
|
+
signPayloadCore,
|
|
293
|
+
signRevocationList,
|
|
294
|
+
utf8,
|
|
295
|
+
validateFieldSchema,
|
|
296
|
+
verifyAttestation,
|
|
297
|
+
verifyRevocationList
|
|
298
|
+
};
|
|
299
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/encoding.ts","../src/normalize.ts","../src/hashing.ts","../src/ed25519.ts","../src/qr.ts","../src/revocation.ts","../src/verify.ts"],"sourcesContent":["/**\n * Pure encoding + hashing primitives. Zero deps; WebCrypto only.\n *\n * `canonicalJson` and `sha256Hex` are intentionally byte-identical to\n * hub's `history/ledger/entry.ts` implementations. They are REPLICATED\n * here (not imported) because this package is upstream of hub — importing\n * from hub would invert the dependency. The conformance test pins the\n * shared contract via fixed vectors.\n */\n\nexport function utf8(s: string): Uint8Array {\n return new TextEncoder().encode(s)\n}\n\nexport function bytesToHex(bytes: Uint8Array): string {\n let out = ''\n for (const b of bytes) out += b.toString(16).padStart(2, '0')\n return out\n}\n\nexport function bytesToB64url(bytes: Uint8Array): string {\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nexport function b64urlToBytes(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4))\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad\n const bin = atob(b64)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\nexport function canonicalJson(value: unknown): string {\n if (value === null) return 'null'\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`canonicalJson: refusing to encode non-finite number ${String(value)}`)\n }\n return JSON.stringify(value)\n }\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'bigint') throw new Error('canonicalJson: BigInt is not JSON-serializable')\n if (typeof value === 'undefined' || typeof value === 'function') {\n throw new Error(`canonicalJson: refusing to encode ${typeof value} — include all fields explicitly`)\n }\n if (Array.isArray(value)) return '[' + value.map((v) => canonicalJson(v)).join(',') + ']'\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n const parts: string[] = []\n for (const key of keys) parts.push(JSON.stringify(key) + ':' + canonicalJson(obj[key]))\n return '{' + parts.join(',') + '}'\n }\n throw new Error(`canonicalJson: unexpected value type: ${typeof value}`)\n}\n\nexport async function sha256Bytes(input: string): Promise<Uint8Array> {\n const digest = await globalThis.crypto.subtle.digest('SHA-256', utf8(input) as BufferSource)\n return new Uint8Array(digest)\n}\n\nexport async function sha256Hex(input: string): Promise<string> {\n return bytesToHex(await sha256Bytes(input))\n}\n","import type { AttestationFieldSchema, Normalizer } from './types.js'\n\nconst NORMALIZERS: ReadonlySet<string> = new Set<Normalizer>([\n 'trim', 'lower', 'upper', 'alnum-upper', 'digits', 'cents', 'iso-date',\n])\n\nexport function getPath(obj: unknown, path: string): unknown {\n return path.split('.').reduce<unknown>(\n (o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]),\n obj,\n )\n}\n\n/**\n * Normalize a field value to a canonical string for hashing.\n *\n * `iso-date` returns the **UTC** calendar date (`toISOString().slice(0,10)`),\n * so a local-time `Date` near midnight can shift by ±1 day in non-UTC\n * environments — prefer passing an ISO date string (e.g. `'2026-05-29'`)\n * over a local `new Date(2026, 4, 29)`. Issue and verify use this same\n * function, so a value passed identically on both sides always round-trips.\n */\nexport function normalizeField(value: unknown, n: Normalizer): string {\n switch (n) {\n case 'trim':\n return String(value).trim()\n case 'lower':\n return String(value).trim().toLowerCase()\n case 'upper':\n return String(value).trim().toUpperCase()\n case 'alnum-upper':\n return String(value).replace(/[^A-Za-z0-9]/g, '').toUpperCase()\n case 'digits':\n return String(value).replace(/[^0-9]/g, '')\n case 'cents': {\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`)\n }\n return String(Math.round(value * 100))\n }\n // For string values: strip currency symbols / spaces / commas but keep digits, dot, minus\n const stripped = String(value).replace(/[^0-9.-]/g, '')\n // A valid numeric string must have at least one digit\n if (!/[0-9]/.test(stripped)) {\n throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`)\n }\n const num = Number(stripped)\n if (!Number.isFinite(num)) {\n throw new Error(`normalizeField(cents): not a finite number: ${String(value)}`)\n }\n return String(Math.round(num * 100))\n }\n case 'iso-date': {\n const d = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(d.getTime())) {\n throw new Error(`normalizeField(iso-date): unparseable date: ${String(value)}`)\n }\n return d.toISOString().slice(0, 10)\n }\n default: {\n const exhaustive: never = n\n throw new Error(`normalizeField: unknown normalizer ${String(exhaustive)}`)\n }\n }\n}\n\nexport function validateFieldSchema(schema: AttestationFieldSchema): void {\n if (!schema.fields || schema.fields.length === 0) {\n throw new Error('validateFieldSchema: schema must declare at least one field')\n }\n const seen = new Set<string>()\n for (const f of schema.fields) {\n if (!NORMALIZERS.has(f.normalize)) {\n throw new Error(\n `validateFieldSchema: unknown normalizer '${String(f.normalize)}' for path '${f.path}'`,\n )\n }\n if (seen.has(f.path)) {\n throw new Error(`validateFieldSchema: duplicate path '${f.path}'`)\n }\n seen.add(f.path)\n }\n}\n","import type { AttestationFieldSchema } from './types.js'\nimport { canonicalJson, sha256Bytes, bytesToB64url } from './encoding.js'\nimport { getPath, normalizeField } from './normalize.js'\n\n/**\n * One salted, domain-separated hash per declared field, in schema order:\n * fieldHash[i] = base64url( sha256( canonicalJson([salt, path, normalizedValue]) ) )\n * Per-document salt defeats brute-force of low-entropy fields and cross-\n * document correlation; the path in the input domain-separates fields\n * that happen to share a value.\n */\nexport async function computeFieldHashes(\n saltB64: string,\n schema: AttestationFieldSchema,\n values: Record<string, unknown>,\n): Promise<string[]> {\n const out: string[] = []\n for (const f of schema.fields) {\n const raw = getPath(values, f.path)\n if (raw === undefined || raw === null) {\n throw new Error(`computeFieldHashes: missing value at declared path '${f.path}'`)\n }\n const norm = normalizeField(raw, f.normalize)\n const digest = await sha256Bytes(canonicalJson([saltB64, f.path, norm]))\n out.push(bytesToB64url(digest))\n }\n return out\n}\n","import { bytesToB64url, b64urlToBytes, sha256Hex } from './encoding.js'\n\nconst ALG = 'Ed25519'\n\n/** Stable key identifier: first 16 hex chars of sha256(publicKeyB64). */\nexport async function keyIdFor(publicKeyB64: string): Promise<string> {\n return (await sha256Hex(publicKeyB64)).slice(0, 16)\n}\n\nexport async function generateDocSigningKeyPair(): Promise<{\n keyId: string\n publicKeyB64: string // base64url raw (32 bytes) — non-secret, publishable\n privateKeyPkcs8B64: string // base64url pkcs8 — secret, wrap before persisting\n}> {\n const kp = await globalThis.crypto.subtle.generateKey(ALG, true, ['sign', 'verify'])\n const rawPub = new Uint8Array(await globalThis.crypto.subtle.exportKey('raw', kp.publicKey))\n const pkcs8 = new Uint8Array(await globalThis.crypto.subtle.exportKey('pkcs8', kp.privateKey))\n const publicKeyB64 = bytesToB64url(rawPub)\n return { keyId: await keyIdFor(publicKeyB64), publicKeyB64, privateKeyPkcs8B64: bytesToB64url(pkcs8) }\n}\n\nexport async function ed25519Sign(privateKeyPkcs8B64: string, message: Uint8Array): Promise<string> {\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n b64urlToBytes(privateKeyPkcs8B64) as BufferSource,\n ALG,\n false,\n ['sign'],\n )\n const sig = new Uint8Array(await globalThis.crypto.subtle.sign(ALG, key, message as BufferSource))\n return bytesToB64url(sig)\n}\n\nexport async function ed25519Verify(publicKeyB64: string, sigB64url: string, message: Uint8Array): Promise<boolean> {\n try {\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n b64urlToBytes(publicKeyB64) as BufferSource,\n ALG,\n false,\n ['verify'],\n )\n return await globalThis.crypto.subtle.verify(\n ALG,\n key,\n b64urlToBytes(sigB64url) as BufferSource,\n message as BufferSource,\n )\n } catch {\n return false\n }\n}\n","import { bytesToB64url, b64urlToBytes, utf8 } from './encoding.js'\n\nexport interface QrPayload {\n readonly v: 1\n readonly docId: string\n readonly salt: string\n readonly alg: 'ed25519'\n readonly keyId: string\n readonly fieldHashes: readonly string[]\n readonly sig: string\n}\n\n/** Compact JSON → base64url. (CBOR + base45 density optimisation deferred.) */\nexport function encodeQr(p: QrPayload): string {\n return bytesToB64url(utf8(JSON.stringify(p)))\n}\n\nexport function decodeQr(s: string): QrPayload {\n let parsed: unknown\n try {\n parsed = JSON.parse(new TextDecoder().decode(b64urlToBytes(s)))\n } catch {\n throw new Error('decodeQr: invalid base64url-encoded JSON payload')\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('decodeQr: invalid payload — expected a JSON object')\n }\n const p = parsed as Record<string, unknown>\n if (p['v'] !== 1) throw new Error(`decodeQr: unsupported version ${String(p['v'])} (expected 1)`)\n if (typeof p['docId'] !== 'string' || typeof p['salt'] !== 'string' || p['alg'] !== 'ed25519'\n || typeof p['keyId'] !== 'string' || typeof p['sig'] !== 'string'\n || !Array.isArray(p['fieldHashes']) || !p['fieldHashes'].every((h) => typeof h === 'string')) {\n throw new Error('decodeQr: invalid payload shape')\n }\n return {\n v: 1, docId: p['docId'], salt: p['salt'], alg: 'ed25519',\n keyId: p['keyId'], fieldHashes: p['fieldHashes'], sig: p['sig'],\n }\n}\n","import { canonicalJson, utf8 } from './encoding.js'\nimport { ed25519Sign, ed25519Verify } from './ed25519.js'\n\nexport interface RevocationList {\n readonly v: 1\n readonly revokedDocIds: readonly string[]\n readonly asOf: string\n readonly keyId: string\n readonly sig: string\n}\n\nfunction listCore(revokedDocIds: readonly string[], asOf: string, keyId: string): Uint8Array {\n return utf8(canonicalJson({ v: 1, revokedDocIds: [...revokedDocIds].sort(), asOf, keyId }))\n}\n\nexport function isRevoked(docId: string, list: RevocationList): boolean {\n // The list is untrusted (typically network-fetched). Fail closed on a\n // malformed shape rather than throwing a raw TypeError.\n if (!Array.isArray(list?.revokedDocIds)) return false\n return list.revokedDocIds.includes(docId)\n}\n\nexport async function signRevocationList(\n revokedDocIds: readonly string[], asOf: string, keyId: string, privateKeyPkcs8B64: string,\n): Promise<RevocationList> {\n const sorted = [...revokedDocIds].sort()\n const sig = await ed25519Sign(privateKeyPkcs8B64, listCore(sorted, asOf, keyId))\n return { v: 1, revokedDocIds: sorted, asOf, keyId, sig }\n}\n\nexport async function verifyRevocationList(list: RevocationList, publicKeyB64: string): Promise<boolean> {\n // Untrusted input — validate the shape before touching it (no raw TypeError).\n if (list?.v !== 1 || !Array.isArray(list.revokedDocIds)\n || typeof list.asOf !== 'string' || typeof list.keyId !== 'string' || typeof list.sig !== 'string') {\n return false\n }\n return ed25519Verify(publicKeyB64, list.sig, listCore(list.revokedDocIds, list.asOf, list.keyId))\n}\n","import type { AttestationFieldSchema } from './types.js'\nimport type { QrPayload } from './qr.js'\nimport type { RevocationList } from './revocation.js'\nimport { canonicalJson, utf8 } from './encoding.js'\nimport { ed25519Sign, ed25519Verify } from './ed25519.js'\nimport { computeFieldHashes } from './hashing.js'\nimport { decodeQr } from './qr.js'\nimport { isRevoked } from './revocation.js'\n\nexport interface VerifyInput {\n readonly qr: string\n readonly claimedFields: Record<string, unknown>\n readonly fieldSchema: AttestationFieldSchema\n readonly publicKeys: Readonly<Record<string, string>>\n readonly revocation?: { list: RevocationList }\n}\nexport interface VerifyResult {\n readonly valid: boolean\n readonly signatureValid: boolean\n readonly perField: ReadonlyArray<{ path: string; match: boolean }>\n readonly revoked: boolean | null\n readonly reason?: string\n}\n\n/**\n * The bytes the signature covers: canonicalJson of the payload minus `alg`/`sig`.\n * Excludes `alg` by design — v1 has exactly one algorithm (`ed25519`). If a `v:2`\n * ever introduces a second algorithm, `alg` MUST be added here to prevent a\n * downgrade attack.\n */\nfunction signedCore(core: { v: 1; docId: string; salt: string; keyId: string; fieldHashes: readonly string[] }): Uint8Array {\n return utf8(canonicalJson({ v: core.v, docId: core.docId, salt: core.salt, keyId: core.keyId, fieldHashes: core.fieldHashes }))\n}\n\nexport async function signPayloadCore(\n core: { v: 1; docId: string; salt: string; keyId: string; fieldHashes: readonly string[] },\n privateKeyPkcs8B64: string,\n): Promise<string> {\n return ed25519Sign(privateKeyPkcs8B64, signedCore(core))\n}\n\nexport async function verifyAttestation(input: VerifyInput): Promise<VerifyResult> {\n const p: QrPayload = decodeQr(input.qr)\n const pub = input.publicKeys[p.keyId]\n const signatureValid = pub\n ? await ed25519Verify(pub, p.sig, signedCore({ v: p.v, docId: p.docId, salt: p.salt, keyId: p.keyId, fieldHashes: p.fieldHashes }))\n : false\n\n const schema = input.fieldSchema\n const perField: Array<{ path: string; match: boolean }> = []\n let allMatch = true\n let countMismatch = false\n if (schema.fields.length !== p.fieldHashes.length) {\n countMismatch = true\n allMatch = false\n for (const f of schema.fields) perField.push({ path: f.path, match: false })\n } else {\n const recomputed = await computeFieldHashes(p.salt, schema, input.claimedFields)\n for (let i = 0; i < schema.fields.length; i++) {\n const match = recomputed[i] === p.fieldHashes[i]\n perField.push({ path: schema.fields[i]!.path, match })\n if (!match) allMatch = false\n }\n }\n\n // Membership check only — the CALLER must have verified the list's own\n // signature with `verifyRevocationList` before passing it here. A list that\n // omits a genuinely-revoked id lets that doc pass; that risk is the caller's.\n const revoked = input.revocation ? isRevoked(p.docId, input.revocation.list) : null\n const valid = signatureValid && allMatch && revoked !== true\n\n let reason: string | undefined\n if (!signatureValid) reason = pub ? 'signature invalid' : 'unknown keyId'\n else if (countMismatch) reason = 'schema/payload field-count mismatch'\n else if (!allMatch) reason = 'field mismatch'\n else if (revoked === true) reason = 'revoked'\n\n return reason !== undefined\n ? { valid, signatureValid, perField, revoked, reason }\n : { valid, signatureValid, perField, revoked }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noy-db/attestation",
|
|
3
|
+
"version": "0.2.0-pre.2",
|
|
4
|
+
"description": "Pure, zero-dependency document-attestation primitive for noy-db — per-field salted commitments, Ed25519 sign/verify, QR credential codec, and revocation checks. Runs in Node and the browser; the offline verifier imports only this.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
|
+
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/attestation#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vLannaAi/noy-db.git",
|
|
11
|
+
"directory": "packages/attestation"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vLannaAi/noy-db/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"default": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"noy-db",
|
|
43
|
+
"attestation",
|
|
44
|
+
"document",
|
|
45
|
+
"commitment",
|
|
46
|
+
"ed25519",
|
|
47
|
+
"verification",
|
|
48
|
+
"qr",
|
|
49
|
+
"tamper-evident"
|
|
50
|
+
],
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"tag": "latest"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"lint": "eslint src/",
|
|
59
|
+
"typecheck": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|