@sanning/proof 0.17.0 → 0.19.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.
package/README.md CHANGED
@@ -63,7 +63,7 @@ npx @sanning/proof verify trace-bundle.json
63
63
  npx @sanning/proof verify trace-bundle.json --gateways https://arweave.net,https://permagate.io
64
64
  ```
65
65
 
66
- It prints a per-event verdict, then the PACK verdict, and exits by the one table every verb uses: `0` verified · `1` a real failure (bad signature / tamper / broken inclusion) · `2` usage or malformed · `3` undetermined (`partial`, `pending`, `incomplete` or `not_found`: a gateway that could not be reached, a stamp no witness published, or a pack the grading rule calls `incomplete`) · `4` mixed. Since 0.13.0 `partial` exits `3` whatever caused it; it exited `0` unless a gateway was unreachable. The headline is the pack verdict, the same one `gradePack` gives the browser verify page: a record nobody hashed leaves the pack incomplete even when the signed bundle itself is `verified`, and both verdicts are printed when they differ. Only `arweave` witnesses are re-fetched: a checkpoint or stamp whose `anchor_ref.kind` is anything else is not checked, and with gateways passed the run answers `partial`, exit `3`. The producer's asserted `verdict` is shown but **never trusted**, the displayed verdict is always recomputed from the body. The same CLI also verifies the agent's `sanning.agent.proof/v1` inclusion bundle (it sniffs `spec_version`), so one command covers anchor + agent.
66
+ It prints a per-event verdict, then the PACK verdict, and exits by the one table every verb uses: `0` verified · `1` a real failure (bad signature / tamper / broken inclusion) · `2` usage or malformed · `3` undetermined (`partial`, `pending`, `incomplete` or `not_found`: a gateway that could not be reached, a stamp no witness published, or a pack the grading rule calls `incomplete`) · `4` mixed · `5` sandbox: every check passed, and a record is signed `environment: "dev"`. Since 0.13.0 `partial` exits `3` whatever caused it; it exited `0` unless a gateway was unreachable. A pack holding a record signed `dev` never prints `VERIFIED`: the headline is `SANDBOX`, the line `Sandbox evidence (environment dev): not production evidence` follows it, `--json` carries `environment_marker` (`dev` or `mixed`), and a run that would exit `0` exits `5`. So does a pack whose records are all `dev` whose only unchecked witness, with `--gateways`, is `simulated`; a failure is still `1`, and a production or mixed pack with a simulated witness is still `3`. A production pack prints what it did before. The headline is the pack verdict, the same one `gradePack` gives the browser verify page: a record nobody hashed leaves the pack incomplete even when the signed bundle itself is `verified`, and both verdicts are printed when they differ. Only `arweave` witnesses are re-fetched: a checkpoint or stamp whose `anchor_ref.kind` is anything else is not checked, and with gateways passed the run answers `partial`, exit `3`. A sandbox plane's `simulated` witness is never fetched and never counted as checked on a chain: where it is the only witness, the verdict line says nothing was looked up and `--json` writes `"on_chain_checked": false`. The producer's asserted `verdict` is shown but **never trusted**, the displayed verdict is always recomputed from the body. The same CLI also verifies the agent's `sanning.agent.proof/v1` inclusion bundle (it sniffs `spec_version`), so one command covers anchor + agent.
67
67
 
68
68
  > The `verify` CLI is live in `@sanning/proof` ≥ 0.2.2, `npx @sanning/proof verify <bundle> [--gateways <url,url>]` (the gateway list was a second positional argument until 0.13.0). `verifyEvidenceBundle` is the programmatic API.
69
69
 
@@ -226,7 +226,7 @@ The walk is bounded and follows no link. A symbolic link is never followed: it i
226
226
 
227
227
  Known differences from the verify page: the page opens a dropped `.zip`; the page drops a leading byte-order mark from `bundle.json`, which `verify`'s reader refuses; the page reads whatever the browser hands it for a link, a bad name or a very deep tree; and the page loses a record named `__proto__`, which this command keeps.
228
228
 
229
- The first line is the case verdict, then the kernel's summary and every exception through `renderException`, then the packs, the files outside them, and the on-chain half. Exit codes: `0` verified · `1` failed · `2` malformed, usage, an input that cannot be read as a case, or an unexpected error in the command · `3` incomplete or partial · `4` mixed (more than one outcome: read the exceptions). This is the one table every verb uses (`EXIT_CODES`, exported from the package root), and `verify` exits `3` on `partial` since 0.13.0.
229
+ The first line is the case verdict, then the kernel's summary and every exception through `renderException`, then the packs, the files outside them, and the on-chain half. Exit codes: `0` verified · `1` failed · `2` malformed, usage, an input that cannot be read as a case, or an unexpected error in the command · `3` incomplete or partial · `4` mixed (more than one outcome: read the exceptions) · `5` sandbox (it would be `0`, and a pack holds a record signed `environment: "dev"`, or every record is `dev` and only simulated witnesses are unchecked; the title is then `Sandbox`, never `Verified`). This is the one table every verb uses (`EXIT_CODES`, exported from the package root), and `verify` exits `3` on `partial` since 0.13.0.
230
230
 
231
231
  A case reading establishes that every pack verified offline, that every record every pack supplied was read in one pile under envelope-spec §4.1's hand-off rules, and that a hand-off record with no counterpart is always named. It does **not** establish that the case is complete, that a party which presented no pack under the named key recorded nothing, that two organizations are independent, or that any witness was re-fetched on chain: the output says `on-chain NOT CHECKED` unless you pass `--gateways <url,url>`. With gateways, each pack's witnesses are re-fetched pack by pack and reported beside the reading, which they never change; a witness that does not match exits `1`, and an on-chain half that could not be settled turns `0` into `3`.
232
232
 
package/dist/case-file.js CHANGED
@@ -82,7 +82,8 @@ export function howToVerifyText(name) {
82
82
  ` sanning-proof case ${name}\n` +
83
83
  "\n" +
84
84
  "Exit codes: 0 verified, 1 failed, 2 not readable or a usage error,\n" +
85
- "3 could not be settled (partial, pending, incomplete, not found), 4 mixed.\n" +
85
+ "3 could not be settled (partial, pending, incomplete, not found), 4 mixed,\n" +
86
+ "5 sandbox (records signed environment dev, not production evidence).\n" +
86
87
  "\n" +
87
88
  "To also look each witness up on the public record, add --gateways <url,url>.\n" +
88
89
  "The lookup is reported on its own line and never changes the verdict word.\n" +
package/dist/case.js CHANGED
@@ -241,7 +241,8 @@
241
241
  * sequence. Narrowing any of it here with `handoffs[]` is precisely the move
242
242
  * that would reopen proof#137's door.
243
243
  */
244
- import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
244
+ import { DECLARATION_VERSION, DECLARATION_VERSIONS, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
245
+ import { readChainLinks } from "./chain.js";
245
246
  import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
246
247
  import { coSignaturesPresent, resolveTraceEvents, verifyEvidenceBundle } from "./evidence.js";
247
248
  import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
@@ -1152,10 +1153,14 @@ export async function readCase(packs) {
1152
1153
  grade = signed.status === "verified" ? "malformed" : signed.status;
1153
1154
  }
1154
1155
  else {
1155
- check = checkDeclaration(pack.bundle, pack.content);
1156
+ // Rule 11 reads the chain links of the same pack, and the reader reads
1157
+ // only the signed body, so the bare check below takes the same reading
1158
+ // (evidence-bundle.md 5.4.2 v1.36, A45).
1159
+ const chainLinks = await readChainLinks(pack.bundle);
1160
+ check = checkDeclaration(pack.bundle, pack.content, chainLinks);
1156
1161
  const completeness = completenessNotes(pack.bundle);
1157
1162
  full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
1158
- const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null));
1163
+ const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null, chainLinks));
1159
1164
  // NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
1160
1165
  // where it is `failed` or `malformed`; a failure only the side map
1161
1166
  // produced is `incomplete`; otherwise the full grade.
@@ -1619,7 +1624,9 @@ function declarationVersionOf(bundle) {
1619
1624
  return null;
1620
1625
  }
1621
1626
  const version = declaration["declaration_version"];
1622
- return version === DECLARATION_VERSION || version === DECLARATION_VERSION_V2 ? version : null;
1627
+ return typeof version === "string" && DECLARATION_VERSIONS.includes(version)
1628
+ ? version
1629
+ : null;
1623
1630
  }
1624
1631
  // ---- Presentation (optional) ------------------------------------------------
1625
1632
  /**
package/dist/chain.d.ts CHANGED
@@ -79,6 +79,34 @@ export interface ChainReading {
79
79
  chains: ChainSummary[];
80
80
  findings: ChainFinding[];
81
81
  }
82
+ /**
83
+ * One record's place in a chain, as the reader grouped it, for a check that
84
+ * needs the links themselves rather than the findings: rule 11 of
85
+ * evidence-bundle.md 5.4.2, which holds a claimed chain to every link. The
86
+ * twin of Python's `ChainLink`.
87
+ */
88
+ export interface ChainLink {
89
+ eventId: string;
90
+ publicKey: string;
91
+ chainKey: string;
92
+ source: ChainSource;
93
+ namespace?: string;
94
+ /** The SHA-256 of the record's committed bytes, or its event id for an adapter chain. */
95
+ selfId: string;
96
+ /** The pointer the record carries, or `null` where it begins a chain. */
97
+ prev: string | null;
98
+ seq: number | null;
99
+ /** Position in `body.events`. */
100
+ order: number;
101
+ }
102
+ /**
103
+ * Every record the chain reader places in a chain, in bundle order, with the
104
+ * identity and pointer it read: the grouping `readChains` reads, over every
105
+ * record an envelope binds, `event_bytes` or the inline `payload` where no
106
+ * `event_bytes` travel, with the identity the SHA-256 of those bytes, exactly
107
+ * as the envelope verifier binds `payload_hash`.
108
+ */
109
+ export declare function readChainLinks(bundle: unknown): Promise<ChainLink[]>;
82
110
  /**
83
111
  * Follow every chain the bundle's records describe, and report the links that
84
112
  * do not resolve within it.
package/dist/chain.js CHANGED
@@ -49,12 +49,13 @@
49
49
  //
50
50
  // (2) is a transitional accommodation, not the design. `metadata` is
51
51
  // caller-owned and the profile says a verifier MUST NOT read it for
52
- // verification — which is exactly why nothing here feeds a verdict. The fix is
52
+ // verification — which is exactly why nothing read through it feeds a
53
+ // verdict, a claimed chain included (rule 11 reads linkage (1) only). The fix is
53
54
  // for every adapter to set the record's own `previous_hash` (filed separately
54
55
  // against `anchor`); when it lands, the namespace table below stops mattering
55
56
  // and can go.
56
57
  import { hexToBytes, sha256Hex } from "./crypto.js";
57
- import { jcsParse } from "./verifier.js";
58
+ import { inlinePayloadBytes, jcsParse } from "./verifier.js";
58
59
  import { resolveTraceEvents } from "./evidence.js";
59
60
  /**
60
61
  * The one sentence a surface should print beside these findings, so the reader
@@ -186,7 +187,33 @@ function adapterLinkage(record) {
186
187
  * nothing and means a relabelled record DANGLES instead of quietly resolving.
187
188
  * That is why this function is async.
188
189
  */
189
- async function groupChains(bundle) {
190
+ /**
191
+ * The record an envelope binds, for rule 11's links: `event_bytes` where the
192
+ * entry carries them, else the inline `payload` read through
193
+ * `inlinePayloadBytes`, the bytes `payload_hash` binds. Its identity is the
194
+ * SHA-256 of whichever bytes it is read from. Python's `_bound_record`.
195
+ */
196
+ function boundRecord(entry) {
197
+ const eventBytes = entry?.event_bytes;
198
+ if (typeof eventBytes === "string" && eventBytes.length > 0)
199
+ return decodeRecord(eventBytes);
200
+ const envelope = entry?.envelope;
201
+ if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope))
202
+ return null;
203
+ if (!("payload" in envelope))
204
+ return null;
205
+ try {
206
+ const bytes = inlinePayloadBytes(envelope);
207
+ const record = jcsParse(bytes);
208
+ if (record === null || typeof record !== "object" || Array.isArray(record))
209
+ return null;
210
+ return { record, bytes };
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ async function groupChains(bundle, inline = false) {
190
217
  const groups = new Map();
191
218
  const events = eventsOf(bundle);
192
219
  for (let order = 0; order < events.length; order++) {
@@ -197,7 +224,11 @@ async function groupChains(bundle) {
197
224
  // event id an adapter pointer has nothing to resolve against.
198
225
  if (eventId === null || publicKey === null)
199
226
  continue;
200
- const decoded = decodeRecord(entry?.event_bytes);
227
+ // `inline` (rule 11's links only): a record carried as the envelope's
228
+ // inline `payload` with no `event_bytes` is read too. The chain reading
229
+ // itself keeps reading `event_bytes` alone, so its findings and its rule
230
+ // table do not move.
231
+ const decoded = inline ? boundRecord(entry) : decodeRecord(entry?.event_bytes);
201
232
  if (decoded === null)
202
233
  continue;
203
234
  const { record, bytes } = decoded;
@@ -346,6 +377,32 @@ function findingsFor(group) {
346
377
  }
347
378
  return findings;
348
379
  }
380
+ /**
381
+ * Every record the chain reader places in a chain, in bundle order, with the
382
+ * identity and pointer it read: the grouping `readChains` reads, over every
383
+ * record an envelope binds, `event_bytes` or the inline `payload` where no
384
+ * `event_bytes` travel, with the identity the SHA-256 of those bytes, exactly
385
+ * as the envelope verifier binds `payload_hash`.
386
+ */
387
+ export async function readChainLinks(bundle) {
388
+ const links = [];
389
+ for (const g of await groupChains(bundle, true)) {
390
+ for (const m of g.members) {
391
+ links.push({
392
+ eventId: m.eventId,
393
+ publicKey: g.publicKey,
394
+ chainKey: g.chainKey,
395
+ source: g.source,
396
+ ...(g.namespace !== undefined ? { namespace: g.namespace } : {}),
397
+ selfId: m.selfId,
398
+ prev: m.prev,
399
+ seq: m.seq,
400
+ order: m.order,
401
+ });
402
+ }
403
+ }
404
+ return links.sort((a, b) => a.order - b.order);
405
+ }
349
406
  /**
350
407
  * Follow every chain the bundle's records describe, and report the links that
351
408
  * do not resolve within it.