feat(gateway): make the IPFS gateway setting actually govern websites and links - #123
Merged
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
fxfiles.toplinkSo 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:
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:
?gw=__proto__/toString/constructorreturned a truthy inherited value from the object literal, whose.cidisundefined. The throw dropped the request onto the IPNS fallback, which does not resolve — so a typo broke the link rather than using the default. NowObject.hasOwn.@or a backslash could re-point the host. Now charset-checked.Locationheader.Testing
flutter test— 1070 passed, 25 skippedflutter build web --release -t lib/main_web.dart— ✅ (same commanddeploy-pages.ymlruns)flutter analyzeon every changed file — No issues foundnode --test cloudflare/ipns-resolver-worker.test.mjs— 33 passed (new)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
use_build_context_synchronouslyinfos inlib/features/websites/screens/website_detail_screen.dartare pre-existing — they merely shifted five lines from the import added here.cloudflare/**is not in the Pages trigger; it needsnpx wrangler deployfromcloudflare/. 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_GATEWAYis 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