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.
Files changed (31) hide show
  1. package/README.md +38 -3
  2. package/bin/cli.mjs +41 -24
  3. package/package.json +5 -3
  4. package/src/acceptance-binding.mjs +309 -0
  5. package/src/settlement-link-vectors.mjs +124 -11
  6. package/src/verify-delivery.mjs +507 -0
  7. package/src/x402-conformance-check.mjs +41 -5
  8. package/src/x402-discovery-census.mjs +24 -1
  9. package/src/x402-offer-receipt.mjs +49 -3
  10. package/tools/naar-kanon-vorm.mjs +172 -0
  11. package/tools/pack-inhoud.mjs +45 -1
  12. package/vectors/acceptance-binding/crossed-deliverable.json +40 -0
  13. package/vectors/acceptance-binding/pairs-same-deliverable.json +39 -0
  14. package/vectors/acceptance-binding/real-record-as-issued.json +68 -0
  15. package/vectors/acceptance-binding/real-record-crossed-deliverable.json +65 -0
  16. package/vectors/acceptance-binding/real-record-crossed-x402-keys.json +74 -0
  17. package/vectors/acceptance-binding/real-record-with-producer-key.json +66 -0
  18. package/vectors/acceptance-binding/real-record-x402-keys-match.json +76 -0
  19. package/vectors/settlement-link-kanon/README.md +69 -0
  20. package/vectors/settlement-link-kanon/find-candidates-onchain.json +43 -0
  21. package/vectors/settlement-link-kanon/no-candidates-not-measured.json +42 -0
  22. package/vectors/settlement-link-kanon/opens-live-2.json +47 -0
  23. package/vectors/settlement-link-kanon/opens-live-3.json +47 -0
  24. package/vectors/settlement-link-kanon/opens-live.json +47 -0
  25. package/vectors/settlement-link-kanon/two-identical-payments.json +76 -0
  26. package/vectors/settlement-link-kanon/wrong-nonce-live-2.json +46 -0
  27. package/vectors/settlement-link-kanon/wrong-nonce-live-3.json +46 -0
  28. package/vectors/settlement-link-kanon/wrong-nonce-live.json +46 -0
  29. package/vectors/settlement-link-kanon/wrong-salt-live-2.json +46 -0
  30. package/vectors/settlement-link-kanon/wrong-salt-live-3.json +46 -0
  31. package/vectors/settlement-link-kanon/wrong-salt-live.json +46 -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
  }