11import { useEffect , type ReactNode } from "react" ;
2- import { useNavigate , useParams } from "@tanstack/react-router" ;
2+ import { useNavigate , useParams , useRouterState } from "@tanstack/react-router" ;
33
44// ---------------------------------------------------------------------------
55// Org-slug URL canonicalization for org-scoped hosts (cloud, self-host,
66// cloudflare). Console routes live under an optional `{-$orgSlug}` segment, so
77// the same tree serves `/policies` and `/acme/policies`; this gate, mounted
8- // inside the authenticated shell, pins a BARE URL to the active org's slug:
8+ // inside the authenticated shell, pins the URL to the active org's slug:
99//
10- // - bare URL → replace with `/<active-slug>/…` (canonicalize)
11- // - slug already in URL → render
10+ // - bare URL → replace with `/<active-slug>/…` (canonicalize)
11+ // - active slug already in URL → render
12+ // - any other slug in URL → replace with `/<active-slug>/…` (canonicalize)
1213//
1314// The URL slug is the request SCOPE, not just a label: every API call carries
1415// it (the `x-executor-organization` header), and the server re-checks live
1516// membership and resolves data for that org — same as the MCP URL-pinned org.
16- // So a foreign slug never reaches this gate as "active": the server returns no
17- // organization for an org the caller can't see, and the shell 404s upstream.
18- // That makes two browser tabs on different orgs fully independent — no shared
19- // "active org" to steal.
17+ // So a foreign slug never reaches this gate as "active" on a multi-org host:
18+ // the server returns no organization for an org the caller can't see, and the
19+ // shell 404s upstream. That makes two browser tabs on different orgs fully
20+ // independent — no shared "active org" to steal. On a single-org host (e.g.
21+ // self-host) every slug resolves to the same org server-side, so a bogus slug
22+ // (e.g. `/totally-bogus`) would otherwise fuzzy-match a route and render
23+ // under the wrong URL forever; canonicalizing it here fixes the URL instead.
24+ //
25+ // A genuinely unmatched path is neither of the above: it has no `orgSlug`
26+ // param (nothing below root matched) but isn't a bare URL either, so it must
27+ // NOT canonicalize — doing so would silently rewrite a bad URL into a valid
28+ // one instead of letting the not-found page render.
2029// ---------------------------------------------------------------------------
2130
2231export interface OrgSlugGateProps {
@@ -31,9 +40,13 @@ export function OrgSlugGate(props: OrgSlugGateProps) {
3140 const urlSlug = params . orgSlug ?? null ;
3241 const navigate = useNavigate ( ) ;
3342
34- // Only a BARE URL canonicalizes. A slug in the URL is the scope the whole
35- // request chain already ran with, so it always matches `activeSlug` here.
36- const needsCanonicalize = urlSlug === null ;
43+ // Skip canonicalization whenever the router is currently sitting on a
44+ // not-found match — see the file header for why.
45+ const isNotFound = useRouterState ( {
46+ select : ( state ) => state . matches . some ( ( match ) => match . globalNotFound ) ,
47+ } ) ;
48+
49+ const needsCanonicalize = urlSlug !== activeSlug && ! isNotFound ;
3750
3851 useEffect ( ( ) => {
3952 if ( ! needsCanonicalize ) return ;
0 commit comments