@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.
@@ -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 verifyAttestationSignature2, decodeAttestationBlob as decodeAttestationBlob2 } from "@hap/core";
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 approvers = Object.values(committedBy);
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: who.length > 0 ? who.join(", ") : "unknown",
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 = decodeAttestationBlob(firstBlob);
311
- await verifyAttestationSignature(attestation, check.entry.asPublicKey);
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 : formatOwnerLabel(did);
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 startTime = parseTimestampSeconds(startMessage.received_at);
388
- if (startTime === void 0) {
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 = decodeAttestationBlob2(att.blob);
527
- await verifyAttestationSignature2(attestation, publicKeyHex);
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
@@ -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-JUFH4SKS.mjs";
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. Numbers you compute yourself are shown as "AI analysis \u2014 not verified".
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
- if (name.toLowerCase().startsWith("on")) continue;
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
- return `Case ${String(d.caseId)}: ${steps} ${steps === 1 ? "step" : "steps"}, ${formatDuration(d.totalDurationSeconds)}`;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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, interactive) {
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, interactive) {
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>` + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id, String(d.ticketId ?? "")))}" target="_top">Details</a></div>` : "") + `</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, interactive) {
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>` : "") + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id))}" target="_top">Details</a></div>` : "") + `</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(caseElementId, node, isGoal, interactive) {
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
- if (!interactive) return `<div class="${cls}">${inner}</div>`;
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 renderCase(el, interactive) {
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: `<div class="sv-step"><div class="sv-step-k">start</div><div class="sv-step-t">Email in</div>${formatDateTime(start.time)}</div>` } : void 0;
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(el.id, node, isGoal, interactive) });
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>` : "") + `<div class="sv-tl">${stepHtml}</div>` + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id))}" target="_top">Case details</a></div>` : "") + `</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, interactive) {
6449
+ function renderElement(el) {
6426
6450
  switch (el.kind) {
6427
6451
  case "sv-ticket":
6428
- return renderTicket(el, interactive);
6452
+ return renderTicket(el);
6429
6453
  case "sv-approval":
6430
- return renderApproval(el, interactive);
6454
+ return renderApproval(el);
6431
6455
  case "sv-mandate":
6432
- return renderMandate(el, interactive);
6456
+ return renderMandate(el);
6433
6457
  case "sv-record":
6434
6458
  return renderRecord(el);
6435
6459
  case "sv-case":
6436
- return renderCase(el, interactive);
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
- function renderReportHtml(html, elements, interactive = true) {
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, interactive);
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)} &middot; 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)} &middot; 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, false);
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
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isExportBundle,
3
3
  verifyExportBundle
4
- } from "./chunk-JUFH4SKS.mjs";
4
+ } from "./chunk-QOKPECVY.mjs";
5
5
 
6
6
  // bin/report-verify-cli.ts
7
7
  import { readFileSync } from "fs";