import { useEffect, type ReactNode } from "react"; import { useNavigate, useParams, useRouterState } from "@tanstack/react-router"; // --------------------------------------------------------------------------- // Org-slug URL canonicalization for org-scoped hosts (cloud, self-host, // cloudflare). Console routes live under an optional `{-$orgSlug}` segment, so // the same tree serves `/policies` and `/acme/policies`; this gate, mounted // inside the authenticated shell, pins the URL to the active org's slug: // // - bare URL → replace with `//…` (canonicalize) // - active slug already in URL → render // - any other slug in URL → replace with `//…` (canonicalize) // // The URL slug is the request SCOPE, not just a label: every API call carries // it (the `x-executor-organization` header), and the server re-checks live // membership and resolves data for that org — same as the MCP URL-pinned org. // So a foreign slug never reaches this gate as "active" on a multi-org host: // the server returns no organization for an org the caller can't see, and the // shell 404s upstream. The host is what enforces that — it must not build this // subtree until the authenticated answer NAMES the slug in the URL, or an // optimistic answer about the session's own org (cloud's auth-hint cookie) // would mount the shell under a foreign address before the server ever // replies. That makes two browser tabs on different orgs fully // independent — no shared "active org" to steal. On a single-org host (e.g. // self-host) every slug resolves to the same org server-side, so a bogus slug // (e.g. `/totally-bogus`) would otherwise fuzzy-match a route and render // under the wrong URL forever; canonicalizing it here fixes the URL instead. // // A genuinely unmatched path is neither of the above: it has no `orgSlug` // param (nothing below root matched) but isn't a bare URL either, so it must // NOT canonicalize — doing so would silently rewrite a bad URL into a valid // one instead of letting the not-found page render. // --------------------------------------------------------------------------- export interface OrgSlugGateProps { /** The active organization's slug (from `useAuth().organization`). */ readonly activeSlug: string; readonly children: ReactNode; } export function OrgSlugGate(props: OrgSlugGateProps) { const { activeSlug } = props; const params = useParams({ strict: false }) as { orgSlug?: string }; const urlSlug = params.orgSlug ?? null; const navigate = useNavigate(); // Skip canonicalization whenever the router is currently sitting on a // not-found match — see the file header for why. const isNotFound = useRouterState({ select: (state) => state.matches.some((match) => match.globalNotFound), }); const needsCanonicalize = urlSlug !== activeSlug && !isNotFound; useEffect(() => { if (!needsCanonicalize) return; // Re-target the CURRENT route with the active slug. `to: "."` + // `search: true` keeps path and query (deep links like // `/integrations/add/mcp?url=…` canonicalize in place); only the orgSlug // param changes. void navigate({ to: ".", params: (previous: Record) => ({ ...previous, orgSlug: activeSlug }), search: true, replace: true, }); }, [needsCanonicalize, activeSlug, navigate]); // Render through while canonicalizing — the target is the same route, so // withholding children would only flash the page. return <>{props.children}; }