@ziffer-io/verify 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,120 @@
1
+ ZIFFER SDK LICENSE
2
+
3
+ Copyright (c) 2026 code75 SASU, Paris, France. All rights reserved.
4
+ ZIFFER is a registered trademark of code75 SASU ("code75") — code75.io · ziffer.io
5
+
6
+ This licence governs the software package it accompanies (the "SDK"): the ZIFFER client
7
+ libraries, receipt verifier, wire types and MCP server published by code75. The SDK is
8
+ proprietary software. It is not open source.
9
+
10
+ 1. DEFINITIONS
11
+ "Agreement" means the written contract under which code75 provides the ZIFFER service to
12
+ you or the organisation you act for. code75's standard form of that contract is the ZIFFER
13
+ Customer Agreement, docs/legal/ziffer-customer-agreement.md, v0.1, provided on request.
14
+ "Service" means the ZIFFER service so provided. "You" means the person or organisation
15
+ using the SDK under an Agreement.
16
+
17
+ 2. LICENCE
18
+ Subject to the Agreement and to this licence, code75 grants you a limited, non-exclusive,
19
+ non-transferable, non-sublicensable, revocable licence to install and run the SDK, in
20
+ unmodified form, solely to interact with the Service and to verify the receipts it issues,
21
+ for the term of the Agreement.
22
+
23
+ 3. RESTRICTIONS
24
+ Except as expressly permitted by section 2 or by a law that cannot be excluded by
25
+ contract, you shall not, and shall not permit anyone else to:
26
+ (a) copy, distribute, publish, sublicense, sell, rent, lease or lend the SDK, or make it
27
+ available to any third party;
28
+ (b) modify, adapt, translate or create derivative works of the SDK;
29
+ (c) reverse engineer, decompile, disassemble or otherwise attempt to derive the source
30
+ code, algorithms or protocols of the SDK or the Service;
31
+ (d) use the SDK to build, train or benchmark a product or service that competes with the
32
+ Service, or to access the Service other than through the interfaces code75 documents;
33
+ (e) circumvent, disable or interfere with any security, verification or usage-control
34
+ mechanism of the SDK or the Service;
35
+ (f) remove, obscure or alter any copyright, trademark or other proprietary notice on or
36
+ in the SDK.
37
+
38
+ 4. OWNERSHIP AND INTELLECTUAL PROPERTY
39
+ The SDK is licensed, not sold. code75 and its licensors own and retain all right, title
40
+ and interest in and to the SDK and the Service, including all copyright, patent, trade
41
+ secret, trademark and other intellectual property rights, and all improvements and
42
+ derivative works, by whomever made. Nothing in this licence transfers any such right to
43
+ you. All rights not expressly granted are reserved. Any suggestion, idea or feedback you
44
+ provide about the SDK or the Service may be used by code75 without restriction or
45
+ compensation. "ZIFFER" and "code75" and the associated logos are brands and marks of
46
+ code75; this licence grants no right to use them.
47
+
48
+ 5. THIRD-PARTY COMPONENTS
49
+ The SDK includes third-party open-source components listed in the accompanying file
50
+ THIRD-PARTY-NOTICES. Those components are licensed under their own terms, which govern
51
+ them and prevail over this licence for those components only. This licence does not
52
+ apply to them.
53
+
54
+ 6. UPDATES
55
+ code75 may release updated versions of the SDK. This licence applies to each version you
56
+ install unless a later version is accompanied by a different licence. code75 has no
57
+ obligation to provide updates or support except as stated in the Agreement.
58
+
59
+ 7. YOUR RESPONSIBILITY FOR YOUR ACTIONS AND YOUR DATA
60
+ The Service issues decisions and signed receipts; it does not perform any action. You
61
+ alone decide what your systems do with a decision, you alone hold the credentials with
62
+ which any action is performed, and you alone are responsible for every action your
63
+ systems take or fail to take, for the policy you write and sign, for the data you submit
64
+ to the Service, and for your compliance with applicable law, including data protection,
65
+ export control and sanctions laws. A decision or receipt is not legal, regulatory,
66
+ financial or professional advice and is not a guarantee that any action is lawful,
67
+ appropriate or safe.
68
+
69
+ 7A. INDEMNITY
70
+ You shall defend, indemnify and hold harmless code75, its officers, employees and
71
+ contractors from and against any claim, loss, liability, damage, cost or expense
72
+ (including reasonable legal fees) arising out of or relating to: (a) your use of the
73
+ SDK or the Service; (b) any action taken or not taken by your systems; (c) the policy,
74
+ data or content you provide; or (d) your breach of this licence, the Agreement or
75
+ applicable law.
76
+
77
+ 7B. PRE-RELEASE SOFTWARE
78
+ The SDK and the Service may be provided in a pre-release, evaluation or limited form.
79
+ You accept that such software may contain errors and may change or be withdrawn, and
80
+ you use it at your own risk.
81
+
82
+ 8. NO WARRANTY
83
+ THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY OF ANY KIND, EXPRESS,
84
+ IMPLIED OR STATUTORY, INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY,
85
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, TO THE MAXIMUM EXTENT
86
+ PERMITTED BY LAW.
87
+
88
+ 9. LIMITATION OF LIABILITY
89
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, CODE75 SHALL NOT BE LIABLE FOR ANY INDIRECT,
90
+ INCIDENTAL, SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF PROFITS,
91
+ REVENUE, DATA OR GOODWILL, ARISING OUT OF OR RELATING TO THE SDK, HOWEVER CAUSED AND
92
+ UNDER ANY THEORY OF LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
93
+ CODE75'S TOTAL LIABILITY UNDER THIS LICENCE SHALL NOT EXCEED THE AMOUNT STATED IN THE
94
+ AGREEMENT OR, IF NONE IS STATED, ONE HUNDRED EUROS (EUR 100). Nothing in this licence
95
+ excludes or limits liability that cannot be excluded or limited under applicable law.
96
+
97
+ 10. TERMINATION
98
+ This licence terminates automatically, without notice, if you breach it or when the
99
+ Agreement ends. On termination you shall stop all use of the SDK and destroy every copy
100
+ in your possession or control. Sections 3, 4, 7, 7A, 8, 9, 10, 11 and 12 survive
101
+ termination.
102
+
103
+ 11. GOVERNING LAW AND JURISDICTION
104
+ This licence is governed by the laws of France, without regard to conflict-of-law rules.
105
+ Any dispute arising out of or relating to this licence is submitted to the exclusive
106
+ jurisdiction of the competent courts of Paris, France, unless the Agreement states
107
+ otherwise.
108
+
109
+ 12. GENERAL
110
+ This licence, the Agreement and THIRD-PARTY-NOTICES are the entire terms for the SDK.
111
+ Where this licence and the Agreement conflict, the Agreement prevails. If any provision
112
+ of this licence is held unenforceable, it is enforced to the maximum extent permitted
113
+ and the remainder stays in effect. No failure or delay by code75 in exercising a right
114
+ is a waiver of it. You may not assign this licence without code75's written consent;
115
+ code75 may assign it to a successor of the ZIFFER business. Any claim by you relating to
116
+ the SDK must be brought within one (1) year after it arises, to the extent the law
117
+ allows. This licence is written in English; a translation is for convenience only and
118
+ the English text prevails.
119
+
120
+ Contact: code75 SASU, Paris — code75.io · ziffer.io
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @ziffer-io/verify
2
+
3
+ The **third implementation** of ACP receipt verification. The first two live in the
4
+ engine repository at the pin this workspace's `Cargo.toml` names: the Python reference
5
+ (`reference/src/acp_executor.py`, the readable §9.3 checklist) and Rust
6
+ (`crates/acp-decision`, the production decision path). This package lives in ziffer
7
+ **only because the engine repository is publicly frozen and ziffer CI cannot reach new
8
+ engine commits**; the mirrored corpus in `fixtures/` plus `tools/check-verify-mirror.py`
9
+ is what keeps three copies of one rule honest. Engine-home for this package is revisited
10
+ if the freeze lifts.
11
+
12
+ ```bash
13
+ npm install @ziffer-io/verify
14
+ ```
15
+
16
+ Most integrations do not install this directly. `@ziffer-io/client` depends on it and re-exports
17
+ `verifyReceipt`, so there is one verifier in your dependency tree rather than two that can
18
+ disagree. Install it on its own when you verify receipts and never call the API.
19
+
20
+ ## What it does
21
+
22
+ ```ts
23
+ import { verifyReceipt, Refusal } from '@ziffer-io/verify';
24
+
25
+ const verified = verifyReceipt(receiptJson, proposalBytes, {
26
+ classical, // Ed25519 verification key, 32 bytes, from the signed bundle (PB-12)
27
+ pq, // ML-DSA-65 verification key, 1,952 bytes, same home
28
+ minSuite: 'hybrid-ed25519-mldsa65', // CR-4 floor, from the signed manifest
29
+ });
30
+ // throws Refusal on any defect; verified.proposalHash is the verifier's OWN hash
31
+ ```
32
+
33
+ `verifyReceipt` runs exactly the **stateless** portion of the §9.3 checklist — the part
34
+ answerable with only receipt + proposal + identity: AB-0 (receipt version 3 only),
35
+ AB-5/AB-6 (signed-body exclusion and byte cap), CR-1/CR-4/CR-3 (suite known, floor by
36
+ containment, hybrid **conjunctive** — every primitive must verify), 9.3-2 (decision
37
+ domain), 9.3-3 (proposal hash **recomputed** from the caller's own bytes, never read
38
+ from the message — RES-8), 9.3-5/L-14 (temporal position and window ceiling, over
39
+ strictly parsed RFC 3339 UTC instants), WE-4/L-17 (nonce type and size).
40
+
41
+ A pass means "the stateless set found nothing", never "the receipt may be consumed":
42
+ steps 4 (policy basis), 6's CL-2 claim, 7 (grading recomputation), 7b (the quorum),
43
+ 8 (tenant scoping) and 9–10 need the signed bundle, the attester registry, a ledger and
44
+ a context store, and are deliberately absent rather than approximated. The module doc in
45
+ `src/verify.ts` carries the full table and the R/B/T classification.
46
+
47
+ Every refusal is a thrown `Refusal` whose `clause` is spelled **exactly as the engine's
48
+ Rust clause constants** spell it (`AB-0`, `AB-6`, `CR-1`, `CR-4`, `AT-8a`, `9.3-1`,
49
+ `9.3-2`, `9.3-3`, `9.3-5`, `L-14`, `WE-4`, `L-17`) — the clause is what the three
50
+ implementations are compared on.
51
+
52
+ ## What it does not do
53
+
54
+ - **It is not a receipt gate.** A pass is "the stateless set found nothing", not "this receipt may
55
+ be consumed" — the paragraph above lists the six §9.3 steps that need a signed bundle, an
56
+ attester registry, a ledger and a context store, and are absent rather than approximated.
57
+ - **It fetches nothing and trusts nothing you did not hand it.** The identity keys and the CR-4
58
+ suite floor are arguments. It will not read them from the receipt, because a verifier that takes
59
+ a derived security value from the party it is verifying is the defect class this specification
60
+ has published five corrections for (RES-8).
61
+ - **It does not hash your proposal for you.** You pass the bytes your own code passes to the API,
62
+ and the verifier recomputes the hash from them. Handing it the hash would verify nothing.
63
+ - **It has no `approve`, and no test mode that mints a receipt.** Signing lives behind keys this
64
+ package never holds.
65
+
66
+ ## Strict Ed25519, written out
67
+
68
+ `@noble/curves`' `verify` with `{ zip215: false }` enforces canonical encodings but —
69
+ like OpenSSL — does **not** refuse small-order points. The ACP-106 rule (a small-order
70
+ public key or a small-order `R` makes the verification equation stop mentioning the
71
+ message) is therefore written out in `src/ed25519.ts`, exactly as the Python reference
72
+ writes it in `verify_prim` and Rust gets from `verify_strict`. The mirrored
73
+ `fixtures/refused_ed25519.json` is the executable statement: every vector is accepted by
74
+ RFC 8032 as OpenSSL implements it and refused by all three implementations. Deleting the
75
+ guard here was observed to accept the corpus's `r_is_the_identity_point` vector.
76
+
77
+ ## Canonicalization, proven not transcribed
78
+
79
+ `src/canon.ts` reproduces the engine canon (sorted keys at every depth by **code
80
+ point** — not JS's UTF-16 unit order — no whitespace, raw UTF-8). The proof is a
81
+ triangle: `fixtures/canon_vectors.json` was generated by importing and executing the
82
+ engine's own `acp_executor.canon` (`fixtures/gen_canon_vectors.py`), the TS tests assert
83
+ byte-identity with the fixture on every case, and `tools/check-verify-mirror.py`
84
+ re-derives every case against the engine at the pin.
85
+
86
+ **Disclosed divergence (fail-closed):** the engine accepts a float *nested* in a signed
87
+ structure (the ACP-75 bug-for-bug agreement between Python and Rust). This
88
+ implementation refuses any non-integer number at any depth under `AT-8a`, because
89
+ `JSON.parse` collapses `1000.0` and `1000` into one value and reproducing the engine's
90
+ bytes would be a guess about bytes it never saw. Integers beyond `2^53−1` are refused
91
+ for the same reason (the ACP-54 integer-domain family reached from the JS side). No
92
+ conforming wire receipt carries either — every temporal field is an RFC 3339 string
93
+ since v1.3.28 (ACP-167). Both refusals are pinned by tests so the divergence cannot
94
+ vanish silently.
95
+
96
+ ## Fixtures
97
+
98
+ Byte-identical mirrors of engine files, each with a `.provenance.json` sidecar (source
99
+ path, rev, sha256 — a header *inside* the JSON would change the bytes the identity claim
100
+ is about): `refused_ed25519.json`, `python_signatures.json` (the reference's signatures,
101
+ which this side must accept — the TS leg of `tests/python_interop.rs`' claim; its seeds
102
+ also pin the test-key derivation mirror in `src/testkeys.ts`), and
103
+ `instant-type-vectors.json` (this package is the shared instant corpus's fourth
104
+ consumer). Plus the derived `canon_vectors.json` above. Re-mirror at a pin bump;
105
+ `tools/check-verify-mirror.py` refuses a mirror whose provenance rev is not the
106
+ workspace pin.
107
+
108
+ ## Running
109
+
110
+ ```sh
111
+ pnpm --filter @ziffer-io/verify test # tsc -b && node --test (27 tests)
112
+ python3 tools/check-verify-mirror.py # from the repo root; needs ACP_REPO_PATH at the pin
113
+ ```
114
+
115
+ ## Licence
116
+
117
+ Proprietary. Copyright (c) 2026 code75 SASU, Paris, France. ZIFFER is a registered trademark of code75
118
+ SASU. This package is **not open source**: it is licensed for use with the ZIFFER service under
119
+ your agreement with code75, on the terms in `LICENSE` beside this file. The third-party
120
+ open-source components it redistributes are listed in `THIRD-PARTY-NOTICES` and are governed by
121
+ their own licences.
@@ -0,0 +1,40 @@
1
+ THIRD-PARTY NOTICES for @ziffer-io/verify
2
+
3
+ GENERATED FILE -- do not edit by hand.
4
+ Regenerate with: node tools/third-party-notices.mjs
5
+ tools/release-npm.sh refuses to publish when this file is out of date.
6
+
7
+ The LICENSE beside this file governs the ZIFFER SDK itself, which is
8
+ proprietary software of code75 SASU. The components below are third-party
9
+ open-source, are redistributed under their own licences, and those licences
10
+ govern them and prevail for those components only (SDK licence, section 5).
11
+ Each entry is read from the component as it is installed, at the version
12
+ this release resolves.
13
+
14
+ 4 component(s):
15
+
16
+ ----------------------------------------------------------------------------
17
+ @noble/ciphers 2.3.0
18
+ licence: MIT
19
+ Copyright (c) 2022 Paul Miller (https://paulmillr.com)
20
+ full text: LICENSE, as distributed in the package
21
+
22
+ ----------------------------------------------------------------------------
23
+ @noble/curves 2.3.0
24
+ licence: MIT
25
+ Copyright (c) 2022 Paul Miller (https://paulmillr.com)
26
+ full text: LICENSE, as distributed in the package
27
+
28
+ ----------------------------------------------------------------------------
29
+ @noble/hashes 2.3.0
30
+ licence: MIT
31
+ Copyright (c) 2022 Paul Miller (https://paulmillr.com)
32
+ full text: LICENSE, as distributed in the package
33
+
34
+ ----------------------------------------------------------------------------
35
+ @noble/post-quantum 0.7.0
36
+ licence: MIT
37
+ Copyright (c) 2024 Paul Miller (https://paulmillr.com)
38
+ full text: LICENSE, as distributed in the package
39
+
40
+ ----------------------------------------------------------------------------
@@ -0,0 +1,61 @@
1
+ /**
2
+ * AT-8a -- the canonical bytes, the third writing of one encoding.
3
+ *
4
+ * The engine's two: `reference/src/acp_executor.py::canon` (json.dumps,
5
+ * sort_keys, separators (",", ":"), ensure_ascii=False) and
6
+ * `crates/acp-decision/src/receipt.rs::canon` (sorted keys at every depth,
7
+ * scalars through serde's writer). `fixtures/canon_vectors.json` holds bytes
8
+ * the ENGINE's own canon produced, and tools/check-verify-mirror.py re-derives
9
+ * them against the engine at the pin -- so this file agreeing with the fixture
10
+ * is this file agreeing with the engine, not with a transcription of it.
11
+ *
12
+ * # Key order is CODE-POINT order, sorted here, at every depth
13
+ *
14
+ * Python's `sort_keys=True` sorts str by code point; Rust sorts `&str` by
15
+ * UTF-8 bytes, which is the same order. JavaScript's default string comparison
16
+ * is by UTF-16 CODE UNIT, which disagrees for exactly one region: an astral
17
+ * character (>= U+10000) encodes as a surrogate pair starting at 0xD800, so it
18
+ * sorts BEFORE U+E000..U+FFFF instead of after. `compareCodePoints` below is
19
+ * written out for that reason, and the fixture's
20
+ * `astral_keys_sort_by_code_point_not_utf16_unit` case is red under a naive
21
+ * `keys.sort()`. Sorting is done HERE, never by trusting object insertion
22
+ * order -- the ACP-126 lesson (a map that is sorted "by construction" is
23
+ * sorted in exactly one build configuration) applies to JS objects verbatim:
24
+ * they preserve insertion order, and integer-like keys don't even do that.
25
+ *
26
+ * # Numbers: safe integers ONLY, and this is a disclosed divergence
27
+ *
28
+ * The engine accepts a float NESTED in a structure (top-level only is refused)
29
+ * -- the ACP-75 bug-for-bug agreement between Python and Rust. This
30
+ * implementation cannot join that agreement honestly: JSON.parse collapses
31
+ * `1000.0` and `1000` into one value, and Python/serde render the first as
32
+ * "1000.0" while JS would render "1000" -- silently different signed bytes for
33
+ * bytes the engine accepted. Guessing would be a forgery of the preimage, so
34
+ * a non-integer number is REFUSED here at any depth (AT-8a, fail closed).
35
+ * No conforming wire receipt carries one: every temporal field is an RFC 3339
36
+ * string since v1.3.28 (ACP-167). The same rule refuses integers outside
37
+ * Number.MAX_SAFE_INTEGER, where JSON.parse has already lost the digits the
38
+ * engine would have signed (the ACP-54 integer-domain family, reached from the
39
+ * JS side). Both refusals are pinned by tests so the divergence cannot vanish
40
+ * silently.
41
+ */
42
+ /** What a parsed JSON document can be. `unknown` is narrowed at the walk. */
43
+ export type Json = null | boolean | number | string | Json[] | {
44
+ [key: string]: Json;
45
+ };
46
+ /**
47
+ * Code-point comparison -- Python's `sorted()` over str, Rust's `&str` `Ord`.
48
+ *
49
+ * Iterating a string with `for..of` yields whole code points (surrogate pairs
50
+ * arrive as one unit), so comparing `codePointAt(0)` per step IS code-point
51
+ * order; the loop exists because `<` on the full strings would be UTF-16 order.
52
+ */
53
+ export declare function compareCodePoints(a: string, b: string): number;
54
+ /**
55
+ * The canonical UTF-8 bytes of a parsed JSON value.
56
+ *
57
+ * One encoding for BOTH signing preimages and id derivation, exactly as the
58
+ * reference states it: sorted keys, no whitespace, no floats, raw UTF-8.
59
+ */
60
+ export declare function canon(value: unknown): Uint8Array;
61
+ //# sourceMappingURL=canon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canon.d.ts","sourceRoot":"","sources":["../src/canon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAKH,6EAA6E;AAC7E,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEvF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB9D;AAkED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAIhD"}
package/dist/canon.js ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * AT-8a -- the canonical bytes, the third writing of one encoding.
3
+ *
4
+ * The engine's two: `reference/src/acp_executor.py::canon` (json.dumps,
5
+ * sort_keys, separators (",", ":"), ensure_ascii=False) and
6
+ * `crates/acp-decision/src/receipt.rs::canon` (sorted keys at every depth,
7
+ * scalars through serde's writer). `fixtures/canon_vectors.json` holds bytes
8
+ * the ENGINE's own canon produced, and tools/check-verify-mirror.py re-derives
9
+ * them against the engine at the pin -- so this file agreeing with the fixture
10
+ * is this file agreeing with the engine, not with a transcription of it.
11
+ *
12
+ * # Key order is CODE-POINT order, sorted here, at every depth
13
+ *
14
+ * Python's `sort_keys=True` sorts str by code point; Rust sorts `&str` by
15
+ * UTF-8 bytes, which is the same order. JavaScript's default string comparison
16
+ * is by UTF-16 CODE UNIT, which disagrees for exactly one region: an astral
17
+ * character (>= U+10000) encodes as a surrogate pair starting at 0xD800, so it
18
+ * sorts BEFORE U+E000..U+FFFF instead of after. `compareCodePoints` below is
19
+ * written out for that reason, and the fixture's
20
+ * `astral_keys_sort_by_code_point_not_utf16_unit` case is red under a naive
21
+ * `keys.sort()`. Sorting is done HERE, never by trusting object insertion
22
+ * order -- the ACP-126 lesson (a map that is sorted "by construction" is
23
+ * sorted in exactly one build configuration) applies to JS objects verbatim:
24
+ * they preserve insertion order, and integer-like keys don't even do that.
25
+ *
26
+ * # Numbers: safe integers ONLY, and this is a disclosed divergence
27
+ *
28
+ * The engine accepts a float NESTED in a structure (top-level only is refused)
29
+ * -- the ACP-75 bug-for-bug agreement between Python and Rust. This
30
+ * implementation cannot join that agreement honestly: JSON.parse collapses
31
+ * `1000.0` and `1000` into one value, and Python/serde render the first as
32
+ * "1000.0" while JS would render "1000" -- silently different signed bytes for
33
+ * bytes the engine accepted. Guessing would be a forgery of the preimage, so
34
+ * a non-integer number is REFUSED here at any depth (AT-8a, fail closed).
35
+ * No conforming wire receipt carries one: every temporal field is an RFC 3339
36
+ * string since v1.3.28 (ACP-167). The same rule refuses integers outside
37
+ * Number.MAX_SAFE_INTEGER, where JSON.parse has already lost the digits the
38
+ * engine would have signed (the ACP-54 integer-domain family, reached from the
39
+ * JS side). Both refusals are pinned by tests so the divergence cannot vanish
40
+ * silently.
41
+ */
42
+ import { CLAUSE_CANONICAL } from './clauses.js';
43
+ import { Refusal } from './refusal.js';
44
+ /**
45
+ * Code-point comparison -- Python's `sorted()` over str, Rust's `&str` `Ord`.
46
+ *
47
+ * Iterating a string with `for..of` yields whole code points (surrogate pairs
48
+ * arrive as one unit), so comparing `codePointAt(0)` per step IS code-point
49
+ * order; the loop exists because `<` on the full strings would be UTF-16 order.
50
+ */
51
+ export function compareCodePoints(a, b) {
52
+ const ia = a[Symbol.iterator]();
53
+ const ib = b[Symbol.iterator]();
54
+ for (;;) {
55
+ const na = ia.next();
56
+ const nb = ib.next();
57
+ if (na.done && nb.done)
58
+ return 0;
59
+ if (na.done)
60
+ return -1;
61
+ if (nb.done)
62
+ return 1;
63
+ const ca = na.value.codePointAt(0);
64
+ const cb = nb.value.codePointAt(0);
65
+ // codePointAt(0) on a non-empty string cannot be undefined; the guard is
66
+ // for the type system, and refusing loudly beats a silent NaN comparison.
67
+ if (ca === undefined || cb === undefined) {
68
+ throw new Refusal(CLAUSE_CANONICAL, 'key is not a comparable string');
69
+ }
70
+ if (ca !== cb)
71
+ return ca - cb;
72
+ }
73
+ }
74
+ // A type predicate, not an `as` cast (the project rules forbid the cast, and
75
+ // here it would be load-bearing): null and arrays are handled before this is
76
+ // consulted, so what remains of `object` is a plain key-value record.
77
+ function isPlainObject(v) {
78
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
79
+ }
80
+ function writeCanon(value, out) {
81
+ if (value === null || value === true || value === false) {
82
+ out.push(String(value));
83
+ return;
84
+ }
85
+ if (typeof value === 'number') {
86
+ if (!Number.isSafeInteger(value)) {
87
+ // Both halves of the disclosed divergence in the module doc: a float at
88
+ // ANY depth (the engine refuses top-level only), and an integer beyond
89
+ // 2^53-1 (JSON.parse already lost digits the engine would have signed).
90
+ throw new Refusal(CLAUSE_CANONICAL, 'number is not a safe integer: JS cannot reproduce the engine canon bytes for it');
91
+ }
92
+ // A JSON `-0` parses to negative zero here and to integer zero in Python
93
+ // and serde; String(-0) is "0", so all three emit the same byte. Noted so
94
+ // nobody "fixes" this to preserve the sign JS kept and JSON did not.
95
+ out.push(String(value));
96
+ return;
97
+ }
98
+ if (typeof value === 'string') {
99
+ // JSON.stringify escapes exactly what Python's ensure_ascii=False and
100
+ // serde_json escape: the quote, the backslash, \b \t \n \f \r, and other
101
+ // control chars as \u00xx -- everything else travels as raw UTF-8. The
102
+ // fixture's control-char and astral cases hold all three to it.
103
+ out.push(JSON.stringify(value));
104
+ return;
105
+ }
106
+ if (Array.isArray(value)) {
107
+ out.push('[');
108
+ for (let i = 0; i < value.length; i += 1) {
109
+ if (i > 0)
110
+ out.push(',');
111
+ writeCanon(value[i], out);
112
+ }
113
+ out.push(']');
114
+ return;
115
+ }
116
+ if (isPlainObject(value)) {
117
+ const obj = value;
118
+ const keys = Object.keys(obj).sort(compareCodePoints);
119
+ out.push('{');
120
+ for (let i = 0; i < keys.length; i += 1) {
121
+ const key = keys[i];
122
+ if (key === undefined)
123
+ continue; // unreachable; index is in range
124
+ if (i > 0)
125
+ out.push(',');
126
+ out.push(JSON.stringify(key));
127
+ out.push(':');
128
+ writeCanon(obj[key], out);
129
+ }
130
+ out.push('}');
131
+ return;
132
+ }
133
+ // undefined, functions, bigints, symbols: not JSON, not encodable.
134
+ throw new Refusal(CLAUSE_CANONICAL, `type ${typeof value} is not canonically encodable`);
135
+ }
136
+ /**
137
+ * The canonical UTF-8 bytes of a parsed JSON value.
138
+ *
139
+ * One encoding for BOTH signing preimages and id derivation, exactly as the
140
+ * reference states it: sorted keys, no whitespace, no floats, raw UTF-8.
141
+ */
142
+ export function canon(value) {
143
+ const out = [];
144
+ writeCanon(value, out);
145
+ return new TextEncoder().encode(out.join(''));
146
+ }
147
+ //# sourceMappingURL=canon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canon.js","sourceRoot":"","sources":["../src/canon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The refusal names, spelled EXACTLY as the engine's Rust clause constants
3
+ * spell them (crates/acp-decision/src/{receipt,decide,quorum}.rs at the pin).
4
+ *
5
+ * This is the third implementation of receipt verification, and the clause a
6
+ * refusal carries is what the three are compared ON: two verifiers that both
7
+ * refuse a forged, expired receipt -- one saying 9.3-1, the other 9.3-5 --
8
+ * have not been shown to agree on anything an operator could act on
9
+ * (decide.rs module doc). So every name here is a wire contract, not a label,
10
+ * and a constant may change only when the engine's does.
11
+ *
12
+ * The Rust constant NAME is kept beside each value so a reader diffing the two
13
+ * files sees one table, not two vocabularies.
14
+ */
15
+ /** Rust `receipt::CLAUSE_VERSION` -- AB-0, the receipt does not declare version 3. */
16
+ export declare const CLAUSE_VERSION = "AB-0";
17
+ /** Rust `receipt::CLAUSE_BODY_SIZE` -- AB-6, the signed body exceeds the custody byte cap. */
18
+ export declare const CLAUSE_BODY_SIZE = "AB-6";
19
+ /** Rust `receipt::CLAUSE_UNKNOWN_SUITE` -- CR-1, the suite name is not one this build knows. */
20
+ export declare const CLAUSE_UNKNOWN_SUITE = "CR-1";
21
+ /** Rust `receipt::CLAUSE_SUITE_FLOOR` -- CR-4, the suite does not contain every floor primitive. */
22
+ export declare const CLAUSE_SUITE_FLOOR = "CR-4";
23
+ /** Rust `receipt::CLAUSE_CANONICAL` -- AT-8a, the structure is not canonically encodable. */
24
+ export declare const CLAUSE_CANONICAL = "AT-8a";
25
+ /** Rust `receipt::CLAUSE_SIGNATURE` -- 9.3-1, the signature did not verify under the bundle key. */
26
+ export declare const CLAUSE_SIGNATURE = "9.3-1";
27
+ /** Rust `receipt::CLAUSE_DECISION` -- 9.3-2, the decision was not ALLOW. */
28
+ export declare const CLAUSE_DECISION = "9.3-2";
29
+ /**
30
+ * Rust `decide.rs` spells 9.3-3 as a literal at the refusal site, not a
31
+ * constant; the value is the contract either way. Receipt not bound to this
32
+ * proposal (B-1a).
33
+ */
34
+ export declare const CLAUSE_PROPOSAL_BINDING = "9.3-3";
35
+ /** Rust literal in `decide.rs` step 5 -- temporal position (missing, malformed, expired, future). */
36
+ export declare const CLAUSE_TEMPORAL = "9.3-5";
37
+ /**
38
+ * Rust literal in `decide.rs` step 5 -- L-14, the validity-window ceiling.
39
+ * A SEPARATE clause from 9.3-5: a receipt whose window is 10x too long is an
40
+ * attacker widening the interval a stolen receipt is usable in (Y2), and
41
+ * calling that "expired" would tell an operator the wrong thing.
42
+ */
43
+ export declare const CLAUSE_VALIDITY_WINDOW = "L-14";
44
+ /** Rust `quorum::CLAUSE_WIRE_TYPE` -- WE-4, the nonce is not `b64:` + RFC 4648 s4 base64. */
45
+ export declare const CLAUSE_WIRE_TYPE = "WE-4";
46
+ /**
47
+ * Rust `decide::CLAUSE_RECEIPT_NONCE_SIZE` -- L-17, the receipt nonce is
48
+ * well-formed but not 128-bit. The receipt nonce's OWN size clause, never
49
+ * AT-1 (which sizes the attestation nonce): one number, two clauses, and the
50
+ * refusal carries the field's (ACP-88/ACP-89).
51
+ */
52
+ export declare const CLAUSE_RECEIPT_NONCE_SIZE = "L-17";
53
+ //# sourceMappingURL=clauses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clauses.d.ts","sourceRoot":"","sources":["../src/clauses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sFAAsF;AACtF,eAAO,MAAM,cAAc,SAAS,CAAC;AACrC,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,SAAS,CAAC;AACvC,gGAAgG;AAChG,eAAO,MAAM,oBAAoB,SAAS,CAAC;AAC3C,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,SAAS,CAAC;AACzC,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,UAAU,CAAC;AACxC,oGAAoG;AACpG,eAAO,MAAM,gBAAgB,UAAU,CAAC;AACxC,4EAA4E;AAC5E,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAC/C,qGAAqG;AACrG,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,SAAS,CAAC;AAC7C,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,SAAS,CAAC;AACvC;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,SAAS,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The refusal names, spelled EXACTLY as the engine's Rust clause constants
3
+ * spell them (crates/acp-decision/src/{receipt,decide,quorum}.rs at the pin).
4
+ *
5
+ * This is the third implementation of receipt verification, and the clause a
6
+ * refusal carries is what the three are compared ON: two verifiers that both
7
+ * refuse a forged, expired receipt -- one saying 9.3-1, the other 9.3-5 --
8
+ * have not been shown to agree on anything an operator could act on
9
+ * (decide.rs module doc). So every name here is a wire contract, not a label,
10
+ * and a constant may change only when the engine's does.
11
+ *
12
+ * The Rust constant NAME is kept beside each value so a reader diffing the two
13
+ * files sees one table, not two vocabularies.
14
+ */
15
+ /** Rust `receipt::CLAUSE_VERSION` -- AB-0, the receipt does not declare version 3. */
16
+ export const CLAUSE_VERSION = 'AB-0';
17
+ /** Rust `receipt::CLAUSE_BODY_SIZE` -- AB-6, the signed body exceeds the custody byte cap. */
18
+ export const CLAUSE_BODY_SIZE = 'AB-6';
19
+ /** Rust `receipt::CLAUSE_UNKNOWN_SUITE` -- CR-1, the suite name is not one this build knows. */
20
+ export const CLAUSE_UNKNOWN_SUITE = 'CR-1';
21
+ /** Rust `receipt::CLAUSE_SUITE_FLOOR` -- CR-4, the suite does not contain every floor primitive. */
22
+ export const CLAUSE_SUITE_FLOOR = 'CR-4';
23
+ /** Rust `receipt::CLAUSE_CANONICAL` -- AT-8a, the structure is not canonically encodable. */
24
+ export const CLAUSE_CANONICAL = 'AT-8a';
25
+ /** Rust `receipt::CLAUSE_SIGNATURE` -- 9.3-1, the signature did not verify under the bundle key. */
26
+ export const CLAUSE_SIGNATURE = '9.3-1';
27
+ /** Rust `receipt::CLAUSE_DECISION` -- 9.3-2, the decision was not ALLOW. */
28
+ export const CLAUSE_DECISION = '9.3-2';
29
+ /**
30
+ * Rust `decide.rs` spells 9.3-3 as a literal at the refusal site, not a
31
+ * constant; the value is the contract either way. Receipt not bound to this
32
+ * proposal (B-1a).
33
+ */
34
+ export const CLAUSE_PROPOSAL_BINDING = '9.3-3';
35
+ /** Rust literal in `decide.rs` step 5 -- temporal position (missing, malformed, expired, future). */
36
+ export const CLAUSE_TEMPORAL = '9.3-5';
37
+ /**
38
+ * Rust literal in `decide.rs` step 5 -- L-14, the validity-window ceiling.
39
+ * A SEPARATE clause from 9.3-5: a receipt whose window is 10x too long is an
40
+ * attacker widening the interval a stolen receipt is usable in (Y2), and
41
+ * calling that "expired" would tell an operator the wrong thing.
42
+ */
43
+ export const CLAUSE_VALIDITY_WINDOW = 'L-14';
44
+ /** Rust `quorum::CLAUSE_WIRE_TYPE` -- WE-4, the nonce is not `b64:` + RFC 4648 s4 base64. */
45
+ export const CLAUSE_WIRE_TYPE = 'WE-4';
46
+ /**
47
+ * Rust `decide::CLAUSE_RECEIPT_NONCE_SIZE` -- L-17, the receipt nonce is
48
+ * well-formed but not 128-bit. The receipt nonce's OWN size clause, never
49
+ * AT-1 (which sizes the attestation nonce): one number, two clauses, and the
50
+ * refusal carries the field's (ACP-88/ACP-89).
51
+ */
52
+ export const CLAUSE_RECEIPT_NONCE_SIZE = 'L-17';
53
+ //# sourceMappingURL=clauses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clauses.js","sourceRoot":"","sources":["../src/clauses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,8FAA8F;AAC9F,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AACvC,gGAAgG;AAChG,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAC3C,oGAAoG;AACpG,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AACzC,6FAA6F;AAC7F,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AACxC,oGAAoG;AACpG,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AACxC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AACvC;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAC/C,qGAAqG;AACrG,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AACvC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAC7C,6FAA6F;AAC7F,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AACvC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC"}