agent-receipt-verify 0.2.6 → 0.2.7

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.
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * METHODEFOUT-BENCHMARK -- meet de foutmarge van DEZE meter tegen de echte markt.
4
+ *
5
+ * WAAROM DIT BESTAAT, en het is een eigen fout die het nodig maakte. Deze verifieerder
6
+ * sondeert standaard met POST. Een betaalde route die alleen GET bedient antwoordt
7
+ * daarop 405, of 404 bij veel webraamwerken, en dat las de meter als "dit eindpunt vraagt
8
+ * niet om betaling zoals x402 voorschrijft". Gemeten op 18 oktober 2026, op zestig unieke
9
+ * hosts uit de CDP-index: van de 57 die ECHT om betaling vragen keurde de meter er 36 af.
10
+ * 63,2 procent. Dat bleef onopgemerkt omdat er alleen tegen eigen testvectoren getoetst
11
+ * werd, en testvectoren bevatten precies de vorm die de schrijver al in gedachten had.
12
+ *
13
+ * Een meter die zijn eigen foutmarge tegen de werkelijkheid niet meet, meet niets.
14
+ *
15
+ * DIT GEBRUIKT DE ECHTE METER, niet een nagebouwde sonde. Een eerste versie van dit
16
+ * bestand bouwde het verzoek zelf na, en dat is precies de fout die het wil opsporen: dan
17
+ * benchmark je een replica die met de echte code uit elkaar loopt. Daarom importeert dit
18
+ * `meetEindpunt` uit src.
19
+ *
20
+ * GEDRAGSREGELS, hard, want dit raakt eindpunten van anderen:
21
+ * - de meter doet zelf een POST en hoogstens een herhaling; dit voegt er alleen bij een
22
+ * AFKEURING nog een GET aan toe om te zien of de afkeuring terecht was
23
+ * - een host maar een keer, anders weegt een operator met honderd paden honderd keer
24
+ * - nooit betalen, nooit fuzzen, nooit herhalen om iets af te dwingen
25
+ * - rust tussen de hosts
26
+ *
27
+ * GEBRUIK:
28
+ * node bench/methodefout.mjs --urls <bestand.json> een array van resource-urls
29
+ * node bench/methodefout.mjs --urls - < bronnen.json hetzelfde, van stdin
30
+ * node bench/methodefout.mjs --urls x.json --uit uitslag.jsonl
31
+ *
32
+ * De lijst met bronnen komt van buiten dit bestand, want die vergt indextoegang en een
33
+ * sleutel, en een benchmark hoort geen geheimen te hoeven lezen.
34
+ *
35
+ * EXITCODES: 0 gemeten, 1 er is een RESTFOUT (de meter keurt nog hosts af die wel om
36
+ * betaling vragen), 2 de meting kon niet draaien.
37
+ */
38
+
39
+ import fs from 'node:fs'
40
+ import { meetEindpunt, hoogsteTrede } from '../src/x402-conformance-check.mjs'
41
+
42
+ const UA = 'x402-conformance-check/1 (read-only, github.com/holistis/agent-receipt-verify)'
43
+ const rust = (ms) => new Promise((r) => setTimeout(r, ms))
44
+
45
+ function vlag(naam, standaard = null) {
46
+ const v = process.argv.find((a) => a === naam || a.startsWith(naam + '='))
47
+ if (!v) return standaard
48
+ if (v.includes('=')) return v.slice(naam.length + 1)
49
+ return process.argv[process.argv.indexOf(v) + 1] ?? standaard
50
+ }
51
+
52
+ /** Een host maar EEN keer, anders weegt een operator met honderd paden honderd keer. */
53
+ export function eenPerHost(urls) {
54
+ const perHost = new Map()
55
+ for (const u of urls) {
56
+ try {
57
+ const h = new URL(u).host
58
+ if (!perHost.has(h)) perHost.set(h, u)
59
+ } catch {}
60
+ }
61
+ return [...perHost.values()]
62
+ }
63
+
64
+ /**
65
+ * Antwoordt dit eindpunt op GET WEL met 402? Dit wordt alleen gevraagd als de meter het
66
+ * eindpunt AFKEURDE, om te zien of die afkeuring terecht was. Een afkeuring die wij niet
67
+ * natrekken, telt stil als een eigenschap van de markt in plaats van als onze restfout.
68
+ */
69
+ async function tweedeKansOpGet(url, fetchImpl) {
70
+ try {
71
+ const r = await fetchImpl(url, {
72
+ method: 'GET',
73
+ headers: { 'User-Agent': UA, Accept: 'application/json' },
74
+ signal: AbortSignal.timeout(12000),
75
+ })
76
+ return r.status === 402
77
+ } catch {
78
+ return false
79
+ }
80
+ }
81
+
82
+ export async function benchmark(urls, { fetchImpl = fetch, pauzeMs = 500 } = {}) {
83
+ const t = {
84
+ gemeten: 0,
85
+ haaltL0: 0,
86
+ // Hoeveel hosts de meter alleen haalt DOORDAT hij een herhaling doet. Dit is wat de
87
+ // reparatie oplevert, en het is tegelijk de omvang van de oude fout.
88
+ gereddenDoorHerhaling: 0,
89
+ // De restfout: de meter keurt af, maar het eindpunt antwoordt wel 402 op GET. Dit
90
+ // hoort nul te zijn. Staat hij boven nul, dan is er nog een vorm die wij missen.
91
+ restfout: 0,
92
+ terechtAfgekeurd: 0,
93
+ ongemeten: 0,
94
+ }
95
+ const gered = []
96
+ const rest = []
97
+ for (const u of urls) {
98
+ t.gemeten++
99
+ const m = await meetEindpunt(u, { fetchImpl })
100
+ const trede = hoogsteTrede(m)
101
+ if (m.treden.t0 === 'ONGEMETEN') {
102
+ t.ongemeten++
103
+ } else if (trede >= 0) {
104
+ t.haaltL0++
105
+ if (m.bewijs.methode_opnieuw) {
106
+ t.gereddenDoorHerhaling++
107
+ gered.push({ url: u, eerste_status: m.bewijs.methode_opnieuw.eerste_status, tweede: m.bewijs.methode_opnieuw.tweede })
108
+ }
109
+ } else {
110
+ await rust(Math.min(300, pauzeMs))
111
+ if (await tweedeKansOpGet(u, fetchImpl)) {
112
+ t.restfout++
113
+ rest.push({ url: u, bewijs: m.bewijs.t0 })
114
+ } else {
115
+ t.terechtAfgekeurd++
116
+ }
117
+ }
118
+ if (pauzeMs) await rust(pauzeMs)
119
+ }
120
+ const vraagtEchtOmBetaling = t.haaltL0 + t.restfout
121
+ return {
122
+ ...t,
123
+ vraagtEchtOmBetaling,
124
+ restfoutPct: vraagtEchtOmBetaling > 0 ? Number(((t.restfout / vraagtEchtOmBetaling) * 100).toFixed(1)) : null,
125
+ zonderHerhalingFoutPct:
126
+ vraagtEchtOmBetaling > 0
127
+ ? Number((((t.gereddenDoorHerhaling + t.restfout) / vraagtEchtOmBetaling) * 100).toFixed(1))
128
+ : null,
129
+ gered,
130
+ rest,
131
+ }
132
+ }
133
+
134
+ if (process.argv[1]?.endsWith('methodefout.mjs')) {
135
+ const bron = vlag('--urls')
136
+ if (!bron) {
137
+ console.error('geef een bestand met resource-urls: --urls bronnen.json (of - voor stdin)')
138
+ process.exit(2)
139
+ }
140
+ let ruw
141
+ try {
142
+ ruw = bron === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(bron, 'utf8')
143
+ } catch (e) {
144
+ console.error('kan de bronlijst niet lezen: ' + e.message)
145
+ process.exit(2)
146
+ }
147
+ let lijst
148
+ try {
149
+ const j = JSON.parse(ruw)
150
+ lijst = Array.isArray(j) ? j : (j.urls ?? j.resources ?? [])
151
+ } catch {
152
+ lijst = ruw.split(/\r?\n/).map((s) => s.trim()).filter((s) => /^https?:\/\//.test(s))
153
+ }
154
+ const hosts = eenPerHost(lijst.filter((u) => typeof u === 'string'))
155
+ const aantal = Number(vlag('--aantal', String(hosts.length)))
156
+ const teMeten = hosts.slice(0, aantal)
157
+ if (teMeten.length === 0) {
158
+ console.error('geen bruikbare urls, dus NIETS gemeten. Dit is geen uitslag van nul fouten.')
159
+ process.exit(2)
160
+ }
161
+
162
+ console.log('urls in de lijst ' + lijst.length + ', unieke hosts ' + hosts.length + ', gemeten ' + teMeten.length)
163
+ const u = await benchmark(teMeten)
164
+ console.log('')
165
+ console.log(' haalt trede 0 : ' + u.haaltL0)
166
+ console.log(' waarvan alleen door de methode-herhaling : ' + u.gereddenDoorHerhaling)
167
+ console.log(' RESTFOUT, afgekeurd maar antwoordt 402 : ' + u.restfout)
168
+ console.log(' terecht afgekeurd : ' + u.terechtAfgekeurd)
169
+ console.log(' niet bereikbaar, dus ongemeten : ' + u.ongemeten)
170
+ console.log('')
171
+ console.log(' van de ' + u.vraagtEchtOmBetaling + ' hosts die echt om betaling vragen:')
172
+ console.log(' foutmarge NU : ' + u.restfoutPct + ' procent')
173
+ console.log(' foutmarge ZONDER de herhaling: ' + u.zonderHerhalingFoutPct + ' procent')
174
+ if (u.rest.length) {
175
+ console.log('')
176
+ console.log(' nog steeds fout, en dit is het werk dat overblijft:')
177
+ for (const r of u.rest.slice(0, 12)) console.log(' ' + r.url + ' ' + r.bewijs)
178
+ }
179
+
180
+ const uit = vlag('--uit')
181
+ if (uit) {
182
+ const regel = {
183
+ gemeten_op: new Date().toISOString(),
184
+ gemeten: u.gemeten,
185
+ haalt_l0: u.haaltL0,
186
+ gered_door_herhaling: u.gereddenDoorHerhaling,
187
+ restfout: u.restfout,
188
+ terecht_afgekeurd: u.terechtAfgekeurd,
189
+ ongemeten: u.ongemeten,
190
+ vraagt_echt_om_betaling: u.vraagtEchtOmBetaling,
191
+ restfout_pct: u.restfoutPct,
192
+ zonder_herhaling_fout_pct: u.zonderHerhalingFoutPct,
193
+ rest: u.rest.slice(0, 10).map((r) => r.url),
194
+ }
195
+ fs.appendFileSync(uit, JSON.stringify(regel) + '\n')
196
+ console.log('')
197
+ console.log('regel bijgeschreven in ' + uit)
198
+ }
199
+
200
+ // Een restfout boven nul is een echte, open fout van deze meter. Die hoort een
201
+ // exitcode te geven, anders leest een groene run als "alles klopt" terwijl er hosts
202
+ // afgekeurd worden die wel om betaling vragen.
203
+ if (u.restfout > 0) process.exitCode = 1
204
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-receipt-verify",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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 && node test/naar-kanon-vorm.mjs && node test/verify-delivery.mjs && node test/acceptance-binding.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 && node test/acceptance-binding.mjs && node test/methodefout-bench.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",
@@ -22,7 +22,8 @@
22
22
  "bench-eip3009": "node bench/eip3009-beleid.mjs",
23
23
  "link-vectors": "node src/settlement-link-vectors.mjs",
24
24
  "verify-delivery": "node src/verify-delivery.mjs",
25
- "kanon-vorm": "node tools/naar-kanon-vorm.mjs"
25
+ "kanon-vorm": "node tools/naar-kanon-vorm.mjs",
26
+ "bench-methodefout": "node bench/methodefout.mjs"
26
27
  },
27
28
  "optionalDependencies": {
28
29
  "ethers": "^6.17.0"
@@ -90,7 +90,15 @@ export async function meetEindpunt(url, { fetchImpl = fetch, methode = 'POST', l
90
90
  // De herhaling blijft bewust smal: alleen 404 en 405, alleen als wij NIET met GET
91
91
  // sondeerden, en precies een keer. Elke 404 blind herhalen zou het verkeer naar elk
92
92
  // dood adres verdubbelen, en dat is niet aan ons om te doen.
93
- if (resp.status === 405 || resp.status === 404) {
93
+ // GEMETEN, EN DAARNA VERBREED. Na de 404 en 405 bleef er een restfout van 3,4 procent
94
+ // staan op zestig echte hosts, en die bestond uit twee andere codes die hetzelfde
95
+ // zeggen: cheaptokens.ai/api/buy gaf 400 op onze POST met een leeg lichaam en 402 op
96
+ // GET, en www.getbags.app/api/v1 gaf 406 op onze Accept-kop en 402 op GET. Alle vier
97
+ // de codes melden iets over ONS VERZOEK en niets over de bron, dus alle vier verdienen
98
+ // precies een tweede poging. De verzameling staat hier met naam zodat een vijfde code
99
+ // een gemeten beslissing vergt en niet stil meegroeit.
100
+ const VRAAG_OPNIEUW_BIJ = new Set([400, 404, 405, 406])
101
+ if (VRAAG_OPNIEUW_BIJ.has(resp.status)) {
94
102
  const allow = resp.headers.get('allow') || resp.headers.get('Allow') || ''
95
103
  const genoemd = allow
96
104
  .split(',')