Skip to content

feat: free gasless offchain ENS subnames under fcnova.eth#3

Open
HAPPYS1NGH wants to merge 5 commits into
FilOzone:mainfrom
HAPPYS1NGH:offchain-ens-subname-issuance-de2mj
Open

feat: free gasless offchain ENS subnames under fcnova.eth#3
HAPPYS1NGH wants to merge 5 commits into
FilOzone:mainfrom
HAPPYS1NGH:offchain-ens-subname-issuance-de2mj

Conversation

@HAPPYS1NGH

@HAPPYS1NGH HAPPYS1NGH commented Jun 8, 2026

Copy link
Copy Markdown

🎥 Demo

Watch the walkthrough

Summary

Every deploy can claim a free, gasless <label>.fcnova.eth name pointing at the deployed site — no gas, and no need to own an ENS domain. Additive: the existing onchain --ens path is untouched.

Built on Namespace offchain subnames (@thenamespace/offchain-manager), gasless and served through eth.limo.

nova deploy ./dist --subname mysite   # -> mysite.fcnova.eth.limo  (you own it)
nova demo   ./dist --subname mysite   # -> mysite.demo.fcnova.eth.limo  (free, no wallet)

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 pure fetch clients (src/subname.ts) — no SDK, no secret.

The CLI signs a claim {label,parent,cid,expiry,owner} with whatever key it holds and asserts owner:

  • disk key (NOVA_PIN_KEY) → signs locally, owner = its address
  • browser (no disk key) → signs with the ephemeral session key from the existing fil.focify.eth.limo wallet-auth; asserts owner = the real wallet

Worker: 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 to owner via the on-chain SessionKeyRegistry (browser). Transient Filecoin RPC failure on update → retryable 503, never a silent overwrite. No DB, no second signing page.

Demo isolation

Demo names nest under a reserved demo label (<label>.demo.fcnova.eth), ungated and create-only — first-come, never re-pointed (409 if taken), so a shared demo URL can't be hijacked. LABEL_RE forbids 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:

  Your site gets a free ENS name — gasless, and you own it.
? Pick your name [mysite] › gm-happy
  ✔ Name ready: https://gm-happy.fcnova.eth.limo

--json / CI issues once non-interactively and never hangs. --no-subname opts out.

Files

  • src/subname.ts — client (claim signing, issue, availability), no SDK/secrets
  • workers/subname/ — Cloudflare Worker (sole key holder). Endpoints: POST /issue, POST /demo-issue, GET /status
  • src/cli.ts — interactive picker, --subname/--no-subname, clearer free-name vs own-ENS prompts
  • src/mcp.tsnova_subname tool + subname output on deploy/demo (12 tools)
  • README + CLAUDE.md documented

Verified

tsc clean; client (9) + Worker (7) unit tests pass; live create / update / 409 / SessionKeyRegistry resolution against a deployed Worker (incl. the glif 2880-block getLogs cap fix).

Remaining before merge

  • Move the Worker off the personal *.workers.dev URL → org/branded infra; update DEFAULT_SUBNAME_WORKER_URL
  • Confirm fcnova.eth resolver = Namespace CCIP (so <name>.fcnova.eth.limo serves)
  • Rotate NAMESPACE_API_KEY (shared during dev)
  • Live browser wallet-auth update-path test (MetaMask)
  • Decide on optional address record (skipped for now — contenthash only)

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +128 to +131
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)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +113 to +116
// 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)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/demo.ts
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/cli.ts
Comment on lines 2002 to 2004
ens: { type: "string" },
subname: { type: "string" },
"max-pages": { type: "string" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@HAPPYS1NGH HAPPYS1NGH marked this pull request as draft June 8, 2026 15:12
@HAPPYS1NGH HAPPYS1NGH closed this Jun 8, 2026
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.
@HAPPYS1NGH HAPPYS1NGH reopened this Jun 8, 2026
@HAPPYS1NGH HAPPYS1NGH marked this pull request as ready for review June 8, 2026 19:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread README.md

## 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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@HAPPYS1NGH

Copy link
Copy Markdown
Author

@TippyFlitsUK - Let me know if you have any questions regarding this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant