ccip-router 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -15
- package/dist/mesh/records.js +2 -2
- package/dist/mesh/vni.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,6 +165,40 @@ What you get:
|
|
|
165
165
|
- Admin dashboard at `/admin` with peer management + sync controls
|
|
166
166
|
- Setup wizard at `/setup` on first boot
|
|
167
167
|
|
|
168
|
+
### ENS — built-in wildcard resolver
|
|
169
|
+
|
|
170
|
+
`withEns()` decodes `resolve(bytes name, bytes data)` calldata (EIP-137 wildcard pattern), dispatches to a clean handler, and ABI-encodes the response. DNS wire-format, selector dispatch, and null-to-zero-value fallbacks are handled for you.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { CcipRouter, withEns } from 'ccip-router'
|
|
174
|
+
import type { EnsResolverFn } from 'ccip-router'
|
|
175
|
+
|
|
176
|
+
const resolver: EnsResolverFn = async (name, record) => {
|
|
177
|
+
// name → "vitalik.eth"
|
|
178
|
+
// record → { type: 'addr' } | { type: 'addr', coinType: 60n }
|
|
179
|
+
// { type: 'text', key: 'avatar' } | { type: 'contenthash' }
|
|
180
|
+
return db.lookup(name, record) // return string or null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const ccip = new CcipRouter({
|
|
184
|
+
namespace: 'ens-offchain',
|
|
185
|
+
db,
|
|
186
|
+
gatewayKey: process.env.GATEWAY_PRIVATE_KEY,
|
|
187
|
+
resolver: withEns(resolver),
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Standalone mode:** ENS records are managed from the admin panel ("ENS Records" panel — no code required). Any name pointing to this gateway via an on-chain CCIP-Read wildcard resolver is served automatically.
|
|
192
|
+
|
|
193
|
+
**Compose with attestation:**
|
|
194
|
+
```typescript
|
|
195
|
+
resolver: withWyriwe(withEns(resolver), attestationOpts)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use `isEnsCalldata(calldata)` to safely gate `withEns()` in a multi-purpose resolver that also handles non-ENS calldata.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
168
202
|
### Advanced — wrap any resolver with WYRIWE EIP-712 attestation
|
|
169
203
|
|
|
170
204
|
```typescript
|
|
@@ -200,9 +234,13 @@ What `withWyriwe()` adds on top of basic:
|
|
|
200
234
|
```bash
|
|
201
235
|
npm install
|
|
202
236
|
npm run dev
|
|
203
|
-
# → open http://localhost:3000
|
|
204
|
-
# →
|
|
205
|
-
# →
|
|
237
|
+
# → open http://localhost:3000/setup
|
|
238
|
+
# → step 1: generate or import your signing key
|
|
239
|
+
# → step 2: optional Bearer secret for CLI access
|
|
240
|
+
# → step 3: namespace, port, sync interval
|
|
241
|
+
# → step 4: confirm → config.json written, node restarts
|
|
242
|
+
# → /admin/login: connect any MetaMask wallet → first signer claims admin
|
|
243
|
+
# → /admin: dashboard ready
|
|
206
244
|
```
|
|
207
245
|
|
|
208
246
|
Or configure via environment (no wizard needed):
|
|
@@ -244,12 +282,17 @@ Visit `/admin` after setup. Features:
|
|
|
244
282
|
- Live stats: record count, peer count, last sync time
|
|
245
283
|
- Peer panel: add/remove peers, per-peer health + signer address + last sync
|
|
246
284
|
- Recent records panel: local vs peer-synced, timestamps
|
|
285
|
+
- ENS records panel — add/edit/delete addr, text, contenthash records without a restart
|
|
247
286
|
- Manual sync trigger
|
|
248
287
|
- Auto-refresh every 15 seconds
|
|
249
288
|
|
|
250
|
-
**Auth:**
|
|
289
|
+
**Auth — claim on first login (EIP-4361 SIWE):** On a fresh node the login page shows an amber "Unclaimed node" banner. Connect any browser wallet and sign once — that wallet address is saved to `config.json` as the permanent admin. Subsequent logins must match that address. Admin wallet is completely decoupled from the gateway signing key (`GATEWAY_PRIVATE_KEY` stays server-side).
|
|
251
290
|
|
|
252
|
-
**
|
|
291
|
+
**Transfer admin:** While logged in, open the "Admin wallet" panel → Transfer. Switch MetaMask to the new wallet, sign a transfer message to prove ownership — `adminAddress` is updated live and a new session is issued, no restart required.
|
|
292
|
+
|
|
293
|
+
**Bearer fallback:** `Authorization: Bearer <ADMIN_SECRET>` always works for CLI / scripts regardless of SIWE state.
|
|
294
|
+
|
|
295
|
+
**Stack status row:** A compact pill row below the header shows which tiers are active — Signing / ERC-8004 / WYRIWE / OCP / VNI / On-chain — derived from `/admin/api/status`. Green = active, grey = unconfigured.
|
|
253
296
|
|
|
254
297
|
**Node logs panel:** Live ring buffer of the last 200 log lines (info/warn/error), colour-coded. Auto-refreshes every 10 seconds.
|
|
255
298
|
|
|
@@ -314,7 +357,7 @@ GET /{sender}/{data}.json
|
|
|
314
357
|
GET /records?namespace=<str>&since=<unix>&limit=<n>&cursor=<str>
|
|
315
358
|
→ {
|
|
316
359
|
protocol: 1,
|
|
317
|
-
node_version: "0.
|
|
360
|
+
node_version: "0.2.0",
|
|
318
361
|
namespace: "agent-attestations",
|
|
319
362
|
records: [{ inputHash, namespace, key, value, timestamp, signature, sourcePeer }],
|
|
320
363
|
cursor: "<next>" | null
|
|
@@ -402,19 +445,37 @@ GET /health
|
|
|
402
445
|
}
|
|
403
446
|
```
|
|
404
447
|
|
|
405
|
-
### Admin
|
|
448
|
+
### Admin auth (SIWE + Bearer)
|
|
406
449
|
```
|
|
407
|
-
GET /admin/
|
|
450
|
+
GET /admin/siwe/nonce → { nonce, domain, chainId, authorizedAddress, claimed }
|
|
451
|
+
POST /admin/siwe/verify { message, signature }
|
|
452
|
+
unclaimed node → first caller claims admin, saved to config.json
|
|
453
|
+
claimed node → must match stored adminAddress
|
|
454
|
+
→ { ok, address, claimed, redirect }
|
|
455
|
+
POST /admin/siwe/transfer { message, signature } — signed by NEW wallet
|
|
456
|
+
current session required; updates adminAddress live
|
|
457
|
+
→ { ok, address }
|
|
458
|
+
POST /admin/logout clear session cookie
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Admin API (requires auth)
|
|
462
|
+
```
|
|
463
|
+
GET /admin/api/status node info, peers, recent records, tiers, adminAddress
|
|
408
464
|
GET /admin/api/logs last 200 log lines [{ ts, level, msg }]
|
|
409
|
-
GET /admin/api/audit per-spec compliance report (EIP-3668/WYRIWE/ERC-8004/OCP)
|
|
465
|
+
GET /admin/api/audit per-spec compliance report (EIP-3668/WYRIWE/ERC-8004/OCP/VNI)
|
|
410
466
|
POST /admin/api/sync trigger immediate peer sync
|
|
411
467
|
POST /admin/api/publish batch-publish recent WYRIWE records to AttestationIndex
|
|
412
468
|
body: { limit?: number } (default 50, max 200)
|
|
413
469
|
→ { published, skipped, errors }
|
|
414
470
|
POST /admin/api/peers { url } — add peer
|
|
415
471
|
DEL /admin/api/peers { url } — remove peer
|
|
416
|
-
|
|
417
|
-
POST /admin/
|
|
472
|
+
GET /admin/api/ens-records ?name= — list ENS records
|
|
473
|
+
POST /admin/api/ens-records { name, type, coinType?, textKey?, value } — upsert
|
|
474
|
+
DEL /admin/api/ens-records { name, type, coinType?, textKey? } — delete
|
|
475
|
+
GET /admin/api/config safe config snapshot (never exposes private key)
|
|
476
|
+
POST /admin/api/config update config fields → writes config.json, restarts node
|
|
477
|
+
POST /admin/api/key { gatewayKey } — rotate signing key → restart
|
|
478
|
+
POST /admin/api/register register node on-chain via NodeRegistry
|
|
418
479
|
```
|
|
419
480
|
|
|
420
481
|
---
|
|
@@ -512,6 +573,12 @@ Protocol version `1` is the current stable spec. Nodes on a different version ar
|
|
|
512
573
|
**Setup wizard — node owner onboarding**
|
|
513
574
|
- [x] Admin secret as dedicated step 2 — prominent warning box, two-step skip confirmation
|
|
514
575
|
- [x] Post-setup checklist — signing ✓, admin ✓/⚠, WYRIWE/ERC-8004/VNI ○ with next-step hints
|
|
576
|
+
- [x] Spawn-based node restart (setup + config save) — works without a process manager
|
|
577
|
+
- [x] Claim-on-first-login — first MetaMask wallet to sign becomes permanent admin, no pre-configuration required
|
|
578
|
+
- [x] Admin transfer — logged-in admin proves new wallet ownership via SIWE, `adminAddress` updated live with no restart
|
|
579
|
+
- [x] ENS records panel — live table, add/delete addr / text / addr_coin / contenthash records, changes take effect immediately
|
|
580
|
+
- [x] Admin wallet panel in dashboard — current address, two-step transfer UI
|
|
581
|
+
- [x] `withEns()` — ENS wildcard resolver wrapper; DNS wire-format decode, selector dispatch (addr / addr_coin / text / contenthash), null → zero-value fallbacks, `isEnsCalldata()` guard
|
|
515
582
|
|
|
516
583
|
---
|
|
517
584
|
|
|
@@ -526,9 +593,9 @@ npm test
|
|
|
526
593
|
Expected output:
|
|
527
594
|
|
|
528
595
|
```
|
|
529
|
-
ℹ tests
|
|
530
|
-
ℹ suites
|
|
531
|
-
ℹ pass
|
|
596
|
+
ℹ tests 61
|
|
597
|
+
ℹ suites 22
|
|
598
|
+
ℹ pass 61
|
|
532
599
|
ℹ fail 0
|
|
533
600
|
```
|
|
534
601
|
|
|
@@ -538,15 +605,37 @@ Expected output:
|
|
|
538
605
|
|---|---|
|
|
539
606
|
| `src/__tests__/gateway.test.ts` | `decodeRequest` — address + calldata parsing, `.json` suffix stripping, `CcipRequestError` on bad inputs; `encodeResponse` envelope |
|
|
540
607
|
| `src/__tests__/crypto.test.ts` | `signRecord` / `recoverRecordSigner` round-trip; `verifyRecord` correct signer → `true`, wrong signer / tampered value → `false` |
|
|
541
|
-
| `src/__tests__/db.test.ts` | `insertRecord`, `getRecord` (with/without namespace), `getRecordsByInputHash`, `INSERT OR IGNORE` deduplication, cursor pagination, `getContributions` grouping, peer upsert + remove |
|
|
608
|
+
| `src/__tests__/db.test.ts` | `insertRecord`, `getRecord` (with/without namespace), `getRecordsByInputHash`, `INSERT OR IGNORE` deduplication, cursor pagination, `getContributions` grouping, peer upsert + remove, ENS record upsert/delete/list |
|
|
542
609
|
| `src/__tests__/ocp.test.ts` | `buildCommitmentHash` determinism, 32-byte hex output, field-sensitivity (agentId / outputHash / timestamp) |
|
|
543
610
|
| `src/__tests__/wyriwe.test.ts` | Sentinel path (`inputHash === rawInputHash`), non-sentinel path (`keccak256(abi.encode(rawInputHash, sanitizationPipelineHash))`), paths produce distinct hashes for same calldata |
|
|
544
611
|
| `src/__tests__/vni.test.ts` | `makeVni` field shape + stable `nodeId`; `verifyVni` round-trip; tamper detection (url / signerAddress / nodeId → `null`) |
|
|
612
|
+
| `src/__tests__/ens.test.ts` | DNS wire-format encode/decode round-trip; `withEns()` dispatch for all 4 record types (addr, addr_coin, text, contenthash); null → zero-value fallbacks; unknown selector → `0x`; wrong outer selector throws |
|
|
545
613
|
|
|
546
614
|
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.
|
|
547
615
|
|
|
548
616
|
---
|
|
549
617
|
|
|
618
|
+
## Roadmap
|
|
619
|
+
|
|
620
|
+
### v0.3.0 — IPFS browser resolution
|
|
621
|
+
Native ENS browsers (Brave, eth.link) resolve `contenthash` directly on-chain — they don't follow CCIP-Read. v0.3.0 will add an **IPFS + Browser resolution** admin panel that closes this gap:
|
|
622
|
+
|
|
623
|
+
- Pin a file or CID to IPFS (via Pinata) from the admin panel
|
|
624
|
+
- Set the resulting CID as the ENS name's `contenthash` on-chain (MetaMask, no stored key)
|
|
625
|
+
- Manage multiple names from one panel
|
|
626
|
+
|
|
627
|
+
Combined with `withEns()` (CCIP-Read, dynamic records) this makes the gateway handle both resolution paths:
|
|
628
|
+
|
|
629
|
+
| Path | Who | How |
|
|
630
|
+
|---|---|---|
|
|
631
|
+
| Static pages | Brave / native ENS browsers | `contenthash` on-chain → IPFS |
|
|
632
|
+
| Dynamic data | dapps / smart contracts | `offchainLookup` → CCIP-Read → `withEns()` |
|
|
633
|
+
|
|
634
|
+
### v0.4.0+ — Phase 2 / Phase 3
|
|
635
|
+
See [GATEWAY_DECENTRALIZATION_PLAN.md](https://github.com/Echo-Merlini/ccip-router) for the full decentralisation roadmap (chain as source of truth, incentivised node network).
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
550
639
|
## Related
|
|
551
640
|
|
|
552
641
|
- [ens-boiler](https://github.com/Echo-Merlini/ens-boiler) — opinionated ENS agent stack built on `ccip-router`
|
package/dist/mesh/records.js
CHANGED
|
@@ -18,7 +18,7 @@ recordsRouter.get('/', async (c) => {
|
|
|
18
18
|
: null;
|
|
19
19
|
return c.json({
|
|
20
20
|
protocol: 1,
|
|
21
|
-
node_version: '0.
|
|
21
|
+
node_version: '0.2.0',
|
|
22
22
|
namespace,
|
|
23
23
|
records,
|
|
24
24
|
cursor: nextCursor,
|
|
@@ -34,7 +34,7 @@ peersRouter.get('/', async (c) => {
|
|
|
34
34
|
const signerAddress = config.gatewayKey ? privateKeyToAccount(config.gatewayKey).address : null;
|
|
35
35
|
return c.json({
|
|
36
36
|
protocol: 1,
|
|
37
|
-
node_version: '0.
|
|
37
|
+
node_version: '0.2.0',
|
|
38
38
|
signerAddress,
|
|
39
39
|
peers: peers.map((p) => ({
|
|
40
40
|
url: p.url,
|
package/dist/mesh/vni.js
CHANGED
|
@@ -5,7 +5,7 @@ function vniMessage(doc) {
|
|
|
5
5
|
return 'ccip-router:vni:' + JSON.stringify(doc);
|
|
6
6
|
}
|
|
7
7
|
// Produce a fresh signed VNI for this node.
|
|
8
|
-
export async function makeVni(gatewayKey, url, version = '0.
|
|
8
|
+
export async function makeVni(gatewayKey, url, version = '0.2.0') {
|
|
9
9
|
const account = privateKeyToAccount(gatewayKey);
|
|
10
10
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
11
11
|
const nodeId = keccak256(toBytes(account.address));
|
package/package.json
CHANGED