@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,665 @@
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 { amendmentTag, resolveAmendments } from "./amendments.js";
114
+ import { MONEY_AMOUNT_PATTERN, MONEY_CURRENCY_PATTERN } from "./money.js";
115
+ // ---------------------------------------------------------------------------
116
+ // Amendments
117
+ // ---------------------------------------------------------------------------
118
+ /**
119
+ * Apply an {@link AmendmentMap} to an Affidavit, returning a new object: each
120
+ * amended field's `value` replaced and the reviewer's own tag put in force on its
121
+ * provenance chain, with the tag it supersedes preserved beneath it.
122
+ *
123
+ * **The tag is `model/amendments.ts`'s, not this module's.** `amendmentTag` is the
124
+ * single definition of what an accepted amendment does to a field's provenance, and
125
+ * both paths call it — so the bytes this function produces for a decision are the
126
+ * bytes the Docket row's own `amendedAffidavit` produces for the same decision.
127
+ * There is no second, nearly-identical tag to drift.
128
+ *
129
+ * **DK-2**: `null` under a key clears the field; an absent key leaves it untouched.
130
+ * The two are never conflated.
131
+ *
132
+ * **AF-1**, on what a clear does: a **mandatory** field keeps its place with value
133
+ * `null` under an `Empty` tag at confidence `0`, and an **optional** field leaves
134
+ * `fields[]` entirely — a reviewer clearing an optional field is saying "do not
135
+ * write this one", and a field the write no longer proposes is absent rather than
136
+ * present with nothing in it. A field with no `isMandatory` property is read as
137
+ * optional, which is what the property's absence means everywhere else.
138
+ *
139
+ * **AF-4**: every confidence number the document carries — the aggregate, the
140
+ * populated minimum and the empty-field count — is recomputed over the amended
141
+ * fields. A canonical form that kept a pre-correction number would let a grant bind
142
+ * to an Affidavit whose own summary contradicts its fields.
143
+ *
144
+ * @param affidavit The Affidavit as filed.
145
+ * @param amendments The reviewer's accepted amendments, keyed by field name.
146
+ * @param act The decision the amendments arrived on (PV-2).
147
+ * @throws TypeError when the input carries no `fields` array, when a field is not
148
+ * an object, or when an amendment names a field the Affidavit does not
149
+ * carry — an amendment to something nobody swore to is a bug in the caller,
150
+ * and swallowing it would let two implementations disagree in silence.
151
+ * @throws RangeError when `amendments` holds `undefined` under a key (DK-2).
152
+ */
153
+ export function applyAmendmentsForCanonical(affidavit, amendments, act) {
154
+ const resolved = resolveAmendments(amendments);
155
+ if (resolved.length === 0)
156
+ return affidavit;
157
+ const byName = new Map(resolved.map((entry) => [entry.name, entry.amendment]));
158
+ const record = asRecord(affidavit, "the Affidavit");
159
+ const fields = record["fields"];
160
+ if (!Array.isArray(fields)) {
161
+ throw new TypeError("SR-1: amendments are applied per field, so the Affidavit must carry a fields array; " +
162
+ `received ${describe(fields)} under "fields".`);
163
+ }
164
+ const conversationTurn = typeof record["conversationTurn"] === "number" ? record["conversationTurn"] : null;
165
+ const amended = new Set();
166
+ const nextFields = [];
167
+ for (const [index, field] of fields.entries()) {
168
+ const entry = asRecord(field, `fields[${String(index)}]`);
169
+ const name = entry["name"];
170
+ if (typeof name !== "string") {
171
+ nextFields.push(field);
172
+ continue;
173
+ }
174
+ const amendment = byName.get(name);
175
+ if (amendment === undefined) {
176
+ nextFields.push(field);
177
+ continue;
178
+ }
179
+ amended.add(name);
180
+ // AF-1: a cleared optional field is a field the write no longer proposes.
181
+ if (amendment.kind === "clear" && entry["isMandatory"] !== true)
182
+ continue;
183
+ nextFields.push({
184
+ ...entry,
185
+ value: amendment.kind === "clear" ? null : amendment.value,
186
+ provenance: withReviewerAct(entry["provenance"], amendmentTag(amendment, act, conversationTurn)),
187
+ });
188
+ }
189
+ for (const { name } of resolved) {
190
+ if (!amended.has(name)) {
191
+ throw new TypeError(`DK-2: the amendment map names the field ${JSON.stringify(name)}, which this Affidavit ` +
192
+ `does not carry. An amendment applies to a field that was sworn to; a key with no ` +
193
+ `field is a caller bug, not an empty amendment.`);
194
+ }
195
+ }
196
+ const numbers = confidenceOf(nextFields);
197
+ const next = { ...record, fields: nextFields };
198
+ // Each of the three is rewritten only where the document already carries it, so
199
+ // this function never adds a property to a record that did not have one.
200
+ if (typeof record["aggregateConfidence"] === "number") {
201
+ next["aggregateConfidence"] = numbers.aggregateConfidence;
202
+ }
203
+ if (Object.hasOwn(record, "populatedConfidence")) {
204
+ next["populatedConfidence"] = numbers.populatedConfidence;
205
+ }
206
+ if (typeof record["emptyFieldCount"] === "number") {
207
+ next["emptyFieldCount"] = numbers.emptyFieldCount;
208
+ }
209
+ return next;
210
+ }
211
+ /**
212
+ * AF-2's three numbers over already-serialized fields.
213
+ *
214
+ * - `aggregateConfidence` — the minimum confidence, with an `Empty` tag counting as
215
+ * `0` and no proposed field at all counting as `0`.
216
+ * - `populatedConfidence` — the minimum over the non-`Empty` fields, `null` when
217
+ * there are none.
218
+ * - `emptyFieldCount` — how many fields are tagged `Empty`.
219
+ *
220
+ * Written out here rather than reached for from `model/affidavit.ts` because this
221
+ * module serializes *whatever object it is handed*, including the wire shape whose
222
+ * fields are not core `AffidavitField`s. A field whose chain says nothing readable
223
+ * contributes `0`: an unreadable grade is not evidence of a good one.
224
+ *
225
+ * **Why all three and not only the aggregate.** AF-4 says an accepted amendment
226
+ * recomputes the numbers, and the v0.1 record carries three of them. Recomputing
227
+ * one and carrying the other two over from the proposal would produce an accepted
228
+ * state that contradicts its own fields — a record whose only remaining field is
229
+ * sworn at `1` while `populatedConfidence` still reads the pre-correction `0.9` —
230
+ * and SR-1 defines the canonical form over the accepted state, so those bytes would
231
+ * be what a host's execution grant binds to. The seed-shaped record carried one
232
+ * number and is unaffected: a property that is not there is not written.
233
+ */
234
+ function confidenceOf(fields) {
235
+ let lowest = 1;
236
+ let populated = null;
237
+ let emptyFieldCount = 0;
238
+ for (const field of fields) {
239
+ const chain = field.provenance;
240
+ const current = chain?.current;
241
+ const isEmpty = current?.source === "Empty";
242
+ const confidence = isEmpty || typeof current?.confidence !== "number" ? 0 : current.confidence;
243
+ if (confidence < lowest)
244
+ lowest = confidence;
245
+ if (isEmpty) {
246
+ emptyFieldCount += 1;
247
+ }
248
+ else {
249
+ populated = populated === null ? confidence : Math.min(populated, confidence);
250
+ }
251
+ }
252
+ return {
253
+ aggregateConfidence: fields.length === 0 ? 0 : lowest,
254
+ populatedConfidence: populated,
255
+ emptyFieldCount,
256
+ };
257
+ }
258
+ /**
259
+ * Put `tag` in force on a provenance chain, preserving the tag it supersedes.
260
+ *
261
+ * Two spellings of the history array are accepted — `prior`, which the protocol's
262
+ * seed schemas, the wire fixtures and the core model all use, and `history`, which
263
+ * an earlier working draft used. A chain that carries neither gets `prior`.
264
+ */
265
+ function withReviewerAct(chain, tag) {
266
+ if (chain === null || chain === undefined)
267
+ return { current: tag, prior: [] };
268
+ const record = asRecord(chain, "provenance");
269
+ const key = !Array.isArray(record["prior"]) && Array.isArray(record["history"]) ? "history" : "prior";
270
+ const existing = record[key];
271
+ const history = Array.isArray(existing) ? existing : [];
272
+ const superseded = record["current"];
273
+ return {
274
+ ...record,
275
+ current: tag,
276
+ [key]: superseded === undefined ? [...history] : [superseded, ...history],
277
+ };
278
+ }
279
+ // ---------------------------------------------------------------------------
280
+ // The public canonicalization entry points
281
+ // ---------------------------------------------------------------------------
282
+ /**
283
+ * The canonical form of `affidavit` and its accepted `amendments`, as UTF-8 bytes
284
+ * (SR-1).
285
+ *
286
+ * @param affidavit The Affidavit as filed.
287
+ * @param amendments The accepted amendments, or `null` / omitted for none. An empty
288
+ * map is the same as none and needs no `reviewerAct`.
289
+ * @param options {@link CanonicalizeOptions.reviewerAct}, required whenever
290
+ * `amendments` is non-empty.
291
+ * @throws RangeError on a non-finite number; TypeError on anything else with no
292
+ * canonical form (see the module header).
293
+ */
294
+ export function canonicalize(affidavit, amendments, options) {
295
+ return new TextEncoder().encode(canonicalString(affidavit, amendments, options));
296
+ }
297
+ /**
298
+ * The canonical form of `affidavit` and its accepted `amendments`, as a string —
299
+ * the same document {@link canonicalize} returns, before UTF-8 encoding.
300
+ *
301
+ * Useful where the bytes are not what is wanted: a fixture that has to be readable
302
+ * in a diff, a log line, a comparison in a test. The bytes are the contract; this is
303
+ * the same document one encoding step earlier.
304
+ */
305
+ export function canonicalString(affidavit, amendments, options) {
306
+ return canonicalJson(withAmendments(affidavit, amendments, options));
307
+ }
308
+ /**
309
+ * The SHA-256 of the canonical form, as lowercase hex (SR-1, RT-1).
310
+ *
311
+ * Asynchronous on every runtime because Web Crypto has no synchronous digest, and
312
+ * Web Crypto is the only digest a package that must run on Node, Bun and workerd
313
+ * can reach (RT-1). This is a stated portability choice, not an oversight: the
314
+ * .NET line may hash synchronously, since the conformance fixtures assert *values*,
315
+ * not call shapes.
316
+ *
317
+ * @returns 64 lowercase hex characters.
318
+ */
319
+ export async function canonicalHash(affidavit, amendments, options) {
320
+ return sha256Hex(canonicalize(affidavit, amendments, options));
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // The canonical form of a Docket row
324
+ // ---------------------------------------------------------------------------
325
+ /**
326
+ * The Affidavit a Docket row's canonical form is taken over: the state a reviewer's
327
+ * amendments produced if there is one, and the proposal otherwise (SR-1).
328
+ *
329
+ * This is what SR-1's "the Affidavit **and its accepted amendments**" means on a
330
+ * row. The row keeps both — `affidavit` as the agent proposed it, never edited, and
331
+ * `amendedAffidavit` as the approval accepted it — so the sworn form and the
332
+ * proposal are separately readable and only one of them is what a grant binds to.
333
+ */
334
+ export function swornAffidavitOf(entry) {
335
+ return entry.amendedAffidavit ?? entry.affidavit;
336
+ }
337
+ /**
338
+ * The canonical form of a Docket row, as UTF-8 bytes (SR-1).
339
+ *
340
+ * Equivalent to `canonicalize(entry.amendedAffidavit ?? entry.affidavit)`, with no
341
+ * amendment argument: the amendments were applied when the approval was recorded,
342
+ * by the same `amendmentTag` this module uses, so there is nothing left to apply.
343
+ *
344
+ * **This is the function a host's execution grant hashes over.** Binding a grant to
345
+ * the proposal instead would let a grant minted for the Affidavit a reviewer was
346
+ * shown validate the one they amended — the substitution the framework exists to
347
+ * prevent.
348
+ */
349
+ export function canonicalizeEntry(entry) {
350
+ return canonicalize(swornAffidavitOf(entry));
351
+ }
352
+ /** The canonical form of a Docket row as a string — {@link canonicalizeEntry} one encoding step earlier. */
353
+ export function canonicalStringEntry(entry) {
354
+ return canonicalString(swornAffidavitOf(entry));
355
+ }
356
+ /** The SHA-256 of a Docket row's canonical form, as lowercase hex (SR-1, RT-1). */
357
+ export async function canonicalHashEntry(entry) {
358
+ return canonicalHash(swornAffidavitOf(entry));
359
+ }
360
+ /**
361
+ * SHA-256 over arbitrary bytes, as lowercase hex, through Web Crypto (RT-1).
362
+ *
363
+ * Exported because the canonical form is not the only thing this framework hashes:
364
+ * an `utterance-span` binding hashes the span it points at, so an auditor can
365
+ * re-derive it, and a host's execution grant hashes what it grants over.
366
+ *
367
+ * @throws Error when the runtime exposes no `crypto.subtle` — which, on a runtime
368
+ * this package claims to support, means the host has replaced a standard
369
+ * global rather than that the digest is unavailable.
370
+ */
371
+ export async function sha256Hex(bytes) {
372
+ const subtle = globalThis.crypto?.subtle;
373
+ if (subtle === undefined) {
374
+ throw new Error("RT-1: this package hashes through Web Crypto only (globalThis.crypto.subtle), which is " +
375
+ "present on Node 22, Bun and workerd. There is no node:crypto fallback by design: a " +
376
+ "synchronous fallback on one runtime would make the hash path synchronous there and " +
377
+ "asynchronous everywhere else, and the contract is asynchronous end to end.");
378
+ }
379
+ // Copied into a view this function owns: `digest` takes an ArrayBuffer-backed
380
+ // source, and a caller's array may be backed by a SharedArrayBuffer another
381
+ // thread can write to while the digest runs. A canonical form is small, and a
382
+ // hash of bytes that changed underneath it would be worse than a copy.
383
+ const digest = await subtle.digest("SHA-256", new Uint8Array(bytes));
384
+ const view = new Uint8Array(digest);
385
+ let hex = "";
386
+ for (const byte of view)
387
+ hex += byte.toString(16).padStart(2, "0");
388
+ return hex;
389
+ }
390
+ /** Apply the amendments if there are any, refusing an amendment with no reviewer act. */
391
+ function withAmendments(affidavit, amendments, options) {
392
+ if (amendments === null || amendments === undefined)
393
+ return affidavit;
394
+ if (Object.keys(amendments).length === 0)
395
+ return affidavit;
396
+ const act = options?.reviewerAct;
397
+ if (act === undefined) {
398
+ throw new TypeError("PV-2: applying amendments to the canonical form needs options.reviewerAct — the Docket " +
399
+ "decision the amendments arrived on, its instant and its principal. The reviewer-act " +
400
+ "binding names that decision, and a binding whose source cannot be checked is not a " +
401
+ "binding.");
402
+ }
403
+ return applyAmendmentsForCanonical(affidavit, amendments, act);
404
+ }
405
+ // ---------------------------------------------------------------------------
406
+ // The writer
407
+ // ---------------------------------------------------------------------------
408
+ /**
409
+ * The canonical form of any JSON value (SR-1), as a string.
410
+ *
411
+ * The general writer the Affidavit entry points are built on, exported because
412
+ * SR-1's form is defined over JSON and not over one type: a binding, a grant
413
+ * payload and a fixture all need the same bytes.
414
+ *
415
+ * **Numbers, and the one place this is stricter than RFC 8785.** The shortest
416
+ * round-trip decimal for a value is what JavaScript's `Number#toString` produces,
417
+ * and that is the form RFC 8785 (JSON Canonicalization Scheme) adopts wholesale —
418
+ * exponent notation included, so `1e21` serializes as `1e+21`. This writer instead
419
+ * writes every number **positionally**: `1000000000000000000000`, and `1e-7` as
420
+ * `0.0000001`. The digits are the same digits; only the decimal point moves, so the
421
+ * value denoted is identical and it parses back to the identical double. What is
422
+ * bought is that a second implementation — in .NET, in Go, in a database function —
423
+ * has to agree about *digits*, not about ECMAScript's exponent thresholds and its
424
+ * `e+21` spelling, which is the part of RFC 8785 that needs an appendix. What is
425
+ * paid is length: a denormal near `5e-324` writes out as roughly a thousand
426
+ * characters. That is a bad number to have in an audit record for reasons that have
427
+ * nothing to do with this function.
428
+ *
429
+ * "Positionally" means the **shortest round-trip digits** with the point moved, not
430
+ * the double's exact binary value: `1.2345678901234569e23` is written
431
+ * `123456789012345690000000`, not the exact `123456789012345685803008`. The two
432
+ * parse to the same double, and SR-1 asks for the shortest round-trip form — so the
433
+ * digits are the ones `Number#toString` chose, and nothing else.
434
+ *
435
+ * @throws RangeError on `NaN` or an infinity — a number with no decimal form at all.
436
+ * @throws TypeError on a value with no canonical form (see the module header).
437
+ */
438
+ export function canonicalJson(value) {
439
+ const out = [];
440
+ writeValue(value, out, "", new Set());
441
+ return out.join("");
442
+ }
443
+ function writeValue(value, out, path, open) {
444
+ if (value === null) {
445
+ out.push("null");
446
+ return;
447
+ }
448
+ if (typeof value === "boolean") {
449
+ out.push(value ? "true" : "false");
450
+ return;
451
+ }
452
+ if (typeof value === "string") {
453
+ out.push(JSON.stringify(value));
454
+ return;
455
+ }
456
+ if (typeof value === "number") {
457
+ out.push(writeNumber(value, path));
458
+ return;
459
+ }
460
+ if (typeof value === "undefined") {
461
+ throw new TypeError(`SR-1: undefined has no canonical form and cannot be written at ${at(path)}. A property ` +
462
+ `whose value is undefined is omitted; an undefined anywhere else is a caller bug.`);
463
+ }
464
+ if (typeof value === "bigint") {
465
+ throw new TypeError(`SR-1: a bigint has no JSON form, so none at ${at(path)}. Carry an integer beyond 2^53 as ` +
466
+ `a string — see this module's header on why the canonical form does not restring numbers ` +
467
+ `for you.`);
468
+ }
469
+ if (typeof value === "function" || typeof value === "symbol") {
470
+ throw new TypeError(`SR-1: a ${typeof value} has no canonical form; found one at ${at(path)}.`);
471
+ }
472
+ if (open.has(value)) {
473
+ throw new TypeError(`SR-1: the value contains a cycle, which reaches ${at(path)}. A canonical form is a finite ` +
474
+ `document; an Affidavit is a tree.`);
475
+ }
476
+ open.add(value);
477
+ try {
478
+ if (Array.isArray(value)) {
479
+ writeArray(value, out, path, open);
480
+ }
481
+ else {
482
+ writeObject(value, out, path, open);
483
+ }
484
+ }
485
+ finally {
486
+ open.delete(value);
487
+ }
488
+ }
489
+ function writeArray(value, out, path, open) {
490
+ out.push("[");
491
+ for (let index = 0; index < value.length; index += 1) {
492
+ if (index > 0)
493
+ out.push(",");
494
+ const element = value[index];
495
+ const elementPath = `${path}/${String(index)}`;
496
+ if (element === undefined) {
497
+ throw new TypeError(`SR-1: undefined at ${at(elementPath)}. "Absent omitted" is a rule about properties; a ` +
498
+ `position cannot be omitted without moving every element after it, and writing null ` +
499
+ `instead would put a value on the record that the producer never wrote.`);
500
+ }
501
+ writeValue(element, out, elementPath, open);
502
+ }
503
+ out.push("]");
504
+ }
505
+ function writeObject(value, out, path, open) {
506
+ const tag = Object.prototype.toString.call(value);
507
+ if (tag !== "[object Object]") {
508
+ throw new TypeError(`SR-1: ${tag} has no canonical form; found one at ${at(path)}. The form is defined over ` +
509
+ `JSON — objects, arrays, strings, numbers, booleans and null. A Date, a Map, a Set or a ` +
510
+ `typed array is converted by the host before it is sworn to, so the record shows what the ` +
511
+ `host meant rather than what a serializer guessed.`);
512
+ }
513
+ const record = value;
514
+ assertNotFloatMoney(record, path);
515
+ const keys = Object.keys(record)
516
+ .filter((key) => record[key] !== undefined)
517
+ .sort(compareCodePoints);
518
+ out.push("{");
519
+ for (let index = 0; index < keys.length; index += 1) {
520
+ const key = keys[index];
521
+ if (index > 0)
522
+ out.push(",");
523
+ out.push(JSON.stringify(key), ":");
524
+ writeValue(record[key], out, `${path}/${key}`, open);
525
+ }
526
+ out.push("}");
527
+ }
528
+ /**
529
+ * SR-2, enforced where it can be enforced without a schema.
530
+ *
531
+ * This writer sees JSON, not field types, so it cannot know which field is
532
+ * monetary. What it can recognise is the money *shape*: an object carrying both
533
+ * `amount` and `currency`, where `currency` already looks like an ISO 4217 code. In
534
+ * that one case the amount must be a decimal string, and a number there is refused
535
+ * rather than hashed — the whole point of SR-2 is that a float never becomes the
536
+ * thing a reviewer swore to, and the canonical form is the last place to catch it.
537
+ *
538
+ * Narrow on purpose: an unrelated object that happens to carry an `amount` and a
539
+ * `currency` that is not a three-letter uppercase code is left alone.
540
+ */
541
+ function assertNotFloatMoney(record, path) {
542
+ const currency = record["currency"];
543
+ if (typeof currency !== "string" || !MONEY_CURRENCY_PATTERN.test(currency))
544
+ return;
545
+ if (!Object.prototype.hasOwnProperty.call(record, "amount"))
546
+ return;
547
+ const amount = record["amount"];
548
+ if (typeof amount === "string" && MONEY_AMOUNT_PATTERN.test(amount))
549
+ return;
550
+ if (typeof amount === "number") {
551
+ throw new TypeError(`SR-2: money at ${at(path)} carries a JSON number amount (${String(amount)}). Money on the ` +
552
+ `wire is { amount: "<decimal string>", currency: "<ISO 4217>" }; a binary float cannot ` +
553
+ `hold the amount a reviewer read, and the canonical form is what a grant binds to.`);
554
+ }
555
+ throw new TypeError(`SR-2: money at ${at(path)} carries an amount that is not a decimal string ` +
556
+ `(${describe(amount)}). Expected ${String(MONEY_AMOUNT_PATTERN)}: no exponent, no ` +
557
+ `thousands separators, no leading "+".`);
558
+ }
559
+ /**
560
+ * The shortest round-trip decimal for `value`, written positionally.
561
+ *
562
+ * `String(n)` is the shortest decimal that parses back to `n` — the ECMAScript
563
+ * `Number::toString` algorithm, identical on every conforming runtime. It uses
564
+ * exponent notation above `1e21` and below `1e-6`; {@link expandExponent} moves the
565
+ * decimal point back into place without touching a digit.
566
+ */
567
+ function writeNumber(value, path) {
568
+ if (!Number.isFinite(value)) {
569
+ throw new RangeError(`SR-1: ${String(value)} at ${at(path)} has no decimal form, so it has no canonical form. ` +
570
+ `NaN and the infinities are not JSON numbers; a host that means "unknown" writes null.`);
571
+ }
572
+ if (Object.is(value, -0))
573
+ return "0";
574
+ return expandExponent(`${value}`);
575
+ }
576
+ /**
577
+ * `"1e+21"` to `"1000000000000000000000"`, `"1.5e-9"` to `"0.0000000015"`; anything
578
+ * without an exponent unchanged.
579
+ *
580
+ * Purely textual: the digits are the digits `Number#toString` produced, and only the
581
+ * position of the decimal point changes, so the decimal value — and therefore the
582
+ * double it parses back to — is untouched.
583
+ */
584
+ function expandExponent(literal) {
585
+ const marker = literal.indexOf("e");
586
+ if (marker === -1)
587
+ return literal;
588
+ const mantissa = literal.slice(0, marker);
589
+ const exponent = Number.parseInt(literal.slice(marker + 1), 10);
590
+ const negative = mantissa.startsWith("-");
591
+ const unsigned = negative ? mantissa.slice(1) : mantissa;
592
+ const point = unsigned.indexOf(".");
593
+ const integerDigits = point === -1 ? unsigned : unsigned.slice(0, point);
594
+ const fractionDigits = point === -1 ? "" : unsigned.slice(point + 1);
595
+ const digits = integerDigits + fractionDigits;
596
+ const pointAt = integerDigits.length + exponent;
597
+ let written;
598
+ if (pointAt <= 0)
599
+ written = `0.${"0".repeat(-pointAt)}${digits}`;
600
+ else if (pointAt >= digits.length)
601
+ written = digits + "0".repeat(pointAt - digits.length);
602
+ else
603
+ written = `${digits.slice(0, pointAt)}.${digits.slice(pointAt)}`;
604
+ return negative ? `-${written}` : written;
605
+ }
606
+ /**
607
+ * Compare two strings by Unicode **code point**, which is what SR-1 says and what
608
+ * JavaScript's `<` does not do.
609
+ *
610
+ * `<` compares UTF-16 code units, so a character above U+FFFF — an emoji, most of
611
+ * the CJK extensions, every historic script — compares as its surrogate pair,
612
+ * starting at U+D800, and sorts *before* U+E000 to U+FFFF instead of after. Every
613
+ * key in a Latin-script Affidavit sorts identically either way, which is exactly
614
+ * what makes this a trap: an implementation that used `<` would pass every test
615
+ * anyone wrote until the day a key carried an emoji, and then two implementations
616
+ * would hash the same Affidavit differently.
617
+ *
618
+ * Iterating a string yields code points, not code units, so the fix is to compare
619
+ * what the iterator yields.
620
+ */
621
+ function compareCodePoints(left, right) {
622
+ const leftPoints = left[Symbol.iterator]();
623
+ const rightPoints = right[Symbol.iterator]();
624
+ for (;;) {
625
+ const a = leftPoints.next();
626
+ const b = rightPoints.next();
627
+ if (a.done === true)
628
+ return b.done === true ? 0 : -1;
629
+ if (b.done === true)
630
+ return 1;
631
+ const x = a.value.codePointAt(0) ?? 0;
632
+ const y = b.value.codePointAt(0) ?? 0;
633
+ if (x !== y)
634
+ return x < y ? -1 : 1;
635
+ }
636
+ }
637
+ /** Every own enumerable property of `value`, or a refusal naming where it was found. */
638
+ function asRecord(value, where) {
639
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
640
+ throw new TypeError(`SR-1: expected ${where} to be an object; received ${describe(value)}.`);
641
+ }
642
+ return value;
643
+ }
644
+ /** A JSON-pointer-ish location for an error message; the root has no path. */
645
+ function at(path) {
646
+ return path === "" ? "the root value" : path;
647
+ }
648
+ /** A short, safe rendering of an arbitrary value for an error message. */
649
+ function describe(value) {
650
+ if (value === null)
651
+ return "null";
652
+ if (value === undefined)
653
+ return "undefined";
654
+ if (typeof value === "string") {
655
+ const text = value.length <= 60 ? value : `${value.slice(0, 57)}...`;
656
+ return `the string ${JSON.stringify(text)}`;
657
+ }
658
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
659
+ return `${typeof value} ${String(value)}`;
660
+ }
661
+ if (Array.isArray(value))
662
+ return `an array of ${String(value.length)}`;
663
+ return Object.prototype.toString.call(value);
664
+ }
665
+ //# sourceMappingURL=canonical.js.map