feat: free gasless offchain ENS subnames under fcnova.eth#3
Conversation
Every deploy can claim a free <label>.fcnova.eth name via Namespace offchain subnames -- no gas, no ENS domain required. Additive to the existing onchain --ens path. - src/subname.ts: pure fetch client (owner-bound claim signing, issue, availability) -- no Namespace SDK, no secrets in the package - workers/subname/: Cloudflare Worker, sole holder of the master Namespace key. CREATE trusts the asserted owner; UPDATE verifies the signer controls it (signer==owner for disk keys, or resolves via the SessionKeyRegistry for browser session keys); transient RPC failure -> retryable 503. Demo names isolated under a reserved `demo` label, ungated. - cli: interactive name picker for deploy + demo; --subname / --no-subname - mcp: nova_subname tool, subname output on nova_deploy/nova_demo (12 tools) - config/signing-url: NOVA_SUBNAME_PARENT, NOVA_SUBNAME_WORKER_URL - docs: README + CLAUDE.md
Announce the free ENS name in the shared picker helper (deploy + demo, no duplication) and reword the bring-your-own-ENS prompts across deploy/demo/clone to read as the optional "too" path -- differentiating the free name we give from an ENS domain the user already owns.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 12ff956a62
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!Array.isArray(logs) || logs.length === 0) return null; | ||
| const rootTopic = logs[logs.length - 1]?.topics?.[1] as string | undefined; // most recent | ||
| if (!rootTopic) return null; | ||
| return `0x${rootTopic.slice(26)}`; |
There was a problem hiding this comment.
Verify that session-key authorization has not expired
When a session key's registry authorization has expired, resolveRoot still returns the root wallet solely because a historical Login event exists. The setup flow explicitly creates time-limited authorizations (login(sessionAddress, expiryTimestamp, ...)), so anyone retaining an expired session key can continue updating the wallet's subnames until its login event falls outside this scan window. Check the registry's current authorization state or decode and enforce the event expiry before accepting the signer.
Useful? React with 👍 / 👎.
| // glif caps eth_getLogs at 2880 blocks; stay safely under it. ~23h of Filecoin | ||
| // blocks (~30s each) -- a fresh wallet-auth re-registers each session, so the | ||
| // session key we're resolving is always minutes old, well inside this window. | ||
| const fromBlock = `0x${Math.max(0, latest - 2800).toString(16)}`; |
There was a problem hiding this comment.
Resolve still-valid sessions older than the log window
The browser-auth setup defaults to a 7-day session and offers sessions up to one year, but this lookup only searches roughly the last 23 hours. If a user keeps their issued NOVA_PIN_KEY and wallet address, the CLI does not re-register it on each deployment; after about 23 hours an otherwise valid session can no longer update its existing subname and is reported as belonging to someone else. Query the registry's current session state or scan far enough to cover supported authorization lifetimes.
Useful? React with 👍 / 👎.
| if (opts?.autoSubname !== false) { | ||
| try { | ||
| const { issueDemoSubname, normalizeLabel, suggestLabel } = await import("./subname.js"); | ||
| const workerUrl = process.env.NOVA_SUBNAME_WORKER_URL || DEFAULT_SUBNAME_WORKER_URL; |
There was a problem hiding this comment.
Honor an empty worker URL when disabling demo subnames
For non-interactive demos and nova_demo, setting NOVA_SUBNAME_WORKER_URL="" does not disable issuance as documented because || replaces the empty value with the baked-in default. This still sends the demo CID and label to the hosted worker and creates a name despite the explicit opt-out. Use an undefined-only fallback, matching resolveConfig, so an empty string remains disabled.
Useful? React with 👍 / 👎.
| ens: { type: "string" }, | ||
| subname: { type: "string" }, | ||
| "max-pages": { type: "string" }, |
There was a problem hiding this comment.
Add the documented demo --no-subname option
The README documents --no-subname for nova demo, but the demo parser only registers --subname, so nova demo <path> --no-subname fails as an unknown option and users cannot opt out through the documented CLI flag. Register the boolean option and pass it through to demoDeploy/the interactive naming step.
Useful? React with 👍 / 👎.
POST /demo-issue overwrote any existing <label>.demo.fcnova.eth name's contenthash with no auth (demo names are ungated by design), so anyone who knew a shared demo URL could silently repoint it at their own CID. Make demo issuance create-only: an existing name now returns 409 taken_by_other and is never overwritten. Bounded to the demo namespace (disjoint from gated names), but a real content-takeover within it. - worker: reject existing demo name with 409 instead of updateSubname - client: map 409 -> SubnameTakenError so the picker loops for another name - cli: demo picker drops the "overwrite?" prompt -> "taken, choose another" - tests: issueDemoSubname 200 / 409->SubnameTakenError / generic-error - docs: CLAUDE.md + README note demo names are create-only / non-overwritable
Collapse the duplicated subname decision logic (label derivation, ownership check, availability gate, error classification) into shared helpers and a single issueSubnameOnce core in src/subname.ts. The MCP tool and the CLI non-interactive path become thin adapters over it; the CLI keeps only its interactive prompts. Behavior and MCP JSON output are unchanged. Drop src/subname.test.ts to match the test-free src/ convention (the Worker's sync test stays as the duplication guard).
Trim the verbose function/module JSDoc across the subname client, MCP adapter, CLI helpers, and Worker to one-line summaries (keeping the no-secrets and byte-identical-claim notes). Comments only; no behavior change.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6aef6084aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| ## Get a Free ENS Name | ||
|
|
||
| Every deploy can get a free, human-readable name -- no gas, and you don't need to own an ENS domain. Nova issues a name under `fcnova.eth` and points it at your site: |
There was a problem hiding this comment.
Wire free-name issuance into all deploy entry points
The new “Every deploy” behavior does not apply to the existing nova clone <url> flow: runClone calls deploy() directly but never invokes runSubnameStep, and its parser does not accept --subname or --no-subname. Consequently, users following the prominently advertised clone-and-deploy command receive no free name despite this promise; either add the subname step to clone deployments or narrow the documentation to the supported commands.
Useful? React with 👍 / 👎.
|
@TippyFlitsUK - Let me know if you have any questions regarding this. |
🎥 Demo
▶ Watch the walkthrough
Summary
Every deploy can claim a free, gasless
<label>.fcnova.ethname pointing at the deployed site — no gas, and no need to own an ENS domain. Additive: the existing onchain--enspath is untouched.Built on Namespace offchain subnames (
@thenamespace/offchain-manager), gasless and served througheth.limo.Ownership model (master key never ships client-side)
Namespace writes are gated by a single master API key. It lives only in a Cloudflare Worker (
workers/subname/); the CLI/MCP are purefetchclients (src/subname.ts) — no SDK, no secret.The CLI signs a claim
{label,parent,cid,expiry,owner}with whatever key it holds and assertsowner:NOVA_PIN_KEY) → signs locally,owner= its addressfil.focify.eth.limowallet-auth; assertsowner= the real walletWorker: CREATE records the asserted owner (no on-chain read; a wrong owner only self-griefs). UPDATE requires the signer to control the stored owner —
signer == owner(disk key) or the signer resolves toownervia the on-chainSessionKeyRegistry(browser). Transient Filecoin RPC failure on update → retryable503, never a silent overwrite. No DB, no second signing page.Demo isolation
Demo names nest under a reserved
demolabel (<label>.demo.fcnova.eth), ungated and create-only — first-come, never re-pointed (409if taken), so a shared demo URL can't be hijacked.LABEL_REforbids dots, so gated and demo namespaces can't collide.Interactive UX
After upload, a shared picker (deploy + demo) announces the free name and loops on taken / already-yours (repoint) / invalid:
--json/ CI issues once non-interactively and never hangs.--no-subnameopts out.Files
src/subname.ts— client (claim signing, issue, availability), no SDK/secretsworkers/subname/— Cloudflare Worker (sole key holder). Endpoints:POST /issue,POST /demo-issue,GET /statussrc/cli.ts— interactive picker,--subname/--no-subname, clearer free-name vs own-ENS promptssrc/mcp.ts—nova_subnametool + subname output on deploy/demo (12 tools)Verified
tscclean; client (9) + Worker (7) unit tests pass; live create / update / 409 / SessionKeyRegistry resolution against a deployed Worker (incl. the glif 2880-blockgetLogscap fix).Remaining before merge
*.workers.devURL → org/branded infra; updateDEFAULT_SUBNAME_WORKER_URLfcnova.ethresolver = Namespace CCIP (so<name>.fcnova.eth.limoserves)NAMESPACE_API_KEY(shared during dev)