@affiant/core 0.1.0-alpha.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 (96) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +354 -0
  3. package/dist/context.d.ts +136 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/context.js +30 -0
  6. package/dist/context.js.map +1 -0
  7. package/dist/docket/entry.d.ts +421 -0
  8. package/dist/docket/entry.d.ts.map +1 -0
  9. package/dist/docket/entry.js +155 -0
  10. package/dist/docket/entry.js.map +1 -0
  11. package/dist/docket/expiry.d.ts +82 -0
  12. package/dist/docket/expiry.d.ts.map +1 -0
  13. package/dist/docket/expiry.js +106 -0
  14. package/dist/docket/expiry.js.map +1 -0
  15. package/dist/docket/memory.d.ts +163 -0
  16. package/dist/docket/memory.d.ts.map +1 -0
  17. package/dist/docket/memory.js +528 -0
  18. package/dist/docket/memory.js.map +1 -0
  19. package/dist/docket/store.d.ts +387 -0
  20. package/dist/docket/store.d.ts.map +1 -0
  21. package/dist/docket/store.js +51 -0
  22. package/dist/docket/store.js.map +1 -0
  23. package/dist/errors.d.ts +153 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +164 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/gate/coverage.d.ts +152 -0
  28. package/dist/gate/coverage.d.ts.map +1 -0
  29. package/dist/gate/coverage.js +114 -0
  30. package/dist/gate/coverage.js.map +1 -0
  31. package/dist/gate/decide.d.ts +207 -0
  32. package/dist/gate/decide.d.ts.map +1 -0
  33. package/dist/gate/decide.js +559 -0
  34. package/dist/gate/decide.js.map +1 -0
  35. package/dist/gate/gate.d.ts +212 -0
  36. package/dist/gate/gate.d.ts.map +1 -0
  37. package/dist/gate/gate.js +175 -0
  38. package/dist/gate/gate.js.map +1 -0
  39. package/dist/gate/pipeline.d.ts +285 -0
  40. package/dist/gate/pipeline.d.ts.map +1 -0
  41. package/dist/gate/pipeline.js +515 -0
  42. package/dist/gate/pipeline.js.map +1 -0
  43. package/dist/gate/policy.d.ts +272 -0
  44. package/dist/gate/policy.d.ts.map +1 -0
  45. package/dist/gate/policy.js +396 -0
  46. package/dist/gate/policy.js.map +1 -0
  47. package/dist/gate/wrap.d.ts +107 -0
  48. package/dist/gate/wrap.d.ts.map +1 -0
  49. package/dist/gate/wrap.js +164 -0
  50. package/dist/gate/wrap.js.map +1 -0
  51. package/dist/index.d.ts +95 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +111 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/model/affidavit.d.ts +354 -0
  56. package/dist/model/affidavit.d.ts.map +1 -0
  57. package/dist/model/affidavit.js +417 -0
  58. package/dist/model/affidavit.js.map +1 -0
  59. package/dist/model/amendments.d.ts +160 -0
  60. package/dist/model/amendments.d.ts.map +1 -0
  61. package/dist/model/amendments.js +183 -0
  62. package/dist/model/amendments.js.map +1 -0
  63. package/dist/model/canonical.d.ts +311 -0
  64. package/dist/model/canonical.d.ts.map +1 -0
  65. package/dist/model/canonical.js +665 -0
  66. package/dist/model/canonical.js.map +1 -0
  67. package/dist/model/money.d.ts +127 -0
  68. package/dist/model/money.d.ts.map +1 -0
  69. package/dist/model/money.js +177 -0
  70. package/dist/model/money.js.map +1 -0
  71. package/dist/model/provenance.d.ts +315 -0
  72. package/dist/model/provenance.d.ts.map +1 -0
  73. package/dist/model/provenance.js +223 -0
  74. package/dist/model/provenance.js.map +1 -0
  75. package/dist/ports.d.ts +269 -0
  76. package/dist/ports.d.ts.map +1 -0
  77. package/dist/ports.js +34 -0
  78. package/dist/ports.js.map +1 -0
  79. package/dist/store-memory.d.ts +21 -0
  80. package/dist/store-memory.d.ts.map +1 -0
  81. package/dist/store-memory.js +20 -0
  82. package/dist/store-memory.js.map +1 -0
  83. package/dist/telemetry-keys.d.ts +65 -0
  84. package/dist/telemetry-keys.d.ts.map +1 -0
  85. package/dist/telemetry-keys.js +72 -0
  86. package/dist/telemetry-keys.js.map +1 -0
  87. package/dist/telemetry.d.ts +77 -0
  88. package/dist/telemetry.d.ts.map +1 -0
  89. package/dist/telemetry.js +43 -0
  90. package/dist/telemetry.js.map +1 -0
  91. package/dist/testing.d.ts +574 -0
  92. package/dist/testing.d.ts.map +1 -0
  93. package/dist/testing.js +1291 -0
  94. package/dist/testing.js.map +1 -0
  95. package/package.json +75 -0
  96. package/telemetry-keys.json +92 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Amendments: what a reviewer's correction means, and what it does to the record.
3
+ *
4
+ * **Rules served: DK-2** (in an amendment map `null` means *cleared* and an absent
5
+ * key means *untouched*, and an implementation never conflates them), **AF-1** (a
6
+ * field with unknown provenance is present and tagged `Empty`; a field the operation
7
+ * does not propose is absent), **AF-2** (the three numbers, with `Empty` counting as
8
+ * `0`), **AF-4** (an accepted amendment recomputes the three confidence numbers, and
9
+ * the amended field's provenance is the reviewer's act — never the machine's
10
+ * pre-correction tag), **PV-2** (that act carries a `reviewer-act` binding naming
11
+ * the Docket decision it was made on).
12
+ *
13
+ * ## What clearing a field does to the numbers
14
+ *
15
+ * A cleared field has no value, so it cannot have confidence in one. Writing the
16
+ * reviewer's `UserStated`/`1.0` tag over an emptied field would make the three
17
+ * numbers *rise* as a reviewer wiped the Affidavit — clear every field and the record
18
+ * reports perfect confidence over nothing, which is the arithmetic hole AF-2 exists
19
+ * to close, arriving through the amendment path. So a clear is resolved against the
20
+ * field rather than pasted onto it:
21
+ *
22
+ * - **A mandatory field stays and is tagged `Empty`** (confidence `0`, AF-1). The
23
+ * entity still requires it, so it is still proposed and still on the card — now
24
+ * visibly with nothing behind it, counted in `emptyFieldCount` and dragging
25
+ * `aggregateConfidence` to `0`.
26
+ * - **An optional field is removed from `fields[]`** (AF-1). A reviewer clearing an
27
+ * optional field is saying "do not write this one", which is a field the write no
28
+ * longer proposes, and AF-1 says a field the operation does not propose is absent
29
+ * rather than present-and-`Empty`.
30
+ *
31
+ * Either way the reviewer's act is not lost: the `Empty` tag carries the same
32
+ * `reviewer-act` binding and the same note a set would (PV-2), and the machine's
33
+ * displaced tag stays in the chain behind it.
34
+ *
35
+ * Why DK-2 needs saying at all: `undefined` and `null` are one keystroke apart in
36
+ * JavaScript and `{ Status: undefined }` reads to a careless consumer as "clear
37
+ * Status", when it is in fact "say nothing about Status". Conflating them silently
38
+ * either wipes a field nobody asked to wipe or ignores a correction a person made
39
+ * on purpose. So this module resolves a map into {@link Amendment} values — `set`
40
+ * or `clear`, both explicit — and a field nobody amended is simply not in the
41
+ * resolved list. `undefined` under a key is refused, loudly.
42
+ *
43
+ * Why AF-4 needs saying: the shipped demo hosts return an amended Affidavit still
44
+ * carrying the machine's pre-correction confidence, so a card can show a corrected
45
+ * value under a number that was never about that value.
46
+ *
47
+ * @packageDocumentation
48
+ */
49
+ import { mintTag, supersede } from "./provenance.js";
50
+ import { withConfidence, } from "./affidavit.js";
51
+ /** Whether `map` says anything at all about `fieldName` (DK-2). */
52
+ export function hasAmendment(map, fieldName) {
53
+ return Object.hasOwn(map, fieldName);
54
+ }
55
+ /**
56
+ * Resolve `map` into explicit {@link Amendment} values, in the map's own key order.
57
+ *
58
+ * @throws RangeError if a key is present holding `undefined` — the one thing an
59
+ * amendment map may not say, because it is indistinguishable at a glance
60
+ * from both of DK-2's meanings and is neither.
61
+ */
62
+ export function resolveAmendments(map) {
63
+ const resolved = [];
64
+ for (const name of Object.keys(map)) {
65
+ const value = map[name];
66
+ if (value === undefined) {
67
+ throw new RangeError(`DK-2: amendment ${JSON.stringify(name)} is undefined; ` +
68
+ `use null to clear the field, or omit the key to leave it untouched`);
69
+ }
70
+ resolved.push(value === null
71
+ ? { name, amendment: { kind: "clear" } }
72
+ : { name, amendment: { kind: "set", value } });
73
+ }
74
+ return resolved;
75
+ }
76
+ /**
77
+ * The provenance tag an accepted amendment puts in force on the field it names
78
+ * (PV-2, AF-2).
79
+ *
80
+ * One function so there is one answer. The canonical form (SR-1) is defined over
81
+ * "the Affidavit and its accepted amendments", and an implementation that minted a
82
+ * *nearly* identical tag on the serialization path would produce different bytes
83
+ * from the same decision — which is the one thing a canonical form exists to make
84
+ * impossible. `model/canonical.ts` calls this, and so does {@link applyAmendments}.
85
+ *
86
+ * A set mints `UserStated` at confidence `1`: a reviewer typing a value is the
87
+ * person stating it, and PV-3 makes that the one place the grade is legitimate. A
88
+ * clear mints `Empty` at confidence `0`: an emptied field has no value to be
89
+ * confident in, so a clearing can never raise a confidence number.
90
+ *
91
+ * Either way the binding is `reviewer-act`, naming the decision *and the instant*,
92
+ * so an auditor can follow a clearing as readily as a correction.
93
+ *
94
+ * @param amendment What the reviewer did to the field.
95
+ * @param act The decision it arrived on.
96
+ * @param conversationTurn The turn the Affidavit belongs to, carried onto the tag.
97
+ */
98
+ export function amendmentTag(amendment, act, conversationTurn) {
99
+ const cleared = amendment.kind === "clear";
100
+ return mintTag({
101
+ source: cleared ? "Empty" : "UserStated",
102
+ confidence: cleared ? 0 : 1,
103
+ note: cleared
104
+ ? `Cleared by ${act.by} on Docket entry ${act.entryId}`
105
+ : `Amended by ${act.by} on Docket entry ${act.entryId}`,
106
+ at: act.decisionAt,
107
+ conversationTurn,
108
+ binding: { kind: "reviewer-act", ref: { entryId: act.entryId, decisionAt: act.decisionAt } },
109
+ });
110
+ }
111
+ /**
112
+ * Apply `map` to `affidavit` as `act`, returning a new Affidavit.
113
+ *
114
+ * What happens to a field the reviewer **set**: its value becomes the amended value,
115
+ * and a `UserStated` tag carrying a `reviewer-act` binding goes **on top of** its
116
+ * chain (PV-2, AF-4). On top rather than merged: a reviewer's correction is not a
117
+ * confidence contest it might lose to the machine's own tag, and the displaced tag
118
+ * stays in `prior` so the card can still show what the machine had proposed.
119
+ *
120
+ * What happens to a field the reviewer **cleared** (`null`, DK-2): a mandatory field
121
+ * keeps its place with value `null` under an `Empty` tag at confidence `0`, and an
122
+ * optional field leaves `fields[]` entirely. See the module note above for why the
123
+ * reviewer's `1.0` is not written over an emptied field.
124
+ *
125
+ * What happens to everything else: nothing. A field the map does not name keeps its
126
+ * value, its `previousValue` and its whole chain, byte for byte (DK-2). And
127
+ * `previousValue` never moves — it is what the entity holds now, which an
128
+ * amendment does not change.
129
+ *
130
+ * The three numbers are recomputed over the amended fields (AF-4).
131
+ *
132
+ * @throws RangeError if `map` names a field the Affidavit does not propose. Not an
133
+ * {@link AffiantError}: the error-code registry names refusals the gate
134
+ * makes at runtime about a proposal's substance or a decider's identity,
135
+ * and this is a caller passing a field name that is not there — a
136
+ * programming error, in the same class as an out-of-range index.
137
+ */
138
+ export function applyAmendments(affidavit, map, act) {
139
+ const resolved = resolveAmendments(map);
140
+ if (resolved.length === 0)
141
+ return affidavit;
142
+ const byName = new Map(resolved.map((entry) => [entry.name, entry.amendment]));
143
+ for (const name of byName.keys()) {
144
+ if (!affidavit.fields.some((field) => field.name === name)) {
145
+ throw new RangeError(`amendment names field ${JSON.stringify(name)}, which this Affidavit does not propose`);
146
+ }
147
+ }
148
+ const fields = affidavit.fields.flatMap((field) => {
149
+ const amendment = byName.get(field.name);
150
+ if (amendment === undefined)
151
+ return [field];
152
+ // AF-1: a cleared optional field is a field the write no longer proposes, so it
153
+ // is absent rather than present with nothing in it.
154
+ if (amendment.kind === "clear" && !field.isMandatory)
155
+ return [];
156
+ const cleared = amendment.kind === "clear";
157
+ const value = cleared ? null : amendment.value;
158
+ const tag = amendmentTag(amendment, act, affidavit.conversationTurn);
159
+ return [
160
+ {
161
+ name: field.name,
162
+ kind: field.kind,
163
+ value,
164
+ previousValue: field.previousValue,
165
+ provenance: supersede(field.provenance, tag),
166
+ isMandatory: field.isMandatory,
167
+ },
168
+ ];
169
+ });
170
+ // AF-4: the numbers are the amended Affidavit's, not the proposal's.
171
+ return withConfidence({
172
+ // SR-4: an amendment corrects values, never the version the record conforms
173
+ // to. Carried through so the accepted state canonicalizes to the same bytes
174
+ // the same record would on the wire.
175
+ protocolVersion: affidavit.protocolVersion,
176
+ operationType: affidavit.operationType,
177
+ entityType: affidavit.entityType,
178
+ entityId: affidavit.entityId,
179
+ conversationTurn: affidavit.conversationTurn,
180
+ createdAt: affidavit.createdAt,
181
+ }, fields);
182
+ }
183
+ //# sourceMappingURL=amendments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amendments.js","sourceRoot":"","sources":["../../src/model/amendments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,311 @@
1
+ /**
2
+ * The canonical form of an Affidavit and its accepted amendments, and the SHA-256
3
+ * hash over it.
4
+ *
5
+ * **Rules served: SR-1, SR-2, RT-1.**
6
+ *
7
+ * **SR-1** — *the canonical form of a filed proposal is a deterministic byte
8
+ * sequence over the Affidavit **and its accepted amendments** (the amended field
9
+ * values with their reviewer-act tags, in the same field order), with UTF-8, object
10
+ * keys sorted by Unicode code point, no insignificant whitespace, numbers as their
11
+ * shortest round-trip decimal representation, `null` written and absent omitted.
12
+ * `canonicalHash` is the SHA-256 of that form.*
13
+ *
14
+ * **Why the amendments are inside the form and not beside it.** A host's execution
15
+ * grant binds to `canonicalHash(Affidavit + amendments)`. If the form covered the
16
+ * Affidavit alone, a grant minted for the proposal a reviewer *was shown* would
17
+ * still validate the proposal they *amended* — the one substitution the whole
18
+ * framework exists to prevent. Two conformance fixtures in this package's vector
19
+ * set differ only by an amendment, and their hashes differ; that is the rule made
20
+ * checkable. On a Docket row the same thing is spelled {@link canonicalizeEntry}:
21
+ * the row keeps the proposal and the accepted state separately, and the form is
22
+ * taken over the accepted state where there is one.
23
+ *
24
+ * **What the form is taken over: the Affidavit as the schema defines it.** Every
25
+ * property of the record is in the bytes, `protocolVersion` included — the schema
26
+ * requires it on an Affidavit, and the promoted byte vectors carry it. The card
27
+ * *envelope*'s presentation is not: `allowedValues`, `pattern`, `warnings` and
28
+ * `requiresConfirmation` are a host's rendering of a proposal rather than its sworn
29
+ * substance, and putting a rendering decision inside a hash a grant is checked
30
+ * against would let restyling an input invalidate a grant minted over evidence that
31
+ * did not change. Until this version the runtime model omitted `protocolVersion`
32
+ * and only `toWire` stamped it, so the bytes a Docket row's hash was taken over
33
+ * were not the bytes of the same record on a card. The model carries it now
34
+ * (`model/affidavit.ts`), and both paths produce one document for one record.
35
+ *
36
+ * **What applying an amendment does, exactly**, is `model/amendments.ts`'s answer and
37
+ * not this module's: the tag in force comes from `amendmentTag`, so the bytes a
38
+ * decision produces here and the row that decision writes cannot disagree. That
39
+ * carries AF-1's clearing rule with it — a cleared **mandatory** field stays, tagged
40
+ * `Empty` at confidence `0`, and a cleared **optional** field leaves the field list,
41
+ * because a field the write no longer proposes is absent rather than present with
42
+ * nothing in it — and AF-4's recompute, where the document carries an aggregate.
43
+ *
44
+ * **Why a canonical form at all.** Three things depend on two independent
45
+ * implementations agreeing byte for byte: the conformance suite compares canonical
46
+ * forms across the .NET and TypeScript lines; an `utterance-span` provenance
47
+ * binding hashes the span it points at, so an auditor can re-derive it years later;
48
+ * and the execution grant above. `JSON.stringify` cannot do any of that — its key
49
+ * order is insertion order, so the same Affidavit built by two code paths produces
50
+ * two different documents and two different hashes.
51
+ *
52
+ * ## The form, precisely
53
+ *
54
+ * A JSON document with no space, tab or newline between tokens. Within it:
55
+ *
56
+ * - **Objects** — `{`, then each own enumerable string-keyed property whose value is
57
+ * not `undefined`, sorted by **Unicode code point** of the key, as `"key":value`
58
+ * separated by `,`, then `}`. Sorting by code point rather than by UTF-16 code
59
+ * unit matters above U+FFFF: a key starting with an emoji sorts *after* a key
60
+ * starting with U+E000, though a naive JavaScript `<` puts it first. Symbol keys
61
+ * are not JSON and are ignored; inherited properties are not own properties and
62
+ * are ignored.
63
+ * - **Arrays** — `[`, elements in their own order, separated by `,`, then `]`.
64
+ * Array order is data, never sorted. An `undefined` element is refused rather
65
+ * than written as `null`: "absent omitted" is meaningful for a property and
66
+ * meaningless for a position, and silently substituting `null` would put a value
67
+ * on an audit record that the producer never wrote.
68
+ * - **Strings** — JSON escaping and nothing more: `"` and `\` escaped, the C0
69
+ * control characters escaped (`\b`, `\t`, `\n`, `\f`, `\r`, else `\u00xx` with
70
+ * lowercase hex), unpaired surrogates escaped as `\udxxx`. Every other character
71
+ * is written as itself and encoded as UTF-8 — **no `\u` escapes for non-ASCII**,
72
+ * so `"é"` is two bytes and not six, and `/` is never escaped.
73
+ * - **Numbers** — the shortest round-trip decimal, written positionally: never an
74
+ * exponent, `-0` written as `0`, non-finite refused. See {@link canonicalJson}
75
+ * for what "positionally" costs and why it is the safer half of the trade.
76
+ * - **`null`** — written. **`true` / `false`** — written.
77
+ * - **Money** — `{ amount, currency }` is two strings (SR-2). A value shaped like
78
+ * money whose amount is a number is refused here rather than hashed, because a
79
+ * float amount that reached the canonical form would be sworn to.
80
+ *
81
+ * ## What is refused
82
+ *
83
+ * A non-finite number is a `RangeError`. Everything else that has no canonical form
84
+ * is a `TypeError`: `undefined` in an array position, a `bigint`, a function, a
85
+ * symbol, a `Date` or `Map` or typed array (anything whose `Object.prototype.toString`
86
+ * tag is not `[object Object]` or `[object Array]`), a cycle, and money with a
87
+ * numeric amount. An audit form never guesses: a value it cannot write exactly is a
88
+ * value it refuses to write at all.
89
+ *
90
+ * ## Integers beyond 2^53 (the documented limit of the rule)
91
+ *
92
+ * JSON numbers are IEEE 754 doubles by the time any implementation sees them, so
93
+ * `9007199254740993` has already become `9007199254740992` in the parser, before the
94
+ * canonical form is reached. This module therefore writes the shortest round-trip
95
+ * decimal of the double it was given and **does not** convert large numbers to strings: a
96
+ * canonical form cannot recover precision the parse already lost, and a form that
97
+ * quietly restringified some numbers and not others would be harder to reimplement,
98
+ * not easier. A host that needs exact integers beyond 2^53 — an account number, a
99
+ * minor-unit amount, an accounting record's id — carries them as strings on the
100
+ * wire. Money
101
+ * already does, by SR-2.
102
+ *
103
+ * ## Runtime (RT-1)
104
+ *
105
+ * `canonicalHash` digests through `globalThis.crypto.subtle`, which has no
106
+ * synchronous form, so every hash path in this framework is asynchronous end to end
107
+ * — the stated portability choice, and the reason `canonicalHash` returns a
108
+ * `Promise` on Node, on Bun and inside workerd alike. No `node:crypto`, no
109
+ * filesystem, no Node global.
110
+ *
111
+ * @packageDocumentation
112
+ */
113
+ import type { AmendmentMap } from "@affiant/contract";
114
+ import type { DocketEntry } from "../docket/entry.js";
115
+ import type { Affidavit } from "./affidavit.js";
116
+ import type { ReviewerAct } from "./amendments.js";
117
+ /**
118
+ * One field of the Affidavit, as much of it as canonicalization needs to know.
119
+ *
120
+ * Deliberately minimal. This module serializes *whatever object it is handed* —
121
+ * every own enumerable property, at every depth — so it does not need the Affidavit
122
+ * type to do its work, and stating a full one here would couple the canonical form
123
+ * to a model that is still being written (`model/affidavit.ts`, pull request C2).
124
+ * What it does need is the two properties amendment application reads: the `name`
125
+ * an amendment is keyed by, and the `value` an amendment replaces. Everything else
126
+ * on a real `AffidavitField` — `previousValue`, `provenance`, `isMandatory`,
127
+ * `kind`, `allowedValues`, `pattern` — is carried through untouched and appears in
128
+ * the bytes, because the bytes are of the object, not of this interface.
129
+ */
130
+ export interface CanonicalField {
131
+ /** The field's name. The key {@link AmendmentMap} is keyed by. */
132
+ readonly name: string;
133
+ /** The proposed value. Any JSON value, including `null`. */
134
+ readonly value?: unknown;
135
+ /** The provenance chain behind the value, if the caller carries one. */
136
+ readonly provenance?: unknown;
137
+ }
138
+ /**
139
+ * The Affidavit being canonicalized, as much of it as this module needs to know.
140
+ *
141
+ * `fields` is optional so the same function can canonicalize the byte vectors that
142
+ * exercise the *form* — key ordering, number shapes, escaping — without dressing
143
+ * them up as Affidavits. A real `Affidavit` from `@affiant/contract` satisfies this
144
+ * interface structurally, and so will C2's core model.
145
+ */
146
+ export interface CanonicalInput {
147
+ /** The sworn fields, in the order the Affidavit carries them. */
148
+ readonly fields?: readonly CanonicalField[];
149
+ /**
150
+ * The protocol version the record conforms to (SR-4).
151
+ *
152
+ * Named here — optional, like `fields`, so a bare byte vector need not dress
153
+ * itself up as an Affidavit — because it is the property whose absence from the
154
+ * runtime model made the canonical form of a Docket row differ from the canonical
155
+ * form of the same record on the wire. A real `Affidavit` always carries it; a
156
+ * record handed here without it is canonicalized without it, because this module
157
+ * serializes the object it is given and never adds a property to a document.
158
+ */
159
+ readonly protocolVersion?: string;
160
+ }
161
+ /** Options for {@link canonicalize}, {@link canonicalString} and {@link canonicalHash}. */
162
+ export interface CanonicalizeOptions {
163
+ /**
164
+ * The decision the amendments arrived on — its entry, **its instant** and its
165
+ * principal. Required whenever a non-empty {@link AmendmentMap} is applied,
166
+ * because PV-2 says a `reviewer-act` binding names the decision that amended the
167
+ * field, and a binding that names nothing is not a binding.
168
+ */
169
+ readonly reviewerAct?: ReviewerAct;
170
+ }
171
+ /**
172
+ * Apply an {@link AmendmentMap} to an Affidavit, returning a new object: each
173
+ * amended field's `value` replaced and the reviewer's own tag put in force on its
174
+ * provenance chain, with the tag it supersedes preserved beneath it.
175
+ *
176
+ * **The tag is `model/amendments.ts`'s, not this module's.** `amendmentTag` is the
177
+ * single definition of what an accepted amendment does to a field's provenance, and
178
+ * both paths call it — so the bytes this function produces for a decision are the
179
+ * bytes the Docket row's own `amendedAffidavit` produces for the same decision.
180
+ * There is no second, nearly-identical tag to drift.
181
+ *
182
+ * **DK-2**: `null` under a key clears the field; an absent key leaves it untouched.
183
+ * The two are never conflated.
184
+ *
185
+ * **AF-1**, on what a clear does: a **mandatory** field keeps its place with value
186
+ * `null` under an `Empty` tag at confidence `0`, and an **optional** field leaves
187
+ * `fields[]` entirely — a reviewer clearing an optional field is saying "do not
188
+ * write this one", and a field the write no longer proposes is absent rather than
189
+ * present with nothing in it. A field with no `isMandatory` property is read as
190
+ * optional, which is what the property's absence means everywhere else.
191
+ *
192
+ * **AF-4**: every confidence number the document carries — the aggregate, the
193
+ * populated minimum and the empty-field count — is recomputed over the amended
194
+ * fields. A canonical form that kept a pre-correction number would let a grant bind
195
+ * to an Affidavit whose own summary contradicts its fields.
196
+ *
197
+ * @param affidavit The Affidavit as filed.
198
+ * @param amendments The reviewer's accepted amendments, keyed by field name.
199
+ * @param act The decision the amendments arrived on (PV-2).
200
+ * @throws TypeError when the input carries no `fields` array, when a field is not
201
+ * an object, or when an amendment names a field the Affidavit does not
202
+ * carry — an amendment to something nobody swore to is a bug in the caller,
203
+ * and swallowing it would let two implementations disagree in silence.
204
+ * @throws RangeError when `amendments` holds `undefined` under a key (DK-2).
205
+ */
206
+ export declare function applyAmendmentsForCanonical(affidavit: CanonicalInput, amendments: AmendmentMap, act: ReviewerAct): CanonicalInput;
207
+ /**
208
+ * The canonical form of `affidavit` and its accepted `amendments`, as UTF-8 bytes
209
+ * (SR-1).
210
+ *
211
+ * @param affidavit The Affidavit as filed.
212
+ * @param amendments The accepted amendments, or `null` / omitted for none. An empty
213
+ * map is the same as none and needs no `reviewerAct`.
214
+ * @param options {@link CanonicalizeOptions.reviewerAct}, required whenever
215
+ * `amendments` is non-empty.
216
+ * @throws RangeError on a non-finite number; TypeError on anything else with no
217
+ * canonical form (see the module header).
218
+ */
219
+ export declare function canonicalize(affidavit: CanonicalInput, amendments?: AmendmentMap | null, options?: CanonicalizeOptions): Uint8Array;
220
+ /**
221
+ * The canonical form of `affidavit` and its accepted `amendments`, as a string —
222
+ * the same document {@link canonicalize} returns, before UTF-8 encoding.
223
+ *
224
+ * Useful where the bytes are not what is wanted: a fixture that has to be readable
225
+ * in a diff, a log line, a comparison in a test. The bytes are the contract; this is
226
+ * the same document one encoding step earlier.
227
+ */
228
+ export declare function canonicalString(affidavit: CanonicalInput, amendments?: AmendmentMap | null, options?: CanonicalizeOptions): string;
229
+ /**
230
+ * The SHA-256 of the canonical form, as lowercase hex (SR-1, RT-1).
231
+ *
232
+ * Asynchronous on every runtime because Web Crypto has no synchronous digest, and
233
+ * Web Crypto is the only digest a package that must run on Node, Bun and workerd
234
+ * can reach (RT-1). This is a stated portability choice, not an oversight: the
235
+ * .NET line may hash synchronously, since the conformance fixtures assert *values*,
236
+ * not call shapes.
237
+ *
238
+ * @returns 64 lowercase hex characters.
239
+ */
240
+ export declare function canonicalHash(affidavit: CanonicalInput, amendments?: AmendmentMap | null, options?: CanonicalizeOptions): Promise<string>;
241
+ /**
242
+ * The Affidavit a Docket row's canonical form is taken over: the state a reviewer's
243
+ * amendments produced if there is one, and the proposal otherwise (SR-1).
244
+ *
245
+ * This is what SR-1's "the Affidavit **and its accepted amendments**" means on a
246
+ * row. The row keeps both — `affidavit` as the agent proposed it, never edited, and
247
+ * `amendedAffidavit` as the approval accepted it — so the sworn form and the
248
+ * proposal are separately readable and only one of them is what a grant binds to.
249
+ */
250
+ export declare function swornAffidavitOf(entry: DocketEntry): Affidavit;
251
+ /**
252
+ * The canonical form of a Docket row, as UTF-8 bytes (SR-1).
253
+ *
254
+ * Equivalent to `canonicalize(entry.amendedAffidavit ?? entry.affidavit)`, with no
255
+ * amendment argument: the amendments were applied when the approval was recorded,
256
+ * by the same `amendmentTag` this module uses, so there is nothing left to apply.
257
+ *
258
+ * **This is the function a host's execution grant hashes over.** Binding a grant to
259
+ * the proposal instead would let a grant minted for the Affidavit a reviewer was
260
+ * shown validate the one they amended — the substitution the framework exists to
261
+ * prevent.
262
+ */
263
+ export declare function canonicalizeEntry(entry: DocketEntry): Uint8Array;
264
+ /** The canonical form of a Docket row as a string — {@link canonicalizeEntry} one encoding step earlier. */
265
+ export declare function canonicalStringEntry(entry: DocketEntry): string;
266
+ /** The SHA-256 of a Docket row's canonical form, as lowercase hex (SR-1, RT-1). */
267
+ export declare function canonicalHashEntry(entry: DocketEntry): Promise<string>;
268
+ /**
269
+ * SHA-256 over arbitrary bytes, as lowercase hex, through Web Crypto (RT-1).
270
+ *
271
+ * Exported because the canonical form is not the only thing this framework hashes:
272
+ * an `utterance-span` binding hashes the span it points at, so an auditor can
273
+ * re-derive it, and a host's execution grant hashes what it grants over.
274
+ *
275
+ * @throws Error when the runtime exposes no `crypto.subtle` — which, on a runtime
276
+ * this package claims to support, means the host has replaced a standard
277
+ * global rather than that the digest is unavailable.
278
+ */
279
+ export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
280
+ /**
281
+ * The canonical form of any JSON value (SR-1), as a string.
282
+ *
283
+ * The general writer the Affidavit entry points are built on, exported because
284
+ * SR-1's form is defined over JSON and not over one type: a binding, a grant
285
+ * payload and a fixture all need the same bytes.
286
+ *
287
+ * **Numbers, and the one place this is stricter than RFC 8785.** The shortest
288
+ * round-trip decimal for a value is what JavaScript's `Number#toString` produces,
289
+ * and that is the form RFC 8785 (JSON Canonicalization Scheme) adopts wholesale —
290
+ * exponent notation included, so `1e21` serializes as `1e+21`. This writer instead
291
+ * writes every number **positionally**: `1000000000000000000000`, and `1e-7` as
292
+ * `0.0000001`. The digits are the same digits; only the decimal point moves, so the
293
+ * value denoted is identical and it parses back to the identical double. What is
294
+ * bought is that a second implementation — in .NET, in Go, in a database function —
295
+ * has to agree about *digits*, not about ECMAScript's exponent thresholds and its
296
+ * `e+21` spelling, which is the part of RFC 8785 that needs an appendix. What is
297
+ * paid is length: a denormal near `5e-324` writes out as roughly a thousand
298
+ * characters. That is a bad number to have in an audit record for reasons that have
299
+ * nothing to do with this function.
300
+ *
301
+ * "Positionally" means the **shortest round-trip digits** with the point moved, not
302
+ * the double's exact binary value: `1.2345678901234569e23` is written
303
+ * `123456789012345690000000`, not the exact `123456789012345685803008`. The two
304
+ * parse to the same double, and SR-1 asks for the shortest round-trip form — so the
305
+ * digits are the ones `Number#toString` chose, and nothing else.
306
+ *
307
+ * @throws RangeError on `NaN` or an infinity — a number with no decimal form at all.
308
+ * @throws TypeError on a value with no canonical form (see the module header).
309
+ */
310
+ export declare function canonicalJson(value: unknown): string;
311
+ //# sourceMappingURL=canonical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../../src/model/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+GG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AASnD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C;;;;;;;;;OASG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,2FAA2F;AAC3F,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;CACpC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,cAAc,EACzB,UAAU,EAAE,YAAY,EACxB,GAAG,EAAE,WAAW,GACf,cAAc,CAsEhB;AAiFD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,cAAc,EACzB,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,EAChC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,UAAU,CAEZ;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,cAAc,EACzB,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,EAChC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,MAAM,CAER;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,SAAS,EAAE,cAAc,EACzB,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,EAChC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAEjB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAE9D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,CAEhE;AAED,4GAA4G;AAC5G,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAE/D;AAED,mFAAmF;AACnF,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBlE;AA0BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIpD"}