ccip-router 0.1.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +528 -0
  3. package/contracts/AttestationIndex.sol +125 -0
  4. package/contracts/NodeRegistry.sol +85 -0
  5. package/dist/attestation/eip712.d.ts +50 -0
  6. package/dist/attestation/eip712.d.ts.map +1 -0
  7. package/dist/attestation/eip712.js +31 -0
  8. package/dist/attestation/eip712.js.map +1 -0
  9. package/dist/attestation/ocp.d.ts +9 -0
  10. package/dist/attestation/ocp.d.ts.map +1 -0
  11. package/dist/attestation/ocp.js +11 -0
  12. package/dist/attestation/ocp.js.map +1 -0
  13. package/dist/attestation/withWyriwe.d.ts +11 -0
  14. package/dist/attestation/withWyriwe.d.ts.map +1 -0
  15. package/dist/attestation/withWyriwe.js +92 -0
  16. package/dist/attestation/withWyriwe.js.map +1 -0
  17. package/dist/chain/abi.d.ts +178 -0
  18. package/dist/chain/abi.d.ts.map +1 -0
  19. package/dist/chain/abi.js +94 -0
  20. package/dist/chain/abi.js.map +1 -0
  21. package/dist/chain/client.d.ts +16762 -0
  22. package/dist/chain/client.d.ts.map +1 -0
  23. package/dist/chain/client.js +21 -0
  24. package/dist/chain/client.js.map +1 -0
  25. package/dist/chain/publish.d.ts +26 -0
  26. package/dist/chain/publish.d.ts.map +1 -0
  27. package/dist/chain/publish.js +66 -0
  28. package/dist/chain/publish.js.map +1 -0
  29. package/dist/chain/register.d.ts +9 -0
  30. package/dist/chain/register.d.ts.map +1 -0
  31. package/dist/chain/register.js +20 -0
  32. package/dist/chain/register.js.map +1 -0
  33. package/dist/crypto/hash.d.ts +3 -0
  34. package/dist/crypto/hash.d.ts.map +1 -0
  35. package/dist/crypto/hash.js +12 -0
  36. package/dist/crypto/hash.js.map +1 -0
  37. package/dist/crypto/index.d.ts +3 -0
  38. package/dist/crypto/index.d.ts.map +1 -0
  39. package/dist/crypto/index.js +3 -0
  40. package/dist/crypto/index.js.map +1 -0
  41. package/dist/crypto/sign.d.ts +6 -0
  42. package/dist/crypto/sign.d.ts.map +1 -0
  43. package/dist/crypto/sign.js +60 -0
  44. package/dist/crypto/sign.js.map +1 -0
  45. package/dist/db/index.d.ts +5 -0
  46. package/dist/db/index.d.ts.map +1 -0
  47. package/dist/db/index.js +13 -0
  48. package/dist/db/index.js.map +1 -0
  49. package/dist/db/schema.d.ts +6 -0
  50. package/dist/db/schema.d.ts.map +1 -0
  51. package/dist/db/schema.js +41 -0
  52. package/dist/db/schema.js.map +1 -0
  53. package/dist/db/sqlite.d.ts +19 -0
  54. package/dist/db/sqlite.d.ts.map +1 -0
  55. package/dist/db/sqlite.js +196 -0
  56. package/dist/db/sqlite.js.map +1 -0
  57. package/dist/db/types.d.ts +34 -0
  58. package/dist/db/types.d.ts.map +1 -0
  59. package/dist/db/types.js +5 -0
  60. package/dist/db/types.js.map +1 -0
  61. package/dist/gateway/__tests__/eip3668.test.d.ts +2 -0
  62. package/dist/gateway/__tests__/eip3668.test.d.ts.map +1 -0
  63. package/dist/gateway/__tests__/eip3668.test.js +31 -0
  64. package/dist/gateway/__tests__/eip3668.test.js.map +1 -0
  65. package/dist/gateway/eip3668.d.ts +14 -0
  66. package/dist/gateway/eip3668.d.ts.map +1 -0
  67. package/dist/gateway/eip3668.js +28 -0
  68. package/dist/gateway/eip3668.js.map +1 -0
  69. package/dist/lib.d.ts +12 -0
  70. package/dist/lib.d.ts.map +1 -0
  71. package/dist/lib.js +8 -0
  72. package/dist/lib.js.map +1 -0
  73. package/dist/mesh/cron.d.ts +4 -0
  74. package/dist/mesh/cron.d.ts.map +1 -0
  75. package/dist/mesh/cron.js +22 -0
  76. package/dist/mesh/cron.js.map +1 -0
  77. package/dist/mesh/records.d.ts +4 -0
  78. package/dist/mesh/records.d.ts.map +1 -0
  79. package/dist/mesh/records.js +47 -0
  80. package/dist/mesh/records.js.map +1 -0
  81. package/dist/mesh/sync.d.ts +5 -0
  82. package/dist/mesh/sync.d.ts.map +1 -0
  83. package/dist/mesh/sync.js +154 -0
  84. package/dist/mesh/sync.js.map +1 -0
  85. package/dist/mesh/vni.d.ts +14 -0
  86. package/dist/mesh/vni.d.ts.map +1 -0
  87. package/dist/mesh/vni.js +52 -0
  88. package/dist/mesh/vni.js.map +1 -0
  89. package/dist/router/CcipRouter.d.ts +26 -0
  90. package/dist/router/CcipRouter.d.ts.map +1 -0
  91. package/dist/router/CcipRouter.js +85 -0
  92. package/dist/router/CcipRouter.js.map +1 -0
  93. package/dist/router/index.d.ts +3 -0
  94. package/dist/router/index.d.ts.map +1 -0
  95. package/dist/router/index.js +2 -0
  96. package/dist/router/index.js.map +1 -0
  97. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Echo-Merlini (dinamic.eth)
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 ADDED
@@ -0,0 +1,528 @@
1
+ # ccip-router
2
+
3
+ **The coordination layer CCIP-Read was missing.**
4
+
5
+ EIP-3668 defines how clients talk to CCIP-Read gateways. It says nothing about how gateways talk to each other. `ccip-router` fills that gap — peer sync, deduplication, signed records, and cryptographic attestation for any CCIP-Read gateway.
6
+
7
+ No ENS required. No agents required. Any CCIP-Read project can plug in a resolver and get a mesh-ready gateway in minutes.
8
+
9
+ ---
10
+
11
+ ## Architecture
12
+
13
+ ### System overview
14
+
15
+ ```mermaid
16
+ flowchart LR
17
+ Client(["CCIP-Read client\nbrowser / contract"])
18
+
19
+ subgraph Node["ccip-router node"]
20
+ Handler["GET /{sender}/{data}.json\nEIP-3668 handler"]
21
+ Resolver["Resolver fn\ncustom logic"]
22
+ Wyriwe["withWyriwe()\nEIP-712 attestation"]
23
+ DB[("SQLite\nWAL · dedup · cursor")]
24
+ Cron["sync cron\n*/5 * * * *"]
25
+ end
26
+
27
+ subgraph Peers["Peer mesh"]
28
+ NodeB["Node B"]
29
+ NodeC["Node C"]
30
+ end
31
+
32
+ subgraph Sepolia["Sepolia (chain 11155111)"]
33
+ AI["AttestationIndex\n0x107D…3698"]
34
+ NR["NodeRegistry\n0x6be4…42b7"]
35
+ end
36
+
37
+ Client -- "EIP-3668 request" --> Handler
38
+ Handler -- "{ data: 0x... }" --> Client
39
+ Handler --> Resolver --> Wyriwe --> DB
40
+ Cron -- "GET /records" --> NodeB & NodeC
41
+ NodeB & NodeC -- "signed records" --> DB
42
+ DB -. "publishAttestation()" .-> AI
43
+ DB -. "register(url, sig)" .-> NR
44
+ ```
45
+
46
+ ### Per-request attestation flow
47
+
48
+ ```mermaid
49
+ sequenceDiagram
50
+ participant Client as CCIP-Read client
51
+ participant GW as ccip-router
52
+ participant Res as Resolver fn
53
+ participant DB as SQLite
54
+ participant Chain as AttestationIndex
55
+
56
+ Client->>GW: GET /{sender}/{data}.json
57
+ GW->>Res: resolve(sender, calldata, namespace)
58
+ Res-->>GW: response bytes
59
+
60
+ Note over GW: withWyriwe() — attestation pipeline
61
+ GW->>GW: rawInputHash = keccak256(calldata)
62
+ GW->>GW: inputHash = rawInputHash (sentinel)<br/>or keccak256(abi.encode(raw, pipelineHash))
63
+ GW->>GW: outputHash = keccak256(response)
64
+ GW->>GW: commitmentHash = keccak256(agentId · modelHash · inputHash · outputHash · ts)
65
+ GW->>GW: EIP-712 sign WyriweAttestation
66
+ GW->>DB: INSERT OR IGNORE signed attestation record
67
+ GW-->>Client: { data: "0x..." }
68
+
69
+ Note over DB,Chain: async — admin-triggered batch publish
70
+ DB->>Chain: record(attestation, sig)
71
+ Chain-->>DB: signerOf[commitmentHash] anchored
72
+ ```
73
+
74
+ ### Attestation stack
75
+
76
+ ```mermaid
77
+ graph TB
78
+ T["EIP-3668 · Transport\nCCIP-Read client-to-gateway"]
79
+ S["EIP-191 · Record signing\nkeccak256(inputHash · namespace · valueHash · ts)"]
80
+ W["WYRIWE · Input provenance\nsentinel path: inputHash = rawInputHash\nnon-sentinel: inputHash = keccak256(abi.encode(raw, pipelineHash))"]
81
+ I["ERC-8004 · Agent identity\nagentId · registryAddress declared on-chain"]
82
+ O["OCP / ERC-8263 · Observation commitment\ncommitmentHash = keccak256(agentId · modelHash · inputHash · outputHash · ts)"]
83
+ A["EIP-712 · WyriweAttestation\nstructured signing · verifiable by any peer · synced by mesh"]
84
+ V["VNI · Node identity\nEIP-191 signed { nodeId · signerAddress · url · version · ts }"]
85
+ C["On-chain anchoring · Sepolia\nAttestationIndex — signerOf · commitmentOf\nNodeRegistry — register(url, sig)"]
86
+
87
+ T --> S --> W --> I --> O --> A --> V --> C
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Contracts
93
+
94
+ Both contracts are permissionless — no owner, no admin. One deployment per chain serves all nodes.
95
+
96
+ | Contract | Sepolia address | Purpose |
97
+ |---|---|---|
98
+ | `AttestationIndex` | [`0x107D706112225aC57eCf6692FBbDC283fb6E3698`](https://sepolia.etherscan.io/address/0x107D706112225aC57eCf6692FBbDC283fb6E3698) | Anchors EIP-712 `WyriweAttestation` records on-chain. Stores `signerOf[commitmentHash]` and `commitmentOf[inputHash]`. |
99
+ | `NodeRegistry` | [`0x6be4966596A9CBaa7260ab6EbbFFA69bBC9a42b7`](https://sepolia.etherscan.io/address/0x6be4966596A9CBaa7260ab6EbbFFA69bBC9a42b7) | Public directory of nodes. `register(url, sig)` proves key ownership via EIP-191 — the relayer (`msg.sender`) does not need to be the signing key. |
100
+
101
+ Deployed by [`0xFf9a176577Fb42b6bc9c19fd05a241e8fCd0ca14`](https://sepolia.etherscan.io/address/0xFf9a176577Fb42b6bc9c19fd05a241e8fCd0ca14) · Solc 0.8.24 · optimizer 200 runs.
102
+
103
+ **To use on Sepolia:** open the admin panel → Deploy contracts → select Sepolia → "Use these addresses →". Addresses are saved to config automatically, no deployment needed.
104
+
105
+ **To deploy to another chain:** open the admin panel → Deploy contracts → select the chain → connect wallet → two transactions (one per contract). No private key is stored — MetaMask signs everything in-browser.
106
+
107
+ Source: [`contracts/AttestationIndex.sol`](contracts/AttestationIndex.sol) · [`contracts/NodeRegistry.sol`](contracts/NodeRegistry.sol)
108
+
109
+ ---
110
+
111
+ ## Two tiers
112
+
113
+ ### Basic — plug in a resolver, get a gateway + mesh
114
+
115
+ ```typescript
116
+ import { CcipRouter } from 'ccip-router'
117
+
118
+ const ccip = new CcipRouter({
119
+ namespace: 'token-metadata',
120
+ db,
121
+ gatewayKey: process.env.GATEWAY_PRIVATE_KEY,
122
+ resolver: async (sender, calldata, namespace) => {
123
+ return encodeMyResponse(calldata)
124
+ },
125
+ })
126
+
127
+ app.route('/', ccip.hono())
128
+ ```
129
+
130
+ What you get:
131
+ - CCIP-Read handler (`/{sender}/{data}.json`)
132
+ - EIP-191 signed records written to SQLite on every call
133
+ - Mesh peer sync (`GET /records?since=&namespace=&limit=&cursor=`)
134
+ - Record deduplication — same `inputHash` never inserted twice
135
+ - Admin dashboard at `/admin` with peer management + sync controls
136
+ - Setup wizard at `/setup` on first boot
137
+
138
+ ### Advanced — wrap any resolver with WYRIWE EIP-712 attestation
139
+
140
+ ```typescript
141
+ import { CcipRouter, withWyriwe } from 'ccip-router'
142
+
143
+ const ccip = new CcipRouter({
144
+ namespace: 'agent-attestations',
145
+ db,
146
+ gatewayKey: config.gatewayKey,
147
+ resolver: withWyriwe(myAgentResolver, {
148
+ gatewayKey: config.gatewayKey,
149
+ registryAddress: process.env.REGISTRY_ADDRESS as `0x${string}`,
150
+ agentId: process.env.AGENT_ID as `0x${string}`,
151
+ modelHash: process.env.MODEL_HASH as `0x${string}`,
152
+ chainId: 1,
153
+ // sanitizationCID: 'ipfs://Qm...', // omit for sentinel (identity) path
154
+ }),
155
+ })
156
+ ```
157
+
158
+ What `withWyriwe()` adds on top of basic:
159
+ - Triple-hash chain — two paths:
160
+ - **Sentinel** (default): `sanitizationPipelineHash = keccak256("IDENTITY_SENTINEL")`, `inputHash = rawInputHash`
161
+ - **Non-sentinel** (`sanitizationCID` set): `sanitizationPipelineHash = keccak256(CID)`, `inputHash = keccak256(abi.encode(rawInputHash, sanitizationPipelineHash))`
162
+ - EIP-712 `WyriweAttestation` signed with the gateway key on every resolver call
163
+ - Attestation records persisted to `{namespace}:wyriwe` — synced by the mesh automatically
164
+ - Verifiable by any peer: recover signer from signature, match against known gateway address
165
+
166
+ ---
167
+
168
+ ## Quick start (setup wizard)
169
+
170
+ ```bash
171
+ npm install
172
+ npm run dev
173
+ # → open http://localhost:3000
174
+ # → setup wizard walks you through key generation + config
175
+ # → config.json is written, node restarts, /admin loads
176
+ ```
177
+
178
+ Or configure via environment (no wizard needed):
179
+
180
+ ```bash
181
+ cp .env.example .env
182
+ # set GATEWAY_PRIVATE_KEY, ADMIN_SECRET, PEERS, etc.
183
+ npm run dev
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Environment variables
189
+
190
+ | Variable | Required | Default | Description |
191
+ |---|---|---|---|
192
+ | `GATEWAY_PRIVATE_KEY` | Yes* | — | 32-byte hex signing key (`0x...`). Without it the node runs in dry-run mode (unsigned records). |
193
+ | `ADMIN_SECRET` | No | — | Protects `/admin`. Set for any non-local deployment. Without it the dashboard is open. |
194
+ | `PORT` | No | `3000` | HTTP port |
195
+ | `DB_PATH` | No | `./data.db` | SQLite file path |
196
+ | `SYNC_NAMESPACE` | No | `agent-attestations` | Record namespace — peers must match |
197
+ | `SYNC_INTERVAL` | No | `*/5 * * * *` | Cron expression for peer sync |
198
+ | `PEERS` | No | — | Comma-separated peer URLs |
199
+ | `AGENT_ID` | No | — | ERC-8004 agent identity (bytes32 hex). Enables `/identity` endpoint. |
200
+ | `REGISTRY_ADDRESS` | No | — | ERC-8004 on-chain registry address. Required alongside `AGENT_ID`. |
201
+ | `CHAIN_ID` | No | `1` | Chain where the ERC-8004 registry is deployed. |
202
+ | `ATTESTATION_INDEX` | No | — | Deployed `AttestationIndex` contract address. Enables on-chain anchoring. |
203
+ | `NODE_REGISTRY` | No | — | Deployed `NodeRegistry` contract address. Enables on-chain node registration. |
204
+ | `RPC_URL` | No | — | JSON-RPC endpoint. Required alongside `ATTESTATION_INDEX`. |
205
+ | `MODEL_HASH` | No | — | `keccak256` of model weights CID. Required to activate WYRIWE attestation. |
206
+
207
+ \* Can also come from `config.json` written by the setup wizard.
208
+
209
+ ---
210
+
211
+ ## Admin dashboard
212
+
213
+ Visit `/admin` after setup. Features:
214
+ - Live stats: record count, peer count, last sync time
215
+ - Peer panel: add/remove peers, per-peer health + signer address + last sync
216
+ - Recent records panel: local vs peer-synced, timestamps
217
+ - Manual sync trigger
218
+ - Auto-refresh every 15 seconds
219
+
220
+ **Auth:** If `ADMIN_SECRET` is set, `/admin` requires a login (cookie session, 7-day). API routes also accept `Authorization: Bearer <secret>` for programmatic access.
221
+
222
+ **Stack status row:** A compact pill row below the header shows which tiers are active — Signing / ERC-8004 / WYRIWE / OCP — derived from `/admin/api/status`. Green = active, grey = unconfigured.
223
+
224
+ **Node logs panel:** Live ring buffer of the last 200 log lines (info/warn/error), colour-coded. Auto-refreshes every 10 seconds.
225
+
226
+ ---
227
+
228
+ ## Two-node local test
229
+
230
+ Uses Hardhat dev keys — safe for local testing only, **never use in production**.
231
+
232
+ ### Option A — two terminals
233
+
234
+ **Terminal 1 — node A**
235
+ ```bash
236
+ PORT=3001 DB_PATH=./node-a.db \
237
+ GATEWAY_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
238
+ PEERS=http://localhost:3002 SYNC_INTERVAL="*/1 * * * *" \
239
+ npm run dev
240
+ ```
241
+
242
+ **Terminal 2 — node B**
243
+ ```bash
244
+ PORT=3002 DB_PATH=./node-b.db \
245
+ GATEWAY_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d \
246
+ PEERS=http://localhost:3001 SYNC_INTERVAL="*/1 * * * *" \
247
+ npm run dev
248
+ ```
249
+
250
+ ### Option B — Docker
251
+
252
+ ```bash
253
+ docker compose up --build
254
+ ```
255
+
256
+ ### Verify mesh sync
257
+
258
+ ```bash
259
+ # trigger a CCIP call on node A — writes a signed record
260
+ curl http://localhost:3001/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266/0xdeadbeef
261
+
262
+ # check node A recorded it
263
+ curl http://localhost:3001/health | jq .records # → 1
264
+
265
+ # wait ~1 minute for sync cron, then check node B
266
+ curl http://localhost:3002/health | jq .records # → 1 (synced from A)
267
+
268
+ # verify by inputHash on node B
269
+ curl http://localhost:3002/verify/<inputHash>
270
+ ```
271
+
272
+ ---
273
+
274
+ ## API reference
275
+
276
+ ### CCIP-Read handler
277
+ ```
278
+ GET /{sender}/{data}.json
279
+ → { data: "0x..." } EIP-3668 response
280
+ ```
281
+
282
+ ### Mesh sync (any peer can pull this)
283
+ ```
284
+ GET /records?namespace=<str>&since=<unix>&limit=<n>&cursor=<str>
285
+ → {
286
+ protocol: 1,
287
+ node_version: "0.1.0",
288
+ namespace: "agent-attestations",
289
+ records: [{ inputHash, namespace, key, value, timestamp, signature, sourcePeer }],
290
+ cursor: "<next>" | null
291
+ }
292
+ ```
293
+
294
+ ### OCP observation commitment (ERC-8263)
295
+ ```
296
+ GET /ocp/:inputHash
297
+ → {
298
+ inputHash,
299
+ found: true,
300
+ commitmentHash: "0x...",
301
+ observation: { agentId, modelHash, inputHash, outputHash, timestamp },
302
+ namespace, sourcePeer
303
+ }
304
+ → { inputHash, found: false } (404 — no WYRIWE attestation for this inputHash)
305
+ ```
306
+
307
+ ### Attestation lookup
308
+ ```
309
+ GET /verify/:inputHash
310
+ → {
311
+ inputHash,
312
+ found: true,
313
+ proofs: [
314
+ {
315
+ namespace: "agent-attestations",
316
+ signingType: "EIP-191",
317
+ verified: true,
318
+ signer: "0x...",
319
+ signature: "0x...",
320
+ timestamp: 1234567890,
321
+ sourcePeer: null | "https://..."
322
+ },
323
+ {
324
+ namespace: "agent-attestations:wyriwe",
325
+ signingType: "EIP-712 WyriweAttestation",
326
+ verified: true,
327
+ signer: "0x...",
328
+ signature: "0x...",
329
+ timestamp: 1234567890,
330
+ attestation: { agentId, registry, modelHash, rawInputHash,
331
+ sanitizationPipelineHash, inputHash, outputHash }
332
+ }
333
+ ]
334
+ }
335
+ → { inputHash, found: false } (404)
336
+ ```
337
+
338
+ ### Identity (ERC-8004)
339
+ ```
340
+ GET /identity
341
+ → { declared: true, agentId, registryAddress, chainId, namespace, signerAddress }
342
+ → { declared: false } (404 — AGENT_ID not configured)
343
+ ```
344
+
345
+ ### Node identity (VNI)
346
+ ```
347
+ GET /vni
348
+ → { nodeId, signerAddress, url, version, timestamp, signature }
349
+ → { declared: false } (404 — NODE_URL not configured)
350
+ ```
351
+
352
+ ### Peer gossip
353
+ ```
354
+ GET /peers
355
+ → { protocol: 1, node_version, signerAddress, peers: [{ url, signerAddress, healthy, lastSyncAt }] }
356
+ ```
357
+
358
+ ### Contributions (ERC-8275)
359
+ ```
360
+ GET /contributions
361
+ → { namespace, contributions: [{ source, records }] }
362
+ ```
363
+
364
+ ### Health
365
+ ```
366
+ GET /health
367
+ → {
368
+ ok, version, namespace, signerAddress,
369
+ identity: { agentId, registryAddress, chainId } | null,
370
+ tiers: { signed, erc8004, wyriwe, ocp },
371
+ peers, records
372
+ }
373
+ ```
374
+
375
+ ### Admin API (requires auth if ADMIN_SECRET set)
376
+ ```
377
+ GET /admin/api/status node info, peers, recent records, tiers
378
+ GET /admin/api/logs last 200 log lines [{ ts, level, msg }]
379
+ GET /admin/api/audit per-spec compliance report (EIP-3668/WYRIWE/ERC-8004/OCP)
380
+ POST /admin/api/sync trigger immediate peer sync
381
+ POST /admin/api/publish batch-publish recent WYRIWE records to AttestationIndex
382
+ body: { limit?: number } (default 50, max 200)
383
+ → { published, skipped, errors }
384
+ POST /admin/api/peers { url } — add peer
385
+ DEL /admin/api/peers { url } — remove peer
386
+ POST /admin/login { secret } — set session cookie
387
+ POST /admin/logout clear session cookie
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Mesh sync protocol
393
+
394
+ Any CCIP-Read gateway implementing `/records` is mesh-compatible:
395
+
396
+ ```
397
+ GET /records?since=<unix>&namespace=<string>&limit=<n>&cursor=<string>
398
+ → { protocol: 1, node_version, namespace, records: [...], cursor: string | null }
399
+ ```
400
+
401
+ Protocol version `1` is the current stable spec. Nodes on a different version are skipped during sync with a warning.
402
+
403
+ **Namespaces** are application-defined and scoped at the record level:
404
+ - `agent-attestations` — ENS Boiler / ERC-8004 agents
405
+ - `agent-attestations:wyriwe` — WYRIWE EIP-712 attestations (auto-produced by `withWyriwe()`)
406
+ - `token-metadata` — NFT gateways
407
+ - anything — define your own
408
+
409
+ ---
410
+
411
+ ## ERC / spec alignment
412
+
413
+ | Spec | Layer | Role | Status |
414
+ |---|---|---|---|
415
+ | EIP-3668 | Transport | CCIP-Read client-to-gateway | ✅ implemented |
416
+ | WYRIWE | L2 Input trust | Triple-hash commitment, EIP-712 attestation | ✅ implemented |
417
+ | ERC-8004 | L1 Identity | Agent identity `agentId` + `registryAddress` in attestation | ✅ implemented |
418
+ | OCP / ERC-8263 | L3 Observation | Observation commitment hash | ✅ implemented |
419
+ | EIP-712 | L4 Attestation | Structured signing (via `withWyriwe`) | ✅ implemented |
420
+ | VNI | L5 Node Identity | Signed node identity, peer gossip | ✅ implemented |
421
+ | ERC-8275 | L6 Economics | Contribution attribution (MVP) | ✅ implemented |
422
+
423
+ ---
424
+
425
+ ## Roadmap
426
+
427
+ ### Done
428
+ - [x] CCIP-Read gateway (EIP-3668)
429
+ - [x] SQLite record store — WAL mode, composite PK `(inputHash, namespace)`, cursor pagination
430
+ - [x] DB versioned migrations (`schema_version` table, v1 applied on first boot)
431
+ - [x] EIP-191 signed records (basic tier)
432
+ - [x] Mesh peer sync with protocol version check
433
+ - [x] Setup wizard (`/setup`) — key generation, config.json persistence
434
+ - [x] Admin dashboard (`/admin`) — peers, records, sync
435
+ - [x] Admin auth — cookie session + Bearer token (`ADMIN_SECRET`)
436
+ - [x] `withWyriwe()` — EIP-712 attestation, triple-hash chain, IDENTITY_SENTINEL path
437
+ - [x] `/verify` — clean proof per namespace: `{ verified, signer, signingType, signature, attestation }`
438
+ - [x] ERC-8004 identity — `AGENT_ID` + `REGISTRY_ADDRESS` + `CHAIN_ID`, `/identity` endpoint, `/health` field
439
+ - [x] OCP / ERC-8263 — `commitmentHash` in `WyriweAttestation`, `/ocp/:inputHash` endpoint
440
+ - [x] Router SVG favicon, dinamic.eth design language
441
+ - [x] Peer signer pinning — reject records with unexpected signer after first sync
442
+ - [x] Peer health polling — fetch `/health` after every sync, populate `nodeVersion` + `signerAddress`
443
+ - [x] Graceful shutdown — `SIGTERM`/`SIGINT` → `server.close()` → `db.close()` → `process.exit(0)`
444
+ - [x] In-memory log ring buffer (200 lines, console-patched) → `/admin/api/logs` + colour-coded log panel
445
+ - [x] Stack status pills in admin header bar — Signing / ERC-8004 / WYRIWE / OCP
446
+ - [x] Library re-export (`src/lib.ts`) — `CcipRouter`, `withWyriwe`, `IdentityOpts`, `WyriweOpts`, `ResolverFn`, DB types
447
+
448
+ - [x] `withWyriwe()` non-sentinel path — `sanitizationCID` option; `inputHash = keccak256(abi.encode(rawInputHash, sanitizationPipelineHash))`
449
+ - [x] Setup wizard reconfigure flow — pre-fills current config, "Keep existing key", `/setup/current-config` endpoint, inherited admin secret
450
+
451
+ - [x] Spec audit accordion panel in admin — per-spec cards (EIP-3668 / WYRIWE / ERC-8004 / OCP), inline summary pills, expandable detail grid with missing-config hints
452
+ - [x] `contracts/AttestationIndex.sol` — on-chain anchor for WyriweAttestations; verifies EIP-712 sig against ERC-8004 registry domain, stores `signerOf[commitmentHash]` + `commitmentOf[inputHash]`
453
+ - [x] `src/chain/` — viem public + wallet clients, `publishAttestation()`, `checkOnChain()`
454
+ - [x] `/verify` on-chain fallback — if `inputHash` not in local DB and `ATTESTATION_INDEX` + `RPC_URL` configured, queries contract and returns on-chain proof
455
+ - [x] `POST /admin/api/publish` — batch-publish recent WYRIWE records to `AttestationIndex`; skips already-anchored; "Publish to chain" button in spec audit panel
456
+ - [x] Open node network — `GET /peers` gossip endpoint; auto-discovery pulls peer lists during sync (bounded at 10/cycle, disable with `AUTO_DISCOVER=false`)
457
+ - [x] VNI (Verifiable Node Identity) — `GET /vni` returns EIP-191 signed `{ nodeId, signerAddress, url, version, timestamp }`; peers verify during sync for authoritative signer resolution
458
+ - [x] `contracts/NodeRegistry.sol` — on-chain node directory; `register(url, sig)` proves key ownership; `POST /admin/api/register` + "Register on-chain" button in VNI spec card
459
+ - [x] ERC-8275 economics (MVP) — contribution attribution via `getContributions(namespace)`; `GET /contributions`; per-peer record counts surfaced in spec audit panel
460
+ - [x] Config: `NODE_URL`, `NODE_REGISTRY`, `AUTO_DISCOVER`; `/health` exposes `tiers.vni` + `tiers.onChain`
461
+
462
+ ### Next — UI & node management
463
+
464
+ **Stack status bar**
465
+ - [x] Add VNI + On-chain tier pills
466
+ - [x] Click signer address pill to copy to clipboard (green flash feedback)
467
+
468
+ **Node info & layout**
469
+ - [x] Move node info bar above the peers/records panels; add namespace field
470
+ - [x] Toast-based error feedback in add-peer form (replaces `alert()`)
471
+
472
+ **Node config panel** *(in-dashboard, no env editing required)*
473
+ - [x] Full config panel — Core / Signing / Network / Identity / Chain / Admin sections
474
+ - [x] `GET /admin/api/config` — safe config snapshot (signer address, never the key)
475
+ - [x] `POST /admin/api/config` — writes `config.json`, preserves gateway key, restarts node
476
+ - [x] Auto-discover toggle, seed peers textarea, unsaved-changes indicator
477
+
478
+ **Wallet & signing**
479
+ - [x] Signing key panel — generate or import, rotate with identity-change warning, `POST /admin/api/key`
480
+ - [x] Dry-run banner — shown when no key configured, "Configure key →" scrolls to key panel
481
+
482
+ **Setup wizard — node owner onboarding**
483
+ - [x] Admin secret as dedicated step 2 — prominent warning box, two-step skip confirmation
484
+ - [x] Post-setup checklist — signing ✓, admin ✓/⚠, WYRIWE/ERC-8004/VNI ○ with next-step hints
485
+
486
+ ---
487
+
488
+ ## Testing
489
+
490
+ The test suite uses the Node.js built-in test runner (`node:test`) with `tsx` for ESM TypeScript — no extra test framework required.
491
+
492
+ ```bash
493
+ npm test
494
+ ```
495
+
496
+ Expected output:
497
+
498
+ ```
499
+ ℹ tests 49
500
+ ℹ suites 16
501
+ ℹ pass 49
502
+ ℹ fail 0
503
+ ```
504
+
505
+ ### What is tested
506
+
507
+ | File | Coverage |
508
+ |---|---|
509
+ | `src/__tests__/gateway.test.ts` | `decodeRequest` — address + calldata parsing, `.json` suffix stripping, `CcipRequestError` on bad inputs; `encodeResponse` envelope |
510
+ | `src/__tests__/crypto.test.ts` | `signRecord` / `recoverRecordSigner` round-trip; `verifyRecord` correct signer → `true`, wrong signer / tampered value → `false` |
511
+ | `src/__tests__/db.test.ts` | `insertRecord`, `getRecord` (with/without namespace), `getRecordsByInputHash`, `INSERT OR IGNORE` deduplication, cursor pagination, `getContributions` grouping, peer upsert + remove |
512
+ | `src/__tests__/ocp.test.ts` | `buildCommitmentHash` determinism, 32-byte hex output, field-sensitivity (agentId / outputHash / timestamp) |
513
+ | `src/__tests__/wyriwe.test.ts` | Sentinel path (`inputHash === rawInputHash`), non-sentinel path (`keccak256(abi.encode(rawInputHash, sanitizationPipelineHash))`), paths produce distinct hashes for same calldata |
514
+ | `src/__tests__/vni.test.ts` | `makeVni` field shape + stable `nodeId`; `verifyVni` round-trip; tamper detection (url / signerAddress / nodeId → `null`) |
515
+
516
+ Tests use `SQLiteDB(':memory:')` directly (bypassing the runtime singleton) and Hardhat dev key 0 for any signing operations — both are safe to commit and require no external services.
517
+
518
+ ---
519
+
520
+ ## Related
521
+
522
+ - [ens-boiler](https://github.com/Echo-Merlini/ens-boiler) — opinionated ENS agent stack built on `ccip-router`
523
+ - [WYRIWE](https://github.com/TMerlini/wyriwe) — input provenance spec
524
+ - [OCP](https://github.com/damonzwicker/observation-commitment-protocol) — observation commitment protocol
525
+
526
+ ---
527
+
528
+ *dinamic.eth*
@@ -0,0 +1,125 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ /// @title AttestationIndex
5
+ /// @notice On-chain anchor for WYRIWE EIP-712 attestations produced by ccip-router nodes.
6
+ /// Any gateway may call `record()` to anchor a signed WyriweAttestation.
7
+ /// Signature is verified against the ERC-8004 registry domain used at signing time.
8
+ ///
9
+ /// @dev Deploy on the same chain as configured in CHAIN_ID / opts.chainId so that
10
+ /// block.chainid matches the chainId used in the EIP-712 domain during signing.
11
+ contract AttestationIndex {
12
+
13
+ // ── Types ─────────────────────────────────────────────────────────────────
14
+
15
+ struct WyriweAttestation {
16
+ bytes32 agentId;
17
+ address registry; // ERC-8004 registry — used as EIP-712 verifyingContract
18
+ bytes32 modelHash;
19
+ bytes32 rawInputHash;
20
+ bytes32 sanitizationPipelineHash;
21
+ bytes32 inputHash;
22
+ bytes32 outputHash;
23
+ bytes32 commitmentHash;
24
+ uint256 timestamp;
25
+ }
26
+
27
+ // ── Constants ─────────────────────────────────────────────────────────────
28
+
29
+ bytes32 private constant DOMAIN_TYPEHASH = keccak256(
30
+ "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
31
+ );
32
+
33
+ bytes32 private constant ATTESTATION_TYPEHASH = keccak256(
34
+ "WyriweAttestation(bytes32 agentId,address registry,bytes32 modelHash,"
35
+ "bytes32 rawInputHash,bytes32 sanitizationPipelineHash,bytes32 inputHash,"
36
+ "bytes32 outputHash,bytes32 commitmentHash,uint256 timestamp)"
37
+ );
38
+
39
+ // ── Storage ───────────────────────────────────────────────────────────────
40
+
41
+ /// @notice Returns the signer address for a commitmentHash (zero = not recorded).
42
+ mapping(bytes32 => address) public signerOf;
43
+
44
+ /// @notice Returns the latest commitmentHash anchored for a given inputHash.
45
+ mapping(bytes32 => bytes32) public commitmentOf;
46
+
47
+ // ── Events ────────────────────────────────────────────────────────────────
48
+
49
+ event AttestationRecorded(
50
+ bytes32 indexed commitmentHash,
51
+ bytes32 indexed inputHash,
52
+ bytes32 indexed agentId,
53
+ address signer,
54
+ uint256 timestamp
55
+ );
56
+
57
+ // ── Write ─────────────────────────────────────────────────────────────────
58
+
59
+ /// @notice Anchor a WyriweAttestation on-chain.
60
+ /// Verifies the EIP-712 signature against the domain used at signing time
61
+ /// (name="WyriweAttestation", version="1", chainId=block.chainid,
62
+ /// verifyingContract=a.registry).
63
+ /// @param a The attestation struct matching the off-chain signed message.
64
+ /// @param signature 65-byte EIP-712 signature produced by the gateway key.
65
+ /// @return signer Recovered signer address.
66
+ function record(
67
+ WyriweAttestation calldata a,
68
+ bytes calldata signature
69
+ ) external returns (address signer) {
70
+ require(signerOf[a.commitmentHash] == address(0), "AttestationIndex: already recorded");
71
+
72
+ // Reconstruct the EIP-712 domain separator as signed by the gateway.
73
+ // verifyingContract is a.registry (ERC-8004 registry) — NOT this contract.
74
+ bytes32 domainSeparator = keccak256(abi.encode(
75
+ DOMAIN_TYPEHASH,
76
+ keccak256("WyriweAttestation"),
77
+ keccak256("1"),
78
+ block.chainid,
79
+ a.registry
80
+ ));
81
+
82
+ bytes32 structHash = keccak256(abi.encode(
83
+ ATTESTATION_TYPEHASH,
84
+ a.agentId, a.registry, a.modelHash,
85
+ a.rawInputHash, a.sanitizationPipelineHash,
86
+ a.inputHash, a.outputHash,
87
+ a.commitmentHash, a.timestamp
88
+ ));
89
+
90
+ bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
91
+ signer = _recover(digest, signature);
92
+ require(signer != address(0), "AttestationIndex: invalid signature");
93
+
94
+ signerOf[a.commitmentHash] = signer;
95
+ commitmentOf[a.inputHash] = a.commitmentHash;
96
+
97
+ emit AttestationRecorded(a.commitmentHash, a.inputHash, a.agentId, signer, a.timestamp);
98
+ }
99
+
100
+ // ── Read ──────────────────────────────────────────────────────────────────
101
+
102
+ /// @notice Returns true if a commitmentHash has been anchored.
103
+ function isRecorded(bytes32 commitmentHash) external view returns (bool) {
104
+ return signerOf[commitmentHash] != address(0);
105
+ }
106
+
107
+ // ── Internal ──────────────────────────────────────────────────────────────
108
+
109
+ function _recover(bytes32 digest, bytes calldata sig) internal pure returns (address) {
110
+ require(sig.length == 65, "AttestationIndex: bad sig length");
111
+ bytes32 r;
112
+ bytes32 s;
113
+ uint8 v;
114
+ assembly {
115
+ r := calldataload(sig.offset)
116
+ s := calldataload(add(sig.offset, 32))
117
+ v := byte(0, calldataload(add(sig.offset, 64)))
118
+ }
119
+ if (v < 27) v += 27;
120
+ require(v == 27 || v == 28, "AttestationIndex: bad v");
121
+ address recovered = ecrecover(digest, v, r, s);
122
+ require(recovered != address(0), "AttestationIndex: ecrecover failed");
123
+ return recovered;
124
+ }
125
+ }