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 (
-
-
+
+
-
-
+
+
);
}