@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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isExportBundle,
3
3
  verifyExportBundle
4
- } from "./chunk-QOKPECVY.mjs";
4
+ } from "./chunk-ZRRZFGRL.mjs";
5
5
 
6
6
  // bin/report-verify-cli.ts
7
7
  import { readFileSync } from "fs";
@@ -19,6 +19,12 @@ function extractProofBundle(html) {
19
19
  } catch (err) {
20
20
  throw new Error(`Could not parse the embedded proof bundle: ${err instanceof Error ? err.message : String(err)}`);
21
21
  }
22
+ const o = parsed;
23
+ if (o && o.format === "suveren-report-export" && o.version === 1) {
24
+ throw new Error(
25
+ "This file was exported by an older gateway (format version 1): it does not carry what the checker needs to compare the values the page SHOWS with the signed data. Export the report again with a current gateway."
26
+ );
27
+ }
22
28
  if (!isExportBundle(parsed)) {
23
29
  throw new Error('The embedded proof bundle is not in the expected "suveren-report-export" format.');
24
30
  }
@@ -46,8 +52,8 @@ function parseArgs(argv) {
46
52
  var HELP_TEXT = `Usage: suveren-gateway verify-report <file> [--key <hex>] [--online]
47
53
 
48
54
  Verifies an "Export with proof" report file OFFLINE: every ticket signature,
49
- every mandate attestation signature, and every reference the report makes to
50
- a ticket, approval, mandate, or case.
55
+ every mandate attestation signature, every reference the report makes to a
56
+ ticket, approval, mandate, or case \u2014 and every VALUE the page shows.
51
57
 
52
58
  --key <hex> Require the embedded Authority Server key to match this exact
53
59
  value \u2014 a fingerprint you already trust (e.g. read aloud over
@@ -61,6 +67,27 @@ confirm the embedded key against something you trust independently of this
61
67
  file; with neither, the file's internal consistency is checked but the key
62
68
  itself is not.
63
69
 
70
+ What the page shows is checked in two steps:
71
+ 1. The visible page must be exactly what the embedded proof data draws \u2014
72
+ the checker re-draws the whole file and compares it byte for byte. Any
73
+ edit to the page (a number, a time, a style, an added box) fails. Keep
74
+ the file as downloaded: re-saving it from a browser changes it. A file
75
+ drawn by a different gateway version may not re-draw identically; check
76
+ it with that version.
77
+ 2. Every value in every green box is checked against its source:
78
+ checked against signature ticket fields (action, time, profile,
79
+ execution context), mandate limits (via
80
+ the recomputed bounds hash), commitment
81
+ mode, owners, intent (via its hash)
82
+ recomputed wait_s, a case's duration_s, metrics \u2014
83
+ recomputed from the file's own values
84
+ not checkable offline approval times and approver (local
85
+ archive), records and a case's start email
86
+ (connector database), the refusals metric
87
+ Values that cannot be checked offline are listed per box under "Not
88
+ checkable offline" and do NOT fail the check \u2014 but they are never
89
+ reported as signed.
90
+
64
91
  References the report itself shows as "not verifiable" (e.g. the AI named a
65
92
  ticket that does not exist) are listed under "Not verifiable (as shown in the
66
93
  report)" and do NOT fail the check \u2014 the file is honest about them. Every
@@ -69,10 +96,12 @@ ticket (and mandate) in the file, or the check fails.
69
96
 
70
97
  Exit codes:
71
98
  0 every signature is valid, every reference shown as verified is backed,
72
- AND the embedded key was confirmed
99
+ every value checked against a signature or recomputed matches, AND the
100
+ embedded key was confirmed
73
101
  1 something is invalid \u2014 a bad signature, a reference shown as verified
74
- with no valid ticket behind it, or a key that does not match
75
- --key/--online
102
+ with no valid ticket behind it, a value on the page that differs from
103
+ the signed data or does not recompute, an edited page, or a key that
104
+ does not match --key/--online
76
105
  2 everything else is valid, but the embedded key was NOT checked (no --key
77
106
  or --online given) \u2014 confirm it yourself before trusting this file
78
107
  `;
@@ -143,7 +172,8 @@ function printSummary(bundle, result) {
143
172
  console.log(` - ${e.elementId}${e.ticketIds.length ? ` (${e.ticketIds.join(", ")})` : ""}${note}`);
144
173
  }
145
174
  }
146
- const invalidAuth = result.authorizations.filter((a) => !a.attestationValid || a.boundsHashMatches === false);
175
+ printBoxes(result);
176
+ const invalidAuth = result.authorizations.filter((a) => !a.attestationValid || a.boundsHashMatches === false || a.boundsValuesProven === false);
147
177
  if (invalidAuth.length > 0) {
148
178
  console.log(` Mandates: ${result.authorizations.length - invalidAuth.length}/${result.authorizations.length} valid \u2014 INVALID:`);
149
179
  for (const a of invalidAuth) console.log(` - ${a.authorizationId}: ${a.error ?? "invalid"}`);
@@ -159,6 +189,33 @@ function printSummary(bundle, result) {
159
189
  console.log(` Key not confirmed \u2014 compare this fingerprint with ${bundle.authorityServer.url}/api/as/pubkey or pass --online.`);
160
190
  }
161
191
  }
192
+ function printBoxes(result) {
193
+ if (result.document.state === "match") console.log(" Page: exactly what the signed data draws.");
194
+ else if (result.document.state === "mismatch") console.log(` INVALID \u2014 page: ${result.document.error}`);
195
+ for (const e of result.drawnErrors) console.log(` INVALID \u2014 ${e}`);
196
+ const bad = result.boxes.filter((b) => b.mismatches.length > 0);
197
+ const ok = result.boxes.filter((b) => b.mismatches.length === 0);
198
+ const fully = ok.filter((b) => b.notCheckable.length === 0);
199
+ console.log(` Boxes: ${fully.length} fully checked \xB7 ${ok.length - fully.length} partly not checkable offline \xB7 ${bad.length} MISMATCH.`);
200
+ for (const b of ok) {
201
+ const parts = [];
202
+ if (b.signed.length > 0) parts.push(`checked against signature: ${[...new Set(b.signed)].join(", ")}`);
203
+ if (b.recomputed.length > 0) parts.push(`recomputed: ${b.recomputed.map((r) => `${r.field} (from ${r.inputs.join(" + ")})`).join(", ")}`);
204
+ if (parts.length > 0) console.log(` - ${b.elementId}: ${parts.join(" \xB7 ")}`);
205
+ }
206
+ if (bad.length > 0) {
207
+ console.log(` INVALID \u2014 ${bad.length} box(es) show a value that differs from its source:`);
208
+ for (const b of bad) for (const m of b.mismatches) console.log(` - ${b.elementId}: ${m}`);
209
+ }
210
+ const unbackable = result.boxes.filter((b) => b.notCheckable.length > 0);
211
+ if (unbackable.length > 0) {
212
+ console.log(" Not checkable offline (shown in the report, not backed by a signature \u2014 does not fail the check):");
213
+ for (const b of unbackable) {
214
+ console.log(` - ${b.elementId}: ${b.notCheckable.map((n) => `${n.field} (${n.source})`).join(", ")}`);
215
+ }
216
+ }
217
+ console.log(" Coverage panel: from the connector database \u2014 not checkable offline.");
218
+ }
162
219
  if (import.meta.url === `file://${process.argv[1]}`) {
163
220
  runVerifyReportCli(process.argv.slice(2)).then((code) => {
164
221
  process.exitCode = code;