agent-receipt-verify 0.2.3 → 0.2.4
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 +38 -3
- package/bin/cli.mjs +10 -0
- package/package.json +5 -3
- package/src/settlement-link-vectors.mjs +124 -11
- package/src/verify-delivery.mjs +507 -0
- package/src/x402-discovery-census.mjs +24 -1
- package/src/x402-offer-receipt.mjs +49 -3
- package/tools/naar-kanon-vorm.mjs +172 -0
- package/tools/pack-inhoud.mjs +45 -1
- package/vectors/settlement-link-kanon/README.md +69 -0
- package/vectors/settlement-link-kanon/find-candidates-onchain.json +43 -0
- package/vectors/settlement-link-kanon/no-candidates-not-measured.json +42 -0
- package/vectors/settlement-link-kanon/opens-live-2.json +47 -0
- package/vectors/settlement-link-kanon/opens-live-3.json +47 -0
- package/vectors/settlement-link-kanon/opens-live.json +47 -0
- package/vectors/settlement-link-kanon/two-identical-payments.json +76 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live-2.json +46 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live-3.json +46 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live-2.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live-3.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live.json +46 -0
package/README.md
CHANGED
|
@@ -77,11 +77,23 @@ vectors, that is interop. One implementation agreeing with itself proves nothing
|
|
|
77
77
|
| x402 settlement link (§5.6.4, same proposal) | own tests plus three live settlements | 22 offline, 3 live | yes, each resolves to its own settlement |
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
|
-
npm test # all of the above, no
|
|
80
|
+
npm test # all of the above. No account, no key, and nobody else's server
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Node built-ins only, apart from keccak256.
|
|
84
|
-
|
|
83
|
+
Node built-ins only, apart from keccak256. Two honest footnotes, because "no network"
|
|
84
|
+
was not quite true and is the kind of claim this package should not make loosely:
|
|
85
|
+
|
|
86
|
+
- One case dials a closed port on localhost on purpose, to prove that a dead address
|
|
87
|
+
reports not-measured instead of crashing the tool. That is a socket, but it leaves
|
|
88
|
+
the machine alone and reaches no third party.
|
|
89
|
+
- Until recently the suite also fetched a live discovery index belonging to someone
|
|
90
|
+
else, roughly twenty thousand entries, because one test handed an unknown flag to
|
|
91
|
+
`census` and `census` ignored unknown flags and went to work. That flag is now
|
|
92
|
+
refused, so the suite finishes in about eleven seconds and contacts nobody. A single
|
|
93
|
+
index round alone used to take over thirty.
|
|
94
|
+
|
|
95
|
+
Only the settlement link below reaches a real network, and then only a public,
|
|
96
|
+
read-only RPC.
|
|
85
97
|
|
|
86
98
|
Confirmed from the other side, which matters more than the table: the author of
|
|
87
99
|
the x402 specification ran their own implementation, which shares no code with
|
|
@@ -98,6 +110,7 @@ taken on trust:
|
|
|
98
110
|
```sh
|
|
99
111
|
npx -y agent-receipt-verify kanon
|
|
100
112
|
npx -y agent-receipt-verify beleid
|
|
113
|
+
npx -y agent-receipt-verify link-vectors
|
|
101
114
|
```
|
|
102
115
|
|
|
103
116
|
The first checks our EIP-3009 verifier against the ten published vectors of
|
|
@@ -107,6 +120,28 @@ about one nonce reused across two payers. Those are the two open questions in
|
|
|
107
120
|
[x402#2725](https://github.com/x402-foundation/x402/issues/2725), and the command
|
|
108
121
|
prints exactly where the two possible answers diverge and where they do not.
|
|
109
122
|
|
|
123
|
+
The third runs twelve vectors for the half AFTER paying: does this receipt belong to
|
|
124
|
+
that one settlement. Eleven of them need no network at all; the twelfth reads the chain
|
|
125
|
+
and is marked and skipped rather than reported as a failure, because a check that could
|
|
126
|
+
not run says nothing about the receipt. The same twelve also exist in the field layout
|
|
127
|
+
of the Kanon vectors, in `vectors/settlement-link-kanon`, so the two corpora can sit
|
|
128
|
+
side by side.
|
|
129
|
+
|
|
130
|
+
Two more commands, both of which measure other people's endpoints rather than our own,
|
|
131
|
+
and both read-only:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
npx -y agent-receipt-verify census [out.json]
|
|
135
|
+
npx -y agent-receipt-verify liveness [out.json]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`census` counts a discovery index per HOST rather than per resource, so a party with
|
|
139
|
+
three hundred paths does not outweigh three hundred parties with one. `liveness` takes
|
|
140
|
+
that census and asks each host whether it still answers 402 today, because a
|
|
141
|
+
registration in an index is not a promise that the endpoint is alive. Both are slow by
|
|
142
|
+
design: they pause between requests, because the hosts in that list did not ask to be
|
|
143
|
+
measured.
|
|
144
|
+
|
|
110
145
|
## Linking a receipt to its settlement
|
|
111
146
|
|
|
112
147
|
A receipt with an empty `transaction` field can be matched to a payment only by
|
package/bin/cli.mjs
CHANGED
|
@@ -58,6 +58,15 @@ function gebruik() {
|
|
|
58
58
|
console.log(' published vector covers: overpayment, and one nonce across two payers.')
|
|
59
59
|
console.log(' Those two are the open questions in x402 issue 2725.')
|
|
60
60
|
console.log('')
|
|
61
|
+
console.log(' npx github:holistis/agent-receipt-verify liveness [uit.json]')
|
|
62
|
+
console.log(' takes the census and asks each host whether it still answers 402 today.')
|
|
63
|
+
console.log(' A registration in an index is not a promise that the endpoint is alive.')
|
|
64
|
+
console.log('')
|
|
65
|
+
console.log(' npx github:holistis/agent-receipt-verify verify-delivery --answer=<file> --endpoint=<url>')
|
|
66
|
+
console.log(' reads, at the source, whether a paid answer is TRUE. The location is')
|
|
67
|
+
console.log(' DERIVED from --endpoint and never taken from the answer, because a seller')
|
|
68
|
+
console.log(' who picks both the token and the place can stage any outcome.')
|
|
69
|
+
console.log('')
|
|
61
70
|
console.log(' npx github:holistis/agent-receipt-verify link-vectors')
|
|
62
71
|
console.log(' twelve vectors for the half AFTER paying: does this receipt belong to')
|
|
63
72
|
console.log(' that one settlement? Eleven run offline, the chain-reading one is')
|
|
@@ -76,6 +85,7 @@ const onderdelen = {
|
|
|
76
85
|
kanon: '../src/eip3009-autorisatie.mjs',
|
|
77
86
|
beleid: '../bench/eip3009-beleid.mjs',
|
|
78
87
|
'link-vectors': '../src/settlement-link-vectors.mjs',
|
|
88
|
+
'verify-delivery': '../src/verify-delivery.mjs',
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
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.
|
|
3
|
+
"version": "0.2.4",
|
|
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",
|
|
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",
|
|
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",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"bundel": "node tools/bundel-bouw.mjs build/bundle",
|
|
21
21
|
"kanon": "node src/eip3009-autorisatie.mjs --vectoren",
|
|
22
22
|
"bench-eip3009": "node bench/eip3009-beleid.mjs",
|
|
23
|
-
"link-vectors": "node src/settlement-link-vectors.mjs"
|
|
23
|
+
"link-vectors": "node src/settlement-link-vectors.mjs",
|
|
24
|
+
"verify-delivery": "node src/verify-delivery.mjs",
|
|
25
|
+
"kanon-vorm": "node tools/naar-kanon-vorm.mjs"
|
|
24
26
|
},
|
|
25
27
|
"optionalDependencies": {
|
|
26
28
|
"ethers": "^6.17.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
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
124
|
-
console.log('
|
|
125
|
-
console.log('
|
|
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
|
}
|