agent-receipt-verify 0.2.2 → 0.2.4

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 (35) hide show
  1. package/README.md +38 -3
  2. package/bin/cli.mjs +16 -0
  3. package/package.json +6 -3
  4. package/src/settlement-link-vectors.mjs +241 -0
  5. package/src/verify-delivery.mjs +507 -0
  6. package/src/x402-discovery-census.mjs +24 -1
  7. package/src/x402-offer-receipt.mjs +49 -3
  8. package/tools/bouw-afrekening-vectoren.mjs +273 -0
  9. package/tools/naar-kanon-vorm.mjs +172 -0
  10. package/tools/pack-inhoud.mjs +45 -1
  11. package/vectors/settlement-link/find-candidates-onchain.json +44 -0
  12. package/vectors/settlement-link/no-candidates-not-measured.json +41 -0
  13. package/vectors/settlement-link/opens-live-2.json +48 -0
  14. package/vectors/settlement-link/opens-live-3.json +48 -0
  15. package/vectors/settlement-link/opens-live.json +48 -0
  16. package/vectors/settlement-link/two-identical-payments.json +86 -0
  17. package/vectors/settlement-link/wrong-nonce-live-2.json +46 -0
  18. package/vectors/settlement-link/wrong-nonce-live-3.json +46 -0
  19. package/vectors/settlement-link/wrong-nonce-live.json +46 -0
  20. package/vectors/settlement-link/wrong-salt-live-2.json +46 -0
  21. package/vectors/settlement-link/wrong-salt-live-3.json +46 -0
  22. package/vectors/settlement-link/wrong-salt-live.json +46 -0
  23. package/vectors/settlement-link-kanon/README.md +69 -0
  24. package/vectors/settlement-link-kanon/find-candidates-onchain.json +43 -0
  25. package/vectors/settlement-link-kanon/no-candidates-not-measured.json +42 -0
  26. package/vectors/settlement-link-kanon/opens-live-2.json +47 -0
  27. package/vectors/settlement-link-kanon/opens-live-3.json +47 -0
  28. package/vectors/settlement-link-kanon/opens-live.json +47 -0
  29. package/vectors/settlement-link-kanon/two-identical-payments.json +76 -0
  30. package/vectors/settlement-link-kanon/wrong-nonce-live-2.json +46 -0
  31. package/vectors/settlement-link-kanon/wrong-nonce-live-3.json +46 -0
  32. package/vectors/settlement-link-kanon/wrong-nonce-live.json +46 -0
  33. package/vectors/settlement-link-kanon/wrong-salt-live-2.json +46 -0
  34. package/vectors/settlement-link-kanon/wrong-salt-live-3.json +46 -0
  35. 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 network, no keys
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. Only the settlement link below touches
84
- the network, and then only a public read-only RPC.
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
@@ -57,6 +57,20 @@ function gebruik() {
57
57
  console.log(' the same ten vectors under both amount policies, plus the two cases no')
58
58
  console.log(' published vector covers: overpayment, and one nonce across two payers.')
59
59
  console.log(' Those two are the open questions in x402 issue 2725.')
60
+ console.log('')
61
+ console.log(' npx github:holistis/agent-receipt-verify liveness [uit.json]')
62
+ console.log(' takes the census and asks each host whether it still answers 402 today.')
63
+ console.log(' A registration in an index is not a promise that the endpoint is alive.')
64
+ console.log('')
65
+ console.log(' npx github:holistis/agent-receipt-verify verify-delivery --answer=<file> --endpoint=<url>')
66
+ console.log(' reads, at the source, whether a paid answer is TRUE. The location is')
67
+ console.log(' DERIVED from --endpoint and never taken from the answer, because a seller')
68
+ console.log(' who picks both the token and the place can stage any outcome.')
69
+ console.log('')
70
+ console.log(' npx github:holistis/agent-receipt-verify link-vectors')
71
+ console.log(' twelve vectors for the half AFTER paying: does this receipt belong to')
72
+ console.log(' that one settlement? Eleven run offline, the chain-reading one is')
73
+ console.log(' marked and skipped rather than failed.')
60
74
  }
61
75
 
62
76
  // Het argument bepaalt welke module draait. De modules zijn ook los te draaien en te
@@ -70,6 +84,8 @@ const onderdelen = {
70
84
  liveness: '../src/x402-index-liveness.mjs',
71
85
  kanon: '../src/eip3009-autorisatie.mjs',
72
86
  beleid: '../bench/eip3009-beleid.mjs',
87
+ 'link-vectors': '../src/settlement-link-vectors.mjs',
88
+ 'verify-delivery': '../src/verify-delivery.mjs',
73
89
  }
74
90
 
75
91
  if (!eerste || eerste === '--help' || eerste === '-h' || eerste === 'help') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-receipt-verify",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",
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",
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",
@@ -19,7 +19,10 @@
19
19
  "mcp": "node src/mcp-server.mjs",
20
20
  "bundel": "node tools/bundel-bouw.mjs build/bundle",
21
21
  "kanon": "node src/eip3009-autorisatie.mjs --vectoren",
22
- "bench-eip3009": "node bench/eip3009-beleid.mjs"
22
+ "bench-eip3009": "node bench/eip3009-beleid.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"
23
26
  },
24
27
  "optionalDependencies": {
25
28
  "ethers": "^6.17.0"
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ // Draait de afrekening-vectoren: hoort dit bonnetje bij die ene betaling?
3
+ //
4
+ // WAAROM DIT EEN LOPER IS EN NIET EEN DOCUMENT. Een vectorbestand dat niemand kan
5
+ // draaien is een bewering met extra stappen. Dit commando leest de vectoren, rekent elk
6
+ // commitment opnieuw uit, en vergelijkt dat met wat er verwacht wordt. Een vreemde
7
+ // draait het zonder account, zonder sleutel, en voor elf van de twaalf gevallen ook
8
+ // zonder internet.
9
+ //
10
+ // DE DRIE UITKOMSTEN, en de derde is het hele punt. LINKED als het commitment op
11
+ // precies een kandidaat opent. NOT_LINKED als er kandidaten waren en geen enkele opent.
12
+ // NOT_MEASURED als er geen kandidatenlijst is VERKREGEN. Dat laatste is geen afkeuring
13
+ // van het bonnetje maar een onvolledige controle, en een woordenlijst met twee waarden
14
+ // kan dat verschil niet uitdrukken. Precies dat gat benoemde de auteur van de
15
+ // kanon-vectoren op 2026-10-17 als iets dat hij wil toevoegen.
16
+ //
17
+ // LET OP HET VERSCHIL TUSSEN LEEG EN AFWEZIG. `candidates: null` betekent dat er geen
18
+ // lijst is opgehaald, `candidates: []` dat er wel een lijst is en die leeg was. Het
19
+ // eerste is NOT_MEASURED, het tweede NOT_LINKED. Een loper die die twee door elkaar
20
+ // haalt, gooit geldige bonnetjes weg bij een haperende RPC.
21
+ //
22
+ // Gebruik:
23
+ // node src/settlement-link-vectors.mjs de offline vectoren
24
+ // npx agent-receipt-verify link-vectors hetzelfde, als commando
25
+
26
+ import fs from 'node:fs'
27
+ import path from 'node:path'
28
+ import { fileURLToPath } from 'node:url'
29
+ import { berekenCommitment } from './x402-offer-receipt.mjs'
30
+
31
+ const WORTEL = fileURLToPath(new URL('..', import.meta.url))
32
+ export const VECTORMAP = path.join(WORTEL, 'vectors', 'settlement-link')
33
+
34
+ /**
35
+ * Beoordeel een enkele vector, puur rekenkundig.
36
+ *
37
+ * Geeft altijd een `outcome` terug uit de drie, nooit een throw voor een gewone
38
+ * uitkomst: een verifieerder die bij een ongeldig bonnetje crasht, is zelf de storing.
39
+ */
40
+ /**
41
+ * Matcht deze kandidaat het commitment? Ja of nee, en nooit iets daartussenin.
42
+ *
43
+ * WAAROM DIT EEN BOOLEAN IS EN GEEN WAARDE. De eerste versie van deze reparatie gaf bij
44
+ * een onberekenbaar veld de TEKENREEKS `0x-niet-te-berekenen` terug, en die werd daarna
45
+ * vergeleken met het commitment uit het bonnetje. Een foutwaarde die in dezelfde ruimte
46
+ * leeft als de echte waarde is geen foutwaarde maar een sleutel: een bonnetje dat
47
+ * precies die tekst als commitment droeg, met een kandidaat die niet te berekenen was,
48
+ * kwam er als GELINKT uit zonder dat er een byte cryptografie aan te pas kwam. Onze
49
+ * eigen keuring heeft dat gedraaid, met en zonder hoofdletters, op beide takken van
50
+ * beoordeelVector. Dat is precies de klasse fout die dit bestand moet uitsluiten, en hij
51
+ * zat er een uur in.
52
+ *
53
+ * `null` teruggeven lost het NIET op, en dat is de reden dat hier een boolean staat:
54
+ * `String(null)` is de tekenreeks "null", dus een bonnetje met commitment "null" zou
55
+ * het gat meteen heropenen. De enige vorm die dicht is, is er niets uit laten komen dat
56
+ * ooit vergeleken wordt. De vergelijking zelf hoort daarom HIER en nergens anders.
57
+ *
58
+ * En de oorspronkelijke reden blijft staan: een kapot veld mag de ronde niet omver
59
+ * trekken. Een onberekenbare kandidaat is geen treffer. Dat is hier de enige betekenis.
60
+ */
61
+ function kandidaatMatcht(velden, commitment) {
62
+ if (typeof commitment !== 'string' || !commitment) return false
63
+ let berekend
64
+ try {
65
+ berekend = berekenCommitment(velden)
66
+ } catch {
67
+ return false
68
+ }
69
+ // berekenCommitment geeft altijd een 0x-sha256 terug of gooit. Deze controle staat er
70
+ // zodat een toekomstige wijziging daar nooit stil iets anders dan een hash kan laten
71
+ // doorstromen naar een gelijkheidsvergelijking.
72
+ if (typeof berekend !== 'string' || !/^0x[0-9a-f]{64}$/i.test(berekend)) return false
73
+ return berekend.toLowerCase() === commitment.toLowerCase()
74
+ }
75
+
76
+ /**
77
+ * Is dit een kandidaat waar uberhaupt iets mee te doen valt?
78
+ *
79
+ * Zonder deze zeef overleefde een `null` in de kandidatenlijst de filter (want
80
+ * `k?.authorizer` is dan gewoon undefined), en als zo'n kandidaat dan matchte, knalde
81
+ * `passend[0].nonce` met een TypeError. De koptekst van dit bestand belooft letterlijk
82
+ * dat deze loper niet gooit voor een gewone uitkomst. Gemeten in de eigen keuring.
83
+ */
84
+ function bruikbareKandidaat(k) {
85
+ return Boolean(k) && typeof k === 'object' && typeof k.nonce === 'string' && typeof k.authorizer === 'string'
86
+ }
87
+
88
+ /**
89
+ * Kunnen wij met ONZE eigen vaste velden uberhaupt een commitment uitrekenen?
90
+ *
91
+ * WAAROM DIT APART STAAT VAN DE KANDIDATEN. Een kapotte KANDIDAAT is geen treffer, en
92
+ * dat is de juiste uitkomst. Een kapot EIGEN veld (een zout of een asset die onze RPC
93
+ * verkeerd teruggaf) laat elke kandidaat falen, en dan rolt er een stellige "geen enkele
94
+ * kandidaat opende dit commitment" uit terwijl de waarheid is dat er niets te berekenen
95
+ * viel. De koptekst van dit bestand noemt dat zelf de ergste fout die deze loper kan
96
+ * maken: een geldig bonnetje weggooien bij een haperende RPC.
97
+ *
98
+ * Deze controle zat al in opentCommitment, en niet hier. Dezelfde commit repareerde de
99
+ * ene functie en liet de andere staan, en de eigen keuring heeft dat gemeten.
100
+ */
101
+ function eigenVeldenMeetbaar({ method, asset, salt }) {
102
+ try {
103
+ berekenCommitment({ method, asset, authorizer: '0x' + '00'.repeat(20), identifier: '0x' + '00'.repeat(32), salt })
104
+ return { ok: true }
105
+ } catch (e) {
106
+ return { ok: false, reden: 'cannot compute a commitment here: ' + String(e?.message ?? e).slice(0, 90) }
107
+ }
108
+ }
109
+
110
+ export function beoordeelVector(v) {
111
+ if (v.requires_chain_read) return { outcome: 'SKIPPED', reden: 'requires an RPC; run the link command for this one' }
112
+
113
+ // Het geval met twee bonnetjes tegelijk: elk moet op zijn eigen kandidaat openen en
114
+ // op geen enkele andere.
115
+ if (Array.isArray(v.input?.receipts)) {
116
+ // GEEN `?? []` HIER. Dat stond er en het maakte van een ONTBREKENDE lijst een LEGE
117
+ // lijst, dus van NOT_MEASURED een NOT_LINKED. De koptekst van dit bestand noemt dat
118
+ // zelf de ergste fout die deze loper kan maken, want dan beschuldigt hij een
119
+ // eerlijke verkoper omdat de keten even niet te lezen was.
120
+ if (v.input.candidates == null) {
121
+ return { outcome: 'NOT_MEASURED', reden: 'no candidate set was obtained' }
122
+ }
123
+ if (!Array.isArray(v.input.candidates)) {
124
+ return { outcome: 'NOT_MEASURED', reden: 'the candidate set is not a list' }
125
+ }
126
+ const kandidaten = v.input.candidates
127
+ const mapping = []
128
+ for (const [i, r] of v.input.receipts.entries()) {
129
+ const p = r?.receipt?.payload
130
+ if (!p) return { outcome: 'NOT_MEASURED', reden: `receipt ${i} has no payload` }
131
+ if (!p.authorizationCommitment) {
132
+ return { outcome: 'NOT_MEASURED', reden: `receipt ${i} carries no commitment` }
133
+ }
134
+ const meetbaar = eigenVeldenMeetbaar({
135
+ method: p.authorizationMethod ?? 'eip3009',
136
+ asset: r.asset,
137
+ salt: r.authorizationSalt,
138
+ })
139
+ if (!meetbaar.ok) return { outcome: 'NOT_MEASURED', reden: `receipt ${i}: ` + meetbaar.reden }
140
+ const passend = kandidaten.filter(bruikbareKandidaat).filter((k) =>
141
+ kandidaatMatcht({
142
+ method: p.authorizationMethod ?? 'eip3009',
143
+ asset: r.asset,
144
+ authorizer: k.authorizer,
145
+ identifier: k.nonce,
146
+ salt: r.authorizationSalt,
147
+ }, p.authorizationCommitment))
148
+ if (passend.length !== 1) {
149
+ return { outcome: 'NOT_LINKED', reden: `receipt ${i} opened on ${passend.length} candidates, expected exactly one` }
150
+ }
151
+ mapping.push({ receipt_index: i, identifier: passend[0].nonce })
152
+ }
153
+ return { outcome: 'LINKED', mapping }
154
+ }
155
+
156
+ const p = v.input?.receipt?.payload
157
+ if (!p) return { outcome: 'NOT_MEASURED', reden: 'no receipt payload in the vector' }
158
+ if (!p.authorizationCommitment) {
159
+ // Een bonnetje van versie 1 draagt geen commitment. Dat is geen fout van het
160
+ // bonnetje en ook geen afkeuring, er valt alleen niets te openen.
161
+ return { outcome: 'NOT_MEASURED', reden: 'receipt is version 1 and carries no commitment' }
162
+ }
163
+
164
+ const kandidaten = v.input.candidates
165
+ if (kandidaten == null) return { outcome: 'NOT_MEASURED', reden: 'no candidate set was obtained' }
166
+ // Een lijst die geen lijst is, is ook geen meting. Zonder deze regel gooide .filter
167
+ // en ging de hele ronde eraan.
168
+ if (!Array.isArray(kandidaten)) return { outcome: 'NOT_MEASURED', reden: 'the candidate set is not a list' }
169
+
170
+ const meetbaar = eigenVeldenMeetbaar({
171
+ method: p.authorizationMethod ?? 'eip3009',
172
+ asset: v.input.asset,
173
+ salt: v.input.authorizationSalt,
174
+ })
175
+ if (!meetbaar.ok) return { outcome: 'NOT_MEASURED', reden: meetbaar.reden }
176
+
177
+ const passend = kandidaten.filter(bruikbareKandidaat).filter((k) =>
178
+ kandidaatMatcht({
179
+ method: p.authorizationMethod ?? 'eip3009',
180
+ asset: v.input.asset,
181
+ authorizer: k.authorizer,
182
+ identifier: k.nonce,
183
+ salt: v.input.authorizationSalt,
184
+ }, p.authorizationCommitment))
185
+
186
+ if (passend.length === 1) return { outcome: 'LINKED', identifier: passend[0].nonce }
187
+ if (passend.length === 0) return { outcome: 'NOT_LINKED', reden: `none of ${kandidaten.length} candidates opened the commitment` }
188
+ return { outcome: 'AMBIGUOUS', reden: `${passend.length} candidates opened the same commitment, which a random 32 byte salt should make practically impossible` }
189
+ }
190
+
191
+ export function laadVectoren(map = VECTORMAP) {
192
+ if (!fs.existsSync(map)) return []
193
+ return fs.readdirSync(map).filter((f) => f.endsWith('.json')).sort()
194
+ .map((f) => ({ bestand: f, ...JSON.parse(fs.readFileSync(path.join(map, f), 'utf8')) }))
195
+ }
196
+
197
+ const ditBestand = fileURLToPath(import.meta.url)
198
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(ditBestand)) {
199
+ const vectoren = laadVectoren()
200
+ if (!vectoren.length) {
201
+ console.error('no vectors found in vectors/settlement-link')
202
+ process.exitCode = 1
203
+ } else {
204
+ console.log('settlement-link vectors: does this receipt belong to that one payment?')
205
+ console.log('')
206
+ let goed = 0, fout = 0, overgeslagen = 0
207
+ for (const v of vectoren) {
208
+ const u = beoordeelVector(v)
209
+ if (u.outcome === 'SKIPPED') {
210
+ overgeslagen++
211
+ console.log(` skip ${v.id}`)
212
+ console.log(` ${u.reden}`)
213
+ continue
214
+ }
215
+ // Een vector zonder verwachting is een kapotte vector, geen reden om de hele
216
+ // ronde af te breken. Zonder deze regel gooide dit een TypeError en werd geen
217
+ // enkele andere vector meer beoordeeld, ook de twaalf goede niet.
218
+ if (!v.expected || typeof v.expected.outcome !== 'string') {
219
+ fout++
220
+ console.log(` FAIL ${v.id ?? v.bestand}`)
221
+ console.log(' this vector carries no expected outcome, so it cannot be judged')
222
+ continue
223
+ }
224
+ const ok = u.outcome === v.expected.outcome
225
+ if (ok) goed++; else fout++
226
+ console.log(` ${ok ? 'pass' : 'FAIL'} ${v.id}`)
227
+ console.log(` expected ${v.expected.outcome}, got ${u.outcome}${u.reden ? ' (' + u.reden + ')' : ''}`)
228
+ }
229
+ console.log('')
230
+ console.log(`${goed} agree, ${fout} disagree, ${overgeslagen} skipped because they need an RPC`)
231
+ console.log('')
232
+ console.log('Every positive case here was re-derived, not copied: the commitment is recomputed')
233
+ console.log('from tag, method, asset, authorizer, identifier and salt, and compared against the')
234
+ console.log('authorizationCommitment field carried in the receipt.')
235
+ console.log('')
236
+ console.log('What this command does NOT do: it does not check the signature over that receipt.')
237
+ console.log('So this answers "does this commitment open on that settlement", not "is this')
238
+ console.log('receipt genuine". For the signature, run the conformance check on the endpoint.')
239
+ process.exitCode = fout === 0 ? 0 : 1
240
+ }
241
+ }