@suveren/gateway 0.18.1 → 0.18.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/dist/mcp-server/{chunk-JUFH4SKS.mjs → chunk-QOKPECVY.mjs} +127 -15
- package/dist/mcp-server/http.mjs +67 -33
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/{index-Dfh38n-b.css → index-Bfi-G4qJ.css} +1 -1
- package/dist/ui/assets/index-D0uXCqQB.js +111 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-78vuGrVE.js +0 -111
|
@@ -42,7 +42,7 @@ function isExportBundle(v) {
|
|
|
42
42
|
|
|
43
43
|
// src/lib/report/verify-export.ts
|
|
44
44
|
import { Parser as Parser2 } from "htmlparser2";
|
|
45
|
-
import { verifyReceiptSignature as verifyReceiptSignature2, verifyAttestationSignature as
|
|
45
|
+
import { verifyReceiptSignature as verifyReceiptSignature2, verifyAttestationSignature as verifyAttestationSignature3, decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
|
|
46
46
|
|
|
47
47
|
// src/lib/report/parse-elements.ts
|
|
48
48
|
import { Parser } from "htmlparser2";
|
|
@@ -66,7 +66,7 @@ function parseElements(html) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// src/lib/report/ticket-resolvers.ts
|
|
69
|
-
import { verifyReceiptSignature, verifyAttestationSignature, decodeAttestationBlob } from "@hap/core";
|
|
69
|
+
import { verifyReceiptSignature, verifyAttestationSignature as verifyAttestationSignature2, decodeAttestationBlob as decodeAttestationBlob2 } from "@hap/core";
|
|
70
70
|
|
|
71
71
|
// src/lib/report/format.ts
|
|
72
72
|
import { getProfile } from "@hap/core";
|
|
@@ -162,9 +162,58 @@ var ACTION_LABELS = {
|
|
|
162
162
|
delete_event: "Event deleted",
|
|
163
163
|
publish_post: "Post published"
|
|
164
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"]);
|
|
165
210
|
function pastTense(verb) {
|
|
166
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`;
|
|
167
215
|
if (/e$/.test(verb)) return `${verb}d`;
|
|
216
|
+
if (/[^aeiou]y$/i.test(verb)) return `${verb.slice(0, -1)}ied`;
|
|
168
217
|
return `${verb}ed`;
|
|
169
218
|
}
|
|
170
219
|
function capitalize(s) {
|
|
@@ -201,6 +250,59 @@ function formatMetricValue(kind, value) {
|
|
|
201
250
|
return String(value);
|
|
202
251
|
}
|
|
203
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
|
+
|
|
204
306
|
// src/lib/report/ticket-resolvers.ts
|
|
205
307
|
function findReceiptEntry(archive, ticketId2) {
|
|
206
308
|
return archive.getReceipts().find((r) => r.receipt.id === ticketId2);
|
|
@@ -262,17 +364,20 @@ async function resolveApprovalElement(archive, ticketRef) {
|
|
|
262
364
|
};
|
|
263
365
|
}
|
|
264
366
|
const committedBy = proposal.committedBy ?? {};
|
|
265
|
-
const
|
|
367
|
+
const approverEntries = Object.entries(committedBy);
|
|
368
|
+
const approvers = approverEntries.map(([, a]) => a);
|
|
266
369
|
const createdAt = typeof proposal.createdAt === "number" ? proposal.createdAt : void 0;
|
|
267
370
|
const decidedAt = approvers.length > 0 ? Math.max(...approvers.map((a) => a.at)) : void 0;
|
|
268
371
|
const waitSeconds = createdAt !== void 0 && decidedAt !== void 0 ? decidedAt - createdAt : void 0;
|
|
269
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 ?? "")));
|
|
270
375
|
return {
|
|
271
376
|
status: "verified",
|
|
272
377
|
data: {
|
|
273
378
|
ticketId: ticketRef,
|
|
274
379
|
who,
|
|
275
|
-
whoLabel:
|
|
380
|
+
whoLabel: whoLabels.length > 0 ? whoLabels.join(", ") : "unknown",
|
|
276
381
|
createdAt,
|
|
277
382
|
createdAtLabel: createdAt !== void 0 ? formatDateTime(createdAt) : "unknown",
|
|
278
383
|
decidedAt,
|
|
@@ -307,15 +412,16 @@ async function resolveMandateElement(archive, ticketRef) {
|
|
|
307
412
|
const firstBlob = auth.attestations[0]?.blob;
|
|
308
413
|
if (firstBlob && check.entry.asPublicKey) {
|
|
309
414
|
try {
|
|
310
|
-
const attestation =
|
|
311
|
-
await
|
|
415
|
+
const attestation = decodeAttestationBlob2(firstBlob);
|
|
416
|
+
await verifyAttestationSignature2(attestation, check.entry.asPublicKey);
|
|
312
417
|
mode = attestation.payload.commitment_mode;
|
|
313
418
|
const dids = attestation.payload.resolved_owners ?? [];
|
|
314
419
|
const subjects = attestation.payload.subjects ?? [];
|
|
315
420
|
ownersRaw = dids;
|
|
421
|
+
const dir = await getIdentityDirectory(archive);
|
|
316
422
|
owners = dids.map((did) => {
|
|
317
423
|
const subject = subjects.find((s) => s.did === did);
|
|
318
|
-
return subject?.assurance === "high" && subject.disclose?.name ? subject.disclose.name :
|
|
424
|
+
return subject?.assurance === "high" && subject.disclose?.name ? subject.disclose.name : ownerLabel(dir, did);
|
|
319
425
|
});
|
|
320
426
|
} catch {
|
|
321
427
|
}
|
|
@@ -377,6 +483,7 @@ function parseCaseAttrs(attrs) {
|
|
|
377
483
|
stepIds: parseStepRefs(attrs.steps)
|
|
378
484
|
};
|
|
379
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.";
|
|
380
487
|
async function resolveCaseElement(archive, emailExport, attrs) {
|
|
381
488
|
const start = parseRef(attrs.start);
|
|
382
489
|
const startMessage = emailExport.inbox.find((m) => m.id === start.id);
|
|
@@ -384,10 +491,13 @@ async function resolveCaseElement(archive, emailExport, attrs) {
|
|
|
384
491
|
return { status: "unverifiable", reason: `start "${attrs.start ?? ""}" is not a loaded case email with a case_id.` };
|
|
385
492
|
}
|
|
386
493
|
const startCaseId = startMessage.case_id;
|
|
387
|
-
const
|
|
388
|
-
if (
|
|
494
|
+
const emailTime = parseTimestampSeconds(startMessage.received_at);
|
|
495
|
+
if (emailTime === void 0) {
|
|
389
496
|
return { status: "unverifiable", reason: `start message "${start.id}" has an unparseable received_at.`, startCaseId };
|
|
390
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";
|
|
391
501
|
const goal = parseRef(attrs.goal);
|
|
392
502
|
const goalCheck = await checkTicket(archive, goal.id);
|
|
393
503
|
if (!goalCheck.ok) {
|
|
@@ -413,7 +523,7 @@ async function resolveCaseElement(archive, emailExport, attrs) {
|
|
|
413
523
|
if (stepTime === void 0 || stepTime < startTime || stepTime > goalTime) {
|
|
414
524
|
return { status: "unverifiable", reason: `step ticket "${stepId}" is outside the case window [${startTime}, ${goalTime}].`, startCaseId };
|
|
415
525
|
}
|
|
416
|
-
steps.push({ ticketId: stepId, time: stepTime, action: stepReceipt.action });
|
|
526
|
+
steps.push({ ticketId: stepId, time: stepTime, action: stepReceipt.action, actionLabel: formatActionLabel(stepReceipt.action) });
|
|
417
527
|
}
|
|
418
528
|
let warningReason;
|
|
419
529
|
const sentForGoal = emailExport.sent.find((m) => m.receipt_id === goal.id);
|
|
@@ -444,12 +554,13 @@ async function resolveCaseElement(archive, emailExport, attrs) {
|
|
|
444
554
|
startCaseId,
|
|
445
555
|
data: {
|
|
446
556
|
caseId: startCaseId,
|
|
447
|
-
start: { id: start.id, time: startTime, sender: startMessage.from_email },
|
|
448
|
-
goal: { ticketId: goal.id, time: goalTime, action: goalReceipt.action },
|
|
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) },
|
|
449
559
|
steps,
|
|
450
560
|
approvals,
|
|
451
561
|
timeline,
|
|
452
|
-
totalDurationSeconds: goalTime - startTime
|
|
562
|
+
totalDurationSeconds: basis === "load-unknown" ? null : goalTime - startTime,
|
|
563
|
+
...basis === "load-unknown" ? { timeUnverifiableReason: CASE_TIME_UNKNOWN_REASON } : {}
|
|
453
564
|
}
|
|
454
565
|
};
|
|
455
566
|
}
|
|
@@ -523,8 +634,8 @@ async function checkAttestations(auth, publicKeyHex) {
|
|
|
523
634
|
}
|
|
524
635
|
for (const att of auth.attestations) {
|
|
525
636
|
try {
|
|
526
|
-
const attestation =
|
|
527
|
-
await
|
|
637
|
+
const attestation = decodeAttestationBlob3(att.blob);
|
|
638
|
+
await verifyAttestationSignature3(attestation, publicKeyHex);
|
|
528
639
|
} catch (err) {
|
|
529
640
|
return { valid: false, error: err instanceof Error ? err.message : String(err) };
|
|
530
641
|
}
|
|
@@ -633,6 +744,7 @@ export {
|
|
|
633
744
|
resolveMandateElement,
|
|
634
745
|
parseTimestampSeconds,
|
|
635
746
|
parseCaseAttrs,
|
|
747
|
+
CASE_TIME_UNKNOWN_REASON,
|
|
636
748
|
resolveCaseElement,
|
|
637
749
|
isExportBundle,
|
|
638
750
|
verifyExportBundle
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CASE_TIME_UNKNOWN_REASON,
|
|
3
4
|
METRIC_LABELS,
|
|
4
5
|
fingerprintOf,
|
|
5
6
|
formatActionLabel,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
resolveCaseElement,
|
|
17
18
|
resolveMandateElement,
|
|
18
19
|
resolveTicketElement
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-QOKPECVY.mjs";
|
|
20
21
|
|
|
21
22
|
// bin/http.ts
|
|
22
23
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -2113,8 +2114,8 @@ What you cannot write yourself: facts that need proof. Place these elements inst
|
|
|
2113
2114
|
|
|
2114
2115
|
Rules:
|
|
2115
2116
|
1. Define every case you worked on with sv-case \u2014 also the ones that did not reach their goal (omit goal, or say why in your text). The gateway shows management how many of the loaded cases your report covers and names the missing ones \u2014 and lists every ticket from the test period that your report does not reference, so name every step.
|
|
2116
|
-
2. Use sv-metric for every headline number.
|
|
2117
|
-
3. A case's goal must be what actually completed it \u2014 for an email reply, the reply to the start email. The gateway checks this link.
|
|
2117
|
+
2. Use sv-metric for every headline number. Everything you write yourself \u2014 text, tables, charts, numbers you compute \u2014 sits under the gateway's label "AI analysis \u2014 not verified"; only the drawn elements carry the gateway's green check. Do not imitate them: class names starting with "sv-" are removed from your HTML.
|
|
2118
|
+
3. A case's goal must be what actually completed it \u2014 for an email reply, the reply to the start email. The gateway checks this link. A case is timed from when its email reached the test (the test data may date its emails earlier \u2014 the gateway uses the later of the two), so steps from before that moment do not belong to it.
|
|
2118
2119
|
4. A wrong or unknown reference is shown as "not verifiable". Do not guess IDs \u2014 read them first.
|
|
2119
2120
|
5. Never claim more than the elements prove. Say plainly what went wrong or waited for a person.
|
|
2120
2121
|
|
|
@@ -2140,6 +2141,9 @@ function isAllowedUrl(value) {
|
|
|
2140
2141
|
if (/^data:image\//i.test(v)) return true;
|
|
2141
2142
|
return false;
|
|
2142
2143
|
}
|
|
2144
|
+
function stripGatewayClasses(value) {
|
|
2145
|
+
return value.split(/\s+/).filter((c) => c && !/^sv-/i.test(c)).join(" ");
|
|
2146
|
+
}
|
|
2143
2147
|
function sanitizeReportHtml(html) {
|
|
2144
2148
|
return sanitizeHtmlLib(html, {
|
|
2145
2149
|
// Allow every tag/attribute by default — see module doc comment for why
|
|
@@ -2182,8 +2186,15 @@ function sanitizeReportHtml(html) {
|
|
|
2182
2186
|
"*": (tagName, attribs) => {
|
|
2183
2187
|
const kept = {};
|
|
2184
2188
|
for (const [name, value] of Object.entries(attribs)) {
|
|
2185
|
-
|
|
2189
|
+
const lower = name.toLowerCase();
|
|
2190
|
+
if (lower.startsWith("on")) continue;
|
|
2186
2191
|
if (isUrlAttribute(name) && !isAllowedUrl(value)) continue;
|
|
2192
|
+
if (lower.startsWith("data-sv")) continue;
|
|
2193
|
+
if (lower === "class") {
|
|
2194
|
+
const classes = stripGatewayClasses(value);
|
|
2195
|
+
if (classes) kept[name] = classes;
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2187
2198
|
kept[name] = value;
|
|
2188
2199
|
}
|
|
2189
2200
|
return { tagName, attribs: kept };
|
|
@@ -2257,6 +2268,7 @@ var UnknownMetricKindError = class extends Error {
|
|
|
2257
2268
|
this.name = "UnknownMetricKindError";
|
|
2258
2269
|
}
|
|
2259
2270
|
};
|
|
2271
|
+
var START_TIME_METRICS = /* @__PURE__ */ new Set(["median-time", "average-time", "refusals"]);
|
|
2260
2272
|
function median(values) {
|
|
2261
2273
|
if (values.length === 0) return 0;
|
|
2262
2274
|
const sorted = [...values].sort((a, b) => a - b);
|
|
@@ -2268,6 +2280,9 @@ function average(values) {
|
|
|
2268
2280
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
2269
2281
|
}
|
|
2270
2282
|
function computeMetric(kind, cases, refusalTimes = []) {
|
|
2283
|
+
if (START_TIME_METRICS.has(kind) && cases.some((c) => c.totalDurationSeconds === null)) {
|
|
2284
|
+
throw new Error("Case start times are not verifiable.");
|
|
2285
|
+
}
|
|
2271
2286
|
switch (kind) {
|
|
2272
2287
|
case "completed":
|
|
2273
2288
|
return cases.length;
|
|
@@ -2435,6 +2450,10 @@ async function verifyReport(html, sources) {
|
|
|
2435
2450
|
});
|
|
2436
2451
|
continue;
|
|
2437
2452
|
}
|
|
2453
|
+
if (START_TIME_METRICS.has(kind) && caseInputs.some((c) => c.totalDurationSeconds === null)) {
|
|
2454
|
+
elements.push({ id: p.id, kind: p.kind, attrs: p.attrs, status: "unverifiable", reason: CASE_TIME_UNKNOWN_REASON });
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2438
2457
|
const refusalTimes = kind === "refusals" ? await collectRefusalTimes(getExport) : [];
|
|
2439
2458
|
const value = computeMetric(kind, caseInputs, refusalTimes);
|
|
2440
2459
|
const data = { kind, cases: casesAttr, value, caseCount: verifiedCount };
|
|
@@ -2493,7 +2512,8 @@ function summarize(el) {
|
|
|
2493
2512
|
return `${String(d.kind ?? "record")} record checked`;
|
|
2494
2513
|
case "sv-case": {
|
|
2495
2514
|
const steps = d.steps?.length ?? 0;
|
|
2496
|
-
|
|
2515
|
+
const time = typeof d.totalDurationSeconds === "number" ? formatDuration(d.totalDurationSeconds) : "time not verifiable";
|
|
2516
|
+
return `Case ${String(d.caseId)}: ${steps} ${steps === 1 ? "step" : "steps"}, ${time}`;
|
|
2497
2517
|
}
|
|
2498
2518
|
case "sv-metric": {
|
|
2499
2519
|
const kind = String(d.kind ?? "");
|
|
@@ -6294,10 +6314,6 @@ function findElementSpans(html) {
|
|
|
6294
6314
|
function escapeHtml(value) {
|
|
6295
6315
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6296
6316
|
}
|
|
6297
|
-
function detailLink(id, ticketId) {
|
|
6298
|
-
const qs = ticketId ? `?element=${encodeURIComponent(id)}&ticket=${encodeURIComponent(ticketId)}` : `?element=${encodeURIComponent(id)}`;
|
|
6299
|
-
return `/reports${qs}`;
|
|
6300
|
-
}
|
|
6301
6317
|
function badge(status, okLabel) {
|
|
6302
6318
|
if (status === "verified") return `<span class="sv-badge sv-badge-ok">\u2713 ${escapeHtml(okLabel)}</span>`;
|
|
6303
6319
|
if (status === "warning") return `<span class="sv-badge sv-badge-warn">\u26A0 verified with a note</span>`;
|
|
@@ -6306,7 +6322,7 @@ function badge(status, okLabel) {
|
|
|
6306
6322
|
function unverifiableCard(el) {
|
|
6307
6323
|
return `<div class="sv-el sv-el-unverifiable" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Not verifiable</b>${badge("unverifiable", "")}</div><div class="sv-el-reason">${escapeHtml(el.reason ?? "This reference could not be checked.")}</div></div>`;
|
|
6308
6324
|
}
|
|
6309
|
-
function renderTicket(el
|
|
6325
|
+
function renderTicket(el) {
|
|
6310
6326
|
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6311
6327
|
const d = el.data;
|
|
6312
6328
|
const href = typeof d.checkUrl === "string" ? d.checkUrl : void 0;
|
|
@@ -6314,27 +6330,26 @@ function renderTicket(el, interactive) {
|
|
|
6314
6330
|
const timeLabel = typeof d.timeLabel === "string" ? d.timeLabel : formatDateTime(d.time);
|
|
6315
6331
|
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : escapeHtml(d.profile ?? "");
|
|
6316
6332
|
const links = [
|
|
6317
|
-
href ? `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">Check on suveren.ai \u2197</a>` : ""
|
|
6318
|
-
interactive ? `<a href="${escapeHtml(detailLink(el.id))}" target="_top">Details</a>` : ""
|
|
6333
|
+
href ? `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">Check on suveren.ai \u2197</a>` : ""
|
|
6319
6334
|
].filter(Boolean);
|
|
6320
6335
|
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(actionLabel)}</b>${badge(el.status, "signature valid")}</div><div class="sv-el-row"><span>${escapeHtml(profileLabel)} \xB7 ${escapeHtml(timeLabel)}</span></div>` + (links.length > 0 ? `<div class="sv-el-row"><span>${links.join(" \xB7 ")}</span></div>` : "") + `</div>`;
|
|
6321
6336
|
}
|
|
6322
|
-
function renderApproval(el
|
|
6337
|
+
function renderApproval(el) {
|
|
6323
6338
|
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6324
6339
|
const d = el.data;
|
|
6325
6340
|
const who = typeof d.whoLabel === "string" ? d.whoLabel : Array.isArray(d.who) ? d.who.join(", ") : "unknown";
|
|
6326
6341
|
const asked = typeof d.createdAtLabel === "string" ? d.createdAtLabel : "unknown";
|
|
6327
6342
|
const approved = typeof d.decidedAtLabel === "string" ? d.decidedAtLabel : "unknown";
|
|
6328
6343
|
const waited = typeof d.waitLabel === "string" ? d.waitLabel : "unknown";
|
|
6329
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Approval</b>${badge(el.status, "from ticket archive")}</div><div class="sv-el-row"><span>asked ${escapeHtml(asked)} \xB7 approved ${escapeHtml(approved)} by ${escapeHtml(who)} (${escapeHtml(waited)})</span></div
|
|
6344
|
+
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Approval</b>${badge(el.status, "from ticket archive")}</div><div class="sv-el-row"><span>asked ${escapeHtml(asked)} \xB7 approved ${escapeHtml(approved)} by ${escapeHtml(who)} (${escapeHtml(waited)})</span></div></div>`;
|
|
6330
6345
|
}
|
|
6331
|
-
function renderMandate(el
|
|
6346
|
+
function renderMandate(el) {
|
|
6332
6347
|
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6333
6348
|
const d = el.data;
|
|
6334
6349
|
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : String(d.profile ?? "");
|
|
6335
6350
|
const owners = Array.isArray(d.owners) && d.owners.length > 0 ? d.owners.join(", ") : "unknown owner";
|
|
6336
6351
|
const limits = Array.isArray(d.limits) && d.limits.length > 0 ? d.limits.join(" \xB7 ") : "";
|
|
6337
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(profileLabel)} \xB7 ${escapeHtml(owners)}</b>${badge(el.status, "matches ticket")}</div>` + (limits ? `<div class="sv-el-row">${escapeHtml(limits)}${d.mode ? ` \xB7 ${escapeHtml(d.mode)}` : ""}</div>` : "") + (d.intent ? `<div class="sv-el-intent">Intent: \u201C${escapeHtml(d.intent)}\u201D</div>` : "") +
|
|
6352
|
+
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(profileLabel)} \xB7 ${escapeHtml(owners)}</b>${badge(el.status, "matches ticket")}</div>` + (limits ? `<div class="sv-el-row">${escapeHtml(limits)}${d.mode ? ` \xB7 ${escapeHtml(d.mode)}` : ""}</div>` : "") + (d.intent ? `<div class="sv-el-intent">Intent: \u201C${escapeHtml(d.intent)}\u201D</div>` : "") + `</div>`;
|
|
6338
6353
|
}
|
|
6339
6354
|
var RECORD_FIELD_ORDER = [
|
|
6340
6355
|
"subject",
|
|
@@ -6369,12 +6384,11 @@ function renderRecord(el) {
|
|
|
6369
6384
|
const rows = RECORD_FIELD_ORDER.filter((k) => d[k] !== void 0 && d[k] !== null && d[k] !== "").slice(0, 5).map((k) => renderRecordField(k, d)).join("");
|
|
6370
6385
|
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(kind)} record</b>${badge(el.status, "in simulator database")}</div>` + rows + `</div>`;
|
|
6371
6386
|
}
|
|
6372
|
-
function renderCaseTicketStep(
|
|
6387
|
+
function renderCaseTicketStep(node, isGoal) {
|
|
6373
6388
|
const cls = isGoal ? "sv-step sv-step-goal" : "sv-step";
|
|
6374
6389
|
const kind = isGoal ? "goal" : "ticket";
|
|
6375
6390
|
const inner = `<div class="sv-step-k">${kind}</div><div class="sv-step-t">${escapeHtml(formatActionLabel(node.action))}</div>${formatDateTime(node.time)}`;
|
|
6376
|
-
|
|
6377
|
-
return `<a class="${cls}" href="${escapeHtml(detailLink(caseElementId, node.ticketId))}" target="_top">${inner}</a>`;
|
|
6391
|
+
return `<div class="${cls}">${inner}</div>`;
|
|
6378
6392
|
}
|
|
6379
6393
|
function renderCaseApprovalStep(approval) {
|
|
6380
6394
|
const who = approval.whoLabel ?? (Array.isArray(approval.who) ? approval.who.join(", ") : "unknown");
|
|
@@ -6383,7 +6397,17 @@ function renderCaseApprovalStep(approval) {
|
|
|
6383
6397
|
const waited = approval.waitLabel ?? "unknown";
|
|
6384
6398
|
return `<div class="sv-step sv-step-approval"><div class="sv-step-k">approval</div><div class="sv-step-t">Approval</div><div class="sv-step-sub">asked ${escapeHtml(asked)} \xB7 approved ${escapeHtml(decided)} by ${escapeHtml(who)} (${escapeHtml(waited)})</div></div>`;
|
|
6385
6399
|
}
|
|
6386
|
-
function
|
|
6400
|
+
function renderCaseStartStep(start) {
|
|
6401
|
+
let sub = "";
|
|
6402
|
+
if (start.basis === "loaded" && typeof start.emailTime === "number") {
|
|
6403
|
+
sub = `<div class="sv-step-sub">Email dated ${escapeHtml(formatDateTime(start.emailTime))} \xB7 timed from when the test data was loaded</div>`;
|
|
6404
|
+
} else if (start.basis === "load-unknown") {
|
|
6405
|
+
sub = `<div class="sv-step-sub">Email dated ${escapeHtml(formatDateTime(start.emailTime ?? start.time))} \xB7 start time not verifiable</div>`;
|
|
6406
|
+
}
|
|
6407
|
+
const time = start.basis === "load-unknown" ? "" : escapeHtml(formatDateTime(start.time));
|
|
6408
|
+
return `<div class="sv-step sv-step-start"><div class="sv-step-k">start</div><div class="sv-step-t">Email in</div>${time}${sub}</div>`;
|
|
6409
|
+
}
|
|
6410
|
+
function renderCase(el) {
|
|
6387
6411
|
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6388
6412
|
const d = el.data;
|
|
6389
6413
|
const steps = Array.isArray(d.steps) ? d.steps : [];
|
|
@@ -6395,7 +6419,7 @@ function renderCase(el, interactive) {
|
|
|
6395
6419
|
}
|
|
6396
6420
|
}
|
|
6397
6421
|
const start = d.start;
|
|
6398
|
-
const startStep = start ? { time: start.time, html:
|
|
6422
|
+
const startStep = start ? { time: start.time, html: renderCaseStartStep(start) } : void 0;
|
|
6399
6423
|
const ticketNodes = [
|
|
6400
6424
|
...steps.map((s) => ({ node: s, isGoal: false })),
|
|
6401
6425
|
...goal ? [{ node: goal, isGoal: true }] : []
|
|
@@ -6408,11 +6432,11 @@ function renderCase(el, interactive) {
|
|
|
6408
6432
|
const approvalTime = Math.min(approval.decidedAt ?? approval.createdAt ?? node.time, node.time);
|
|
6409
6433
|
items.push({ time: approvalTime, html: renderCaseApprovalStep(approval) });
|
|
6410
6434
|
}
|
|
6411
|
-
items.push({ time: node.time, html: renderCaseTicketStep(
|
|
6435
|
+
items.push({ time: node.time, html: renderCaseTicketStep(node, isGoal) });
|
|
6412
6436
|
}
|
|
6413
6437
|
items.sort((a, b) => a.time - b.time);
|
|
6414
6438
|
const stepHtml = items.map((i) => i.html).join("");
|
|
6415
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Case ${escapeHtml(d.caseId)} \xB7 ${formatDuration(d.totalDurationSeconds)}</b>${badge(el.status, "assembled by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") +
|
|
6439
|
+
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Case ${escapeHtml(d.caseId)} \xB7 ${typeof d.totalDurationSeconds === "number" ? formatDuration(d.totalDurationSeconds) : "time not verifiable"}</b>${badge(el.status, "assembled by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") + (typeof d.totalDurationSeconds !== "number" ? `<div class="sv-el-warn">${escapeHtml(d.timeUnverifiableReason ?? "Case time not verifiable.")}</div>` : "") + `<div class="sv-tl">${stepHtml}</div></div>`;
|
|
6416
6440
|
}
|
|
6417
6441
|
function renderMetric(el) {
|
|
6418
6442
|
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
@@ -6422,18 +6446,18 @@ function renderMetric(el) {
|
|
|
6422
6446
|
const display = formatMetricValue(kind, d.value);
|
|
6423
6447
|
return `<div class="sv-el sv-el-${el.status} sv-el-metric" data-sv-id="${escapeHtml(el.id)}"><div class="sv-big">${escapeHtml(display)}</div><div class="sv-el-row"><span>${escapeHtml(label)} (${escapeHtml(d.caseCount ?? 0)} case${d.caseCount === 1 ? "" : "s"})</span>${badge(el.status, "computed by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") + `</div>`;
|
|
6424
6448
|
}
|
|
6425
|
-
function renderElement(el
|
|
6449
|
+
function renderElement(el) {
|
|
6426
6450
|
switch (el.kind) {
|
|
6427
6451
|
case "sv-ticket":
|
|
6428
|
-
return renderTicket(el
|
|
6452
|
+
return renderTicket(el);
|
|
6429
6453
|
case "sv-approval":
|
|
6430
|
-
return renderApproval(el
|
|
6454
|
+
return renderApproval(el);
|
|
6431
6455
|
case "sv-mandate":
|
|
6432
|
-
return renderMandate(el
|
|
6456
|
+
return renderMandate(el);
|
|
6433
6457
|
case "sv-record":
|
|
6434
6458
|
return renderRecord(el);
|
|
6435
6459
|
case "sv-case":
|
|
6436
|
-
return renderCase(el
|
|
6460
|
+
return renderCase(el);
|
|
6437
6461
|
case "sv-metric":
|
|
6438
6462
|
return renderMetric(el);
|
|
6439
6463
|
default:
|
|
@@ -6463,14 +6487,24 @@ var DRAWN_ELEMENT_STYLES = `
|
|
|
6463
6487
|
* wrapping, which reads as the text being cut off at the container's visible
|
|
6464
6488
|
* edge (polish 2026-10-05, second pass). */
|
|
6465
6489
|
.sv-step-approval { max-width:240px; white-space:normal; }
|
|
6490
|
+
.sv-step-start { max-width:240px; white-space:normal; }
|
|
6466
6491
|
@media (max-width: 480px) {
|
|
6467
6492
|
.sv-tl { flex-direction:column; overflow-x:visible; }
|
|
6468
6493
|
.sv-step { min-width:0; }
|
|
6469
6494
|
.sv-step-approval { max-width:none; }
|
|
6495
|
+
.sv-step-start { max-width:none; }
|
|
6470
6496
|
.sv-el-row { flex-direction:column; gap:2px; }
|
|
6471
6497
|
}
|
|
6472
6498
|
`;
|
|
6473
|
-
|
|
6499
|
+
var AI_ANALYSIS_LABEL = "AI analysis \u2014 not verified";
|
|
6500
|
+
var AI_LEGEND_TEXT = "Boxes with a green \u2713 are drawn and checked by the gateway, and each one is listed under \u201CChecked values\u201D. Everything else is the AI's own analysis and is not verified.";
|
|
6501
|
+
function aiLegendBanner() {
|
|
6502
|
+
const box = "display:block !important;visibility:visible !important;opacity:1 !important;position:relative !important;transform:none !important;clip-path:none !important;filter:none !important;z-index:2147483647 !important;margin:0 0 12px 0 !important;padding:8px 12px !important;border:1px solid #d4d4d8 !important;border-left:4px solid #a1a1aa !important;border-radius:8px !important;background:#f4f4f5 !important;color:#3f3f46 !important;font:12.5px/1.45 system-ui, sans-serif !important;text-align:left !important;max-width:none !important;width:auto !important;height:auto !important;";
|
|
6503
|
+
const strong = "display:inline !important;visibility:visible !important;font-weight:700 !important;color:#18181b !important;font-size:inherit !important;";
|
|
6504
|
+
return `<div class="sv-ai-legend" role="note" style="${box}"><b style="${strong}">${escapeHtml(AI_ANALYSIS_LABEL)}.</b> ${escapeHtml(AI_LEGEND_TEXT)}</div>`;
|
|
6505
|
+
}
|
|
6506
|
+
function renderReportHtml(rawHtml, elements) {
|
|
6507
|
+
const html = sanitizeReportHtml(rawHtml);
|
|
6474
6508
|
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
6475
6509
|
const spans = findElementSpans(html);
|
|
6476
6510
|
let out = "";
|
|
@@ -6478,11 +6512,11 @@ function renderReportHtml(html, elements, interactive = true) {
|
|
|
6478
6512
|
for (const span of spans) {
|
|
6479
6513
|
out += html.slice(cursor, span.start);
|
|
6480
6514
|
const el = byId.get(span.id) ?? { id: span.id, kind: span.kind, attrs: {}, status: "unverifiable", reason: "No verification result for this element." };
|
|
6481
|
-
out += renderElement(el
|
|
6515
|
+
out += renderElement(el);
|
|
6482
6516
|
cursor = span.end;
|
|
6483
6517
|
}
|
|
6484
6518
|
out += html.slice(cursor);
|
|
6485
|
-
const styleTag = `<style data-sv-drawn-styles="1">${DRAWN_ELEMENT_STYLES}</style
|
|
6519
|
+
const styleTag = `<style data-sv-drawn-styles="1">${DRAWN_ELEMENT_STYLES}</style>${aiLegendBanner()}`;
|
|
6486
6520
|
if (/<body[^>]*>/i.test(out)) {
|
|
6487
6521
|
return out.replace(/<body([^>]*)>/i, (m) => `${m}${styleTag}`);
|
|
6488
6522
|
}
|
|
@@ -6598,7 +6632,7 @@ function buildExportDocument(params) {
|
|
|
6598
6632
|
const exportedLabel = formatDateTimeWithYear(bundle.exportedAt);
|
|
6599
6633
|
const checkedLabel = formatDateTime(bundle.report.checkedAt);
|
|
6600
6634
|
const tzLabel = utcOffsetLabel(bundle.exportedAt);
|
|
6601
|
-
const headerHtml = `<div class="sv-export-header"><p class="sv-export-meta">Suveren Gateway ${escapeHtml(bundle.gatewayVersion)} \u2014 exported ${escapeHtml(exportedLabel)} · checked ${escapeHtml(checkedLabel)} (times in ${escapeHtml(tzLabel)})</p><p class="sv-export-howto">How to check this report: each ticket below links to its public record on suveren.ai ("Check on suveren.ai \u2197"). To verify this entire file offline (including every signature), run <code>suveren-gateway verify-report ${escapeHtml(suggestedFilename(bundle))}</code> from a terminal with the Suveren gateway CLI installed.</p></div>`;
|
|
6635
|
+
const headerHtml = `<div class="sv-export-header"><p class="sv-export-meta">Suveren Gateway ${escapeHtml(bundle.gatewayVersion)} \u2014 exported ${escapeHtml(exportedLabel)} · checked ${escapeHtml(checkedLabel)} (times in ${escapeHtml(tzLabel)})</p><p class="sv-export-howto">How to check this report: each ticket below links to its public record on suveren.ai ("Check on suveren.ai \u2197"). To verify this entire file offline (including every signature), run <code>suveren-gateway verify-report ${escapeHtml(suggestedFilename(bundle))}</code> from a terminal with the Suveren gateway CLI installed.</p><p class="sv-export-legend" role="note" style="display:block !important;visibility:visible !important;opacity:1 !important;margin:6px 0 0 0 !important;color:#3f3f46 !important;font:12.5px/1.45 system-ui, sans-serif !important;"><b style="display:inline !important;visibility:visible !important;color:#18181b !important;">${escapeHtml(AI_ANALYSIS_LABEL)}:</b> ${escapeHtml(AI_LEGEND_TEXT)}</p></div>`;
|
|
6602
6636
|
const proofScriptJson = JSON.stringify(bundle).replace(/<\//g, "<\\/");
|
|
6603
6637
|
const proofScript = `<script type="application/json" id="suveren-proof">${proofScriptJson}</script>`;
|
|
6604
6638
|
let doc = ensureHtmlDocument(renderedHtml);
|
|
@@ -7830,7 +7864,7 @@ app.get("/internal/report/export", internalOnly, async (req, res) => {
|
|
|
7830
7864
|
authorityServer,
|
|
7831
7865
|
gatewayVersion
|
|
7832
7866
|
});
|
|
7833
|
-
const renderedHtml = renderReportHtml(stored.result.html, stored.result.elements
|
|
7867
|
+
const renderedHtml = renderReportHtml(stored.result.html, stored.result.elements);
|
|
7834
7868
|
const document = buildExportDocument({ bundle, renderedHtml });
|
|
7835
7869
|
res.setHeader("Content-Type", "text/html; charset=utf-8").setHeader("Content-Disposition", `attachment; filename="${suggestedFilename(bundle)}"`).setHeader("Cache-Control", "no-store").send(document);
|
|
7836
7870
|
});
|