agent-receipt-verify 0.2.3 → 0.2.5
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 +38 -3
- package/bin/cli.mjs +41 -24
- package/package.json +5 -3
- package/src/acceptance-binding.mjs +309 -0
- package/src/settlement-link-vectors.mjs +124 -11
- package/src/verify-delivery.mjs +507 -0
- package/src/x402-conformance-check.mjs +41 -5
- package/src/x402-discovery-census.mjs +24 -1
- package/src/x402-offer-receipt.mjs +49 -3
- package/tools/naar-kanon-vorm.mjs +172 -0
- package/tools/pack-inhoud.mjs +45 -1
- package/vectors/acceptance-binding/crossed-deliverable.json +40 -0
- package/vectors/acceptance-binding/pairs-same-deliverable.json +39 -0
- package/vectors/acceptance-binding/real-record-as-issued.json +68 -0
- package/vectors/acceptance-binding/real-record-crossed-deliverable.json +65 -0
- package/vectors/acceptance-binding/real-record-crossed-x402-keys.json +74 -0
- package/vectors/acceptance-binding/real-record-with-producer-key.json +66 -0
- package/vectors/acceptance-binding/real-record-x402-keys-match.json +76 -0
- package/vectors/settlement-link-kanon/README.md +69 -0
- package/vectors/settlement-link-kanon/find-candidates-onchain.json +43 -0
- package/vectors/settlement-link-kanon/no-candidates-not-measured.json +42 -0
- package/vectors/settlement-link-kanon/opens-live-2.json +47 -0
- package/vectors/settlement-link-kanon/opens-live-3.json +47 -0
- package/vectors/settlement-link-kanon/opens-live.json +47 -0
- package/vectors/settlement-link-kanon/two-identical-payments.json +76 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live-2.json +46 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live-3.json +46 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live-2.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live-3.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live.json +46 -0
package/README.md
CHANGED
|
@@ -77,11 +77,23 @@ vectors, that is interop. One implementation agreeing with itself proves nothing
|
|
|
77
77
|
| x402 settlement link (§5.6.4, same proposal) | own tests plus three live settlements | 22 offline, 3 live | yes, each resolves to its own settlement |
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
|
-
npm test # all of the above, no
|
|
80
|
+
npm test # all of the above. No account, no key, and nobody else's server
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Node built-ins only, apart from keccak256.
|
|
84
|
-
|
|
83
|
+
Node built-ins only, apart from keccak256. Two honest footnotes, because "no network"
|
|
84
|
+
was not quite true and is the kind of claim this package should not make loosely:
|
|
85
|
+
|
|
86
|
+
- One case dials a closed port on localhost on purpose, to prove that a dead address
|
|
87
|
+
reports not-measured instead of crashing the tool. That is a socket, but it leaves
|
|
88
|
+
the machine alone and reaches no third party.
|
|
89
|
+
- Until recently the suite also fetched a live discovery index belonging to someone
|
|
90
|
+
else, roughly twenty thousand entries, because one test handed an unknown flag to
|
|
91
|
+
`census` and `census` ignored unknown flags and went to work. That flag is now
|
|
92
|
+
refused, so the suite finishes in about eleven seconds and contacts nobody. A single
|
|
93
|
+
index round alone used to take over thirty.
|
|
94
|
+
|
|
95
|
+
Only the settlement link below reaches a real network, and then only a public,
|
|
96
|
+
read-only RPC.
|
|
85
97
|
|
|
86
98
|
Confirmed from the other side, which matters more than the table: the author of
|
|
87
99
|
the x402 specification ran their own implementation, which shares no code with
|
|
@@ -98,6 +110,7 @@ taken on trust:
|
|
|
98
110
|
```sh
|
|
99
111
|
npx -y agent-receipt-verify kanon
|
|
100
112
|
npx -y agent-receipt-verify beleid
|
|
113
|
+
npx -y agent-receipt-verify link-vectors
|
|
101
114
|
```
|
|
102
115
|
|
|
103
116
|
The first checks our EIP-3009 verifier against the ten published vectors of
|
|
@@ -107,6 +120,28 @@ about one nonce reused across two payers. Those are the two open questions in
|
|
|
107
120
|
[x402#2725](https://github.com/x402-foundation/x402/issues/2725), and the command
|
|
108
121
|
prints exactly where the two possible answers diverge and where they do not.
|
|
109
122
|
|
|
123
|
+
The third runs twelve vectors for the half AFTER paying: does this receipt belong to
|
|
124
|
+
that one settlement. Eleven of them need no network at all; the twelfth reads the chain
|
|
125
|
+
and is marked and skipped rather than reported as a failure, because a check that could
|
|
126
|
+
not run says nothing about the receipt. The same twelve also exist in the field layout
|
|
127
|
+
of the Kanon vectors, in `vectors/settlement-link-kanon`, so the two corpora can sit
|
|
128
|
+
side by side.
|
|
129
|
+
|
|
130
|
+
Two more commands, both of which measure other people's endpoints rather than our own,
|
|
131
|
+
and both read-only:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
npx -y agent-receipt-verify census [out.json]
|
|
135
|
+
npx -y agent-receipt-verify liveness [out.json]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`census` counts a discovery index per HOST rather than per resource, so a party with
|
|
139
|
+
three hundred paths does not outweigh three hundred parties with one. `liveness` takes
|
|
140
|
+
that census and asks each host whether it still answers 402 today, because a
|
|
141
|
+
registration in an index is not a promise that the endpoint is alive. Both are slow by
|
|
142
|
+
design: they pause between requests, because the hosts in that list did not ask to be
|
|
143
|
+
measured.
|
|
144
|
+
|
|
110
145
|
## Linking a receipt to its settlement
|
|
111
146
|
|
|
112
147
|
A receipt with an empty `transaction` field can be matched to a payment only by
|
package/bin/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// BEWUST GEEN NPM-PUBLICATIE NODIG. npx kan een pakket rechtstreeks uit een git-repo
|
|
10
10
|
// draaien, dus dit werkt zonder dat er iets op npm staat:
|
|
11
11
|
//
|
|
12
|
-
// npx
|
|
12
|
+
// npx -y agent-receipt-verify https://een-eindpunt.example/pad
|
|
13
13
|
//
|
|
14
14
|
// Dat is geen trucje maar een echte eigenschap van npx, en het haalt een hele
|
|
15
15
|
// publicatie-stap plus een naamclaim weg. Het betekent ook dat niemand een
|
|
@@ -23,45 +23,61 @@
|
|
|
23
23
|
const args = process.argv.slice(2)
|
|
24
24
|
const eerste = args[0]
|
|
25
25
|
|
|
26
|
+
// De helptekst is ENGELS, en dat is geen voorkeur maar een gemeten noodzaak. Koud
|
|
27
|
+
// gedraaid vanuit een lege map op 18 oktober 2026 bleek het eerste scherm dat een vreemde
|
|
28
|
+
// ziet half Nederlands, terwijl iedereen die dit gereedschap in de x402- en
|
|
29
|
+
// erc-8004-draden gaat draaien Engels schrijft. Het meetresultaat zelf was al Engels, dus
|
|
30
|
+
// alleen de ingang stond nog in de weg. En de regel over wat dit NIET meet stond midden
|
|
31
|
+
// in de lijst in plaats van eronder, waar een lezer hem pas leest nadat hij weet wat het
|
|
32
|
+
// wel doet.
|
|
26
33
|
function gebruik() {
|
|
27
|
-
console.log('agent-receipt-verify,
|
|
34
|
+
console.log('agent-receipt-verify, an independent check on receipts for agent payments')
|
|
28
35
|
console.log('')
|
|
29
|
-
console.log(' npx
|
|
30
|
-
console.log('
|
|
31
|
-
console.log('
|
|
32
|
-
console.log('
|
|
36
|
+
console.log(' npx -y agent-receipt-verify <url> [url ...]')
|
|
37
|
+
console.log(' measures an x402 endpoint against the ladder in CONFORMANCE.md (levels 0')
|
|
38
|
+
console.log(' to 2, without paying). Levels 3 to 5 need a real settlement and come back')
|
|
39
|
+
console.log(' as NOT MEASURED, never as a failure.')
|
|
33
40
|
console.log('')
|
|
34
|
-
console.log(' npx
|
|
35
|
-
console.log('
|
|
41
|
+
console.log(' npx -y agent-receipt-verify census [out.json]')
|
|
42
|
+
console.log(' counts a discovery index per host instead of per resource.')
|
|
36
43
|
console.log('')
|
|
37
|
-
console.log(' npx
|
|
38
|
-
console.log('
|
|
44
|
+
console.log(' npx -y agent-receipt-verify link --receipt=<file>')
|
|
45
|
+
console.log(' for a version 2 receipt, finds the settlement it belongs to on chain.')
|
|
39
46
|
console.log('')
|
|
40
|
-
console.log(' npx
|
|
41
|
-
console.log('
|
|
42
|
-
console.log('
|
|
43
|
-
console.log('
|
|
47
|
+
console.log(' npx -y agent-receipt-verify mcp')
|
|
48
|
+
console.log(' runs as an MCP server over stdio, so an agent CALLS this check instead of')
|
|
49
|
+
console.log(' being advised to run it. Two tools: check an endpoint before you pay, and')
|
|
50
|
+
console.log(' link a receipt to a real settlement.')
|
|
44
51
|
console.log('')
|
|
45
|
-
console.log(' npx
|
|
46
|
-
console.log('
|
|
52
|
+
console.log(' npx -y agent-receipt-verify inam <vectors.json>')
|
|
53
|
+
console.log(' checks countersigned receipts against published vectors.')
|
|
47
54
|
console.log('')
|
|
48
|
-
console.log('
|
|
49
|
-
console.log('eerlijkheid van de partij die het afgaf. Zie CONFORMANCE.md voor wat met opzet')
|
|
50
|
-
console.log('niet gemeten wordt.')
|
|
51
|
-
console.log('')
|
|
52
|
-
console.log(' npx github:holistis/agent-receipt-verify kanon')
|
|
55
|
+
console.log(' npx -y agent-receipt-verify kanon')
|
|
53
56
|
console.log(' cross-check our independent EIP-3009 verifier against the ten published')
|
|
54
57
|
console.log(' vectors of iamonuwa/kanon, offline, no account and no key.')
|
|
55
58
|
console.log('')
|
|
56
|
-
console.log(' npx
|
|
59
|
+
console.log(' npx -y agent-receipt-verify beleid')
|
|
57
60
|
console.log(' the same ten vectors under both amount policies, plus the two cases no')
|
|
58
61
|
console.log(' published vector covers: overpayment, and one nonce across two payers.')
|
|
59
62
|
console.log(' Those two are the open questions in x402 issue 2725.')
|
|
60
63
|
console.log('')
|
|
61
|
-
console.log(' npx
|
|
64
|
+
console.log(' npx -y agent-receipt-verify liveness [out.json]')
|
|
65
|
+
console.log(' takes the census and asks each host whether it still answers 402 today.')
|
|
66
|
+
console.log(' A registration in an index is not a promise that the endpoint is alive.')
|
|
67
|
+
console.log('')
|
|
68
|
+
console.log(' npx -y agent-receipt-verify verify-delivery --answer=<file> --endpoint=<url>')
|
|
69
|
+
console.log(' reads, at the source, whether a paid answer is TRUE. The location is')
|
|
70
|
+
console.log(' DERIVED from --endpoint and never taken from the answer, because a seller')
|
|
71
|
+
console.log(' who picks both the token and the place can stage any outcome.')
|
|
72
|
+
console.log('')
|
|
73
|
+
console.log(' npx -y agent-receipt-verify link-vectors')
|
|
62
74
|
console.log(' twelve vectors for the half AFTER paying: does this receipt belong to')
|
|
63
75
|
console.log(' that one settlement? Eleven run offline, the chain-reading one is')
|
|
64
76
|
console.log(' marked and skipped rather than failed.')
|
|
77
|
+
console.log('')
|
|
78
|
+
console.log('What this does NOT do: it judges the SHAPE of a receipt, not the honesty of')
|
|
79
|
+
console.log('the party that issued it. See CONFORMANCE.md for what is deliberately left')
|
|
80
|
+
console.log('unmeasured.')
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
// Het argument bepaalt welke module draait. De modules zijn ook los te draaien en te
|
|
@@ -76,6 +92,7 @@ const onderdelen = {
|
|
|
76
92
|
kanon: '../src/eip3009-autorisatie.mjs',
|
|
77
93
|
beleid: '../bench/eip3009-beleid.mjs',
|
|
78
94
|
'link-vectors': '../src/settlement-link-vectors.mjs',
|
|
95
|
+
'verify-delivery': '../src/verify-delivery.mjs',
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
if (!eerste || eerste === '--help' || eerste === '-h' || eerste === 'help') {
|
|
@@ -94,7 +111,7 @@ if (!eerste || eerste === '--help' || eerste === '-h' || eerste === 'help') {
|
|
|
94
111
|
process.argv = [process.argv[0], pad, ...args]
|
|
95
112
|
await import('../src/x402-conformance-check.mjs')
|
|
96
113
|
} else {
|
|
97
|
-
console.log('
|
|
114
|
+
console.log('unknown command: ' + eerste)
|
|
98
115
|
console.log('')
|
|
99
116
|
gebruik()
|
|
100
117
|
process.exitCode = 2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-receipt-verify",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Checks x402 endpoint conformance before an agent pays, and links the receipt to its settlement.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"node": ">=20"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/interop.mjs && node test/canonical.mjs && node test/link-onchain.mjs && node test/profiel.mjs && node test/eip3009.mjs && node test/conformance.mjs && node test/jws-offer.mjs && node test/census.mjs && node test/poortwachters.mjs && node test/metingen-bundel.mjs && node test/mcp-server.mjs && node test/cli.mjs && node test/bundel.mjs && node test/settlement-link-vectors.mjs",
|
|
11
|
+
"test": "node test/interop.mjs && node test/canonical.mjs && node test/link-onchain.mjs && node test/profiel.mjs && node test/eip3009.mjs && node test/conformance.mjs && node test/jws-offer.mjs && node test/census.mjs && node test/poortwachters.mjs && node test/metingen-bundel.mjs && node test/mcp-server.mjs && node test/cli.mjs && node test/bundel.mjs && node test/settlement-link-vectors.mjs && node test/naar-kanon-vorm.mjs && node test/verify-delivery.mjs && node test/acceptance-binding.mjs",
|
|
12
12
|
"link-example": "node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia.json && node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia-2.json && node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia-3.json",
|
|
13
13
|
"conformance": "node src/x402-conformance-check.mjs",
|
|
14
14
|
"bench": "node bench/robustness.mjs",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"bundel": "node tools/bundel-bouw.mjs build/bundle",
|
|
21
21
|
"kanon": "node src/eip3009-autorisatie.mjs --vectoren",
|
|
22
22
|
"bench-eip3009": "node bench/eip3009-beleid.mjs",
|
|
23
|
-
"link-vectors": "node src/settlement-link-vectors.mjs"
|
|
23
|
+
"link-vectors": "node src/settlement-link-vectors.mjs",
|
|
24
|
+
"verify-delivery": "node src/verify-delivery.mjs",
|
|
25
|
+
"kanon-vorm": "node tools/naar-kanon-vorm.mjs"
|
|
24
26
|
},
|
|
25
27
|
"optionalDependencies": {
|
|
26
28
|
"ethers": "^6.17.0"
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Koppelt laag 3 aan laag 4: hoort dit acceptatierecord bij dit bonnetje.
|
|
2
|
+
//
|
|
3
|
+
// WAAROM DIT EEN EIGEN BESTAND IS EN NIET BIJ DE SETTLEMENT-LINK HOORT. De
|
|
4
|
+
// settlement-link-vectoren beantwoorden een andere vraag: opent de gezouten belofte in
|
|
5
|
+
// een bonnetje op precies een afrekening op de keten. Dat is betaalbewijs. Een
|
|
6
|
+
// acceptatierecord beantwoordt iets anders: heeft een onafhankelijke partij het geleverde
|
|
7
|
+
// bestand getoetst aan criteria die al vastlagen. Een uitgever die beide door elkaar
|
|
8
|
+
// haalt levert een lezer een vlaggetje "onderbouwd" waarvan niemand meer kan zien welke
|
|
9
|
+
// van de twee vragen nu eigenlijk beantwoord is. Daarom twee sets, twee uitkomsten.
|
|
10
|
+
//
|
|
11
|
+
// WAT HIER WEL GEMETEN WORDT. Uitsluitend de BINDING. Noemen het bonnetje en het
|
|
12
|
+
// acceptatierecord hetzelfde opgeleverde bestand, dezelfde machtiging en dezelfde belofte,
|
|
13
|
+
// en lagen de criteria vast voordat er getoetst werd.
|
|
14
|
+
//
|
|
15
|
+
// WAT HIER MET OPZET NIET GEMETEN WORDT, en dat is geen tekortkoming maar de grens van
|
|
16
|
+
// deze laag. Wij toetsen de handtekening van het acceptatierecord NIET, en wij
|
|
17
|
+
// beoordelen zijn criteria NIET. Dat is het werk van de controleur die het record
|
|
18
|
+
// uitgaf, met zijn eigen sleutel en zijn eigen verifieerder. Wie hier een groen antwoord
|
|
19
|
+
// krijgt weet dus dat de twee bewijsstukken over hetzelfde gaan, en niet dat het
|
|
20
|
+
// acceptatierecord zelf klopt. Die twee door elkaar halen is precies de fout die deze
|
|
21
|
+
// module bestaat om te voorkomen.
|
|
22
|
+
//
|
|
23
|
+
// GEMETEN OP EEN ECHT RECORD, 18 oktober 2026, en dat veranderde deze module wezenlijk.
|
|
24
|
+
// De uitgever publiceerde een live record (invinoveritas, trilatic.acceptance-receipt/1.2).
|
|
25
|
+
// Daaruit bleek dat onze eerste aannames over de VORM niet klopten:
|
|
26
|
+
//
|
|
27
|
+
// 1. De hash van het bonnetje staat BOVENIN de sidecar, naast `bindings`, niet erin.
|
|
28
|
+
// 2. De hash van het opgeleverde bestand staat in het ONDERTEKENDE record, op
|
|
29
|
+
// `record.receipt.files.remediated.sha256`, en helemaal niet in de sidecar.
|
|
30
|
+
// 3. De partijen staan genest in `record.receipt.parties`, met een `key_id` dat leeg
|
|
31
|
+
// kan zijn, en niet als twee platte tekstvelden.
|
|
32
|
+
// 4. Het tijdstip van de toets heet `issued_utc`, niet `checked_at`.
|
|
33
|
+
// 5. De x402-velden zijn TWEE aparte sleutels, en dat is met opzet zo: een belofte mag
|
|
34
|
+
// nooit met een nonce vergeleken worden. `x402_receipt_commitment` hoort bij
|
|
35
|
+
// `authorizationCommitment`, `x402_authorization_nonce` bij `authorizationNonce`.
|
|
36
|
+
//
|
|
37
|
+
// Daarom leest deze module nu de ECHTE vorm, met de vlakke vorm uit onze eerste
|
|
38
|
+
// vectoren als tweede mogelijkheid. Een module die alleen zijn eigen verzonnen vorm
|
|
39
|
+
// aankan, toetst niets in de wereld.
|
|
40
|
+
//
|
|
41
|
+
// EEN AANNAME DIE WEG IS. Onze eerste versie nam aan dat een machtiging in de sidecar
|
|
42
|
+
// gelijk zou zijn aan de nonce in het bonnetje, en las daarvoor ook een generiek veld
|
|
43
|
+
// `authorization`. Dat generieke veld is nu bewust GESCHRAPT. De uitgever splitste de
|
|
44
|
+
// sleutels juist om te voorkomen dat een belofte met een nonce vergeleken wordt, en een
|
|
45
|
+
// generieke terugval zou die bescherming meteen weer weggooien.
|
|
46
|
+
|
|
47
|
+
const HEX32 = /^[0-9a-f]{64}$/
|
|
48
|
+
|
|
49
|
+
const genormaliseerdeHash = (w) => {
|
|
50
|
+
if (typeof w !== 'string') return null
|
|
51
|
+
const kaal = w.trim().toLowerCase().replace(/^0x/, '')
|
|
52
|
+
return HEX32.test(kaal) ? kaal : null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const gelijkeHash = (a, b) => {
|
|
56
|
+
const x = genormaliseerdeHash(a)
|
|
57
|
+
const y = genormaliseerdeHash(b)
|
|
58
|
+
return x !== null && y !== null && x === y
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tijdstip = (w) => {
|
|
62
|
+
if (typeof w === 'number' && Number.isFinite(w)) return w
|
|
63
|
+
if (typeof w !== 'string' || !w.trim()) return null
|
|
64
|
+
const ms = Date.parse(w)
|
|
65
|
+
return Number.isFinite(ms) ? ms / 1000 : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tekst = (w) => (typeof w === 'string' && w.trim() ? w.trim().toLowerCase() : null)
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Normaliseert zowel het ECHTE record als de vlakke vorm uit onze eerste vectoren naar
|
|
72
|
+
* de velden die deze module vergelijkt. Een veld dat niet te vinden is wordt null, nooit
|
|
73
|
+
* een gok, want een gegokt veld levert een oordeel op dat niemand kan narekenen.
|
|
74
|
+
*
|
|
75
|
+
* De `naamruimte` bij elke partij is er met opzet: een key_id en een weergavenaam zijn
|
|
76
|
+
* twee verschillende soorten namen, en die mag je niet met elkaar vergelijken. Zie
|
|
77
|
+
* onafhankelijkheid() voor waarom dat hier het verschil maakt tussen een hard oordeel en
|
|
78
|
+
* een onmeetbaar geval.
|
|
79
|
+
*/
|
|
80
|
+
export function leesRecord(doc) {
|
|
81
|
+
const d = doc && typeof doc === 'object' ? doc : {}
|
|
82
|
+
const echt = d.record?.receipt ?? null
|
|
83
|
+
const acceptance = d.acceptance ?? null
|
|
84
|
+
const sidecar = d.bindings_sidecar ?? d.sidecar ?? null
|
|
85
|
+
|
|
86
|
+
const partij = (knoop, plat) => {
|
|
87
|
+
if (knoop && typeof knoop === 'object') {
|
|
88
|
+
if (tekst(knoop.key_id)) return { id: tekst(knoop.key_id), naamruimte: 'key_id' }
|
|
89
|
+
if (tekst(knoop.name)) return { id: tekst(knoop.name), naamruimte: 'name' }
|
|
90
|
+
return { id: null, naamruimte: null }
|
|
91
|
+
}
|
|
92
|
+
return { id: tekst(plat), naamruimte: tekst(plat) ? 'plat' : null }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
// De hash van het opgeleverde bestand, zoals het ONDERTEKENDE record hem noemt.
|
|
97
|
+
acceptatieLevering:
|
|
98
|
+
echt?.files?.remediated?.sha256 ?? acceptance?.deliverable_sha256 ?? d.deliverable_sha256 ?? null,
|
|
99
|
+
// De hash van het bonnetje, bovenin de sidecar.
|
|
100
|
+
sidecarBonHash: sidecar?.receipt_sha256 ?? d.bindings?.receipt_sha256 ?? null,
|
|
101
|
+
// De x402-sleutels, in het subobject dat daarvoor bedoeld is.
|
|
102
|
+
sidecarBindings: sidecar?.bindings ?? d.bindings ?? {},
|
|
103
|
+
// De vlakke vorm uit onze eerste vectoren noemde de levering ook in de sidecar. Die
|
|
104
|
+
// staat in een echt record NIET in de sidecar, dus dit is puur terugwaartse steun.
|
|
105
|
+
sidecarLevering: d.bindings?.deliverable_sha256 ?? sidecar?.deliverable_sha256 ?? null,
|
|
106
|
+
controleur: partij(echt?.parties?.checker, acceptance?.checker ?? d.checker),
|
|
107
|
+
producent: partij(echt?.parties?.producer, acceptance?.producer ?? d.producer),
|
|
108
|
+
onafhankelijkheidVerklaard: tekst(echt?.independence ?? acceptance?.independence ?? null),
|
|
109
|
+
criteriaVastgelegd:
|
|
110
|
+
echt?.criteria?.committed_at ?? acceptance?.criteria_committed_at ?? d.criteria_committed_at ?? null,
|
|
111
|
+
getoetstOp: echt?.issued_utc ?? acceptance?.checked_at ?? d.checked_at ?? null,
|
|
112
|
+
schema: echt?.schema ?? acceptance?.format ?? null,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const ONAFHANKELIJKHEID = [
|
|
117
|
+
'VERIFIEERBAAR_ONAFHANKELIJK',
|
|
118
|
+
'NIET_ONAFHANKELIJK',
|
|
119
|
+
'ZELFVERKLAARD_ONVERIFIEERBAAR',
|
|
120
|
+
'ONBEKEND',
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Kan een DERDE narekenen dat de controleur niet de producent is?
|
|
125
|
+
*
|
|
126
|
+
* Dit is bewust een drieweg en geen ja of nee, en daar zit de hele waarde. Het live
|
|
127
|
+
* record van 18 oktober 2026 zegt `independence: "independent"` terwijl
|
|
128
|
+
* `parties.producer.key_id` LEEG is. Er is dus niets om de controleur tegen af te zetten:
|
|
129
|
+
* de onafhankelijkheid is door de uitgever zelf verklaard en door niemand anders na te
|
|
130
|
+
* rekenen. Dat is geen afkeuring en geen goedkeuring, het is onmeetbaar, en precies dat
|
|
131
|
+
* onderscheid gaat verloren in een boolean.
|
|
132
|
+
*
|
|
133
|
+
* Even belangrijk: een key_id vergelijken met een weergavenaam is GEEN vergelijking. Dat
|
|
134
|
+
* zijn twee verschillende soorten namen, en "ze zijn verschillend dus onafhankelijk" zou
|
|
135
|
+
* dan een conclusie zijn die uit het formaat komt en niet uit de werkelijkheid.
|
|
136
|
+
*/
|
|
137
|
+
export function onafhankelijkheid(doc) {
|
|
138
|
+
const r = leesRecord(doc)
|
|
139
|
+
const c = r.controleur
|
|
140
|
+
const p = r.producent
|
|
141
|
+
if (c.id === null && p.id === null) {
|
|
142
|
+
return { staat: 'ONBEKEND', reden: 'geen van beide partijen is benoemd', vergelekenOp: null }
|
|
143
|
+
}
|
|
144
|
+
if (c.id === null || p.id === null) {
|
|
145
|
+
return {
|
|
146
|
+
staat: r.onafhankelijkheidVerklaard === 'independent' ? 'ZELFVERKLAARD_ONVERIFIEERBAAR' : 'ONBEKEND',
|
|
147
|
+
reden:
|
|
148
|
+
'een van beide partijen heeft geen identiteit in het record, dus een derde kan niet narekenen ' +
|
|
149
|
+
'of de controleur en de producent dezelfde partij zijn',
|
|
150
|
+
vergelekenOp: null,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (c.naamruimte !== p.naamruimte) {
|
|
154
|
+
return {
|
|
155
|
+
staat: r.onafhankelijkheidVerklaard === 'independent' ? 'ZELFVERKLAARD_ONVERIFIEERBAAR' : 'ONBEKEND',
|
|
156
|
+
reden:
|
|
157
|
+
`de partijen zijn in verschillende soorten namen vastgelegd (${c.naamruimte} tegen ${p.naamruimte}), ` +
|
|
158
|
+
'en die mag je niet met elkaar vergelijken',
|
|
159
|
+
vergelekenOp: null,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (c.id === p.id) {
|
|
163
|
+
return { staat: 'NIET_ONAFHANKELIJK', reden: 'de controleur en de producent zijn dezelfde partij', vergelekenOp: c.naamruimte }
|
|
164
|
+
}
|
|
165
|
+
return { staat: 'VERIFIEERBAAR_ONAFHANKELIJK', reden: null, vergelekenOp: c.naamruimte }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Is er genoeg om uberhaupt een oordeel te vellen? Ontbreekt er iets aan ONZE kant van de
|
|
169
|
+
// vergelijking, dan is het antwoord NOT_MEASURED en nooit NOT_LINKED. Een ontbrekend veld
|
|
170
|
+
// als "niet gekoppeld" lezen zou een ongemeten geval als een afkeuring laten tellen.
|
|
171
|
+
export function bindingMeetbaar(doc) {
|
|
172
|
+
const r = leesRecord(doc)
|
|
173
|
+
const mist = []
|
|
174
|
+
if (genormaliseerdeHash(r.sidecarBonHash) === null) mist.push('sidecar.receipt_sha256')
|
|
175
|
+
if (genormaliseerdeHash(r.acceptatieLevering) === null) mist.push('record.receipt.files.remediated.sha256')
|
|
176
|
+
return mist.length ? { meetbaar: false, mist } : { meetbaar: true, mist: [] }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// De twee voorwaarden van de uitgever zelf (trilatic.acceptance-receipt/1.2): een record
|
|
180
|
+
// telt alleen als de controleur niet de producent is, en als de criteria vastlagen
|
|
181
|
+
// voordat er getoetst werd. Ze staan hier in code omdat een voorwaarde die alleen in
|
|
182
|
+
// proza staat bij de eerste drukke dag wordt overgeslagen.
|
|
183
|
+
//
|
|
184
|
+
// De uitkomst is drieledig, want "ik kan het niet nagaan" is iets anders dan "het is
|
|
185
|
+
// mis". Dat onderscheid is dezelfde correctie die de Kanon-auteur op 17 oktober 2026
|
|
186
|
+
// overnam voor zijn eigen controleur.
|
|
187
|
+
export function recordTelt(doc) {
|
|
188
|
+
const r = leesRecord(doc)
|
|
189
|
+
const o = onafhankelijkheid(doc)
|
|
190
|
+
if (o.staat === 'NIET_ONAFHANKELIJK') return { telt: false, meetbaar: true, reden: 'CHECKER_IS_PRODUCER' }
|
|
191
|
+
if (o.staat !== 'VERIFIEERBAAR_ONAFHANKELIJK') {
|
|
192
|
+
return { telt: false, meetbaar: false, reden: 'INDEPENDENCE_NOT_VERIFIABLE', toelichting: o.reden }
|
|
193
|
+
}
|
|
194
|
+
const vastgelegd = tijdstip(r.criteriaVastgelegd)
|
|
195
|
+
const getoetst = tijdstip(r.getoetstOp)
|
|
196
|
+
if (vastgelegd === null || getoetst === null) {
|
|
197
|
+
return { telt: false, meetbaar: false, reden: 'CRITERIA_TIMING_UNKNOWN' }
|
|
198
|
+
}
|
|
199
|
+
if (!(vastgelegd < getoetst)) {
|
|
200
|
+
return { telt: false, meetbaar: true, reden: 'CRITERIA_NOT_COMMITTED_FIRST' }
|
|
201
|
+
}
|
|
202
|
+
return { telt: true, meetbaar: true, reden: null }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Het eigenlijke oordeel. Drie uitkomsten, nooit twee: gekoppeld, niet gekoppeld, of
|
|
207
|
+
* niet te meten.
|
|
208
|
+
*
|
|
209
|
+
* `receiptSha256` en het bonnetje zelf komen van de AANROEPER, want dat is de kant die
|
|
210
|
+
* hij in handen heeft. Alles aan de record-kant wordt uit het document gelezen.
|
|
211
|
+
*/
|
|
212
|
+
export function koppelAcceptatie({ receipt, acceptance, bindings, sidecar, doc, receiptSha256, deliverableSha256 }) {
|
|
213
|
+
// Een aanroeper mag het hele document meegeven, of de losse stukken zoals onze eerste
|
|
214
|
+
// vectoren dat deden. Beide komen hier op dezelfde genormaliseerde vorm uit.
|
|
215
|
+
const document = doc ?? {
|
|
216
|
+
record: acceptance?.record ? acceptance.record : undefined,
|
|
217
|
+
acceptance,
|
|
218
|
+
bindings,
|
|
219
|
+
bindings_sidecar: sidecar,
|
|
220
|
+
}
|
|
221
|
+
const r = leesRecord(document)
|
|
222
|
+
|
|
223
|
+
const m = bindingMeetbaar(document)
|
|
224
|
+
if (!m.meetbaar) {
|
|
225
|
+
return { valid: false, measurable: false, reason_code: 'NOT_MEASURED', missing: m.mist, independence: onafhankelijkheid(document).staat }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const telt = recordTelt(document)
|
|
229
|
+
if (!telt.telt) {
|
|
230
|
+
return telt.meetbaar
|
|
231
|
+
? { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: telt.reden, independence: onafhankelijkheid(document).staat }
|
|
232
|
+
: {
|
|
233
|
+
valid: false,
|
|
234
|
+
measurable: false,
|
|
235
|
+
reason_code: 'NOT_MEASURED',
|
|
236
|
+
missing: [telt.reden],
|
|
237
|
+
detail: telt.toelichting ?? null,
|
|
238
|
+
independence: onafhankelijkheid(document).staat,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Noemt de sidecar het bonnetje dat wij in handen hebben? Zonder een meegegeven hash van
|
|
243
|
+
// ons eigen bonnetje kan die helft niet getoetst worden, en dan is het niet gemeten.
|
|
244
|
+
if (receiptSha256 !== undefined) {
|
|
245
|
+
if (genormaliseerdeHash(receiptSha256) === null) {
|
|
246
|
+
return { valid: false, measurable: false, reason_code: 'NOT_MEASURED', missing: ['receiptSha256'] }
|
|
247
|
+
}
|
|
248
|
+
if (!gelijkeHash(receiptSha256, r.sidecarBonHash)) {
|
|
249
|
+
return { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: 'RECEIPT_HASH_MISMATCH' }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// De kern: wijzen het record en wat wij in handen hebben naar hetzelfde opgeleverde
|
|
254
|
+
// bestand. De aanroeper mag de hash van het bestand dat hij ECHT ontving meegeven; dat
|
|
255
|
+
// is sterker dan een hash die ook uit de sidecar komt, want dan vergelijk je twee
|
|
256
|
+
// velden van dezelfde uitgever met elkaar.
|
|
257
|
+
const onzeKant = deliverableSha256 ?? r.sidecarLevering
|
|
258
|
+
if (onzeKant === null || onzeKant === undefined) {
|
|
259
|
+
return {
|
|
260
|
+
valid: false,
|
|
261
|
+
measurable: false,
|
|
262
|
+
reason_code: 'NOT_MEASURED',
|
|
263
|
+
missing: ['deliverableSha256'],
|
|
264
|
+
detail: 'het record noemt een opgeleverd bestand, maar er is geen eigen hash om het tegen af te zetten',
|
|
265
|
+
independence: onafhankelijkheid(document).staat,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!gelijkeHash(r.acceptatieLevering, onzeKant)) {
|
|
269
|
+
return { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: 'DELIVERABLE_HASH_MISMATCH' }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// De twee x402-sleutels, ELK met zijn eigen tegenhanger. Een belofte wordt nooit met
|
|
273
|
+
// een nonce vergeleken: daarvoor zijn het twee sleutels. Staat een van de twee kanten
|
|
274
|
+
// niet in het stuk, dan wordt er niet vergeleken, en dat is geen afkeuring.
|
|
275
|
+
const b = r.sidecarBindings ?? {}
|
|
276
|
+
const paren = [
|
|
277
|
+
{
|
|
278
|
+
naam: 'COMMITMENT_MISMATCH',
|
|
279
|
+
sidecar: b.x402_receipt_commitment,
|
|
280
|
+
bon: receipt?.authorizationCommitment ?? receipt?.payload?.authorizationCommitment,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
naam: 'AUTHORIZATION_MISMATCH',
|
|
284
|
+
sidecar: b.x402_authorization_nonce,
|
|
285
|
+
bon: receipt?.authorizationNonce ?? receipt?.payload?.authorizationNonce,
|
|
286
|
+
},
|
|
287
|
+
]
|
|
288
|
+
const vergeleken = []
|
|
289
|
+
for (const p of paren) {
|
|
290
|
+
if (p.sidecar === undefined || p.sidecar === null) continue
|
|
291
|
+
if (p.bon === undefined || p.bon === null) continue
|
|
292
|
+
if (!gelijkeHash(p.sidecar, p.bon)) {
|
|
293
|
+
return { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: p.naam }
|
|
294
|
+
}
|
|
295
|
+
vergeleken.push(p.naam.replace('_MISMATCH', ''))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
valid: true,
|
|
300
|
+
measurable: true,
|
|
301
|
+
reason_code: 'LINKED',
|
|
302
|
+
independence: onafhankelijkheid(document).staat,
|
|
303
|
+
// Wat er WEL en NIET vergeleken is, want een groen antwoord zonder dat lijstje laat
|
|
304
|
+
// een lezer denken dat alles getoetst is.
|
|
305
|
+
compared: vergeleken,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export const UITKOMSTEN = ['LINKED', 'NOT_LINKED', 'NOT_MEASURED']
|