agent-receipt-verify 0.0.0-stage → 0.2.2

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 (40) hide show
  1. package/CONFORMANCE.md +110 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +39 -0
  4. package/README.md +273 -2
  5. package/bench/eip3009-beleid.mjs +113 -0
  6. package/bench/robustness.mjs +264 -0
  7. package/bin/cli.mjs +95 -0
  8. package/examples/receipt-live-base-sepolia-2.json +21 -0
  9. package/examples/receipt-live-base-sepolia-3.json +21 -0
  10. package/examples/receipt-live-base-sepolia.json +21 -0
  11. package/package.json +61 -4
  12. package/profiles/profile.schema.json +115 -0
  13. package/profiles/x402-deployed-ladder-v1.json +178 -0
  14. package/src/eip3009-autorisatie.mjs +228 -0
  15. package/src/inam-countersigned.mjs +162 -0
  16. package/src/mcp-server.mjs +262 -0
  17. package/src/profiel.mjs +145 -0
  18. package/src/x402-conformance-check.mjs +194 -0
  19. package/src/x402-conformance-treden.mjs +246 -0
  20. package/src/x402-discovery-census.mjs +247 -0
  21. package/src/x402-index-liveness.mjs +201 -0
  22. package/src/x402-link-onchain.mjs +280 -0
  23. package/src/x402-metingen-bundel.mjs +96 -0
  24. package/src/x402-offer-receipt.mjs +84 -0
  25. package/src/x402-poortwachters.mjs +241 -0
  26. package/tools/bundel-bouw.mjs +126 -0
  27. package/tools/pack-inhoud.mjs +96 -0
  28. package/vectors/inam/BRON-README.md +38 -0
  29. package/vectors/inam/canonical-vectors.json +122 -0
  30. package/vectors/inam/vectors.json +96 -0
  31. package/vectors/kanon/amount-insufficient-001.json +52 -0
  32. package/vectors/kanon/cross-chain-replay-001.json +54 -0
  33. package/vectors/kanon/cross-contract-replay-001.json +54 -0
  34. package/vectors/kanon/domain-name-mismatch-001.json +53 -0
  35. package/vectors/kanon/expired-001.json +51 -0
  36. package/vectors/kanon/network-mismatch-001.json +52 -0
  37. package/vectors/kanon/nonce-replay-001.json +56 -0
  38. package/vectors/kanon/not-yet-valid-001.json +51 -0
  39. package/vectors/kanon/sig-malleable-high-s-001.json +54 -0
  40. package/vectors/kanon/valid-baseline-001.json +52 -0
@@ -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 ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // Een commando in plaats van een document.
3
+ //
4
+ // WAAROM DIT BESTAAT. Niemand neemt een meetlat over omdat die in een bestand staat.
5
+ // Mensen nemen hem over als het makkelijkste wat ze kunnen doen een regel is die hun
6
+ // eigen eindpunt nakijkt. Dan is de ladder de norm geworden zonder dat er ooit over
7
+ // gestemd is.
8
+ //
9
+ // BEWUST GEEN NPM-PUBLICATIE NODIG. npx kan een pakket rechtstreeks uit een git-repo
10
+ // draaien, dus dit werkt zonder dat er iets op npm staat:
11
+ //
12
+ // npx github:holistis/agent-receipt-verify https://een-eindpunt.example/pad
13
+ //
14
+ // Dat is geen trucje maar een echte eigenschap van npx, en het haalt een hele
15
+ // publicatie-stap plus een naamclaim weg. Het betekent ook dat niemand een
16
+ // tussenversie van ons kan uitgeven: wat je draait is wat er in de repo staat.
17
+ //
18
+ // ethers is OPTIONEEL. Zonder ethers kan trede 2 van een eip712-aanbod niet
19
+ // gecontroleerd worden, en dan zegt de uitslag ONGEMETEN in plaats van stil een
20
+ // zwakkere controle te doen. Een jws-aanbod werkt wel zonder ethers, want dat gaat
21
+ // op ingebouwde crypto.
22
+
23
+ const args = process.argv.slice(2)
24
+ const eerste = args[0]
25
+
26
+ function gebruik() {
27
+ console.log('agent-receipt-verify, een onafhankelijke controle op bonnetjes van agent-betalingen')
28
+ console.log('')
29
+ console.log(' npx github:holistis/agent-receipt-verify <url> [url ...]')
30
+ console.log(' meet een x402-eindpunt tegen de ladder in CONFORMANCE.md (trede 0 tot 2,')
31
+ console.log(' zonder te betalen). Trede 3 tot 5 vereisen een echte afrekening en komen')
32
+ console.log(' terug als ONGEMETEN, nooit als afgekeurd.')
33
+ console.log('')
34
+ console.log(' npx github:holistis/agent-receipt-verify census [uit.json]')
35
+ console.log(' telt een discovery-index per host in plaats van per resource.')
36
+ console.log('')
37
+ console.log(' npx github:holistis/agent-receipt-verify link --receipt=<bestand>')
38
+ console.log(' zoekt bij een bonnetje van versie 2 de bijbehorende afrekening op de keten.')
39
+ console.log('')
40
+ console.log(' npx github:holistis/agent-receipt-verify mcp')
41
+ console.log(' draait als MCP-server over stdio, zodat een agent deze controle AANROEPT')
42
+ console.log(' in plaats van hem aanbevolen te krijgen. Twee gereedschappen: controleer een')
43
+ console.log(' eindpunt voor je betaalt, en link een bonnetje aan een echte afrekening.')
44
+ console.log('')
45
+ console.log(' npx github:holistis/agent-receipt-verify inam <vectors.json>')
46
+ console.log(' controleert countersigned bonnetjes tegen gepubliceerde vectoren.')
47
+ console.log('')
48
+ console.log('Wat dit NIET doet: het oordeelt over de VORM van een bonnetje, niet over de')
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')
53
+ console.log(' cross-check our independent EIP-3009 verifier against the ten published')
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
+ }
61
+
62
+ // Het argument bepaalt welke module draait. De modules zijn ook los te draaien en te
63
+ // importeren; dit bestand voegt alleen een ingang toe, geen tweede implementatie,
64
+ // want twee plekken die hetzelfde doen lopen altijd uit elkaar.
65
+ const onderdelen = {
66
+ mcp: '../src/mcp-server.mjs',
67
+ census: '../src/x402-discovery-census.mjs',
68
+ link: '../src/x402-link-onchain.mjs',
69
+ inam: '../src/inam-countersigned.mjs',
70
+ liveness: '../src/x402-index-liveness.mjs',
71
+ kanon: '../src/eip3009-autorisatie.mjs',
72
+ beleid: '../bench/eip3009-beleid.mjs',
73
+ }
74
+
75
+ if (!eerste || eerste === '--help' || eerste === '-h' || eerste === 'help') {
76
+ gebruik()
77
+ process.exitCode = eerste ? 0 : 2
78
+ } else if (onderdelen[eerste]) {
79
+ // De doorgegeven argumenten zo neerzetten dat de module zijn eigen cli-tak kiest:
80
+ // die kijkt naar process.argv[1], dus die moet naar de module zelf wijzen.
81
+ const { fileURLToPath } = await import('node:url')
82
+ const pad = fileURLToPath(new URL(onderdelen[eerste], import.meta.url))
83
+ process.argv = [process.argv[0], pad, ...args.slice(1)]
84
+ await import(onderdelen[eerste])
85
+ } else if (/^https?:\/\//.test(eerste)) {
86
+ const { fileURLToPath } = await import('node:url')
87
+ const pad = fileURLToPath(new URL('../src/x402-conformance-check.mjs', import.meta.url))
88
+ process.argv = [process.argv[0], pad, ...args]
89
+ await import('../src/x402-conformance-check.mjs')
90
+ } else {
91
+ console.log('onbekend: ' + eerste)
92
+ console.log('')
93
+ gebruik()
94
+ process.exitCode = 2
95
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "note": "The second of two payments with IDENTICAL terms: same payer, same payee, same amount, same resource, minutes apart. Without a commitment these two receipts cannot be told apart. Each one still resolves to its own settlement.",
3
+ "source": "A real payment to https://api.tokenizen.nl/api/paid/verify-ownership on Base Sepolia, 2026-10-16. Buyer and seller are both us.",
4
+ "network": "eip155:84532",
5
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
6
+ "authorizationSalt": "0x63072ea656fe922bf04c9cefc4275314d695fff01e5b57f6628c8df0252cc5dc",
7
+ "receipt": {
8
+ "format": "eip712",
9
+ "payload": {
10
+ "version": 2,
11
+ "network": "eip155:84532",
12
+ "resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
13
+ "payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
14
+ "issuedAt": 1792187518,
15
+ "transaction": "",
16
+ "authorizationMethod": "eip3009",
17
+ "authorizationCommitment": "0xfecee93e1aec7d6858a218bf324f23e2a4eed07925d8d4542b9662c5026e3d22"
18
+ },
19
+ "signature": "0x02f5ba0d2b7928a2dbb1d8845b664a79d38eebcd0a2c4532ed3c6b922051bf8d47c8aafb0520a875a708e3ed480d84a537962ea90d66ce2ee3811f4229aa8b401b"
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "note": "A third payment on the same terms, so an independent implementation has more than one case to reach the same settlement on. Holds only what section 5.6.4 says a later verifier has: the receipt, its salt, and the asset that was paid.",
3
+ "source": "A real payment to https://api.tokenizen.nl/api/paid/verify-ownership on Base Sepolia, 2026-10-17. Buyer and seller are both us.",
4
+ "network": "eip155:84532",
5
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
6
+ "authorizationSalt": "0x349ddb90f38a3119817c5fc9cc253a2c20bcbe44aa845302df76889082b314d8",
7
+ "receipt": {
8
+ "format": "eip712",
9
+ "payload": {
10
+ "version": 2,
11
+ "network": "eip155:84532",
12
+ "resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
13
+ "payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
14
+ "issuedAt": 1792188211,
15
+ "transaction": "",
16
+ "authorizationMethod": "eip3009",
17
+ "authorizationCommitment": "0xea8dd34b8beac90f021a4e8d893a689c25249f86947770c8e6647c1cde206008"
18
+ },
19
+ "signature": "0x5b3a70cce6978ab44e51828b5ebbc55522e98b015045d94559d369420698da3b5309b28050dcb00ce14d65c8b230a273f8ff5ebff6410c6ae2a308e422ed5a611b"
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "note": "The first of two payments with IDENTICAL terms. This file holds exactly what section 5.6.4 says a later verifier has: the receipt, its salt, and the asset that was paid. The settlement nonce and transaction hash are deliberately NOT here; finding them is what the tool has to do.",
3
+ "source": "A real payment to https://api.tokenizen.nl/api/paid/verify-ownership on Base Sepolia, 2026-10-16. Buyer and seller are both us, so this proves the chain closes end to end, not that anyone else uses it.",
4
+ "network": "eip155:84532",
5
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
6
+ "authorizationSalt": "0xea85b2473363fd8fff1ccdd7f8638be8b47c7c05023e3e2e3f81bebb085ba2ad",
7
+ "receipt": {
8
+ "format": "eip712",
9
+ "payload": {
10
+ "version": 2,
11
+ "network": "eip155:84532",
12
+ "resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
13
+ "payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
14
+ "issuedAt": 1792187030,
15
+ "transaction": "",
16
+ "authorizationMethod": "eip3009",
17
+ "authorizationCommitment": "0x5c35a61a09a566a9c284fad18a70ac2ddfff34d01d2a3ce776ce94e8d5b38ff8"
18
+ },
19
+ "signature": "0x78615cb22986af0aad6e08b74ca78773bb58ece21660e84e4b35b3e5c1841b0075238ca9bfdf0f969ad3f2903af8f391fafd1b3fc7ebbf1fb5c949a3948fccde1c"
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "agent-receipt-verify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.2",
4
+ "description": "Checks x402 endpoint conformance before an agent pays, and links the receipt to its settlement.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
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",
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
+ "conformance": "node src/x402-conformance-check.mjs",
14
+ "bench": "node bench/robustness.mjs",
15
+ "census": "node src/x402-discovery-census.mjs",
16
+ "liveness": "node src/x402-index-liveness.mjs",
17
+ "poortwachters": "node src/x402-poortwachters.mjs",
18
+ "metingen-bundel": "node src/x402-metingen-bundel.mjs",
19
+ "mcp": "node src/mcp-server.mjs",
20
+ "bundel": "node tools/bundel-bouw.mjs build/bundle",
21
+ "kanon": "node src/eip3009-autorisatie.mjs --vectoren",
22
+ "bench-eip3009": "node bench/eip3009-beleid.mjs"
23
+ },
24
+ "optionalDependencies": {
25
+ "ethers": "^6.17.0"
26
+ },
27
+ "bin": {
28
+ "agent-receipt-verify": "bin/cli.mjs"
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "src/",
33
+ "profiles/",
34
+ "vectors/",
35
+ "examples/",
36
+ "NOTICE",
37
+ "CONFORMANCE.md",
38
+ "README.md",
39
+ "bench/",
40
+ "tools/"
41
+ ],
42
+ "author": "Abdellah Ouadoudi (https://github.com/holistis)",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/holistis/agent-receipt-verify.git"
46
+ },
47
+ "homepage": "https://github.com/holistis/agent-receipt-verify#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/holistis/agent-receipt-verify/issues"
50
+ },
51
+ "keywords": [
52
+ "x402",
53
+ "mcp",
54
+ "model-context-protocol",
55
+ "conformance",
56
+ "receipts",
57
+ "agent-payments",
58
+ "eip-3009",
59
+ "erc-8004",
60
+ "base",
61
+ "verification"
62
+ ]
63
+ }