@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,36 +1,25 @@
1
1
  /** A caller-built map of disclosed bytes, in either shape it can take. */
2
2
  export type SideContentMap = Readonly<Record<string, Uint8Array | string | undefined>> | ReadonlyMap<string, Uint8Array | string | undefined>;
3
- /** One event's entry, and — separately — whether an entry existed at all. */
3
+ /** One event's entry, and separately whether an entry existed at all. */
4
4
  export interface SideEntry {
5
- /** An entry EXISTS for this id. Never inferred from the value. */
5
+ /** Whether an entry exists for this id. Never inferred from the value. */
6
6
  offered: boolean;
7
- /** What it held, read without ever invoking an accessor. */
7
+ /** What it held, read without invoking an accessor. */
8
8
  value: Uint8Array | string | undefined;
9
9
  }
10
- /** Look up one event's out-of-band disclosure. The rule, stated once. */
10
+ /** Look up one event's out-of-band disclosure. */
11
11
  export declare function sideContentEntry(content: SideContentMap | null | undefined, eventId: string): SideEntry;
12
- /** Whether the map carries a usable entry for this id. The same rule. */
12
+ /** Whether the map carries an entry for this id. */
13
13
  export declare function hasSideContent(content: SideContentMap | null | undefined, eventId: string): boolean;
14
14
  /**
15
- * The ids the map actually carries — the same rule applied to every key, so a
16
- * key that is not an entry cannot be reported as surplus disclosure either.
15
+ * The ids the map carries, by the same rule, so a key that is not an entry is
16
+ * never reported as surplus disclosure.
17
17
  */
18
18
  export declare function sideContentKeys(content: SideContentMap | null | undefined): string[];
19
19
  /**
20
- * Whether an event's IN-BODY `events[].content` field is a disclosure.
21
- *
22
- * The other half of the same question, and it broke the same way. `content`
23
- * is an OPTIONAL field, so `null` is how a producer's serializer spells its
24
- * absence — Go's `json.Marshal` on a nil pointer without `omitempty`, Python's
25
- * `json.dumps(None)`. A bare `!== undefined` marks such an event OFFERED while
26
- * no bytes are selected (only a `string` decodes), and the kernel then splits
27
- * against itself: `verifyEvidenceBundle` reports `contentOk: null` and rolls up
28
- * VERIFIED, while `disclosedContent` reports FAILED — "content was disclosed
29
- * for this event but is not a decodable lowercase-hex string". One kernel, one
30
- * bundle, two verdicts, reachable from any conforming producer.
31
- *
32
- * Non-nullish and not a string — a number, an object, a hex string that is not
33
- * hex — remains a real, unusable disclosure and stays a finding, exactly as on
34
- * the side-input path.
20
+ * Whether an event's in-body `events[].content` is a disclosure: anything
21
+ * non-nullish. `null` is how serializers spell an absent optional field, and
22
+ * treating it as offered would split the bundle verdict from
23
+ * `disclosedContent`.
35
24
  */
36
25
  export declare function inBodyContentOffered(content: unknown): boolean;
@@ -1,54 +1,22 @@
1
- // ONE answer to "did the holder actually supply bytes for this event_id?"
1
+ // The one answer to "did the holder supply bytes for this event_id?", shared by
2
+ // the verifier and the grader so the two cannot disagree.
2
3
  //
3
- // WHY THIS IS A MODULE AND NOT TWO PRIVATE HELPERS.
4
- // The kernel asks that question in two places — the verifier, deciding whether
5
- // an event's content was offered at all (`evidence.ts`), and the grader,
6
- // accounting which records a pack disclosed (`grading.ts`). It had two answers.
7
- // On one and the same content map the grader said PRESENT while the verifier
8
- // said nothing was offered, so `gradePack` returned `incomplete` with ZERO
9
- // causes: a surface saying "incomplete" and unable to say why. That is exactly
10
- // the outcome `grading.ts`'s own comment claims was removed, reappearing
11
- // because the fix landed on one of the two definitions.
4
+ // An entry exists when the map carries an own, enumerable, data property (or
5
+ // `Map` key) holding something other than `null`/`undefined`:
12
6
  //
13
- // Two implementations of one verification rule is the defect `grading.ts` was
14
- // extracted into the kernel to end. A rule stated once cannot drift; a rule
15
- // stated twice already has.
7
+ // own `event_id` is producer-chosen, and `constructor` or `__proto__`
8
+ // must not resolve through the prototype chain.
9
+ // enumerable what `Object.keys` shows is the map the caller built.
10
+ // data an accessor is never invoked: a getter can throw, and a
11
+ // verifier does not raise on adversarial input.
12
+ // non-nullish `undefined` is how `Object.fromEntries` spells "no entry".
13
+ // Anything else, even unusable, is a disclosure and a finding.
16
14
  //
17
- // ── THE RULE ────────────────────────────────────────────────────────────────
18
- //
19
- // An entry exists for `eventId` when the map carries an OWN, ENUMERABLE, DATA
20
- // property (or `Map` key) holding something other than `null`/`undefined`.
21
- // This is the STRICTER of the two definitions that were in the kernel — the
22
- // verifier's — and it is stricter on purpose. Every clause was paid for:
23
- //
24
- // own — an `event_id` is PRODUCER-CHOSEN, and every key on
25
- // `Object.prototype` is a legal one: `constructor`,
26
- // `toString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf`,
27
- // `propertyIsEnumerable`, `toLocaleString`, `__proto__`. An
28
- // indexed lookup walks the prototype chain and resolves every
29
- // one of them, so absent evidence read as good evidence.
30
- // enumerable — what a caller means by "the map I built" is what
31
- // `Object.keys` shows; nothing hidden behind it counts.
32
- // data — an accessor is NEVER invoked. A getter can throw, and this
33
- // kernel's standing rule is that a verifier does not raise on
34
- // adversarial input. Not hypothetical: a plain-object map that
35
- // has taken a `__proto__` write carries `Uint8Array.prototype`
36
- // as its prototype, and reading `byteLength`, `length`,
37
- // `buffer` or `byteOffset` off a plain object through it throws
38
- // a TypeError. Reading a descriptor cannot.
39
- // non-nullish — `{ [id]: undefined }` is how `Object.fromEntries` and friends
40
- // spell "no entry", and `new Map([[id, undefined]])` is the
41
- // same statement in the other shape. Anything else non-nullish
42
- // — a number, an object, a string that is not hex — IS a real,
43
- // unusable disclosure and stays a finding.
44
- //
45
- // `offered` is what separates "nothing was disclosed" (undetermined) from
46
- // "something was disclosed and is unusable" (a finding), and absent disclosure
47
- // is undetermined, not failed (evidence-bundle.md §5.1). So it is returned
48
- // SEPARATELY from the value and is never inferred from it: `value !== undefined`
49
- // is the conflation this module exists to remove, in both directions.
15
+ // `offered` is returned separately from the value and never inferred from it:
16
+ // nothing disclosed is undetermined, something unusable is a finding
17
+ // (evidence-bundle.md §5.1).
50
18
  const ABSENT = { offered: false, value: undefined };
51
- /** Look up one event's out-of-band disclosure. The rule, stated once. */
19
+ /** Look up one event's out-of-band disclosure. */
52
20
  export function sideContentEntry(content, eventId) {
53
21
  if (content === undefined || content === null)
54
22
  return ABSENT;
@@ -62,13 +30,13 @@ export function sideContentEntry(content, eventId) {
62
30
  const value = desc.value;
63
31
  return value === undefined || value === null ? ABSENT : { offered: true, value };
64
32
  }
65
- /** Whether the map carries a usable entry for this id. The same rule. */
33
+ /** Whether the map carries an entry for this id. */
66
34
  export function hasSideContent(content, eventId) {
67
35
  return sideContentEntry(content, eventId).offered;
68
36
  }
69
37
  /**
70
- * The ids the map actually carries — the same rule applied to every key, so a
71
- * key that is not an entry cannot be reported as surplus disclosure either.
38
+ * The ids the map carries, by the same rule, so a key that is not an entry is
39
+ * never reported as surplus disclosure.
72
40
  */
73
41
  export function sideContentKeys(content) {
74
42
  if (content === undefined || content === null)
@@ -77,21 +45,10 @@ export function sideContentKeys(content) {
77
45
  return keys.filter((id) => hasSideContent(content, id));
78
46
  }
79
47
  /**
80
- * Whether an event's IN-BODY `events[].content` field is a disclosure.
81
- *
82
- * The other half of the same question, and it broke the same way. `content`
83
- * is an OPTIONAL field, so `null` is how a producer's serializer spells its
84
- * absence — Go's `json.Marshal` on a nil pointer without `omitempty`, Python's
85
- * `json.dumps(None)`. A bare `!== undefined` marks such an event OFFERED while
86
- * no bytes are selected (only a `string` decodes), and the kernel then splits
87
- * against itself: `verifyEvidenceBundle` reports `contentOk: null` and rolls up
88
- * VERIFIED, while `disclosedContent` reports FAILED — "content was disclosed
89
- * for this event but is not a decodable lowercase-hex string". One kernel, one
90
- * bundle, two verdicts, reachable from any conforming producer.
91
- *
92
- * Non-nullish and not a string — a number, an object, a hex string that is not
93
- * hex — remains a real, unusable disclosure and stays a finding, exactly as on
94
- * the side-input path.
48
+ * Whether an event's in-body `events[].content` is a disclosure: anything
49
+ * non-nullish. `null` is how serializers spell an absent optional field, and
50
+ * treating it as offered would split the bundle verdict from
51
+ * `disclosedContent`.
95
52
  */
96
53
  export function inBodyContentOffered(content) {
97
54
  return content !== undefined && content !== null;
package/dist/types.d.ts CHANGED
@@ -32,4 +32,21 @@ export interface VerificationResult {
32
32
  contentHashOk: boolean | null;
33
33
  contentRole: ContentRole | null;
34
34
  errors: string[];
35
+ /**
36
+ * Why `ok` is false: the worst of `faults`, or `null` where `ok` is true or only the
37
+ * content bind failed. A caller maps each fault to a claim (`ENVELOPE_FAULT_KINDS`) rather
38
+ * than reading `ok` alone: a signature or a binding that fails disproves, a key or an
39
+ * envelope that cannot be read and a version this verifier does not know leave a claim not
40
+ * proven, and a field that breaks the format under a signature that holds is the
41
+ * producer's error.
42
+ */
43
+ fault: EnvelopeFault | null;
44
+ /**
45
+ * Every fault found, in `ENVELOPE_FAULTS` order. A fault that leaves one claim not proven
46
+ * never hides a disproof found beside it.
47
+ */
48
+ faults: EnvelopeFault[];
35
49
  }
50
+ /** Why an envelope does not verify, worst first: the disproofs, then what is not proven. */
51
+ export declare const ENVELOPE_FAULTS: readonly ["signature_failed", "payload_unbound", "nonconforming", "not_an_envelope", "key_unreadable", "spec_version_unknown"];
52
+ export type EnvelopeFault = (typeof ENVELOPE_FAULTS)[number];
package/dist/types.js CHANGED
@@ -1,5 +1,19 @@
1
- // Wire types for Sanning Verifiable Event Envelopes (sanning-io/proof
2
- // specs/envelope-spec.md). We model only the fields the verifier consumes;
3
- // unknown fields are preserved by structural typing (envelopes are verified
4
- // over their canonical bytes, not this interface).
5
- export {};
1
+ // Wire types for Sanning Verifiable Event Envelopes (specs/envelope-spec.md).
2
+ // Only the fields the verifier consumes are modelled; envelopes are verified
3
+ // over their canonical bytes, not this interface.
4
+ /** Why an envelope does not verify, worst first: the disproofs, then what is not proven. */
5
+ export const ENVELOPE_FAULTS = [
6
+ // The Ed25519 signature does not hold.
7
+ "signature_failed",
8
+ // `payload_hash` is 64 hex characters that do not match the bytes it commits to.
9
+ "payload_unbound",
10
+ // The signature holds over a version this verifier knows, and a field breaks the
11
+ // envelope format (a missing `payload_hash` included).
12
+ "nonconforming",
13
+ // Not an object, or an object with none of the envelope's own members.
14
+ "not_an_envelope",
15
+ // `public_key` is not 64 hex characters: no signature can be checked.
16
+ "key_unreadable",
17
+ // A `spec_version` this verifier does not know.
18
+ "spec_version_unknown",
19
+ ];
@@ -1,144 +1,54 @@
1
- import type { ContentRole, Envelope, VerificationResult, VerifyOptions } from "./types.js";
1
+ import type { ContentRole, Envelope, EnvelopeFault, VerificationResult, VerifyOptions } from "./types.js";
2
2
  export declare const ACCEPTED_SPEC_MAJORS: readonly string[];
3
3
  export declare const MAX_CANONICAL_DEPTH = 128;
4
4
  export declare function exceedsDepth(value: unknown, max: number): boolean;
5
5
  /**
6
- * The bytes an envelope's inline `payload` commits to: `JCS(payload)`. The ONE
7
- * place this is spelled: `verifyEnvelope` binds `payload_hash` against these
8
- * bytes, and the chain links rule 11 reads (`readChainLinks`) read an inline
9
- * record through them, so the binding rule and the linkage rule cannot
10
- * disagree about which bytes a record is. Throws where the payload cannot be
11
- * canonicalized. The twin is Python's `inline_payload_bytes`.
6
+ * The bytes an envelope's inline `payload` commits to: `JCS(payload)`. The one
7
+ * place this is defined, so payload binding and chain linkage agree on a
8
+ * record's bytes. Throws where the payload cannot be canonicalized.
12
9
  */
13
10
  export declare function inlinePayloadBytes(envelope: {
14
11
  payload: unknown;
15
12
  }): Uint8Array;
16
13
  export declare function jcs(value: unknown): string;
17
- /**
18
- * Read bytes that claim to be `jcs(value)` (RFC 8785), strictly. The Python
19
- * twin is `jcs_loads` in `src/sanning_proof/canonicalize.py`, and every site in
20
- * either kernel that reads bytes a producer signed goes through one of the two.
21
- *
22
- * ONE RULE, SPELLED OUT, BECAUSE TWO BUILT-INS ARE NOT ONE RULE. Five checks
23
- * carry it, none of them left to a decoder's defaults: strict UTF-8, no leading
24
- * byte-order mark, no non-finite literal (this kernel's `JSON.parse` rejects
25
- * `NaN`, `Infinity` and `-Infinity` natively, and Python names them in a
26
- * `parse_constant` hook), no object that repeats a member name
27
- * (`DuplicateKeyError`, proof#144), and no unpaired surrogate in the parsed
28
- * value.
29
- *
30
- * THE FOURTH CHECK IS NOT THE FIRST ONE TWICE. The strict decode refuses a
31
- * surrogate written as raw bytes; it cannot see one written as the ASCII
32
- * escape `\ud800`, which `JSON.parse` turns into a lone code unit AFTER the
33
- * decode has passed. `"c\ud800x"` is pure ASCII on the wire, so this reader
34
- * accepted it while its own text above said it does not, and one poisoned
35
- * record then deleted the Python kernel's whole chain reading (the gate on
36
- * proof#154). Both spellings are the same malformed record, and both are
37
- * refused here.
38
- *
39
- * `new TextDecoder()` alone is none of that. It replaces every invalid sequence
40
- * with U+FFFD and strips a BOM. Python's `json.loads(<bytes>)` is not it
41
- * either, in the opposite direction: CPython sniffs the encoding and decodes
42
- * with `errors="surrogatepass"`, so it strips a UTF-8 BOM, accepts a UTF-16 or
43
- * UTF-32 body, and passes a lone surrogate and a CESU-8 pair through. Both
44
- * defaults have shipped a split verdict on one pack: a raw `0x80` inside a
45
- * stamp body read `verified` here and `failed` there (gate 4), and then a
46
- * `BOM + JCS(record)` checkpoint read `verified` there and `mixed` here (gate
47
- * 5), both on proof#140. JCS is defined over UTF-8 and I-JSON (RFC 7493)
48
- * forbids a lone surrogate, so each of those inputs is MALFORMED rather than
49
- * another spelling of the same record.
50
- *
51
- * Throws for anything that is not conformant; every call site treats a throw as
52
- * "this reader cannot read these bytes" and names the check that failed.
53
- */
54
- /**
55
- * Bytes as strict UTF-8, a leading byte-order mark KEPT as a character.
56
- * `utf8_strict` in the Python twin's `canonicalize.py` is the same rule, and
57
- * `jcsParse` below performs the identical decode. Throws on bytes that are not
58
- * UTF-8.
59
- */
14
+ /** Bytes as strict UTF-8, a leading byte-order mark kept as a character. Throws on invalid UTF-8. */
60
15
  export declare function utf8Strict(bytes: Uint8Array): string;
61
- export declare function jcsParse(bytes: Uint8Array): unknown;
62
16
  /**
63
- * Whether the bytes nest arrays and objects deeper than `max`, read from the
64
- * bytes before any parser sees them: the greatest number of `[` and `{` open
65
- * at once outside strings. A `"` opens a string and the next `"` no `\`
66
- * escapes closes it; a `]` or `}` outside a string closes one open level, if
67
- * one is open. Defined for every byte string, JSON or not. The Python twin is
68
- * `exceeds_text_depth` in `canonicalize.py`.
17
+ * Read bytes that claim to be `jcs(value)`, strictly: valid UTF-8, no leading
18
+ * BOM, no non-finite literal, no repeated member name, and no unpaired
19
+ * surrogate (including one written as a `\ud800` escape). Every site that reads
20
+ * bytes a producer signed goes through this or its Python twin `jcs_loads`, so
21
+ * the kernels cannot split on decoder defaults.
69
22
  *
70
- * On the bytes because a parser's own limit is its host's: CPython 3.10
71
- * stops near 1,000 levels, `JSON.parse` and CPython 3.13 read far deeper, Go's
72
- * decoder stops at 10,000 (enclave-attestation section 2.1).
73
- */
74
- export declare function exceedsTextDepth(bytes: Uint8Array, max: number): boolean;
75
- /**
76
- * Whether a parsed value holds a number no IEEE 754 double holds, at any
77
- * depth. `JSON.parse` reads `1e400`, and an integer of 400 digits, as
78
- * `Infinity`. Iterative: the value is a producer's. The Python twin is
79
- * `has_non_finite_number`.
23
+ * @throws When the bytes are not conformant.
80
24
  */
81
- export declare function hasNonFiniteNumber(value: unknown): boolean;
82
- /**
83
- * The deepest a JSON text an enclave reader parses may nest, and the depth
84
- * every kernel canonicalizes to (`MAX_CANONICAL_DEPTH`).
85
- */
86
- export declare const MAX_READ_DEPTH = 128;
87
- /**
88
- * `jcsParse`, within the reader limits of enclave-attestation section 2.1:
89
- * the bytes nest at most `MAX_READ_DEPTH` levels, checked on the bytes first,
90
- * and every number in the value is a finite double. Throws past either limit,
91
- * and wherever `jcsParse` throws. The Python twin is `jcs_loads_within_limits`.
92
- */
93
- export declare function jcsParseWithinLimits(bytes: Uint8Array): unknown;
25
+ export declare function jcsParse(bytes: Uint8Array): unknown;
94
26
  /**
95
- * A JSON object that names one member twice (I-JSON, RFC 7493 section 2.3).
96
- * The Python twin is `DuplicateKey` in `canonicalize.py`, and the message is
97
- * the same kernel wording in both: `duplicate key ` and the name as
98
- * `renderKey` spells it.
99
- *
100
- * JCS (RFC 8785) is defined over I-JSON, and I-JSON forbids duplicate member
101
- * names, so bytes that repeat a name are MALFORMED rather than another spelling
102
- * of the last value. `JSON.parse` and Python's `json.loads` both keep the last
103
- * value and say nothing, which let a bundle carrying `"spec_version":"junk"`
104
- * before its real `spec_version` read `verified` in both kernels (proof#144).
27
+ * A JSON object that names one member twice. I-JSON (RFC 7493 §2.3) forbids it,
28
+ * so the bytes are malformed rather than a spelling of the last value.
105
29
  */
106
30
  export declare class DuplicateKeyError extends Error {
107
31
  readonly key: string;
108
32
  constructor(key: string);
109
33
  }
110
34
  /**
111
- * A member name as a quoted, pure-ASCII string, identically in both kernels
112
- * (`render_key` in the Python twin). Printable ASCII other than `"` and `\` is
113
- * kept; `"` and `\` are backslash-escaped; every other UTF-16 code unit is
114
- * `\uXXXX` in lower-case hex. A rendering longer than 64 characters is cut
115
- * there and `...` follows the closing quote.
35
+ * A member name as a quoted, pure-ASCII string, identical in both kernels.
36
+ * Printable ASCII is kept (`"` and `\` escaped), every other code unit is
37
+ * `\uXXXX`, and anything past 64 characters is cut with `...`.
116
38
  */
117
39
  export declare function renderKey(key: string): string;
118
40
  /**
119
- * `JSON.parse(text)`, refusing an object that repeats a member name. The Python
120
- * twin is `loads_refusing_duplicates` in `canonicalize.py`.
41
+ * `JSON.parse(text)`, refusing an object that repeats a member name. Parses
42
+ * first, so non-JSON fails as JSON, then rescans for repeats.
121
43
  *
122
- * `JSON.parse` cannot see a repeat, because it keeps the last value, so the
123
- * text is scanned once more AFTER it parsed. The parse runs first, so text
124
- * that is not JSON at all fails as JSON and never as a duplicate, as it does in
125
- * the Python twin. Throws `DuplicateKeyError`.
44
+ * @throws DuplicateKeyError
126
45
  */
127
46
  export declare function parseRefusingDuplicates(text: string): unknown;
128
47
  /**
129
- * The repeated member name to report for text `JSON.parse` has already
130
- * accepted, or `undefined` when no object repeats one.
131
- *
132
- * WHICH ONE, because both kernels must name the same one. Objects are checked
133
- * as they CLOSE, innermost first, which is the order Python's
134
- * `object_pairs_hook` sees them in; inside one object the name reported is the
135
- * first whose repeat comes earliest. Names compare after unescaping, so `"a"`
136
- * and `"\u0061"` are one name, as they are to both parsers.
137
- *
138
- * ITERATIVE, with an explicit stack, for the reason `rejectUnpairedSurrogates`
139
- * is: this walks bytes a producer wrote. It relies on the text being valid
140
- * JSON, which is why it runs only after `JSON.parse` succeeded: outside a
141
- * string, only `{`, `[`, `}`, `]`, `,` and `"` change what comes next.
48
+ * The repeated member name to report, or `undefined`. Objects are checked as
49
+ * they close, innermost first, matching Python's `object_pairs_hook`, so both
50
+ * kernels name the same key. Iterative, because the text is producer-written;
51
+ * runs only on text `JSON.parse` accepted.
142
52
  */
143
53
  export declare function firstDuplicateKey(text: string): string | undefined;
144
54
  export declare function specVersionSupported(specVersion: string, accepted?: readonly string[]): boolean;
@@ -148,3 +58,19 @@ export declare function contentHashes(env: Envelope): {
148
58
  hash: string;
149
59
  }[];
150
60
  export declare function verifyEnvelope(env: Envelope, options?: VerifyOptions): Promise<VerificationResult>;
61
+ type FaultChecks = {
62
+ signatureOk: boolean;
63
+ payloadHashOk: boolean | null;
64
+ specVersionOk: boolean;
65
+ fieldsOk: boolean;
66
+ };
67
+ /**
68
+ * Every one of `ENVELOPE_FAULTS` that applies, in that order. Each is judged without the
69
+ * others: a key that cannot be read checks no signature, and still the bytes are checked
70
+ * against `payload_hash`. A field's form is judged only under a signature that holds over a
71
+ * version this verifier knows.
72
+ */
73
+ export declare function envelopeFaults(env: unknown, checks: FaultChecks): EnvelopeFault[];
74
+ /** The worst of `envelopeFaults`, or `null`. */
75
+ export declare function envelopeFault(env: unknown, checks: FaultChecks): EnvelopeFault | null;
76
+ export {};