agent-receipt-verify 0.2.3 → 0.2.5
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 +41 -24
- package/package.json +5 -3
- package/src/acceptance-binding.mjs +309 -0
- package/src/settlement-link-vectors.mjs +124 -11
- package/src/verify-delivery.mjs +507 -0
- package/src/x402-conformance-check.mjs +41 -5
- 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/acceptance-binding/crossed-deliverable.json +40 -0
- package/vectors/acceptance-binding/pairs-same-deliverable.json +39 -0
- package/vectors/acceptance-binding/real-record-as-issued.json +68 -0
- package/vectors/acceptance-binding/real-record-crossed-deliverable.json +65 -0
- package/vectors/acceptance-binding/real-record-crossed-x402-keys.json +74 -0
- package/vectors/acceptance-binding/real-record-with-producer-key.json +66 -0
- package/vectors/acceptance-binding/real-record-x402-keys-match.json +76 -0
- 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
|
@@ -16,21 +16,51 @@ import { createHash } from 'node:crypto'
|
|
|
16
16
|
const TAG = Buffer.from('x402-receipt-authorization/0', 'ascii')
|
|
17
17
|
const NUL = Buffer.from([0])
|
|
18
18
|
|
|
19
|
+
// De lengte is VERPLICHT, niet optioneel. Eerder was `verwacht === undefined` stil
|
|
20
|
+
// toegestaan, en dan deed deze functie niets anders dan hex naar bytes omzetten. Dat
|
|
21
|
+
// was precies het gat dat hieronder beschreven staat: zodra `lengtes` per ongeluk geen
|
|
22
|
+
// object was, viel elke lengtecontrole weg zonder dat er iets te zien was.
|
|
19
23
|
const bytes = (h, naam, verwacht) => {
|
|
24
|
+
if (!Number.isInteger(verwacht) || verwacht <= 0) {
|
|
25
|
+
throw new Error(`geen lengte-eis bekend voor ${naam}, dus dit veld wordt niet geaccepteerd`)
|
|
26
|
+
}
|
|
20
27
|
const b = Buffer.from(String(h ?? '').replace(/^0x/, ''), 'hex')
|
|
21
|
-
if (
|
|
28
|
+
if (b.length !== verwacht) {
|
|
22
29
|
throw new Error(`${naam} moet ${verwacht} bytes zijn, kreeg ${b.length}`)
|
|
23
30
|
}
|
|
24
31
|
return b
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
// De bekende methoden, met de lengte-eis per veld.
|
|
35
|
+
//
|
|
36
|
+
// WAAROM DIT EEN APARTE TABEL IS EN MET hasOwn WORDT OPGEZOCHT. Dit stond als
|
|
37
|
+
// `{ eip3009: {...} }[method]` in de functie zelf, en dat is een opzoeking die ook de
|
|
38
|
+
// PROTOTYPEKETEN raakt. Zelf gemeten op de gepubliceerde versie: `constructor`,
|
|
39
|
+
// `toString`, `valueOf`, `hasOwnProperty` en `__proto__` kwamen alle vijf door de
|
|
40
|
+
// `!lengtes`-poort, want ze geven een functie terug en een functie is waar. Daarna was
|
|
41
|
+
// `lengtes.asset` undefined en viel elke lengtecontrole weg, en dan zijn de velden
|
|
42
|
+
// zonder scheiding aan elkaar geplakt. De botsing is echt en narekenbaar:
|
|
43
|
+
//
|
|
44
|
+
// method constructor, asset 0xaabb, authorizer 0xcc
|
|
45
|
+
// method constructor, asset 0xaa, authorizer 0xbbcc
|
|
46
|
+
// geven BEIDE 0xd7f3867bb7a62bc13ba3e5e332c2109f3dc397d94c2f0d8c1238b8d87faa3836
|
|
47
|
+
//
|
|
48
|
+
// Twee verschillende afrekeningen die hetzelfde commitment opleveren is exact wat een
|
|
49
|
+
// commitment moet uitsluiten, dus dit is geen schoonheidsfoutje.
|
|
50
|
+
const LENGTES = { eip3009: { asset: 20, authorizer: 20 } }
|
|
51
|
+
|
|
52
|
+
/** Is dit een methode die wij echt kennen, en niet iets uit de prototypeketen? */
|
|
53
|
+
export function kentMethode(method) {
|
|
54
|
+
return typeof method === 'string' && Object.hasOwn(LENGTES, method)
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
// §5.6.2. Elk veld na `method` heeft een vaste lengte, dus het voorbeeld is
|
|
28
58
|
// ondubbelzinnig. Lengtes worden hier afgedwongen: een veld van de verkeerde lengte
|
|
29
59
|
// zou stilzwijgend een ander commitment opleveren dat nergens op opent.
|
|
30
60
|
export function berekenCommitment({ method, asset, authorizer, identifier, salt }) {
|
|
31
61
|
if (!method) throw new Error('method ontbreekt')
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
if (!kentMethode(method)) throw new Error(`onbekende authorizationMethod: ${method}`)
|
|
63
|
+
const lengtes = LENGTES[method]
|
|
34
64
|
|
|
35
65
|
const pre = Buffer.concat([
|
|
36
66
|
TAG, NUL,
|
|
@@ -52,9 +82,25 @@ export function opentCommitment({ receipt, salt, asset, kandidaatIdentifiers })
|
|
|
52
82
|
const commitment = payload?.authorizationCommitment
|
|
53
83
|
if (!method || !commitment) return { linked: false, reden: 'receipt carries no commitment' }
|
|
54
84
|
|
|
85
|
+
// De vaste velden EERST, buiten de lus. Anders kan een onbruikbaar zout of een
|
|
86
|
+
// onbekende methode nooit zichtbaar worden: elke ronde gooit dan dezelfde fout, de
|
|
87
|
+
// catch slikt hem op, en de uitslag wordt "geen kandidaat opent dit" terwijl de
|
|
88
|
+
// waarheid is dat er niets te berekenen viel. Dat is het verschil tussen een
|
|
89
|
+
// afkeuring en een onvolledige meting, en dat verschil mag nooit wegvallen.
|
|
90
|
+
if (!kentMethode(method)) {
|
|
91
|
+
return { linked: false, meetbaar: false, reden: `unknown authorizationMethod: ${method}` }
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
berekenCommitment({ method, asset, authorizer: payload.payer, identifier: '0x' + '00'.repeat(32), salt })
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return { linked: false, meetbaar: false, reden: 'cannot compute a commitment here: ' + e.message }
|
|
97
|
+
}
|
|
98
|
+
|
|
55
99
|
const treffers = []
|
|
56
100
|
for (const identifier of kandidaatIdentifiers ?? []) {
|
|
57
101
|
let berekend
|
|
102
|
+
// Hier kan nog maar een ding misgaan: deze ene identifier heeft de verkeerde
|
|
103
|
+
// lengte. Dat is dan geen treffer, en dat is de juiste uitkomst.
|
|
58
104
|
try { berekend = berekenCommitment({ method, asset, authorizer: payload.payer, identifier, salt }) } catch { continue }
|
|
59
105
|
if (berekend.toLowerCase() === String(commitment).toLowerCase()) treffers.push(identifier)
|
|
60
106
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Zet onze afrekening-vectoren om naar de vorm van de kanon-vectoren.
|
|
3
|
+
//
|
|
4
|
+
// WAAROM DIT BESTAAT, en het is een verzoek en geen eigen inval. De auteur van de
|
|
5
|
+
// kanon-vectoren schreef op 2026-10-17 in x402#3396: "if you want receipt-to-settlement
|
|
6
|
+
// vectors in Kanon's shape, I'm open to that, with the part that needs a chain read
|
|
7
|
+
// marked as such." Dit is dat, met die markering erin.
|
|
8
|
+
//
|
|
9
|
+
// WAT HIER WEL EN NIET VERTAALD KAN WORDEN, en dat tweede is het interessante deel.
|
|
10
|
+
// Zijn `expected` is `{ valid: boolean, reason_code: string }`. Twee van onze vier
|
|
11
|
+
// uitkomsten passen daar precies in. De andere twee niet, en dat is geen slordigheid
|
|
12
|
+
// van hem maar het gat dat hij in dezelfde reactie zelf benoemde en zei te willen
|
|
13
|
+
// dichten. Daarom laat dit script die twee gevallen ZICHTBAAR open staan in plaats van
|
|
14
|
+
// ze in `valid: false` te duwen:
|
|
15
|
+
//
|
|
16
|
+
// LINKED -> valid true, reason_code LINKED
|
|
17
|
+
// NOT_LINKED -> valid false, reason_code NOT_LINKED
|
|
18
|
+
// AMBIGUOUS -> valid false, reason_code AMBIGUOUS
|
|
19
|
+
// NOT_MEASURED -> GEEN valid. `measurable: false` plus een reason_code.
|
|
20
|
+
//
|
|
21
|
+
// Die laatste regel is het hele punt van de omzetting. Een bonnetje waarvan de
|
|
22
|
+
// kandidatenlijst niet opgehaald kon worden is NIET afgekeurd, en het in `valid: false`
|
|
23
|
+
// schrijven zou een eerlijke verkoper beschuldigen omdat onze RPC haperde. Liever een
|
|
24
|
+
// veld dat zijn validator nog niet kent dan een onwaarheid die hij wel kan lezen.
|
|
25
|
+
//
|
|
26
|
+
// EN DE GRENS VAN DEZE VERTALING, want die hoort erbij. Zijn corpus dekt de autorisatie
|
|
27
|
+
// van de KOPER voor de afrekening: geldigheidsvenster, malleability, domein-replay,
|
|
28
|
+
// nonce-hergebruik, bedrag. Dit corpus dekt de andere kant: opent het gezouten
|
|
29
|
+
// commitment in een ONDERTEKEND bonnetje op precies een afrekening op de keten. Die
|
|
30
|
+
// twee overlappen nergens, dus dit is een aanvulling en geen samenvoeging.
|
|
31
|
+
//
|
|
32
|
+
// Gebruik:
|
|
33
|
+
// node tools/naar-kanon-vorm.mjs schrijft naar vectors/settlement-link-kanon
|
|
34
|
+
// node tools/naar-kanon-vorm.mjs --controleer schrijft niets, meldt alleen afwijkingen
|
|
35
|
+
|
|
36
|
+
import fs from 'node:fs'
|
|
37
|
+
import path from 'node:path'
|
|
38
|
+
import { fileURLToPath } from 'node:url'
|
|
39
|
+
|
|
40
|
+
const WORTEL = fileURLToPath(new URL('..', import.meta.url))
|
|
41
|
+
export const BRONMAP = path.join(WORTEL, 'vectors', 'settlement-link')
|
|
42
|
+
export const DOELMAP = path.join(WORTEL, 'vectors', 'settlement-link-kanon')
|
|
43
|
+
|
|
44
|
+
/** Hun schema-versie, zoals die in elk van hun eigen vectoren staat. */
|
|
45
|
+
export const KANON_SCHEMA_VERSIE = '3.0.0'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* De vertaaltabel van onze uitkomst naar hun expected-blok.
|
|
49
|
+
*
|
|
50
|
+
* `valid` wordt met opzet WEGGELATEN bij NOT_MEASURED, niet op false gezet. Een
|
|
51
|
+
* ontbrekend veld is voor een lezer een vraag; een false is een beschuldiging.
|
|
52
|
+
*/
|
|
53
|
+
export function naarExpected(onze) {
|
|
54
|
+
const o = onze?.outcome
|
|
55
|
+
if (o === 'LINKED') {
|
|
56
|
+
return { valid: true, reason_code: 'LINKED' }
|
|
57
|
+
}
|
|
58
|
+
if (o === 'NOT_LINKED') {
|
|
59
|
+
return { valid: false, reason_code: 'NOT_LINKED' }
|
|
60
|
+
}
|
|
61
|
+
if (o === 'AMBIGUOUS') {
|
|
62
|
+
return { valid: false, reason_code: 'AMBIGUOUS' }
|
|
63
|
+
}
|
|
64
|
+
if (o === 'NOT_MEASURED') {
|
|
65
|
+
return {
|
|
66
|
+
measurable: false,
|
|
67
|
+
reason_code: 'NOT_MEASURED',
|
|
68
|
+
note:
|
|
69
|
+
'Deliberately carries no `valid`. The candidate set could not be obtained, so this says ' +
|
|
70
|
+
'nothing about the receipt. Writing valid:false here would blame an honest seller for a ' +
|
|
71
|
+
'failed chain read. This is the third-outcome gap discussed in x402#3396.',
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Zet een van onze vectoren om. Geeft null terug als de uitkomst niet te vertalen is. */
|
|
78
|
+
export function zetOm(v) {
|
|
79
|
+
const expected = naarExpected(v.expected)
|
|
80
|
+
if (!expected) return null
|
|
81
|
+
const uit = {
|
|
82
|
+
id: v.id,
|
|
83
|
+
schema_version: KANON_SCHEMA_VERSIE,
|
|
84
|
+
protocol: v.protocol ?? 'x402',
|
|
85
|
+
scheme: v.scheme ?? 'exact',
|
|
86
|
+
network: v.network,
|
|
87
|
+
asset_transfer_method: v.asset_transfer_method ?? 'eip3009',
|
|
88
|
+
// Een eigen veld, en het staat er omdat hun corpus deze as niet heeft. Hun vectoren
|
|
89
|
+
// gaan allemaal over de autorisatie VOOR de afrekening; deze gaan over het bonnetje
|
|
90
|
+
// NA de afrekening. Zonder dit veld zou een lezer de twee verzamelingen door elkaar
|
|
91
|
+
// halen zodra ze in een map staan.
|
|
92
|
+
verifies: 'receipt-to-settlement',
|
|
93
|
+
encodes: v.encodes,
|
|
94
|
+
provenance: v.provenance,
|
|
95
|
+
description: v.description,
|
|
96
|
+
// DE MARKERING DIE ERO M GEVRAAGD WERD. Hun schema heeft dit veld niet, dus het is
|
|
97
|
+
// additief: een loper die het niet kent negeert het en draait de vector gewoon, een
|
|
98
|
+
// loper die het wel kent weet dat er een RPC nodig is.
|
|
99
|
+
...(v.requires_chain_read ? { requires_chain_read: true } : {}),
|
|
100
|
+
input: v.input,
|
|
101
|
+
...(v.context ? { context: v.context } : {}),
|
|
102
|
+
expected,
|
|
103
|
+
}
|
|
104
|
+
return uit
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function laadBron(map = BRONMAP) {
|
|
108
|
+
if (!fs.existsSync(map)) return []
|
|
109
|
+
return fs.readdirSync(map).filter((f) => f.endsWith('.json')).sort()
|
|
110
|
+
.map((f) => ({ bestand: f, inhoud: JSON.parse(fs.readFileSync(path.join(map, f), 'utf8')) }))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ditBestand = fileURLToPath(import.meta.url)
|
|
114
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(ditBestand)) {
|
|
115
|
+
const alleenControleren = process.argv.includes('--controleer')
|
|
116
|
+
const bron = laadBron()
|
|
117
|
+
if (!bron.length) {
|
|
118
|
+
console.error('no vectors found in vectors/settlement-link')
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!alleenControleren) fs.mkdirSync(DOELMAP, { recursive: true })
|
|
123
|
+
|
|
124
|
+
let geschreven = 0
|
|
125
|
+
let ongewijzigd = 0
|
|
126
|
+
let afwijkend = 0
|
|
127
|
+
let nietTeVertalen = 0
|
|
128
|
+
const telling = {}
|
|
129
|
+
|
|
130
|
+
for (const { bestand, inhoud } of bron) {
|
|
131
|
+
const om = zetOm(inhoud)
|
|
132
|
+
if (!om) {
|
|
133
|
+
nietTeVertalen++
|
|
134
|
+
console.log(' SKIP ' + bestand + ' (outcome ' + String(inhoud?.expected?.outcome) + ' has no translation)')
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
const code = om.expected.reason_code
|
|
138
|
+
telling[code] = (telling[code] ?? 0) + 1
|
|
139
|
+
const doel = path.join(DOELMAP, bestand)
|
|
140
|
+
const tekst = JSON.stringify(om, null, 2) + '\n'
|
|
141
|
+
if (alleenControleren) {
|
|
142
|
+
const bestaat = fs.existsSync(doel) ? fs.readFileSync(doel, 'utf8') : null
|
|
143
|
+
if (bestaat === tekst) ongewijzigd++
|
|
144
|
+
else { afwijkend++; console.log(' DIFFERS ' + bestand + (bestaat === null ? ' (missing)' : '')) }
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
fs.writeFileSync(doel, tekst)
|
|
148
|
+
geschreven++
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('')
|
|
152
|
+
if (alleenControleren) {
|
|
153
|
+
console.log(ongewijzigd + ' up to date, ' + afwijkend + ' differ, ' + nietTeVertalen + ' untranslatable')
|
|
154
|
+
process.exitCode = afwijkend === 0 ? 0 : 1
|
|
155
|
+
} else {
|
|
156
|
+
console.log(geschreven + ' vectors written to vectors/settlement-link-kanon')
|
|
157
|
+
}
|
|
158
|
+
console.log('')
|
|
159
|
+
console.log('reason codes used:')
|
|
160
|
+
for (const [k, v] of Object.entries(telling).sort((a, b) => b[1] - a[1])) {
|
|
161
|
+
console.log(' ' + String(v).padStart(3) + ' ' + k)
|
|
162
|
+
}
|
|
163
|
+
const ketenLees = bron.filter((b) => b.inhoud.requires_chain_read).length
|
|
164
|
+
console.log('')
|
|
165
|
+
console.log(ketenLees + ' of ' + bron.length + ' carry requires_chain_read:true, so a runner knows to expect an RPC.')
|
|
166
|
+
console.log('')
|
|
167
|
+
console.log('One honest note about the translation, and it is the reason this exists:')
|
|
168
|
+
console.log('Kanon expected is valid plus a reason_code. LINKED, NOT_LINKED and AMBIGUOUS map')
|
|
169
|
+
console.log('onto that cleanly. NOT_MEASURED does not, so those files carry measurable:false')
|
|
170
|
+
console.log('and no valid at all, rather than a valid:false that would read as a verdict about')
|
|
171
|
+
console.log('a seller when the truth is that our own chain read did not happen.')
|
|
172
|
+
}
|
package/tools/pack-inhoud.mjs
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// Exitcode 0 als er een lijst is, 1 als de inhoud niet te meten was.
|
|
29
29
|
|
|
30
30
|
import { execSync } from 'node:child_process'
|
|
31
|
+
import { readFileSync } from 'node:fs'
|
|
31
32
|
import path from 'node:path'
|
|
32
33
|
import { fileURLToPath } from 'node:url'
|
|
33
34
|
|
|
@@ -47,7 +48,13 @@ export function lijstUitPackUitvoer(ruw) {
|
|
|
47
48
|
throw new Error('npm pack gaf geen json terug: ' + String(e.message).slice(0, 80))
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// EEN LEGE LIJST IS GEEN LIJST. `Array.isArray([])` is waar en `[]` is in javascript
|
|
52
|
+
// ook waar, dus een entry met `files: []` kwam hier als geldige uitkomst door en gaf
|
|
53
|
+
// een lege bestandslijst terug. Dat is precies wat de koptekst hierboven belooft NIET
|
|
54
|
+
// te doen, en het is ook de storing die deze file se eigen geschiedenis beschrijft:
|
|
55
|
+
// twaalf controles die verdwenen terwijl de suite groen bleef. Een tarball met nul
|
|
56
|
+
// bestanden bestaat niet, dus dit is altijd een meetfout en nooit een uitkomst.
|
|
57
|
+
const uitEntry = (e) => (e && Array.isArray(e.files) && e.files.length ? e.files.map((f) => f.path) : null)
|
|
51
58
|
|
|
52
59
|
// Vorm 1: een array van entries (npm 11.11.0 en ouder).
|
|
53
60
|
if (Array.isArray(j)) {
|
|
@@ -83,6 +90,43 @@ export function pakInhoud({ cwd = WORTEL, draai = null } = {}) {
|
|
|
83
90
|
return lijstUitPackUitvoer(uit)
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wat de tarball MOET dragen, afgeleid uit de verzendtabel van de cli zelf.
|
|
96
|
+
*
|
|
97
|
+
* WAAROM AFGELEID EN NIET OPGESCHREVEN. De publicatiestap heette "de tarball moet NOTICE,
|
|
98
|
+
* LICENSE en alles waar de cli naar verwijst dragen" en toetste vijf vaste paden, terwijl
|
|
99
|
+
* de verzendtabel negen modules heeft. Zeven subcommando's werden dus nooit gecontroleerd.
|
|
100
|
+
* Zou `files` in package.json ooit `src/` kwijtraken, dan ging er een pakket uit waarvan
|
|
101
|
+
* elk subcommando bij de eerste aanroep crasht, en deze poort zou groen staan. Een lijst
|
|
102
|
+
* die met de hand wordt bijgehouden loopt per definitie achter op de tabel die hij
|
|
103
|
+
* bewaakt; deze leest die tabel.
|
|
104
|
+
*
|
|
105
|
+
* De drie vaste regels eronder staan er wel met de hand, en met opzet: NOTICE en LICENSE
|
|
106
|
+
* zijn een LICENTIEPLICHT (wij verspreiden testvectoren van derden onder Apache-2.0) en
|
|
107
|
+
* volgen uit geen enkele tabel. Het profielbestand staat erbij omdat de ladder het leest
|
|
108
|
+
* zonder dat een importregel dat verraadt.
|
|
109
|
+
*/
|
|
110
|
+
export function verwachteBestanden({ cliPad = null } = {}) {
|
|
111
|
+
const pad = cliPad ?? path.join(WORTEL, 'bin', 'cli.mjs')
|
|
112
|
+
const bron = readFileSync(pad, 'utf8')
|
|
113
|
+
const tabel = bron.match(/const onderdelen = \{([^}]*)\}/)
|
|
114
|
+
if (!tabel) {
|
|
115
|
+
throw new Error('de verzendtabel is niet te vinden in ' + pad + ', dus de verwachte inhoud is ONGEMETEN')
|
|
116
|
+
}
|
|
117
|
+
const modules = [...tabel[1].matchAll(/'([^']+\.mjs)'/g)].map((m) => m[1].replace(/^\.\.\//, ''))
|
|
118
|
+
if (!modules.length) {
|
|
119
|
+
throw new Error('de verzendtabel in ' + pad + ' bevat geen enkel modulepad, dus hier is iets stuk')
|
|
120
|
+
}
|
|
121
|
+
return [
|
|
122
|
+
'NOTICE',
|
|
123
|
+
'LICENSE',
|
|
124
|
+
'bin/cli.mjs',
|
|
125
|
+
'profiles/x402-deployed-ladder-v1.json',
|
|
126
|
+
...modules,
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
|
|
86
130
|
const ditBestand = fileURLToPath(import.meta.url)
|
|
87
131
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(ditBestand)) {
|
|
88
132
|
try {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "acceptance-binding-crossed-deliverable",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"verifies": "receipt-to-acceptance",
|
|
5
|
+
"encodes": "The case babyblueviper1 asked for: an acceptance record paired with a receipt for a different deliverable. It must not link.",
|
|
6
|
+
"provenance": [
|
|
7
|
+
"trilatic.acceptance-receipt/1.2, as cited in x402-foundation/tsc#4",
|
|
8
|
+
"requested in erc-8004/erc-8004-contracts#99 by babyblueviper1 on 18 October 2026: \"a vector in which the acceptance record names a different deliverable hash than the receipt it is paired with. It should not link.\""
|
|
9
|
+
],
|
|
10
|
+
"description": "Identical to the positive control except for one byte of the acceptance record's deliverable hash. Everything else still passes: the sidecar names the right receipt, the checker is independent, and the criteria were committed first. A verifier that reports LINKED here is reading the pair as a whole instead of checking that both halves name the same deliverable, which is the one thing this binding exists to prove.",
|
|
11
|
+
"requires_chain_read": false,
|
|
12
|
+
"input": {
|
|
13
|
+
"receipt": {
|
|
14
|
+
"format": "eip712",
|
|
15
|
+
"authorizationNonce": "0x43457c8e57b7c4ff92d716350c61c88c8f3b644d09d7c813d5a7ee6450a843da"
|
|
16
|
+
},
|
|
17
|
+
"receiptSha256": "9f2c1d4e8a7b6053fe41cc90b2d7a3418e5f6072c9b8d1a4e7f0352698cdba17",
|
|
18
|
+
"acceptance": {
|
|
19
|
+
"format": "trilatic.acceptance-receipt/1.2",
|
|
20
|
+
"deliverable_sha256": "d74ad7d7c41b8e5206f39ac718be40d5392fb6ca07e1d8359f4b26cd0a39e030",
|
|
21
|
+
"checker": "invinoveritas-checker-a4cecab2",
|
|
22
|
+
"producer": "tokenizen-seller-2888f365",
|
|
23
|
+
"criteria_committed_at": "2026-10-18T00:52:03Z",
|
|
24
|
+
"checked_at": "2026-10-18T00:52:09Z"
|
|
25
|
+
},
|
|
26
|
+
"bindings": {
|
|
27
|
+
"receipt_sha256": "9f2c1d4e8a7b6053fe41cc90b2d7a3418e5f6072c9b8d1a4e7f0352698cdba17",
|
|
28
|
+
"deliverable_sha256": "d74ad7d7c41b8e5206f39ac718be40d5392fb6ca07e1d8359f4b26cd0a39e02f",
|
|
29
|
+
"authorization": "0x43457c8e57b7c4ff92d716350c61c88c8f3b644d09d7c813d5a7ee6450a843da"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"expected": {
|
|
33
|
+
"valid": false,
|
|
34
|
+
"reason_code": "NOT_LINKED",
|
|
35
|
+
"detail": "DELIVERABLE_HASH_MISMATCH"
|
|
36
|
+
},
|
|
37
|
+
"limits": [
|
|
38
|
+
"This proves the pair does not hold together. It says nothing about which of the two artifacts is the wrong one."
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "acceptance-binding-pairs-same-deliverable",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"verifies": "receipt-to-acceptance",
|
|
5
|
+
"encodes": "The positive control. Without it, a set that only contains failures proves nothing about the harness.",
|
|
6
|
+
"provenance": [
|
|
7
|
+
"trilatic.acceptance-receipt/1.2, as cited in x402-foundation/tsc#4",
|
|
8
|
+
"proposed in erc-8004/erc-8004-contracts#99 by babyblueviper1 on 18 October 2026"
|
|
9
|
+
],
|
|
10
|
+
"description": "A signed bindings sidecar names one receipt and one deliverable hash, and the acceptance record names that same deliverable hash. The checker is not the producer, and the criteria were committed before the check. This must link.",
|
|
11
|
+
"requires_chain_read": false,
|
|
12
|
+
"input": {
|
|
13
|
+
"receipt": {
|
|
14
|
+
"format": "eip712",
|
|
15
|
+
"authorizationNonce": "0x43457c8e57b7c4ff92d716350c61c88c8f3b644d09d7c813d5a7ee6450a843da"
|
|
16
|
+
},
|
|
17
|
+
"receiptSha256": "9f2c1d4e8a7b6053fe41cc90b2d7a3418e5f6072c9b8d1a4e7f0352698cdba17",
|
|
18
|
+
"acceptance": {
|
|
19
|
+
"format": "trilatic.acceptance-receipt/1.2",
|
|
20
|
+
"deliverable_sha256": "d74ad7d7c41b8e5206f39ac718be40d5392fb6ca07e1d8359f4b26cd0a39e02f",
|
|
21
|
+
"checker": "invinoveritas-checker-a4cecab2",
|
|
22
|
+
"producer": "tokenizen-seller-2888f365",
|
|
23
|
+
"criteria_committed_at": "2026-10-18T00:52:03Z",
|
|
24
|
+
"checked_at": "2026-10-18T00:52:09Z"
|
|
25
|
+
},
|
|
26
|
+
"bindings": {
|
|
27
|
+
"receipt_sha256": "9f2c1d4e8a7b6053fe41cc90b2d7a3418e5f6072c9b8d1a4e7f0352698cdba17",
|
|
28
|
+
"deliverable_sha256": "d74ad7d7c41b8e5206f39ac718be40d5392fb6ca07e1d8359f4b26cd0a39e02f",
|
|
29
|
+
"authorization": "0x43457c8e57b7c4ff92d716350c61c88c8f3b644d09d7c813d5a7ee6450a843da"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"expected": {
|
|
33
|
+
"valid": true,
|
|
34
|
+
"reason_code": "LINKED"
|
|
35
|
+
},
|
|
36
|
+
"limits": [
|
|
37
|
+
"The signature on the acceptance record is not checked here, and neither are its criteria. That is the issuing checker's own verifier. A LINKED verdict says the two artifacts are about the same thing, not that the acceptance record itself is sound."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "3.0.0",
|
|
3
|
+
"verifies": "receipt-to-acceptance",
|
|
4
|
+
"requires_chain_read": false,
|
|
5
|
+
"provenance": [
|
|
6
|
+
"trilatic.acceptance-receipt/1.2, live record fetched from https://api.babyblueviper.com/work-acceptance/record/4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d on 18 October 2026",
|
|
7
|
+
"the two binding keys were specified by babyblueviper1 in erc-8004/erc-8004-contracts#99 on 18 October 2026",
|
|
8
|
+
"the record is issued by invinoveritas, not by us; it is copied here as test material"
|
|
9
|
+
],
|
|
10
|
+
"id": "acceptance-binding-real-record-as-issued",
|
|
11
|
+
"encodes": "A real, live, signed acceptance record whose independence cannot be verified by a third party, because the two parties are named in different namespaces while the issuer declares independence.",
|
|
12
|
+
"description": "This is the record exactly as the issuer serves it, with only the fields this layer reads. It says independence: \"independent\". The checker is identified by key_id (invinoveritas-checker-a4cecab2). The producer has key_id: null and is identified only by a display name (\"Example Vendor (self-test)\"). A key_id and a display name are two different kinds of name, so comparing them is not a comparison: a reader cannot establish that the checker is not the producer, in either direction. The honest outcome is NOT_MEASURED, not a pass and not a fail. A verifier that returns a pass here is reporting the issuer own claim back to the reader as if a third party had checked it, and a verifier that returns a fail is treating an unmeasured field as a refusal.",
|
|
13
|
+
"input": {
|
|
14
|
+
"doc": {
|
|
15
|
+
"record": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"schema": "trilatic.acceptance-receipt/1.2",
|
|
18
|
+
"receipt_id": "INV-WA-aa4b8bc740bc2524",
|
|
19
|
+
"issuer": "invinoveritas",
|
|
20
|
+
"issued_utc": "2026-10-18T00:52:09Z",
|
|
21
|
+
"criteria": {
|
|
22
|
+
"id": "invinoveritas-p1-selftest",
|
|
23
|
+
"sha256": "d74ad7d7492908f1146b233e89db6ca205065c67f02d33758ff0aa9f8b9e02fc",
|
|
24
|
+
"committed_at": "2026-10-18T00:52:03Z"
|
|
25
|
+
},
|
|
26
|
+
"files": {
|
|
27
|
+
"remediated": {
|
|
28
|
+
"name": "refund-policy.txt",
|
|
29
|
+
"bytes": 121,
|
|
30
|
+
"sha256": "9c27eee03d50f4ff00fb60a1551a03d7311dbed257b7b943b3ff5d13eb30c083"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"parties": {
|
|
34
|
+
"checker": {
|
|
35
|
+
"name": "invinoveritas",
|
|
36
|
+
"key_id": "invinoveritas-checker-a4cecab2"
|
|
37
|
+
},
|
|
38
|
+
"producer": {
|
|
39
|
+
"name": "Example Vendor (self-test)",
|
|
40
|
+
"key_id": null
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"independence": "independent",
|
|
44
|
+
"verdict": "pass"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"bindings_sidecar": {
|
|
48
|
+
"version": "invinoveritas.work-acceptance-bindings/v0",
|
|
49
|
+
"receipt_sha256": "4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d",
|
|
50
|
+
"criteria_id": "d74ad7d7492908f1146b233e89db6ca205065c67f02d33758ff0aa9f8b9e02fc",
|
|
51
|
+
"bindings": {}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"receiptSha256": "4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d",
|
|
55
|
+
"deliverableSha256": "9c27eee03d50f4ff00fb60a1551a03d7311dbed257b7b943b3ff5d13eb30c083"
|
|
56
|
+
},
|
|
57
|
+
"expected": {
|
|
58
|
+
"valid": false,
|
|
59
|
+
"measurable": false,
|
|
60
|
+
"reason_code": "NOT_MEASURED",
|
|
61
|
+
"independence": "ZELFVERKLAARD_ONVERIFIEERBAAR"
|
|
62
|
+
},
|
|
63
|
+
"limits": [
|
|
64
|
+
"This says nothing about whether the check itself was good. It says the independence claim is not auditable from the record.",
|
|
65
|
+
"One field closes it: a key_id on the producer, in the same namespace as the checker. See real-record-with-producer-key.json for the same record with that field set.",
|
|
66
|
+
"Where a producer has no key at all, the remaining third-party handle on whether two parties are the same hand is the money, not the paperwork. That is a different measurement and it is not this one."
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "3.0.0",
|
|
3
|
+
"verifies": "receipt-to-acceptance",
|
|
4
|
+
"requires_chain_read": false,
|
|
5
|
+
"provenance": [
|
|
6
|
+
"trilatic.acceptance-receipt/1.2, live record fetched from https://api.babyblueviper.com/work-acceptance/record/4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d on 18 October 2026",
|
|
7
|
+
"the two binding keys were specified by babyblueviper1 in erc-8004/erc-8004-contracts#99 on 18 October 2026",
|
|
8
|
+
"the record is issued by invinoveritas, not by us; it is copied here as test material"
|
|
9
|
+
],
|
|
10
|
+
"id": "acceptance-binding-real-record-crossed-deliverable",
|
|
11
|
+
"encodes": "The case babyblueviper1 asked for, in the shape the issuer actually serves.",
|
|
12
|
+
"description": "Identical to real-record-with-producer-key except for one byte of the deliverable hash we hold. Everything else still passes: the sidecar names the right receipt, the checker is verifiably not the producer, and the criteria were committed first. A verifier that reports LINKED here is reading the pair as a whole instead of checking that both halves name the same deliverable, which is the one thing this binding exists to prove.",
|
|
13
|
+
"input": {
|
|
14
|
+
"doc": {
|
|
15
|
+
"record": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"schema": "trilatic.acceptance-receipt/1.2",
|
|
18
|
+
"receipt_id": "INV-WA-aa4b8bc740bc2524",
|
|
19
|
+
"issuer": "invinoveritas",
|
|
20
|
+
"issued_utc": "2026-10-18T00:52:09Z",
|
|
21
|
+
"criteria": {
|
|
22
|
+
"id": "invinoveritas-p1-selftest",
|
|
23
|
+
"sha256": "d74ad7d7492908f1146b233e89db6ca205065c67f02d33758ff0aa9f8b9e02fc",
|
|
24
|
+
"committed_at": "2026-10-18T00:52:03Z"
|
|
25
|
+
},
|
|
26
|
+
"files": {
|
|
27
|
+
"remediated": {
|
|
28
|
+
"name": "refund-policy.txt",
|
|
29
|
+
"bytes": 121,
|
|
30
|
+
"sha256": "9c27eee03d50f4ff00fb60a1551a03d7311dbed257b7b943b3ff5d13eb30c083"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"parties": {
|
|
34
|
+
"checker": {
|
|
35
|
+
"name": "invinoveritas",
|
|
36
|
+
"key_id": "invinoveritas-checker-a4cecab2"
|
|
37
|
+
},
|
|
38
|
+
"producer": {
|
|
39
|
+
"name": "Example Vendor (self-test)",
|
|
40
|
+
"key_id": "example-vendor-producer-7f31aa02"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"independence": "independent",
|
|
44
|
+
"verdict": "pass"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"bindings_sidecar": {
|
|
48
|
+
"version": "invinoveritas.work-acceptance-bindings/v0",
|
|
49
|
+
"receipt_sha256": "4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d",
|
|
50
|
+
"criteria_id": "d74ad7d7492908f1146b233e89db6ca205065c67f02d33758ff0aa9f8b9e02fc",
|
|
51
|
+
"bindings": {}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"receiptSha256": "4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d",
|
|
55
|
+
"deliverableSha256": "9c27eee03d50f4ff00fb60a1551a03d7311dbed257b7b943b3ff5d13eb30c084"
|
|
56
|
+
},
|
|
57
|
+
"expected": {
|
|
58
|
+
"valid": false,
|
|
59
|
+
"reason_code": "NOT_LINKED",
|
|
60
|
+
"detail": "DELIVERABLE_HASH_MISMATCH"
|
|
61
|
+
},
|
|
62
|
+
"limits": [
|
|
63
|
+
"This proves the pair does not hold together. It says nothing about which of the two artifacts is the wrong one."
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "3.0.0",
|
|
3
|
+
"verifies": "receipt-to-acceptance",
|
|
4
|
+
"requires_chain_read": false,
|
|
5
|
+
"provenance": [
|
|
6
|
+
"trilatic.acceptance-receipt/1.2, live record fetched from https://api.babyblueviper.com/work-acceptance/record/4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d on 18 October 2026",
|
|
7
|
+
"the two binding keys were specified by babyblueviper1 in erc-8004/erc-8004-contracts#99 on 18 October 2026",
|
|
8
|
+
"the record is issued by invinoveritas, not by us; it is copied here as test material"
|
|
9
|
+
],
|
|
10
|
+
"id": "acceptance-binding-real-record-crossed-x402-keys",
|
|
11
|
+
"encodes": "Why the format carries two separate keys: a salted commitment must never be compared against a raw nonce.",
|
|
12
|
+
"description": "The sidecar carries the commitment value in the nonce key and the nonce value in the commitment key. Both are present, both are well-formed 32-byte values, and both belong to the same real payment, so a verifier that treats the two keys as interchangeable, or that falls back to one generic authorization field, will report LINKED. It must not. This is the failure the two-key split was introduced to prevent, and it is the one case a single-field implementation cannot see.",
|
|
13
|
+
"input": {
|
|
14
|
+
"doc": {
|
|
15
|
+
"record": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"schema": "trilatic.acceptance-receipt/1.2",
|
|
18
|
+
"receipt_id": "INV-WA-aa4b8bc740bc2524",
|
|
19
|
+
"issuer": "invinoveritas",
|
|
20
|
+
"issued_utc": "2026-10-18T00:52:09Z",
|
|
21
|
+
"criteria": {
|
|
22
|
+
"id": "invinoveritas-p1-selftest",
|
|
23
|
+
"sha256": "d74ad7d7492908f1146b233e89db6ca205065c67f02d33758ff0aa9f8b9e02fc",
|
|
24
|
+
"committed_at": "2026-10-18T00:52:03Z"
|
|
25
|
+
},
|
|
26
|
+
"files": {
|
|
27
|
+
"remediated": {
|
|
28
|
+
"name": "refund-policy.txt",
|
|
29
|
+
"bytes": 121,
|
|
30
|
+
"sha256": "9c27eee03d50f4ff00fb60a1551a03d7311dbed257b7b943b3ff5d13eb30c083"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"parties": {
|
|
34
|
+
"checker": {
|
|
35
|
+
"name": "invinoveritas",
|
|
36
|
+
"key_id": "invinoveritas-checker-a4cecab2"
|
|
37
|
+
},
|
|
38
|
+
"producer": {
|
|
39
|
+
"name": "Example Vendor (self-test)",
|
|
40
|
+
"key_id": "example-vendor-producer-7f31aa02"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"independence": "independent",
|
|
44
|
+
"verdict": "pass"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"bindings_sidecar": {
|
|
48
|
+
"version": "invinoveritas.work-acceptance-bindings/v0",
|
|
49
|
+
"receipt_sha256": "4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d",
|
|
50
|
+
"criteria_id": "d74ad7d7492908f1146b233e89db6ca205065c67f02d33758ff0aa9f8b9e02fc",
|
|
51
|
+
"bindings": {
|
|
52
|
+
"x402_receipt_commitment": "0x43457c8e57b7c4ff92d716350c61c88c8f3b644d09d7c813d5a7ee6450a843da",
|
|
53
|
+
"x402_authorization_nonce": "0xe04421456465df1012f416b72aece3401f9e3d5be5bd78280324a031efe1d40f"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"receipt": {
|
|
58
|
+
"format": "eip712",
|
|
59
|
+
"authorizationNonce": "0x43457c8e57b7c4ff92d716350c61c88c8f3b644d09d7c813d5a7ee6450a843da",
|
|
60
|
+
"authorizationCommitment": "0xe04421456465df1012f416b72aece3401f9e3d5be5bd78280324a031efe1d40f"
|
|
61
|
+
},
|
|
62
|
+
"receiptSha256": "4c7273afcdb5333586b0a41c4238a968f3dedc23f66f71c5a1928ff3d279ab3d",
|
|
63
|
+
"deliverableSha256": "9c27eee03d50f4ff00fb60a1551a03d7311dbed257b7b943b3ff5d13eb30c083"
|
|
64
|
+
},
|
|
65
|
+
"expected": {
|
|
66
|
+
"valid": false,
|
|
67
|
+
"reason_code": "NOT_LINKED",
|
|
68
|
+
"detail": "COMMITMENT_MISMATCH"
|
|
69
|
+
},
|
|
70
|
+
"limits": [
|
|
71
|
+
"The commitment and the nonce in this vector are from our own mainnet payment of 18 October 2026, so both are real values, not placeholders.",
|
|
72
|
+
"A verifier can also fail this on AUTHORIZATION_MISMATCH depending on which pair it compares first; either refusal is correct, reporting LINKED is not."
|
|
73
|
+
]
|
|
74
|
+
}
|