diff --git a/apps/hosted-components/src/routes/__root.tsx b/apps/hosted-components/src/routes/__root.tsx index 55786d118..ebc437d2d 100644 --- a/apps/hosted-components/src/routes/__root.tsx +++ b/apps/hosted-components/src/routes/__root.tsx @@ -1,6 +1,10 @@ /// -import { StackClientApp, StackProvider, StackTheme } from '@hexclave/react'; +import { HexclaveClientApp, HexclaveProvider, HexclaveTheme } from '@hexclave/react'; import { publishableClientKeyNotNecessarySentinel } from '@hexclave/shared/dist/utils/oauth'; +import { runAsynchronously } from '@hexclave/shared/dist/utils/promises'; +import { validateRedirectUrl } from '@hexclave/shared/dist/utils/redirect-urls'; +import { isRelative } from '@hexclave/shared/dist/utils/urls'; +import { throwErr } from '@hexclave/shared/dist/utils/errors'; import { HeadContent, Outlet, @@ -49,6 +53,27 @@ function getApiBaseUrlFromEnv(): string | undefined { return import.meta.env.VITE_HEXCLAVE_API_URL ?? import.meta.env.VITE_STACK_API_URL ?? undefined; } +function isTrustedNavigationTarget(to: string): boolean { + return isRelative(to) || validateRedirectUrl(to, { trustedDomains: [window.location.origin] }); +} + +function useHostedComponentsNavigate() { + const navigate = useNavigate(); + + return useMemo(() => (to: string) => { + runAsynchronously(async () => { + if (to.startsWith("#")) { + await navigate({ hash: to.slice(1) }); + } else { + if (!isTrustedNavigationTarget(to)) { + throw new Error("Refusing to navigate to an untrusted URL"); + } + await navigate({ href: to }); + } + }); + }, [navigate]); +} + function FullPageError({ title, message }: { title: string, message: string }) { return (
@@ -142,7 +167,7 @@ function RootComponent() { const hexclaveApp = useMemo(() => { if (!projectId || !isValidProjectId) return null; - return new StackClientApp({ + return new HexclaveClientApp({ projectId, publishableClientKey: publishableClientKeyNotNecessarySentinel, tokenStore: "cookie", @@ -155,7 +180,7 @@ function RootComponent() { afterSignUp: "/", afterSignOut: "/handler/sign-in", }, - redirectMethod: { useNavigate: useNavigate as any } + redirectMethod: { useNavigate: useHostedComponentsNavigate }, }); }, [isValidProjectId, projectId]); @@ -171,13 +196,15 @@ function RootComponent() { return ; } + const app = hexclaveApp ?? throwErr("RootComponent expected a HexclaveClientApp after project ID validation."); + return ( - - + + - - + + ); }