agent-receipt-verify 0.0.0-stage → 0.2.1
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/CONFORMANCE.md +110 -0
- package/LICENSE +201 -0
- package/NOTICE +39 -0
- package/README.md +256 -2
- package/bin/cli.mjs +89 -0
- package/examples/receipt-live-base-sepolia-2.json +21 -0
- package/examples/receipt-live-base-sepolia-3.json +21 -0
- package/examples/receipt-live-base-sepolia.json +21 -0
- package/package.json +59 -4
- package/profiles/profile.schema.json +115 -0
- package/profiles/x402-deployed-ladder-v1.json +178 -0
- package/src/eip3009-autorisatie.mjs +195 -0
- package/src/inam-countersigned.mjs +162 -0
- package/src/mcp-server.mjs +262 -0
- package/src/profiel.mjs +145 -0
- package/src/x402-conformance-check.mjs +194 -0
- package/src/x402-conformance-treden.mjs +246 -0
- package/src/x402-discovery-census.mjs +247 -0
- package/src/x402-index-liveness.mjs +201 -0
- package/src/x402-link-onchain.mjs +280 -0
- package/src/x402-metingen-bundel.mjs +96 -0
- package/src/x402-offer-receipt.mjs +84 -0
- package/src/x402-poortwachters.mjs +241 -0
- package/tools/bundel-bouw.mjs +126 -0
- package/vectors/inam/BRON-README.md +38 -0
- package/vectors/inam/canonical-vectors.json +122 -0
- package/vectors/inam/vectors.json +96 -0
- package/vectors/kanon/amount-insufficient-001.json +52 -0
- package/vectors/kanon/cross-chain-replay-001.json +54 -0
- package/vectors/kanon/cross-contract-replay-001.json +54 -0
- package/vectors/kanon/domain-name-mismatch-001.json +53 -0
- package/vectors/kanon/expired-001.json +51 -0
- package/vectors/kanon/network-mismatch-001.json +52 -0
- package/vectors/kanon/nonce-replay-001.json +56 -0
- package/vectors/kanon/not-yet-valid-001.json +51 -0
- package/vectors/kanon/sig-malleable-high-s-001.json +54 -0
- package/vectors/kanon/valid-baseline-001.json +52 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Meet de POORTWACHTERS, niet de kraampjes.
|
|
2
|
+
//
|
|
3
|
+
// WAAROM DIT APART STAAT VAN DE ANDERE METINGEN. x402-conformance-check meet een
|
|
4
|
+
// verkoper, en x402-discovery-census telt wat verkopers in EEN index hebben
|
|
5
|
+
// aangemeld. Beide meten de kraampjes. Maar een verkoper kan niets afrekenen zonder
|
|
6
|
+
// een facilitator, en niemand vindt hem zonder een index. Dat zijn er maar een paar,
|
|
7
|
+
// en daar zit de macht in deze laag. Een scorebord over een handvol poortwachters is
|
|
8
|
+
// daarom iets anders dan een scorebord over duizend winkeltjes: de poortwachters
|
|
9
|
+
// kunnen niet doen alsof ze het niet lezen.
|
|
10
|
+
//
|
|
11
|
+
// WAT HIER MET OPZET NIET GEBEURT. Geen afrekening, geen betaling, geen poging om
|
|
12
|
+
// iets af te dwingen, en nooit meer dan een paar gewone GET-verzoeken per host, met
|
|
13
|
+
// een eerlijke user-agent en rust ertussen. Dit meet wat een poortwachter ZELF
|
|
14
|
+
// publiek over zichzelf zegt via zijn eigen endpoints. Niets meer.
|
|
15
|
+
//
|
|
16
|
+
// DE TREDEN, en net als bij de verkopersladder is "niet gemeten" geen afkeuring:
|
|
17
|
+
// G0 antwoordt /supported met bruikbare JSON (minstens een netwerk en een scheme)
|
|
18
|
+
// G1 noemt in dat antwoord welke extensies hij ondersteunt
|
|
19
|
+
// G2 biedt een discovery-index aan die JSON teruggeeft
|
|
20
|
+
// G3 die index meldt een totaal, zodat zijn omvang narekenbaar is
|
|
21
|
+
// G4 de index is niet door een enkele partij gedomineerd (grootste host onder de
|
|
22
|
+
// helft van de EERSTE PAGINA)
|
|
23
|
+
//
|
|
24
|
+
// EN DAAR ZIT EEN VALKUIL IN DIE BIJ DE EERSTE RUN METEEN BLEEK. De eerste pagina is
|
|
25
|
+
// NIET representatief. Op payai gaf de eerste pagina 21,0 procent voor de grootste
|
|
26
|
+
// host, terwijl onze volledige census over alle 20.597 registraties 51,8 procent
|
|
27
|
+
// vond voor het grootste domein. Een index wordt niet in willekeurige volgorde
|
|
28
|
+
// geleverd, dus wie alleen de eerste pagina weegt onderschat de concentratie, en in
|
|
29
|
+
// dit geval met meer dan een factor twee. G4 is daarom een ONDERGRENS en nooit een
|
|
30
|
+
// vrijbrief: hij haalt alleen de gevallen boven water die al op de eerste pagina
|
|
31
|
+
// zichtbaar zijn. De echte concentratie meet je met x402-discovery-census.mjs, dat
|
|
32
|
+
// de hele index leest.
|
|
33
|
+
//
|
|
34
|
+
// G4 bestaat omdat onze eigen census op 2026-10-17 vond dat in de grootste index
|
|
35
|
+
// EEN domein meer dan de helft van alle registraties houdt, over elf subdomeinen.
|
|
36
|
+
// Dat is geen verwijt aan die partij (gemeten: het zijn echte, losse eindpunten),
|
|
37
|
+
// maar het betekent dat de omvang van een index niets zegt over het aantal partijen
|
|
38
|
+
// in de markt. Een index die dat niet zichtbaar maakt, laat iedereen die hem citeert
|
|
39
|
+
// een verkeerd getal noemen.
|
|
40
|
+
|
|
41
|
+
const UA = 'x402-conformance-check/1 (read-only, github.com/holistis/agent-receipt-verify)'
|
|
42
|
+
const slaap = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
43
|
+
|
|
44
|
+
/** De lijst die x402 zelf publiceert, plus de twee die wij al gemeten hebben. */
|
|
45
|
+
export const POORTWACHTERS = [
|
|
46
|
+
{ naam: 'payai', basis: 'https://facilitator.payai.network' },
|
|
47
|
+
{ naam: 'x402.org', basis: 'https://x402.org/facilitator' },
|
|
48
|
+
{ naam: 'celo', basis: 'https://api.x402.celo.org' },
|
|
49
|
+
{ naam: 'celo-sepolia', basis: 'https://api.x402.sepolia.celo.org' },
|
|
50
|
+
{ naam: 'corbits', basis: 'https://corbits.dev' },
|
|
51
|
+
{ naam: 'dexter', basis: 'https://dexter.cash/facilitator' },
|
|
52
|
+
{ naam: 'mogami', basis: 'https://facilitator.mogami.tech' },
|
|
53
|
+
{ naam: 'mrdn', basis: 'https://mrdn.finance' },
|
|
54
|
+
{ naam: 'solvador', basis: 'https://solvador.com' },
|
|
55
|
+
{ naam: 'mikedotexe', basis: 'https://x402.mikedotexe.com' },
|
|
56
|
+
{ naam: 't54-xrpl', basis: 'https://xrpl-x402.t54.ai' },
|
|
57
|
+
{ naam: 'ftptech', basis: 'https://www.ftptech.xyz/x402' },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
async function haalJson(url, { fetchImpl = fetch } = {}) {
|
|
61
|
+
try {
|
|
62
|
+
const r = await fetchImpl(url, {
|
|
63
|
+
headers: { 'User-Agent': UA, Accept: 'application/json' },
|
|
64
|
+
signal: AbortSignal.timeout(20000),
|
|
65
|
+
})
|
|
66
|
+
const tekst = await r.text().catch(() => '')
|
|
67
|
+
if (!r.ok) return { status: r.status, json: null, reden: 'status ' + r.status }
|
|
68
|
+
try {
|
|
69
|
+
return { status: r.status, json: JSON.parse(tekst) }
|
|
70
|
+
} catch {
|
|
71
|
+
// Een HTML-pagina op een API-pad is GEEN kapotte facilitator, het betekent
|
|
72
|
+
// dat dit endpoint hier niet bestaat. Dat onderscheid hoort in de uitslag.
|
|
73
|
+
return { status: r.status, json: null, reden: tekst.trim().startsWith('<') ? 'html, geen json' : 'geen json' }
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return { status: null, json: null, reden: 'onbereikbaar: ' + String(e.message).slice(0, 50) }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Leest een /supported-antwoord zonder aan te nemen hoe het precies heet. */
|
|
81
|
+
export function leesSupported(j) {
|
|
82
|
+
if (!j || typeof j !== 'object') return { netwerken: [], schemes: [], extensies: [] }
|
|
83
|
+
const rijen = Array.isArray(j) ? j : (j.kinds ?? j.supported ?? j.data ?? j.items ?? [])
|
|
84
|
+
const netwerken = new Set()
|
|
85
|
+
const schemes = new Set()
|
|
86
|
+
const extensies = new Set()
|
|
87
|
+
for (const r of Array.isArray(rijen) ? rijen : []) {
|
|
88
|
+
if (r?.network) netwerken.add(String(r.network))
|
|
89
|
+
if (r?.scheme) schemes.add(String(r.scheme))
|
|
90
|
+
// Een lijst en een object moeten apart: Object.keys op een lijst geeft de
|
|
91
|
+
// INDEXEN, dus ["bazaar"] zou "0" als extensienaam opleveren. Gevonden door de
|
|
92
|
+
// test, en precies het soort stille fout dat een scorebord onzin maakt.
|
|
93
|
+
if (Array.isArray(r?.extensions)) for (const e of r.extensions) extensies.add(String(e))
|
|
94
|
+
else for (const k of Object.keys(r?.extensions ?? {})) extensies.add(k)
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(j.extensions)) for (const e of j.extensions) extensies.add(String(e))
|
|
97
|
+
else for (const k of Object.keys(j.extensions ?? {})) extensies.add(k)
|
|
98
|
+
return { netwerken: [...netwerken], schemes: [...schemes], extensies: [...extensies] }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Concentratie op de EERSTE pagina: het aandeel van de grootste host. */
|
|
102
|
+
export function concentratie(items) {
|
|
103
|
+
const perHost = new Map()
|
|
104
|
+
let bruikbaar = 0
|
|
105
|
+
for (const it of Array.isArray(items) ? items : []) {
|
|
106
|
+
let h = null
|
|
107
|
+
try { h = new URL(it?.resource ?? it?.url ?? '').host.toLowerCase() } catch { h = null }
|
|
108
|
+
if (!h) continue
|
|
109
|
+
bruikbaar++
|
|
110
|
+
perHost.set(h, (perHost.get(h) ?? 0) + 1)
|
|
111
|
+
}
|
|
112
|
+
if (bruikbaar === 0) return { hosts: 0, grootste: null, aandeel: null, gemeten: 0 }
|
|
113
|
+
const top = [...perHost.entries()].sort((a, b) => b[1] - a[1])[0]
|
|
114
|
+
return { hosts: perHost.size, grootste: top[0], aandeel: top[1] / bruikbaar, gemeten: bruikbaar }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** De hoogste trede die deze poortwachter aantoonbaar haalt. */
|
|
118
|
+
export function hoogsteTrede(m) {
|
|
119
|
+
if (!m.g0) return -1
|
|
120
|
+
if (!m.g1) return 0
|
|
121
|
+
if (!m.g2) return 1
|
|
122
|
+
if (!m.g3) return 2
|
|
123
|
+
if (!m.g4) return 3
|
|
124
|
+
return 4
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function meetPoortwachter(p, { fetchImpl = fetch, pauzeMs = 900 } = {}) {
|
|
128
|
+
const m = { naam: p.naam, basis: p.basis, bewijs: {} }
|
|
129
|
+
|
|
130
|
+
const sup = await haalJson(p.basis.replace(/\/$/, '') + '/supported', { fetchImpl })
|
|
131
|
+
const gelezen = leesSupported(sup.json)
|
|
132
|
+
m.g0 = gelezen.netwerken.length > 0 && gelezen.schemes.length > 0
|
|
133
|
+
m.bewijs.g0 = m.g0
|
|
134
|
+
? gelezen.netwerken.length + ' netwerk(en), scheme(s): ' + gelezen.schemes.join(', ')
|
|
135
|
+
: 'geen bruikbaar /supported (' + (sup.reden ?? 'leeg antwoord') + ')'
|
|
136
|
+
m.netwerken = gelezen.netwerken
|
|
137
|
+
m.schemes = gelezen.schemes
|
|
138
|
+
|
|
139
|
+
m.g1 = gelezen.extensies.length > 0
|
|
140
|
+
m.bewijs.g1 = m.g1 ? 'noemt: ' + gelezen.extensies.join(', ') : 'noemt geen enkele extensie'
|
|
141
|
+
m.extensies = gelezen.extensies
|
|
142
|
+
|
|
143
|
+
await slaap(pauzeMs)
|
|
144
|
+
const disc = await haalJson(p.basis.replace(/\/$/, '') + '/discovery/resources?limit=100&offset=0', { fetchImpl })
|
|
145
|
+
const items = disc.json?.items ?? disc.json?.resources ?? disc.json?.data ?? null
|
|
146
|
+
m.g2 = Array.isArray(items)
|
|
147
|
+
m.bewijs.g2 = m.g2 ? items.length + ' op de eerste pagina' : 'geen index (' + (disc.reden ?? 'geen items-lijst') + ')'
|
|
148
|
+
|
|
149
|
+
const totaal = disc.json?.pagination?.total ?? disc.json?.total ?? null
|
|
150
|
+
m.g3 = m.g2 && Number.isFinite(Number(totaal))
|
|
151
|
+
m.bewijs.g3 = m.g3 ? 'meldt totaal ' + totaal : 'meldt geen totaal, dus zijn omvang is niet na te rekenen'
|
|
152
|
+
m.gemeldTotaal = m.g3 ? Number(totaal) : null
|
|
153
|
+
|
|
154
|
+
const c = m.g2 ? concentratie(items) : null
|
|
155
|
+
m.concentratie = c
|
|
156
|
+
if (!m.g2) {
|
|
157
|
+
m.g4 = false
|
|
158
|
+
m.bewijs.g4 = 'geen index om te wegen'
|
|
159
|
+
} else if (!c || c.gemeten === 0) {
|
|
160
|
+
m.g4 = false
|
|
161
|
+
m.bewijs.g4 = 'index zonder bruikbare adressen'
|
|
162
|
+
} else {
|
|
163
|
+
m.g4 = c.aandeel < 0.5
|
|
164
|
+
m.bewijs.g4 = 'grootste host ' + c.grootste + ' is ' + (c.aandeel * 100).toFixed(1) +
|
|
165
|
+
'% van ' + c.gemeten + ' op de EERSTE PAGINA, over ' + c.hosts + ' hosts. Dit is een' +
|
|
166
|
+
' ondergrens: een volledige census van dezelfde index gaf een duidelijk hoger aandeel.'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
m.trede = hoogsteTrede(m)
|
|
170
|
+
return m
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (process.argv[1]?.endsWith('x402-poortwachters.mjs')) {
|
|
174
|
+
const uitPad = process.argv[2]
|
|
175
|
+
|
|
176
|
+
// Controles eerst, net als bij de verkopersmeting: een ronde waarin alles faalt
|
|
177
|
+
// bewijst niet dat de wereld faalt, hij kan ook bewijzen dat het gereedschap kapot
|
|
178
|
+
// is. payai is bekend als index-aanbieder, en een documentatiepagina hoort niets
|
|
179
|
+
// te halen.
|
|
180
|
+
console.log('--- controls first ---')
|
|
181
|
+
const pos = await meetPoortwachter({ naam: 'control-payai', basis: 'https://facilitator.payai.network' })
|
|
182
|
+
const neg = await meetPoortwachter({ naam: 'control-docs', basis: 'https://docs.github.com' })
|
|
183
|
+
const posOk = pos.g2 === true
|
|
184
|
+
const negOk = neg.trede === -1
|
|
185
|
+
console.log((posOk ? ' ok ' : ' FAIL ') + 'a known index offers one: g2=' + pos.g2)
|
|
186
|
+
console.log((negOk ? ' ok ' : ' FAIL ') + 'a documentation site reaches nothing: level ' + neg.trede)
|
|
187
|
+
if (!posOk || !negOk) {
|
|
188
|
+
console.log('')
|
|
189
|
+
console.log('The harness did not behave on the controls. Treat everything below as invalid.')
|
|
190
|
+
}
|
|
191
|
+
console.log('')
|
|
192
|
+
|
|
193
|
+
const uit = []
|
|
194
|
+
for (const p of POORTWACHTERS) {
|
|
195
|
+
const m = await meetPoortwachter(p)
|
|
196
|
+
uit.push(m)
|
|
197
|
+
console.log(
|
|
198
|
+
(m.trede >= 0 ? 'G' + m.trede : 'G-') + ' ' + m.naam.padEnd(14) +
|
|
199
|
+
(m.netwerken.length ? m.netwerken.length + ' net' : ' ') +
|
|
200
|
+
' ' + (m.g2 ? 'index ja' : 'index nee') +
|
|
201
|
+
(m.gemeldTotaal != null ? ', totaal ' + m.gemeldTotaal : '')
|
|
202
|
+
)
|
|
203
|
+
await slaap(900)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log('')
|
|
207
|
+
const metIndex = uit.filter((m) => m.g2)
|
|
208
|
+
const metExt = uit.filter((m) => m.g1)
|
|
209
|
+
console.log(' ' + uit.filter((m) => m.g0).length + ' of ' + uit.length + ' answer /supported with a usable network and scheme')
|
|
210
|
+
console.log(' ' + metExt.length + ' of ' + uit.length + ' name any extension at all')
|
|
211
|
+
console.log(' ' + metIndex.length + ' of ' + uit.length + ' offer a discovery index that returns json')
|
|
212
|
+
for (const m of metIndex) {
|
|
213
|
+
if (m.concentratie?.aandeel != null) {
|
|
214
|
+
console.log(' ' + m.naam + ': largest host ' + (m.concentratie.aandeel * 100).toFixed(1) + '% of the FIRST PAGE')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (metIndex.some((m) => m.concentratie?.aandeel != null)) {
|
|
218
|
+
console.log('')
|
|
219
|
+
console.log(' Read that last number as a floor, not as the concentration. An index is not')
|
|
220
|
+
console.log(' served in random order, so the first page understates it: on payai the first')
|
|
221
|
+
console.log(' page gives 21.0% while a full census of the same index gave 51.8% for the')
|
|
222
|
+
console.log(' largest domain. Use x402-discovery-census.mjs when the number matters.')
|
|
223
|
+
}
|
|
224
|
+
console.log('')
|
|
225
|
+
console.log(' levels 1 to 4 are about what a gatekeeper publishes about itself. A facilitator')
|
|
226
|
+
console.log(' that settles fine but documents nothing scores low here, and that is the point:')
|
|
227
|
+
console.log(' a seller cannot choose what he cannot inspect.')
|
|
228
|
+
|
|
229
|
+
if (uitPad) {
|
|
230
|
+
const { writeFileSync, mkdirSync } = await import('node:fs')
|
|
231
|
+
const { dirname } = await import('node:path')
|
|
232
|
+
mkdirSync(dirname(uitPad), { recursive: true })
|
|
233
|
+
writeFileSync(uitPad, JSON.stringify({
|
|
234
|
+
bron: 'docs/dev-tools/facilitators.md in x402-foundation/x402, plus x402.org/facilitator',
|
|
235
|
+
controles: { positief: { g2: pos.g2 }, negatief: { trede: neg.trede }, geldig: posOk && negOk },
|
|
236
|
+
poortwachters: uit,
|
|
237
|
+
}, null, 2))
|
|
238
|
+
console.log('')
|
|
239
|
+
console.log('written: ' + uitPad)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Stageert de MCPB-bundel en BEWIJST dat hij werkt, voor er iets gepubliceerd wordt.
|
|
2
|
+
//
|
|
3
|
+
// WAAROM DIT EEN SCRIPT IS EN GEEN STUK YAML. Een controle die alleen in een
|
|
4
|
+
// workflow staat, is alleen op een server te draaien en dus niet na te kijken door
|
|
5
|
+
// wie hem moet vertrouwen. Dit bestand draait ook lokaal:
|
|
6
|
+
//
|
|
7
|
+
// node tools/bundel-bouw.mjs <doelmap>
|
|
8
|
+
//
|
|
9
|
+
// WAT HET BEWIJST, en dit is het hele punt. Een bundel is een losse kopie van de
|
|
10
|
+
// server. Wie hem samenstelt uit een lijst mappen, kan een bestand vergeten dat de
|
|
11
|
+
// repo wel heeft: dan werkt alles bij ons en gaat het pas stuk bij een vreemde die
|
|
12
|
+
// de bundel installeert. Daarom start dit script de server UIT de gestageerde map en
|
|
13
|
+
// voert een echte initialize plus tools/list uit over stdio. Faalt dat, dan is er
|
|
14
|
+
// niets om te publiceren.
|
|
15
|
+
//
|
|
16
|
+
// ethers zit er BEWUST niet in. Het is een optionele afhankelijkheid voor één
|
|
17
|
+
// controle binnen trede 2, en zonder dat pakket meldt die controle ONGEMETEN in
|
|
18
|
+
// plaats van te slagen. Een bundel van 184 kB die eerlijk zegt wat hij niet kon
|
|
19
|
+
// meten, is beter dan een bundel met een hele node_modules erin.
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
import { spawn } from 'node:child_process'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const wortel = fileURLToPath(new URL('..', import.meta.url))
|
|
27
|
+
|
|
28
|
+
// Alleen wat de server bij het draaien ECHT nodig heeft, plus de stukken die een
|
|
29
|
+
// mens moet kunnen lezen als hij de bundel binnenhaalt. vectors/ en test/ horen hier
|
|
30
|
+
// niet: die zijn er voor onze eigen bewijslast, niet voor het draaien.
|
|
31
|
+
export const INHOUD = {
|
|
32
|
+
bestanden: ['manifest.json', 'LICENSE', 'NOTICE', 'README.md', 'CONFORMANCE.md'],
|
|
33
|
+
mappen: ['src', 'profiles'],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function kopieerMap(van, naar) {
|
|
37
|
+
fs.mkdirSync(naar, { recursive: true })
|
|
38
|
+
for (const item of fs.readdirSync(van, { withFileTypes: true })) {
|
|
39
|
+
const a = path.join(van, item.name), b = path.join(naar, item.name)
|
|
40
|
+
if (item.isDirectory()) kopieerMap(a, b)
|
|
41
|
+
else fs.copyFileSync(a, b)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function stageer(doel) {
|
|
46
|
+
fs.rmSync(doel, { recursive: true, force: true })
|
|
47
|
+
fs.mkdirSync(doel, { recursive: true })
|
|
48
|
+
for (const f of INHOUD.bestanden) {
|
|
49
|
+
const bron = path.join(wortel, f)
|
|
50
|
+
if (!fs.existsSync(bron)) throw new Error(`bundel mist een bestand dat de repo hoort te hebben: ${f}`)
|
|
51
|
+
fs.copyFileSync(bron, path.join(doel, f))
|
|
52
|
+
}
|
|
53
|
+
for (const m of INHOUD.mappen) {
|
|
54
|
+
const bron = path.join(wortel, m)
|
|
55
|
+
if (!fs.existsSync(bron)) throw new Error(`bundel mist een map die de repo hoort te hebben: ${m}`)
|
|
56
|
+
kopieerMap(bron, path.join(doel, m))
|
|
57
|
+
}
|
|
58
|
+
return doel
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** De drie versienummers moeten gelijk zijn, anders publiceren wij een versie die niet bestaat. */
|
|
62
|
+
export function versiesKloppen(leesJson) {
|
|
63
|
+
// De lezer is een parameter zodat een test de rode kant kan aantonen zonder
|
|
64
|
+
// bestanden in de repo te verminken. Een controle die alleen groen kan worden,
|
|
65
|
+
// bewijst niets.
|
|
66
|
+
const lees = leesJson || ((f) => JSON.parse(fs.readFileSync(path.join(wortel, f), 'utf8')))
|
|
67
|
+
const pkg = lees('package.json').version
|
|
68
|
+
const man = lees('manifest.json').version
|
|
69
|
+
const srv = lees('server.json').version
|
|
70
|
+
const klachten = []
|
|
71
|
+
if (man !== pkg) klachten.push(`manifest.json zegt ${man}, package.json zegt ${pkg}`)
|
|
72
|
+
if (srv !== pkg) klachten.push(`server.json zegt ${srv}, package.json zegt ${pkg}`)
|
|
73
|
+
return { versie: pkg, klachten }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Start de server uit de bundel en eis een echte handdruk plus een echte toollijst. */
|
|
77
|
+
export async function bewijsDatDeBundelWerkt(doel, { wachtMs = 2500 } = {}) {
|
|
78
|
+
const entry = path.join(doel, JSON.parse(fs.readFileSync(path.join(doel, 'manifest.json'), 'utf8')).server.entry_point)
|
|
79
|
+
if (!fs.existsSync(entry)) throw new Error(`het manifest wijst naar een entry_point die niet in de bundel zit: ${entry}`)
|
|
80
|
+
|
|
81
|
+
const kind = spawn(process.execPath, [entry], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
82
|
+
let uit = '', err = ''
|
|
83
|
+
kind.stdout.on('data', (d) => { uit += d })
|
|
84
|
+
kind.stderr.on('data', (d) => { err += d })
|
|
85
|
+
const stuur = (o) => kind.stdin.write(JSON.stringify(o) + '\n')
|
|
86
|
+
|
|
87
|
+
stuur({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'bundel-bouw', version: '1' } } })
|
|
88
|
+
await new Promise((r) => setTimeout(r, 700))
|
|
89
|
+
stuur({ jsonrpc: '2.0', id: 2, method: 'tools/list' })
|
|
90
|
+
await new Promise((r) => setTimeout(r, wachtMs))
|
|
91
|
+
kind.kill()
|
|
92
|
+
|
|
93
|
+
const frames = uit.trim().split('\n').filter(Boolean).map((r) => { try { return JSON.parse(r) } catch { return null } }).filter(Boolean)
|
|
94
|
+
const init = frames.find((f) => f.id === 1)
|
|
95
|
+
const lijst = frames.find((f) => f.id === 2)
|
|
96
|
+
if (!init?.result?.serverInfo?.name) throw new Error('de bundel gaf geen bruikbaar initialize-antwoord. stderr: ' + err.slice(0, 400))
|
|
97
|
+
if (!Array.isArray(lijst?.result?.tools) || lijst.result.tools.length === 0) throw new Error('de bundel gaf geen tools terug')
|
|
98
|
+
// Alles wat geen protocolframe is hoort naar stderr. Een regel op stdout die geen
|
|
99
|
+
// JSON-RPC is, breekt elke client, en dat moet hier opvallen en niet bij een vreemde.
|
|
100
|
+
const nietFrames = uit.trim().split('\n').filter((r) => r.trim()).filter((r) => { try { JSON.parse(r); return false } catch { return true } })
|
|
101
|
+
if (nietFrames.length) throw new Error('er staat iets op stdout dat geen frame is: ' + nietFrames[0].slice(0, 120))
|
|
102
|
+
|
|
103
|
+
return { naam: init.result.serverInfo.name, versie: init.result.serverInfo.version, protocol: init.result.protocolVersion, tools: lijst.result.tools.map((t) => t.name) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (process.argv[1]?.endsWith('bundel-bouw.mjs')) {
|
|
107
|
+
const doel = process.argv[2]
|
|
108
|
+
if (!doel) {
|
|
109
|
+
console.log('gebruik: node tools/bundel-bouw.mjs <doelmap>')
|
|
110
|
+
process.exitCode = 2
|
|
111
|
+
} else {
|
|
112
|
+
const v = versiesKloppen()
|
|
113
|
+
if (v.klachten.length) {
|
|
114
|
+
console.log('versies lopen uiteen, dit stopt hier:')
|
|
115
|
+
for (const k of v.klachten) console.log(' ' + k)
|
|
116
|
+
process.exitCode = 1
|
|
117
|
+
} else {
|
|
118
|
+
stageer(doel)
|
|
119
|
+
const n = fs.readdirSync(doel, { recursive: true }).length
|
|
120
|
+
const uitslag = await bewijsDatDeBundelWerkt(doel)
|
|
121
|
+
console.log(`versie ${v.versie}, ${n} items gestageerd in ${doel}`)
|
|
122
|
+
console.log(`de bundel spreekt MCP: ${uitslag.naam} v${uitslag.versie}, protocol ${uitslag.protocol}`)
|
|
123
|
+
console.log(`tools: ${uitslag.tools.join(', ')}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Counterparty-countersigned feedback: signed vectors
|
|
2
|
+
|
|
3
|
+
INAM grounds a rating in a **counterparty countersignature over the work**: the provider signs a receipt naming `task.specHash` and `result.outputHash` (both `sha256:` of the actual bytes), then the requester signs the same canonical content ([SPEC §4](../../SPEC.md#4-execution-receipt)). Over an HTTP call, both hashes are derived from the request and response bytes ([§15](../../SPEC.md#15-receipts-over-http-v041)). The finalized receipt goes into ERC-8004 `giveFeedback` as in [§11.1](../../SPEC.md#111-inam-and-erc-8004). It needs no payment to be visible on-chain, so it composes with `x402-settlement` instead of replacing it.
|
|
4
|
+
|
|
5
|
+
These vectors are offered for the grounding conformance set discussed in [erc-8004-contracts#99](https://github.com/erc-8004/erc-8004-contracts/issues/99) and [x402#3772](https://github.com/x402-foundation/x402/issues/3772).
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
node interop/countersigned/verify.mjs # Node built-ins only, no INAM server, no npm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Cases (`vectors.json`)
|
|
12
|
+
|
|
13
|
+
| Case | What changes | Must be rejected by |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `POSITIVE` | nothing: genuine feedback, right wallet, hashes match the observed bytes | accepted |
|
|
16
|
+
| `WRONG-WALLET-COPY` | the genuine file, byte for byte, sent from another wallet | `fileClientAddressMatchesSender`, `senderIsRequesterWallet` |
|
|
17
|
+
| `TAMPERED-OUTPUT-HASH` | `result.outputHash` edited in the carried receipt; `feedbackHash` recomputed over the edited file | `receiptIdRecomputes`, both signatures, `outputHashMatchesObservedBytes` |
|
|
18
|
+
| `OUTPUT-NOT-WHAT-WAS-SERVED` | both signatures valid, but the provider's `outputHash` commits to bytes the requester never received | `outputHashMatchesObservedBytes` |
|
|
19
|
+
|
|
20
|
+
The last case is the one the countersignature rule exists for. A requester that recomputes both hashes from the bytes it observed (SPEC §15 step 3) refuses to sign; a requester that signed anyway produces a record every signature check accepts and only the binding check catches. That recomputation is a verification step, not a second derivation of the interaction's identity: nothing is re-derived, the requester compares its own bytes against the value the provider derived.
|
|
21
|
+
|
|
22
|
+
## What each check does
|
|
23
|
+
|
|
24
|
+
`feedbackHashMatchesFile` keccak256 of the file bytes equals the event's `feedbackHash`. `receiptIdRecomputes` content-addressing (§4.2). `workerSignature` / `requesterSignature` Ed25519 over the same canonical content (§4.3). `valueMatchesOutcome` 100 / 50 / 0 for success / partial / failed. `fileClientAddressMatchesSender` the file names the address that sent the event. `senderIsRequesterWallet` that address is the `linked.erc8004_id` the requester's INAM ID proved control of (here the fixture's stand-in for the registry lookup). `specHashMatchesObservedBytes` / `outputHashMatchesObservedBytes` the hashes equal `sha256` of the request (`METHOD path?query\n` + body) and response bytes in `observed`.
|
|
25
|
+
|
|
26
|
+
`verify.mjs` includes a small Keccak-256 because Node ships none; `tests/countersignedInterop.test.ts` checks it against the SDK's.
|
|
27
|
+
|
|
28
|
+
## Files
|
|
29
|
+
|
|
30
|
+
| File | What it is |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `vectors.json` | The four cases, the observed request/response bytes and the parties. |
|
|
33
|
+
| `verify.mjs` | The independent check. Prints PASS/FAIL per case. |
|
|
34
|
+
| `generate.ts` | Rebuilds `vectors.json` byte for byte with the INAM SDK (`npx tsx interop/countersigned/generate.ts`). |
|
|
35
|
+
|
|
36
|
+
**Test keys.** Both DIDs come from private keys derived from public strings in `generate.ts`; anyone can sign as them. The EVM addresses are made up and are not derived from any key. They exist only for these fixtures.
|
|
37
|
+
|
|
38
|
+
**Not covered.** The registry's view of the receipt (status `finalized`, not disputed) is outside these files, and so is checking that the ERC-8004 `agentId` belongs to the provider; both need live state ([§11.1](../../SPEC.md#111-inam-and-erc-8004) step 4). INAM's registry is run by its maintainers, so these vectors test the format and the checks, not that anyone should trust the operator.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Canonical JSON vectors with non-ASCII and non-BMP content; reference: sdk-js/src/crypto/canonical.ts",
|
|
3
|
+
"vectors": [
|
|
4
|
+
{
|
|
5
|
+
"name": "astral-key-sorts-before-U+FFFF",
|
|
6
|
+
"input": {
|
|
7
|
+
"": 1,
|
|
8
|
+
"😀": 2
|
|
9
|
+
},
|
|
10
|
+
"canonical": "{\"😀\":2,\"\":1}",
|
|
11
|
+
"utf8Sha256": "48c5d098713870a7b88be7fc0ffc0a79e912d20f80ea8a4a5604426f49add103"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "astral-key-sorts-before-U+E000",
|
|
15
|
+
"input": {
|
|
16
|
+
"": "private-use",
|
|
17
|
+
"𐀀": "linear-b",
|
|
18
|
+
"a": 0
|
|
19
|
+
},
|
|
20
|
+
"canonical": "{\"a\":0,\"𐀀\":\"linear-b\",\"\":\"private-use\"}",
|
|
21
|
+
"utf8Sha256": "5b26b7dcd5b746fc5f2d1f8e58c70b7bb9abfabff1fbde2a55936ce5965b79be"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "astral-keys-among-bmp-keys",
|
|
25
|
+
"input": {
|
|
26
|
+
"z": 1,
|
|
27
|
+
"𝄞": 2,
|
|
28
|
+
"é": 3,
|
|
29
|
+
"日本": 4,
|
|
30
|
+
"A": 5,
|
|
31
|
+
"A": 6
|
|
32
|
+
},
|
|
33
|
+
"canonical": "{\"A\":5,\"z\":1,\"é\":3,\"日本\":4,\"𝄞\":2,\"A\":6}",
|
|
34
|
+
"utf8Sha256": "b6b7a2a1095bb9a8470e26cc35264d2669255d6abde0d490d4fd2bf691f347dd"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "astral-key-nested-in-array",
|
|
38
|
+
"input": {
|
|
39
|
+
"list": [
|
|
40
|
+
{
|
|
41
|
+
"�": 1,
|
|
42
|
+
"🚀": 2
|
|
43
|
+
},
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
"𠀀": "cjk-ext-b",
|
|
47
|
+
"": "x"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"canonical": "{\"list\":[{\"🚀\":2,\"�\":1},[{\"𠀀\":\"cjk-ext-b\",\"\":\"x\"}]]}",
|
|
53
|
+
"utf8Sha256": "db5689390c763ec36522bbb1798d569f22b03ca010eb2be9408ec3f6375bdda1"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "emoji-values",
|
|
57
|
+
"input": {
|
|
58
|
+
"a": "😀",
|
|
59
|
+
"b": "👨👩👧",
|
|
60
|
+
"c": "🇹🇷",
|
|
61
|
+
"d": "𝄞 clef"
|
|
62
|
+
},
|
|
63
|
+
"canonical": "{\"a\":\"😀\",\"b\":\"👨👩👧\",\"c\":\"🇹🇷\",\"d\":\"𝄞 clef\"}",
|
|
64
|
+
"utf8Sha256": "8907f06176aa2941e64fec7710d8505188d48d63f2d4e7c32126e8c601c3dc03"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "bmp-non-ascii",
|
|
68
|
+
"input": {
|
|
69
|
+
"name": "Café",
|
|
70
|
+
"jp": "日本語",
|
|
71
|
+
"tr": "ğüşıöç İ"
|
|
72
|
+
},
|
|
73
|
+
"canonical": "{\"jp\":\"日本語\",\"name\":\"Café\",\"tr\":\"ğüşıöç İ\"}",
|
|
74
|
+
"utf8Sha256": "7482b59d09447b1fde215ad8a7bf2e41678a4bf35b9a80a41003e3e4d2e8cca9"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "no-unicode-normalization",
|
|
78
|
+
"input": {
|
|
79
|
+
"nfc": "é",
|
|
80
|
+
"nfd": "é"
|
|
81
|
+
},
|
|
82
|
+
"canonical": "{\"nfc\":\"é\",\"nfd\":\"é\"}",
|
|
83
|
+
"utf8Sha256": "79eae438d105e852f86ea598ffdda91643386fad0b4f336b941528d30fbf1558"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "lone-surrogates-escaped",
|
|
87
|
+
"input": {
|
|
88
|
+
"hi": "\ud800",
|
|
89
|
+
"lo": "\udc00",
|
|
90
|
+
"rev": "\udc00\ud800",
|
|
91
|
+
"\ud83d": "key"
|
|
92
|
+
},
|
|
93
|
+
"canonical": "{\"hi\":\"\\ud800\",\"lo\":\"\\udc00\",\"rev\":\"\\udc00\\ud800\",\"\\ud83d\":\"key\"}",
|
|
94
|
+
"utf8Sha256": "450ec9ac72567a72d99cddf0e97bad0c9952a1bed75ce16b802f435276c0a087"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "control-and-special-chars",
|
|
98
|
+
"input": {
|
|
99
|
+
"c": "\u0000\b\t\n\f\r\u001f",
|
|
100
|
+
"q": "\"/",
|
|
101
|
+
"del": "",
|
|
102
|
+
"ls": "
"
|
|
103
|
+
},
|
|
104
|
+
"canonical": "{\"c\":\"\\u0000\\b\\t\\n\\f\\r\\u001f\",\"del\":\"\",\"ls\":\"
\",\"q\":\"\\\"/\"}",
|
|
105
|
+
"utf8Sha256": "b230efe90e496aac442bf66f13ba37036df1b102b9a8a723e794722eb95d1600"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "key-order-digits-case-accents",
|
|
109
|
+
"input": {
|
|
110
|
+
"9": 4,
|
|
111
|
+
"10": 3,
|
|
112
|
+
"b": 1,
|
|
113
|
+
"B": 2,
|
|
114
|
+
"é": 5,
|
|
115
|
+
"e": 6,
|
|
116
|
+
"": 7
|
|
117
|
+
},
|
|
118
|
+
"canonical": "{\"\":7,\"10\":3,\"9\":4,\"B\":2,\"b\":1,\"e\":6,\"é\":5}",
|
|
119
|
+
"utf8Sha256": "ca65da7e132c4e02d1b747eee06e008c2f6442b731afe16d04d993109188ad22"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"suite": "inam-countersigned-grounding-v0.1",
|
|
3
|
+
"method": "counterparty-countersigned",
|
|
4
|
+
"note": "Test keys and made-up addresses only. Checks: feedbackHash, receiptId, both signatures, value, sender, specHash/outputHash against the bytes the requester observed.",
|
|
5
|
+
"parties": {
|
|
6
|
+
"requester": {
|
|
7
|
+
"did": "did:key:z6Mkkrq4oHrSaU8E2ZctUreJkiXyrVbPRM4FZgPxYx3X8XCS",
|
|
8
|
+
"erc8004Address": "0x1111111111111111111111111111111111111111"
|
|
9
|
+
},
|
|
10
|
+
"provider": {
|
|
11
|
+
"did": "did:key:z6MkenP9pSJt69Re5GYC3HeA6JC4BbFSATBbVkjx3TVbE4LL",
|
|
12
|
+
"erc8004Address": "0x2222222222222222222222222222222222222222"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"observed": {
|
|
16
|
+
"request": {
|
|
17
|
+
"method": "POST",
|
|
18
|
+
"url": "https://worker.example/translate",
|
|
19
|
+
"body": "{\"text\":\"merhaba dünya\",\"to\":\"en\"}"
|
|
20
|
+
},
|
|
21
|
+
"response": {
|
|
22
|
+
"status": 200,
|
|
23
|
+
"body": "{\"text\":\"hello world\"}"
|
|
24
|
+
},
|
|
25
|
+
"expectedSpecHash": "sha256:423007c3904c15d5f43b0485ff3e442b0003925cb283ac7b58c16634429bce20",
|
|
26
|
+
"expectedOutputHash": "sha256:e344fa3b83cf2b142293de2a4e56c6b211d4a157f8adb3c954c51ab67f41a46a"
|
|
27
|
+
},
|
|
28
|
+
"cases": [
|
|
29
|
+
{
|
|
30
|
+
"id": "POSITIVE",
|
|
31
|
+
"description": "Genuine feedback: sent from the requester's proven wallet, both signatures valid, hashes match the observed bytes.",
|
|
32
|
+
"feedbackFile": "{\"agentId\":7,\"agentRegistry\":\"eip155:8453:0x8004000000000000000000000000000000000000\",\"clientAddress\":\"eip155:8453:0x1111111111111111111111111111111111111111\",\"createdAt\":\"2026-10-03T00:00:00.000Z\",\"inam\":{\"receipt\":{\"agentA\":{\"id\":\"did:key:z6Mkkrq4oHrSaU8E2ZctUreJkiXyrVbPRM4FZgPxYx3X8XCS\",\"role\":\"requester\"},\"agentB\":{\"id\":\"did:key:z6MkenP9pSJt69Re5GYC3HeA6JC4BbFSATBbVkjx3TVbE4LL\",\"role\":\"worker\"},\"jobId\":\"http:countersigned-vectors-v0.1\",\"receiptId\":\"sha256:c4b80bbc9472979127dca9491a729396514bc9b6c36391b79ba9419fbb2828c3\",\"receiptVersion\":\"1.0\",\"result\":{\"completedAt\":\"2026-10-01T10:00:01.000Z\",\"outputHash\":\"sha256:e344fa3b83cf2b142293de2a4e56c6b211d4a157f8adb3c954c51ab67f41a46a\"},\"signatures\":{\"agentA\":\"mjXSDW6FMCAL5wBv0ASZ4BR7Dst7KXNMtQF1mrPaApJw0QCzuwWHPsuQqEkVjb4LOTvGfmJzaa6QtK/QLbKTCQ==\",\"agentB\":\"wDDUXpemxNcVEVQfH7kaTZr2rJk/zzLNNpAkx3a70UjyrGK9OPMAX/dhgxsVneGCse/jXV2s6F+n5WLFDqYwBQ==\"},\"task\":{\"capability\":\"translation.tr-en\",\"createdAt\":\"2026-10-01T10:00:00.000Z\",\"specHash\":\"sha256:423007c3904c15d5f43b0485ff3e442b0003925cb283ac7b58c16634429bce20\"},\"verification\":{\"method\":\"payer_confirmation\",\"outcome\":\"success\"}}},\"tag1\":\"inam-receipt\",\"tag2\":\"translation.tr-en\",\"value\":100,\"valueDecimals\":0}",
|
|
33
|
+
"onchainEvent": {
|
|
34
|
+
"clientAddress": "0x1111111111111111111111111111111111111111",
|
|
35
|
+
"feedbackHash": "0x9d7bb681bc76d26fe927d690d0042a1c3176dbf3b4831d498c99f1e3a39ecfe6",
|
|
36
|
+
"value": 100
|
|
37
|
+
},
|
|
38
|
+
"expected": {
|
|
39
|
+
"accepted": true,
|
|
40
|
+
"failedChecks": []
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "WRONG-WALLET-COPY",
|
|
45
|
+
"description": "The genuine file, byte for byte, sent from a wallet that is not the requester's proven erc8004_id (the Sybil copy).",
|
|
46
|
+
"feedbackFile": "{\"agentId\":7,\"agentRegistry\":\"eip155:8453:0x8004000000000000000000000000000000000000\",\"clientAddress\":\"eip155:8453:0x1111111111111111111111111111111111111111\",\"createdAt\":\"2026-10-03T00:00:00.000Z\",\"inam\":{\"receipt\":{\"agentA\":{\"id\":\"did:key:z6Mkkrq4oHrSaU8E2ZctUreJkiXyrVbPRM4FZgPxYx3X8XCS\",\"role\":\"requester\"},\"agentB\":{\"id\":\"did:key:z6MkenP9pSJt69Re5GYC3HeA6JC4BbFSATBbVkjx3TVbE4LL\",\"role\":\"worker\"},\"jobId\":\"http:countersigned-vectors-v0.1\",\"receiptId\":\"sha256:c4b80bbc9472979127dca9491a729396514bc9b6c36391b79ba9419fbb2828c3\",\"receiptVersion\":\"1.0\",\"result\":{\"completedAt\":\"2026-10-01T10:00:01.000Z\",\"outputHash\":\"sha256:e344fa3b83cf2b142293de2a4e56c6b211d4a157f8adb3c954c51ab67f41a46a\"},\"signatures\":{\"agentA\":\"mjXSDW6FMCAL5wBv0ASZ4BR7Dst7KXNMtQF1mrPaApJw0QCzuwWHPsuQqEkVjb4LOTvGfmJzaa6QtK/QLbKTCQ==\",\"agentB\":\"wDDUXpemxNcVEVQfH7kaTZr2rJk/zzLNNpAkx3a70UjyrGK9OPMAX/dhgxsVneGCse/jXV2s6F+n5WLFDqYwBQ==\"},\"task\":{\"capability\":\"translation.tr-en\",\"createdAt\":\"2026-10-01T10:00:00.000Z\",\"specHash\":\"sha256:423007c3904c15d5f43b0485ff3e442b0003925cb283ac7b58c16634429bce20\"},\"verification\":{\"method\":\"payer_confirmation\",\"outcome\":\"success\"}}},\"tag1\":\"inam-receipt\",\"tag2\":\"translation.tr-en\",\"value\":100,\"valueDecimals\":0}",
|
|
47
|
+
"onchainEvent": {
|
|
48
|
+
"clientAddress": "0x3333333333333333333333333333333333333333",
|
|
49
|
+
"feedbackHash": "0x9d7bb681bc76d26fe927d690d0042a1c3176dbf3b4831d498c99f1e3a39ecfe6",
|
|
50
|
+
"value": 100
|
|
51
|
+
},
|
|
52
|
+
"expected": {
|
|
53
|
+
"accepted": false,
|
|
54
|
+
"failedChecks": [
|
|
55
|
+
"fileClientAddressMatchesSender",
|
|
56
|
+
"senderIsRequesterWallet"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "TAMPERED-OUTPUT-HASH",
|
|
62
|
+
"description": "result.outputHash edited inside the carried receipt; feedbackHash recomputed over the edited file, sent from the right wallet.",
|
|
63
|
+
"feedbackFile": "{\"agentId\":7,\"agentRegistry\":\"eip155:8453:0x8004000000000000000000000000000000000000\",\"clientAddress\":\"eip155:8453:0x1111111111111111111111111111111111111111\",\"createdAt\":\"2026-10-03T00:00:00.000Z\",\"inam\":{\"receipt\":{\"agentA\":{\"id\":\"did:key:z6Mkkrq4oHrSaU8E2ZctUreJkiXyrVbPRM4FZgPxYx3X8XCS\",\"role\":\"requester\"},\"agentB\":{\"id\":\"did:key:z6MkenP9pSJt69Re5GYC3HeA6JC4BbFSATBbVkjx3TVbE4LL\",\"role\":\"worker\"},\"jobId\":\"http:countersigned-vectors-v0.1\",\"receiptId\":\"sha256:c4b80bbc9472979127dca9491a729396514bc9b6c36391b79ba9419fbb2828c3\",\"receiptVersion\":\"1.0\",\"result\":{\"completedAt\":\"2026-10-01T10:00:01.000Z\",\"outputHash\":\"sha256:2910e9a528b169d433bad2f53171e547726ca717868e29362ec178cfb58b97cb\"},\"signatures\":{\"agentA\":\"mjXSDW6FMCAL5wBv0ASZ4BR7Dst7KXNMtQF1mrPaApJw0QCzuwWHPsuQqEkVjb4LOTvGfmJzaa6QtK/QLbKTCQ==\",\"agentB\":\"wDDUXpemxNcVEVQfH7kaTZr2rJk/zzLNNpAkx3a70UjyrGK9OPMAX/dhgxsVneGCse/jXV2s6F+n5WLFDqYwBQ==\"},\"task\":{\"capability\":\"translation.tr-en\",\"createdAt\":\"2026-10-01T10:00:00.000Z\",\"specHash\":\"sha256:423007c3904c15d5f43b0485ff3e442b0003925cb283ac7b58c16634429bce20\"},\"verification\":{\"method\":\"payer_confirmation\",\"outcome\":\"success\"}}},\"tag1\":\"inam-receipt\",\"tag2\":\"translation.tr-en\",\"value\":100,\"valueDecimals\":0}",
|
|
64
|
+
"onchainEvent": {
|
|
65
|
+
"clientAddress": "0x1111111111111111111111111111111111111111",
|
|
66
|
+
"feedbackHash": "0x7feec35c27e69bccc6da108795931a25b2acbd49bd46dae7787fede41f3eadf0",
|
|
67
|
+
"value": 100
|
|
68
|
+
},
|
|
69
|
+
"expected": {
|
|
70
|
+
"accepted": false,
|
|
71
|
+
"failedChecks": [
|
|
72
|
+
"receiptIdRecomputes",
|
|
73
|
+
"workerSignature",
|
|
74
|
+
"requesterSignature",
|
|
75
|
+
"outputHashMatchesObservedBytes"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "OUTPUT-NOT-WHAT-WAS-SERVED",
|
|
81
|
+
"description": "Both signatures are valid, but the provider's outputHash commits to different bytes than the requester received. A requester that recomputes (SPEC §15 step 3) refuses to countersign this.",
|
|
82
|
+
"feedbackFile": "{\"agentId\":7,\"agentRegistry\":\"eip155:8453:0x8004000000000000000000000000000000000000\",\"clientAddress\":\"eip155:8453:0x1111111111111111111111111111111111111111\",\"createdAt\":\"2026-10-03T00:00:00.000Z\",\"inam\":{\"receipt\":{\"agentA\":{\"id\":\"did:key:z6Mkkrq4oHrSaU8E2ZctUreJkiXyrVbPRM4FZgPxYx3X8XCS\",\"role\":\"requester\"},\"agentB\":{\"id\":\"did:key:z6MkenP9pSJt69Re5GYC3HeA6JC4BbFSATBbVkjx3TVbE4LL\",\"role\":\"worker\"},\"jobId\":\"http:countersigned-vectors-v0.1\",\"receiptId\":\"sha256:51968202116a120715be7387e83f03d26ea3c1281d63110a148159e4d4c7cf90\",\"receiptVersion\":\"1.0\",\"result\":{\"completedAt\":\"2026-10-01T10:00:01.000Z\",\"outputHash\":\"sha256:2910e9a528b169d433bad2f53171e547726ca717868e29362ec178cfb58b97cb\"},\"signatures\":{\"agentA\":\"+fXRHCXTDOA+tUqARcBXm2cn8qkIeD+ByCQXPOTC7Nm2LOIc2mmmWQy0UKLcTpRUbvHaNv9mc3Lmz1IzsKUOBA==\",\"agentB\":\"P46kDb36mIo7D4HFeLR/DrVlKdwWfQdZHwnq2y4XmqjE4udQLFI7yNwkFMvgxCspXZL12pGX3ajp11bnreIMDQ==\"},\"task\":{\"capability\":\"translation.tr-en\",\"createdAt\":\"2026-10-01T10:00:00.000Z\",\"specHash\":\"sha256:423007c3904c15d5f43b0485ff3e442b0003925cb283ac7b58c16634429bce20\"},\"verification\":{\"method\":\"payer_confirmation\",\"outcome\":\"success\"}}},\"tag1\":\"inam-receipt\",\"tag2\":\"translation.tr-en\",\"value\":100,\"valueDecimals\":0}",
|
|
83
|
+
"onchainEvent": {
|
|
84
|
+
"clientAddress": "0x1111111111111111111111111111111111111111",
|
|
85
|
+
"feedbackHash": "0x81d5ed166f4c3fa4d7bb918240fae486c92299077fb00494ec65928a0b967770",
|
|
86
|
+
"value": 100
|
|
87
|
+
},
|
|
88
|
+
"expected": {
|
|
89
|
+
"accepted": false,
|
|
90
|
+
"failedChecks": [
|
|
91
|
+
"outputHashMatchesObservedBytes"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|