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.
- package/bench/methodefout.mjs +204 -0
- package/package.json +4 -3
- package/src/x402-conformance-check.mjs +9 -1
|
@@ -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.
|
|
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
|
-
|
|
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(',')
|