@ziffer-io/verify 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -107
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -1,121 +1,71 @@
|
|
|
1
1
|
# @ziffer-io/verify
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
3
|
+
Verify a ZIFFER decision receipt in your own process, against a key you configured yourself.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
11
6
|
|
|
12
7
|
```bash
|
|
13
8
|
npm install @ziffer-io/verify
|
|
14
9
|
```
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
`verifyReceipt`, so
|
|
18
|
-
disagree. Install it on its own when you verify receipts and never call the API.
|
|
11
|
+
Node 22 or later. If you also call the ZIFFER API, install `@ziffer-io/client` instead. It
|
|
12
|
+
depends on this package and re-exports `verifyReceipt`, so one verifier is in your tree.
|
|
19
13
|
|
|
20
|
-
##
|
|
14
|
+
## Quickstart
|
|
21
15
|
|
|
22
16
|
```ts
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { verifyReceipt, type TrustAnchor } from '@ziffer-io/verify';
|
|
19
|
+
|
|
20
|
+
const keys = JSON.parse(readFileSync(process.env.ZIFFER_TRUST_ANCHOR, 'utf8'));
|
|
21
|
+
const anchor: TrustAnchor = {
|
|
22
|
+
classical: Buffer.from(keys.ed25519_pk_hex, 'hex'),
|
|
23
|
+
pq: Buffer.from(keys.mldsa65_pk_hex, 'hex'),
|
|
24
|
+
minSuite: process.env.ZIFFER_SUITE_FLOOR,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const proposalBytes = new TextEncoder().encode(JSON.stringify(proposal));
|
|
28
|
+
const verified = verifyReceipt(receipt, proposalBytes, anchor);
|
|
29
|
+
// It returned, so the receipt holds. verified.proposalHash is this verifier's own hash.
|
|
30
|
+
await bank.transfer(amount, toAccount); // your line, unchanged
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
```
|
|
33
|
+
It reaches no network and trusts nothing it was not handed. The keys and the suite floor are
|
|
34
|
+
arguments you pass in. The proposal hash is recomputed from your bytes, never read from the
|
|
35
|
+
receipt.
|
|
36
|
+
Key order and spacing do not matter, because the verifier canonicalises your bytes itself.
|
|
37
|
+
|
|
38
|
+
`verifyReceipt` checks the answer. Your own `if` is what stops the action.
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
| Variable | What it is | Where the value comes from |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `ZIFFER_TRUST_ANCHOR` | Path to the public key file your receipts are signed under. | We give you the file. Take it from us, never from the API you are checking. |
|
|
45
|
+
| `ZIFFER_SUITE_FLOOR` | The weakest signature suite you will accept. | You choose it. There is no default. |
|
|
46
|
+
|
|
47
|
+
## When a request is refused
|
|
48
|
+
|
|
49
|
+
Every refusal is a thrown `Refusal` whose `clause` names the rule that fired; the table of every
|
|
50
|
+
clause, what it means and what to do is at https://ziffer.io/docs/refusals. Narrow with
|
|
51
|
+
`instanceof Refusal`, record the clause, and do not retry it.
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
- Quickstart: https://ziffer.io/docs/quickstart
|
|
56
|
+
- Integrating the SDK: https://ziffer.io/docs/developers/sdk
|
|
57
|
+
- Every refusal: https://ziffer.io/docs/refusals
|
|
58
|
+
- The specification: https://ziffer.io/docs/specification
|
|
59
|
+
- Glossary: https://ziffer.io/docs/glossary
|
|
60
|
+
|
|
61
|
+
## Support
|
|
62
|
+
|
|
63
|
+
Write to hello@ziffer.io. Your trust anchor file and your suite floor come from us. So does an
|
|
64
|
+
answer about a refusal you cannot explain.
|
|
114
65
|
|
|
115
|
-
##
|
|
66
|
+
## License
|
|
116
67
|
|
|
117
|
-
Proprietary. Copyright (c) 2026 code75 SASU, Paris, France. ZIFFER is a registered trademark of
|
|
118
|
-
SASU. This package is
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
their own licences.
|
|
68
|
+
Proprietary. Copyright (c) 2026 code75 SASU, Paris, France. ZIFFER is a registered trademark of
|
|
69
|
+
code75 SASU. This package is not open source. Its use is governed by your agreement with code75
|
|
70
|
+
and by `LICENSE` beside this file. The open-source components it redistributes are listed in
|
|
71
|
+
`THIRD-PARTY-NOTICES`, under their own licences.
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziffer-io/verify",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Verify a ZIFFER decision receipt in your own process, against a key you configured.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ziffer",
|
|
7
|
+
"agent",
|
|
8
|
+
"ai-agent",
|
|
9
|
+
"guardrail",
|
|
10
|
+
"approval",
|
|
11
|
+
"receipt"
|
|
12
|
+
],
|
|
5
13
|
"author": "code75 SASU",
|
|
6
14
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
-
"comment-license": "The SDK is proprietary (code75 SASU). \"license\" is the SPDX escape hatch for exactly this case: there is no SPDX identifier for these terms, so the field points at the file that states them, and LICENSE ships in the tarball beside THIRD-PARTY-NOTICES. Do not put an OSI identifier here -- Apache-2.0 stood in these four files until ACP-214 and was wrong the whole time.",
|
|
8
15
|
"type": "module",
|
|
9
16
|
"main": "./dist/index.js",
|
|
10
17
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"comment-exports": "The `./dist/*` entry is not laziness and removing it breaks a caller. `exports` is a gate as well as a map: declaring only `.` makes every other path unreachable, and packages/mcp/src/tools.test.ts imports `@ziffer-io/verify/dist/testkeys.js` ON PURPOSE -- testkeys is deliberately absent from index.ts so it cannot be reached by accident, and the deep import is how the MCP package's tests get a signing key without a second definition of one identity. That import resolves today because there is no exports map; adding a `.`-only one would break it at publish time, which is the class of defect an exports map introduces rather than prevents. So the dist is declared addressable, and the reason is here rather than in a reviewer's memory.",
|
|
12
18
|
"exports": {
|
|
13
19
|
".": {
|
|
14
20
|
"types": "./dist/index.d.ts",
|
|
@@ -34,12 +40,9 @@
|
|
|
34
40
|
"publishConfig": {
|
|
35
41
|
"access": "public"
|
|
36
42
|
},
|
|
37
|
-
"comment-no-provenance": "There is deliberately no \"provenance\": true here, and it was removed rather than never added. npm generates a provenance attestation only from a recognised CI runner, and its documentation states plainly that provenance is NOT SUPPORTED for private repositories (docs.npmjs.com/trusted-publishers, read 2026-09-03) -- ziffer-hq/ziffer is private (`gh api` says so). Setting the flag does not degrade to a warning: npm attempts the attestation and the publish FAILS, so the field would have broken the operator's very first publish from a laptop and every CI publish after it. tools/release-npm.sh asserts the field stays absent, and that assertion is the thing to delete on the day this repository becomes public -- at which point trusted publishing generates provenance on its own, with no flag at all.",
|
|
38
43
|
"ziffer": {
|
|
39
|
-
"enginePin": "
|
|
44
|
+
"enginePin": "8ae262196dbe75210a975a456785df5d9f0524a3"
|
|
40
45
|
},
|
|
41
|
-
"comment-enginePin": "The engine commit whose clause spellings, canon and mirrored corpus this implementation is bound to. A COPY of the rev in Cargo.toml, which is the one authority tools/guard.sh reads; tools/release-npm.sh refuses to release when the two differ, by name. tools/bump-pin.sh does not move this field -- see packages/types/package.json for why and what closing it costs.",
|
|
42
|
-
"comment-deps": "Exactly the two signature primitives, at the versions services/approval already pins -- the DR-2 permitted common set minus the wire types, which this package does not consume: it verifies raw parsed JSON, and typing the wire is the client's (@ziffer-io/client) business. Adding anything else needs written justification (ACP-197 runbook section 5).",
|
|
43
46
|
"dependencies": {
|
|
44
47
|
"@noble/curves": "2.3.0",
|
|
45
48
|
"@noble/post-quantum": "0.7.0"
|
|
@@ -51,7 +54,7 @@
|
|
|
51
54
|
"scripts": {
|
|
52
55
|
"build": "tsc -b",
|
|
53
56
|
"typecheck": "tsc -b",
|
|
54
|
-
"pretest": "tsc -b && if ! find dist -name '*.test.js' -print -quit | grep -q .; then echo '
|
|
57
|
+
"pretest": "tsc -b && if ! find dist -name '*.test.js' -print -quit | grep -q .; then echo 'no compiled test file under dist/ -- node --test reports 0 tests and exits 0, so this package would go green having run nothing' >&2; exit 1; fi",
|
|
55
58
|
"test": "tsc -b && node --test \"dist/**/*.test.js\""
|
|
56
59
|
}
|
|
57
60
|
}
|