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.
Files changed (37) hide show
  1. package/CONFORMANCE.md +110 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +39 -0
  4. package/README.md +256 -2
  5. package/bin/cli.mjs +89 -0
  6. package/examples/receipt-live-base-sepolia-2.json +21 -0
  7. package/examples/receipt-live-base-sepolia-3.json +21 -0
  8. package/examples/receipt-live-base-sepolia.json +21 -0
  9. package/package.json +59 -4
  10. package/profiles/profile.schema.json +115 -0
  11. package/profiles/x402-deployed-ladder-v1.json +178 -0
  12. package/src/eip3009-autorisatie.mjs +195 -0
  13. package/src/inam-countersigned.mjs +162 -0
  14. package/src/mcp-server.mjs +262 -0
  15. package/src/profiel.mjs +145 -0
  16. package/src/x402-conformance-check.mjs +194 -0
  17. package/src/x402-conformance-treden.mjs +246 -0
  18. package/src/x402-discovery-census.mjs +247 -0
  19. package/src/x402-index-liveness.mjs +201 -0
  20. package/src/x402-link-onchain.mjs +280 -0
  21. package/src/x402-metingen-bundel.mjs +96 -0
  22. package/src/x402-offer-receipt.mjs +84 -0
  23. package/src/x402-poortwachters.mjs +241 -0
  24. package/tools/bundel-bouw.mjs +126 -0
  25. package/vectors/inam/BRON-README.md +38 -0
  26. package/vectors/inam/canonical-vectors.json +122 -0
  27. package/vectors/inam/vectors.json +96 -0
  28. package/vectors/kanon/amount-insufficient-001.json +52 -0
  29. package/vectors/kanon/cross-chain-replay-001.json +54 -0
  30. package/vectors/kanon/cross-contract-replay-001.json +54 -0
  31. package/vectors/kanon/domain-name-mismatch-001.json +53 -0
  32. package/vectors/kanon/expired-001.json +51 -0
  33. package/vectors/kanon/network-mismatch-001.json +52 -0
  34. package/vectors/kanon/nonce-replay-001.json +56 -0
  35. package/vectors/kanon/not-yet-valid-001.json +51 -0
  36. package/vectors/kanon/sig-malleable-high-s-001.json +54 -0
  37. package/vectors/kanon/valid-baseline-001.json +52 -0
@@ -0,0 +1,247 @@
1
+ // Census of the DECLARED layer of one x402 discovery index, counted per host.
2
+ //
3
+ // WAAROM DIT APART STAAT VAN x402-conformance-check.mjs. Dat bestand meet een
4
+ // eindpunt LIVE: het stuurt een verzoek en leest wat er terugkomt. Dit bestand
5
+ // raakt geen enkel eindpunt van iemand anders aan. Het leest alleen wat partijen
6
+ // ZELF in een discovery-index hebben laten registreren. Dat zijn twee
7
+ // verschillende beweringen en ze mogen nooit tot een cijfer samengeknepen worden:
8
+ //
9
+ // gedeclareerd = wat in de index staat, door de partij zelf aangemeld
10
+ // live = wat het eindpunt vandaag werkelijk antwoordt
11
+ //
12
+ // Een census van de eerste is goedkoop en volledig. Een meting van de tweede kost
13
+ // een verzoek aan iemand anders en kan daarom alleen op een steekproef. Daarom
14
+ // deze volgorde: eerst de census, en de census bepaalt waar de live-steekproef
15
+ // naar kijkt. Een willekeurige steekproef trekken voor je weet wat er te vinden
16
+ // is, is verzoeken verspillen aan andermans server.
17
+ //
18
+ // DE EENHEID IS DE HOST, NIET DE RESOURCE. Een partij met driehonderd paden zou
19
+ // anders driehonderd keer zo zwaar wegen als een partij met een pad. Zie
20
+ // CONFORMANCE.md; dit bestand telt op dezelfde eenheid.
21
+ //
22
+ // WAT DIT NIET IS, en dit hoort in elke uitslag die eruit komt:
23
+ // - geen ecosysteem-meting. Het is EEN index, op EEN dag.
24
+ // - host is niet hetzelfde als operator. Twee subdomeinen van hetzelfde bedrijf
25
+ // tellen hier als twee. Dat is een bekende grens, niet stilzwijgend opgelost.
26
+ // - een registratie zegt niets over of het eindpunt vandaag nog leeft.
27
+
28
+ const UA = 'x402-conformance-check/1 (read-only, github.com/holistis/agent-receipt-verify)'
29
+ const INDEX = 'https://facilitator.payai.network/discovery/resources'
30
+ const PAGINA = 1000
31
+
32
+ const slaap = (ms) => new Promise((r) => setTimeout(r, ms))
33
+
34
+ /**
35
+ * De door de partij zelf gedeclareerde invoer: padparameters en voorbeeldlichaam.
36
+ *
37
+ * Toegevoegd 2026-10-17, nadat de eerste editie bleek dat de index SJABLOON-URL's
38
+ * bevat (".../v1/ip/:ip") en dat wij die letterlijk opvroegen. De index levert de
39
+ * waarde ernaast; wij gebruikten hem niet. Dat gaf een 400 die niets over x402 zei.
40
+ * Dit veld staat er zodat een volgende meting dat niet overdoet.
41
+ */
42
+ export function gedeclareerdeInvoer(item) {
43
+ const ext = item?.extensions ?? {}
44
+ for (const k of Object.keys(ext)) {
45
+ const inp = ext[k]?.info?.input
46
+ if (inp && typeof inp === 'object') {
47
+ return {
48
+ pathParams: inp.pathParams ?? null,
49
+ queryParams: inp.queryParams ?? null,
50
+ body: inp.body ?? null,
51
+ }
52
+ }
53
+ }
54
+ return { pathParams: null, queryParams: null, body: null }
55
+ }
56
+
57
+ /** De door de partij zelf gedeclareerde HTTP-methode, of null als hij niets zegt. */
58
+ export function gedeclareerdeMethode(item) {
59
+ const ext = item?.extensions ?? {}
60
+ for (const k of Object.keys(ext)) {
61
+ const m = ext[k]?.info?.input?.method
62
+ if (typeof m === 'string' && /^(GET|POST|PUT|PATCH|DELETE)$/i.test(m.trim())) {
63
+ return m.trim().toUpperCase()
64
+ }
65
+ }
66
+ return null
67
+ }
68
+
69
+ function host(url) {
70
+ try {
71
+ return new URL(url).host.toLowerCase()
72
+ } catch {
73
+ return null
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Haalt de hele index op, pagina voor pagina.
79
+ *
80
+ * Een pagina die faalt WERPT, in plaats van stil een korter resultaat te geven.
81
+ * Een census met een onbekend gat erin is geen census, en een halve uitslag die
82
+ * zich als heel voordoet is precies de fout die deze hele meetlat moet vermijden.
83
+ */
84
+ export async function haalIndex({ fetchImpl = fetch, pauzeMs = 800, maxPaginas = 60 } = {}) {
85
+ const items = []
86
+ let offset = 0
87
+ let gemeldTotaal = null
88
+ for (let p = 0; p < maxPaginas; p++) {
89
+ const url = INDEX + '?limit=' + PAGINA + '&offset=' + offset
90
+ const resp = await fetchImpl(url, {
91
+ headers: { 'User-Agent': UA, Accept: 'application/json' },
92
+ signal: AbortSignal.timeout(60000),
93
+ })
94
+ if (!resp.ok) throw new Error('index page at offset ' + offset + ' returned ' + resp.status)
95
+ const j = await resp.json()
96
+ const batch = Array.isArray(j.items) ? j.items : []
97
+ if (gemeldTotaal === null) gemeldTotaal = j.pagination?.total ?? null
98
+ // De echte lusindex, niet indexOf: twee registraties kunnen inhoudelijk gelijk
99
+ // zijn en dan geeft indexOf beide dezelfde positie. De positie bepaalt welke
100
+ // resource per host de live-steekproef in gaat, dus een verschuiving hier
101
+ // maakt de steekproef onnavolgbaar voor iemand die hem natrekt.
102
+ for (let i = 0; i < batch.length; i++) items.push({ item: batch[i], offset: offset + i })
103
+ offset += batch.length
104
+ if (batch.length < PAGINA) return { items, gemeldTotaal, paginas: p + 1 }
105
+ if (pauzeMs) await slaap(pauzeMs)
106
+ }
107
+ throw new Error('index did not end within ' + maxPaginas + ' pages')
108
+ }
109
+
110
+ /** Vouwt de ruwe index samen tot een telling per host. */
111
+ export function censusPerHost(ruw) {
112
+ const perHost = new Map()
113
+ let zonderHost = 0
114
+ const gezieneResources = new Set()
115
+ let dubbeleResources = 0
116
+
117
+ for (const { item, offset } of ruw) {
118
+ const res = item?.resource
119
+ const h = host(res)
120
+ if (!h) {
121
+ zonderHost++
122
+ continue
123
+ }
124
+ if (gezieneResources.has(res)) dubbeleResources++
125
+ else gezieneResources.add(res)
126
+
127
+ if (!perHost.has(h)) {
128
+ perHost.set(h, {
129
+ host: h,
130
+ aantalRegistraties: 0,
131
+ eersteOffset: offset,
132
+ eersteResource: res,
133
+ eersteMethode: gedeclareerdeMethode(item),
134
+ eersteInvoer: gedeclareerdeInvoer(item),
135
+ extensieSleutels: new Set(),
136
+ netwerken: new Set(),
137
+ schemes: new Set(),
138
+ x402Versies: new Set(),
139
+ declareertOfferReceipt: false,
140
+ })
141
+ }
142
+ const o = perHost.get(h)
143
+ o.aantalRegistraties++
144
+ // Eerste in index-volgorde is de deterministische keuze voor de live-meting,
145
+ // zodat iemand anders exact dezelfde steekproef kan natrekken.
146
+ if (offset < o.eersteOffset) {
147
+ o.eersteOffset = offset
148
+ o.eersteResource = res
149
+ o.eersteMethode = gedeclareerdeMethode(item)
150
+ o.eersteInvoer = gedeclareerdeInvoer(item)
151
+ }
152
+ for (const k of Object.keys(item?.extensions ?? {})) {
153
+ o.extensieSleutels.add(k)
154
+ if (/offer-?receipt/i.test(k)) o.declareertOfferReceipt = true
155
+ }
156
+ const a = Array.isArray(item?.accepts) ? item.accepts[0] : null
157
+ if (a?.network) o.netwerken.add(String(a.network))
158
+ if (a?.scheme) o.schemes.add(String(a.scheme))
159
+ if (item?.x402Version != null) o.x402Versies.add(String(item.x402Version))
160
+ }
161
+
162
+ const operators = [...perHost.values()]
163
+ .map((o) => ({
164
+ ...o,
165
+ extensieSleutels: [...o.extensieSleutels].sort(),
166
+ netwerken: [...o.netwerken].sort(),
167
+ schemes: [...o.schemes].sort(),
168
+ x402Versies: [...o.x402Versies].sort(),
169
+ }))
170
+ .sort((a, b) => b.aantalRegistraties - a.aantalRegistraties || a.host.localeCompare(b.host))
171
+
172
+ return { operators, zonderHost, dubbeleResources, uniekeResources: gezieneResources.size }
173
+ }
174
+
175
+ if (process.argv[1]?.endsWith('x402-discovery-census.mjs')) {
176
+ const uitPad = process.argv[2]
177
+ const ruw = await haalIndex()
178
+ const c = censusPerHost(ruw.items)
179
+
180
+ const extTelling = new Map()
181
+ for (const o of c.operators) {
182
+ for (const k of o.extensieSleutels) extTelling.set(k, (extTelling.get(k) ?? 0) + 1)
183
+ }
184
+ const netwerkTelling = new Map()
185
+ for (const o of c.operators) {
186
+ for (const n of o.netwerken) netwerkTelling.set(n, (netwerkTelling.get(n) ?? 0) + 1)
187
+ }
188
+
189
+ console.log('source : ' + INDEX)
190
+ console.log('reported : ' + ruw.gemeldTotaal + ' resources, in ' + ruw.paginas + ' pages')
191
+ console.log('seen : ' + ruw.items.length + ' records, ' + c.uniekeResources + ' distinct resource URLs')
192
+ if (c.dubbeleResources) console.log('duplicates : ' + c.dubbeleResources + ' records repeat a resource URL already seen')
193
+ if (c.zonderHost) console.log('unparseable : ' + c.zonderHost + ' records without a usable URL')
194
+ console.log('hosts : ' + c.operators.length)
195
+ console.log('')
196
+ console.log('registrations per host, the ten largest:')
197
+ for (const o of c.operators.slice(0, 10)) {
198
+ console.log(
199
+ ' ' + String(o.aantalRegistraties).padStart(6) + ' ' + o.host +
200
+ ' [' + (o.extensieSleutels.join(', ') || 'no extensions') + ']'
201
+ )
202
+ }
203
+ const een = c.operators.filter((o) => o.aantalRegistraties === 1).length
204
+ console.log('')
205
+ console.log(' ' + een + ' of ' + c.operators.length + ' hosts registered exactly one resource')
206
+ console.log(' largest host holds ' + c.operators[0].aantalRegistraties + ' of ' + c.uniekeResources +
207
+ ' resources (' + ((c.operators[0].aantalRegistraties / c.uniekeResources) * 100).toFixed(1) + ' percent)')
208
+ console.log('')
209
+ console.log('extensions declared, counted per host:')
210
+ for (const [k, v] of [...extTelling].sort((a, b) => b[1] - a[1])) {
211
+ console.log(' ' + String(v).padStart(6) + ' ' + k)
212
+ }
213
+ const or = c.operators.filter((o) => o.declareertOfferReceipt)
214
+ console.log('')
215
+ console.log('offer-receipt declared by ' + or.length + ' of ' + c.operators.length + ' hosts')
216
+ for (const o of or.slice(0, 25)) console.log(' ' + o.host + ' ' + o.eersteResource)
217
+ console.log('')
218
+ console.log('networks, counted per host:')
219
+ for (const [k, v] of [...netwerkTelling].sort((a, b) => b[1] - a[1]).slice(0, 12)) {
220
+ console.log(' ' + String(v).padStart(6) + ' ' + k)
221
+ }
222
+
223
+ if (uitPad) {
224
+ const { writeFileSync, mkdirSync } = await import('node:fs')
225
+ const { dirname } = await import('node:path')
226
+ mkdirSync(dirname(uitPad), { recursive: true })
227
+ writeFileSync(
228
+ uitPad,
229
+ JSON.stringify(
230
+ {
231
+ bron: INDEX,
232
+ gemeldTotaal: ruw.gemeldTotaal,
233
+ paginas: ruw.paginas,
234
+ recordsGezien: ruw.items.length,
235
+ uniekeResources: c.uniekeResources,
236
+ dubbeleResources: c.dubbeleResources,
237
+ zonderHost: c.zonderHost,
238
+ operators: c.operators,
239
+ },
240
+ null,
241
+ 2
242
+ )
243
+ )
244
+ console.log('')
245
+ console.log('written: ' + uitPad)
246
+ }
247
+ }
@@ -0,0 +1,201 @@
1
+ // Van de hosts die in een discovery-index STAAN, hoeveel antwoorden er vandaag
2
+ // werkelijk zoals x402 voorschrijft.
3
+ //
4
+ // WAAROM DEZE VRAAG, en niet de vraag waar de ladder voor gebouwd is. De census
5
+ // (x402-discovery-census.mjs) heeft op 2026-10-17 geteld dat nul van de 1127 hosts
6
+ // de offer-receipt-extensie declareert. Trede 1 tot 5 van de ladder hebben dus
7
+ // geen populatie. Een uitslag daarover zou een tabel met nullen zijn plus ons
8
+ // eigen eindpunt, en dat is geen meting maar een advertentie. Trede 0 heeft wel
9
+ // een populatie, en die vraag is nog door niemand gepubliceerd: een registratie
10
+ // in een index zegt niets over of het eindpunt nog leeft.
11
+ //
12
+ // DE STEEKPROEFREGEL STAAT HIER, zodat iemand anders exact dezelfde hosts kan
13
+ // natrekken en kan zien waar hij afwijkt:
14
+ // 1. Per host de resource met de LAAGSTE index-positie (de census bepaalt die).
15
+ // 2. Hosts gesorteerd op die positie, oplopend.
16
+ // 3. Daaruit elke n-de, met n zo gekozen dat de steekproef over de hele
17
+ // registratievolgorde ligt. Niet de eerste N: die zijn van een handvol
18
+ // partijen die vroeg massaal aanmeldden, en dat meet dan die partijen in
19
+ // plaats van de index.
20
+ // 4. De methode die de partij ZELF declareert in de index. Blind POST sturen
21
+ // laat een GET-route zakken op een 405 die niets over x402 zegt.
22
+ //
23
+ // TWEE CONTROLES, want een meting waarin alles faalt bewijst niet dat de
24
+ // werkelijkheid faalt, hij kan ook bewijzen dat het meetgereedschap kapot is:
25
+ // - positief: een eindpunt waarvan wij weten dat het trede 2 haalt.
26
+ // - negatief: een gewone pagina die niets met x402 doet.
27
+ // Gaan die twee niet zoals verwacht, dan is de uitslag ongeldig en zegt het
28
+ // script dat, in plaats van de cijfers alsnog op te leveren.
29
+
30
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
31
+ import { dirname } from 'node:path'
32
+ import { meetEindpunt, hoogsteTrede } from './x402-conformance-check.mjs'
33
+
34
+ const POSITIEVE_CONTROLE = { url: 'https://api.tokenizen.nl/api/paid/verify-ownership', methode: 'POST', verwacht: 2 }
35
+ const NEGATIEVE_CONTROLE = { url: 'https://example.com/', methode: 'GET', verwacht: -1 }
36
+
37
+ const slaap = (ms) => new Promise((r) => setTimeout(r, ms))
38
+
39
+ /**
40
+ * Vult sjabloon-parameters in een resource-URL met de waarden die de index zelf geeft.
41
+ *
42
+ * TOEGEVOEGD NA EEN EIGEN FOUT, 2026-10-17. De index bevat URL's als
43
+ * "https://api.apibooth.com/v1/ip/:ip" en wij vroegen die LETTERLIJK op, inclusief
44
+ * de dubbele punt. Dat gaf een 400 die wij bijna als "weigert het verzoek" hadden
45
+ * gepubliceerd, terwijl de index de waarde ("8.8.8.8") ernaast vermeldde. Dat is je
46
+ * eigen verzoek meten in plaats van het eindpunt.
47
+ *
48
+ * Geeft {url, ingevuld, resterend} terug: resterend is wat NIET ingevuld kon worden,
49
+ * zodat een meting op een half-ingevuld sjabloon zichzelf kan diskwalificeren in
50
+ * plaats van een getal te produceren dat nergens op rust.
51
+ */
52
+ export function vulSjabloon(url, pathParams) {
53
+ const gevonden = [...String(url).matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)].map((m) => m[1])
54
+ const uniek = [...new Set(gevonden)]
55
+ if (uniek.length === 0) return { url, ingevuld: [], resterend: [] }
56
+ let uit = url
57
+ const ingevuld = []
58
+ const resterend = []
59
+ for (const naam of uniek) {
60
+ const waarde = pathParams?.[naam]
61
+ if (waarde === undefined || waarde === null || String(waarde) === '') {
62
+ resterend.push(naam)
63
+ continue
64
+ }
65
+ uit = uit.split(':' + naam).join(encodeURIComponent(String(waarde)))
66
+ ingevuld.push(naam)
67
+ }
68
+ return { url: uit, ingevuld, resterend }
69
+ }
70
+
71
+ /** Stap 1 tot 3 van de steekproefregel hierboven, puur rekenwerk, geen netwerk. */
72
+ export function kiesSteekproef(operators, aantal) {
73
+ const opPositie = [...operators].sort((a, b) => a.eersteOffset - b.eersteOffset)
74
+ if (aantal >= opPositie.length) return opPositie
75
+ const stap = opPositie.length / aantal
76
+ const uit = []
77
+ for (let i = 0; i < aantal; i++) uit.push(opPositie[Math.floor(i * stap)])
78
+ return uit
79
+ }
80
+
81
+ /**
82
+ * Verdeelt een meting in emmers die NIET door elkaar lopen.
83
+ *
84
+ * Het verschil tussen "doet geen x402" en "wees mijn verzoek af voor het ooit bij
85
+ * de betaalmuur kwam" is echt, en een meting die beide als mislukking telt is met
86
+ * een enkele controle onderuit te halen. Een correcte implementatie geeft zijn
87
+ * 402 voor hij een lichaam valideert; niet iedereen doet dat.
88
+ */
89
+ export function emmer(meting) {
90
+ const trede = hoogsteTrede(meting)
91
+ if (trede >= 0) return 'x402_ok'
92
+ const s = meting.bewijs?.status
93
+ if (s == null) return 'onbereikbaar'
94
+ if (s === 402) return 'status_402_onbruikbaar'
95
+ if (s === 401 || s === 403) return 'afgeschermd'
96
+ if (s === 404 || s === 410) return 'pad_weg'
97
+ if (s === 405) return 'methode_geweigerd'
98
+ if (s === 429) return 'tempo_geweigerd'
99
+ if (s >= 400 && s < 500) return 'verzoek_geweigerd'
100
+ if (s >= 500) return 'serverfout'
101
+ return 'geen_betaalmuur'
102
+ }
103
+
104
+ export async function meetSteekproef(gekozen, { pauzeMs = 1200, fetchImpl = fetch } = {}) {
105
+ const uit = []
106
+ for (const o of gekozen) {
107
+ const methode = o.eersteMethode ?? 'POST'
108
+ const sjab = vulSjabloon(o.eersteResource, o.eersteInvoer?.pathParams)
109
+ const m = await meetEindpunt(sjab.url, { methode, fetchImpl, lichaam: o.eersteInvoer?.body ?? null })
110
+ uit.push({
111
+ host: o.host,
112
+ url: sjab.url,
113
+ urlInIndex: o.eersteResource,
114
+ sjabloonIngevuld: sjab.ingevuld,
115
+ sjabloonNietIngevuld: sjab.resterend,
116
+ lichaamUitIndex: o.eersteInvoer?.body != null,
117
+ indexPositie: o.eersteOffset,
118
+ registratiesVanDezeHost: o.aantalRegistraties,
119
+ gedeclareerdeMethode: o.eersteMethode,
120
+ gebruikteMethode: methode,
121
+ status: m.bewijs?.status ?? null,
122
+ hoogsteTrede: hoogsteTrede(m),
123
+ emmer: emmer(m),
124
+ treden: m.treden,
125
+ bewijs: m.bewijs,
126
+ })
127
+ if (pauzeMs) await slaap(pauzeMs)
128
+ }
129
+ return uit
130
+ }
131
+
132
+ if (process.argv[1]?.endsWith('x402-index-liveness.mjs')) {
133
+ const censusPad = process.argv[2]
134
+ const aantal = Number(process.argv[3] ?? 50)
135
+ const uitPad = process.argv[4]
136
+ if (!censusPad) {
137
+ console.log('usage: node src/x402-index-liveness.mjs <census.json> [sample size] [out.json]')
138
+ process.exitCode = 2
139
+ } else {
140
+ const census = JSON.parse(readFileSync(censusPad, 'utf8'))
141
+ const gekozen = kiesSteekproef(census.operators, aantal)
142
+
143
+ console.log('--- controls first, because a sweep where everything fails proves nothing ---')
144
+ const controles = []
145
+ for (const c of [POSITIEVE_CONTROLE, NEGATIEVE_CONTROLE]) {
146
+ const m = await meetEindpunt(c.url, { methode: c.methode })
147
+ const t = hoogsteTrede(m)
148
+ const ok = t === c.verwacht
149
+ controles.push({ ...c, gemeten: t, emmer: emmer(m), status: m.bewijs?.status ?? null, ok })
150
+ console.log((ok ? ' ok ' : ' FAIL ') + c.url + ' expected L' + c.verwacht + ', measured L' + t)
151
+ await slaap(1200)
152
+ }
153
+ const controlesGoed = controles.every((c) => c.ok)
154
+ if (!controlesGoed) {
155
+ console.log('')
156
+ console.log('The harness did not behave as expected on the controls. Treat the numbers below')
157
+ console.log('as invalid until that is explained. A measurement you cannot trust on a known')
158
+ console.log('case tells you nothing about the unknown ones.')
159
+ }
160
+
161
+ console.log('')
162
+ console.log('--- sample: ' + gekozen.length + ' of ' + census.operators.length + ' hosts ---')
163
+ const metingen = await meetSteekproef(gekozen)
164
+
165
+ const tel = new Map()
166
+ for (const m of metingen) tel.set(m.emmer, (tel.get(m.emmer) ?? 0) + 1)
167
+ for (const m of metingen) {
168
+ console.log(
169
+ (m.hoogsteTrede >= 0 ? 'L' + m.hoogsteTrede : '--') +
170
+ ' ' + String(m.status ?? '---').padStart(3) +
171
+ ' ' + m.emmer.padEnd(22) +
172
+ ' ' + m.host
173
+ )
174
+ }
175
+ console.log('')
176
+ for (const [k, v] of [...tel].sort((a, b) => b[1] - a[1])) {
177
+ console.log(' ' + String(v).padStart(4) + ' ' + k + ' (' + ((v / metingen.length) * 100).toFixed(1) + '%)')
178
+ }
179
+ const ok = metingen.filter((m) => m.hoogsteTrede >= 0).length
180
+ console.log('')
181
+ console.log(' ' + ok + ' of ' + metingen.length + ' sampled hosts answer the way x402 says (level 0)')
182
+ console.log(' levels 1 to 5 are not reported here: the census found 0 of ' + census.operators.length +
183
+ ' hosts declaring the offer-receipt extension, so there is no population to measure')
184
+
185
+ if (uitPad) {
186
+ mkdirSync(dirname(uitPad), { recursive: true })
187
+ writeFileSync(uitPad, JSON.stringify({
188
+ bron: census.bron,
189
+ steekproefregel: 'per host the resource with the lowest index position; hosts sorted by that position ascending; every nth taken so the sample spans the whole registration order; the method each party declares in the index',
190
+ hostsInCensus: census.operators.length,
191
+ steekproefGrootte: metingen.length,
192
+ controles,
193
+ controlesGoed,
194
+ emmers: Object.fromEntries(tel),
195
+ metingen,
196
+ }, null, 2))
197
+ console.log('')
198
+ console.log('written: ' + uitPad)
199
+ }
200
+ }
201
+ }