fix(frontend): stop SPA fallthrough on /privacy, /cookies, /opt-out #12
Labels
No labels
agent:implement
agent:ready
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
ready
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/CL8Y-web#12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Stop first-party SPAs from treating guessed legal URLs as product routes. Today
/privacy,/cookies,/opt-out, and similar paths are rewritten toindex.html(HTTP 200) and then painted as the marketing site, CL8Y DEX Swap, or CL8Y Bridge transfer shell. Crawlers and people who type those paths see product chrome and homepage Open Graph tags, not a missing page and not a Privacy Notice.This issue is the shared spec. The implementer worktree for this repo is
code/CL8Y-web(https://cl8y.com). Matching changes are required incode/cl8y-dex-terraclassicandcode/cl8y-bridge-monorepousing the same reserved-path table; do not edit those trees from a CL8Y-web worktree.Bundle (do not split):
/privacy,/cookies,/opt-out, and documented aliases).index.html.Navigateto/.path="*"→/currently turns/privacyinto Swap / Transfer.Until a dedicated Privacy Notice exists as a first-party HTTPS document, 404 is the correct behavior. Do not invent notice copy in the marketing, DEX, or bridge shells. Do not treat T&Cs as the Privacy Notice.
Current codebase
Marketing (
code/CL8Y-web) — SPA fallback is explicitsrc/app/index.tsxis aBrowserRouterwith only:/homepage/engine,/security,/tokenomics,/community,/institutional)/blog,/blog/:slug/cl8y_whitepaperThere is no
/privacy(or cookies / opt-out) route and no catch-all 404 route. Unknown paths still mountSiteHeader+SiteFooter.DefaultHeadapplies marketing SEO (siteCopy.seo.title/ homepage description and/opengraph.png) to every non-blog pathname, so/privacyis advertised as the product site.Host fallback (order is the bug):
render.yamlrepeatssource: /*→/index.html.PROJECT_GUIDE.md§13 tells operators to keep SPA fallbacks. Blog prerender rules exist so/blog/*wins over that fallback; legal-guess paths do not.Footer (
SiteFooter.tsx+src/data/copy.ts) has disclaimer and law-enforcement copy. It does not link a Privacy Notice. Canonical product URLs insrc/content/invariants.tsare only Bridge and DEX origins.DEX (
code/cl8y-dex-terraclassic) — unknown paths become Swapdocker/frontend/nginx.conf:Hashed JS/CSS already 404 via
@hashed_asset_miss(#706). HTML routes do not.frontend-dapp/src/App.tsxends with:/is Swap. A visitor (or crawler) on/privacygets HTTP 200 + client replace to Swap.LegalFooterNotice.tsxlinks security / incidents / LP how-to / report — not privacy, cookies, or opt-out./swapalias preservation (SwapAliasRedirect.tsx, #711) must stay. Do not reserve/,/swap,/pool,/protocol,/portfolio,/trade, or other real dApp paths.Bridge (
code/cl8y-bridge-monorepo) — unknown paths become Transferpackages/frontend/src/main.tsxis the same pattern:Layout(/,/transfer/:xchainHashId,/history,/verify,/settings)<Route path="*" element={<Navigate to="/" replace />} />VITE_UNDER_CONSTRUCTIONis true,path="*"rendersUnderConstructionPagefor every URL, including legal guessesThere is no in-tree nginx deny list for these paths. Client catch-all is enough to impersonate the bridge as
/privacy.Legal portal (
code/cl8y-ecosystem-legal) — related, not this worktreeweb/src/main.tsswitches/sign/evm|solana|terra-classic|telegramand defaults every other path torenderHome(T&Cs acceptance portal). If the canonical Privacy Notice is later hosted on that origin,/privacymust not keep rendering T&Cs home. Authoring the notice itself is out of scope here.No existing issue in CL8Y-web, DEX, bridge, or legal tracks this fallthrough (searched privacy / cookies / opt-out / SPA / fallthrough). Not a duplicate of DEX #1202 (product events) or legal CORS #7.
Why this is needed
/privacy,/cookies, and/opt-outon the apex and on product hosts. Serving the trading or bridge UI at those URLs looks like the product is the privacy policy.DefaultHeadand DEX/bridge shells advertise the dApp. A 404 (or a real notice) is honest; a Swap page at/privacyis not.Navigateto/hides the miss. DEX and bridge catch-alls make/privacyindistinguishable from a successful product landing. Marketing at least leaves a blank main; DEX/bridge actively replace the URL.This is routing / static-host config. No wallet, no indexer, no contracts, no T&Cs rewrite.
Constraints and guardrails
/protocol,/portfolio,/pool,/securitylegacy redirect,/blog/*)./privacy.Locationis aURLbuilt from a hardcoded first-party origin + path. Never read the target from query, hash,Referer, orwindow.location. Rejectjavascript:,data:,//, and non-HTTPS./* → index.html/try_files … /index.html. Blog prerender rewrites on CL8Y-web must keep working.path="*" → Navigate to="/". Marketing must not wrap them in SiteHeader/SiteFooter + DefaultHead product SEO.#3host headers), token directory, Swap query params (#711/#713), or TermsGate.@hashed_asset_missmust remain; reserved HTML paths are a new exact-location block, not a change to JS/CSS caching.VITE_PUBLIC_ORIGIN/ env-built redirect for the notice URL unless it is already the established first-party legal origin pattern; prefer a constant next toCANONICAL_PRODUCT_URLS.Relevant files
public/_redirects/* → index.html 200— reserved paths must precede thisrender.yaml/*rewrite for the static hostsrc/app/index.tsxsrc/app/DefaultHead.tsxsrc/app/LegacyRedirect.tsxsrc/content/invariants.tssrc/content/invariants.test.tsPROJECT_GUIDE.mde2e/*.spec.ts+playwright.config.tscode/cl8y-dex-terraclassicdocker/frontend/nginx.conflocation =beforelocation /code/cl8y-dex-terraclassicfrontend-dapp/src/App.tsx*→/for reserved pathscode/cl8y-bridge-monorepopackages/frontend/src/main.tsxcode/cl8y-ecosystem-legalweb/src/main.ts/privacyif the notice lives thereRecommended direction
Shared path table (pure helper + unit tests), e.g.
src/lib/reservedLegalGuessPaths.ts:Exact (with/without trailing slash; match case-insensitively):
/privacy,/privacy-policy,/privacy_policy,/privacypolicy,/privacy-notice/cookies,/cookie,/cookie-policy,/cookies-policy/opt-out,/optout,/opt_out/do-not-sell,/donotsell,/ccpaMatch pathname only (strip query/hash). Do not treat
/privacy-xyzor/blog/privacyas reserved unless the path is exactly a list entry.CL8Y-web host: In
_redirectsandrender.yaml, put reserved rules before/* → /index.html. Prefer404(or301to the canonical notice once that URL is a real first-party document). Keep/blogand/blog/*prerender rewrites first.CL8Y-web client: If a reserved path still reaches React (Vite dev), render a minimal non-product 404 (no homepage hero, no DefaultHead product title) or
window.location.replaceonly to the canonical notice URL constant. Do notNavigateto/.DEX nginx:
location = /privacy { return 404; }(and the rest of the table, including/privacy/) beforelocation /. Keeptry_filesfor real dApp routes. InApp.tsx, check reserved paths before the*→/route (render nothing / a 404 view, or rely on the host 404 so the JS never runs).Bridge: Same client exception to
path="*". If the frontend image has a static server config in-tree, add the same exact locations; if not, client-side is still mandatory.Redirect mode (optional, only when the notice exists): Single hardcoded
https://…Privacy Notice URL. 301/302. No query forwarding except maybeutm_strip. Cookies and opt-out guesses may 301 to the same notice (or 404) — pick one and test it; do not invent extra pages.Acceptance criteria
GET /privacy,/cookies, and/opt-out(and trailing-slash variants) on the marketing static host do not returnindex.htmlas 200 with product chrome. They return 404 or 301/302 to the canonical Privacy Notice.App.tsxso those paths neverNavigateto Swap.main.tsxso those paths neverNavigateto Transfer (including under-constructionpath="*")./,/blog,/blog/:slug, DEX/,/swap,/pool,/protocol,/trade, bridge/,/history,/settings,/verify./privacy?next=https://evil.exampledoes not redirect off-origin. Query is ignored for routing.PROJECT_GUIDE.mdand/or a short skill note) say SPA fallback must not cover reserved legal-guess paths.Test plan (functional paths)
/privacy/privacy//PRIVACY/cookies,/cookie-policy/opt-out,/optout,/do-not-sell//blogand a prerendered/blog/:slug/security/#trustunchanged/cl8y_whitepaperyarn preview/privacy/privacy(preview or nginx fixture)//and/swap?from=/privacy//history,/settings,/verify/privacy-policyvs/blog/privacy/privacy?foo=barPlaywright:
yarn test:e2ealready uses--workers=5. Add a dedicated spec thatrequest.gets reserved paths (status +content-type/ body must not be the homepage) and that product routes still 200. Unit tests for the matcher insrc/content/invariants.test.tsor a newsrc/lib/reservedLegalGuessPaths.test.ts.Test plan (attack / hack / abuse)
/privacy?redirect=https://phishLocationjavascript:/data:///evilas redirect target/privacy/../or/privacy/%2e%2e//privacy-not-a-policy*— do not expand this issue to all 404s)/protocol/protocolstill the protocol page/ %70rivacyvs/privacyindex.html(host mis-order)Navigateto/and must not set product OGAcceptHTML on/privacyog:titlePOST/PUTto/privacy/*fallbackpath="*"LocationURL/ nginxreturn 301 https://…; no string concat of user inputVerification criteria
yarn test,yarn typecheck,yarn test:e2e(5 workers) green. New matcher tests cover every reserved alias.yarn preview+curl -sI http://127.0.0.1:5173/privacyis 404 or 301, not 200 homepage._redirects/render.yamlorder verified by reading the builtdist/_redirects(or equivalent) so reserved rules precede/*./privacyis 404/301;/is still Swap; hashed.jsmiss still 404 (not HTML). RTL or router test: reserved path does not render SwapPage./privacydoes not render TransferPage;/still does./privacy,/cookies,/opt-outon marketing, DEX, and bridge preview builds; confirm no product shell. Open/,/blog, DEX/pool, bridge/historyand confirm no regression.Out of scope
code/cl8y-ecosystem-legal).S1 opt-out is required (cl8y-pm inbox). Reserved-path routing on this ticket still must not paint Swap; opt-out UI is not authored here.
cl8y-agent-control: queued
implementjobcacd6e4a-83c1-4602-b7c5-6bd72041301e(not executed; no Hetzner VM).cl8y-agent-control: needs_human inbox card POST failed. Job stays parked.