agent-receipt-verify 0.2.4 → 0.2.6
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 +33 -26
- package/package.json +2 -2
- package/src/acceptance-binding.mjs +309 -0
- package/src/x402-conformance-check.mjs +54 -5
- 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/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
|
|
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,
|
|
34
|
+
console.log('agent-receipt-verify, an independent check on receipts for agent payments')
|
|
28
35
|
console.log('')
|
|
29
|
-
console.log(' npx
|
|
30
|
-
console.log('
|
|
31
|
-
console.log('
|
|
32
|
-
console.log('
|
|
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
|
|
35
|
-
console.log('
|
|
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
|
|
38
|
-
console.log('
|
|
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
|
|
41
|
-
console.log('
|
|
42
|
-
console.log('
|
|
43
|
-
console.log('
|
|
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
|
|
46
|
-
console.log('
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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.
|
|
3
|
+
"version": "0.2.6",
|
|
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,71 @@ 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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
method:
|
|
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:
|
|
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
|
+
// EN EEN 404 IS DEZELFDE FOUT MET EEN ANDER NUMMER. Gemeten dezelfde dag, op een
|
|
83
|
+
// eindpunt van een DERDE: api.thaikit.dev/number-text antwoordt 402 met een geldige
|
|
84
|
+
// payment-required-kop op GET, en 404 op POST. Dat is geen afwijkend eindpunt maar
|
|
85
|
+
// het normale gedrag van veel webraamwerken, die een pad plus een niet-geregistreerde
|
|
86
|
+
// methode als "niet gevonden" behandelen in plaats van als "methode niet toegestaan".
|
|
87
|
+
// Onze meter noemde die levende, correcte verkoper daardoor niet-conform. Alleen de
|
|
88
|
+
// 405 repareren liet dus de helft van het probleem staan.
|
|
89
|
+
//
|
|
90
|
+
// De herhaling blijft bewust smal: alleen 404 en 405, alleen als wij NIET met GET
|
|
91
|
+
// sondeerden, en precies een keer. Elke 404 blind herhalen zou het verkeer naar elk
|
|
92
|
+
// dood adres verdubbelen, en dat is niet aan ons om te doen.
|
|
93
|
+
if (resp.status === 405 || resp.status === 404) {
|
|
94
|
+
const allow = resp.headers.get('allow') || resp.headers.get('Allow') || ''
|
|
95
|
+
const genoemd = allow
|
|
96
|
+
.split(',')
|
|
97
|
+
.map((s) => s.trim().toUpperCase())
|
|
98
|
+
.filter((m) => m && m !== methode && m !== 'OPTIONS' && m !== 'HEAD')
|
|
99
|
+
const tweede = genoemd[0] ?? (methode !== 'GET' ? 'GET' : null)
|
|
100
|
+
if (tweede) {
|
|
101
|
+
r.bewijs.methode_opnieuw = {
|
|
102
|
+
eerste: methode,
|
|
103
|
+
eerste_status: resp.status,
|
|
104
|
+
tweede,
|
|
105
|
+
bron: genoemd.length
|
|
106
|
+
? 'Allow header'
|
|
107
|
+
: 'guessed, the ' + resp.status + ' carried no Allow header',
|
|
108
|
+
}
|
|
109
|
+
resp = await vraag(tweede)
|
|
110
|
+
gebruikteMethode = tweede
|
|
111
|
+
}
|
|
112
|
+
}
|
|
65
113
|
} catch (e) {
|
|
66
114
|
r.treden.t0 = 'ONGEMETEN'
|
|
67
115
|
r.bewijs.t0 = 'not reachable: ' + String(e.message).slice(0, 80)
|
|
68
116
|
return r
|
|
69
117
|
}
|
|
118
|
+
r.bewijs.methode = gebruikteMethode
|
|
70
119
|
|
|
71
120
|
r.bewijs.status = resp.status
|
|
72
121
|
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
|
+
}
|