Browser-verified ENS gateway. Visit <name>.eth.<GATEWAY_DOMAIN> and the
browser resolves the ENS name, fetches the IPFS/IPNS contenthash, and verifies
every block against its CID — all through a service worker. The gateway
operator hosts only a static bootstrap bundle; no request proxying, no
server-side content retrieval.
eth.limo is a server-side HTTPS proxy: the operator resolves ENS, fetches the IPFS content, and streams it to the browser. The content the user sees is whatever the proxy returns.
eth.tennis does the resolution and retrieval in the browser. After the
service worker activates, the browser queries an Ethereum RPC for the ENS
contenthash and pulls content through @helia/verified-fetch, which
hash-verifies every block against that contenthash. The server-side surface
is only a static bootstrap.
In v1 the Ethereum RPC is still a trust anchor for the name → contenthash mapping. User-configurable RPC and light-client mode are post-v1 work.
- Cold visit to
vitalik.eth.<GATEWAY_DOMAIN>/path. A static host serves the bootstrap HTML. - Bootstrap registers
/gw-sw.js, then asks it (viapostMessage) to resolve-and-fetch. The SW streams progress lines back, rendered as a boot-log-style terminal. - SW uses
@ensdomains/ensjsto look up the contenthash, then@helia/verified-fetchto retrieve and verify the content. - Bootstrap triggers a reload; the SW now controls the origin and serves content and sub-resources from its primed cache.
- Warm visits skip the bootstrap entirely — the SW intercepts the first request.
Failures (no contenthash, unreachable content, bad IPNS record, RPC down,
etc.) are surfaced by re-rendering the precached bootstrap with state
injected into window.__GATEWAY_STATE__. Each class returns a scoped HTTP
status.
| Path | Package | License | Purpose |
|---|---|---|---|
packages/gateway-sw-core |
@cypsela/gateway-sw-core |
MIT | Reusable SW primitives: ENS resolution, verified-fetch dispatch, errors. |
packages/gateway |
@cypsela/gateway |
AGPL-3.0-only | Deployable site (bootstrap + SW entrypoint). Private; not published. |
Content authors who want gateway resolution in their own SW can build on
@cypsela/gateway-sw-core, which packages ENS resolution and verified-fetch
dispatch as composable primitives.
pnpm install
pnpm dev # runs @cypsela/gateway on :5173
pnpm build # builds both packages
pnpm test # unit tests (vitest) across the workspace
pnpm ci # build + test (what CI runs)Try http://vitalik.eth.tennis.localhost:5173/ once dev is up — Chromium
resolves *.localhost to 127.0.0.1 so no hosts-file edits are required.
Per-package scripts live in packages/*/package.json. @cypsela/gateway
also exposes pnpm --filter @cypsela/gateway test:e2e for Playwright.
Read by @cypsela/gateway at build and dev time:
| Name | Default | Purpose |
|---|---|---|
VITE_GATEWAY_DOMAIN |
gateway.example |
Suffix the SW strips from location.hostname to extract the ENS name. |
VITE_RPC_URLS |
required | Comma-separated Ethereum RPC endpoints. Tried sequentially on failure. |
Local defaults live in packages/gateway/.env.development; a template is at
packages/gateway/.env.example.
Any static host works. TLS must cover every subname depth the gateway is
expected to serve (*.eth.<domain>, *.*.eth.<domain>, and so on for
ENSIP-10 subnames like app.vitalik.eth.<domain>).
v1 in active development. End-to-end working: ipfs:// contenthashes,
ipns:// contenthashes (libp2p-key and DNSLink), subnames, CCIP-read.
Fetch timeouts remain a known limitation tracked against the spec.