Hosted Components Accounts Navigation bug (#1565)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/hexclave/hexclave/blob/dev/CONTRIBUTING.md

-->

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes and hardens hosted components navigation. Restores Accounts tab
behavior without full-page reloads by using a validated, async navigate
adapter and migrating to `Hexclave*` components.

- **Bug Fixes**
- Replaced `redirectMethod` with `useHostedComponentsNavigate` to route
`#...` via `hash` and others via `href`, allowing only trusted targets
via `isRelative`/`validateRedirectUrl` with `trustedDomains:
[window.location.origin]`.
- Runs redirects with `runAsynchronously` from `@hexclave/shared` to
avoid race conditions.

- **Refactors**
- Migrated from `StackClientApp`/`StackProvider`/`StackTheme` to
`HexclaveClientApp`/`HexclaveProvider`/`HexclaveTheme` in
`@hexclave/react`.
- Regenerated Mintlify docs/snippets to use Hexclave naming and clarify
the Auth SDK `urls` option and `hexclave dev` env var injection.

<sup>Written for commit 41ba5d0b15.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1565?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved client-side navigation handling: allow hash navigation and
enforce/trust-check redirect targets to prevent unsafe external
navigation.

* **Refactor**
* Switched UI runtime to Hexclave-specific app, provider, and theme
components for consistent theming and behavior.

* **Documentation**
* Clarified setup: `hexclave dev` now auto-injects required environment
variables.
* Updated guidance for configuring auth/redirect URLs to ensure correct
post-auth navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Armaan Jain 2026-06-10 11:14:38 -07:00 committed by GitHub
parent 76fc62e98b
commit 88ef2ce85f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 50 additions and 8 deletions

View File

@ -1,6 +1,10 @@
/// <reference types="vite/client" />
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 (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
@ -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 <FullPageError title="Something went wrong" message={`Invalid project ID: ${projectId}. Project IDs must be UUIDs.`} />;
}
const app = hexclaveApp ?? throwErr("RootComponent expected a HexclaveClientApp after project ID validation.");
return (
<ErrorBoundary>
<StackProvider app={hexclaveApp!}>
<StackTheme>
<HexclaveProvider app={app}>
<HexclaveTheme>
<Outlet />
</StackTheme>
</StackProvider>
</HexclaveTheme>
</HexclaveProvider>
</ErrorBoundary>
);
}

View File

@ -787,6 +787,8 @@ export const onSetupFilterClick = (event) => {
}
}
```
`hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup.
</Accordion>
<Accordion title="Option 2: Connecting to a production project hosted in the cloud">
@ -982,6 +984,8 @@ export const onSetupFilterClick = (event) => {
}
}
```
`hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup.
</Accordion>
<Accordion title="Option 2: Connecting to a production project hosted in the cloud">
@ -1211,6 +1215,8 @@ export const onSetupFilterClick = (event) => {
}
}
```
`hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup.
</Accordion>
<Accordion title="Option 2: Connecting to a production project hosted in the cloud">
@ -1399,6 +1405,8 @@ export const onSetupFilterClick = (event) => {
}
}
```
`hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup.
</Accordion>
<Accordion title="Option 2: Connecting to a production project hosted in the cloud">
@ -1650,6 +1658,8 @@ export const onSetupFilterClick = (event) => {
}
}
```
`hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup.
</Accordion>
<Accordion title="Option 2: Connecting to a production project hosted in the cloud">
@ -1842,6 +1852,8 @@ export const onSetupFilterClick = (event) => {
}
}
```
`hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup.
</Accordion>
<Accordion title="Option 2: Connecting to a production project hosted in the cloud">

View File

@ -18,6 +18,7 @@ Below are some reminders on Hexclave and how to learn more about it. If you're s
- Language, framework, and library-specific details:
- JavaScript & TypeScript:
- Hexclave has different SDK packages for different frameworks and languages. As of the time of writing these reminders, they are: @hexclave/js (JavaScript/TypeScript), @hexclave/next (Next.js), @hexclave/react (React), @hexclave/tanstack-start (TanStack Start). You can find all of these on npm. They are all versioned together, meaning that vX.Y.Z of one SDK was released at the same time as vX.Y.Z of another SDK. For the most part, they are the same, although each has platform-specific features and differences.
- The Hexclave/Stack Auth SDK constructor accepts a `urls` option that tells the SDK where auth pages and post-auth redirects live. When you add a custom auth page such as a `sign-in`, `sign-up`, `forgot-password`, `account-settings`, etc., update the corresponding `urls` key to point to that route; also set redirect targets such as `afterSignIn`, `afterSignUp`, `afterSignOut`, and `home` when those destinations are customized. The `urls` option is the source of truth for redirect helpers such as `redirectToSignIn()`, hosted or handler-page flows, and post-auth navigation; if it is left pointing at the default pages after custom pages are added, users can hit extra redirects, land on the wrong auth page, or return to an unexpected page after signing in or out.
- The `Result<T, E>` type is `{ status: "ok", data: T } | { status: "error", error: E }`.
- `KnownErrors[KNOWN_ERROR_CODE]` refers to a specific known error type. Each KnownError may have its own properties, but they all inherit from `Error & { statusCode: number, humanReadableMessage: string, details?: Json }`.
- React & Next.js:
@ -255,6 +256,8 @@ The frameworks and languages with explicit SDK support are:
}
}
```
`hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup.
</Accordion>
<Accordion title="Option 2: Connecting to a production project hosted in the cloud">

File diff suppressed because one or more lines are too long