agent-receipt-verify 0.2.1 → 0.2.3

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
@@ -2,9 +2,9 @@
2
2
 
3
3
  Independent verification of payment- and outcome-proof receipts used by AI agents.
4
4
 
5
- An agent economy has two kinds of receipt. One says **who was allowed to act**
6
- (delegation: UCAN, Authproof, Agent Passport). The other says **what was paid for
7
- and whether the output matches** (settlement and outcome: x402 offer-receipt, INAM
5
+ An agent economy has two kinds of receipt. One says who was allowed to act
6
+ (delegation: UCAN, Authproof, Agent Passport). The other says what was paid for
7
+ and whether the output matches (settlement and outcome: x402 offer-receipt, INAM
8
8
  countersigned receipts, and the proofs ERC-8004 consumers count).
9
9
 
10
10
  `receipt-commons/receipt-interop` covers the first kind. This covers the second.
@@ -90,6 +90,23 @@ settlements, starting from the receipt, the salt and the asset as §5.6.4
90
90
  requires. Their result and ours are in
91
91
  [x402#3827](https://github.com/x402-foundation/x402/pull/3827#issuecomment-6230063003).
92
92
 
93
+ ## Recompute it yourself, without an account
94
+
95
+ Two of the cross-checks run offline from a single command, so nothing here has to be
96
+ taken on trust:
97
+
98
+ ```sh
99
+ npx -y agent-receipt-verify kanon
100
+ npx -y agent-receipt-verify beleid
101
+ ```
102
+
103
+ The first checks our EIP-3009 verifier against the ten published vectors of
104
+ iamonuwa/kanon. The second exists because agreement can hide a question: all ten of
105
+ those vectors cover underpayment only, so they say nothing about overpayment, or
106
+ about one nonce reused across two payers. Those are the two open questions in
107
+ [x402#2725](https://github.com/x402-foundation/x402/issues/2725), and the command
108
+ prints exactly where the two possible answers diverge and where they do not.
109
+
93
110
  ## Linking a receipt to its settlement
94
111
 
95
112
  A receipt with an empty `transaction` field can be matched to a payment only by
@@ -170,7 +187,7 @@ not show that anyone else is using it.
170
187
 
171
188
  ## Honest limits
172
189
 
173
- The format checks test the **form** of a receipt: signatures, content addressing,
190
+ The format checks test the form of a receipt: signatures, content addressing,
174
191
  and whether the hashes match the bytes a counterparty observed. They do not tell
175
192
  you whether an operator is honest, or whether a registry holds a receipt as
176
193
  finalized.
@@ -0,0 +1,113 @@
1
+ // Een matrix over de twee open vragen uit x402#2725, zodat een antwoord daar een
2
+ // meting is en geen mening.
3
+ //
4
+ // DE TWEE VRAGEN, letterlijk van de steller van dat issue: moet het betaalde bedrag
5
+ // precies kloppen of mag er te veel betaald worden, en moet hergebruik van een nonce
6
+ // per betaler worden bijgehouden. Geen van beide wordt gedekt door een gepubliceerde
7
+ // vector, en dus heeft onze eigen "eens op tien van tien" er niets over gezegd. Dat is
8
+ // precies waarom dit bestand bestaat: een cijfer dat klopt kan een vraag verbergen.
9
+ //
10
+ // WAT HIER GEMETEN WORDT. Dezelfde tien vectoren onder beide bedragbeleiden, plus twee
11
+ // gevallen die zij niet dekken en die het verschil zichtbaar maken. De uitslag is een
12
+ // tabel waarin te zien is waar de twee keuzes uit elkaar lopen en waar niet.
13
+ //
14
+ // DE UITVOER IS ENGELS, de commentaren zijn dat niet. Sinds 0.2.2 is dit een eigen
15
+ // commando (`agent-receipt-verify beleid`), dus de regels die iemand op zijn scherm
16
+ // krijgt zijn voor een vreemde bedoeld. Een meting die niemand kan lezen is geen
17
+ // meting die iemand overneemt.
18
+ //
19
+ // Gebruik: node bench/eip3009-beleid.mjs of npx agent-receipt-verify beleid
20
+
21
+ import fs from 'node:fs'
22
+ import path from 'node:path'
23
+ import { fileURLToPath } from 'node:url'
24
+ import { beoordeelVector, beoordeelAutorisatie } from '../src/eip3009-autorisatie.mjs'
25
+
26
+ const wortel = fileURLToPath(new URL('..', import.meta.url))
27
+ const MAP = path.join(wortel, 'vectors', 'kanon')
28
+ const lees = (n) => JSON.parse(fs.readFileSync(path.join(MAP, n), 'utf8'))
29
+
30
+ const vectoren = fs.readdirSync(MAP).filter((f) => f.endsWith('.json')).sort()
31
+
32
+ console.log('EIP-3009: the two open questions from x402 issue 2725, measured')
33
+ console.log('')
34
+
35
+ // ---------------------------------------------------------------------------
36
+ console.log('1. The same ten vectors under both amount policies')
37
+ console.log('')
38
+ const uitkomst = {}
39
+ for (const beleid of ['exact', 'at-least']) {
40
+ let eens = 0
41
+ const afwijkend = []
42
+ for (const f of vectoren) {
43
+ const v = lees(f)
44
+ const u = await beoordeelAutorisatie(v.input, { ...(v.context ?? {}), doelNetwerk: v.network, amountPolicy: beleid })
45
+ if (u.valid === v.expected.valid && u.reason_code === v.expected.reason_code) eens++
46
+ else afwijkend.push(v.id + ' (' + u.reason_code + ' against ' + v.expected.reason_code + ')')
47
+ }
48
+ uitkomst[beleid] = { eens, afwijkend }
49
+ console.log(' ' + beleid.padEnd(9) + ' agrees on ' + eens + ' of ' + vectoren.length +
50
+ (afwijkend.length ? ' differs: ' + afwijkend.join(', ') : ''))
51
+ }
52
+ console.log('')
53
+ console.log(' Both policies reach the same verdict on all ten. That is the point: the')
54
+ console.log(' published vectors only cover UNDERpayment, so they cannot answer the')
55
+ console.log(' question. Anyone who stops here thinks they know.')
56
+
57
+ // ---------------------------------------------------------------------------
58
+ console.log('')
59
+ console.log('2. The case no vector covers: paying too much')
60
+ console.log('')
61
+ {
62
+ const basis = lees('valid-baseline-001.json')
63
+ for (const beleid of ['exact', 'at-least']) {
64
+ const v = JSON.parse(JSON.stringify(basis))
65
+ v.input.accepted.amount = String(BigInt(v.input.payload.authorization.value) - 1n)
66
+ const u = await beoordeelAutorisatie(v.input, { ...(v.context ?? {}), doelNetwerk: v.network, amountPolicy: beleid })
67
+ console.log(' ' + beleid.padEnd(9) + ' signed ' + v.input.payload.authorization.value +
68
+ ', asked ' + v.input.accepted.amount + ' -> ' + (u.valid ? 'VALID' : u.reason_code))
69
+ }
70
+ console.log('')
71
+ console.log(' Two reference implementations reject this:')
72
+ console.log(' go/mechanisms/evm/exact/facilitator/eip3009.go:65 Cmp(requiredValue) != 0')
73
+ console.log(' python/x402/mechanisms/evm/exact/facilitator.py:235 value != amount')
74
+ console.log(' and that same python says on line 168: "Amount exactly matches requirements.amount"')
75
+ console.log(' So exact is our default. It stays a choice, which is why it is printed in')
76
+ console.log(' the result and not buried in the code.')
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ console.log('')
81
+ console.log('3. The second case no vector covers: the same nonce, a DIFFERENT payer')
82
+ console.log('')
83
+ {
84
+ const basis = lees('valid-baseline-001.json')
85
+ const nonce = basis.input.payload.authorization.nonce
86
+ const andereBetaler = '0x000000000000000000000000000000000000dEaD'
87
+
88
+ // Per betaler, zoals het token-contract het doet.
89
+ const perBetaler = await beoordeelVector({
90
+ ...basis,
91
+ context: { ...basis.context, seen_nonces: { [andereBetaler]: [nonce] } },
92
+ })
93
+ // De grove vorm: een lijst zonder onderscheid naar betaler.
94
+ const plat = await beoordeelVector({
95
+ ...basis,
96
+ context: { ...basis.context, seen_nonces: [nonce] },
97
+ })
98
+
99
+ console.log(' nonce already spent by ANOTHER address')
100
+ console.log(' per payer (how the contract does it) -> ' + (perBetaler.valid ? 'VALID' : perBetaler.reason_code))
101
+ console.log(' the same nonce spent by THIS payer')
102
+ console.log(' flat list -> ' + (plat.valid ? 'VALID' : plat.reason_code))
103
+ console.log('')
104
+ console.log(' EIP-3009 tracks this per address itself: mapping(address => mapping(bytes32')
105
+ console.log(' => bool)), readable as authorizationState(address authorizer, bytes32 nonce),')
106
+ console.log(' and the spec says nonces are random 32 bytes, unique to the authorizer. A')
107
+ console.log(' verifier holding a list without the payer is therefore STRICTER than the')
108
+ console.log(' chain, and could refuse a valid payment from someone else.')
109
+ }
110
+
111
+ console.log('')
112
+ console.log('Short version: on the ten published vectors neither choice makes a difference.')
113
+ console.log('On the two cases nobody published, it decides everything.')
@@ -0,0 +1,264 @@
1
+ // BENCHMARK: kan onze verifieerder voor de gek gehouden worden, en kunnen wij zelf
2
+ // een geldig tegengetekend bonnetje maken?
3
+ //
4
+ // WAAROM DIT APART VAN DE TESTS STAAT. De tests tonen aan dat wij op de vier
5
+ // gepubliceerde vectoren hetzelfde oordeel geven als hun eigen verifieerder. Dat is
6
+ // correctheid, en correctheid is niet hetzelfde als POSITIE. Een controle die alles
7
+ // goedkeurt geeft op vier vectoren precies dezelfde uitslag als een goede controle,
8
+ // zolang drie van die vier negatief zijn en hij per ongeluk de juiste reden noemt.
9
+ //
10
+ // Deze benchmark meet daarom twee dingen die de vectoren niet kunnen meten:
11
+ //
12
+ // 1. PRECISIE ONDER AANVAL. Neem het POSITIEVE geval en verander er systematisch
13
+ // een ding in. Elke mutatie MOET afgekeurd worden. Een mutatie die we
14
+ // goedkeuren is geen cosmetisch gebrek maar precies het gat waardoor een
15
+ // vervalst bonnetje erdoor komt, en dat is het enige dat deze verifieerder te
16
+ // verkopen heeft.
17
+ //
18
+ // 2. KUNNEN WIJ TEGENPARTIJ ZIJN. De onderhouder van INAM vroeg of wij de
19
+ // tegenpartij willen zijn op een echt bonnetje. Dat beloven zonder te weten of
20
+ // wij er zelf een kunnen maken en ondertekenen, is een belofte op krediet. Dit
21
+ // deel bouwt er een, met eigen sleutels, en laat onze eigen verifieerder erover
22
+ // oordelen. Lukt dat niet, dan is het antwoord nee en weten we dat nu.
23
+ //
24
+ // Geen netwerk, geen sleutel van de koning, alleen node-ingebouwde crypto.
25
+
26
+ import { createHash, generateKeyPairSync, sign as edSign } from 'node:crypto'
27
+ import { readFileSync } from 'node:fs'
28
+ import { fileURLToPath } from 'node:url'
29
+ import { dirname, join } from 'node:path'
30
+ import { canoniek, toetsGeval } from '../src/inam-countersigned.mjs'
31
+
32
+ const HIER = dirname(fileURLToPath(import.meta.url))
33
+ const suite = JSON.parse(readFileSync(join(HIER, '..', 'vectors', 'inam', 'vectors.json'), 'utf8'))
34
+ const positief = suite.cases.find((c) => c.id === 'POSITIVE')
35
+ if (!positief) {
36
+ console.error('geen POSITIVE-vector gevonden, deze benchmark heeft er een nodig')
37
+ process.exit(2)
38
+ }
39
+
40
+ const sha256hex = (s) => createHash('sha256').update(s, 'utf8').digest('hex')
41
+
42
+ // ── deel 1: precisie onder aanval ────────────────────────────────────────────
43
+
44
+ /** Maak een kopie van het geval met een mutatie in het ondertekende bestand. */
45
+ function muteer(naam, fn) {
46
+ const f = JSON.parse(positief.feedbackFile)
47
+ fn(f)
48
+ return { naam, geval: { ...positief, feedbackFile: JSON.stringify(f) } }
49
+ }
50
+
51
+ const eersteTeken = (s) => (s[0] === 'a' ? 'b' : 'a') + s.slice(1)
52
+
53
+ const mutaties = [
54
+ muteer('handtekening van de aanvrager omgedraaid', (f) => {
55
+ f.inam.receipt.signatures.agentA = eersteTeken(f.inam.receipt.signatures.agentA)
56
+ }),
57
+ muteer('handtekening van de uitvoerder omgedraaid', (f) => {
58
+ f.inam.receipt.signatures.agentB = eersteTeken(f.inam.receipt.signatures.agentB)
59
+ }),
60
+ muteer('beide handtekeningen weg', (f) => {
61
+ f.inam.receipt.signatures = {}
62
+ }),
63
+ muteer('de uitvoer-hash veranderd', (f) => {
64
+ f.inam.receipt.result.outputHash = 'sha256:' + '0'.repeat(64)
65
+ }),
66
+ muteer('de opdracht-hash veranderd', (f) => {
67
+ f.inam.receipt.task.specHash = 'sha256:' + '0'.repeat(64)
68
+ }),
69
+ muteer('het bonnetje-kenmerk veranderd', (f) => {
70
+ f.inam.receipt.receiptId = 'sha256:' + '1'.repeat(64)
71
+ }),
72
+ muteer('de opdracht-naam veranderd', (f) => {
73
+ f.inam.receipt.jobId = 'http:iets-anders'
74
+ }),
75
+ muteer('de aanvrager vervangen door een ander adres', (f) => {
76
+ f.clientAddress = 'eip155:8453:0x9999999999999999999999999999999999999999'
77
+ }),
78
+ muteer('de waarde opgeschroefd', (f) => {
79
+ f.value = 999
80
+ }),
81
+ muteer('de uitvoerder vervangen door de aanvrager', (f) => {
82
+ f.inam.receipt.agentB = { ...f.inam.receipt.agentA, role: 'worker' }
83
+ }),
84
+ muteer('een veld toegevoegd aan de ondertekende inhoud', (f) => {
85
+ f.inam.receipt.task.extra = 'ik was hier'
86
+ }),
87
+ muteer('de rol van de aanvrager omgedraaid', (f) => {
88
+ f.inam.receipt.agentA.role = 'worker'
89
+ }),
90
+ ]
91
+
92
+ console.log('DEEL 1: precisie onder aanval')
93
+ console.log(' Elke mutatie van ondertekende inhoud MOET afgekeurd worden.')
94
+ console.log('')
95
+
96
+ // Eerst de controle dat het onveranderde geval wel wordt geaccepteerd. Zonder die
97
+ // regel meet deel 1 niets: een verifieerder die alles afkeurt haalt hier anders
98
+ // een perfecte score.
99
+ const basis = toetsGeval(positief, suite)
100
+ console.log(' basislijn, onveranderd: ' + (basis.accepted ? 'GEACCEPTEERD (goed)' : 'AFGEKEURD op ' + basis.failedChecks.join(',') + ' (FOUT, dan meet de rest niets)'))
101
+
102
+ let afgekeurd = 0
103
+ let doorgelaten = 0
104
+ let alleenBytes = 0
105
+
106
+ // De byte-hash (feedbackHashMatchesFile) faalt bij ELKE mutatie, want het bestand is
107
+ // opnieuw geserialiseerd. Dat maakt 12 van 12 een misleidend cijfer: het zegt niet of
108
+ // de INHOUDELIJKE controles de mutatie pakken. Wie dat onderscheid niet maakt, meet
109
+ // zijn eigen serialisatie in plaats van zijn verifieerder. Daarom wordt die ene check
110
+ // hier apart gezet en geteld hoeveel mutaties ALLEEN daardoor sneuvelen.
111
+ const BYTE_CHECK = 'feedbackHashMatchesFile'
112
+
113
+ for (const m of mutaties) {
114
+ let uit
115
+ try {
116
+ uit = toetsGeval(m.geval, suite)
117
+ } catch (e) {
118
+ uit = { accepted: false, failedChecks: ['wierp een fout: ' + String(e.message).slice(0, 40)] }
119
+ }
120
+ const inhoudelijk = uit.failedChecks.filter((c) => c !== BYTE_CHECK)
121
+ if (uit.accepted) {
122
+ doorgelaten++
123
+ console.log(' DOORGELATEN ' + m.naam + ' <- dit is een gat')
124
+ } else if (inhoudelijk.length === 0) {
125
+ alleenBytes++
126
+ console.log(' alleen bytes ' + m.naam + ' <- geen inhoudelijke controle pakt dit')
127
+ } else {
128
+ afgekeurd++
129
+ console.log(' inhoudelijk ' + m.naam + ' (' + inhoudelijk.slice(0, 3).join(', ') + ')')
130
+ }
131
+ }
132
+ console.log('')
133
+ console.log(' ' + afgekeurd + ' van ' + mutaties.length + ' mutaties door een INHOUDELIJKE controle gepakt')
134
+ console.log(' ' + alleenBytes + ' alleen door de byte-hash, ' + doorgelaten + ' helemaal doorgelaten')
135
+ if (alleenBytes > 0) {
136
+ console.log('')
137
+ console.log(' Let op wat dat betekent. De byte-hash vergelijkt het bestand met een hash in een')
138
+ console.log(' on-chain gebeurtenis. Waar die gebeurtenis er niet is, of waar dezelfde partij')
139
+ console.log(' beide kan zetten, is die controle geen bescherming. Een mutatie die alleen daar')
140
+ console.log(' op sneuvelt, komt in zo een geval dus wel door.')
141
+ }
142
+
143
+ // ── deel 2: kunnen wij tegenpartij zijn ──────────────────────────────────────
144
+
145
+ console.log('')
146
+ console.log('DEEL 2: kunnen wij zelf een tegengetekend bonnetje maken')
147
+
148
+ const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
149
+ function base58encode(buf) {
150
+ let getal = 0n
151
+ for (const b of buf) getal = getal * 256n + BigInt(b)
152
+ let uit = ''
153
+ while (getal > 0n) {
154
+ uit = B58[Number(getal % 58n)] + uit
155
+ getal /= 58n
156
+ }
157
+ for (const b of buf) {
158
+ if (b === 0) uit = '1' + uit
159
+ else break
160
+ }
161
+ return uit
162
+ }
163
+
164
+ /** did:key voor een ed25519 publieke sleutel, multicodec 0xed01. */
165
+ function didKey(publiek) {
166
+ const raw = publiek.export({ format: 'der', type: 'spki' }).subarray(-32)
167
+ return 'did:key:z' + base58encode(Buffer.concat([Buffer.from([0xed, 0x01]), raw]))
168
+ }
169
+
170
+ const paarA = generateKeyPairSync('ed25519')
171
+ const paarB = generateKeyPairSync('ed25519')
172
+ const didA = didKey(paarA.publicKey)
173
+ const didB = didKey(paarB.publicKey)
174
+ console.log(' twee verse sleutels gemaakt, geen sleutel van de koning gebruikt')
175
+
176
+ // De bytes die een tegenpartij in het echt zou hebben gezien. Hier verzonnen, maar
177
+ // de hashes eroverheen worden echt berekend, want dat is de hele constructie.
178
+ const verzoek = { method: 'POST', url: 'https://api.tokenizen.nl/api/paid/verify-ownership', body: '{"endpoint":"https://example.org/sse"}' }
179
+ const antwoord = { body: '{"ok":true,"method":"dns-txt"}' }
180
+ const u = new URL(verzoek.url)
181
+ const specHash = 'sha256:' + sha256hex(verzoek.method + ' ' + u.pathname + u.search + '\n' + verzoek.body)
182
+ const outputHash = 'sha256:' + sha256hex(antwoord.body)
183
+
184
+ const kern = {
185
+ agentA: { id: didA, role: 'requester' },
186
+ agentB: { id: didB, role: 'worker' },
187
+ jobId: 'http:tokenizen-conformance-check',
188
+ receiptVersion: '1.0',
189
+ task: { capability: 'endpoint.ownership-verification', createdAt: '2026-10-17T10:00:00.000Z', specHash },
190
+ result: { completedAt: '2026-10-17T10:00:01.000Z', outputHash },
191
+ settlement: { rail: 'x402', network: 'eip155:84532', amount: '1000' },
192
+ verification: { method: 'dns-txt' },
193
+ }
194
+
195
+ const zeven = {}
196
+ for (const k of ['jobId', 'agentA', 'agentB', 'task', 'result', 'settlement', 'verification']) zeven[k] = kern[k]
197
+ const receiptId = 'sha256:' + sha256hex(canoniek(zeven))
198
+
199
+ const teTekenen = { ...kern, receiptId }
200
+ const inhoud = canoniek(teTekenen)
201
+ const sigA = edSign(null, Buffer.from(inhoud, 'utf8'), paarA.privateKey).toString('base64')
202
+ const sigB = edSign(null, Buffer.from(inhoud, 'utf8'), paarB.privateKey).toString('base64')
203
+
204
+ const bestand = {
205
+ agentId: 'did:tokenizen:conformance',
206
+ agentRegistry: 'eip155:84532',
207
+ clientAddress: 'eip155:84532:0x3333333333333333333333333333333333333333',
208
+ createdAt: '2026-10-17T10:00:02.000Z',
209
+ value: 100,
210
+ valueDecimals: 0,
211
+ tag1: 'conformance',
212
+ tag2: 'x402',
213
+ inam: { receipt: { ...teTekenen, signatures: { agentA: sigA, agentB: sigB } } },
214
+ }
215
+ const bestandBytes = JSON.stringify(bestand)
216
+
217
+ const eigenSuite = {
218
+ suite: 'eigen-rondgang',
219
+ parties: {
220
+ requester: { did: didA, erc8004Address: '0x3333333333333333333333333333333333333333' },
221
+ provider: { did: didB, erc8004Address: '0x4444444444444444444444444444444444444444' },
222
+ },
223
+ observed: { request: verzoek, response: antwoord },
224
+ }
225
+ const eigenGeval = {
226
+ id: 'EIGEN-RONDGANG',
227
+ feedbackFile: bestandBytes,
228
+ onchainEvent: {
229
+ clientAddress: '0x3333333333333333333333333333333333333333',
230
+ value: 100,
231
+ feedbackHash: null,
232
+ },
233
+ }
234
+
235
+ const oordeel = toetsGeval(eigenGeval, eigenSuite)
236
+ const alleenKeccak = oordeel.failedChecks.length === 1 && oordeel.failedChecks[0] === 'feedbackHashMatchesFile'
237
+ console.log(' ons eigen bonnetje door onze eigen verifieerder: ' + (oordeel.accepted ? 'GEACCEPTEERD' : 'afgekeurd op ' + oordeel.failedChecks.join(', ')))
238
+ if (!oordeel.accepted && alleenKeccak) {
239
+ console.log(' (alleen de keccak-controle faalt, en die vergelijkt met een on-chain gebeurtenis die hier')
240
+ console.log(' niet bestaat omdat er niets is uitgezonden. De acht controles die over het BONNETJE gaan,')
241
+ console.log(' inclusief beide handtekeningen en het herrekende kenmerk, slagen.)')
242
+ }
243
+
244
+ const kanTegenpartijZijn = oordeel.accepted || alleenKeccak
245
+ console.log(' conclusie: wij ' + (kanTegenpartijZijn ? 'KUNNEN' : 'kunnen NIET') + ' tegenpartij zijn op een echt bonnetje')
246
+
247
+ // ── samenvatting ─────────────────────────────────────────────────────────────
248
+
249
+ console.log('')
250
+ console.log('SAMENVATTING')
251
+ console.log(' basislijn geaccepteerd : ' + (basis.accepted ? 'ja' : 'NEE'))
252
+ console.log(' inhoudelijk gepakt : ' + afgekeurd + '/' + mutaties.length)
253
+ console.log(' alleen door de byte-hash : ' + alleenBytes)
254
+ console.log(' doorgelaten : ' + doorgelaten)
255
+ console.log(' eigen bonnetje maakbaar : ' + (kanTegenpartijZijn ? 'ja' : 'NEE'))
256
+
257
+ // alleenBytes maakt dit NIET rood: het is een eigenschap van de constructie en een
258
+ // bevinding om te melden, geen gebrek in onze code. Doorgelaten wel, want dat is een gat.
259
+ const faalt = !basis.accepted || doorgelaten > 0 || !kanTegenpartijZijn
260
+ if (faalt) {
261
+ console.log('')
262
+ console.log(' Dit is een bevinding, niet alleen een cijfer. Repareer het voor er een claim op gebaseerd wordt.')
263
+ }
264
+ process.exitCode = faalt ? 1 : 0
package/bin/cli.mjs CHANGED
@@ -52,6 +52,16 @@ function gebruik() {
52
52
  console.log(' npx github:holistis/agent-receipt-verify kanon')
53
53
  console.log(' cross-check our independent EIP-3009 verifier against the ten published')
54
54
  console.log(' vectors of iamonuwa/kanon, offline, no account and no key.')
55
+ console.log('')
56
+ console.log(' npx github:holistis/agent-receipt-verify beleid')
57
+ console.log(' the same ten vectors under both amount policies, plus the two cases no')
58
+ console.log(' published vector covers: overpayment, and one nonce across two payers.')
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 link-vectors')
62
+ console.log(' twelve vectors for the half AFTER paying: does this receipt belong to')
63
+ console.log(' that one settlement? Eleven run offline, the chain-reading one is')
64
+ console.log(' marked and skipped rather than failed.')
55
65
  }
56
66
 
57
67
  // Het argument bepaalt welke module draait. De modules zijn ook los te draaien en te
@@ -64,6 +74,8 @@ const onderdelen = {
64
74
  inam: '../src/inam-countersigned.mjs',
65
75
  liveness: '../src/x402-index-liveness.mjs',
66
76
  kanon: '../src/eip3009-autorisatie.mjs',
77
+ beleid: '../bench/eip3009-beleid.mjs',
78
+ 'link-vectors': '../src/settlement-link-vectors.mjs',
67
79
  }
68
80
 
69
81
  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.1",
3
+ "version": "0.2.3",
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",
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",
@@ -18,7 +18,9 @@
18
18
  "metingen-bundel": "node src/x402-metingen-bundel.mjs",
19
19
  "mcp": "node src/mcp-server.mjs",
20
20
  "bundel": "node tools/bundel-bouw.mjs build/bundle",
21
- "kanon": "node src/eip3009-autorisatie.mjs --vectoren"
21
+ "kanon": "node src/eip3009-autorisatie.mjs --vectoren",
22
+ "bench-eip3009": "node bench/eip3009-beleid.mjs",
23
+ "link-vectors": "node src/settlement-link-vectors.mjs"
22
24
  },
23
25
  "optionalDependencies": {
24
26
  "ethers": "^6.17.0"
@@ -35,6 +37,7 @@
35
37
  "NOTICE",
36
38
  "CONFORMANCE.md",
37
39
  "README.md",
40
+ "bench/",
38
41
  "tools/"
39
42
  ],
40
43
  "author": "Abdellah Ouadoudi (https://github.com/holistis)",
@@ -141,19 +141,52 @@ export async function beoordeelAutorisatie(input, context = {}) {
141
141
  return { valid: false, reason_code: 'SIGNER_MISMATCH', hersteld }
142
142
  }
143
143
 
144
- // 5. Het bedrag, pas NA de handtekening: een authentiek ondertekende onderbetaling.
145
- if (BigInt(auth.value ?? 0) < BigInt(accepted.amount ?? 0)) {
146
- return { valid: false, reason_code: 'AMOUNT_INSUFFICIENT' }
144
+ // 5. Het bedrag, pas NA de handtekening: een authentiek ondertekende afwijking.
145
+ //
146
+ // DE STANDAARD NEEMT HIER GEEN POSITIE IN, DUS WIJ MAKEN DE KEUZE ZICHTBAAR. Twee
147
+ // referentie-implementaties eisen GELIJKHEID en wijzen dus ook te veel betalen af:
148
+ // go/mechanisms/evm/exact/facilitator/eip3009.go regel 65 (`Cmp(requiredValue) != 0`,
149
+ // fout ErrAuthorizationValueMismatch) en python/x402/mechanisms/evm/exact/
150
+ // facilitator.py regel 235 (`!=`), met op regel 168 de eigen zin "Amount exactly
151
+ // matches requirements.amount". Onze eerste versie liet te veel betalen toe en zou
152
+ // dus op dat ene geval van hen afwijken, terwijl geen enkele gepubliceerde vector
153
+ // het dekt. Daarom staat 'exact' nu als standaard, is 'at-least' een bewuste keuze,
154
+ // en draagt de uitslag ALTIJD welke van de twee gebruikt is.
155
+ const beleid = context.amountPolicy === 'at-least' ? 'at-least' : 'exact'
156
+ const betaald = BigInt(auth.value ?? 0)
157
+ const gevraagd = BigInt(accepted.amount ?? 0)
158
+ const bedragFout = beleid === 'exact' ? betaald !== gevraagd : betaald < gevraagd
159
+ if (bedragFout) {
160
+ return { valid: false, reason_code: 'AMOUNT_INSUFFICIENT', amountPolicy: beleid }
147
161
  }
148
162
 
149
- // 6. Is deze nonce al verbruikt. Dit is toestand van buiten, geen eigenschap van de
150
- // autorisatie, en komt daarom uit de context in plaats van uit de betaling.
151
- const gezien = (context.seen_nonces ?? []).map((n) => String(n).toLowerCase())
152
- if (gezien.includes(String(auth.nonce).toLowerCase())) {
163
+ // 6. Is deze nonce al verbruikt, PER BETALER en niet over iedereen heen.
164
+ //
165
+ // EIP-3009 houdt dit zelf per adres bij: `mapping(address => mapping(bytes32 =>
166
+ // bool))`, en de leesfunctie heet `authorizationState(address authorizer, bytes32
167
+ // nonce)`. De spec zegt er bovendien bij dat nonces willekeurige 32 bytes zijn,
168
+ // uniek per adres, en juist geen volgnummers. Een verifieerder die EEN lijst
169
+ // bijhoudt zonder onderscheid naar betaler is dus STRENGER dan de keten zelf: hij
170
+ // zou een geldige betaling van een andere partij weigeren die toevallig dezelfde
171
+ // 32 bytes koos. Onze eerste versie deed precies dat.
172
+ //
173
+ // Twee vormen worden aanvaard. Een platte lijst hoort bij DEZE betaler, want zo
174
+ // gebruiken de gepubliceerde vectoren hem. Een object met het adres als sleutel is
175
+ // de nauwkeurige vorm en komt overeen met wat het token-contract doet.
176
+ const betaler = String(auth.from ?? '').toLowerCase()
177
+ const bron = context.seen_nonces
178
+ let gezien = []
179
+ if (Array.isArray(bron)) gezien = bron
180
+ else if (bron && typeof bron === 'object') {
181
+ for (const [adres, lijst] of Object.entries(bron)) {
182
+ if (String(adres).toLowerCase() === betaler) gezien = lijst ?? []
183
+ }
184
+ }
185
+ if (gezien.map((n) => String(n).toLowerCase()).includes(String(auth.nonce).toLowerCase())) {
153
186
  return { valid: false, reason_code: 'NONCE_REPLAY' }
154
187
  }
155
188
 
156
- return { valid: true, reason_code: 'VALID', ondertekenaar: hersteld }
189
+ return { valid: true, reason_code: 'VALID', ondertekenaar: hersteld, amountPolicy: beleid }
157
190
  }
158
191
 
159
192
  /** Een hele vector beoordelen, met zijn eigen context en doelnetwerk. */