@sanning/proof 0.19.0 → 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.
- package/README.md +30 -29
- package/dist/agent-record.d.ts +62 -0
- package/dist/agent-record.js +527 -0
- package/dist/agent-schemas.generated.d.ts +15 -0
- package/dist/agent-schemas.generated.js +5759 -0
- package/dist/case-file.d.ts +32 -89
- package/dist/case-file.js +41 -93
- package/dist/case.d.ts +343 -810
- package/dist/case.js +693 -1151
- package/dist/chain.d.ts +19 -47
- package/dist/chain.js +78 -205
- package/dist/claims.d.ts +211 -0
- package/dist/claims.js +1081 -0
- package/dist/cli.d.ts +45 -68
- package/dist/cli.js +618 -1022
- package/dist/compose.js +23 -65
- package/dist/crypto.d.ts +7 -8
- package/dist/crypto.js +60 -147
- package/dist/declaration.d.ts +68 -194
- package/dist/declaration.js +175 -518
- package/dist/environment.d.ts +9 -30
- package/dist/environment.js +17 -66
- package/dist/evidence.d.ts +119 -160
- package/dist/evidence.js +744 -1103
- package/dist/exit-codes.d.ts +11 -27
- package/dist/exit-codes.js +24 -59
- package/dist/file-match.d.ts +33 -79
- package/dist/file-match.js +33 -69
- package/dist/grading.d.ts +98 -135
- package/dist/grading.js +224 -380
- package/dist/handoff.d.ts +62 -160
- package/dist/handoff.js +153 -393
- package/dist/index.d.ts +14 -10
- package/dist/index.js +60 -167
- package/dist/json-schema.d.ts +29 -0
- package/dist/json-schema.js +298 -0
- package/dist/merkle.js +16 -28
- package/dist/message-parts.d.ts +56 -212
- package/dist/message-parts.js +142 -502
- package/dist/side-content.d.ts +11 -22
- package/dist/side-content.js +22 -65
- package/dist/types.d.ts +17 -0
- package/dist/types.js +19 -5
- package/dist/verifier.d.ts +40 -114
- package/dist/verifier.js +196 -535
- package/dist/witness-cache.d.ts +24 -45
- package/dist/witness-cache.js +38 -81
- package/dist/witness-lookup.d.ts +36 -57
- package/dist/witness-lookup.js +89 -139
- package/dist/zip-reader.d.ts +18 -33
- package/dist/zip-reader.js +19 -34
- package/package.json +11 -3
- package/src/agent-record.ts +608 -0
- package/src/agent-schemas.generated.ts +5764 -0
- package/src/case-file.ts +45 -109
- package/src/case.ts +848 -1322
- package/src/chain.ts +85 -224
- package/src/claims.ts +1253 -0
- package/src/cli.ts +714 -1176
- package/src/compose.ts +33 -79
- package/src/crypto.ts +60 -147
- package/src/declaration.ts +219 -656
- package/src/environment.ts +17 -65
- package/src/evidence.ts +958 -1381
- package/src/exit-codes.ts +24 -63
- package/src/file-match.ts +40 -89
- package/src/grading.ts +287 -421
- package/src/handoff.ts +175 -427
- package/src/index.ts +132 -177
- package/src/json-schema.ts +295 -0
- package/src/merkle.ts +16 -28
- package/src/message-parts.ts +155 -533
- package/src/side-content.ts +25 -68
- package/src/types.ts +55 -36
- package/src/verifier.ts +202 -526
- package/src/witness-cache.ts +45 -94
- package/src/witness-lookup.ts +107 -177
- package/src/zip-reader.ts +20 -38
- package/vectors/README.md +52 -34
- package/vectors/agent-record.json +2175 -0
- package/vectors/case-file-index.json +1 -1
- package/vectors/case-split.json +2 -2
- package/vectors/chain-reading.json +3 -3
- package/vectors/file-match.json +285 -148
- package/vectors/jcs-reader.json +6 -6
- package/vectors/pack-grading.json +102 -25
- package/vectors/sandbox-evidence.json +597 -580
- package/vectors/stamp-on-chain.json +18 -18
- package/vectors/witness-cache.json +49 -49
- package/dist/agent-proof.d.ts +0 -28
- package/dist/agent-proof.js +0 -226
- package/dist/run-adapters.d.ts +0 -10
- package/dist/run-adapters.js +0 -35
- package/dist/run.d.ts +0 -209
- package/dist/run.js +0 -1405
- package/src/agent-proof.ts +0 -264
- package/src/run-adapters.ts +0 -44
- package/src/run.ts +0 -1598
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Verify Sanning evidence-plane provenance records, **with no Sanning code or service in the trust path**.
|
|
4
4
|
|
|
5
|
-
This is the TypeScript verification kernel for the *Verifiable Event Envelope* family: the signed JSON records that
|
|
5
|
+
This is the TypeScript verification kernel for the *Verifiable Event Envelope* family: the signed JSON records that Sanning's SDKs anchor permanently to Arweave. Given an envelope (fetched from any AR.IO gateway: a gateway that serves Arweave data over HTTP; any gateway that serves the transaction works), this package answers: *is it authentic, and does it commit to the bytes I'm holding?*
|
|
6
6
|
|
|
7
7
|
- **RFC 8785 (JCS)** canonicalization
|
|
8
8
|
- **SHA-256** payload binding (WebCrypto)
|
|
@@ -18,12 +18,12 @@ npm install @sanning/proof
|
|
|
18
18
|
|
|
19
19
|
## Verify an envelope
|
|
20
20
|
|
|
21
|
-
Fetch the raw transaction from any
|
|
21
|
+
Fetch the raw transaction from any AR.IO gateway and verify it client-side:
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
import { verifyEnvelope } from "@sanning/proof";
|
|
25
25
|
|
|
26
|
-
const env = await (await fetch("https://
|
|
26
|
+
const env = await (await fetch("https://gateway.example/raw/<tx_id>")).json();
|
|
27
27
|
const result = await verifyEnvelope(env);
|
|
28
28
|
|
|
29
29
|
result.ok; // spec_version + payload_hash + Ed25519 all passed
|
|
@@ -53,19 +53,19 @@ contentHashes(env); // all content hashes an envelope commits to, by event typ
|
|
|
53
53
|
|
|
54
54
|
## Verify a whole trace bundle: one command
|
|
55
55
|
|
|
56
|
-
`npm install -g @sanning/proof` puts `sanning-proof` on your path, the same name `pip install sanning-proof` does
|
|
56
|
+
`npm install -g @sanning/proof` puts `sanning-proof` on your path, the same name `pip install sanning-proof` does. `npx @sanning/proof <command>` runs it without installing.
|
|
57
57
|
|
|
58
58
|
The `@sanning/anchor` SDK hands a producer a set of `InclusionReceipt`s and can serialize them into one portable, self-verifying `sanning.evidence/v1` bundle (`body_type: sanning.anchor.trace/v3`). Verify the entire bundle (every event's signature + payload binding + Merkle inclusion, all offline) with the bundled CLI:
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
npx @sanning/proof verify trace-bundle.json
|
|
62
62
|
# also re-fetch each checkpoint on-chain to confirm it's anchored:
|
|
63
|
-
npx @sanning/proof verify trace-bundle.json --gateways https://
|
|
63
|
+
npx @sanning/proof verify trace-bundle.json --gateways https://gateway-a.example,https://gateway-b.example
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
It prints a per
|
|
66
|
+
It prints the person layer first (a verdict of `Verified`, `Failed` or `Incomplete`, one line per claim that is disproven or not proven, the next action, the notes, the sandbox line and the witness line), then the detail, and exits by the one table every verb uses: `0` verified · `1` failed (a claim is disproven: a bad signature, a tamper, a broken inclusion, a record that does not follow its vocabulary) · `2` incomplete (nothing is disproven and a claim is not proven: a stamp no witness published, a record nobody hashed, or a pack the grading rule calls `incomplete`) · `3` sandbox: the verdict is `verified` and a record is signed `environment: "dev"` · `4` could not run (a bad flag, a missing file, or an input that is not a pack or a case at all). A `failed` verdict carries a reason kind in `--json`: `nonconforming` where an agent record is signed and intact and breaks its vocabulary and no other claim is disproven (a producer's error, nothing proven altered), `changed` otherwise; both exit `1`. Sandbox is a label, never a verdict: a pack holding a record signed `dev` keeps its verdict word, the line `Sandbox: these records are sandbox records, not production evidence.` is printed, `--json` carries `environment_marker` (`dev` or `mixed`), and a run that would exit `0` exits `3`; a failure is still `1` and an incomplete run `2`. For a production pack `environment_marker` is absent and no sandbox line prints. 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 looked up, the witness line says so, and the verdict and the exit code do not move (only a lookup that finds other bytes or another time moves one, to `1`). A sandbox plane's `simulated` witness is never fetched and never counted as checked on a chain: where it is the only witness, the report says nothing was looked up and `--json` writes `"on_chain_checked": false`. A bundle carries no verdict: the verdict is always recomputed from the body, and a wrapper `verdict` member, where an older producer wrote one, is never read.
|
|
67
67
|
|
|
68
|
-
> The
|
|
68
|
+
> The command is `npx @sanning/proof verify <bundle> [--gateways <url,url>]`, and `verifyEvidenceBundle` is the programmatic API.
|
|
69
69
|
|
|
70
70
|
Programmatically:
|
|
71
71
|
|
|
@@ -73,16 +73,16 @@ Programmatically:
|
|
|
73
73
|
import { verifyEvidenceBundle } from "@sanning/proof";
|
|
74
74
|
|
|
75
75
|
const bundle = JSON.parse(await fs.readFile("trace-bundle.json", "utf8"));
|
|
76
|
-
const result = await verifyEvidenceBundle(bundle, { gateways: ["https://
|
|
76
|
+
const result = await verifyEvidenceBundle(bundle, { gateways: ["https://gateway.example"] });
|
|
77
77
|
|
|
78
|
-
result.status; // "verified" | "
|
|
78
|
+
result.status; // "verified" | "failed" | "incomplete" (recomputed)
|
|
79
79
|
result.signatureOk; // wrapper Ed25519 signature
|
|
80
80
|
result.bodyHashOk; // body_hash === SHA-256(JCS(body))
|
|
81
81
|
result.checkpoints; // per-checkpoint envelope / merkle-root / on-chain results
|
|
82
82
|
result.events; // per-event signature / payload-binding / inclusion results
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
A withheld `event_bytes` (external commitment, record not disclosed) leaves that event's payload binding **
|
|
85
|
+
A withheld `event_bytes` (external commitment, record not disclosed) leaves that event's payload binding **not proven**, surfaced, never failed (the bundle stays cryptographically sound).
|
|
86
86
|
|
|
87
87
|
### …including the raw logs, when the holder discloses them
|
|
88
88
|
|
|
@@ -94,7 +94,7 @@ A bundle a producer built with disclosure carries the bytes **in the signed body
|
|
|
94
94
|
npx @sanning/proof verify trace-bundle.json # in-body content verified inline
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
If the logs travel **alongside** a minimal bundle instead, pass them with `--logs`. It takes either shape below, and which one you passed is
|
|
97
|
+
If the logs travel **alongside** a minimal bundle instead, pass them with `--logs`. It takes either shape below, and which one you passed is determined by `stat`, never by the extension.
|
|
98
98
|
|
|
99
99
|
**A materialized pack: a DIRECTORY.** `materializeBundle` (in [`@sanning/anchor`](https://www.npmjs.com/package/@sanning/anchor)) writes `bundle.json` + `logs/<event_id>.json`, and each file's **raw bytes are the disclosure**. Point `--logs` at that directory and the pack verifies with nothing of ours in the path:
|
|
100
100
|
|
|
@@ -116,14 +116,14 @@ The directory form's edge cases, **none of which is a verification failure**, an
|
|
|
116
116
|
|
|
117
117
|
| Situation | What happens |
|
|
118
118
|
|---|---|
|
|
119
|
-
| A record with no file | That event's content is **
|
|
120
|
-
| An empty directory | Valid. Discloses nothing and says so. The exit code is whatever the pack reads with no `--logs` at all, which for a pack holding any undisclosed record is **
|
|
119
|
+
| A record with no file | That event's content is **not proven** (`~`) and reported as undisclosed. Exit code unchanged. |
|
|
120
|
+
| An empty directory | Valid. Discloses nothing and says so. The exit code is whatever the pack reads with no `--logs` at all, which for a pack holding any undisclosed record is **2**, not 0. |
|
|
121
121
|
| A file whose name is no event id | Read, binds to nothing, and is **named** in a note: a typo'd filename is otherwise indistinguishable from minimal disclosure. |
|
|
122
122
|
| A subdirectory | Skipped, never recursed: the layout is flat. Noted. |
|
|
123
123
|
| Anything not named `<event_id>.json`, or not a regular file | Skipped, noted. |
|
|
124
|
-
| A file we selected but cannot read | **Exit
|
|
124
|
+
| A file we selected but cannot read | **Exit 4.** We were pointed at it, so a permissions or broken-link problem must not read as minimal disclosure. |
|
|
125
125
|
|
|
126
|
-
Either way each event gains a `contentOk`: **`true`** match · **`false`** mismatch (the event *and* the rollup fail, exit `1`) · **`null`**
|
|
126
|
+
Either way each event gains a `contentOk`: **`true`** match · **`false`** mismatch (the event *and* the rollup fail, exit `1`) · **`null`** not proven, nothing disclosed, or no committed `content_hash` (a withheld record); never a failure, mirroring the payload-binding rule. The CLI prints a `logs ✓/✗/~` mark per event and a `logs: N/M record(s) content-verified` line, where M is the records in the PACK and not the records that happened to disclose bytes: a pack disclosing two of three reads `2/3`, not the full score `2/2`.
|
|
127
127
|
|
|
128
128
|
Programmatically the same side input is `content`, keyed by `event_id`: a `Uint8Array` is used as-is, a `string` is hex. **Prefer a `Map`:**
|
|
129
129
|
|
|
@@ -199,8 +199,8 @@ folder names are never compared, and the verdict and the exit code never move.
|
|
|
199
199
|
(`too_large`) and not read, so a sparse file of a terabyte cannot hold the
|
|
200
200
|
command. `case` bounds its other reads of files on disk as well; the section on `case` lists them.
|
|
201
201
|
- A folder nested more than 32 levels deep, one holding more than 50,000 files
|
|
202
|
-
and folders, or one with a name that is not valid UTF-8, exits
|
|
203
|
-
does. A path that is not a readable file or folder exits
|
|
202
|
+
and folders, or one with a name that is not valid UTF-8, exits 4, as `case`
|
|
203
|
+
does. A path that is not a readable file or folder exits 4.
|
|
204
204
|
|
|
205
205
|
## Read several packs together: a case reading
|
|
206
206
|
|
|
@@ -220,15 +220,15 @@ Each argument is a folder, or a case file named as a `.zip`. A folder is one pac
|
|
|
220
220
|
npx @sanning/proof case audit-file.zip
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
This is information beside the case reading and never inside it: the verdict, the exit code and the summary are the same whether a file matches or not, and a changed file reads as no match, nothing more. A match is by hash alone, so no name or folder is compared. Files after `--files` are offered as well. Each file that cannot be hashed, such as a link, a pipe, a zip entry the reader refused (with the reader's own words) or a file that cannot be read, is listed as skipped with the reason, so every file outside a pack is matched, unmatched or skipped. Two files with one hash are both listed; an empty file is an ordinary file; a file is read in pieces. The files share 1 GiB in all, the cap the `.zip` reader uses: one that would push past it is listed as skipped (`too_large`) and not read, so a sparse file of a terabyte in a download cannot hold `case` at that step. Every other read `case` makes of a file on disk is bounded too, so a sparse file of a terabyte can neither stall `case` nor exhaust its memory. The index comparison hashes every file under `packs/` and `report/` in pieces, within its own 1 GiB, the files the index lists first, and a listed file it has no room left for is reported as `case_index_file_too_large`, never as a match and never as absent. A pack's own files (`bundle.json`, `logs/`), `contents.json` and `witness-cache.json` are read whole within one 1 GiB total across the run: a pack's own file with no room left exits
|
|
223
|
+
This is information beside the case reading and never inside it: the verdict, the exit code and the summary are the same whether a file matches or not, and a changed file reads as no match, nothing more. A match is by hash alone, so no name or folder is compared. Files after `--files` are offered as well. Each file that cannot be hashed, such as a link, a pipe, a zip entry the reader refused (with the reader's own words) or a file that cannot be read, is listed as skipped with the reason, so every file outside a pack is matched, unmatched or skipped. Two files with one hash are both listed; an empty file is an ordinary file; a file is read in pieces. The files share 1 GiB in all, the cap the `.zip` reader uses: one that would push past it is listed as skipped (`too_large`) and not read, so a sparse file of a terabyte in a download cannot hold `case` at that step. Every other read `case` makes of a file on disk is bounded too, so a sparse file of a terabyte can neither stall `case` nor exhaust its memory. The index comparison hashes every file under `packs/` and `report/` in pieces, within its own 1 GiB, the files the index lists first, and a listed file it has no room left for is reported as `case_index_file_too_large`, never as a match and never as absent. A pack's own files (`bundle.json`, `logs/`), `contents.json` and `witness-cache.json` are read whole within one 1 GiB total across the run: a pack's own file with no room left exits 4 at once, naming the file (the words say the run's total is used up, not that the file is itself over 1 GiB), a `contents.json` with no room left reads as an unreadable index (`case_index_unreadable`), and a `witness-cache.json` with no room left reads as no cache, with a `note` line saying so. A `.zip` larger than 1.25 GiB on disk exits 4 before it is read. A file outside every pack, `contents.json` and `witness-cache.json` included, therefore never keeps `case` from its verdict, and one a pack owns, or an archive that large, is refused promptly instead. Without `--files` the report lists each file read and then one line, `N recorded commitment(s) matched no loose file and no log in their own pack`, with no line for each record; `--files` and `--json` list every commitment no file matched, those the count leaves out included. The section is printed when a file sits outside every pack or `--files` is given, so a case with neither prints what it printed before, and the `files` member of `--json` follows the same rule.
|
|
224
224
|
|
|
225
225
|
The walk is bounded and follows no link. A symbolic link is never followed: it is listed as outside every pack and not read, even under a pack's `logs/`, and a link named on the command line exits `2`. A folder nested more than 32 levels deep, more than 50,000 files and folders in all, or a name that is not valid UTF-8 exits `2`; a bad name is refused rather than escaped, because two different names would then read as one. A `bundle.json` nested more than 512 levels deep exits `2`, from `case` and from `verify` alike. The rules are pinned for both kernels by `vectors/case-split.json`, shipped in this tarball, as directory trees.
|
|
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
|
|
229
|
+
The first lines are the person layer, which opens with the case verdict, then come the kernel's summary and every exception through `renderException`, then the packs, the files outside them, and the on-chain half. A case reads its worst pack: `failed`, then `incomplete`, then `verified`. Exit codes: `0` verified · `1` failed (either reason kind; a proven failure keeps the headline) · `2` incomplete · `3` sandbox (the verdict is `verified` and a pack holds a record signed `environment: "dev"`) · `4` could not run (a bad flag, a missing file, an input that cannot be read as a case, or an unexpected error in the command). This is the one table every verb uses (`EXIT_CODES`, exported from the package root).
|
|
230
230
|
|
|
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
|
|
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 witness line reads `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 a lookup that could not be settled moves nothing.
|
|
232
232
|
|
|
233
233
|
The Python kernel ships the same command as `sanning-proof case`, and `cross-kernel/run_case_cli.sh` requires the two to print the same bytes, the same `--json` and the same exit code over every `case/` corpus vector and every tree in `vectors/case-split.json`.
|
|
234
234
|
|
|
@@ -241,7 +241,7 @@ npx @sanning/proof export source.json --attestations attestations.json --key exp
|
|
|
241
241
|
npx @sanning/proof verify export.json
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
`export` exits `0` when it wrote the export, `1` when the recomputed export is `failed` (it still writes it, so you can inspect it), and `
|
|
244
|
+
`export` exits `0` when it wrote the export, `1` when the recomputed export is `failed` (it still writes it, so you can inspect it), and `4` on a bad flag or an input that is not a bundle at all. The Python kernel ships the same command as `sanning-proof export`.
|
|
245
245
|
|
|
246
246
|
## Verify a Merkle inclusion proof
|
|
247
247
|
|
|
@@ -263,29 +263,30 @@ const ok = await verifyInclusion(
|
|
|
263
263
|
|
|
264
264
|
| Export | What it does |
|
|
265
265
|
|---|---|
|
|
266
|
-
| `verifyEnvelope(env, options?)` | The three load-bearing checks (spec-version registry, payload-hash recompute, Ed25519 over the signed scope) + optional content bind. Pass `{ payloadBytes }` for external-commitment envelopes, `{ expectedContentHash }` for the content bind (a bare string is refused
|
|
266
|
+
| `verifyEnvelope(env, options?)` | The three load-bearing checks (spec-version registry, payload-hash recompute, Ed25519 over the signed scope) + optional content bind. Pass `{ payloadBytes }` for external-commitment envelopes, `{ expectedContentHash }` for the content bind (a bare string is refused), and `{ acceptedSpecMajors }` to narrow the accept-set for one call. Returns `VerificationResult`, whose `fault` says why `ok` is false (`ENVELOPE_FAULTS`: `not_an_envelope`, `key_unreadable`, `signature_failed`, `payload_unbound`, `spec_version_unknown`, `nonconforming`); `ENVELOPE_FAULT_KINDS` maps each to the kind, claim and state a pack reads (envelope-spec section 2). |
|
|
267
267
|
| `verifyEvidenceBundle(bundle, { gateways?, fetchImpl? })` | Verify a signed `sanning.evidence/v1` bundle (`sanning.anchor.trace/v3`, `sanning.stamp.trace/v1`, `sanning.stamp.evidence/v2` or `sanning.evidence.export/v2`): wrapper signature + `body_hash` + every checkpoint + every event's inclusion, offline (optional on-chain re-fetch). Returns `EvidenceBundleResult`. Powers the `npx @sanning/proof verify` CLI. |
|
|
268
268
|
| `disclosedContent(bundle, { gateways?, content?, fetchImpl? })` | **Ingest:** verify a bundle, then hand back the DECODED raw bytes (`Uint8Array`) of every event whose disclosure bound to its committed `content_hash`, so materializing a received pack never means re-implementing the hex convention. Returns `DisclosedContentResult` (`content` map + per-event `verified` / `not_disclosed` / `undetermined` / `failed`, plus the full `verification`). |
|
|
269
|
-
| `gradePack(result, account, notes)` + `accountDisclosure(bundle, content)` / `completenessNotes(bundle)` / `renderCause(cause)` | **Pack grading:** what a SURFACE may claim about a pack: the bundle plus whatever disclosed bytes travelled with it.
|
|
270
|
-
| `
|
|
269
|
+
| `gradePack(result, account, notes)` + `accountDisclosure(bundle, content)` / `completenessNotes(bundle)` / `renderCause(cause)` | **Pack grading:** what a SURFACE may claim about a pack: the bundle plus whatever disclosed bytes travelled with it. `verified` only when every record's own bytes bound to its own commitment, nothing is missing, unreadable, surplus or omitted, and every agent record reads `verified`; `failed` where a claim is disproven (a proven mismatch, or an agent record that breaks its vocabulary, which carries the reason kind `nonconforming` where nothing else is disproven); `incomplete` where a claim is not proven. Absent evidence is not proven, never an accusation. Pure: no I/O; transport stays yours. Conformance table: `vectors/pack-grading.json`, shipped in this tarball. |
|
|
270
|
+
| `readAgentRecord(record)` / `readAgentRecordWithHeld(subject, held)` | Read one `agent.*` event record (`agent-events.md` section 10 of the pinned `sanning-io/specs` release): steps 1 to 6 alone, or steps 1 to 7 with the records you hold beside it. The schemas ship inside the package (`AGENT_SCHEMA_VERSIONS`, pinned by `AGENT_SCHEMA_PIN`); nothing is read from disk or fetched. Returns `{ verdict, reason?, at?, reasons?, findings }`: `verified`; `failed` or `incomplete` with every reason and its JSON Pointer in step order (`reasons`; `reason` and `at` are the first), `incomplete` for a vocabulary version this build does not know or a listed record not held (never `verified`); or `null` for a record outside the vocabulary (which reads `failed`, `bad_value`, at `/context/salt` where its salt is not 32 lowercase hex characters). `AGENT_EVENT_TYPES` is the closed type set per version. Conformance table: `vectors/agent-record.json`. |
|
|
271
|
+
| `readAgentRecords(items)` / `renderAgentReading(reading)` | The reading `verify`, `case` and `verifyEvidenceBundle` wire in, over every `sanning.events/v2` record. `readAgentRecords` reads each item of a pile through steps 1 to 7 with every item held beside it: a type outside the closed set reads `failed`, and `null` is the reading of one of the seven types outside the vocabulary with no `event_schema` and no `context.salt` in the wrong form. The bundle verifier puts each reading on `EventResult.agent`, over the pack; `readCase` reads every pack of the case as one pile and puts it on `records[].agent`. `gradePack` (rule version 9, `vectors/pack-grading.json`) turns each reason of an agent record that is not `verified` into a cause: `agent_record_nonconforming` for a reason that reads `failed` (the pack is `failed`, reason kind `nonconforming`) and `agent_record_incomplete` for one that reads `incomplete`; a finding never moves the verdict. A hand-off record that reads `failed` makes the record it pairs with read `counterpart_nonconforming`. |
|
|
271
272
|
| `contentHashes(env)` | The content hash(es) an envelope commits to, by event type: the reverse-provenance join keys. |
|
|
272
|
-
| `specVersionSupported(v)` | Fail-closed accept-check: `sanning.
|
|
273
|
+
| `specVersionSupported(v)` | Fail-closed accept-check: `sanning.events/v2` and additive minors (`sanning.events/v2.<n>`). Unknown majors and profiles, the retired `sanning.agent/v1` among them, are rejected. |
|
|
273
274
|
| `jcs(value)` | RFC 8785 canonical JSON bytes. |
|
|
274
275
|
| `sha256Hex(bytes)` / `sha256Bytes(bytes)` | SHA-256 via WebCrypto. |
|
|
275
|
-
| `ed25519Verify(message, signatureHex, publicKeyHex)` | Raw Ed25519 verification, strict RFC 8032 (`zip215: false`). The message comes first
|
|
276
|
+
| `ed25519Verify(message, signatureHex, publicKeyHex)` | Raw Ed25519 verification, strict RFC 8032 (`zip215: false`). The message comes first, as in the Python `ed25519_verify`. |
|
|
276
277
|
| `leafHash` / `nodeHash` / `merkleRoot` / `auditPath` / `verifyInclusion` / `EMPTY_TREE_ROOT_HEX` | RFC 9162 binary Merkle tree primitives. |
|
|
277
278
|
| `utf8` / `bytesToHex` / `hexToBytes` | Encoding helpers. |
|
|
278
279
|
| `Envelope` / `VerificationResult` / `ContentRole` / `Subject` | Types. |
|
|
279
280
|
|
|
280
281
|
### Signed scope
|
|
281
282
|
|
|
282
|
-
The primary signature covers `JCS(envelope minus signature minus co_signatures)` (`envelope-spec.md` §2
|
|
283
|
+
The primary signature covers `JCS(envelope minus signature minus co_signatures)` ([`specs/envelope-spec.md`](https://github.com/sanning-io/proof/tree/main/specs) §2 and §7.1, in the source repository). The reserved `co_signatures` carve-out lets countersignatures be added later without invalidating the primary signature.
|
|
283
284
|
|
|
284
285
|
## Why you can trust it (without trusting us)
|
|
285
286
|
|
|
286
|
-
|
|
287
|
+
Two **independent implementations** of this kernel exist: this package and the Python [`sanning-proof`](https://pypi.org/project/sanning-proof/) (PyPI). They are conformance-gated **byte-for-byte** in CI against one shared corpus, pinned per file by SHA-256 at the newest cut tag, `test-vectors-v5.0`, named in `test-vectors/CORPUS-v5.md`, and a cross-kernel agreement gate re-runs the corpus plus adversarial negatives through both, asserting identical verdicts on identical bytes.
|
|
287
288
|
|
|
288
|
-
That mutual gate is the point: anyone can verify these provenance records with **no Sanning code in the trust path**, write your own verifier against the specs
|
|
289
|
+
That mutual gate is the point: anyone can verify these provenance records with **no Sanning code in the trust path**, write your own verifier against the specs in the [source repository](https://github.com/sanning-io/proof/tree/main/specs), and the corpus will tell you if it is conformant.
|
|
289
290
|
|
|
290
291
|
## What this package deliberately does NOT do
|
|
291
292
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** A reading's verdict. `null`: the record is outside this vocabulary and this layer gives none. */
|
|
2
|
+
export type AgentVerdict = "verified" | "failed" | "incomplete";
|
|
3
|
+
/** One reason (section 10.1) and a JSON Pointer into the record for the field it is about. */
|
|
4
|
+
export interface AgentReason {
|
|
5
|
+
readonly reason: string;
|
|
6
|
+
readonly at: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentRecordReading {
|
|
9
|
+
readonly verdict: AgentVerdict | null;
|
|
10
|
+
/** The first reason of the verdict, when the verdict is not `verified`. */
|
|
11
|
+
readonly reason?: string;
|
|
12
|
+
/** Where `reason` is about. */
|
|
13
|
+
readonly at?: string;
|
|
14
|
+
/** Every reason, in step order, when the verdict is not `verified`. */
|
|
15
|
+
readonly reasons?: readonly AgentReason[];
|
|
16
|
+
/** Findings in the order step 6, then steps 7a to 7j, produce them. */
|
|
17
|
+
readonly findings: string[];
|
|
18
|
+
/**
|
|
19
|
+
* For each `listed_record_absent` reason, in order, the `event_id` the closing record
|
|
20
|
+
* lists and the verifier does not hold. Not part of the reading as `expected.json`
|
|
21
|
+
* spells it: present only where there is one, and not enumerable, so `JSON.stringify`
|
|
22
|
+
* leaves it out. The claims name each record by it.
|
|
23
|
+
*/
|
|
24
|
+
readonly absent?: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
/** A record as the verifier holds it after the envelope layer: its envelope's `event_id` and key. */
|
|
27
|
+
export interface AgentHeldRecord {
|
|
28
|
+
readonly event_id: string;
|
|
29
|
+
readonly public_key: string;
|
|
30
|
+
readonly record: unknown;
|
|
31
|
+
}
|
|
32
|
+
/** The `sanning.events/v2` types outside the agent vocabulary (§3), read by the profile and run-records.md. */
|
|
33
|
+
export declare const RECORD_TYPES_OUTSIDE_AGENT: readonly string[];
|
|
34
|
+
/** The `agent.*` types of each vocabulary version this kernel knows, keyed by `extras.event_schema`. With RECORD_TYPES_OUTSIDE_AGENT they are that version's closed set. */
|
|
35
|
+
export declare const AGENT_EVENT_TYPES: Readonly<Record<string, readonly string[]>>;
|
|
36
|
+
export declare const INCOMPLETE_REASONS: ReadonlySet<string>;
|
|
37
|
+
/**
|
|
38
|
+
* One reading in the words both kernels print: the verdict, every reason and
|
|
39
|
+
* where, then the findings, which are reported beside the verdict and never
|
|
40
|
+
* move it. Byte-identical to `render_agent_reading` in the Python kernel.
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderAgentReading(reading: AgentRecordReading): string;
|
|
43
|
+
/** Steps 1 to 6 of agent-events.md §10: one record on its own. */
|
|
44
|
+
export declare function readAgentRecord(record: unknown): AgentRecordReading;
|
|
45
|
+
/**
|
|
46
|
+
* Steps 1 to 7 of agent-events.md §10: `subject` read with the records the
|
|
47
|
+
* verifier holds beside it. Asynchronous only because 7a hashes a field to
|
|
48
|
+
* compare it with a hidden one.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readAgentRecordWithHeld(subject: AgentHeldRecord, held?: readonly AgentHeldRecord[]): Promise<AgentRecordReading>;
|
|
51
|
+
/**
|
|
52
|
+
* Read a pile: each item through steps 1 to 7, with every item held beside it.
|
|
53
|
+
* Each item is a `sanning.events/v2` record whose envelope verified and whose
|
|
54
|
+
* record bytes bound to it. Step 1 runs on every item, so a type outside the
|
|
55
|
+
* closed set reads `failed` whatever its namespace. `null` for an item
|
|
56
|
+
* that is not an object and for one of the seven types outside this vocabulary
|
|
57
|
+
* with no `event_schema`, which step 1 gives no verdict unless its salt is in
|
|
58
|
+
* the wrong form. The item is set aside from its own pile: a record is never
|
|
59
|
+
* one of its own held records. The one reading `verify`, `case` and the bundle
|
|
60
|
+
* verifiers wire in.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readAgentRecords(items: readonly AgentHeldRecord[]): Promise<(AgentRecordReading | null)[]>;
|