agent-receipt-verify 0.2.4 → 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/bin/cli.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  // BEWUST GEEN NPM-PUBLICATIE NODIG. npx kan een pakket rechtstreeks uit een git-repo
10
10
  // draaien, dus dit werkt zonder dat er iets op npm staat:
11
11
  //
12
- // npx github:holistis/agent-receipt-verify https://een-eindpunt.example/pad
12
+ // npx -y agent-receipt-verify https://een-eindpunt.example/pad
13
13
  //
14
14
  // Dat is geen trucje maar een echte eigenschap van npx, en het haalt een hele
15
15
  // publicatie-stap plus een naamclaim weg. Het betekent ook dat niemand een
@@ -23,54 +23,61 @@
23
23
  const args = process.argv.slice(2)
24
24
  const eerste = args[0]
25
25
 
26
+ // De helptekst is ENGELS, en dat is geen voorkeur maar een gemeten noodzaak. Koud
27
+ // gedraaid vanuit een lege map op 18 oktober 2026 bleek het eerste scherm dat een vreemde
28
+ // ziet half Nederlands, terwijl iedereen die dit gereedschap in de x402- en
29
+ // erc-8004-draden gaat draaien Engels schrijft. Het meetresultaat zelf was al Engels, dus
30
+ // alleen de ingang stond nog in de weg. En de regel over wat dit NIET meet stond midden
31
+ // in de lijst in plaats van eronder, waar een lezer hem pas leest nadat hij weet wat het
32
+ // wel doet.
26
33
  function gebruik() {
27
- console.log('agent-receipt-verify, een onafhankelijke controle op bonnetjes van agent-betalingen')
34
+ console.log('agent-receipt-verify, an independent check on receipts for agent payments')
28
35
  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.')
36
+ console.log(' npx -y agent-receipt-verify <url> [url ...]')
37
+ console.log(' measures an x402 endpoint against the ladder in CONFORMANCE.md (levels 0')
38
+ console.log(' to 2, without paying). Levels 3 to 5 need a real settlement and come back')
39
+ console.log(' as NOT MEASURED, never as a failure.')
33
40
  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.')
41
+ console.log(' npx -y agent-receipt-verify census [out.json]')
42
+ console.log(' counts a discovery index per host instead of per resource.')
36
43
  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.')
44
+ console.log(' npx -y agent-receipt-verify link --receipt=<file>')
45
+ console.log(' for a version 2 receipt, finds the settlement it belongs to on chain.')
39
46
  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.')
47
+ console.log(' npx -y agent-receipt-verify mcp')
48
+ console.log(' runs as an MCP server over stdio, so an agent CALLS this check instead of')
49
+ console.log(' being advised to run it. Two tools: check an endpoint before you pay, and')
50
+ console.log(' link a receipt to a real settlement.')
44
51
  console.log('')
45
- console.log(' npx github:holistis/agent-receipt-verify inam <vectors.json>')
46
- console.log(' controleert countersigned bonnetjes tegen gepubliceerde vectoren.')
52
+ console.log(' npx -y agent-receipt-verify inam <vectors.json>')
53
+ console.log(' checks countersigned receipts against published vectors.')
47
54
  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')
55
+ console.log(' npx -y agent-receipt-verify kanon')
53
56
  console.log(' cross-check our independent EIP-3009 verifier against the ten published')
54
57
  console.log(' vectors of iamonuwa/kanon, offline, no account and no key.')
55
58
  console.log('')
56
- console.log(' npx github:holistis/agent-receipt-verify beleid')
59
+ console.log(' npx -y agent-receipt-verify beleid')
57
60
  console.log(' the same ten vectors under both amount policies, plus the two cases no')
58
61
  console.log(' published vector covers: overpayment, and one nonce across two payers.')
59
62
  console.log(' Those two are the open questions in x402 issue 2725.')
60
63
  console.log('')
61
- console.log(' npx github:holistis/agent-receipt-verify liveness [uit.json]')
64
+ console.log(' npx -y agent-receipt-verify liveness [out.json]')
62
65
  console.log(' takes the census and asks each host whether it still answers 402 today.')
63
66
  console.log(' A registration in an index is not a promise that the endpoint is alive.')
64
67
  console.log('')
65
- console.log(' npx github:holistis/agent-receipt-verify verify-delivery --answer=<file> --endpoint=<url>')
68
+ console.log(' npx -y agent-receipt-verify verify-delivery --answer=<file> --endpoint=<url>')
66
69
  console.log(' reads, at the source, whether a paid answer is TRUE. The location is')
67
70
  console.log(' DERIVED from --endpoint and never taken from the answer, because a seller')
68
71
  console.log(' who picks both the token and the place can stage any outcome.')
69
72
  console.log('')
70
- console.log(' npx github:holistis/agent-receipt-verify link-vectors')
73
+ console.log(' npx -y agent-receipt-verify link-vectors')
71
74
  console.log(' twelve vectors for the half AFTER paying: does this receipt belong to')
72
75
  console.log(' that one settlement? Eleven run offline, the chain-reading one is')
73
76
  console.log(' marked and skipped rather than failed.')
77
+ console.log('')
78
+ console.log('What this does NOT do: it judges the SHAPE of a receipt, not the honesty of')
79
+ console.log('the party that issued it. See CONFORMANCE.md for what is deliberately left')
80
+ console.log('unmeasured.')
74
81
  }
75
82
 
76
83
  // Het argument bepaalt welke module draait. De modules zijn ook los te draaien en te
@@ -104,7 +111,7 @@ if (!eerste || eerste === '--help' || eerste === '-h' || eerste === 'help') {
104
111
  process.argv = [process.argv[0], pad, ...args]
105
112
  await import('../src/x402-conformance-check.mjs')
106
113
  } else {
107
- console.log('onbekend: ' + eerste)
114
+ console.log('unknown command: ' + eerste)
108
115
  console.log('')
109
116
  gebruik()
110
117
  process.exitCode = 2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-receipt-verify",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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",
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",
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",
@@ -0,0 +1,309 @@
1
+ // Koppelt laag 3 aan laag 4: hoort dit acceptatierecord bij dit bonnetje.
2
+ //
3
+ // WAAROM DIT EEN EIGEN BESTAND IS EN NIET BIJ DE SETTLEMENT-LINK HOORT. De
4
+ // settlement-link-vectoren beantwoorden een andere vraag: opent de gezouten belofte in
5
+ // een bonnetje op precies een afrekening op de keten. Dat is betaalbewijs. Een
6
+ // acceptatierecord beantwoordt iets anders: heeft een onafhankelijke partij het geleverde
7
+ // bestand getoetst aan criteria die al vastlagen. Een uitgever die beide door elkaar
8
+ // haalt levert een lezer een vlaggetje "onderbouwd" waarvan niemand meer kan zien welke
9
+ // van de twee vragen nu eigenlijk beantwoord is. Daarom twee sets, twee uitkomsten.
10
+ //
11
+ // WAT HIER WEL GEMETEN WORDT. Uitsluitend de BINDING. Noemen het bonnetje en het
12
+ // acceptatierecord hetzelfde opgeleverde bestand, dezelfde machtiging en dezelfde belofte,
13
+ // en lagen de criteria vast voordat er getoetst werd.
14
+ //
15
+ // WAT HIER MET OPZET NIET GEMETEN WORDT, en dat is geen tekortkoming maar de grens van
16
+ // deze laag. Wij toetsen de handtekening van het acceptatierecord NIET, en wij
17
+ // beoordelen zijn criteria NIET. Dat is het werk van de controleur die het record
18
+ // uitgaf, met zijn eigen sleutel en zijn eigen verifieerder. Wie hier een groen antwoord
19
+ // krijgt weet dus dat de twee bewijsstukken over hetzelfde gaan, en niet dat het
20
+ // acceptatierecord zelf klopt. Die twee door elkaar halen is precies de fout die deze
21
+ // module bestaat om te voorkomen.
22
+ //
23
+ // GEMETEN OP EEN ECHT RECORD, 18 oktober 2026, en dat veranderde deze module wezenlijk.
24
+ // De uitgever publiceerde een live record (invinoveritas, trilatic.acceptance-receipt/1.2).
25
+ // Daaruit bleek dat onze eerste aannames over de VORM niet klopten:
26
+ //
27
+ // 1. De hash van het bonnetje staat BOVENIN de sidecar, naast `bindings`, niet erin.
28
+ // 2. De hash van het opgeleverde bestand staat in het ONDERTEKENDE record, op
29
+ // `record.receipt.files.remediated.sha256`, en helemaal niet in de sidecar.
30
+ // 3. De partijen staan genest in `record.receipt.parties`, met een `key_id` dat leeg
31
+ // kan zijn, en niet als twee platte tekstvelden.
32
+ // 4. Het tijdstip van de toets heet `issued_utc`, niet `checked_at`.
33
+ // 5. De x402-velden zijn TWEE aparte sleutels, en dat is met opzet zo: een belofte mag
34
+ // nooit met een nonce vergeleken worden. `x402_receipt_commitment` hoort bij
35
+ // `authorizationCommitment`, `x402_authorization_nonce` bij `authorizationNonce`.
36
+ //
37
+ // Daarom leest deze module nu de ECHTE vorm, met de vlakke vorm uit onze eerste
38
+ // vectoren als tweede mogelijkheid. Een module die alleen zijn eigen verzonnen vorm
39
+ // aankan, toetst niets in de wereld.
40
+ //
41
+ // EEN AANNAME DIE WEG IS. Onze eerste versie nam aan dat een machtiging in de sidecar
42
+ // gelijk zou zijn aan de nonce in het bonnetje, en las daarvoor ook een generiek veld
43
+ // `authorization`. Dat generieke veld is nu bewust GESCHRAPT. De uitgever splitste de
44
+ // sleutels juist om te voorkomen dat een belofte met een nonce vergeleken wordt, en een
45
+ // generieke terugval zou die bescherming meteen weer weggooien.
46
+
47
+ const HEX32 = /^[0-9a-f]{64}$/
48
+
49
+ const genormaliseerdeHash = (w) => {
50
+ if (typeof w !== 'string') return null
51
+ const kaal = w.trim().toLowerCase().replace(/^0x/, '')
52
+ return HEX32.test(kaal) ? kaal : null
53
+ }
54
+
55
+ const gelijkeHash = (a, b) => {
56
+ const x = genormaliseerdeHash(a)
57
+ const y = genormaliseerdeHash(b)
58
+ return x !== null && y !== null && x === y
59
+ }
60
+
61
+ const tijdstip = (w) => {
62
+ if (typeof w === 'number' && Number.isFinite(w)) return w
63
+ if (typeof w !== 'string' || !w.trim()) return null
64
+ const ms = Date.parse(w)
65
+ return Number.isFinite(ms) ? ms / 1000 : null
66
+ }
67
+
68
+ const tekst = (w) => (typeof w === 'string' && w.trim() ? w.trim().toLowerCase() : null)
69
+
70
+ /**
71
+ * Normaliseert zowel het ECHTE record als de vlakke vorm uit onze eerste vectoren naar
72
+ * de velden die deze module vergelijkt. Een veld dat niet te vinden is wordt null, nooit
73
+ * een gok, want een gegokt veld levert een oordeel op dat niemand kan narekenen.
74
+ *
75
+ * De `naamruimte` bij elke partij is er met opzet: een key_id en een weergavenaam zijn
76
+ * twee verschillende soorten namen, en die mag je niet met elkaar vergelijken. Zie
77
+ * onafhankelijkheid() voor waarom dat hier het verschil maakt tussen een hard oordeel en
78
+ * een onmeetbaar geval.
79
+ */
80
+ export function leesRecord(doc) {
81
+ const d = doc && typeof doc === 'object' ? doc : {}
82
+ const echt = d.record?.receipt ?? null
83
+ const acceptance = d.acceptance ?? null
84
+ const sidecar = d.bindings_sidecar ?? d.sidecar ?? null
85
+
86
+ const partij = (knoop, plat) => {
87
+ if (knoop && typeof knoop === 'object') {
88
+ if (tekst(knoop.key_id)) return { id: tekst(knoop.key_id), naamruimte: 'key_id' }
89
+ if (tekst(knoop.name)) return { id: tekst(knoop.name), naamruimte: 'name' }
90
+ return { id: null, naamruimte: null }
91
+ }
92
+ return { id: tekst(plat), naamruimte: tekst(plat) ? 'plat' : null }
93
+ }
94
+
95
+ return {
96
+ // De hash van het opgeleverde bestand, zoals het ONDERTEKENDE record hem noemt.
97
+ acceptatieLevering:
98
+ echt?.files?.remediated?.sha256 ?? acceptance?.deliverable_sha256 ?? d.deliverable_sha256 ?? null,
99
+ // De hash van het bonnetje, bovenin de sidecar.
100
+ sidecarBonHash: sidecar?.receipt_sha256 ?? d.bindings?.receipt_sha256 ?? null,
101
+ // De x402-sleutels, in het subobject dat daarvoor bedoeld is.
102
+ sidecarBindings: sidecar?.bindings ?? d.bindings ?? {},
103
+ // De vlakke vorm uit onze eerste vectoren noemde de levering ook in de sidecar. Die
104
+ // staat in een echt record NIET in de sidecar, dus dit is puur terugwaartse steun.
105
+ sidecarLevering: d.bindings?.deliverable_sha256 ?? sidecar?.deliverable_sha256 ?? null,
106
+ controleur: partij(echt?.parties?.checker, acceptance?.checker ?? d.checker),
107
+ producent: partij(echt?.parties?.producer, acceptance?.producer ?? d.producer),
108
+ onafhankelijkheidVerklaard: tekst(echt?.independence ?? acceptance?.independence ?? null),
109
+ criteriaVastgelegd:
110
+ echt?.criteria?.committed_at ?? acceptance?.criteria_committed_at ?? d.criteria_committed_at ?? null,
111
+ getoetstOp: echt?.issued_utc ?? acceptance?.checked_at ?? d.checked_at ?? null,
112
+ schema: echt?.schema ?? acceptance?.format ?? null,
113
+ }
114
+ }
115
+
116
+ export const ONAFHANKELIJKHEID = [
117
+ 'VERIFIEERBAAR_ONAFHANKELIJK',
118
+ 'NIET_ONAFHANKELIJK',
119
+ 'ZELFVERKLAARD_ONVERIFIEERBAAR',
120
+ 'ONBEKEND',
121
+ ]
122
+
123
+ /**
124
+ * Kan een DERDE narekenen dat de controleur niet de producent is?
125
+ *
126
+ * Dit is bewust een drieweg en geen ja of nee, en daar zit de hele waarde. Het live
127
+ * record van 18 oktober 2026 zegt `independence: "independent"` terwijl
128
+ * `parties.producer.key_id` LEEG is. Er is dus niets om de controleur tegen af te zetten:
129
+ * de onafhankelijkheid is door de uitgever zelf verklaard en door niemand anders na te
130
+ * rekenen. Dat is geen afkeuring en geen goedkeuring, het is onmeetbaar, en precies dat
131
+ * onderscheid gaat verloren in een boolean.
132
+ *
133
+ * Even belangrijk: een key_id vergelijken met een weergavenaam is GEEN vergelijking. Dat
134
+ * zijn twee verschillende soorten namen, en "ze zijn verschillend dus onafhankelijk" zou
135
+ * dan een conclusie zijn die uit het formaat komt en niet uit de werkelijkheid.
136
+ */
137
+ export function onafhankelijkheid(doc) {
138
+ const r = leesRecord(doc)
139
+ const c = r.controleur
140
+ const p = r.producent
141
+ if (c.id === null && p.id === null) {
142
+ return { staat: 'ONBEKEND', reden: 'geen van beide partijen is benoemd', vergelekenOp: null }
143
+ }
144
+ if (c.id === null || p.id === null) {
145
+ return {
146
+ staat: r.onafhankelijkheidVerklaard === 'independent' ? 'ZELFVERKLAARD_ONVERIFIEERBAAR' : 'ONBEKEND',
147
+ reden:
148
+ 'een van beide partijen heeft geen identiteit in het record, dus een derde kan niet narekenen ' +
149
+ 'of de controleur en de producent dezelfde partij zijn',
150
+ vergelekenOp: null,
151
+ }
152
+ }
153
+ if (c.naamruimte !== p.naamruimte) {
154
+ return {
155
+ staat: r.onafhankelijkheidVerklaard === 'independent' ? 'ZELFVERKLAARD_ONVERIFIEERBAAR' : 'ONBEKEND',
156
+ reden:
157
+ `de partijen zijn in verschillende soorten namen vastgelegd (${c.naamruimte} tegen ${p.naamruimte}), ` +
158
+ 'en die mag je niet met elkaar vergelijken',
159
+ vergelekenOp: null,
160
+ }
161
+ }
162
+ if (c.id === p.id) {
163
+ return { staat: 'NIET_ONAFHANKELIJK', reden: 'de controleur en de producent zijn dezelfde partij', vergelekenOp: c.naamruimte }
164
+ }
165
+ return { staat: 'VERIFIEERBAAR_ONAFHANKELIJK', reden: null, vergelekenOp: c.naamruimte }
166
+ }
167
+
168
+ // Is er genoeg om uberhaupt een oordeel te vellen? Ontbreekt er iets aan ONZE kant van de
169
+ // vergelijking, dan is het antwoord NOT_MEASURED en nooit NOT_LINKED. Een ontbrekend veld
170
+ // als "niet gekoppeld" lezen zou een ongemeten geval als een afkeuring laten tellen.
171
+ export function bindingMeetbaar(doc) {
172
+ const r = leesRecord(doc)
173
+ const mist = []
174
+ if (genormaliseerdeHash(r.sidecarBonHash) === null) mist.push('sidecar.receipt_sha256')
175
+ if (genormaliseerdeHash(r.acceptatieLevering) === null) mist.push('record.receipt.files.remediated.sha256')
176
+ return mist.length ? { meetbaar: false, mist } : { meetbaar: true, mist: [] }
177
+ }
178
+
179
+ // De twee voorwaarden van de uitgever zelf (trilatic.acceptance-receipt/1.2): een record
180
+ // telt alleen als de controleur niet de producent is, en als de criteria vastlagen
181
+ // voordat er getoetst werd. Ze staan hier in code omdat een voorwaarde die alleen in
182
+ // proza staat bij de eerste drukke dag wordt overgeslagen.
183
+ //
184
+ // De uitkomst is drieledig, want "ik kan het niet nagaan" is iets anders dan "het is
185
+ // mis". Dat onderscheid is dezelfde correctie die de Kanon-auteur op 17 oktober 2026
186
+ // overnam voor zijn eigen controleur.
187
+ export function recordTelt(doc) {
188
+ const r = leesRecord(doc)
189
+ const o = onafhankelijkheid(doc)
190
+ if (o.staat === 'NIET_ONAFHANKELIJK') return { telt: false, meetbaar: true, reden: 'CHECKER_IS_PRODUCER' }
191
+ if (o.staat !== 'VERIFIEERBAAR_ONAFHANKELIJK') {
192
+ return { telt: false, meetbaar: false, reden: 'INDEPENDENCE_NOT_VERIFIABLE', toelichting: o.reden }
193
+ }
194
+ const vastgelegd = tijdstip(r.criteriaVastgelegd)
195
+ const getoetst = tijdstip(r.getoetstOp)
196
+ if (vastgelegd === null || getoetst === null) {
197
+ return { telt: false, meetbaar: false, reden: 'CRITERIA_TIMING_UNKNOWN' }
198
+ }
199
+ if (!(vastgelegd < getoetst)) {
200
+ return { telt: false, meetbaar: true, reden: 'CRITERIA_NOT_COMMITTED_FIRST' }
201
+ }
202
+ return { telt: true, meetbaar: true, reden: null }
203
+ }
204
+
205
+ /**
206
+ * Het eigenlijke oordeel. Drie uitkomsten, nooit twee: gekoppeld, niet gekoppeld, of
207
+ * niet te meten.
208
+ *
209
+ * `receiptSha256` en het bonnetje zelf komen van de AANROEPER, want dat is de kant die
210
+ * hij in handen heeft. Alles aan de record-kant wordt uit het document gelezen.
211
+ */
212
+ export function koppelAcceptatie({ receipt, acceptance, bindings, sidecar, doc, receiptSha256, deliverableSha256 }) {
213
+ // Een aanroeper mag het hele document meegeven, of de losse stukken zoals onze eerste
214
+ // vectoren dat deden. Beide komen hier op dezelfde genormaliseerde vorm uit.
215
+ const document = doc ?? {
216
+ record: acceptance?.record ? acceptance.record : undefined,
217
+ acceptance,
218
+ bindings,
219
+ bindings_sidecar: sidecar,
220
+ }
221
+ const r = leesRecord(document)
222
+
223
+ const m = bindingMeetbaar(document)
224
+ if (!m.meetbaar) {
225
+ return { valid: false, measurable: false, reason_code: 'NOT_MEASURED', missing: m.mist, independence: onafhankelijkheid(document).staat }
226
+ }
227
+
228
+ const telt = recordTelt(document)
229
+ if (!telt.telt) {
230
+ return telt.meetbaar
231
+ ? { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: telt.reden, independence: onafhankelijkheid(document).staat }
232
+ : {
233
+ valid: false,
234
+ measurable: false,
235
+ reason_code: 'NOT_MEASURED',
236
+ missing: [telt.reden],
237
+ detail: telt.toelichting ?? null,
238
+ independence: onafhankelijkheid(document).staat,
239
+ }
240
+ }
241
+
242
+ // Noemt de sidecar het bonnetje dat wij in handen hebben? Zonder een meegegeven hash van
243
+ // ons eigen bonnetje kan die helft niet getoetst worden, en dan is het niet gemeten.
244
+ if (receiptSha256 !== undefined) {
245
+ if (genormaliseerdeHash(receiptSha256) === null) {
246
+ return { valid: false, measurable: false, reason_code: 'NOT_MEASURED', missing: ['receiptSha256'] }
247
+ }
248
+ if (!gelijkeHash(receiptSha256, r.sidecarBonHash)) {
249
+ return { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: 'RECEIPT_HASH_MISMATCH' }
250
+ }
251
+ }
252
+
253
+ // De kern: wijzen het record en wat wij in handen hebben naar hetzelfde opgeleverde
254
+ // bestand. De aanroeper mag de hash van het bestand dat hij ECHT ontving meegeven; dat
255
+ // is sterker dan een hash die ook uit de sidecar komt, want dan vergelijk je twee
256
+ // velden van dezelfde uitgever met elkaar.
257
+ const onzeKant = deliverableSha256 ?? r.sidecarLevering
258
+ if (onzeKant === null || onzeKant === undefined) {
259
+ return {
260
+ valid: false,
261
+ measurable: false,
262
+ reason_code: 'NOT_MEASURED',
263
+ missing: ['deliverableSha256'],
264
+ detail: 'het record noemt een opgeleverd bestand, maar er is geen eigen hash om het tegen af te zetten',
265
+ independence: onafhankelijkheid(document).staat,
266
+ }
267
+ }
268
+ if (!gelijkeHash(r.acceptatieLevering, onzeKant)) {
269
+ return { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: 'DELIVERABLE_HASH_MISMATCH' }
270
+ }
271
+
272
+ // De twee x402-sleutels, ELK met zijn eigen tegenhanger. Een belofte wordt nooit met
273
+ // een nonce vergeleken: daarvoor zijn het twee sleutels. Staat een van de twee kanten
274
+ // niet in het stuk, dan wordt er niet vergeleken, en dat is geen afkeuring.
275
+ const b = r.sidecarBindings ?? {}
276
+ const paren = [
277
+ {
278
+ naam: 'COMMITMENT_MISMATCH',
279
+ sidecar: b.x402_receipt_commitment,
280
+ bon: receipt?.authorizationCommitment ?? receipt?.payload?.authorizationCommitment,
281
+ },
282
+ {
283
+ naam: 'AUTHORIZATION_MISMATCH',
284
+ sidecar: b.x402_authorization_nonce,
285
+ bon: receipt?.authorizationNonce ?? receipt?.payload?.authorizationNonce,
286
+ },
287
+ ]
288
+ const vergeleken = []
289
+ for (const p of paren) {
290
+ if (p.sidecar === undefined || p.sidecar === null) continue
291
+ if (p.bon === undefined || p.bon === null) continue
292
+ if (!gelijkeHash(p.sidecar, p.bon)) {
293
+ return { valid: false, measurable: true, reason_code: 'NOT_LINKED', detail: p.naam }
294
+ }
295
+ vergeleken.push(p.naam.replace('_MISMATCH', ''))
296
+ }
297
+
298
+ return {
299
+ valid: true,
300
+ measurable: true,
301
+ reason_code: 'LINKED',
302
+ independence: onafhankelijkheid(document).staat,
303
+ // Wat er WEL en NIET vergeleken is, want een groen antwoord zonder dat lijstje laat
304
+ // een lezer denken dat alles getoetst is.
305
+ compared: vergeleken,
306
+ }
307
+ }
308
+
309
+ export const UITKOMSTEN = ['LINKED', 'NOT_LINKED', 'NOT_MEASURED']
@@ -51,22 +51,58 @@ function decodeerHeader(waarde) {
51
51
  */
52
52
  export async function meetEindpunt(url, { fetchImpl = fetch, methode = 'POST', lichaam = null } = {}) {
53
53
  const r = { url, treden: {}, bewijs: {} }
54
- let resp
55
- try {
56
- resp = await fetchImpl(url, {
57
- method: methode,
54
+
55
+ const vraag = (m) =>
56
+ fetchImpl(url, {
57
+ method: m,
58
58
  headers: { 'User-Agent': UA, Accept: 'application/json', 'Content-Type': 'application/json' },
59
59
  // Een gedeclareerd voorbeeldlichaam als de index er een geeft. Een lege {} naar
60
60
  // een route die velden eist, levert een 400 die niets over x402 zegt: dan meet
61
61
  // je je eigen verzoek in plaats van het eindpunt.
62
- body: methode === 'GET' || methode === 'DELETE' ? undefined : lichaam != null ? JSON.stringify(lichaam) : '{}',
62
+ body: m === 'GET' || m === 'DELETE' ? undefined : lichaam != null ? JSON.stringify(lichaam) : '{}',
63
63
  signal: AbortSignal.timeout(15000),
64
64
  })
65
+
66
+ let resp
67
+ let gebruikteMethode = methode
68
+ try {
69
+ resp = await vraag(methode)
70
+
71
+ // EEN EINDPUNT MAG NOOIT AFGEKEURD WORDEN OP EEN METHODE DIE HET NIET AANBIEDT.
72
+ // Gemeten 18 oktober 2026, op ons eigen eindpunt en koud vanuit een lege map: deze
73
+ // meter sondeert standaard met POST, en een route die alleen GET bedient antwoordt
74
+ // daarop correct met 405. Dat werd hier als "haalt trede 0 niet" gelezen, dus als
75
+ // een fout van het eindpunt, terwijl het een fout van ons verzoek was. Elk
76
+ // GET-eindpunt in de wereld werd daardoor ten onrechte afgekeurd.
77
+ //
78
+ // Dus: bij een 405 een keer opnieuw meten met een methode die het eindpunt zelf
79
+ // noemt in zijn Allow-kop. Noemt het er geen, dan is GET de enige redelijke tweede
80
+ // poging, en dat staat dan ook als gok in het bewijs. Een meter die zijn eigen
81
+ // aanname niet opschrijft, laat een lezer niet zien waar hij kan afwijken.
82
+ if (resp.status === 405) {
83
+ const allow = resp.headers.get('allow') || resp.headers.get('Allow') || ''
84
+ const genoemd = allow
85
+ .split(',')
86
+ .map((s) => s.trim().toUpperCase())
87
+ .filter((m) => m && m !== methode && m !== 'OPTIONS' && m !== 'HEAD')
88
+ const tweede = genoemd[0] ?? (methode !== 'GET' ? 'GET' : null)
89
+ if (tweede) {
90
+ r.bewijs.methode_opnieuw = {
91
+ eerste: methode,
92
+ eerste_status: 405,
93
+ tweede,
94
+ bron: genoemd.length ? 'Allow header' : 'guessed, the 405 carried no Allow header',
95
+ }
96
+ resp = await vraag(tweede)
97
+ gebruikteMethode = tweede
98
+ }
99
+ }
65
100
  } catch (e) {
66
101
  r.treden.t0 = 'ONGEMETEN'
67
102
  r.bewijs.t0 = 'not reachable: ' + String(e.message).slice(0, 80)
68
103
  return r
69
104
  }
105
+ r.bewijs.methode = gebruikteMethode
70
106
 
71
107
  r.bewijs.status = resp.status
72
108
  const tekst = await resp.text().catch(() => '')
@@ -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
+ }
@@ -0,0 +1,66 @@
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-with-producer-key",
11
+ "encodes": "The positive control, and at the same time the exact one-field diff that makes the record auditable.",
12
+ "description": "Byte-identical to real-record-as-issued except that parties.producer.key_id is set to a distinct key. Now the checker and the producer are both named in the same namespace and differ, so a third party can verify the independence claim instead of accepting it. This must link. Without this vector the set would only contain refusals, which proves nothing about the harness.",
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": "9c27eee03d50f4ff00fb60a1551a03d7311dbed257b7b943b3ff5d13eb30c083"
56
+ },
57
+ "expected": {
58
+ "valid": true,
59
+ "reason_code": "LINKED",
60
+ "independence": "VERIFIEERBAAR_ONAFHANKELIJK"
61
+ },
62
+ "limits": [
63
+ "The signature on the record is not checked here, and neither are its criteria. That is the issuing checker's own verifier.",
64
+ "A LINKED verdict says the two artifacts are about the same thing, not that the acceptance record itself is sound."
65
+ ]
66
+ }
@@ -0,0 +1,76 @@
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-x402-keys-match",
11
+ "encodes": "Both payment keys present and each compared against its own counterpart. The positive control for the two-key split.",
12
+ "description": "The sidecar carries the commitment in the commitment key and the nonce in the nonce key, both matching the receipt. This must link, and a verifier should report which pairs it actually compared, so that a reader can tell a full comparison from a record that simply carried neither key.",
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": "0xe04421456465df1012f416b72aece3401f9e3d5be5bd78280324a031efe1d40f",
53
+ "x402_authorization_nonce": "0x43457c8e57b7c4ff92d716350c61c88c8f3b644d09d7c813d5a7ee6450a843da"
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": true,
67
+ "reason_code": "LINKED",
68
+ "compared": [
69
+ "COMMITMENT",
70
+ "AUTHORIZATION"
71
+ ]
72
+ },
73
+ "limits": [
74
+ "Linking the payment to the acceptance record does not prove the payment settled. That is the receipt-to-settlement question and it needs a chain read."
75
+ ]
76
+ }