@suveren/gateway 0.18.2 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/bin/suveren-gateway.js +204 -13
- package/dist/control-plane/index.mjs +22 -0
- package/dist/mcp-server/chunk-ZRRZFGRL.mjs +2412 -0
- package/dist/mcp-server/http.mjs +385 -674
- package/dist/mcp-server/report-verify-cli.mjs +64 -7
- package/dist/ui/assets/index-DF3kB2R3.js +111 -0
- package/dist/ui/assets/index-kObsFuwK.css +1 -0
- package/dist/ui/index.html +2 -2
- package/lib/install-settings.d.mts +41 -0
- package/lib/install-settings.mjs +318 -0
- package/lib/policy.d.mts +6 -1
- package/lib/policy.mjs +48 -4
- package/node_modules/@hap/core/dist/index.d.mts +13 -0
- package/node_modules/@hap/core/dist/index.d.ts +13 -0
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/types.ts +13 -0
- package/package.json +1 -1
- package/profiles/index.json +2 -1
- package/profiles/reporting/0.2.profile.json +98 -0
- package/server.js +20 -4
- package/dist/mcp-server/chunk-QOKPECVY.mjs +0 -751
- package/dist/ui/assets/index-Bfi-G4qJ.css +0 -1
- package/dist/ui/assets/index-D0uXCqQB.js +0 -111
|
@@ -1,751 +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 verifyAttestationSignature3, decodeAttestationBlob as decodeAttestationBlob3 } 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 as verifyAttestationSignature2, decodeAttestationBlob as decodeAttestationBlob2 } 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
|
-
var IRREGULAR_PAST = {
|
|
166
|
-
write: "written",
|
|
167
|
-
rewrite: "rewritten",
|
|
168
|
-
send: "sent",
|
|
169
|
-
resend: "resent",
|
|
170
|
-
run: "run",
|
|
171
|
-
rerun: "rerun",
|
|
172
|
-
set: "set",
|
|
173
|
-
reset: "reset",
|
|
174
|
-
put: "put",
|
|
175
|
-
make: "made",
|
|
176
|
-
build: "built",
|
|
177
|
-
rebuild: "rebuilt",
|
|
178
|
-
begin: "begun",
|
|
179
|
-
take: "taken",
|
|
180
|
-
give: "given",
|
|
181
|
-
do: "done",
|
|
182
|
-
redo: "redone",
|
|
183
|
-
read: "read",
|
|
184
|
-
find: "found",
|
|
185
|
-
get: "retrieved",
|
|
186
|
-
buy: "bought",
|
|
187
|
-
pay: "paid",
|
|
188
|
-
sell: "sold",
|
|
189
|
-
hold: "held",
|
|
190
|
-
keep: "kept",
|
|
191
|
-
leave: "left",
|
|
192
|
-
draw: "drawn",
|
|
193
|
-
withdraw: "withdrawn",
|
|
194
|
-
forbid: "forbidden",
|
|
195
|
-
choose: "chosen",
|
|
196
|
-
speak: "spoken",
|
|
197
|
-
meet: "met",
|
|
198
|
-
lead: "led",
|
|
199
|
-
split: "split",
|
|
200
|
-
cut: "cut",
|
|
201
|
-
upload: "uploaded",
|
|
202
|
-
forward: "forwarded",
|
|
203
|
-
bring: "brought",
|
|
204
|
-
tell: "told",
|
|
205
|
-
show: "shown",
|
|
206
|
-
drive: "driven",
|
|
207
|
-
break: "broken"
|
|
208
|
-
};
|
|
209
|
-
var DOUBLE_FINAL = /* @__PURE__ */ new Set(["stop", "plan", "ship", "tag", "log", "submit", "drop", "flag", "pin", "unpin", "map", "wrap", "chat", "admit", "commit", "permit", "refer", "transfer"]);
|
|
210
|
-
function pastTense(verb) {
|
|
211
|
-
if (!verb) return verb;
|
|
212
|
-
const lower = verb.toLowerCase();
|
|
213
|
-
if (IRREGULAR_PAST[lower]) return IRREGULAR_PAST[lower];
|
|
214
|
-
if (DOUBLE_FINAL.has(lower)) return `${verb}${verb.slice(-1)}ed`;
|
|
215
|
-
if (/e$/.test(verb)) return `${verb}d`;
|
|
216
|
-
if (/[^aeiou]y$/i.test(verb)) return `${verb.slice(0, -1)}ied`;
|
|
217
|
-
return `${verb}ed`;
|
|
218
|
-
}
|
|
219
|
-
function capitalize(s) {
|
|
220
|
-
return s.length > 0 ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
221
|
-
}
|
|
222
|
-
function formatActionLabel(action) {
|
|
223
|
-
const raw = String(action ?? "").trim();
|
|
224
|
-
if (!raw) return "Action";
|
|
225
|
-
const withoutSystem = raw.includes("__") ? raw.slice(raw.indexOf("__") + 2) : raw;
|
|
226
|
-
const known = ACTION_LABELS[withoutSystem];
|
|
227
|
-
if (known) return known;
|
|
228
|
-
const parts = withoutSystem.split("_").filter(Boolean);
|
|
229
|
-
if (parts.length === 0) return capitalize(raw);
|
|
230
|
-
const [verb, ...rest] = parts;
|
|
231
|
-
if (rest.length === 0) return capitalize(pastTense(verb));
|
|
232
|
-
return `${capitalize(rest.join(" "))} ${pastTense(verb)}`;
|
|
233
|
-
}
|
|
234
|
-
var METRIC_LABELS = {
|
|
235
|
-
completed: "cases completed",
|
|
236
|
-
"median-time": "median time, start \u2192 goal",
|
|
237
|
-
"average-time": "average time, start \u2192 goal",
|
|
238
|
-
"without-approval": "share without approval",
|
|
239
|
-
approvals: "approvals",
|
|
240
|
-
"median-approval-wait": "median approval wait",
|
|
241
|
-
tickets: "tickets",
|
|
242
|
-
refusals: "system refusals"
|
|
243
|
-
};
|
|
244
|
-
function formatMetricValue(kind, value) {
|
|
245
|
-
const n = typeof value === "number" ? value : Number(value);
|
|
246
|
-
if (kind === "without-approval" && Number.isFinite(n)) return `${Math.round(n * 100)}%`;
|
|
247
|
-
if ((kind === "median-time" || kind === "average-time" || kind === "median-approval-wait") && Number.isFinite(n)) {
|
|
248
|
-
return formatDuration(n);
|
|
249
|
-
}
|
|
250
|
-
return String(value);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// src/lib/report/identity.ts
|
|
254
|
-
import { decodeAttestationBlob, verifyAttestationSignature } from "@hap/core";
|
|
255
|
-
var cache = /* @__PURE__ */ new WeakMap();
|
|
256
|
-
function cacheKey(archive) {
|
|
257
|
-
const auths = archive.getAuthorizations();
|
|
258
|
-
return `${auths.length}:${auths.map((a) => `${a.authorizationId}/${a.attestations.length}`).join(",")}:${archive.getReceipts().length}`;
|
|
259
|
-
}
|
|
260
|
-
async function build(archive) {
|
|
261
|
-
const names = /* @__PURE__ */ new Map();
|
|
262
|
-
const domainDids = /* @__PURE__ */ new Map();
|
|
263
|
-
const receipts = archive.getReceipts();
|
|
264
|
-
for (const auth of archive.getAuthorizations()) {
|
|
265
|
-
const key = receipts.find((r) => r.authorizationId === auth.authorizationId && r.asPublicKey)?.asPublicKey;
|
|
266
|
-
if (!key) continue;
|
|
267
|
-
for (const att of auth.attestations) {
|
|
268
|
-
try {
|
|
269
|
-
const attestation = decodeAttestationBlob(att.blob);
|
|
270
|
-
await verifyAttestationSignature(attestation, key);
|
|
271
|
-
for (const s of attestation.payload.subjects ?? []) {
|
|
272
|
-
if (s.assurance === "high" && s.disclose?.name && !names.has(s.did)) names.set(s.did, s.disclose.name);
|
|
273
|
-
}
|
|
274
|
-
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
275
|
-
if (rd?.domain && rd?.did && !domainDids.has(rd.domain)) domainDids.set(rd.domain, rd.did);
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return { names, domainDids };
|
|
282
|
-
}
|
|
283
|
-
function getIdentityDirectory(archive) {
|
|
284
|
-
const key = cacheKey(archive);
|
|
285
|
-
const hit = cache.get(archive);
|
|
286
|
-
if (hit && hit.key === key) return hit.dir;
|
|
287
|
-
const dir = build(archive);
|
|
288
|
-
cache.set(archive, { key, dir });
|
|
289
|
-
return dir;
|
|
290
|
-
}
|
|
291
|
-
function ownerLabel(dir, did) {
|
|
292
|
-
return dir.names.get(did) ?? formatOwnerLabel(did);
|
|
293
|
-
}
|
|
294
|
-
function accountTail(id) {
|
|
295
|
-
const head = id.split("-")[0] || id;
|
|
296
|
-
return head.length > 6 ? head.slice(-6) : head;
|
|
297
|
-
}
|
|
298
|
-
function approverLabel(dir, domainKey, userId) {
|
|
299
|
-
const did = dir.domainDids.get(domainKey) ?? dir.domainDids.get(userId);
|
|
300
|
-
const name = did ? dir.names.get(did) : void 0;
|
|
301
|
-
if (name) return name;
|
|
302
|
-
if (!userId) return "a person (unknown account)";
|
|
303
|
-
return `a person (account \u2026${accountTail(userId)})`;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// src/lib/report/ticket-resolvers.ts
|
|
307
|
-
function findReceiptEntry(archive, ticketId2) {
|
|
308
|
-
return archive.getReceipts().find((r) => r.receipt.id === ticketId2);
|
|
309
|
-
}
|
|
310
|
-
function findAuthorization(archive, authorizationId) {
|
|
311
|
-
return archive.getAuthorizations().find((a) => a.authorizationId === authorizationId);
|
|
312
|
-
}
|
|
313
|
-
async function checkTicket(archive, ticketId2) {
|
|
314
|
-
const entry = findReceiptEntry(archive, ticketId2);
|
|
315
|
-
if (!entry) {
|
|
316
|
-
return { ok: false, reason: `No ticket "${ticketId2}" in the local archive.` };
|
|
317
|
-
}
|
|
318
|
-
if (!entry.asPublicKey) {
|
|
319
|
-
return { ok: false, reason: `Ticket "${ticketId2}" has no archived Authority Server key to verify it against.`, entry };
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
await verifyReceiptSignature(entry.receipt, entry.asPublicKey);
|
|
323
|
-
} catch (err) {
|
|
324
|
-
return {
|
|
325
|
-
ok: false,
|
|
326
|
-
reason: `Ticket "${ticketId2}" signature does not verify: ${err instanceof Error ? err.message : String(err)}`,
|
|
327
|
-
entry
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
return { ok: true, entry };
|
|
331
|
-
}
|
|
332
|
-
function checkUrlFor(entry) {
|
|
333
|
-
const id = String(entry.receipt.id ?? "");
|
|
334
|
-
return `${entry.asUrl.replace(/\/+$/, "")}/r/${id}`;
|
|
335
|
-
}
|
|
336
|
-
async function resolveTicketElement(archive, ref) {
|
|
337
|
-
const check = await checkTicket(archive, ref);
|
|
338
|
-
if (!check.ok) return { status: "unverifiable", reason: check.reason };
|
|
339
|
-
const r = check.entry.receipt;
|
|
340
|
-
return {
|
|
341
|
-
status: "verified",
|
|
342
|
-
data: {
|
|
343
|
-
ticketId: ref,
|
|
344
|
-
action: r.action,
|
|
345
|
-
actionLabel: formatActionLabel(r.action),
|
|
346
|
-
time: r.timestamp,
|
|
347
|
-
timeLabel: formatDateTime(r.timestamp),
|
|
348
|
-
profile: r.profileId,
|
|
349
|
-
profileLabel: profileShortLabel(typeof r.profileId === "string" ? r.profileId : void 0),
|
|
350
|
-
authorizationId: r.authorizationId,
|
|
351
|
-
limitsUsed: r.limits ?? r.executionContext ?? {},
|
|
352
|
-
checkUrl: checkUrlFor(check.entry)
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
async function resolveApprovalElement(archive, ticketRef) {
|
|
357
|
-
const check = await checkTicket(archive, ticketRef);
|
|
358
|
-
if (!check.ok) return { status: "unverifiable", reason: check.reason };
|
|
359
|
-
const proposal = check.entry.proposal;
|
|
360
|
-
if (!proposal) {
|
|
361
|
-
return {
|
|
362
|
-
status: "unverifiable",
|
|
363
|
-
reason: `Ticket "${ticketRef}" has no archived approval (it ran without review).`
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
const committedBy = proposal.committedBy ?? {};
|
|
367
|
-
const approverEntries = Object.entries(committedBy);
|
|
368
|
-
const approvers = approverEntries.map(([, a]) => a);
|
|
369
|
-
const createdAt = typeof proposal.createdAt === "number" ? proposal.createdAt : void 0;
|
|
370
|
-
const decidedAt = approvers.length > 0 ? Math.max(...approvers.map((a) => a.at)) : void 0;
|
|
371
|
-
const waitSeconds = createdAt !== void 0 && decidedAt !== void 0 ? decidedAt - createdAt : void 0;
|
|
372
|
-
const who = approvers.map((a) => a.userId);
|
|
373
|
-
const dir = await getIdentityDirectory(archive);
|
|
374
|
-
const whoLabels = approverEntries.map(([domain, a]) => approverLabel(dir, domain, String(a.userId ?? "")));
|
|
375
|
-
return {
|
|
376
|
-
status: "verified",
|
|
377
|
-
data: {
|
|
378
|
-
ticketId: ticketRef,
|
|
379
|
-
who,
|
|
380
|
-
whoLabel: whoLabels.length > 0 ? whoLabels.join(", ") : "unknown",
|
|
381
|
-
createdAt,
|
|
382
|
-
createdAtLabel: createdAt !== void 0 ? formatDateTime(createdAt) : "unknown",
|
|
383
|
-
decidedAt,
|
|
384
|
-
decidedAtLabel: decidedAt !== void 0 ? formatDateTime(decidedAt) : "unknown",
|
|
385
|
-
waitSeconds,
|
|
386
|
-
waitLabel: waitSeconds !== void 0 ? formatDuration(waitSeconds) : "unknown",
|
|
387
|
-
status: proposal.status
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
async function resolveMandateElement(archive, ticketRef) {
|
|
392
|
-
const check = await checkTicket(archive, ticketRef);
|
|
393
|
-
if (!check.ok) return { status: "unverifiable", reason: check.reason };
|
|
394
|
-
const r = check.entry.receipt;
|
|
395
|
-
const authorizationId = typeof r.authorizationId === "string" ? r.authorizationId : void 0;
|
|
396
|
-
if (!authorizationId) {
|
|
397
|
-
return { status: "unverifiable", reason: `Ticket "${ticketRef}" carries no authorizationId.` };
|
|
398
|
-
}
|
|
399
|
-
const auth = findAuthorization(archive, authorizationId);
|
|
400
|
-
if (!auth) {
|
|
401
|
-
return { status: "unverifiable", reason: `No archived mandate "${authorizationId}" for ticket "${ticketRef}".` };
|
|
402
|
-
}
|
|
403
|
-
if (auth.boundsHash && typeof r.boundsHash === "string" && auth.boundsHash !== r.boundsHash) {
|
|
404
|
-
return {
|
|
405
|
-
status: "unverifiable",
|
|
406
|
-
reason: `Mandate "${authorizationId}" boundsHash does not match ticket "${ticketRef}".`
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
let mode;
|
|
410
|
-
let owners = [];
|
|
411
|
-
let ownersRaw = [];
|
|
412
|
-
const firstBlob = auth.attestations[0]?.blob;
|
|
413
|
-
if (firstBlob && check.entry.asPublicKey) {
|
|
414
|
-
try {
|
|
415
|
-
const attestation = decodeAttestationBlob2(firstBlob);
|
|
416
|
-
await verifyAttestationSignature2(attestation, check.entry.asPublicKey);
|
|
417
|
-
mode = attestation.payload.commitment_mode;
|
|
418
|
-
const dids = attestation.payload.resolved_owners ?? [];
|
|
419
|
-
const subjects = attestation.payload.subjects ?? [];
|
|
420
|
-
ownersRaw = dids;
|
|
421
|
-
const dir = await getIdentityDirectory(archive);
|
|
422
|
-
owners = dids.map((did) => {
|
|
423
|
-
const subject = subjects.find((s) => s.did === did);
|
|
424
|
-
return subject?.assurance === "high" && subject.disclose?.name ? subject.disclose.name : ownerLabel(dir, did);
|
|
425
|
-
});
|
|
426
|
-
} catch {
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
const fallbackCurrency = typeof auth.context?.currency === "string" ? auth.context.currency : void 0;
|
|
430
|
-
const rawLimits = auth.bounds ?? {};
|
|
431
|
-
const limits = Object.entries(rawLimits).map(([key, value]) => formatBoundLabel(auth.profileId, key, value, fallbackCurrency));
|
|
432
|
-
return {
|
|
433
|
-
status: "verified",
|
|
434
|
-
data: {
|
|
435
|
-
authorizationId,
|
|
436
|
-
profile: auth.profileId,
|
|
437
|
-
profileLabel: profileShortLabel(auth.profileId),
|
|
438
|
-
limits,
|
|
439
|
-
rawLimits,
|
|
440
|
-
intent: auth.intent,
|
|
441
|
-
mode,
|
|
442
|
-
owners,
|
|
443
|
-
ownersRaw
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// src/lib/report/time.ts
|
|
449
|
-
function parseTimestampSeconds(value) {
|
|
450
|
-
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
451
|
-
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;
|
|
452
|
-
const ms = Date.parse(iso);
|
|
453
|
-
return Number.isFinite(ms) ? Math.floor(ms / 1e3) : void 0;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// src/lib/report/case-resolvers.ts
|
|
457
|
-
function parseRef(value) {
|
|
458
|
-
if (!value) return { id: "" };
|
|
459
|
-
const i = value.indexOf(":");
|
|
460
|
-
if (i === -1) return { id: value.trim() };
|
|
461
|
-
return { type: value.slice(0, i).trim(), id: value.slice(i + 1).trim() };
|
|
462
|
-
}
|
|
463
|
-
function parseStepRefs(value) {
|
|
464
|
-
if (!value) return [];
|
|
465
|
-
return value.split(/\s+/).filter(Boolean).map((s) => parseRef(s).id || s);
|
|
466
|
-
}
|
|
467
|
-
function parseRecipients(toJson, ccJson) {
|
|
468
|
-
const out = [];
|
|
469
|
-
for (const raw of [toJson, ccJson]) {
|
|
470
|
-
if (!raw) continue;
|
|
471
|
-
try {
|
|
472
|
-
const arr = JSON.parse(raw);
|
|
473
|
-
if (Array.isArray(arr)) out.push(...arr.filter((x) => typeof x === "string"));
|
|
474
|
-
} catch {
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return out.map((s) => s.toLowerCase());
|
|
478
|
-
}
|
|
479
|
-
function parseCaseAttrs(attrs) {
|
|
480
|
-
return {
|
|
481
|
-
startId: parseRef(attrs.start).id,
|
|
482
|
-
goalId: parseRef(attrs.goal).id,
|
|
483
|
-
stepIds: parseStepRefs(attrs.steps)
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
var CASE_TIME_UNKNOWN_REASON = "Not verifiable: the time the test data was loaded is unknown, and the email's own date may be set by the test package.";
|
|
487
|
-
async function resolveCaseElement(archive, emailExport, attrs) {
|
|
488
|
-
const start = parseRef(attrs.start);
|
|
489
|
-
const startMessage = emailExport.inbox.find((m) => m.id === start.id);
|
|
490
|
-
if (!startMessage || !startMessage.case_id) {
|
|
491
|
-
return { status: "unverifiable", reason: `start "${attrs.start ?? ""}" is not a loaded case email with a case_id.` };
|
|
492
|
-
}
|
|
493
|
-
const startCaseId = startMessage.case_id;
|
|
494
|
-
const emailTime = parseTimestampSeconds(startMessage.received_at);
|
|
495
|
-
if (emailTime === void 0) {
|
|
496
|
-
return { status: "unverifiable", reason: `start message "${start.id}" has an unparseable received_at.`, startCaseId };
|
|
497
|
-
}
|
|
498
|
-
const loadedAt = parseTimestampSeconds(emailExport.simulation_load?.loaded_at);
|
|
499
|
-
const startTime = loadedAt !== void 0 ? Math.max(emailTime, loadedAt) : emailTime;
|
|
500
|
-
const basis = loadedAt === void 0 ? "load-unknown" : emailTime >= loadedAt ? "email" : "loaded";
|
|
501
|
-
const goal = parseRef(attrs.goal);
|
|
502
|
-
const goalCheck = await checkTicket(archive, goal.id);
|
|
503
|
-
if (!goalCheck.ok) {
|
|
504
|
-
return { status: "unverifiable", reason: `goal: ${goalCheck.reason}`, startCaseId };
|
|
505
|
-
}
|
|
506
|
-
const goalReceipt = goalCheck.entry.receipt;
|
|
507
|
-
const goalTime = typeof goalReceipt.timestamp === "number" ? goalReceipt.timestamp : void 0;
|
|
508
|
-
if (goalTime === void 0) {
|
|
509
|
-
return { status: "unverifiable", reason: `goal ticket "${goal.id}" has no numeric timestamp.`, startCaseId };
|
|
510
|
-
}
|
|
511
|
-
if (goalTime < startTime) {
|
|
512
|
-
return { status: "unverifiable", reason: `goal ticket "${goal.id}" is timestamped before start "${start.id}".`, startCaseId };
|
|
513
|
-
}
|
|
514
|
-
const stepIds = parseStepRefs(attrs.steps);
|
|
515
|
-
const steps = [];
|
|
516
|
-
for (const stepId of stepIds) {
|
|
517
|
-
const stepCheck = await checkTicket(archive, stepId);
|
|
518
|
-
if (!stepCheck.ok) {
|
|
519
|
-
return { status: "unverifiable", reason: `step: ${stepCheck.reason}`, startCaseId };
|
|
520
|
-
}
|
|
521
|
-
const stepReceipt = stepCheck.entry.receipt;
|
|
522
|
-
const stepTime = typeof stepReceipt.timestamp === "number" ? stepReceipt.timestamp : void 0;
|
|
523
|
-
if (stepTime === void 0 || stepTime < startTime || stepTime > goalTime) {
|
|
524
|
-
return { status: "unverifiable", reason: `step ticket "${stepId}" is outside the case window [${startTime}, ${goalTime}].`, startCaseId };
|
|
525
|
-
}
|
|
526
|
-
steps.push({ ticketId: stepId, time: stepTime, action: stepReceipt.action, actionLabel: formatActionLabel(stepReceipt.action) });
|
|
527
|
-
}
|
|
528
|
-
let warningReason;
|
|
529
|
-
const sentForGoal = emailExport.sent.find((m) => m.receipt_id === goal.id);
|
|
530
|
-
if (sentForGoal) {
|
|
531
|
-
const isReply = sentForGoal.in_reply_to === start.id;
|
|
532
|
-
const recipients = parseRecipients(sentForGoal.to_json, sentForGoal.cc_json);
|
|
533
|
-
const addressedToSender = recipients.includes(startMessage.from_email.toLowerCase());
|
|
534
|
-
if (!isReply && !addressedToSender) {
|
|
535
|
-
warningReason = "goal link not confirmed";
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
const approvals = [];
|
|
539
|
-
for (const ticketId2 of [goal.id, ...stepIds]) {
|
|
540
|
-
const approval = await resolveApprovalElement(archive, ticketId2);
|
|
541
|
-
if (approval.status === "verified") {
|
|
542
|
-
approvals.push({ ticketId: ticketId2, ...approval.data });
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
const timeline = [
|
|
546
|
-
{ type: "start", time: startTime, label: start.id },
|
|
547
|
-
...steps.map((s) => ({ type: "step", time: s.time, label: s.ticketId })),
|
|
548
|
-
...approvals.map((a) => ({ type: "approval", time: a.decidedAt ?? a.createdAt ?? 0, label: a.ticketId })),
|
|
549
|
-
{ type: "goal", time: goalTime, label: goal.id }
|
|
550
|
-
].sort((a, b) => a.time - b.time);
|
|
551
|
-
return {
|
|
552
|
-
status: warningReason ? "warning" : "verified",
|
|
553
|
-
reason: warningReason,
|
|
554
|
-
startCaseId,
|
|
555
|
-
data: {
|
|
556
|
-
caseId: startCaseId,
|
|
557
|
-
start: { id: start.id, time: startTime, sender: startMessage.from_email, emailTime, basis },
|
|
558
|
-
goal: { ticketId: goal.id, time: goalTime, action: goalReceipt.action, actionLabel: formatActionLabel(goalReceipt.action) },
|
|
559
|
-
steps,
|
|
560
|
-
approvals,
|
|
561
|
-
timeline,
|
|
562
|
-
totalDurationSeconds: basis === "load-unknown" ? null : goalTime - startTime,
|
|
563
|
-
...basis === "load-unknown" ? { timeUnverifiableReason: CASE_TIME_UNKNOWN_REASON } : {}
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// src/lib/report/verify-export.ts
|
|
569
|
-
function collectReferencedTicketIds(html) {
|
|
570
|
-
const ids = /* @__PURE__ */ new Set();
|
|
571
|
-
for (const el of parseElements(html)) {
|
|
572
|
-
if (el.kind === "sv-ticket" && el.attrs.ref) ids.add(el.attrs.ref);
|
|
573
|
-
if ((el.kind === "sv-approval" || el.kind === "sv-mandate") && el.attrs.ticket) ids.add(el.attrs.ticket);
|
|
574
|
-
if (el.kind === "sv-case") {
|
|
575
|
-
const { goalId, stepIds } = parseCaseAttrs(el.attrs);
|
|
576
|
-
if (goalId) ids.add(goalId);
|
|
577
|
-
for (const s of stepIds) ids.add(s);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return ids;
|
|
581
|
-
}
|
|
582
|
-
function ticketIdsOf(el) {
|
|
583
|
-
if (el.kind === "sv-ticket") return el.attrs.ref ? [el.attrs.ref] : [];
|
|
584
|
-
if (el.kind === "sv-approval" || el.kind === "sv-mandate") return el.attrs.ticket ? [el.attrs.ticket] : [];
|
|
585
|
-
if (el.kind === "sv-case") {
|
|
586
|
-
const { goalId, stepIds } = parseCaseAttrs(el.attrs);
|
|
587
|
-
return [...goalId ? [goalId] : [], ...stepIds];
|
|
588
|
-
}
|
|
589
|
-
return [];
|
|
590
|
-
}
|
|
591
|
-
var TICKET_BACKED_KINDS = /* @__PURE__ */ new Set(["sv-ticket", "sv-approval", "sv-mandate", "sv-case"]);
|
|
592
|
-
var PROOF_SCRIPT_BLOCK_RE = /<script[^>]*id="suveren-proof"[^>]*>[\s\S]*?<\/script>/gi;
|
|
593
|
-
function collectPresentedStates(documentHtml) {
|
|
594
|
-
const html = documentHtml.replace(PROOF_SCRIPT_BLOCK_RE, "");
|
|
595
|
-
const open = [];
|
|
596
|
-
const flaggedById = /* @__PURE__ */ new Map();
|
|
597
|
-
let depth = 0;
|
|
598
|
-
const classesOf = (attribs) => new Set((attribs.class ?? "").split(/\s+/).filter(Boolean));
|
|
599
|
-
const settle = (f) => {
|
|
600
|
-
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"));
|
|
601
|
-
flaggedById.set(f.id, (flaggedById.get(f.id) ?? true) && flagged);
|
|
602
|
-
};
|
|
603
|
-
const parser = new Parser2(
|
|
604
|
-
{
|
|
605
|
-
onopentag(_name, attribs) {
|
|
606
|
-
depth++;
|
|
607
|
-
const classes = classesOf(attribs);
|
|
608
|
-
if ([...classes].some((c) => c.startsWith("sv-badge"))) {
|
|
609
|
-
for (const f of open) f.badges.push(classes);
|
|
610
|
-
}
|
|
611
|
-
const id = attribs["data-sv-id"];
|
|
612
|
-
if (typeof id === "string" && id) open.push({ id, depth, classes, badges: [] });
|
|
613
|
-
},
|
|
614
|
-
onclosetag() {
|
|
615
|
-
while (open.length > 0 && open[open.length - 1].depth >= depth) settle(open.pop());
|
|
616
|
-
depth--;
|
|
617
|
-
}
|
|
618
|
-
},
|
|
619
|
-
{ decodeEntities: true }
|
|
620
|
-
);
|
|
621
|
-
parser.write(html);
|
|
622
|
-
parser.end();
|
|
623
|
-
while (open.length > 0) settle(open.pop());
|
|
624
|
-
const states = /* @__PURE__ */ new Map();
|
|
625
|
-
for (const [id, flagged] of flaggedById) states.set(id, flagged ? "not-verifiable" : "verified");
|
|
626
|
-
return states;
|
|
627
|
-
}
|
|
628
|
-
function ticketId(receipt) {
|
|
629
|
-
return typeof receipt.id === "string" ? receipt.id : "";
|
|
630
|
-
}
|
|
631
|
-
async function checkAttestations(auth, publicKeyHex) {
|
|
632
|
-
if (auth.attestations.length === 0) {
|
|
633
|
-
return { valid: false, error: "No archived mandate attestation for this authorization." };
|
|
634
|
-
}
|
|
635
|
-
for (const att of auth.attestations) {
|
|
636
|
-
try {
|
|
637
|
-
const attestation = decodeAttestationBlob3(att.blob);
|
|
638
|
-
await verifyAttestationSignature3(attestation, publicKeyHex);
|
|
639
|
-
} catch (err) {
|
|
640
|
-
return { valid: false, error: err instanceof Error ? err.message : String(err) };
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
return { valid: true };
|
|
644
|
-
}
|
|
645
|
-
async function verifyExportBundle(bundle, opts = {}) {
|
|
646
|
-
const byId = new Map(bundle.tickets.map((t) => [ticketId(t), t]));
|
|
647
|
-
const referencedIds = collectReferencedTicketIds(bundle.report.html);
|
|
648
|
-
const allIds = /* @__PURE__ */ new Set([...referencedIds, ...byId.keys()]);
|
|
649
|
-
const tickets = [];
|
|
650
|
-
for (const id of allIds) {
|
|
651
|
-
const receipt = byId.get(id);
|
|
652
|
-
if (!receipt) {
|
|
653
|
-
tickets.push({ ticketId: id, referenced: referencedIds.has(id), present: false, signatureValid: false, error: "Not present in the bundle." });
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
try {
|
|
657
|
-
await verifyReceiptSignature2(receipt, bundle.authorityServer.publicKeyHex);
|
|
658
|
-
tickets.push({ ticketId: id, referenced: referencedIds.has(id), present: true, signatureValid: true });
|
|
659
|
-
} catch (err) {
|
|
660
|
-
tickets.push({
|
|
661
|
-
ticketId: id,
|
|
662
|
-
referenced: referencedIds.has(id),
|
|
663
|
-
present: true,
|
|
664
|
-
signatureValid: false,
|
|
665
|
-
error: err instanceof Error ? err.message : String(err)
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
const authorizations = [];
|
|
670
|
-
for (const [authorizationId, auth] of Object.entries(bundle.authorizations)) {
|
|
671
|
-
const { valid: attestationValid, error: attestationError } = await checkAttestations(auth, bundle.authorityServer.publicKeyHex);
|
|
672
|
-
let boundsHashMatches = "n/a";
|
|
673
|
-
const representative = [...byId.values()].find((r) => r.authorizationId === authorizationId);
|
|
674
|
-
if (auth.boundsHash && representative && typeof representative.boundsHash === "string") {
|
|
675
|
-
boundsHashMatches = auth.boundsHash === representative.boundsHash;
|
|
676
|
-
}
|
|
677
|
-
authorizations.push({
|
|
678
|
-
authorizationId,
|
|
679
|
-
attestationValid,
|
|
680
|
-
boundsHashMatches,
|
|
681
|
-
...attestationError ? { error: attestationError } : boundsHashMatches === false ? { error: `boundsHash does not match ticket ${representative?.id ?? "unknown"}.` } : {}
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
const ticketById = new Map(tickets.map((t) => [t.ticketId, t]));
|
|
685
|
-
const authById = new Map(authorizations.map((a) => [a.authorizationId, a]));
|
|
686
|
-
const presentedStates = opts.documentHtml !== void 0 ? collectPresentedStates(opts.documentHtml) : /* @__PURE__ */ new Map();
|
|
687
|
-
const elements = [];
|
|
688
|
-
for (const el of parseElements(bundle.report.html)) {
|
|
689
|
-
if (!TICKET_BACKED_KINDS.has(el.kind)) continue;
|
|
690
|
-
const ticketIds = ticketIdsOf(el);
|
|
691
|
-
const presented = presentedStates.get(el.id) ?? "verified";
|
|
692
|
-
let error;
|
|
693
|
-
if (ticketIds.length === 0) error = "Names no ticket.";
|
|
694
|
-
for (const id of ticketIds) {
|
|
695
|
-
if (error) break;
|
|
696
|
-
const t = ticketById.get(id);
|
|
697
|
-
if (!t || !t.present) error = `Ticket ${id} is not in the bundle.`;
|
|
698
|
-
else if (!t.signatureValid) error = `Ticket ${id} has an invalid signature.`;
|
|
699
|
-
}
|
|
700
|
-
if (!error && el.kind === "sv-mandate") {
|
|
701
|
-
const receipt = byId.get(ticketIds[0]);
|
|
702
|
-
const authorizationId = typeof receipt.authorizationId === "string" ? receipt.authorizationId : void 0;
|
|
703
|
-
const auth = authorizationId ? bundle.authorizations[authorizationId] : void 0;
|
|
704
|
-
const authCheck = authorizationId ? authById.get(authorizationId) : void 0;
|
|
705
|
-
if (!auth || !authCheck) error = `The mandate for ticket ${ticketIds[0]} is not in the bundle.`;
|
|
706
|
-
else if (!authCheck.attestationValid) error = `The mandate for ticket ${ticketIds[0]} has an invalid attestation.`;
|
|
707
|
-
else if (auth.boundsHash && typeof receipt.boundsHash === "string" && auth.boundsHash !== receipt.boundsHash) {
|
|
708
|
-
error = `The mandate's boundsHash does not match ticket ${ticketIds[0]}.`;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
elements.push({ elementId: el.id, kind: el.kind, ticketIds, presented, backed: !error, ...error ? { error } : {} });
|
|
712
|
-
}
|
|
713
|
-
const keyFingerprint = fingerprintOf(bundle.authorityServer.publicKeyHex);
|
|
714
|
-
const keyConfirmation = resolveKeyConfirmation(bundle.authorityServer.publicKeyHex, opts);
|
|
715
|
-
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);
|
|
716
|
-
return { tickets, elements, authorizations, keyFingerprint, keyConfirmation, allValid };
|
|
717
|
-
}
|
|
718
|
-
function resolveKeyConfirmation(actualKeyHex, opts) {
|
|
719
|
-
const normalize = (h) => h.trim().toLowerCase();
|
|
720
|
-
if (opts.expectedKeyHex !== void 0) {
|
|
721
|
-
if (normalize(opts.expectedKeyHex) === normalize(actualKeyHex)) return { state: "confirmed", source: "provided" };
|
|
722
|
-
return { state: "mismatch", source: "provided", expectedFingerprint: fingerprintOf(opts.expectedKeyHex) };
|
|
723
|
-
}
|
|
724
|
-
if (opts.onlineKeyHex !== void 0) {
|
|
725
|
-
if (normalize(opts.onlineKeyHex) === normalize(actualKeyHex)) return { state: "confirmed", source: "online" };
|
|
726
|
-
return { state: "mismatch", source: "online", expectedFingerprint: fingerprintOf(opts.onlineKeyHex) };
|
|
727
|
-
}
|
|
728
|
-
return { state: "unconfirmed" };
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
export {
|
|
732
|
-
readPairing,
|
|
733
|
-
fingerprintOf,
|
|
734
|
-
parseElements,
|
|
735
|
-
formatDateTime,
|
|
736
|
-
formatDuration,
|
|
737
|
-
formatCurrency,
|
|
738
|
-
profileShortLabel,
|
|
739
|
-
formatActionLabel,
|
|
740
|
-
METRIC_LABELS,
|
|
741
|
-
formatMetricValue,
|
|
742
|
-
resolveTicketElement,
|
|
743
|
-
resolveApprovalElement,
|
|
744
|
-
resolveMandateElement,
|
|
745
|
-
parseTimestampSeconds,
|
|
746
|
-
parseCaseAttrs,
|
|
747
|
-
CASE_TIME_UNKNOWN_REASON,
|
|
748
|
-
resolveCaseElement,
|
|
749
|
-
isExportBundle,
|
|
750
|
-
verifyExportBundle
|
|
751
|
-
};
|