@sanning/proof 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +30 -29
  2. package/dist/agent-record.d.ts +62 -0
  3. package/dist/agent-record.js +527 -0
  4. package/dist/agent-schemas.generated.d.ts +15 -0
  5. package/dist/agent-schemas.generated.js +5759 -0
  6. package/dist/case-file.d.ts +32 -89
  7. package/dist/case-file.js +41 -93
  8. package/dist/case.d.ts +339 -822
  9. package/dist/case.js +667 -1160
  10. package/dist/chain.d.ts +19 -47
  11. package/dist/chain.js +78 -205
  12. package/dist/claims.d.ts +211 -0
  13. package/dist/claims.js +1081 -0
  14. package/dist/cli.d.ts +45 -68
  15. package/dist/cli.js +618 -1022
  16. package/dist/compose.js +23 -65
  17. package/dist/crypto.d.ts +7 -8
  18. package/dist/crypto.js +60 -147
  19. package/dist/declaration.d.ts +68 -194
  20. package/dist/declaration.js +175 -518
  21. package/dist/environment.d.ts +9 -30
  22. package/dist/environment.js +17 -66
  23. package/dist/evidence.d.ts +119 -160
  24. package/dist/evidence.js +744 -1103
  25. package/dist/exit-codes.d.ts +11 -27
  26. package/dist/exit-codes.js +24 -59
  27. package/dist/file-match.d.ts +33 -79
  28. package/dist/file-match.js +33 -69
  29. package/dist/grading.d.ts +98 -135
  30. package/dist/grading.js +224 -380
  31. package/dist/handoff.d.ts +62 -160
  32. package/dist/handoff.js +153 -393
  33. package/dist/index.d.ts +14 -10
  34. package/dist/index.js +60 -167
  35. package/dist/json-schema.d.ts +29 -0
  36. package/dist/json-schema.js +298 -0
  37. package/dist/merkle.js +16 -28
  38. package/dist/message-parts.d.ts +56 -212
  39. package/dist/message-parts.js +142 -502
  40. package/dist/side-content.d.ts +11 -22
  41. package/dist/side-content.js +22 -65
  42. package/dist/types.d.ts +17 -0
  43. package/dist/types.js +19 -5
  44. package/dist/verifier.d.ts +40 -114
  45. package/dist/verifier.js +196 -535
  46. package/dist/witness-cache.d.ts +24 -45
  47. package/dist/witness-cache.js +38 -81
  48. package/dist/witness-lookup.d.ts +36 -57
  49. package/dist/witness-lookup.js +89 -139
  50. package/dist/zip-reader.d.ts +18 -33
  51. package/dist/zip-reader.js +19 -34
  52. package/package.json +11 -3
  53. package/src/agent-record.ts +608 -0
  54. package/src/agent-schemas.generated.ts +5764 -0
  55. package/src/case-file.ts +45 -109
  56. package/src/case.ts +814 -1328
  57. package/src/chain.ts +85 -224
  58. package/src/claims.ts +1253 -0
  59. package/src/cli.ts +714 -1176
  60. package/src/compose.ts +33 -79
  61. package/src/crypto.ts +60 -147
  62. package/src/declaration.ts +219 -656
  63. package/src/environment.ts +17 -65
  64. package/src/evidence.ts +958 -1381
  65. package/src/exit-codes.ts +24 -63
  66. package/src/file-match.ts +40 -89
  67. package/src/grading.ts +287 -421
  68. package/src/handoff.ts +175 -427
  69. package/src/index.ts +131 -177
  70. package/src/json-schema.ts +295 -0
  71. package/src/merkle.ts +16 -28
  72. package/src/message-parts.ts +155 -533
  73. package/src/side-content.ts +25 -68
  74. package/src/types.ts +55 -36
  75. package/src/verifier.ts +202 -526
  76. package/src/witness-cache.ts +45 -94
  77. package/src/witness-lookup.ts +107 -177
  78. package/src/zip-reader.ts +20 -38
  79. package/vectors/README.md +52 -34
  80. package/vectors/agent-record.json +2175 -0
  81. package/vectors/case-file-index.json +1 -1
  82. package/vectors/case-split.json +2 -2
  83. package/vectors/chain-reading.json +3 -3
  84. package/vectors/file-match.json +285 -148
  85. package/vectors/jcs-reader.json +6 -6
  86. package/vectors/pack-grading.json +102 -25
  87. package/vectors/sandbox-evidence.json +573 -581
  88. package/vectors/stamp-on-chain.json +18 -18
  89. package/vectors/witness-cache.json +49 -49
  90. package/dist/agent-proof.d.ts +0 -28
  91. package/dist/agent-proof.js +0 -226
  92. package/dist/run-adapters.d.ts +0 -10
  93. package/dist/run-adapters.js +0 -35
  94. package/dist/run.d.ts +0 -209
  95. package/dist/run.js +0 -1405
  96. package/src/agent-proof.ts +0 -264
  97. package/src/run-adapters.ts +0 -44
  98. package/src/run.ts +0 -1598
@@ -1,26 +1,14 @@
1
1
  // The public-record (witness) lookup the CLI runs with `--gateways`
2
- // (evidence-bundle.md §5.5.6, v1.20, A24, D7): re-fetch each pack's witness on
3
- // the public record and report what it said. Moved here from `cli.ts`
4
- // (proof#200) so the CLI and a consumer that is not the CLI, the verify page,
5
- // a browser app, call the SAME implementation rather than each keeping its
6
- // own copy that can drift.
2
+ // (evidence-bundle.md §5.5.6): re-fetch each pack's witness on the public record
3
+ // and report what it said. The CLI and non-CLI callers (the verify page, a
4
+ // browser app) share this one implementation.
7
5
  //
8
- // Pure by the kernel's own rule (specs/architecture.md §3): the network call
9
- // is the one exception this module exists to hold, and it never runs unless a
10
- // caller hands in a gateway to ask. `options.fetchImpl` is the seam: where a
11
- // caller supplies one (a browser page's own `fetch`, or a test's fake), that
12
- // is the ONE this module calls; where none is given, it falls back to the
13
- // ambient global `fetch`, the same fallback `cli.ts` already applied at its
14
- // own call site before this module existed.
15
- //
16
- // `onChainHalf` and `witnessLine` are the CLI's own building blocks,
17
- // UNCHANGED (proof#200 is additive: the CLI's `case` output and exit codes are
18
- // byte-for-byte what they were). `lookupPublicRecord` is the new export:
19
- // one pack (or several) in, one four-state result out, with a pack's
20
- // `witness-cache.json` read as part of the pack rather than as a side file a
21
- // caller must remember to also pass in.
6
+ // The network call is the one exception to the kernel's purity (specs/architecture.md §3).
7
+ // It never runs unless a caller hands in a gateway, and `options.fetchImpl`
8
+ // is the seam: the caller's `fetch` if given, else the ambient global.
22
9
 
23
10
  import { SIMULATED_WITNESS_KIND, verifyEvidenceBundle } from "./evidence.js";
11
+ import type { EvidenceBundleResult } from "./evidence.js";
24
12
  import type { WitnessLine, WitnessState } from "./case.js";
25
13
  import { WITNESS_NOT_LOOKED_UP, renderWitnessLine } from "./case.js";
26
14
  import {
@@ -32,7 +20,7 @@ import {
32
20
  renderWitnessTime,
33
21
  } from "./witness-cache.js";
34
22
 
35
- /** The seam every network call in this module goes through: used where given, else the ambient global `fetch`. */
23
+ /** The seam every network call goes through: the caller's `fetch` if given, else the ambient global. */
36
24
  export interface WitnessLookupOptions {
37
25
  fetchImpl?: typeof fetch;
38
26
  }
@@ -44,13 +32,9 @@ export interface OnChainRow {
44
32
  mismatched: number;
45
33
  unreachable: number;
46
34
  not_checked: number;
47
- /**
48
- * How many of the `not_checked` witnesses are simulated: a sandbox plane's
49
- * stand-in for a chain, never looked up. Present only where there
50
- * is one, so a production pack's row is unchanged.
51
- */
35
+ /** How many of the `not_checked` witnesses are simulated (a sandbox plane's stand-in for a chain, never looked up). Present only where there is one. */
52
36
  simulated?: number;
53
- /** Present only where the pack caches a witness time (5.6.6, v1.25, A30). */
37
+ /** Present only where the pack caches a witness time (5.6.6). */
54
38
  witness_times?: WitnessTimeRow[];
55
39
  }
56
40
 
@@ -86,29 +70,68 @@ export interface OnChainHalf {
86
70
  checked: boolean;
87
71
  gateways: string[];
88
72
  packs: OnChainRow[];
89
- verdict: "not_checked" | "confirmed" | "failed" | "undetermined";
73
+ verdict: "not_checked" | "confirmed" | "differs" | "not_settled";
74
+ }
75
+
76
+ /** One pack's witnesses as a lookup left them: how many, and each one's rollup. */
77
+ export function onChainRow(
78
+ ref: string,
79
+ result: EvidenceBundleResult,
80
+ ): { row: OnChainRow; answered: Set<string> } {
81
+ const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
82
+ const row: OnChainRow = {
83
+ ref,
84
+ witnesses: witnesses.length,
85
+ confirmed: 0,
86
+ mismatched: 0,
87
+ unreachable: 0,
88
+ not_checked: 0,
89
+ };
90
+ for (const w of witnesses) {
91
+ const rollup = w.onChain?.rollup ?? null;
92
+ if (rollup === "confirm") row.confirmed++;
93
+ else if (rollup === "mismatch") row.mismatched++;
94
+ else if (rollup === "unreachable") row.unreachable++;
95
+ else row.not_checked++;
96
+ }
97
+ const simulated = witnesses.filter((w) => w.anchorRef?.kind === SIMULATED_WITNESS_KIND).length;
98
+ if (simulated > 0) row.simulated = simulated;
99
+ const answered = new Set<string>();
100
+ for (const w of witnesses) {
101
+ for (const outcome of w.onChain?.perGateway ?? []) {
102
+ if (outcome.outcome === "confirm" || outcome.outcome === "mismatch") {
103
+ answered.add(outcome.gateway);
104
+ }
105
+ }
106
+ }
107
+ return { row, answered };
90
108
  }
91
109
 
92
110
  /**
93
- * Each pack's witnesses, re-fetched through `verifyEvidenceBundle`. Outside
94
- * the case reading on purpose: `readCase` makes no network call, and neither
95
- * does the verify page's case reading, so the case verdict stays the one both
96
- * surfaces compute. This adds the half the reading names as not checked.
97
- *
98
- * Moved from `cli.ts` unchanged, apart from taking `fetchImpl` directly
99
- * instead of a `Cli` stream (`io.fetchImpl ?? fetch`, the same fallback the
100
- * CLI already applied at its own call site).
111
+ * `differs` where a lookup found other bytes or another time, `not_settled` where one
112
+ * did not settle, `confirmed` otherwise. Only `differs` moves a verdict.
113
+ */
114
+ export function onChainOutcome(rows: readonly OnChainRow[]): "differs" | "not_settled" | "confirmed" {
115
+ if (
116
+ rows.some((r) => r.mismatched > 0 || (r.witness_times ?? []).some((t) => t.outcome === "differs"))
117
+ ) {
118
+ return "differs";
119
+ }
120
+ if (rows.some((r) => r.witnesses === 0 || r.unreachable > 0 || r.not_checked > 0)) {
121
+ return "not_settled";
122
+ }
123
+ return "confirmed";
124
+ }
125
+
126
+ /**
127
+ * Each pack's witnesses, re-fetched through `verifyEvidenceBundle`. Outside the
128
+ * case reading on purpose: `readCase` makes no network call, so the case verdict
129
+ * is the same on every surface. This adds the half the reading names as not checked.
101
130
  */
102
131
  export async function onChainHalf(
103
132
  packs: readonly (readonly [string, unknown, readonly CachedWitnessTime[]])[],
104
133
  gateways: readonly string[],
105
134
  options: WitnessLookupOptions = {},
106
- /**
107
- * Where given, receives for each pack the `stamp_ref` of every stamp whose
108
- * witness this lookup confirmed: the run reading evaluates an enclave token
109
- * at a stamp's interval end only there.
110
- */
111
- witnessed?: string[][],
112
135
  ): Promise<OnChainHalf> {
113
136
  if (gateways.length === 0) return { checked: false, gateways: [], packs: [], verdict: "not_checked" };
114
137
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -116,53 +139,19 @@ export async function onChainHalf(
116
139
  const answered = new Set<string>();
117
140
  for (const [ref, bundle, cached] of packs) {
118
141
  const result = await verifyEvidenceBundle(bundle, { gateways: [...gateways], fetchImpl });
119
- const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
120
- const row: OnChainRow = {
121
- ref,
122
- witnesses: witnesses.length,
123
- confirmed: 0,
124
- mismatched: 0,
125
- unreachable: 0,
126
- not_checked: 0,
127
- };
128
- witnessed?.push(
129
- (result.stamps ?? []).filter((st) => st.ok && st.onChain?.rollup === "confirm").map((st) => st.stampRef),
130
- );
131
- for (const w of witnesses) {
132
- const rollup = w.onChain?.rollup ?? null;
133
- if (rollup === "confirm") row.confirmed++;
134
- else if (rollup === "mismatch") row.mismatched++;
135
- else if (rollup === "unreachable") row.unreachable++;
136
- else row.not_checked++;
137
- }
138
- const simulated = witnesses.filter((w) => w.anchorRef?.kind === SIMULATED_WITNESS_KIND).length;
139
- if (simulated > 0) row.simulated = simulated;
140
- // The witness times the pack's witness-cache.json gives, looked up where
141
- // it gives any (5.6.6, v1.25, A30). A pack with no cache gets no key and
142
- // no request.
142
+ const { row, answered: heard } = onChainRow(ref, result);
143
+ for (const g of heard) answered.add(g);
144
+ // The witness times the pack's witness-cache.json gives (5.6.6). A pack with no cache gets no key and no request.
143
145
  const times = await checkWitnessCache(cached, gateways, fetchImpl);
144
146
  if (times.length > 0) row.witness_times = times.map(witnessTimeRow);
145
147
  rows.push(row);
146
- for (const w of witnesses) {
147
- for (const outcome of w.onChain?.perGateway ?? []) {
148
- if (outcome.outcome === "confirm" || outcome.outcome === "mismatch") {
149
- answered.add(outcome.gateway);
150
- }
151
- }
152
- }
153
148
  for (const t of times) {
154
149
  for (const a of t.answers) {
155
150
  if (a.outcome === "matches" || a.outcome === "differs") answered.add(a.gateway);
156
151
  }
157
152
  }
158
153
  }
159
- const verdict = rows.some(
160
- (r) => r.mismatched > 0 || (r.witness_times ?? []).some((t) => t.outcome === "differs"),
161
- )
162
- ? "failed"
163
- : rows.some((r) => r.witnesses === 0 || r.unreachable > 0 || r.not_checked > 0)
164
- ? "undetermined"
165
- : "confirmed";
154
+ const verdict = onChainOutcome(rows);
166
155
  return {
167
156
  answered: gateways.filter((g) => answered.has(g)),
168
157
  checked: true,
@@ -179,8 +168,7 @@ function distinctTimes(times: Iterable<[number, number]>): [number, number][] {
179
168
  return [...seen.values()].sort((a, b) => a[1] - b[1] || a[0] - b[0]);
180
169
  }
181
170
 
182
- // Only the keys that carry something: a line with no cached or recorded time
183
- // is the line 0.13.0 printed, field for field.
171
+ // Only the keys that carry something.
184
172
  function withTimes(
185
173
  line: WitnessLine,
186
174
  cached: [number, number][],
@@ -194,15 +182,11 @@ function withTimes(
194
182
  }
195
183
 
196
184
  /**
197
- * `cached` is every witness time the packs cache (5.6.6, v1.25, A30). Where no
198
- * lookup ran, each is printed labelled not checked. Where one ran, each time
199
- * the public record matched is printed as the record's, each time no lookup
200
- * settled stays labelled not checked, and a time the record contradicts is the
201
- * reason the line reads `differs`.
202
- *
203
- * Moved from `cli.ts` unchanged: same inputs, same three states
204
- * (`WitnessState`), same rendering. The CLI's `case` command still calls this
205
- * exact function, so its witness line is unchanged.
185
+ * `cached` is every witness time the packs cache (5.6.6). Where no lookup ran,
186
+ * each is printed labelled not checked. Where one ran, each time the public
187
+ * record matched is printed as the record's, each time no lookup settled stays
188
+ * labelled not checked, and a time the record contradicts makes the line read
189
+ * `differs`.
206
190
  */
207
191
  export function witnessLine(onChain: OnChainHalf, cached: readonly CachedWitnessTime[] = []): WitnessLine {
208
192
  if (!onChain.checked) {
@@ -239,7 +223,7 @@ export function witnessLine(onChain: OnChainHalf, cached: readonly CachedWitness
239
223
  `, and the public record dates it ${renderWitnessTime(a.block_height as number, a.block_time as number)}`;
240
224
  if (!contradicted.includes(reason)) contradicted.push(reason);
241
225
  }
242
- if (onChain.verdict === "failed") {
226
+ if (onChain.verdict === "differs") {
243
227
  return withTimes({ state: "differs", reasons: contradicted, services }, unsettled, recorded);
244
228
  }
245
229
  if (onChain.verdict === "confirmed") {
@@ -258,39 +242,20 @@ export function witnessLine(onChain: OnChainHalf, cached: readonly CachedWitness
258
242
  return withTimes({ state: "not_checked", reasons, services }, unsettled, recorded);
259
243
  }
260
244
 
261
- // ─── The public-entry-point export (proof#200) ─────────────────────────────
262
- //
263
- // `onChainHalf` and `witnessLine` above are the CLI's own two-step shape:
264
- // gateways in, an `OnChainHalf` (every row, every gateway) out, then a
265
- // `WitnessLine` (three states: `confirmed`, `not_checked`, `differs`) rendered
266
- // from it. A second caller, the verify page, does not want the CLI's
267
- // internal row shape and does want a fourth state the CLI's three-state
268
- // `WitnessLine` cannot name: a pack that carries NO witness at all yet is not
269
- // "not checked" (a lookup that could have found something and didn't), it is
270
- // "there is nothing on the public record to check". Folding that pack into
271
- // `not_checked` is not wrong for the CLI (`case` already says so in the
272
- // reason text: "a pack names no witness"), but a tag that only ever reads
273
- // "Not checked" hides the one distinction an auditor most wants: did we look
274
- // and find nothing, or is there nothing to look for yet.
245
+ // ─── The public-entry-point export ─────────────────────────────
275
246
  //
276
- // `lookupPublicRecord` is additive over the two functions above: it calls
277
- // them, never reimplements them, so there is exactly one place the network
278
- // call and the verdict arithmetic live. It never throws: the underlying
279
- // primitives (`verifyEvidenceBundle`, `checkWitnessCache`) already treat a
280
- // gateway's answer as untrusted input and report `unreachable`/`differs`
281
- // rather than raise, and this function adds no new path to the network or the
282
- // parser.
247
+ // `lookupPublicRecord` adds a fourth state the CLI's three-state `WitnessLine`
248
+ // cannot name: a pack that names no witness at all is not "not checked" (a
249
+ // lookup that found nothing), it is "nothing on the public record to check". It
250
+ // calls `onChainHalf` and `witnessLine` rather than reimplementing them, so the
251
+ // network call and the verdict arithmetic live in one place.
283
252
 
284
- /** The four states this export's result can carry. The CLI's own `WitnessState` plus `no_public_record`. */
253
+ /** The states a lookup can carry: the CLI's `WitnessState` plus `no_public_record`. */
285
254
  export type PublicRecordState = WitnessState | "no_public_record";
286
255
 
287
256
  /**
288
- * One pack, as this lookup needs it: the parsed `bundle.json` and, where the
289
- * pack carries one, the RAW BYTES of its `witness-cache.json`, read as part
290
- * of the pack, per proof#200's acceptance criteria, rather than as a
291
- * separately-loaded side file. `null` and `undefined` both mean "this pack has
292
- * no witness-cache.json", the same reading `readWitnessCache` gives a missing
293
- * file.
257
+ * One pack: the parsed `bundle.json` and, where it has one, the raw bytes of its
258
+ * `witness-cache.json`. `null` and `undefined` both mean no cache file.
294
259
  */
295
260
  export interface PublicRecordPack {
296
261
  /** How this pack is named in the result; carries no meaning beyond that. */
@@ -303,36 +268,27 @@ export interface PublicRecordResult {
303
268
  state: PublicRecordState;
304
269
  reasons: string[];
305
270
  services: string[];
306
- /** Witness times a pack caches that no lookup settled (5.6.6, v1.25, A30). */
271
+ /** Witness times a pack caches that no lookup settled (5.6.6). */
307
272
  cached?: [number, number][];
308
273
  /** Witness times the public record returned that matched a pack's cache. */
309
274
  recorded?: [number, number][];
310
- /** The line as both the CLI and a page print it, one string, kernel words. */
275
+ /** The line as the CLI and a page print it. */
311
276
  rendered: string;
312
277
  }
313
278
 
314
279
  /**
315
- * The public-record lookup the CLI runs with `--gateways`, exported so a
316
- * consumer that is not the CLI can run the SAME lookup rather than copy it
317
- * (proof#200; the copy this closes is verify-page#70's `src/lib/verify.ts`).
280
+ * The public-record lookup the CLI runs with `--gateways`, for non-CLI callers.
318
281
  *
319
- * `packs` is every pack in the drop being read; `gateways` is empty for an
320
- * offline reading, which never makes a network call and always answers
321
- * `not_checked` or `no_public_record`. `options.fetchImpl` is the one seam a
322
- * network call goes through, a browser page supplies its own `fetchImpl`;
323
- * with none given this falls back to the ambient global `fetch`, so a
324
- * browser page and a test can each supply their own and Node's `fetch` is
325
- * never assumed where a caller passed something else.
282
+ * `packs` is every pack in the drop; `gateways` is empty for an offline reading,
283
+ * which makes no network call and answers `not_checked` or `no_public_record`.
326
284
  *
327
- * `state` is `confirmed` only about the BYTES: the public record holds what a
328
- * pack names. It says nothing about whether the pack itself verifies (its
329
- * wrapper signature, its `body_hash`), and a caller must never show
330
- * `confirmed` apart from the pack's own verdict (proof#201).
285
+ * `state` is `confirmed` only about the bytes: the public record holds what a
286
+ * pack names. It says nothing about whether the pack itself verifies, so a
287
+ * caller must never show `confirmed` apart from the pack's own verdict.
331
288
  *
332
- * Never throws. Every input, including a malformed pack, a non-string
333
- * gateway, or a `fetchImpl` that itself misbehaves, is read as untrusted, and
334
- * anything this function or what it calls cannot make sense of settles as
335
- * `not_checked` rather than escape as an exception.
289
+ * Never throws. Every input (a malformed pack, a non-string gateway, a
290
+ * misbehaving `fetchImpl`) is untrusted, and anything it cannot make sense of
291
+ * settles as `not_checked`.
336
292
  */
337
293
  export async function lookupPublicRecord(
338
294
  packs: readonly PublicRecordPack[],
@@ -340,10 +296,7 @@ export async function lookupPublicRecord(
340
296
  options: WitnessLookupOptions = {},
341
297
  ): Promise<PublicRecordResult> {
342
298
  try {
343
- // Nothing to look up: zero packs means the CALLER handed in nothing to
344
- // check, which is not the same claim as "the packs it handed in name no
345
- // witness" (`no_public_record`, below). Checked first, and unconditionally,
346
- // so a caller than mishandles an empty drop never reads it as confirmed.
299
+ // Zero packs is the caller handing in nothing, not packs that name no witness (`no_public_record`). Checked first so an empty drop never reads as confirmed.
347
300
  if (packs.length === 0) {
348
301
  const reasons = [...WITNESS_NOT_LOOKED_UP.reasons];
349
302
  return {
@@ -363,23 +316,12 @@ export async function lookupPublicRecord(
363
316
  const onChain = await onChainHalfForWitnessCount(rows, gateways, options);
364
317
  const line = witnessLine(onChain.half, caches.flat());
365
318
 
366
- // "No public record yet": every pack this lookup was handed names ZERO
367
- // witnesses, so there is nothing the public record could confirm or
368
- // contradict. This holds independently of whether a lookup ran, an
369
- // offline reading of a fresh, unstamped pack is exactly as much "nothing to
370
- // check yet" as an online one, and it is computed from the SAME witness
371
- // count `onChainHalf` derives from `verifyEvidenceBundle`'s own parse, never
372
- // a second reading of the bundle.
373
- //
374
- // A pack that does NOT verify (a broken wrapper signature, `bundle: null`,
375
- // garbage, an unrecognised body type) also names zero witnesses, by
376
- // construction: `verifyEvidenceBundle` never reports a checkpoint or a
377
- // stamp for a bundle it could not authenticate. That is not "nothing to
378
- // check yet", it is a pack this lookup could not read, so `no_public_record`
379
- // is reserved for a pack whose wrapper genuinely verifies
380
- // (`signatureOk && bodyHashOk && status !== "failed"`) and that carries no
381
- // cached witness time either (a cache entry names something to check).
382
- // Anything else that would otherwise qualify reads `not_checked` instead.
319
+ // `no_public_record`: every pack names zero witnesses, so nothing could be
320
+ // confirmed or contradicted. Reserved for packs whose wrapper verifies
321
+ // (`signatureOk && bodyHashOk && status !== "failed"`) and that carry no
322
+ // cached witness time: a pack that fails to verify also names zero
323
+ // witnesses, and that is a pack this lookup could not read, so it reads
324
+ // `not_checked`.
383
325
  const noWitnesses = onChain.witnessCounts.every((n) => n === 0);
384
326
  const everyPackVerifies = onChain.verifies.every((v) => v);
385
327
  const noCacheRead = caches.every((c) => c.length === 0);
@@ -392,18 +334,12 @@ export async function lookupPublicRecord(
392
334
  reasons: line.reasons,
393
335
  services: line.services,
394
336
  ...(line.cached ? { cached: line.cached } : {}),
395
- // `recorded` is a claim about the public record confirming a cached
396
- // time; it never rides alongside anything but a `confirmed` verdict, so
397
- // a pack whose wrapper does not verify can never surface a cached time
398
- // as though the record had confirmed it.
337
+ // `recorded` rides only with a `confirmed` verdict, so a pack whose wrapper does not verify never surfaces a cached time as record-confirmed.
399
338
  ...(state === "confirmed" && line.recorded ? { recorded: line.recorded } : {}),
400
339
  rendered: renderWitnessLine({ ...line, state: state === "no_public_record" ? "not_checked" : state }),
401
340
  };
402
341
  } catch (e) {
403
- // Untrusted input all the way down: a gateway's answer, a pack's shape, a
404
- // caller's `fetchImpl`. Anything this function or what it calls cannot
405
- // make sense of settles `not_checked` rather than throw out of an export a
406
- // browser page calls directly.
342
+ // Untrusted input all the way down (a gateway's answer, a pack's shape, a `fetchImpl`): settle `not_checked` rather than throw out of an export a page calls directly.
407
343
  const reason =
408
344
  e instanceof Error ? `the lookup did not settle: ${e.message}` : "the lookup did not settle";
409
345
  const reasons = [reason];
@@ -416,15 +352,9 @@ export async function lookupPublicRecord(
416
352
  }
417
353
  }
418
354
 
419
- // `onChainHalf` short-circuits to `{ checked: false, packs: [] }` with no
420
- // gateways, which is right for the CLI (nothing to print per pack) but leaves
421
- // `lookupPublicRecord` unable to tell "no gateways" from "no witnesses" for an
422
- // OFFLINE reading. This wrapper reads each pack's witness count, and whether
423
- // its wrapper verifies, the same way `onChainHalf` does, via
424
- // `verifyEvidenceBundle`: an OFFLINE parse (`gateways: []`), so it costs no
425
- // extra request whether or not a lookup runs, and `verifies` is never itself
426
- // a statement about the public record, only about the pack's own signature
427
- // and `body_hash`.
355
+ // Reads each pack's witness count and whether its wrapper verifies, via an
356
+ // offline `verifyEvidenceBundle` parse (`gateways: []`), so it costs no request.
357
+ // `verifies` is about the pack's own signature and `body_hash`, never the public record.
428
358
  async function onChainHalfForWitnessCount(
429
359
  packs: readonly (readonly [string, unknown, readonly CachedWitnessTime[]])[],
430
360
  gateways: readonly string[],
package/src/zip-reader.ts CHANGED
@@ -1,42 +1,34 @@
1
1
  /**
2
2
  * The one ZIP reader `case` uses, for a case file named on the command line.
3
3
  *
4
- * The TypeScript half of `src/sanning_proof/zip_reader.py`. Node has no ZIP
5
- * reader, and Python's `zipfile` is not used, so the two kernels read one
6
- * archive by one rule rather than by two libraries that disagree on the edges:
7
- * a central directory that contradicts a local header, a name that is not
8
- * UTF-8, data after the end of a deflate stream. Where either library would
9
- * have made a choice, this module refuses instead.
4
+ * Node has no ZIP reader. This one refuses where two libraries would disagree
5
+ * (evidence-bundle.md 5.6.1). It reads the central directory and each entry
6
+ * through its local header, stored and deflate only.
10
7
  *
11
- * What it reads (evidence-bundle.md 5.6.1): the central directory, and each
12
- * entry's data through its local header. Stored and deflate only. What it
13
- * REFUSES, as a whole archive: no end-of-central-directory record, ZIP64, more
14
- * than one disk, an entry the central directory places outside the archive, a
15
- * local header that does not match its central entry, a size or CRC-32 that
16
- * does not match the data, two entries with one name, a name that is not
17
- * UTF-8, more than MAX_ZIP_ENTRIES entries or MAX_ZIP_BYTES bytes in all, and
18
- * a name nested deeper than MAX_ZIP_DEPTH folders. What it REPORTS and never
19
- * reads, entry by entry: an encrypted entry, a symbolic link, a method other
20
- * than stored or deflate, and a name that is absolute, holds a backslash or a
21
- * NUL, or has an empty, `.` or `..` segment.
8
+ * It refuses the whole archive for: no end-of-central-directory record, ZIP64,
9
+ * more than one disk, an entry placed outside the archive, a local header that
10
+ * contradicts its central entry, a size or CRC-32 that does not match, two
11
+ * entries with one name, a name that is not UTF-8, more than MAX_ZIP_ENTRIES
12
+ * entries or MAX_ZIP_BYTES bytes, or a name nested deeper than MAX_ZIP_DEPTH.
13
+ * It reports without reading, per entry: an encrypted entry, a symbolic link,
14
+ * another method, and a name that is absolute, holds a backslash or NUL, or has
15
+ * an empty, `.` or `..` segment.
22
16
  *
23
- * Pure: bytes in, entries out. The caller turns them into the tree `case`
24
- * splits.
17
+ * Pure: bytes in, entries out.
25
18
  */
26
19
 
27
20
  import { inflateRawSync } from "node:zlib";
28
21
 
29
22
  import { utf8Strict } from "./verifier.js";
30
23
 
31
- /** More entries than this refuse the archive: the walk's own bound on names. */
24
+ /** More entries than this refuse the archive. */
32
25
  export const MAX_ZIP_ENTRIES = 50_000;
33
26
  /** More uncompressed bytes than this, across every entry, refuse the archive. */
34
27
  export const MAX_ZIP_BYTES = 2 ** 30;
35
28
  /**
36
29
  * An archive file larger than this is refused before it is read: the
37
- * uncompressed cap, and room for the headers, the central directory and the
38
- * deflate framing of an archive that holds that much. The caller checks the
39
- * size on disk, so the archive is never read whole to find out it is too big.
30
+ * uncompressed cap plus room for headers and framing. The caller checks the
31
+ * size on disk.
40
32
  */
41
33
  export const MAX_ZIP_ARCHIVE_BYTES = MAX_ZIP_BYTES + 256 * 2 ** 20;
42
34
  /** A name nested deeper than this many folders refuses the archive. */
@@ -49,7 +41,7 @@ const S_IFMT = 0o170000;
49
41
  const S_IFLNK = 0o120000;
50
42
  const UNIX = 3;
51
43
 
52
- /** The archive cannot be read as a case file. `message` is the whole report, identical in both kernels. */
44
+ /** The archive cannot be read as a case file. `message` is the whole report. */
53
45
  export class ZipReadError extends Error {
54
46
  constructor(message: string) {
55
47
  super(message);
@@ -57,10 +49,7 @@ export class ZipReadError extends Error {
57
49
  }
58
50
  }
59
51
 
60
- /**
61
- * One file entry. `refused` is why the entry is reported and not read, or
62
- * `null`; `data` is its bytes where it was read.
63
- */
52
+ /** One file entry. `refused` is why it is reported and not read, or `null`; `data` is set where it was read. */
64
53
  export interface ZipEntry {
65
54
  name: string;
66
55
  refused: string | null;
@@ -101,18 +90,13 @@ function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
101
90
  return true;
102
91
  }
103
92
 
104
- /**
105
- * Every FILE entry of a ZIP archive, in central-directory order. A directory
106
- * entry (a name ending in `/`) is not a file and is not returned. Throws
107
- * `ZipReadError`.
108
- */
93
+ /** Every file entry of a ZIP archive, in central-directory order (directory entries are skipped). Throws `ZipReadError`. */
109
94
  export function readZip(data: Uint8Array): ZipEntry[] {
110
95
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
111
96
  const n = data.length;
112
97
  const u32 = (at: number) => view.getUint32(at, true);
113
98
  const u16 = (at: number) => view.getUint16(at, true);
114
- // The end-of-central-directory record: 22 bytes plus a comment of at most
115
- // 65535, searched backwards so the LAST signature wins.
99
+ // 22 bytes plus a comment of at most 65535, searched backwards so the last signature wins.
116
100
  let eocd = -1;
117
101
  const lowest = Math.max(0, n - 22 - 0xffff);
118
102
  for (let at = n - 22; at >= lowest; at--) {
@@ -214,9 +198,7 @@ export function readZip(data: Uint8Array): ZipEntry[] {
214
198
  if (method === 0) {
215
199
  body = packed;
216
200
  } else {
217
- // `info: true` hands back the engine, whose `bytesWritten` is how many
218
- // input bytes the stream consumed. Anything left over is data after the
219
- // end of the deflate stream, which Python's `unused_data` refuses too.
201
+ // Bytes left over after the end of the deflate stream are refused.
220
202
  let result: { buffer: Buffer; engine: { bytesWritten: number } };
221
203
  try {
222
204
  result = inflateRawSync(packed, {