@suveren/gateway 0.18.1 → 0.19.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.
@@ -1,639 +0,0 @@
1
- // src/lib/as-pairing.ts
2
- import { createHash } from "crypto";
3
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
4
- import { dirname, join } from "path";
5
- function pairingPath(dataDir) {
6
- return join(dataDir, "as-pairing.json");
7
- }
8
- function readPairing(dataDir) {
9
- const path = pairingPath(dataDir);
10
- if (!existsSync(path)) return null;
11
- try {
12
- const data = JSON.parse(readFileSync(path, "utf-8"));
13
- if (typeof data.asUrl !== "string" || typeof data.publicKeyHex !== "string") return null;
14
- return {
15
- asUrl: data.asUrl,
16
- publicKeyHex: data.publicKeyHex,
17
- pairedAt: data.pairedAt ?? "",
18
- ...typeof data.tlsSpkiPinHex === "string" ? { tlsSpkiPinHex: data.tlsSpkiPinHex } : {}
19
- };
20
- } catch {
21
- return null;
22
- }
23
- }
24
- function fingerprintOf(publicKeyHex) {
25
- const raw = Buffer.from(publicKeyHex, "hex");
26
- const digest = createHash("sha256").update(raw).digest("hex").toUpperCase();
27
- return digest.match(/.{1,4}/g)?.join(":") ?? digest;
28
- }
29
-
30
- // src/lib/report/export-types.ts
31
- function isExportBundle(v) {
32
- if (!v || typeof v !== "object") return false;
33
- const o = v;
34
- if (o.format !== "suveren-report-export" || o.version !== 1) return false;
35
- if (!o.report || typeof o.report !== "object") return false;
36
- if (!Array.isArray(o.tickets)) return false;
37
- if (!o.authorizations || typeof o.authorizations !== "object") return false;
38
- if (!o.authorityServer || typeof o.authorityServer !== "object") return false;
39
- const as = o.authorityServer;
40
- return typeof as.url === "string" && typeof as.publicKeyHex === "string";
41
- }
42
-
43
- // src/lib/report/verify-export.ts
44
- import { Parser as Parser2 } from "htmlparser2";
45
- import { verifyReceiptSignature as verifyReceiptSignature2, verifyAttestationSignature as verifyAttestationSignature2, decodeAttestationBlob as decodeAttestationBlob2 } from "@hap/core";
46
-
47
- // src/lib/report/parse-elements.ts
48
- import { Parser } from "htmlparser2";
49
- function parseElements(html) {
50
- const found = [];
51
- const seenCount = /* @__PURE__ */ new Map();
52
- const parser = new Parser(
53
- {
54
- onopentag(name, attribs) {
55
- if (!name.startsWith("sv-")) return;
56
- const n = seenCount.get(name) ?? 0;
57
- seenCount.set(name, n + 1);
58
- found.push({ id: `${name}-${n}`, kind: name, attrs: { ...attribs } });
59
- }
60
- },
61
- { decodeEntities: true }
62
- );
63
- parser.write(html);
64
- parser.end();
65
- return found;
66
- }
67
-
68
- // src/lib/report/ticket-resolvers.ts
69
- import { verifyReceiptSignature, verifyAttestationSignature, decodeAttestationBlob } from "@hap/core";
70
-
71
- // src/lib/report/format.ts
72
- import { getProfile } from "@hap/core";
73
- var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
74
- function formatDateTime(value) {
75
- const n = typeof value === "number" ? value : Number(value);
76
- if (!Number.isFinite(n)) return "unknown time";
77
- const d = new Date(n * 1e3);
78
- const day = d.getDate();
79
- const month = MONTHS[d.getMonth()];
80
- const hh = String(d.getHours()).padStart(2, "0");
81
- const mm = String(d.getMinutes()).padStart(2, "0");
82
- return `${day} ${month}, ${hh}:${mm}`;
83
- }
84
- function formatDuration(value) {
85
- const n = typeof value === "number" ? value : Number(value);
86
- if (!Number.isFinite(n) || n < 0) return "unknown duration";
87
- const mins = Math.round(n / 60);
88
- if (mins < 60) return `${mins} min`;
89
- const hrs = Math.floor(mins / 60);
90
- const rem = mins % 60;
91
- return `${hrs} h ${String(rem).padStart(2, "0")} min`;
92
- }
93
- function groupThousands(n) {
94
- const sign = n < 0 ? "-" : "";
95
- const [intPart, frac] = Math.abs(n).toString().split(".");
96
- const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
97
- return sign + grouped + (frac ? `.${frac}` : "");
98
- }
99
- var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
100
- function formatCurrency(amount, currencyCode) {
101
- const n = typeof amount === "number" ? amount : Number(amount);
102
- if (!Number.isFinite(n)) return String(amount ?? "");
103
- if (!currencyCode) return groupThousands(n);
104
- const code = currencyCode.toUpperCase();
105
- const symbol = CURRENCY_SYMBOLS[code];
106
- return symbol ? `${symbol} ${groupThousands(n)}` : `${code} ${groupThousands(n)}`;
107
- }
108
- function formatBoundValue(unit, value, fallbackCurrency) {
109
- const n = typeof value === "number" ? value : Number(value);
110
- if (!Number.isFinite(n)) return String(value ?? "");
111
- if (!unit || unit === "count") {
112
- return fallbackCurrency ? formatCurrency(n, fallbackCurrency) : groupThousands(n);
113
- }
114
- if (unit === "minutes") return `${groupThousands(n)} min`;
115
- if (unit === "hours") return `${groupThousands(n)} h`;
116
- if (unit === "days") return `${groupThousands(n)} d`;
117
- if (unit === "percent") return `${groupThousands(n)}%`;
118
- if (unit.startsWith("currency:")) return formatCurrency(n, unit.slice("currency:".length));
119
- return `${groupThousands(n)} ${unit}`;
120
- }
121
- function humanizeKey(key) {
122
- return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
123
- }
124
- function formatBoundLabel(profileId, key, value, fallbackCurrency) {
125
- const profile = profileId ? getProfile(profileId) : void 0;
126
- const field = profile?.boundsSchema?.fields?.[key];
127
- const displayName = field?.displayName ?? humanizeKey(key);
128
- return `${displayName}: ${formatBoundValue(field?.unit, value, fallbackCurrency)}`;
129
- }
130
- function formatOwnerLabel(did) {
131
- if (!did) return "Unknown owner";
132
- const tail = did.length > 6 ? did.slice(-6) : did;
133
- return `Owner (key \u2026${tail})`;
134
- }
135
- function profileShortLabel(profileId) {
136
- if (!profileId) return "Unknown profile";
137
- const name = profileId.split("@")[0].split("/").pop();
138
- if (!name) return profileId;
139
- return name.charAt(0).toUpperCase() + name.slice(1);
140
- }
141
- var ACTION_LABELS = {
142
- create_quote: "Quote created",
143
- update_quote: "Quote updated",
144
- send_quote: "Quote sent",
145
- convert_quote_to_order: "Order placed",
146
- create_order: "Order created",
147
- update_order: "Order updated",
148
- send_message: "Reply sent",
149
- send_email: "Reply sent",
150
- create_contact: "Contact created",
151
- update_contact: "Contact updated",
152
- create_deal: "Deal created",
153
- update_deal: "Deal updated",
154
- create_task: "Task created",
155
- update_task: "Task updated",
156
- log_activity: "Activity logged",
157
- create_record: "Record created",
158
- update_record: "Record updated",
159
- delete_record: "Record deleted",
160
- create_event: "Event created",
161
- update_event: "Event updated",
162
- delete_event: "Event deleted",
163
- publish_post: "Post published"
164
- };
165
- function pastTense(verb) {
166
- if (!verb) return verb;
167
- if (/e$/.test(verb)) return `${verb}d`;
168
- return `${verb}ed`;
169
- }
170
- function capitalize(s) {
171
- return s.length > 0 ? s.charAt(0).toUpperCase() + s.slice(1) : s;
172
- }
173
- function formatActionLabel(action) {
174
- const raw = String(action ?? "").trim();
175
- if (!raw) return "Action";
176
- const withoutSystem = raw.includes("__") ? raw.slice(raw.indexOf("__") + 2) : raw;
177
- const known = ACTION_LABELS[withoutSystem];
178
- if (known) return known;
179
- const parts = withoutSystem.split("_").filter(Boolean);
180
- if (parts.length === 0) return capitalize(raw);
181
- const [verb, ...rest] = parts;
182
- if (rest.length === 0) return capitalize(pastTense(verb));
183
- return `${capitalize(rest.join(" "))} ${pastTense(verb)}`;
184
- }
185
- var METRIC_LABELS = {
186
- completed: "cases completed",
187
- "median-time": "median time, start \u2192 goal",
188
- "average-time": "average time, start \u2192 goal",
189
- "without-approval": "share without approval",
190
- approvals: "approvals",
191
- "median-approval-wait": "median approval wait",
192
- tickets: "tickets",
193
- refusals: "system refusals"
194
- };
195
- function formatMetricValue(kind, value) {
196
- const n = typeof value === "number" ? value : Number(value);
197
- if (kind === "without-approval" && Number.isFinite(n)) return `${Math.round(n * 100)}%`;
198
- if ((kind === "median-time" || kind === "average-time" || kind === "median-approval-wait") && Number.isFinite(n)) {
199
- return formatDuration(n);
200
- }
201
- return String(value);
202
- }
203
-
204
- // src/lib/report/ticket-resolvers.ts
205
- function findReceiptEntry(archive, ticketId2) {
206
- return archive.getReceipts().find((r) => r.receipt.id === ticketId2);
207
- }
208
- function findAuthorization(archive, authorizationId) {
209
- return archive.getAuthorizations().find((a) => a.authorizationId === authorizationId);
210
- }
211
- async function checkTicket(archive, ticketId2) {
212
- const entry = findReceiptEntry(archive, ticketId2);
213
- if (!entry) {
214
- return { ok: false, reason: `No ticket "${ticketId2}" in the local archive.` };
215
- }
216
- if (!entry.asPublicKey) {
217
- return { ok: false, reason: `Ticket "${ticketId2}" has no archived Authority Server key to verify it against.`, entry };
218
- }
219
- try {
220
- await verifyReceiptSignature(entry.receipt, entry.asPublicKey);
221
- } catch (err) {
222
- return {
223
- ok: false,
224
- reason: `Ticket "${ticketId2}" signature does not verify: ${err instanceof Error ? err.message : String(err)}`,
225
- entry
226
- };
227
- }
228
- return { ok: true, entry };
229
- }
230
- function checkUrlFor(entry) {
231
- const id = String(entry.receipt.id ?? "");
232
- return `${entry.asUrl.replace(/\/+$/, "")}/r/${id}`;
233
- }
234
- async function resolveTicketElement(archive, ref) {
235
- const check = await checkTicket(archive, ref);
236
- if (!check.ok) return { status: "unverifiable", reason: check.reason };
237
- const r = check.entry.receipt;
238
- return {
239
- status: "verified",
240
- data: {
241
- ticketId: ref,
242
- action: r.action,
243
- actionLabel: formatActionLabel(r.action),
244
- time: r.timestamp,
245
- timeLabel: formatDateTime(r.timestamp),
246
- profile: r.profileId,
247
- profileLabel: profileShortLabel(typeof r.profileId === "string" ? r.profileId : void 0),
248
- authorizationId: r.authorizationId,
249
- limitsUsed: r.limits ?? r.executionContext ?? {},
250
- checkUrl: checkUrlFor(check.entry)
251
- }
252
- };
253
- }
254
- async function resolveApprovalElement(archive, ticketRef) {
255
- const check = await checkTicket(archive, ticketRef);
256
- if (!check.ok) return { status: "unverifiable", reason: check.reason };
257
- const proposal = check.entry.proposal;
258
- if (!proposal) {
259
- return {
260
- status: "unverifiable",
261
- reason: `Ticket "${ticketRef}" has no archived approval (it ran without review).`
262
- };
263
- }
264
- const committedBy = proposal.committedBy ?? {};
265
- const approvers = Object.values(committedBy);
266
- const createdAt = typeof proposal.createdAt === "number" ? proposal.createdAt : void 0;
267
- const decidedAt = approvers.length > 0 ? Math.max(...approvers.map((a) => a.at)) : void 0;
268
- const waitSeconds = createdAt !== void 0 && decidedAt !== void 0 ? decidedAt - createdAt : void 0;
269
- const who = approvers.map((a) => a.userId);
270
- return {
271
- status: "verified",
272
- data: {
273
- ticketId: ticketRef,
274
- who,
275
- whoLabel: who.length > 0 ? who.join(", ") : "unknown",
276
- createdAt,
277
- createdAtLabel: createdAt !== void 0 ? formatDateTime(createdAt) : "unknown",
278
- decidedAt,
279
- decidedAtLabel: decidedAt !== void 0 ? formatDateTime(decidedAt) : "unknown",
280
- waitSeconds,
281
- waitLabel: waitSeconds !== void 0 ? formatDuration(waitSeconds) : "unknown",
282
- status: proposal.status
283
- }
284
- };
285
- }
286
- async function resolveMandateElement(archive, ticketRef) {
287
- const check = await checkTicket(archive, ticketRef);
288
- if (!check.ok) return { status: "unverifiable", reason: check.reason };
289
- const r = check.entry.receipt;
290
- const authorizationId = typeof r.authorizationId === "string" ? r.authorizationId : void 0;
291
- if (!authorizationId) {
292
- return { status: "unverifiable", reason: `Ticket "${ticketRef}" carries no authorizationId.` };
293
- }
294
- const auth = findAuthorization(archive, authorizationId);
295
- if (!auth) {
296
- return { status: "unverifiable", reason: `No archived mandate "${authorizationId}" for ticket "${ticketRef}".` };
297
- }
298
- if (auth.boundsHash && typeof r.boundsHash === "string" && auth.boundsHash !== r.boundsHash) {
299
- return {
300
- status: "unverifiable",
301
- reason: `Mandate "${authorizationId}" boundsHash does not match ticket "${ticketRef}".`
302
- };
303
- }
304
- let mode;
305
- let owners = [];
306
- let ownersRaw = [];
307
- const firstBlob = auth.attestations[0]?.blob;
308
- if (firstBlob && check.entry.asPublicKey) {
309
- try {
310
- const attestation = decodeAttestationBlob(firstBlob);
311
- await verifyAttestationSignature(attestation, check.entry.asPublicKey);
312
- mode = attestation.payload.commitment_mode;
313
- const dids = attestation.payload.resolved_owners ?? [];
314
- const subjects = attestation.payload.subjects ?? [];
315
- ownersRaw = dids;
316
- owners = dids.map((did) => {
317
- const subject = subjects.find((s) => s.did === did);
318
- return subject?.assurance === "high" && subject.disclose?.name ? subject.disclose.name : formatOwnerLabel(did);
319
- });
320
- } catch {
321
- }
322
- }
323
- const fallbackCurrency = typeof auth.context?.currency === "string" ? auth.context.currency : void 0;
324
- const rawLimits = auth.bounds ?? {};
325
- const limits = Object.entries(rawLimits).map(([key, value]) => formatBoundLabel(auth.profileId, key, value, fallbackCurrency));
326
- return {
327
- status: "verified",
328
- data: {
329
- authorizationId,
330
- profile: auth.profileId,
331
- profileLabel: profileShortLabel(auth.profileId),
332
- limits,
333
- rawLimits,
334
- intent: auth.intent,
335
- mode,
336
- owners,
337
- ownersRaw
338
- }
339
- };
340
- }
341
-
342
- // src/lib/report/time.ts
343
- function parseTimestampSeconds(value) {
344
- if (typeof value !== "string" || !value.trim()) return void 0;
345
- const iso = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}/.test(value) && !/[zZ]|[+-]\d{2}:?\d{2}$/.test(value) ? `${value.replace(" ", "T")}Z` : value;
346
- const ms = Date.parse(iso);
347
- return Number.isFinite(ms) ? Math.floor(ms / 1e3) : void 0;
348
- }
349
-
350
- // src/lib/report/case-resolvers.ts
351
- function parseRef(value) {
352
- if (!value) return { id: "" };
353
- const i = value.indexOf(":");
354
- if (i === -1) return { id: value.trim() };
355
- return { type: value.slice(0, i).trim(), id: value.slice(i + 1).trim() };
356
- }
357
- function parseStepRefs(value) {
358
- if (!value) return [];
359
- return value.split(/\s+/).filter(Boolean).map((s) => parseRef(s).id || s);
360
- }
361
- function parseRecipients(toJson, ccJson) {
362
- const out = [];
363
- for (const raw of [toJson, ccJson]) {
364
- if (!raw) continue;
365
- try {
366
- const arr = JSON.parse(raw);
367
- if (Array.isArray(arr)) out.push(...arr.filter((x) => typeof x === "string"));
368
- } catch {
369
- }
370
- }
371
- return out.map((s) => s.toLowerCase());
372
- }
373
- function parseCaseAttrs(attrs) {
374
- return {
375
- startId: parseRef(attrs.start).id,
376
- goalId: parseRef(attrs.goal).id,
377
- stepIds: parseStepRefs(attrs.steps)
378
- };
379
- }
380
- async function resolveCaseElement(archive, emailExport, attrs) {
381
- const start = parseRef(attrs.start);
382
- const startMessage = emailExport.inbox.find((m) => m.id === start.id);
383
- if (!startMessage || !startMessage.case_id) {
384
- return { status: "unverifiable", reason: `start "${attrs.start ?? ""}" is not a loaded case email with a case_id.` };
385
- }
386
- const startCaseId = startMessage.case_id;
387
- const startTime = parseTimestampSeconds(startMessage.received_at);
388
- if (startTime === void 0) {
389
- return { status: "unverifiable", reason: `start message "${start.id}" has an unparseable received_at.`, startCaseId };
390
- }
391
- const goal = parseRef(attrs.goal);
392
- const goalCheck = await checkTicket(archive, goal.id);
393
- if (!goalCheck.ok) {
394
- return { status: "unverifiable", reason: `goal: ${goalCheck.reason}`, startCaseId };
395
- }
396
- const goalReceipt = goalCheck.entry.receipt;
397
- const goalTime = typeof goalReceipt.timestamp === "number" ? goalReceipt.timestamp : void 0;
398
- if (goalTime === void 0) {
399
- return { status: "unverifiable", reason: `goal ticket "${goal.id}" has no numeric timestamp.`, startCaseId };
400
- }
401
- if (goalTime < startTime) {
402
- return { status: "unverifiable", reason: `goal ticket "${goal.id}" is timestamped before start "${start.id}".`, startCaseId };
403
- }
404
- const stepIds = parseStepRefs(attrs.steps);
405
- const steps = [];
406
- for (const stepId of stepIds) {
407
- const stepCheck = await checkTicket(archive, stepId);
408
- if (!stepCheck.ok) {
409
- return { status: "unverifiable", reason: `step: ${stepCheck.reason}`, startCaseId };
410
- }
411
- const stepReceipt = stepCheck.entry.receipt;
412
- const stepTime = typeof stepReceipt.timestamp === "number" ? stepReceipt.timestamp : void 0;
413
- if (stepTime === void 0 || stepTime < startTime || stepTime > goalTime) {
414
- return { status: "unverifiable", reason: `step ticket "${stepId}" is outside the case window [${startTime}, ${goalTime}].`, startCaseId };
415
- }
416
- steps.push({ ticketId: stepId, time: stepTime, action: stepReceipt.action });
417
- }
418
- let warningReason;
419
- const sentForGoal = emailExport.sent.find((m) => m.receipt_id === goal.id);
420
- if (sentForGoal) {
421
- const isReply = sentForGoal.in_reply_to === start.id;
422
- const recipients = parseRecipients(sentForGoal.to_json, sentForGoal.cc_json);
423
- const addressedToSender = recipients.includes(startMessage.from_email.toLowerCase());
424
- if (!isReply && !addressedToSender) {
425
- warningReason = "goal link not confirmed";
426
- }
427
- }
428
- const approvals = [];
429
- for (const ticketId2 of [goal.id, ...stepIds]) {
430
- const approval = await resolveApprovalElement(archive, ticketId2);
431
- if (approval.status === "verified") {
432
- approvals.push({ ticketId: ticketId2, ...approval.data });
433
- }
434
- }
435
- const timeline = [
436
- { type: "start", time: startTime, label: start.id },
437
- ...steps.map((s) => ({ type: "step", time: s.time, label: s.ticketId })),
438
- ...approvals.map((a) => ({ type: "approval", time: a.decidedAt ?? a.createdAt ?? 0, label: a.ticketId })),
439
- { type: "goal", time: goalTime, label: goal.id }
440
- ].sort((a, b) => a.time - b.time);
441
- return {
442
- status: warningReason ? "warning" : "verified",
443
- reason: warningReason,
444
- startCaseId,
445
- data: {
446
- caseId: startCaseId,
447
- start: { id: start.id, time: startTime, sender: startMessage.from_email },
448
- goal: { ticketId: goal.id, time: goalTime, action: goalReceipt.action },
449
- steps,
450
- approvals,
451
- timeline,
452
- totalDurationSeconds: goalTime - startTime
453
- }
454
- };
455
- }
456
-
457
- // src/lib/report/verify-export.ts
458
- function collectReferencedTicketIds(html) {
459
- const ids = /* @__PURE__ */ new Set();
460
- for (const el of parseElements(html)) {
461
- if (el.kind === "sv-ticket" && el.attrs.ref) ids.add(el.attrs.ref);
462
- if ((el.kind === "sv-approval" || el.kind === "sv-mandate") && el.attrs.ticket) ids.add(el.attrs.ticket);
463
- if (el.kind === "sv-case") {
464
- const { goalId, stepIds } = parseCaseAttrs(el.attrs);
465
- if (goalId) ids.add(goalId);
466
- for (const s of stepIds) ids.add(s);
467
- }
468
- }
469
- return ids;
470
- }
471
- function ticketIdsOf(el) {
472
- if (el.kind === "sv-ticket") return el.attrs.ref ? [el.attrs.ref] : [];
473
- if (el.kind === "sv-approval" || el.kind === "sv-mandate") return el.attrs.ticket ? [el.attrs.ticket] : [];
474
- if (el.kind === "sv-case") {
475
- const { goalId, stepIds } = parseCaseAttrs(el.attrs);
476
- return [...goalId ? [goalId] : [], ...stepIds];
477
- }
478
- return [];
479
- }
480
- var TICKET_BACKED_KINDS = /* @__PURE__ */ new Set(["sv-ticket", "sv-approval", "sv-mandate", "sv-case"]);
481
- var PROOF_SCRIPT_BLOCK_RE = /<script[^>]*id="suveren-proof"[^>]*>[\s\S]*?<\/script>/gi;
482
- function collectPresentedStates(documentHtml) {
483
- const html = documentHtml.replace(PROOF_SCRIPT_BLOCK_RE, "");
484
- const open = [];
485
- const flaggedById = /* @__PURE__ */ new Map();
486
- let depth = 0;
487
- const classesOf = (attribs) => new Set((attribs.class ?? "").split(/\s+/).filter(Boolean));
488
- const settle = (f) => {
489
- const flagged = f.classes.has("sv-el-unverifiable") && !f.classes.has("sv-el-verified") && !f.classes.has("sv-el-warning") && f.badges.length > 0 && f.badges.every((b) => b.has("sv-badge-bad") && !b.has("sv-badge-ok") && !b.has("sv-badge-warn"));
490
- flaggedById.set(f.id, (flaggedById.get(f.id) ?? true) && flagged);
491
- };
492
- const parser = new Parser2(
493
- {
494
- onopentag(_name, attribs) {
495
- depth++;
496
- const classes = classesOf(attribs);
497
- if ([...classes].some((c) => c.startsWith("sv-badge"))) {
498
- for (const f of open) f.badges.push(classes);
499
- }
500
- const id = attribs["data-sv-id"];
501
- if (typeof id === "string" && id) open.push({ id, depth, classes, badges: [] });
502
- },
503
- onclosetag() {
504
- while (open.length > 0 && open[open.length - 1].depth >= depth) settle(open.pop());
505
- depth--;
506
- }
507
- },
508
- { decodeEntities: true }
509
- );
510
- parser.write(html);
511
- parser.end();
512
- while (open.length > 0) settle(open.pop());
513
- const states = /* @__PURE__ */ new Map();
514
- for (const [id, flagged] of flaggedById) states.set(id, flagged ? "not-verifiable" : "verified");
515
- return states;
516
- }
517
- function ticketId(receipt) {
518
- return typeof receipt.id === "string" ? receipt.id : "";
519
- }
520
- async function checkAttestations(auth, publicKeyHex) {
521
- if (auth.attestations.length === 0) {
522
- return { valid: false, error: "No archived mandate attestation for this authorization." };
523
- }
524
- for (const att of auth.attestations) {
525
- try {
526
- const attestation = decodeAttestationBlob2(att.blob);
527
- await verifyAttestationSignature2(attestation, publicKeyHex);
528
- } catch (err) {
529
- return { valid: false, error: err instanceof Error ? err.message : String(err) };
530
- }
531
- }
532
- return { valid: true };
533
- }
534
- async function verifyExportBundle(bundle, opts = {}) {
535
- const byId = new Map(bundle.tickets.map((t) => [ticketId(t), t]));
536
- const referencedIds = collectReferencedTicketIds(bundle.report.html);
537
- const allIds = /* @__PURE__ */ new Set([...referencedIds, ...byId.keys()]);
538
- const tickets = [];
539
- for (const id of allIds) {
540
- const receipt = byId.get(id);
541
- if (!receipt) {
542
- tickets.push({ ticketId: id, referenced: referencedIds.has(id), present: false, signatureValid: false, error: "Not present in the bundle." });
543
- continue;
544
- }
545
- try {
546
- await verifyReceiptSignature2(receipt, bundle.authorityServer.publicKeyHex);
547
- tickets.push({ ticketId: id, referenced: referencedIds.has(id), present: true, signatureValid: true });
548
- } catch (err) {
549
- tickets.push({
550
- ticketId: id,
551
- referenced: referencedIds.has(id),
552
- present: true,
553
- signatureValid: false,
554
- error: err instanceof Error ? err.message : String(err)
555
- });
556
- }
557
- }
558
- const authorizations = [];
559
- for (const [authorizationId, auth] of Object.entries(bundle.authorizations)) {
560
- const { valid: attestationValid, error: attestationError } = await checkAttestations(auth, bundle.authorityServer.publicKeyHex);
561
- let boundsHashMatches = "n/a";
562
- const representative = [...byId.values()].find((r) => r.authorizationId === authorizationId);
563
- if (auth.boundsHash && representative && typeof representative.boundsHash === "string") {
564
- boundsHashMatches = auth.boundsHash === representative.boundsHash;
565
- }
566
- authorizations.push({
567
- authorizationId,
568
- attestationValid,
569
- boundsHashMatches,
570
- ...attestationError ? { error: attestationError } : boundsHashMatches === false ? { error: `boundsHash does not match ticket ${representative?.id ?? "unknown"}.` } : {}
571
- });
572
- }
573
- const ticketById = new Map(tickets.map((t) => [t.ticketId, t]));
574
- const authById = new Map(authorizations.map((a) => [a.authorizationId, a]));
575
- const presentedStates = opts.documentHtml !== void 0 ? collectPresentedStates(opts.documentHtml) : /* @__PURE__ */ new Map();
576
- const elements = [];
577
- for (const el of parseElements(bundle.report.html)) {
578
- if (!TICKET_BACKED_KINDS.has(el.kind)) continue;
579
- const ticketIds = ticketIdsOf(el);
580
- const presented = presentedStates.get(el.id) ?? "verified";
581
- let error;
582
- if (ticketIds.length === 0) error = "Names no ticket.";
583
- for (const id of ticketIds) {
584
- if (error) break;
585
- const t = ticketById.get(id);
586
- if (!t || !t.present) error = `Ticket ${id} is not in the bundle.`;
587
- else if (!t.signatureValid) error = `Ticket ${id} has an invalid signature.`;
588
- }
589
- if (!error && el.kind === "sv-mandate") {
590
- const receipt = byId.get(ticketIds[0]);
591
- const authorizationId = typeof receipt.authorizationId === "string" ? receipt.authorizationId : void 0;
592
- const auth = authorizationId ? bundle.authorizations[authorizationId] : void 0;
593
- const authCheck = authorizationId ? authById.get(authorizationId) : void 0;
594
- if (!auth || !authCheck) error = `The mandate for ticket ${ticketIds[0]} is not in the bundle.`;
595
- else if (!authCheck.attestationValid) error = `The mandate for ticket ${ticketIds[0]} has an invalid attestation.`;
596
- else if (auth.boundsHash && typeof receipt.boundsHash === "string" && auth.boundsHash !== receipt.boundsHash) {
597
- error = `The mandate's boundsHash does not match ticket ${ticketIds[0]}.`;
598
- }
599
- }
600
- elements.push({ elementId: el.id, kind: el.kind, ticketIds, presented, backed: !error, ...error ? { error } : {} });
601
- }
602
- const keyFingerprint = fingerprintOf(bundle.authorityServer.publicKeyHex);
603
- const keyConfirmation = resolveKeyConfirmation(bundle.authorityServer.publicKeyHex, opts);
604
- const allValid = tickets.filter((t) => t.present).every((t) => t.signatureValid) && authorizations.every((a) => a.attestationValid && a.boundsHashMatches !== false) && elements.every((e) => e.presented === "not-verifiable" || e.backed);
605
- return { tickets, elements, authorizations, keyFingerprint, keyConfirmation, allValid };
606
- }
607
- function resolveKeyConfirmation(actualKeyHex, opts) {
608
- const normalize = (h) => h.trim().toLowerCase();
609
- if (opts.expectedKeyHex !== void 0) {
610
- if (normalize(opts.expectedKeyHex) === normalize(actualKeyHex)) return { state: "confirmed", source: "provided" };
611
- return { state: "mismatch", source: "provided", expectedFingerprint: fingerprintOf(opts.expectedKeyHex) };
612
- }
613
- if (opts.onlineKeyHex !== void 0) {
614
- if (normalize(opts.onlineKeyHex) === normalize(actualKeyHex)) return { state: "confirmed", source: "online" };
615
- return { state: "mismatch", source: "online", expectedFingerprint: fingerprintOf(opts.onlineKeyHex) };
616
- }
617
- return { state: "unconfirmed" };
618
- }
619
-
620
- export {
621
- readPairing,
622
- fingerprintOf,
623
- parseElements,
624
- formatDateTime,
625
- formatDuration,
626
- formatCurrency,
627
- profileShortLabel,
628
- formatActionLabel,
629
- METRIC_LABELS,
630
- formatMetricValue,
631
- resolveTicketElement,
632
- resolveApprovalElement,
633
- resolveMandateElement,
634
- parseTimestampSeconds,
635
- parseCaseAttrs,
636
- resolveCaseElement,
637
- isExportBundle,
638
- verifyExportBundle
639
- };