capacity-attest 0.4.0 → 0.6.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdellah Ouadoudi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,161 +1,198 @@
1
- # capacity-attest
2
-
3
- MCP-server voor leverings-attestaties bij x402-capaciteitshandel tussen AI-agents.
4
-
5
- > Status: MVP, gepubliceerd op npm (`npm install capacity-attest`) en in het officiële MCP-register (`io.github.holistis/capacity-attest`).
6
-
7
- ## Waarom dit bestaat
8
-
9
- Wanneer een AI-agent via het [x402-protocol](https://www.x402.org/) betaalt voor capaciteit (GPU-uren, opslag, API/inference-credits, bandbreedte) bij een andere agent of dienst, is er na de betaling geen bewijs dat het beloofde ook echt geleverd is. De kopende agent weet het zelf (hij zag de output, of zag hem niet), maar die kennis gaat verloren zodra de sessie eindigt. De volgende agent die met dezelfde verkoper zaken wil doen, begint weer blind.
10
-
11
- `capacity-attest` lost dat specifieke gat op: na afwikkeling laat de **betalende** agent een cryptografisch ondertekende, feitelijke claim achter (`delivered: yes/no/partial` + een hash van het bewijsmateriaal). Andere agents kunnen die geschiedenis opvragen **voordat** ze zelf met die verkoper in zee gaan.
12
-
13
- Geen oordeel. Geen reputatiescore. Geen "vonnis", puur een ondertekende bon-plus-claim, net zoals een afleverbon bij een fysieke levering.
14
-
15
- ## Wat dit NIET is
16
-
17
- Dit is bewust en hardcoded **niet**:
18
-
19
- - **Geen reputatiescore of rating.** `get_delivery_history` retourneert de ruwe, chronologische lijst van claims, geen gemiddelde, geen percentage, geen "trust score". Het samenvatten tot één getal is impliciet een oordeel, en dat is expliciet afgewezen tijdens de besluitvorming voor dit project.
20
- - **Geen financieel product.** Geen rente, geen tijd-disconto op betalingen, geen yield op het ledger-saldo (er ís geen saldo, dit is geen escrow), geen lening, geen onderpand, geen invoice-financing/factoring. `assetType` is een gesloten enum van capaciteitssoorten (`gpu-hours`, `storage`, `api-credits`, `bandwidth`) en bevat bewust niets dat op een financieel instrument lijkt.
21
- - **Geen eigen token of munt.** Betalingen lopen via x402/USDC zoals gebruikelijk; dit project registreert alleen de *bon* van een afwikkeling die al ergens anders heeft plaatsgevonden.
22
- - **Geen krediet-verlening.** Een claim wordt pas gemaakt **na** een voltooide betaling. Dit project financiert niets, het documenteert een reeds afgeronde ijara (verhuur/dienst)-transactie.
23
- - **Geen eigen identity-, autoriteits- of geschillenlaag.** `externalRefs` (zie hieronder) is puur een citaat naar een systeem van een ander (ERC-8004, AP2, Legal Context Protocol, ...). Dit project resolvet, verifieert of beoordeelt die verwijzing zelf nooit. Zie [DECISIONS.md](./DECISIONS.md) D-007 t/m D-013 voor waarom dit bewust geen eigen protocol is geworden.
24
-
25
- Dit is een bewuste, formeel getoetste ontwerpkeuze, niet een toevallige scope-beperking. Zie de guardrails-sectie in het project-brief als je overweegt hier iets aan toe te voegen: bij twijfel of een veld/functie hiertegenaan schuurt, laat het weg.
26
-
27
- Bewust uitgestelde features (verankering, tussentijdse status, formele conformance-vectoren), inclusief de precieze voorwaarde waaronder we ze alsnog zouden bouwen: zie [DECISIONS.md](./DECISIONS.md).
28
-
29
- ## Hoe het werkt
30
-
31
- ### 1. `record_delivery`
32
-
33
- De betalende agent (de koper) roept dit aan **na** een x402-afwikkeling, zodra bekend is of het beloofde is aangekomen. De claim bevat:
34
-
35
- | Veld | Betekenis |
36
- | --- | --- |
37
- | `sellerAddress` | 0x-adres van de partij die betaald werd |
38
- | `buyerAddress` | 0x-adres van de betalende agent, moet overeenkomen met het adres dat uit `signature` wordt teruggerekend |
39
- | `assetType` | `gpu-hours` \| `storage` \| `api-credits` \| `bandwidth` |
40
- | `promisedSpec` | Wat er beloofd was: vrije tekst of een gestructureerd object |
41
- | `delivered` | `yes` \| `no` \| `partial` |
42
- | `evidenceHash` | sha256-hex van bewijsmateriaal (logs, response-payload, ...), het bewijs zelf wordt niet opgeslagen |
43
- | `settlementRef` | x402-payment-ref of on-chain tx-hash van de onderliggende betaling |
44
- | `timestamp` | ISO-8601 tijdstip |
45
- | `claimId` | content-addressed sha256-hash van alle velden hierboven, zie `computeClaimId()` in `src/schema.ts` |
46
- | `signature` | EIP-191 personal-sign handtekening van de koper over `claimId` |
47
- | `externalRefs` | *(optioneel, sinds 0.3.0)* ongeverifieerde verwijzingen naar andere agent-economie-infrastructuur: `sellerAgentRef`/`buyerAgentRef` (bv. een ERC-8004-agent-id of DID), `mandateRef`+`mandateIssuerDid` (een extern uitgegeven AP2/AAE-mandaat), `intentRef` (een extern AP2 IntentMandate), `disputeContext` (`protocol`+`termsHash`+optioneel `resolutionRef`, bv. een Legal Context Protocol-verwijzing). Zie [DECISIONS.md](./DECISIONS.md) D-007 t/m D-013 |
48
- | `priorClaimId` | *(optioneel, sinds 0.4.0)* de `claimId` van jouw vorige claim over dezelfde `sellerAddress`, zodat jouw claims over die verkoper een ketting vormen. Weggelaten bij je eerste claim over een verkoper. Zit in de ondertekende inhoud, dus een host kan het niet weghalen. Laat een lezer een host betrappen die een middelste claim verbergt. Zie [DECISIONS.md](./DECISIONS.md) D-006 |
49
-
50
- De server valideert eerst het schema, dan of `claimId` echt de hash van de inhoud is, en dan of `signature` echt terugrekent naar `buyerAddress`. Alleen dan wordt de claim toegevoegd aan de append-only ledger (`data/claims.jsonl`). Een ongeldige handtekening of een claim die al eerder is opgeslagen (zelfde `claimId`) wordt geweigerd.
51
-
52
- ### 2. `get_delivery_history`
53
-
54
- Gegeven een `sellerAddress`, retourneert dit alle bekende claims tegen die verkoper op déze installatie, chronologisch (oudst eerst). Puur feitelijk, geen samengevat getal. Een kopende agent roept dit aan **vóórdat** hij betaalt, om de ruwe leveringsgeschiedenis van een potentiële verkoper te zien en zelf te beoordelen.
55
-
56
- Het antwoord bevat naast `sellerAddress`, `count` en `claims` ook `scope` (altijd `"local-ledger"`) en `note`: een vaste, feitelijke tekst die uitlegt dat dit resultaat alleen de lokale ledger van déze installatie weerspiegelt. Een lege of korte geschiedenis betekent niet dat de verkoper een schone staat van dienst heeft, het kan ook betekenen dat er hier simpelweg nog geen claims zijn vastgelegd. Zie [DECISIONS.md](./DECISIONS.md) (D-005) voor de bredere architectuurvraag hierachter: hoe vindt een koper claims die op een ándere installatie zijn vastgelegd.
57
-
58
- Sinds 0.4.0 bevat het antwoord ook `completeness`: een analyse van de per-koper ketens (`priorClaimId`) in precies deze uitkomst. Als een getoonde claim terugverwijst naar een claim die NIET in de uitkomst zit, komt die in `possibleOmissions` te staan. Dat is een concreet, controleerbaar signaal dat de host mogelijk een middelste claim verbergt, in plaats van een vaag vermoeden.
59
-
60
- Let op, dit is het belangrijkste punt: dat `completeness`-veld wordt berekend door dezelfde server die de claims teruggeeft. Vertrouw je die server niet, vertrouw dan ook het veld niet, want een oneerlijke host kan er gewoon "alles compleet" in zetten. De echte zekerheid zit in de ondertekende `priorClaimId` in de claims zelf, die een host niet kan vervalsen of weghalen. Reken de controle dus zelf opnieuw uit over de teruggekregen claims:
61
-
62
- ```js
63
- // recompute-completeness.mjs
64
- import { verifyClaim } from "capacity-attest/dist/signing.js";
65
- import { analyzeCompleteness } from "capacity-attest/dist/completeness.js";
66
-
67
- // `claims` = de array uit het get_delivery_history-antwoord.
68
- const allSigned = claims.every((c) => verifyClaim(c).ok); // elke claim echt?
69
- const report = analyzeCompleteness(claims); // zelf herrekenen, niet het host-veld geloven
70
- console.log({ allSigned, chainConsistent: report.chainConsistent, possibleOmissions: report.possibleOmissions });
71
- ```
72
-
73
- Eerlijke grens: ook zelf-herrekenen betrapt geen verborgen laatste claim en geen verborgen hele koper, want daar valt geen schakel over te struikelen. En een losse terugverwijzing hoeft geen bedrog te zijn: de eerdere claim kan ook gewoon op een andere installatie zijn vastgelegd (het D-005-geval). Voor echte zekerheid blijven de externe getuigen nodig: je eigen bewaarde kopie hierboven, en de betaling op de keten via `settlementRef`. Zie [DECISIONS.md](./DECISIONS.md) D-006.
74
-
75
- Een openbaar, zelf-controleerbaar voorbeeld met een nagebootste verbergende host en expres-kapotte testgevallen staat in [docs/COMPLETENESS-FIXTURE.md](./docs/COMPLETENESS-FIXTURE.md). Draai het met `npm run fixture`; dezelfde controles draaien bij elke push als test. Zo kun je onze claim zelf natellen in plaats van ons op ons woord te geloven.
76
-
77
- ### 3. `resolve_agent_identity` *(sinds 0.3.0)*
78
-
79
- Read-only opzoeking tegen een ERC-8004 Identity Registry: wie bezit `agentId` (`ownerOf`) en waar staat zijn registratiebestand (`tokenURI`). Alleen de standaard ERC-721-interface wordt aangeroepen, niets ERC-8004-specifieks. Vereist van de aanroeper zowel `agentRegistryRef` (`"eip155:<chainId>:<registryAddress>"`) als een `rpcUrl` voor die chain: dit project bundelt bewust geen eigen RPC-provider en geen canoniek registry-adres, want ERC-8004 heeft onafhankelijke deployments per chain en de EIP-tekst zelf noemt geen vast adres. Haalt bewust NOOIT op wat `tokenURI` aanwijst (dat blijft een pointer die de aanroeper zelf desgewenst opvraagt); dat zou een SSRF-vormig risico zijn op aanroeper-gecontroleerde on-chain data.
80
-
81
- Getest tegen een injecteerbare `ContractFactory` (`src/erc8004.test.ts`, geen netwerkafhankelijkheid) én live tegen de echte, gedeployde registry op Base mainnet (`examples/verify-erc8004-live.mjs`, `npm run build && node examples/verify-erc8004-live.mjs`). Zie [DECISIONS.md](./DECISIONS.md) D-007 voor de volledige achtergrond.
82
-
83
- ## Ondertekening
84
-
85
- De claim wordt ondertekend door de **koper** (de partij die betaalde en dus weet wat er wel/niet aankwam), niet door de verkoper. Dit is bewust eenvoudige EIP-191 `personal_sign` over `claimId` (via `ethers.Signer#signMessage`), geen EIP-712 typed data. Dat houdt het crypto-oppervlak van deze MVP klein en makkelijk te controleren. Een latere upgrade naar EIP-712 (zoals in `mcp-paywall/src/x402.mjs`) is additief mogelijk zonder bestaande claims ongeldig te maken.
86
-
87
- ## Een claim onafhankelijk verifiëren
88
-
89
- Elke claim in de ledger is met alleen het npm-package en de rauwe claim-bytes na te rekenen, zonder toegang tot dit project of een netwerkoproep naar ons. Geen account, geen hosted call.
90
-
91
- ```bash
92
- npm install capacity-attest@0.2.0
93
- ```
94
-
95
- ```js
96
- // verify.mjs, als ES module draaien (top-level await)
97
- import { verifyClaim } from "capacity-attest/dist/signing.js";
98
-
99
- const claim = JSON.parse(await (await fetch("<url naar een claim.jsonl-regel>")).text());
100
- console.log(verifyClaim(claim));
101
- // { ok: true } als claimId echt de hash van de inhoud is EN signature echt naar buyerAddress terugrekent
102
- ```
103
-
104
- Let op: importeer `capacity-attest/dist/signing.js` rechtstreeks, niet het package-root. De root (`dist/index.js`) start bij het importeren meteen de MCP-server over stdio, wat een los verificatie-script laat hangen.
105
-
106
- `verifyClaim()` controleert precies twee dingen: dat `claimId` de content-addressed hash van de claim-velden is, en dat `signature` (EIP-191) terugrekent naar `buyerAddress`. Het controleert niet of de onderliggende afwikkeling (`settlementRef`) echt on-chain klopt, dat is een losse, aparte check tegen de betreffende chain, en het controleert niet of `delivered` waar is of of `evidenceHash` een echt bewijsstuk dekt, dat blijft de eigen verklaring van de kopende agent.
107
-
108
- Een werkend, extern gereproduceerd voorbeeld van deze exacte stappen staat in [github.com/YE-YI7/asm-spec, PR #18](https://github.com/YE-YI7/asm-spec/pull/18): een onafhankelijk project dat dit tegen een echte, live geregistreerde claim heeft gedraaid.
109
-
110
- ## Je eigen ingediende claims delen, los van een host (D-006)
111
-
112
- `get_delivery_history` vertrouwt op de eerlijkheid van wie de MCP-server bedient: zie de `note` in dat tool-antwoord en [DECISIONS.md](./DECISIONS.md) (D-006). Elke getoonde claim is wel degelijk echt (ondertekening wordt sinds 2026-09-06 ook bij het lezen opnieuw gecontroleerd, niet alleen bij het schrijven), maar niets bewijst dat de host de VOLLEDIGE set laat zien die hij daadwerkelijk heeft.
113
-
114
- Als jij zelf de koper bent die een claim indiende, hoef je op die host niet te wachten: jij hebt die claim zelf al ondertekend, dus jij kan 'm rechtstreeks aan een wantrouwende tegenpartij laten zien, buiten elke host om.
115
-
116
- ```js
117
- // export-my-claims.mjs
118
- import { claimsForSeller } from "capacity-attest/dist/ledger.js";
119
-
120
- const myAddress = "0x..."; // jouw buyerAddress
121
- const seller = "0x..."; // de verkoper waar het over gaat
122
-
123
- const mine = (await claimsForSeller(seller)).filter(
124
- (c) => c.buyerAddress.toLowerCase() === myAddress.toLowerCase(),
125
- );
126
- console.log(JSON.stringify(mine, null, 2));
127
- ```
128
-
129
- Elke claim in die lijst is zelfstandig verifieerbaar met `verifyClaim()` (zie hierboven), zonder dat de ontvanger jouw installatie of enige host hoeft te vertrouwen. Dit lost geen vindbaarheid op (D-005: hoe vindt iemand anders jouw claim zonder dat jij 'm deelt) en geen volledigheid over ALLE kopers samen (D-006: dit bewijst alleen wat JIJ indiende, niet wat een host verder mogelijk verzwijgt van andere kopers), maar het geeft een concrete, kosteloze manier om één specifiek geschil te bewijzen zonder een host te hoeven vertrouwen.
130
-
131
- ## Lokaal draaien
132
-
133
- ```bash
134
- npm install
135
- npm run build # tsc -> dist/
136
- npm run typecheck # tsc --noEmit
137
- npm test # vitest run
138
- npm run demo # end-to-end lokale demo met TEST-sleutels, geen live infra
139
- npm start # start de MCP-server over stdio (bijv. voor Claude Desktop/Code als lokale MCP-server)
140
- ```
141
-
142
- De ledger-locatie is instelbaar via `CAPACITY_ATTEST_DATA_DIR` (default: `./data` in dit package). Tests en de demo gebruiken altijd een eigen, wegwerpbare tijdelijke map, nooit de echte `data/` map.
143
-
144
- ## Architectuur
145
-
146
- ```text
147
- src/
148
- schema.ts DeliveryClaim zod-schema + content-addressing (computeClaimId, canonicalize)
149
- signing.ts sign/verify van een claim (ethers, EIP-191 personal-sign)
150
- ledger.ts append-only JSONL-opslag (data/claims.jsonl), nooit muteerbaar
151
- tools.ts de daadwerkelijke logica achter beide MCP-tools, transport-onafhankelijk
152
- config.ts waar de ledger-map leeft, lazy zodat tests 'm kunnen overriden
153
- index.ts MCP-server wiring (registreert record_delivery + get_delivery_history)
154
- examples/demo.ts end-to-end lokaal voorbeeld met TEST-sleutels
155
- ```
156
-
157
- `tools.ts` bevat de eigenlijke business-logica; `index.ts` vertaalt dat alleen naar MCP tool-calls. Zo kunnen tests en de demo dezelfde logica direct aanroepen zonder een stdio-transport op te tuigen.
158
-
159
- ## Relatie tot x402
160
-
161
- Dit project verifieert of settelt zelf géén x402-betalingen, dat gebeurt al bij de betaalstap zelf (zie bijvoorbeeld `mcp-paywall/src/x402.mjs` in dit ecosysteem voor een volledige EIP-3009-verify/settle-implementatie). `settlementRef` verwijst simpelweg naar die reeds-voltooide afwikkeling. Dat betekent ook dat de MVP-koppeling met een echte x402-facilitator eenvoudig kan blijven: `settlementRef` is vrije tekst, met als aanname dat de koper 'm eerlijk invult. Een latere versie kan dat veld optioneel verifiëren tegen een echte facilitator (TODO, niet in deze MVP).
1
+ # capacity-attest
2
+
3
+ MCP-server voor leverings-attestaties bij x402-capaciteitshandel tussen AI-agents.
4
+
5
+ > Status: MVP, gepubliceerd op npm (`npm install capacity-attest`) en in het officiële MCP-register (`io.github.holistis/capacity-attest`).
6
+
7
+ ## Onafhankelijk gecontroleerd, niet alleen beweerd
8
+
9
+ Beweringen over dit project zijn hieronder allemaal aanklikbaar en zelf na te trekken, niet op ons woord te geloven.
10
+
11
+ | Wat | Door wie | Status |
12
+ |---|---|---|
13
+ | Gebruikt `capacity-attest@0.2.0` als echte dependency, verifieert claim-digest/claimId/handtekening via onze eigen code | [YE-YI7/asm-spec#18](https://github.com/YE-YI7/asm-spec/pull/18) | Gemerged |
14
+ | BSV-rail-adapter op hetzelfde content-geadresseerde claim-formaat | [YE-YI7/asm-spec#19](https://github.com/YE-YI7/asm-spec/pull/19) (auteur EmbryoSpace) | Gemerged |
15
+ | Onze grensuitspraken ("vindbaarheid ≠ volledigheid") zelf op de keten geverifieerd door een derde, geen woord aangenomen | [x402-foundation/x402#3379](https://github.com/x402-foundation/x402/issues/3379) | Publiek, doorlopend |
16
+ | Live delivery-claims als on-chain attestaties op Base mainnet, door iedereen te decoderen | [delivered=yes](https://base.easscan.org/attestation/view/0x81a55d54452b2cf8bdda7918f63a27bf9ff79e5025b485f7316aae6259288ccc) · [delivered=no](https://base.easscan.org/attestation/view/0xe736b005cbcb54f8f196ac64ef09d75d939c8a18c0d5d9670b5c5025c07398c4) | Live |
17
+ | Echte `giveFeedback()`-aanroep op de ERC-8004 Reputation Registry, Base mainnet | [tx 0x2217...efc0](https://basescan.org/tx/0x221797800d5941dff62e87022083e7c6dfba3e07b35c84e56b10fdca8967efc0) | Live |
18
+ | Voorgesteld als koperszijde-aanvulling op een andermans agent-spec | [omworldprotocol/om-world#18](https://github.com/omworldprotocol/om-world/pull/18) | In review, nog niet gemerged |
19
+
20
+ **Eerlijk apart gehouden van bovenstaande tabel, want dit is geen derde-partij-controle:** voor 0.6.0 (de eerste keer dat dit pakket echt naar de blockchain schrijft) hebben we zelf een adversariële security review uitgevoerd. Tien bevindingen, allemaal gefixt met een eigen regressietest, geen onafhankelijke audit door een externe partij. Volledig, controleerbaar verslag: [docs/SECURITY-REVIEW-2026-09-11.md](./docs/SECURITY-REVIEW-2026-09-11.md).
21
+
22
+ ## Waarom dit bestaat
23
+
24
+ Wanneer een AI-agent via het [x402-protocol](https://www.x402.org/) betaalt voor capaciteit (GPU-uren, opslag, API/inference-credits, bandbreedte) bij een andere agent of dienst, is er na de betaling geen bewijs dat het beloofde ook echt geleverd is. De kopende agent weet het zelf (hij zag de output, of zag hem niet), maar die kennis gaat verloren zodra de sessie eindigt. De volgende agent die met dezelfde verkoper zaken wil doen, begint weer blind.
25
+
26
+ `capacity-attest` lost dat specifieke gat op: na afwikkeling laat de **betalende** agent een cryptografisch ondertekende, feitelijke claim achter (`delivered: yes/no/partial` + een hash van het bewijsmateriaal). Andere agents kunnen die geschiedenis opvragen **voordat** ze zelf met die verkoper in zee gaan.
27
+
28
+ Geen oordeel. Geen reputatiescore. Geen "vonnis", puur een ondertekende bon-plus-claim, net zoals een afleverbon bij een fysieke levering.
29
+
30
+ ## Wat dit NIET is
31
+
32
+ Dit is bewust en hardcoded **niet**:
33
+
34
+ - **Geen reputatiescore of rating.** `get_delivery_history` retourneert de ruwe, chronologische lijst van claims, geen gemiddelde, geen percentage, geen "trust score". Het samenvatten tot één getal is impliciet een oordeel, en dat is expliciet afgewezen tijdens de besluitvorming voor dit project.
35
+ - **Geen financieel product.** Geen rente, geen tijd-disconto op betalingen, geen yield op het ledger-saldo (er ís geen saldo, dit is geen escrow), geen lening, geen onderpand, geen invoice-financing/factoring. `assetType` is een gesloten enum van capaciteitssoorten (`gpu-hours`, `storage`, `api-credits`, `bandwidth`) en bevat bewust niets dat op een financieel instrument lijkt.
36
+ - **Geen eigen token of munt.** Betalingen lopen via x402/USDC zoals gebruikelijk; dit project registreert alleen de *bon* van een afwikkeling die al ergens anders heeft plaatsgevonden.
37
+ - **Geen krediet-verlening.** Een claim wordt pas gemaakt **na** een voltooide betaling. Dit project financiert niets, het documenteert een reeds afgeronde ijara (verhuur/dienst)-transactie.
38
+ - **Geen eigen identity-, autoriteits- of geschillenlaag.** `externalRefs` (zie hieronder) is puur een citaat naar een systeem van een ander (ERC-8004, AP2, Legal Context Protocol, ...). Dit project resolvet, verifieert of beoordeelt die verwijzing zelf nooit. Zie [DECISIONS.md](./DECISIONS.md) D-007 t/m D-013 voor waarom dit bewust geen eigen protocol is geworden.
39
+
40
+ Dit is een bewuste, formeel getoetste ontwerpkeuze, niet een toevallige scope-beperking. Zie de guardrails-sectie in het project-brief als je overweegt hier iets aan toe te voegen: bij twijfel of een veld/functie hiertegenaan schuurt, laat het weg.
41
+
42
+ Bewust uitgestelde features (verankering, tussentijdse status, formele conformance-vectoren), inclusief de precieze voorwaarde waaronder we ze alsnog zouden bouwen: zie [DECISIONS.md](./DECISIONS.md).
43
+
44
+ ## Hoe het werkt
45
+
46
+ ### 1. `record_delivery`
47
+
48
+ De betalende agent (de koper) roept dit aan **na** een x402-afwikkeling, zodra bekend is of het beloofde is aangekomen. De claim bevat:
49
+
50
+ | Veld | Betekenis |
51
+ | --- | --- |
52
+ | `sellerAddress` | 0x-adres van de partij die betaald werd |
53
+ | `buyerAddress` | 0x-adres van de betalende agent, moet overeenkomen met het adres dat uit `signature` wordt teruggerekend |
54
+ | `assetType` | `gpu-hours` \| `storage` \| `api-credits` \| `bandwidth` |
55
+ | `promisedSpec` | Wat er beloofd was: vrije tekst of een gestructureerd object |
56
+ | `delivered` | `yes` \| `no` \| `partial` |
57
+ | `evidenceHash` | sha256-hex van bewijsmateriaal (logs, response-payload, ...), het bewijs zelf wordt niet opgeslagen |
58
+ | `settlementRef` | x402-payment-ref of on-chain tx-hash van de onderliggende betaling |
59
+ | `timestamp` | ISO-8601 tijdstip |
60
+ | `claimId` | content-addressed sha256-hash van alle velden hierboven, zie `computeClaimId()` in `src/schema.ts` |
61
+ | `signature` | EIP-191 personal-sign handtekening van de koper over `claimId` |
62
+ | `externalRefs` | *(optioneel, sinds 0.3.0)* ongeverifieerde verwijzingen naar andere agent-economie-infrastructuur: `sellerAgentRef`/`buyerAgentRef` (bv. een ERC-8004-agent-id of DID), `mandateRef`+`mandateIssuerDid` (een extern uitgegeven AP2/AAE-mandaat), `intentRef` (een extern AP2 IntentMandate), `disputeContext` (`protocol`+`termsHash`+optioneel `resolutionRef`, bv. een Legal Context Protocol-verwijzing). Zie [DECISIONS.md](./DECISIONS.md) D-007 t/m D-013 |
63
+ | `priorClaimId` | *(optioneel, sinds 0.4.0)* de `claimId` van jouw vorige claim over dezelfde `sellerAddress`, zodat jouw claims over die verkoper een ketting vormen. Weggelaten bij je eerste claim over een verkoper. Zit in de ondertekende inhoud, dus een host kan het niet weghalen. Laat een lezer een host betrappen die een middelste claim verbergt. Zie [DECISIONS.md](./DECISIONS.md) D-006 |
64
+
65
+ De server valideert eerst het schema, dan of `claimId` echt de hash van de inhoud is, en dan of `signature` echt terugrekent naar `buyerAddress`. Alleen dan wordt de claim toegevoegd aan de append-only ledger (`data/claims.jsonl`). Een ongeldige handtekening of een claim die al eerder is opgeslagen (zelfde `claimId`) wordt geweigerd.
66
+
67
+ ### 2. `get_delivery_history`
68
+
69
+ Gegeven een `sellerAddress`, retourneert dit alle bekende claims tegen die verkoper op déze installatie, chronologisch (oudst eerst). Puur feitelijk, geen samengevat getal. Een kopende agent roept dit aan **vóórdat** hij betaalt, om de ruwe leveringsgeschiedenis van een potentiële verkoper te zien en zelf te beoordelen.
70
+
71
+ Het antwoord bevat naast `sellerAddress`, `count` en `claims` ook `scope` (altijd `"local-ledger"`) en `note`: een vaste, feitelijke tekst die uitlegt dat dit resultaat alleen de lokale ledger van déze installatie weerspiegelt. Een lege of korte geschiedenis betekent niet dat de verkoper een schone staat van dienst heeft, het kan ook betekenen dat er hier simpelweg nog geen claims zijn vastgelegd. Zie [DECISIONS.md](./DECISIONS.md) (D-005) voor de bredere architectuurvraag hierachter: hoe vindt een koper claims die op een ándere installatie zijn vastgelegd.
72
+
73
+ Sinds 0.4.0 bevat het antwoord ook `completeness`: een analyse van de per-koper ketens (`priorClaimId`) in precies deze uitkomst. Als een getoonde claim terugverwijst naar een claim die NIET in de uitkomst zit, komt die in `possibleOmissions` te staan. Dat is een concreet, controleerbaar signaal dat de host mogelijk een middelste claim verbergt, in plaats van een vaag vermoeden.
74
+
75
+ Let op, dit is het belangrijkste punt: dat `completeness`-veld wordt berekend door dezelfde server die de claims teruggeeft. Vertrouw je die server niet, vertrouw dan ook het veld niet, want een oneerlijke host kan er gewoon "alles compleet" in zetten. De echte zekerheid zit in de ondertekende `priorClaimId` in de claims zelf, die een host niet kan vervalsen of weghalen. Reken de controle dus zelf opnieuw uit over de teruggekregen claims:
76
+
77
+ ```js
78
+ // recompute-completeness.mjs
79
+ import { verifyClaim } from "capacity-attest/dist/signing.js";
80
+ import { analyzeCompleteness } from "capacity-attest/dist/completeness.js";
81
+
82
+ // `claims` = de array uit het get_delivery_history-antwoord.
83
+ const allSigned = claims.every((c) => verifyClaim(c).ok); // elke claim echt?
84
+ const report = analyzeCompleteness(claims); // zelf herrekenen, niet het host-veld geloven
85
+ console.log({ allSigned, chainConsistent: report.chainConsistent, possibleOmissions: report.possibleOmissions });
86
+ ```
87
+
88
+ Eerlijke grens: ook zelf-herrekenen betrapt geen verborgen laatste claim en geen verborgen hele koper, want daar valt geen schakel over te struikelen. En een losse terugverwijzing hoeft geen bedrog te zijn: de eerdere claim kan ook gewoon op een andere installatie zijn vastgelegd (het D-005-geval). Voor echte zekerheid blijven de externe getuigen nodig: je eigen bewaarde kopie hierboven, en de betaling op de keten via `settlementRef`. Zie [DECISIONS.md](./DECISIONS.md) D-006.
89
+
90
+ Een openbaar, zelf-controleerbaar voorbeeld met een nagebootste verbergende host en expres-kapotte testgevallen staat in [docs/COMPLETENESS-FIXTURE.md](./docs/COMPLETENESS-FIXTURE.md). Draai het met `npm run fixture`; dezelfde controles draaien bij elke push als test. Zo kun je onze claim zelf natellen in plaats van ons op ons woord te geloven.
91
+
92
+ ## Claims van andere installaties vinden (D-005)
93
+
94
+ `get_delivery_history` is per definitie lokaal: koper B ziet niet wat koper A op een andere installatie vastlegde over dezelfde verkoper. Omdat elke claim zelf-verifieerbaar is, heeft vindbaarheid geen vertrouwde index nodig. `discoverDeliveryHistory(seller, sources)` (zie `src/discovery.ts`) leest een verkopers claims uit meerdere onafhankelijke, ONvertrouwde bronnen (je lokale ledger plus elk host-onafhankelijk substraat dat je wilt lezen), ontdubbelt, herverifieert elke claim, filtert andere verkopers eruit, en draait de completeness-check over het geheel. Een bron die nep injecteert wordt geweigerd; een bron die weglaat is het D-006-probleem, meegenomen maar niet magisch opgelost.
95
+
96
+ De productie-onderlaag (EAS op Base, ERC-8004) is bewust nog niet live gekoppeld: dat kost gas en wacht op een echte integrator. De naad staat klaar. Een openbaar, draaibaar voorbeeld met twee nagebootste installaties staat in [docs/DISCOVERY-FIXTURE.md](./docs/DISCOVERY-FIXTURE.md), draai het met `npm run discovery-fixture`. Zie [DECISIONS.md](./DECISIONS.md) D-005.
97
+
98
+ ### 3. `resolve_agent_identity` *(sinds 0.3.0)*
99
+
100
+ Read-only opzoeking tegen een ERC-8004 Identity Registry: wie bezit `agentId` (`ownerOf`) en waar staat zijn registratiebestand (`tokenURI`). Alleen de standaard ERC-721-interface wordt aangeroepen, niets ERC-8004-specifieks. Vereist van de aanroeper zowel `agentRegistryRef` (`"eip155:<chainId>:<registryAddress>"`) als een `rpcUrl` voor die chain: dit project bundelt bewust geen eigen RPC-provider en geen canoniek registry-adres, want ERC-8004 heeft onafhankelijke deployments per chain en de EIP-tekst zelf noemt geen vast adres. Haalt bewust NOOIT op wat `tokenURI` aanwijst (dat blijft een pointer die de aanroeper zelf desgewenst opvraagt); dat zou een SSRF-vormig risico zijn op aanroeper-gecontroleerde on-chain data.
101
+
102
+ Getest tegen een injecteerbare `ContractFactory` (`src/erc8004.test.ts`, geen netwerkafhankelijkheid) én live tegen de echte, gedeployde registry op Base mainnet (`examples/verify-erc8004-live.mjs`, `npm run build && node examples/verify-erc8004-live.mjs`). Zie [DECISIONS.md](./DECISIONS.md) D-007 voor de volledige achtergrond.
103
+
104
+ ### 4. `publishReputationFeedback` *(sinds 0.6.0, library-functie, geen MCP-tool)*
105
+
106
+ **Let op (adversariele review 2026-09-11): op het moment van schrijven staat op npm nog versie 0.5.0 gepubliceerd, zonder deze functie.** Wie via GitHub leest en meteen `npm install capacity-attest` doet zoals verderop in dit document beschreven, krijgt dus nog geen `publishReputationFeedback` — check `npm view capacity-attest version` voor de daadwerkelijk gepubliceerde versie voor je dit importeert. Alles hieronder beschrijft de code zoals hij op de `main`-branch staat.
107
+
108
+ Publiceert het `delivered`-feit van een al ondertekende claim naar een ERC-8004 Reputation Registry se `giveFeedback()` — dezelfde plek waar ~500k geregistreerde agents al naar reputatiesignalen kunnen kijken, in plaats van alleen naar deze installatie se eigen ledger of EAS. Het contract vereist een numeriek `value`+`valueDecimals`-veld; dit pakket verzint daar bewust geen eigen beoordelingsschaal voor. `value` is een letterlijke, mechanische spiegel van `delivered` (yes=1.0, partial=0.5, no=0.0), nooit een nieuw oordeel, en capacity-attest leest of toont dat getal zelf nergens terug. Herverifieert de claim se handtekening voordat er iets on-chain geschreven wordt.
109
+
110
+ Vereist van de aanroeper `reputationRegistryRef` (`"eip155:<chainId>:<registryAddress>"`, de Reputation Registry, niet de Identity Registry), `agentRegistryRef` (dezelfde chain, maar de Identity Registry) en een `rpcUrl`, zelfde caller-levert-alles-postuur als `resolve_agent_identity`. `agentId` (de verkoper se ERC-8004-agent) moet al een geldig geregistreerde Identity-Registry-agent zijn; het contract weigert zelf feedback van de agent se eigen eigenaar ("Self-feedback not allowed"). Sinds de adversariele review van 2026-09-11 wordt `agentId` se geregistreerde eigenaar (via `agentRegistryRef`) ook altijd tegen `claim.sellerAddress` gecontroleerd voor er iets on-chain geschreven wordt — zonder die controle kon een aanroeper een echte, geldig ondertekende claim aan een willekeurig ander agentId hangen.
111
+
112
+ **Bewust GEEN MCP-tool**, om dezelfde reden als EAS se `publishClaim`: dit is een schrijf-actie die een echte, gefinancierde signer en gas vereist, en deze server bundelt of bewaart bewust geen eigen private key. Beschikbaar als directe import (`src/erc8004-reputation.ts`) voor wie zelf een signer beheert.
113
+
114
+ Getest tegen een injecteerbare `ReputationContractFactory` (`src/erc8004-reputation.test.ts`, 19 tests, geen netwerkafhankelijkheid, inclusief een expliciete test dat `value` uitsluitend van `delivered` afhangt, en drie tests voor de agentId-eigenaarschapscontrole) én live tegen de echte, gedeployde registry op Base mainnet (`examples/erc8004-reputation-live-demo.ts`, `npm run erc8004-reputation-demo`): op 2026-09-10 bevestigd via een eigen, wegwerpbare test-agent (agentId 85888) en een echte `giveFeedback()`-aanroep, [tx 0x221797800d5941dff62e87022083e7c6dfba3e07b35c84e56b10fdca8967efc0](https://basescan.org/tx/0x221797800d5941dff62e87022083e7c6dfba3e07b35c84e56b10fdca8967efc0), onafhankelijk teruggecontroleerd via een losse `eth_getTransactionReceipt`-aanroep. Zie [DECISIONS.md](./DECISIONS.md) D-016 voor de volledige achtergrond, inclusief waarom dit ondanks D-005's eigen trigger-criterium toch vandaag gebouwd is.
115
+
116
+ ## Ondertekening
117
+
118
+ De claim wordt ondertekend door de **koper** (de partij die betaalde en dus weet wat er wel/niet aankwam), niet door de verkoper. Dit is bewust eenvoudige EIP-191 `personal_sign` over `claimId` (via `ethers.Signer#signMessage`), geen EIP-712 typed data. Dat houdt het crypto-oppervlak van deze MVP klein en makkelijk te controleren. Een latere upgrade naar EIP-712 (zoals in `mcp-paywall/src/x402.mjs`) is additief mogelijk zonder bestaande claims ongeldig te maken.
119
+
120
+ ## Een claim onafhankelijk verifiëren
121
+
122
+ Elke claim in de ledger is met alleen het npm-package en de rauwe claim-bytes na te rekenen, zonder toegang tot dit project of een netwerkoproep naar ons. Geen account, geen hosted call.
123
+
124
+ ```bash
125
+ npm install capacity-attest
126
+ ```
127
+
128
+ ```js
129
+ // verify.mjs, als ES module draaien (top-level await)
130
+ import { verifyClaim } from "capacity-attest/dist/signing.js";
131
+
132
+ const claim = JSON.parse(await (await fetch("<url naar een claim.jsonl-regel>")).text());
133
+ console.log(verifyClaim(claim));
134
+ // { ok: true } als claimId echt de hash van de inhoud is EN signature echt naar buyerAddress terugrekent
135
+ ```
136
+
137
+ Let op: importeer `capacity-attest/dist/signing.js` rechtstreeks, niet het package-root. De root (`dist/index.js`) start bij het importeren meteen de MCP-server over stdio, wat een los verificatie-script laat hangen.
138
+
139
+ `verifyClaim()` controleert precies twee dingen: dat `claimId` de content-addressed hash van de claim-velden is, en dat `signature` (EIP-191) terugrekent naar `buyerAddress`. Het controleert niet of de onderliggende afwikkeling (`settlementRef`) echt on-chain klopt, dat is een losse, aparte check tegen de betreffende chain, en het controleert niet of `delivered` waar is of of `evidenceHash` een echt bewijsstuk dekt, dat blijft de eigen verklaring van de kopende agent.
140
+
141
+ Een werkend, extern gereproduceerd voorbeeld van deze exacte stappen staat in [github.com/YE-YI7/asm-spec, PR #18](https://github.com/YE-YI7/asm-spec/pull/18): een onafhankelijk project dat dit tegen een echte, live geregistreerde claim heeft gedraaid.
142
+
143
+ ## Je eigen ingediende claims delen, los van een host (D-006)
144
+
145
+ `get_delivery_history` vertrouwt op de eerlijkheid van wie de MCP-server bedient: zie de `note` in dat tool-antwoord en [DECISIONS.md](./DECISIONS.md) (D-006). Elke getoonde claim is wel degelijk echt (ondertekening wordt sinds 2026-09-06 ook bij het lezen opnieuw gecontroleerd, niet alleen bij het schrijven), maar niets bewijst dat de host de VOLLEDIGE set laat zien die hij daadwerkelijk heeft.
146
+
147
+ Als jij zelf de koper bent die een claim indiende, hoef je op die host niet te wachten: jij hebt die claim zelf al ondertekend, dus jij kan 'm rechtstreeks aan een wantrouwende tegenpartij laten zien, buiten elke host om.
148
+
149
+ ```js
150
+ // export-my-claims.mjs
151
+ import { claimsForSeller } from "capacity-attest/dist/ledger.js";
152
+
153
+ const myAddress = "0x..."; // jouw buyerAddress
154
+ const seller = "0x..."; // de verkoper waar het over gaat
155
+
156
+ const mine = (await claimsForSeller(seller)).filter(
157
+ (c) => c.buyerAddress.toLowerCase() === myAddress.toLowerCase(),
158
+ );
159
+ console.log(JSON.stringify(mine, null, 2));
160
+ ```
161
+
162
+ Elke claim in die lijst is zelfstandig verifieerbaar met `verifyClaim()` (zie hierboven), zonder dat de ontvanger jouw installatie of enige host hoeft te vertrouwen. Dit lost geen vindbaarheid op (D-005: hoe vindt iemand anders jouw claim zonder dat jij 'm deelt) en geen volledigheid over ALLE kopers samen (D-006: dit bewijst alleen wat JIJ indiende, niet wat een host verder mogelijk verzwijgt van andere kopers), maar het geeft een concrete, kosteloze manier om één specifiek geschil te bewijzen zonder een host te hoeven vertrouwen.
163
+
164
+ ## Lokaal draaien
165
+
166
+ ```bash
167
+ npm install
168
+ npm run build # tsc -> dist/
169
+ npm run typecheck # tsc --noEmit
170
+ npm test # vitest run
171
+ npm run demo # end-to-end lokale demo met TEST-sleutels, geen live infra
172
+ npm start # start de MCP-server over stdio (bijv. voor Claude Desktop/Code als lokale MCP-server)
173
+ ```
174
+
175
+ De ledger-locatie is instelbaar via `CAPACITY_ATTEST_DATA_DIR` (default: `./data` in dit package). Tests en de demo gebruiken altijd een eigen, wegwerpbare tijdelijke map, nooit de echte `data/` map.
176
+
177
+ ## Architectuur
178
+
179
+ ```text
180
+ src/
181
+ schema.ts DeliveryClaim zod-schema + content-addressing (computeClaimId, canonicalize)
182
+ signing.ts sign/verify van een claim (ethers, EIP-191 personal-sign)
183
+ ledger.ts append-only JSONL-opslag (data/claims.jsonl), nooit muteerbaar
184
+ tools.ts de daadwerkelijke logica achter beide MCP-tools, transport-onafhankelijk
185
+ config.ts waar de ledger-map leeft, lazy zodat tests 'm kunnen overriden
186
+ index.ts MCP-server wiring (registreert record_delivery + get_delivery_history)
187
+ examples/demo.ts end-to-end lokaal voorbeeld met TEST-sleutels
188
+ ```
189
+
190
+ `tools.ts` bevat de eigenlijke business-logica; `index.ts` vertaalt dat alleen naar MCP tool-calls. Zo kunnen tests en de demo dezelfde logica direct aanroepen zonder een stdio-transport op te tuigen.
191
+
192
+ ## Relatie tot x402
193
+
194
+ Dit project verifieert of settelt zelf géén x402-betalingen, dat gebeurt al bij de betaalstap zelf (zie bijvoorbeeld `mcp-paywall/src/x402.mjs` in dit ecosysteem voor een volledige EIP-3009-verify/settle-implementatie). `settlementRef` verwijst simpelweg naar die reeds-voltooide afwikkeling. Dat betekent ook dat de MVP-koppeling met een echte x402-facilitator eenvoudig kan blijven: `settlementRef` is vrije tekst, met als aanname dat de koper 'm eerlijk invult. Een latere versie kan dat veld optioneel verifiëren tegen een echte facilitator (TODO, niet in deze MVP).
195
+
196
+ ## Relatie tot AWS Bedrock AgentCore Payments
197
+
198
+ Geen overlap, geen concurrentie: verschillende stap in de keten. Bedrock AgentCore Payments (Amazon, sinds 2026) regelt de betaalstap zelf, tot en met het moment dat "the merchant verifies the payment proof... [and] returns the requested content" ([officiële AWS-documentatie](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-how-it-works.html)). Dat bewijs is een bewijs van **betaling**, niet van **levering**: er staat nergens vastgelegd of de agent na die stap ook echt kreeg wat beloofd was. Precies daar begint `capacity-attest`. Net als bij x402 hierboven: dit project settelt geen betalingen en concurreert niet met de betaalrail, het legt vast wat er ná de betaling wel of niet daadwerkelijk aankwam, ongeacht welke rail (x402 of anders) die betaling afhandelde.
@@ -0,0 +1,26 @@
1
+ import { ethers } from "ethers";
2
+ import { type DeliveryClaim } from "./schema.js";
3
+ export declare const BUYER_A: ethers.Wallet;
4
+ export declare const BUYER_B: ethers.Wallet;
5
+ export declare const SELLER_X = "0x00000000000000000000000000000000000000aa";
6
+ export interface DiscoveryFixture {
7
+ a1: DeliveryClaim;
8
+ a2: DeliveryClaim;
9
+ a3: DeliveryClaim;
10
+ b1: DeliveryClaim;
11
+ y1: DeliveryClaim;
12
+ forged: DeliveryClaim;
13
+ tampered: DeliveryClaim;
14
+ }
15
+ export declare function buildDiscoveryFixture(): Promise<DiscoveryFixture>;
16
+ export type ControlKind = "GREEN" | "RED";
17
+ export interface Control {
18
+ id: string;
19
+ kind: ControlKind;
20
+ what: string;
21
+ expected: string;
22
+ actual: string;
23
+ pass: boolean;
24
+ }
25
+ export declare function runControls(): Promise<Control[]>;
26
+ //# sourceMappingURL=discovery-fixture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery-fixture.d.ts","sourceRoot":"","sources":["../src/discovery-fixture.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAKpE,eAAO,MAAM,OAAO,eAA4C,CAAC;AACjE,eAAO,MAAM,OAAO,eAA4C,CAAC;AAGjE,eAAO,MAAM,QAAQ,+CAA+C,CAAC;AASrE,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,aAAa,CAAC;IAClB,EAAE,EAAE,aAAa,CAAC;IAClB,EAAE,EAAE,aAAa,CAAC;IAClB,EAAE,EAAE,aAAa,CAAC;IAClB,EAAE,EAAE,aAAa,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAkBvE;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,KAAK,CAAC;AAC1C,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AASD,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CA2CtD"}
@@ -0,0 +1,95 @@
1
+ // discovery-fixture.ts — a public, reproducible fixture for cross-installation
2
+ // discovery (D-005), the companion to completeness-fixture.ts (D-006).
3
+ //
4
+ // It demonstrates, with claims you can rebuild from fixed test keys, that:
5
+ // 1. The gap is real: buyer B, reading only B's own installation, does not
6
+ // see buyer A's claims about the same seller.
7
+ // 2. Discovery closes it: aggregating B's local ledger with a shared,
8
+ // UNTRUSTED substrate (simulated here, EAS/ERC-8004 in production) makes
9
+ // A's claims visible to B, and every one is re-verified locally.
10
+ // 3. It stays trustless: a malicious source's forged/tampered claims are
11
+ // rejected, and claims about a different seller are filtered out.
12
+ // 4. It composes with D-006: a source that hides a MIDDLE claim in a chain
13
+ // it otherwise reveals is still caught by the completeness analysis, now
14
+ // across installations.
15
+ //
16
+ // Honest boundary, same as everywhere else: discovery is bounded by what
17
+ // sources reveal. It solves findability, not completeness. A source that omits
18
+ // a claim (a hidden tail, or a whole buyer) cannot be forced to reveal it; no
19
+ // aggregation invents what no source shows.
20
+ //
21
+ // Pure, offline, deterministic: fixed keys, fixed timestamps, no ledger, no
22
+ // network. Consumed by examples/discovery-fixture.ts and the discovery tests.
23
+ import { ethers } from "ethers";
24
+ import { signClaim } from "./signing.js";
25
+ import { discoverDeliveryHistory, staticSource } from "./discovery.js";
26
+ // Two fixed TEST buyers on two notional installations, and one impostor.
27
+ // source: simulated (deterministic keys, not real identities).
28
+ export const BUYER_A = new ethers.Wallet("0x" + "a1".repeat(32));
29
+ export const BUYER_B = new ethers.Wallet("0x" + "b2".repeat(32));
30
+ const IMPOSTOR = new ethers.Wallet("0x" + "cc".repeat(32));
31
+ export const SELLER_X = "0x00000000000000000000000000000000000000aa";
32
+ const SELLER_Y = "0x00000000000000000000000000000000000000bb"; // a different seller, for the filter control
33
+ const EVIDENCE_HASH = "a".repeat(64);
34
+ async function sign(wallet, content) {
35
+ const { claimId, signature } = await signClaim(wallet, content);
36
+ return { ...content, claimId, signature };
37
+ }
38
+ export async function buildDiscoveryFixture() {
39
+ const aBase = { sellerAddress: SELLER_X, buyerAddress: BUYER_A.address, assetType: "gpu-hours", evidenceHash: EVIDENCE_HASH };
40
+ const a1 = await sign(BUYER_A, { ...aBase, promisedSpec: "1x A100, 4h", delivered: "yes", settlementRef: "0x" + "a1".repeat(32), timestamp: "2026-01-01T00:00:00.000Z" });
41
+ const a2 = await sign(BUYER_A, { ...aBase, promisedSpec: "1x A100, 8h", delivered: "no", settlementRef: "0x" + "a2".repeat(32), timestamp: "2026-02-01T00:00:00.000Z", priorClaimId: a1.claimId });
42
+ const a3 = await sign(BUYER_A, { ...aBase, promisedSpec: "1x A100, 2h", delivered: "yes", settlementRef: "0x" + "a3".repeat(32), timestamp: "2026-03-01T00:00:00.000Z", priorClaimId: a2.claimId });
43
+ const b1 = await sign(BUYER_B, { sellerAddress: SELLER_X, buyerAddress: BUYER_B.address, assetType: "gpu-hours", promisedSpec: "1x A100, 1h", delivered: "yes", evidenceHash: EVIDENCE_HASH, settlementRef: "0x" + "b1".repeat(32), timestamp: "2026-02-15T00:00:00.000Z" });
44
+ const y1 = await sign(BUYER_A, { sellerAddress: SELLER_Y, buyerAddress: BUYER_A.address, assetType: "gpu-hours", promisedSpec: "other seller", delivered: "yes", evidenceHash: EVIDENCE_HASH, settlementRef: "0x" + "01".repeat(32), timestamp: "2026-01-15T00:00:00.000Z" });
45
+ // forged: content claims buyerAddress = A, but signed by the impostor, so the
46
+ // signature does not recover to A. verifyClaim must reject it.
47
+ const forgedContent = { sellerAddress: SELLER_X, buyerAddress: BUYER_A.address, assetType: "gpu-hours", promisedSpec: "forged", delivered: "yes", evidenceHash: EVIDENCE_HASH, settlementRef: "0x" + "de".repeat(32), timestamp: "2026-04-01T00:00:00.000Z" };
48
+ const forgedSig = await signClaim(IMPOSTOR, forgedContent);
49
+ const forged = { ...forgedContent, ...forgedSig };
50
+ // tampered: take a genuine a1 and flip delivered without recomputing claimId.
51
+ const tampered = { ...a1, delivered: "no" };
52
+ return { a1, a2, a3, b1, y1, forged, tampered };
53
+ }
54
+ function control(id, kind, what, expected, actual) {
55
+ return { id, kind, what, expected, actual, pass: expected === actual };
56
+ }
57
+ function has(result, claim) {
58
+ return result.claims.some((c) => c.claimId.toLowerCase() === claim.claimId.toLowerCase());
59
+ }
60
+ export async function runControls() {
61
+ const { a1, a2, a3, b1, y1, forged, tampered } = await buildDiscoveryFixture();
62
+ const controls = [];
63
+ // D1 GREEN — the gap: B, reading only its own installation, does not see A's negative claim.
64
+ const bOnly = await discoverDeliveryHistory(SELLER_X, [staticSource("installation-B-local", [b1])]);
65
+ controls.push(control("D1", "GREEN", "B's own installation alone does NOT contain A's negative claim (the D-005 gap)", "a2 absent, count=1", `a2 ${has(bOnly, a2) ? "present" : "absent"}, count=${bOnly.count}`));
66
+ // D2 GREEN — findability: B + shared substrate surfaces A's claims to B. The
67
+ // substrate is a RAW source (does not pre-filter by seller) that also carries
68
+ // a Y-seller claim, so discovery's OWN seller filter is what must drop y1.
69
+ const substrate = { name: "shared-substrate (simulated EAS/ERC-8004)", fetchForSeller: async () => [a1, a2, a3, y1] };
70
+ const discovered = await discoverDeliveryHistory(SELLER_X, [staticSource("installation-B-local", [b1]), substrate]);
71
+ controls.push(control("D2", "GREEN", "Discovery surfaces A's negative claim to B across installations", "a2 present, count=4", `a2 ${has(discovered, a2) ? "present" : "absent"}, count=${discovered.count}`));
72
+ // D3 GREEN — every claim in the aggregate is verified.
73
+ const { verifyClaim } = await import("./signing.js");
74
+ const allVerify = discovered.claims.every((c) => verifyClaim(c).ok);
75
+ controls.push(control("D3", "GREEN", "Every claim in the aggregate re-verifies locally", "true", String(allVerify)));
76
+ // D4 GREEN — a claim about a different seller (y1) is filtered out of X's aggregate.
77
+ controls.push(control("D4", "GREEN", "A different-seller claim from a source is filtered out", "y1 absent", `y1 ${has(discovered, y1) ? "present" : "absent"}`));
78
+ // D5 GREEN — dedup: the same claim from two sources is counted once.
79
+ const deduped = await discoverDeliveryHistory(SELLER_X, [staticSource("src1", [b1, a1]), staticSource("src2", [b1, a1])]);
80
+ const src2 = deduped.sources[1];
81
+ controls.push(control("D5", "GREEN", "The same claim from two sources is counted once", "count=2, dup=2", `count=${deduped.count}, dup=${src2?.duplicates}`));
82
+ // D6 RED — a forged claim (wrong signer) from a source is rejected, not surfaced.
83
+ const withForged = await discoverDeliveryHistory(SELLER_X, [staticSource("malicious", [forged])]);
84
+ controls.push(control("D6", "RED", "A forged claim (wrong signer) is rejected, not surfaced", "count=0, rejected>=1", `count=${withForged.count}, ${(withForged.sources[0]?.rejected ?? 0) >= 1 ? "rejected>=1" : "rejected=0"}`));
85
+ // D7 RED — a tampered claim (flipped byte, claimId not recomputed) is rejected.
86
+ const withTampered = await discoverDeliveryHistory(SELLER_X, [staticSource("malicious", [tampered])]);
87
+ controls.push(control("D7", "RED", "A tampered claim (flipped byte) is rejected, not surfaced", "count=0, rejected>=1", `count=${withTampered.count}, ${(withTampered.sources[0]?.rejected ?? 0) >= 1 ? "rejected>=1" : "rejected=0"}`));
88
+ // D8 GREEN — composition with D-006: a substrate that hides A's MIDDLE claim
89
+ // (shows a1 and a3, omits a2) is still caught, now across installations.
90
+ const hiding = await discoverDeliveryHistory(SELLER_X, [staticSource("installation-B-local", [b1]), staticSource("substrate-hiding-a2", [a1, a3])]);
91
+ const flaggedA2 = hiding.completeness.possibleOmissions.some((o) => o.missingPriorClaimId.toLowerCase() === a2.claimId.toLowerCase());
92
+ controls.push(control("D8", "GREEN", "A source hiding a middle claim is caught by completeness, across installations", "true", String(flaggedA2)));
93
+ return controls;
94
+ }
95
+ //# sourceMappingURL=discovery-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery-fixture.js","sourceRoot":"","sources":["../src/discovery-fixture.ts"],"names":[],"mappings":"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"}