Skip to content

feat(gateway): make the IPFS gateway setting actually govern websites and links - #123

Merged
ehsan6sha merged 1 commit into
mainfrom
feat/ipfs-gateway-selection
Sep 12, 2026
Merged

feat(gateway): make the IPFS gateway setting actually govern websites and links#123
ehsan6sha merged 1 commit into
mainfrom
feat/ipfs-gateway-selection

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

Why

dweb.link now returns HTTP 429 for generated sites once they see real traffic — measured 2026-09-12: a freshly generated site 429'd on dweb.link and returned 200 from Filebase for the same CID at the same moment.

The app already had an IPFS gateway setting. Choosing a different one changed almost nothing:

before
asset URLs kept the URL recorded at import time — a later change never reached them
the stable fxfiles.top link always resolved to dweb
the setting itself a raw URL text field buried in More → API Configuration, among endpoint overrides no ordinary user should touch

So rather than add a separate per-generation gateway picker, this makes the existing global setting do what it already claims to.

What changed

Settings — the gateway is promoted to its own top-level section under Billing, as a picker (dweb.link / Filebase / Custom…) instead of a URL field. The raw field is removed from More → API Configuration. Two editors for one key would let a near-miss typed in the advanced screen silently demote a preset user to "custom", and with it drop the ?gw= that makes their links follow the choice.

Assets — built from the CID with the current template instead of the URL captured at import time. This was the bug that made the setting look inert.

The stable link — the Worker takes an optional ?gw=, and the app appends it when a link is read, never when the pointer is minted. A pointer is written once and lives for the life of the website, so baking the gateway in would freeze it at whatever was configured that day — exactly the staleness being fixed for assets.

Two deliberate exceptions:

  • the URL embedded in a social-media caption does freeze, because the caption is a permanent public artifact;
  • the public directory listing URL is left undecorated — the server rejects any URL carrying a query string (isAllowedListingUrl), and one submitter's gateway preference has no business steering a stranger's browser.

?gw= is a fixed allowlist. A user's custom template governs their own asset URLs but is not honoured on the shared link: accepting a caller-supplied destination there would turn a link anyone can share into an open redirector.

Worker hardening found while testing

The Worker had no test coverage, and it fronts every shared link the app has ever minted. Writing tests surfaced five real defects, all now fixed and pinned:

  1. ?gw=__proto__ / toString / constructor returned a truthy inherited value from the object literal, whose .cid is undefined. The throw dropped the request onto the IPNS fallback, which does not resolve — so a typo broke the link rather than using the default. Now Object.hasOwn.
  2. The CID from w3name is interpolated into the AUTHORITY of the subdomain shape, where a hostile answer containing @ or a backslash could re-point the host. Now charset-checked.
  3. A CIDv0 (base58, case-sensitive) or an over-63-char CID cannot be a DNS label (RFC 1035) — both silently corrupt in a subdomain. They now use the path form.
  4. An IPNS record may point into a directory; that suffix was being dropped, serving the wrong page.
  5. A control character in the record value could split the Location header.

Testing

  • flutter test1070 passed, 25 skipped
  • flutter build web --release -t lib/main_web.dart — ✅ (same command deploy-pages.yml runs)
  • flutter analyze on every changed file — No issues found
  • node --test cloudflare/ipns-resolver-worker.test.mjs33 passed (new)
  • 19 new Dart cases for the gateway helper

Reviewed by agy-advisor, which validated the two security fixes and found defects 3 and 4 above. The rest of the advisor panel was unavailable (kimi 403 plan-gated, codex 402, GLM 429, cursor auth-expired, mimo 403).

Reviewer notes

  • The two use_build_context_synchronously infos in lib/features/websites/screens/website_detail_screen.dart are pre-existing — they merely shifted five lines from the import added here.
  • The Worker is not deployed by this PR. cloudflare/** is not in the Pages trigger; it needs npx wrangler deploy from cloudflare/. The change is backward-compatible in both directions (the old Worker ignores ?gw=; the new one treats a bare link exactly as before), so merge order is safe — but the feature is not visible on shared links until the Worker ships.
  • DEFAULT_GATEWAY is left on dweb, per the brief ("dweb.link as default, changeable to filebase"). Given dweb is actively 429-ing, flipping it would rescue links already shared in the wild, which carry no ?gw= — that is a policy call, deliberately left open.

Known limitation (not addressed here)

A site's HTML embeds absolute asset URLs at generation time. Switching the gateway makes the page itself load from the new gateway, but images inside an already-generated site still point at the old one — so an existing 429-ing site needs a fresh generation to fully recover. Making asset references relative (assets in the site's own IPFS directory) is the real fix and belongs in its own change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AwMWmCivEpYTmmzzmjSTAf

… and links

dweb.link now returns HTTP 429 for generated sites once they see any real
traffic (measured 2026-09-12: a fresh site 429'd on dweb.link and returned 200
from Filebase for the same CID at the same moment). The app already had a
gateway setting, but choosing a different one changed almost nothing:

  * assets kept the URL recorded at import time, so a later change never
    reached them;
  * the stable fxfiles.top link always resolved to dweb;
  * the setting itself was a raw text field buried in More -> API
    Configuration, alongside endpoint overrides no ordinary user should edit.

Rather than add a per-generation gateway picker, this makes the existing
global setting do what it says.

Settings
  Promote the gateway to its own top-level section under Billing, as a picker
  (dweb.link / Filebase / Custom) rather than a URL field, and REMOVE the raw
  field from More -> API Configuration. Two editors for one key would let a
  near-miss typed in the advanced screen silently demote a preset user to
  "custom" -- and with it drop the ?gw= that makes their links follow the
  choice.

Assets
  Build asset URLs from the CID with the CURRENT template instead of reusing
  the URL captured at import time. This was the bug that made the setting look
  inert.

The stable link
  The Worker takes an optional ?gw= selecting which gateway it redirects to,
  and the app appends it when a link is read -- never when the pointer is
  minted. A pointer is written once and lives for the life of the website, so
  baking the gateway in would freeze it at whatever was configured that day:
  exactly the staleness being fixed for assets. The one deliberate exception is
  the URL embedded in a social-media caption, which freezes because the caption
  is a permanent public artifact.

  ?gw= is a FIXED ALLOWLIST. A user's custom template governs their own asset
  URLs but is not honoured on the shared link -- accepting a caller-supplied
  destination there would turn a link anyone can share into an open redirector.
  The public directory listing is likewise left undecorated: the server rejects
  any URL carrying a query string, and one submitter's preference has no
  business steering a stranger's browser.

Worker hardening found while testing (all now covered by tests)
  * ?gw=__proto__ / toString / constructor returned a TRUTHY inherited value
    from the object literal, whose .cid is undefined -- the throw dropped the
    request onto the IPNS fallback, which does not resolve. A typo broke the
    link instead of using the default. Now Object.hasOwn.
  * The CID from w3name is interpolated into the AUTHORITY of the subdomain
    shape, where a hostile answer containing @ or a backslash could re-point
    the host. Now charset-checked.
  * A CIDv0 (base58, case-sensitive) or an over-63-char CID cannot be a DNS
    label; both silently corrupt in a subdomain. They now use the path form.
  * An IPNS record may point INTO a directory; that suffix was being dropped.
  * A control character in the record value could split the Location header.

Tests: 19 Dart cases for the helper, 33 Node cases for the Worker (none
existed before -- it fronts every shared link the app has minted).

Notes for review: the two use_build_context_synchronously infos reported in
lib/features/websites/screens/website_detail_screen.dart are PRE-EXISTING and
merely shifted five lines by the import added here.

Known limitation, not addressed here: a site's HTML embeds ABSOLUTE asset URLs
at generation time, so switching the gateway makes the page load from the new
gateway while images inside an ALREADY-GENERATED site still point at the old
one. Only a fresh generation re-embeds them. Making asset references relative
is the real fix and is a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwMWmCivEpYTmmzzmjSTAf
@ehsan6sha
ehsan6sha merged commit 91fb413 into main Sep 12, 2026
2 of 3 checks passed
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