agent-receipt-verify 0.2.3 → 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.
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
@@ -58,6 +58,15 @@ function gebruik() {
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
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('')
61
70
  console.log(' npx github:holistis/agent-receipt-verify link-vectors')
62
71
  console.log(' twelve vectors for the half AFTER paying: does this receipt belong to')
63
72
  console.log(' that one settlement? Eleven run offline, the chain-reading one is')
@@ -76,6 +85,7 @@ const onderdelen = {
76
85
  kanon: '../src/eip3009-autorisatie.mjs',
77
86
  beleid: '../bench/eip3009-beleid.mjs',
78
87
  'link-vectors': '../src/settlement-link-vectors.mjs',
88
+ 'verify-delivery': '../src/verify-delivery.mjs',
79
89
  }
80
90
 
81
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.3",
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 && 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",
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"
@@ -37,24 +37,114 @@ export const VECTORMAP = path.join(WORTEL, 'vectors', 'settlement-link')
37
37
  * Geeft altijd een `outcome` terug uit de drie, nooit een throw voor een gewone
38
38
  * uitkomst: een verifieerder die bij een ongeldig bonnetje crasht, is zelf de storing.
39
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
+
40
110
  export function beoordeelVector(v) {
41
111
  if (v.requires_chain_read) return { outcome: 'SKIPPED', reden: 'requires an RPC; run the link command for this one' }
42
112
 
43
113
  // Het geval met twee bonnetjes tegelijk: elk moet op zijn eigen kandidaat openen en
44
114
  // op geen enkele andere.
45
115
  if (Array.isArray(v.input?.receipts)) {
46
- const kandidaten = v.input.candidates ?? []
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
47
127
  const mapping = []
48
128
  for (const [i, r] of v.input.receipts.entries()) {
49
- const p = r.receipt.payload
50
- const passend = kandidaten.filter((k) =>
51
- String(berekenCommitment({
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({
52
142
  method: p.authorizationMethod ?? 'eip3009',
53
143
  asset: r.asset,
54
144
  authorizer: k.authorizer,
55
145
  identifier: k.nonce,
56
146
  salt: r.authorizationSalt,
57
- })).toLowerCase() === String(p.authorizationCommitment).toLowerCase())
147
+ }, p.authorizationCommitment))
58
148
  if (passend.length !== 1) {
59
149
  return { outcome: 'NOT_LINKED', reden: `receipt ${i} opened on ${passend.length} candidates, expected exactly one` }
60
150
  }
@@ -73,15 +163,25 @@ export function beoordeelVector(v) {
73
163
 
74
164
  const kandidaten = v.input.candidates
75
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 }
76
176
 
77
- const passend = kandidaten.filter((k) =>
78
- String(berekenCommitment({
177
+ const passend = kandidaten.filter(bruikbareKandidaat).filter((k) =>
178
+ kandidaatMatcht({
79
179
  method: p.authorizationMethod ?? 'eip3009',
80
180
  asset: v.input.asset,
81
181
  authorizer: k.authorizer,
82
182
  identifier: k.nonce,
83
183
  salt: v.input.authorizationSalt,
84
- })).toLowerCase() === String(p.authorizationCommitment).toLowerCase())
184
+ }, p.authorizationCommitment))
85
185
 
86
186
  if (passend.length === 1) return { outcome: 'LINKED', identifier: passend[0].nonce }
87
187
  if (passend.length === 0) return { outcome: 'NOT_LINKED', reden: `none of ${kandidaten.length} candidates opened the commitment` }
@@ -112,6 +212,15 @@ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(ditBestand
112
212
  console.log(` ${u.reden}`)
113
213
  continue
114
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
+ }
115
224
  const ok = u.outcome === v.expected.outcome
116
225
  if (ok) goed++; else fout++
117
226
  console.log(` ${ok ? 'pass' : 'FAIL'} ${v.id}`)
@@ -120,9 +229,13 @@ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(ditBestand
120
229
  console.log('')
121
230
  console.log(`${goed} agree, ${fout} disagree, ${overgeslagen} skipped because they need an RPC`)
122
231
  console.log('')
123
- console.log('Every positive case here was re-derived from the signed receipt, not copied:')
124
- console.log('the commitment is recomputed from tag, method, asset, authorizer, identifier and')
125
- console.log('salt, and compared against what the seller actually signed.')
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.')
126
239
  process.exitCode = fout === 0 ? 0 : 1
127
240
  }
128
241
  }