@sanning/proof 0.10.0 → 0.11.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.
package/dist/index.js CHANGED
@@ -51,6 +51,40 @@ export { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRA
51
51
  // derivation to it, and diff its copy against ours. Additive — no existing
52
52
  // export changes.
53
53
  export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
54
+ // The declaration (src/declaration.ts): what a pack SAYS it holds, checked
55
+ // against what it does (evidence-bundle.md §5.4).
56
+ //
57
+ // Every other check in this kernel checks a record that is PRESENT, so a pack
58
+ // with half its records dropped verifies perfectly and an auditor cannot tell
59
+ // a full hand-over from a trimmed one. The declaration is the pack's own
60
+ // statement of what it was meant to contain, inside `body` and therefore under
61
+ // the bundle's signature. Any deviation FAILS and every deviation is NAMED; a
62
+ // pack that carries no declaration is `partial`, never `verified`; and a
63
+ // reading that rests on the UNSIGNED content handed in beside the pack is
64
+ // named on its own and carries the weaker word, because whoever assembles a
65
+ // case chose those bytes (proof#147).
66
+ //
67
+ // Pure: data in, data out, no I/O. `gradePack` takes the result as an
68
+ // optional fourth argument, so a surface gets one verdict; omitting it is the
69
+ // pre-§5.4 behavior, unchanged. Additive: no existing export changes.
70
+ export { DECLARATION_VERSION, checkDeclaration, declarationClaims, packRecords, renderContentFinding, renderDeclarationSummary, renderDeviation, renderNote, } from "./declaration.js";
71
+ // The case (src/case.ts): one verdict over SEVERAL packs, across organizations
72
+ // (anchor#198). The layer above the declaration, and the closure for what a
73
+ // declaration cannot reach.
74
+ //
75
+ // A pack a party assembles about itself is not self-proving about what that
76
+ // party LEFT OUT (envelope-spec.md §4.1 A15, evidence-bundle.md §5.4.2 rule 7),
77
+ // because the omission happens before any reader runs. Reading several parties'
78
+ // packs at once is what closes it: a `handoff.sent` with no counterpart is
79
+ // always named, and where a pack signed by the key it names is in the case, the
80
+ // reading says that pack holds no counterpart. Whether a party that presented
81
+ // no such pack recorded anything is not established (proof#143). `readCase`
82
+ // verifies every pack itself, offline, so it is async. Matching is on keys and hashes alone and NEVER on tenancy, so a pair from two
83
+ // organizations verifies (DL-60).
84
+ //
85
+ // Pure: data in, data out, no I/O. Additive: a new module, no existing export
86
+ // changes, and no new verdict word anywhere.
87
+ export { CASE_BODY_TYPES, CASE_EXCEPTION_KINDS, CASE_QUARANTINED_PROFILES, CASE_RECORD_PROFILES, CASE_VERDICTS, readCase, renderCaseSummary, renderException, } from "./case.js";
54
88
  // Chain reading (src/chain.ts): following the pointer every sanning.events/v2
55
89
  // record carries to the record before it, over the records ONE bundle presents.
56
90
  //
@@ -81,3 +115,19 @@ export { attestationSignatureToHex, composeExport } from "./compose.js";
81
115
  // sanning.agent.proof/v1 bundle. The CLI sniffs spec_version and routes here so
82
116
  // one verifier covers both agent and anchor bundles.
83
117
  export { AGENT_PROOF_SPEC_PREFIX, isAgentProofSpec, verifyAgentProofBundle } from "./agent-proof.js";
118
+ // The hand-off pairing rule (src/handoff.ts): which records are candidates to
119
+ // be the counterpart of a hand-off record, how a candidate set of more than one
120
+ // is ordered, and what a formed pair grades to (specs/envelope-spec.md §4.1
121
+ // v1.9, proof#132 and proof#136). The Python half is src/sanning_proof/handoff.py and the two
122
+ // answer identically on identical bytes. Additive: a new module, and no
123
+ // existing export changes.
124
+ export { CANDIDATE_NAMESPACE, DEFINED_EVENT_TYPES, NOT_A_HANDOFF_FINDING, isConformantHandoffRecord, readHandoff, readHandoffs, recordIsMalformed, } from "./handoff.js";
125
+ // A file part's BYTES (src/message-parts.ts): what a file part is, and what a
126
+ // receiver that HOLDS a part's bytes commits to (envelope-spec.md section 4.1
127
+ // v1.13, A18, proof#145). A receiver that holds the bytes hashes them and, on a
128
+ // mismatch, commits to the message AS IT HELD IT, so the pair fails on
129
+ // `message_hash` rather than verifying over bytes nobody checked. The Python
130
+ // half is src/sanning_proof/message_parts.py and the two are held together by
131
+ // cross-kernel/run_parts.sh. Pure: it neither canonicalizes nor hashes the
132
+ // message.
133
+ export { BYTES_FOR_UNKNOWN_PART, CONFLICTING_PART_BYTES, INLINE_BYTES_KEYS, MESSAGE_RESULT_ORDER, PARTS_NOT_AN_ARRAY, PART_ENCODING_MISMATCH, PART_INLINE_BYTES, PART_INLINE_MISMATCHED, PART_MALFORMED_DESCRIPTOR, PART_MATCHED, PART_MISMATCHED, PART_NOT_A_FILE, PART_SUPPLIED_TWICE, PART_UNCHECKED, REFUSAL_ORDER, commitHeldMessage, isFilePart, uncheckedHeldParts, } from "./message-parts.js";
@@ -0,0 +1,283 @@
1
+ /**
2
+ * The result of a part whose supplied bytes hash to its `content_hash` AND
3
+ * count to its `content_length`.
4
+ */
5
+ export declare const PART_MATCHED = "part_matched";
6
+ /**
7
+ * The result of a part whose supplied bytes miss either of those, with no
8
+ * respelling of them that explains the digest. The held-message substitution
9
+ * applies to this part, and it replaces BOTH values with the held bytes' own.
10
+ */
11
+ export declare const PART_MISMATCHED = "part_mismatched";
12
+ /**
13
+ * A file part whose bytes were not supplied, or whose supply was REFUSED. It is
14
+ * not checked and it is not clean: section 4.1 forbids a reader treating it as
15
+ * checked.
16
+ */
17
+ export declare const PART_UNCHECKED = "part_unchecked";
18
+ /**
19
+ * A `kind: "file"` part carrying its content inside the message, and ACCEPTED
20
+ * (proof#157, ratified by Phil 2026-09-22). It is committed by the MESSAGE
21
+ * ITSELF: `message_hash` is SHA-256 over the canonical bytes of the message
22
+ * object, so altering that content changes the message, changes the hash, and
23
+ * fails the pair. Nothing is hashed against it here and nothing is substituted,
24
+ * because there is nothing travelling beside the message for the message to
25
+ * disagree with. That is why the swapped-bytes attack cannot reach this shape.
26
+ */
27
+ export declare const PART_INLINE_BYTES = "part_inline_bytes";
28
+ /**
29
+ * An inline part whose `content_hash`, `content_length` or both DISAGREE with
30
+ * its inline content: no single reading of that content satisfies every claim
31
+ * the part writes beside it. The message's own commitment still governs and
32
+ * nothing is substituted; this names the SECOND claim that disagrees with it,
33
+ * which is a finding in its own right (proof#157).
34
+ *
35
+ * ONE name for both claims, exactly as `PART_MISMATCHED` is one name for both
36
+ * on a descriptor part. It was `part_inline_hash_mismatch` for one round of
37
+ * this unmerged PR, when only the hash was compared; a name that says "hash"
38
+ * about a result that also covers a length asserts more than its body does, so
39
+ * the name was re-cut with the rule (proof#156 second gate, F4).
40
+ *
41
+ * The check is deliberately permissive ACROSS readings: the raw UTF-8 of the
42
+ * value, its base64 decoding and its hex decoding are all tried, so this fires
43
+ * only where no reading agrees at all. It is not permissive BETWEEN claims: one
44
+ * reading has to satisfy all of them, because a hash that agrees under the
45
+ * base64 decoding beside a length that agrees under the raw text describes no
46
+ * blob that exists.
47
+ */
48
+ export declare const PART_INLINE_MISMATCHED = "part_inline_mismatched";
49
+ /**
50
+ * `kind: "file"` without a conformant `content_hash` and `content_length`. A
51
+ * sender's defect rather than a receiver's, so it is named rather than refused:
52
+ * the receiver did nothing wrong by holding it.
53
+ */
54
+ export declare const PART_MALFORMED_DESCRIPTOR = "part_malformed_descriptor";
55
+ /**
56
+ * Any other part. It is committed as ordinary JSON content and this rule checks
57
+ * nothing about it.
58
+ */
59
+ export declare const PART_NOT_A_FILE = "part_not_a_file";
60
+ /**
61
+ * REFUSAL of ONE supplied entry. Bytes were supplied for no part of the
62
+ * message, or for a part this rule cannot check. The caller and the message
63
+ * disagree about what was sent, and hashing bytes against a part that is not
64
+ * there establishes nothing. Where the entry names a part that EXISTS but is
65
+ * not checkable, the refusal carries that part's index and the part keeps the
66
+ * result it classified to; where it names no part at all, the refusal carries
67
+ * the entry position and no part. Neither stops the record being written.
68
+ */
69
+ export declare const BYTES_FOR_UNKNOWN_PART = "bytes_for_unknown_part";
70
+ /**
71
+ * REFUSAL. Two DIFFERENT byte strings were supplied for one part. A receiver
72
+ * holding two answers has not established which one it held.
73
+ */
74
+ export declare const CONFLICTING_PART_BYTES = "conflicting_part_bytes";
75
+ /**
76
+ * REFUSAL of ONE supplied entry. The supplied bytes miss the descriptor, but a
77
+ * base64 or hex RESPELLING of them in EITHER direction hashes to it: either the
78
+ * bytes DECODE to the part (the receiver's stack never decoded what it was
79
+ * handed) or they ENCODE to it (the receiver's stack decoded bytes the sender
80
+ * committed in their encoded spelling, which is what a single-line DER
81
+ * attachment, a PEM body and a JWT all are). That is a coding defect in the
82
+ * receiver, not a tamper by the sender: a proto3 JSON transcoder re-encodes
83
+ * base64 URL-safe and unpadded while behaving honestly, and under the silence
84
+ * this amendment removes it produced a permanent tamper record against a sender
85
+ * who did nothing. Recording evidence out of a receiver's own bug is the one
86
+ * outcome worse than recording nothing, so BOTH directions are guarded
87
+ * (proof#156 gate): guarding only the decoding direction left the other half of
88
+ * one honest stack's behavior reading as a tamper.
89
+ */
90
+ export declare const PART_ENCODING_MISMATCH = "part_encoding_mismatch";
91
+ /**
92
+ * A MESSAGE-LEVEL result. The message carries a `parts` key whose value is
93
+ * present and is not an array, which is how proto3 JSON encodes a map and so is
94
+ * reachable from the same transport family the motivating example comes from.
95
+ * Section 4.1 asks for one result per part; a message shaped like this has
96
+ * part-shaped content and no parts, and before proof#156's gate it produced
97
+ * zero readings and an accepted record. It is named rather than refused: the
98
+ * receiver did nothing wrong by holding it, and nothing in it is checked. A
99
+ * `parts` written as `null` is an ABSENT `parts` and is not this, on A14's
100
+ * ruling that an absent value and one written as `null` name one thing.
101
+ */
102
+ export declare const PARTS_NOT_AN_ARRAY = "parts_not_an_array";
103
+ /**
104
+ * NOTE, not a refusal. One part supplied more than once with IDENTICAL bytes is
105
+ * one supply stated twice, and the part is checked normally. Different bytes for
106
+ * one part are `CONFLICTING_PART_BYTES`.
107
+ */
108
+ export declare const PART_SUPPLIED_TWICE = "part_supplied_twice";
109
+ /**
110
+ * The order refusals are reported in, fixed so two kernels report one list
111
+ * rather than two orderings of it.
112
+ */
113
+ export declare const REFUSAL_ORDER: readonly ["bytes_for_unknown_part", "conflicting_part_bytes", "part_encoding_mismatch"];
114
+ /**
115
+ * The order message-level results are reported in, for the same reason as
116
+ * `REFUSAL_ORDER`.
117
+ */
118
+ export declare const MESSAGE_RESULT_ORDER: readonly ["parts_not_an_array"];
119
+ /**
120
+ * The keys that make a part carry its content INLINE, in the order they are
121
+ * read. Named rather than inferred: `bytes` is A2A's spelling of an inline file
122
+ * part, and `data` and `content` are the two other spellings in the wild. The
123
+ * FIRST of them present with a value that is not `null` is the inline content,
124
+ * so a part spelling it twice reads one way in both kernels.
125
+ *
126
+ * A key present with `null` is an ABSENT inline value, on A14's ruling that an
127
+ * absent value and one written as `null` name one thing. That matters: an A2A
128
+ * `FilePart` with `uri` set carries `"bytes": null`, and so does proto3 JSON,
129
+ * and reading those as inline made a conformant DESCRIPTOR part uncheckable
130
+ * (proof#156 gate). They fall through to the descriptor rule, which is what
131
+ * they are.
132
+ */
133
+ export declare const INLINE_BYTES_KEYS: readonly ["bytes", "content", "data"];
134
+ /**
135
+ * The largest integer a double carries exactly, the same bound section 4.1 puts
136
+ * on `event.sequence`: a `content_length` outside it is a value the two kernels
137
+ * would read as two numbers.
138
+ */
139
+ export declare const MAX_SAFE_INTEGER: number;
140
+ /**
141
+ * One byte string a receiver holds, and the part it holds it for.
142
+ *
143
+ * `part` is an INDEX into the message's `parts` array. It is an index rather
144
+ * than a content hash on purpose: keying the supply by the value the check is
145
+ * about to grade would mean a receiver could only supply bytes it had already
146
+ * agreed with, which is the whole defect stated backwards.
147
+ */
148
+ export interface SuppliedPart {
149
+ part: unknown;
150
+ data: Uint8Array;
151
+ }
152
+ /** What the receiver reports for one part of the message, one per part. */
153
+ export interface PartReading {
154
+ index: number;
155
+ result: string;
156
+ }
157
+ /**
158
+ * A caller error. Nothing is written, nothing is signed, no record made.
159
+ *
160
+ * `part` is the part index where the refusal is about a part of the message,
161
+ * and `entry` is the POSITION of the offending supplied entry in the sequence
162
+ * the caller passed. The position is recorded rather than the value the caller
163
+ * named, so a caller that names a part with a float or a string cannot make two
164
+ * kernels serialize one reading two ways.
165
+ */
166
+ export interface Refusal {
167
+ name: string;
168
+ part: number | null;
169
+ entry: number | null;
170
+ }
171
+ /**
172
+ * Something worth reporting that refuses nothing and checks nothing out. Same
173
+ * shape as a `Refusal` and carried in its own list, so no caller can read a
174
+ * note as a refusal by looking only at a name.
175
+ */
176
+ export type Note = Refusal;
177
+ /**
178
+ * The message a receiver commits to, and everything it found on the way.
179
+ *
180
+ * `heldMessage` is ALWAYS the message the receiver commits to and is never
181
+ * null: a refusal is scoped to the supply it is about and never denies the
182
+ * receiver a record (proof#156 gate). The caller commits `event.message_hash`
183
+ * as SHA-256 over its JCS (RFC 8785) canonical bytes, which is the rule section
184
+ * 4.1 already states and the reason this module does not hash anything itself.
185
+ *
186
+ * `messageResults` carries the message-level named results, in
187
+ * `MESSAGE_RESULT_ORDER`. It is empty for every message whose shape the rule
188
+ * reads.
189
+ *
190
+ * `substituted` carries the indexes of the parts whose descriptor the receiver
191
+ * replaced with the held bytes' own hash and length. It is empty for an honest
192
+ * hand-off, and where it is not empty the receiving side writes
193
+ * `outcome: "rejected"`.
194
+ */
195
+ export interface HeldMessage {
196
+ parts: PartReading[];
197
+ messageResults: string[];
198
+ refusals: Refusal[];
199
+ notes: Note[];
200
+ heldMessage: unknown | null;
201
+ substituted: number[];
202
+ }
203
+ /**
204
+ * Whether this part is one this rule CHECKS against bytes travelling BESIDE the
205
+ * message: a conformant descriptor part. Exactly the three conditions section
206
+ * 4.1 states, in the order that keeps inline content out. An INLINE part is
207
+ * accepted and committed by the message itself (proof#157), so there is nothing
208
+ * here for supplied bytes to be checked against and this answers `false`.
209
+ */
210
+ export declare function isFilePart(part: unknown): boolean;
211
+ /**
212
+ * What a receiver commits to, given the message and the bytes it holds.
213
+ *
214
+ * `supplied` is the bytes the receiver actually holds, each named by the index
215
+ * of the part it belongs to. An empty list is the common case, a receiver that
216
+ * holds no part's bytes, and it reads exactly as section 4.1 read before this
217
+ * amendment: every file part is `PART_UNCHECKED` and the message a receiver
218
+ * commits to is the message it was given.
219
+ *
220
+ * Where a part's bytes hash to something other than its `content_hash`, or
221
+ * where their count is not its `content_length`, the returned `heldMessage`
222
+ * carries that part's `content_hash` and `content_length` replaced by the held
223
+ * bytes' own. The receiver commits `event.message_hash` over THAT, so its
224
+ * record and the sender's disagree and the pair fails on `message_hash`.
225
+ *
226
+ * A refusal is scoped to the supplied entry it is about: the part it names is
227
+ * not checked and not substituted, every other part reads as it would have, and
228
+ * `heldMessage` is returned either way. Nothing a caller or a sender can do
229
+ * denies the receiver a record.
230
+ */
231
+ export declare function commitHeldMessage(message: unknown, supplied?: readonly SuppliedPart[]): Promise<HeldMessage>;
232
+ /**
233
+ * The parts the receiver HELD bytes for and did NOT check.
234
+ *
235
+ * The gap between "nothing was substituted" and "every part is clean", named
236
+ * so a caller does not have to re-derive it from the refusal list (proof#156
237
+ * second gate, F5). A part is in here where a refusal NAMES it and its reading
238
+ * is one of `HELD_UNCHECKED_RESULTS`: the receiver had bytes for that part,
239
+ * the rule declined to grade them, and NOTHING about that reaches the
240
+ * committed record.
241
+ *
242
+ * THE RULE, in one sentence, because a filter is not a rule. A part is in here
243
+ * where the receiver held bytes for it AND the message itself does not commit
244
+ * its content. `PART_UNCHECKED` is a refused supply;
245
+ * `PART_MALFORMED_DESCRIPTOR` is a part whose content travels BESIDE the
246
+ * message under a claim no receiver can use, so the receiver's bytes are
247
+ * exactly as ungraded and the record says as little. The first cut of this
248
+ * function omitted the second, which the third gate measured: a `kind: "file"`
249
+ * part with a non-conformant `content_hash` or `content_length`, bytes held,
250
+ * read `[]` here, so a caller following the rule this module prescribes wrote
251
+ * `accepted` over a part it held bytes for and never checked. That is the
252
+ * class the mechanism exists to close, and Phil named it in proof#162's
253
+ * ratification as the gap that decision inherits unless it is closed here.
254
+ *
255
+ * Three results are excluded ON PURPOSE. An inline part (`PART_INLINE_BYTES`,
256
+ * `PART_INLINE_MISMATCHED`) and a non-file part (`PART_NOT_A_FILE`) carry
257
+ * their content INSIDE the message, and `message_hash` commits it on both
258
+ * sides: bytes supplied beside one of those grade nothing and hide nothing,
259
+ * because a reader of either pack reads the content itself. Naming them would
260
+ * fire the caller's obligation on parts the swapped-bytes attack cannot reach
261
+ * at all.
262
+ *
263
+ * Why it is worth a function rather than a note. Delivering a tampered copy of
264
+ * a part alone is `part_mismatched`, is substituted, and fails the pair on
265
+ * `message_hash`. Delivering the tampered copy AND the true one is
266
+ * `conflicting_part_bytes`, is never substituted, and the pair VERIFIES, so
267
+ * adding data suppresses detection and the only trace is a refusal the
268
+ * receiving side publishes at its own discretion.
269
+ *
270
+ * NOT the same as "this part reads `PART_UNCHECKED`". A receiver that holds no
271
+ * bytes at all reads every file part that way, and that is the COMMON CASE,
272
+ * unchanged by A18 and not a reason to withhold `accepted`. Only a part whose
273
+ * bytes the receiver actually held and could not grade is in here.
274
+ *
275
+ * What this does NOT do: it cannot stop a caller writing `accepted` anyway.
276
+ * This module reads and does not write the record. proof#162 ratified what the
277
+ * record layer does instead (Phil, 2026-09-22): `accepted` keeps its meaning
278
+ * and the COUNT of these parts rides in an additive `context` key, REQUIRED
279
+ * when it is not zero, enforced against SDKs through conformance vectors. So a
280
+ * part missing from this list is a part no record built on that rule will ever
281
+ * mention.
282
+ */
283
+ export declare function uncheckedHeldParts(reading: HeldMessage): number[];