Merge remote-tracking branch 'origin/dev' into cl/hexclave-pr3

# Conflicts:
#	apps/backend/src/app/api/latest/internal/backend-urls/route.tsx
#	packages/template/src/index.ts
#	packages/template/src/lib/stack-app/index.ts
This commit is contained in:
Bilal Godil 2026-05-28 09:45:26 -07:00
commit 5b047e101b
26 changed files with 210 additions and 89 deletions

View File

@ -12,7 +12,7 @@ A: `packages/stack-cli/src/lib/init-prompt.ts` re-exports `createInitPrompt` fro
A: Connected accounts live in `ProjectUserOAuthAccount`. Stored refresh tokens are in `OAuthToken` (`oauthAccountId`, `scopes`, `isValid`), and cached access tokens are in `OAuthAccessToken` (`expiresAt`, `scopes`, `isValid`). A null `OAuthAccessToken.expiresAt` means the OAuth provider did not supply an access-token expiry; `retrieveOrRefreshAccessToken` treats null-expiry tokens as candidates and still calls the provider-specific validity check before returning them. If no usable access token exists, it looks for valid refresh tokens with matching scopes and invalidates only those that the provider explicitly rejects.
## Q: Which Hexclave rename compatibility layers should be avoided in PR #1475 follow-ups?
A: Do not keep backwards compatibility for the MCP tool name, cross-domain auth query parameter names, `NEXT_PUBLIC_STACK_PORT_PREFIX`, or a parallel `hexclaveAppInternalsSymbol`. For refresh/access cookies, read both legacy Stack and new Hexclave cookie names, but only write the canonical Hexclave cookies.
A: Do not keep backwards compatibility for the MCP tool name, cross-domain auth query parameter names, `NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX`, or a parallel `hexclaveAppInternalsSymbol`. For refresh/access cookies, read both legacy Stack and new Hexclave cookie names, but only write the canonical Hexclave cookies.
## Q: How should GitHub Contents API request-body assertions be written in Stack Auth tests?
A: Prefer inline snapshots over individual field selectors. For request bodies that contain base64 file content, parse the JSON body, assert it is an object, decode the `content` field back to UTF-8, and snapshot the normalized call object so the test verifies the path, method, headers, branch, message, sha, and rendered file content together.
@ -393,7 +393,7 @@ A: Use `getAuthorizationHeader()`, which returns `Bearer stackauth_<base64(getAu
A: `RequestLike` accepts both `{ headers: { get(name): string | null } }` and `{ headers: Record<string, string | null> }`. Header lookup is case-insensitive for record-style headers, and supports `authorization`, `x-stack-auth`, and `cookie`.
### Q: Which env var should emulator onboarding URLs use for dashboard port?
A: Use `EMULATOR_DASHBOARD_PORT` (default `26700`) or explicit `STACK_LOCAL_EMULATOR_DASHBOARD_URL`. Do not derive emulator URLs from `NEXT_PUBLIC_STACK_PORT_PREFIX`, because that points to the host dev environment ports (e.g. `92xx`) rather than the emulator host-forwarded ports.
A: Use `EMULATOR_DASHBOARD_PORT` (default `26700`) or explicit `STACK_LOCAL_EMULATOR_DASHBOARD_URL`. Do not derive emulator URLs from `NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX`, because that points to the host dev environment ports (e.g. `92xx`) rather than the emulator host-forwarded ports.
### Q: Why does `PATCH /api/v1/internal/projects/current` fail in local emulator when updating only `onboarding_state`?
A: `createOrUpdateProjectWithLegacyConfig` always called `overrideEnvironmentConfigOverride`, even when there were zero config override keys to apply. In local emulator mode, environment config overrides are intentionally blocked, so this threw `Environment configuration overrides cannot be changed in the local emulator` and returned 500. The fix is to skip `overrideEnvironmentConfigOverride` unless `configOverrideOverride` has at least one key.

View File

@ -34,11 +34,11 @@ You should ALWAYS add new E2E tests when you change the API or SDK interface. Ge
Hexclave is a monorepo using Turbo for build orchestration. The main components are:
### Apps (`/apps`)
- **backend** (`/apps/backend`): Next.js API backend running on port `${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02` (defaults to 8102)
- **backend** (`/apps/backend`): Next.js API backend running on port `${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02` (defaults to 8102)
- Main API routes in `/apps/backend/src/app/api/latest`
- Database models using Prisma
- **dashboard** (`/apps/dashboard`): Admin dashboard on port `${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01` (defaults to 8101)
- **dev-launchpad**: Development portal on port `${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}00` (defaults to 8100)
- **dashboard** (`/apps/dashboard`): Admin dashboard on port `${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}01` (defaults to 8101)
- **dev-launchpad**: Development portal on port `${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}00` (defaults to 8100)
- **e2e**: End-to-end tests
### Packages (`/packages`)
@ -76,7 +76,7 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
- When writing tests, prefer .toMatchInlineSnapshot over other selectors, if possible. You can check (and modify) the snapshot-serializer.ts file to see how the snapshots are formatted and how non-deterministic values are handled.
- Whenever you learn something new, or at the latest right before you call the `Stop` tool, write whatever you learned into the .claude/CLAUDE-KNOWLEDGE.md file, in the Q&A format in there. You will later be able to look up knowledge from there (based on the question you asked).
- Animations: Keep hover/click transitions snappy and fast. Don't delay the action with a pre-transition (e.g. no fade-in when hovering a button) — it makes the UI feel sluggish. Instead, apply transitions after the action, like a smooth fade-out when the hover ends.
- Whenever you make changes in the dashboard, provide the user with a deep link to the dashboard page that you've just changed. Usually, this takes the form of `http://localhost:<whatever-is-in-$NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/...`, although sometimes it's different. If $NEXT_PUBLIC_STACK_PORT_PREFIX is set to 91, 92, or 93, use `a.localhost`, `b.localhost`, and `c.localhost` for the domains, respectively.
- Whenever you make changes in the dashboard, provide the user with a deep link to the dashboard page that you've just changed. Usually, this takes the form of `http://localhost:<whatever-is-in-$NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX>01/projects/-selector-/...`, although sometimes it's different. If $NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX is set to 91, 92, or 93, use `a.localhost`, `b.localhost`, and `c.localhost` for the domains, respectively.
- To update the list of apps available, edit `apps-frontend.tsx` and `apps-config.ts`. When you're tasked to implement a new app or a new page, always check existing apps for inspiration on how you could implement the new app or page.
- NEVER use Next.js dynamic functions if you can avoid them. Instead, prefer using a client component to make sure the page remains static (eg. prefer `usePathname` instead of `await params`).
- Whenever you make backwards-incompatible changes to the config schema, you must update the migration functions in `packages/stack-shared/src/config/schema.ts`!

View File

@ -36,7 +36,7 @@ For any security-related concerns & bug bounties, please email us at [security@h
NOTE: Every line of code should be reviewed by a human BEFORE you submit a PR. DO NOT waste our time by creating and submitting an AI-generated PR.
For vibecoding, it can help to have multiple parallel copies of the codebase open in different windows. For this, you can set the environment variable `NEXT_PUBLIC_STACK_PORT_PREFIX` to a different value (default 81). To do this consistently across all coding agents (Claude Code/Cursor Agent/Codex),we recommend you use `direnv` with a `.envrc` file:
For vibecoding, it can help to have multiple parallel copies of the codebase open in different windows. For this, you can set the environment variable `NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX` to a different value (default 81). To do this consistently across all coding agents (Claude Code/Cursor Agent/Codex),we recommend you use `direnv` with a `.envrc` file:
1. Install `direnv` if you haven't already. On Mac, the easiest way is to install it with Homebrew: `brew install direnv`.
2. Update ALL your shell configs to append the following lines. On most Mac setups, this is `~/.bash_profile`, `~/.bashrc`, `~/.zprofile`, `~/.zshrc`, and `~/.zshenv`.
@ -51,13 +51,13 @@ For vibecoding, it can help to have multiple parallel copies of the codebase ope
```sh
# .envrc
# make sure to install direnv and add it to your shell rc file (e.g. ~/.bashrc or ~/.zshrc)
export NEXT_PUBLIC_STACK_PORT_PREFIX=181
export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=181
# with this many processes running, it can be useful to add a custom title to all Node.js processes
# export NODE_OPTIONS="--require=<path-to-the-workspace-folder>/scripts/set-process-title.js $NODE_OPTIONS"
```
When you do this, it is recommended that you give all workspaces a port prefix other than 81, to prevent accidental conflicts when you forgot to make a feature support the $NEXT_PUBLIC_STACK_PORT_PREFIX environment variable. (for example: first workspace at 181, second workspace at 182, etc.)
When you do this, it is recommended that you give all workspaces a port prefix other than 81, to prevent accidental conflicts when you forgot to make a feature support the $NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX environment variable. (for example: first workspace at 181, second workspace at 182, etc.)
Also, the cookies on different ports may conflict with each other. To prevent this, open `a.localhost:18101` and `b.localhost:18201` instead or normal localhost, so the cookies are scoped differently.

View File

@ -1,5 +1,14 @@
import { describe, expect, it } from 'vitest';
import { parseAndValidateConfig } from './route';
import { getDefaultEntriesForRequest, parseAndValidateConfig } from './route';
function reqForHost(host: string, forwardedHost?: string) {
return {
headers: {
host: [host],
...(forwardedHost ? { "x-forwarded-host": [forwardedHost] } : {}),
},
};
}
describe('parseAndValidateConfig', () => {
it('should parse a single entry with probability 1', () => {
@ -66,3 +75,44 @@ describe('parseAndValidateConfig', () => {
expect(() => parseAndValidateConfig({ "1": "https://api.hexclave.com" })).toThrow();
});
});
describe('getDefaultEntriesForRequest', () => {
it('keeps legacy Stack Auth requests on Stack Auth API fallbacks', () => {
expect(getDefaultEntriesForRequest(reqForHost("api.stack-auth.com"))).toEqual([
{
probability: 1,
urls: [
"https://api.stack-auth.com",
"https://api1.stack-auth.com",
"https://api2.stack-auth.com",
],
},
]);
});
it('keeps Hexclave requests on Hexclave API fallbacks', () => {
expect(getDefaultEntriesForRequest(reqForHost("api.hexclave.com"))).toEqual([
{
probability: 1,
urls: [
"https://api.hexclave.com",
"https://api1.hexclave.com",
"https://api2.hexclave.com",
],
},
]);
});
it('maps fallback hosts back to the same brand canonical API host', () => {
expect(getDefaultEntriesForRequest(reqForHost("api2.stack-auth.com"))[0].urls[0]).toBe("https://api.stack-auth.com");
expect(getDefaultEntriesForRequest(reqForHost("api2.hexclave.com"))[0].urls[0]).toBe("https://api.hexclave.com");
});
it('prefers x-forwarded-host over host when selecting the brand', () => {
expect(getDefaultEntriesForRequest(reqForHost("api.stack-auth.com", "api.hexclave.com"))[0].urls).toEqual([
"https://api.hexclave.com",
"https://api1.hexclave.com",
"https://api2.hexclave.com",
]);
});
});

View File

@ -1,8 +1,8 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { getApiUrlForRequest } from "@/lib/request-api-url";
import { urlSchema, yupArray, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { getDefaultApiUrls } from "@hexclave/shared/dist/utils/urls";
/**
* Env var format: JSON object mapping probability (as string number) to URL arrays.
@ -44,24 +44,47 @@ export function parseAndValidateConfig(raw: unknown): Array<{ probability: numbe
}
let cachedEntries: ReturnType<typeof parseAndValidateConfig> | undefined;
function getCachedConfig() {
function getConfiguredEntries() {
if (!cachedEntries) {
const rawEnv = getEnvVariable("STACK_BACKEND_URLS_CONFIG", "");
if (rawEnv) {
let parsed;
try {
parsed = JSON.parse(rawEnv);
} catch (e) {
throw new HexclaveAssertionError(`STACK_BACKEND_URLS_CONFIG is not valid JSON: ${e}`);
}
cachedEntries = parseAndValidateConfig(parsed);
} else {
cachedEntries = [{ probability: 1, urls: getDefaultApiUrls(getEnvVariable("NEXT_PUBLIC_STACK_API_URL")) }];
if (!rawEnv) {
return undefined;
}
let parsed;
try {
parsed = JSON.parse(rawEnv);
} catch (e) {
throw new HexclaveAssertionError(`STACK_BACKEND_URLS_CONFIG is not valid JSON: ${e}`);
}
cachedEntries = parseAndValidateConfig(parsed);
}
return cachedEntries;
}
function getDefaultBackendUrls(primaryBaseUrl: string): string[] {
if (primaryBaseUrl === "https://api.stack-auth.com") {
return ["https://api.stack-auth.com", "https://api1.stack-auth.com", "https://api2.stack-auth.com"];
}
if (primaryBaseUrl === "https://api.dev.stack-auth.com") {
return ["https://api.dev.stack-auth.com", "https://api1.dev.stack-auth.com", "https://api2.dev.stack-auth.com"];
}
if (primaryBaseUrl === "https://api.hexclave.com") {
return ["https://api.hexclave.com", "https://api1.hexclave.com", "https://api2.hexclave.com"];
}
if (primaryBaseUrl === "https://api.dev.hexclave.com") {
return ["https://api.dev.hexclave.com", "https://api1.dev.hexclave.com", "https://api2.dev.hexclave.com"];
}
const localhostMatch = primaryBaseUrl.match(/^http:\/\/localhost:(\d+)02$/);
if (localhostMatch) {
return [primaryBaseUrl, `http://localhost:${localhostMatch[1]}10`];
}
return [primaryBaseUrl];
}
export function getDefaultEntriesForRequest(req: { headers: Record<string, string[] | undefined> }): ReturnType<typeof parseAndValidateConfig> {
return [{ probability: 1, urls: getDefaultBackendUrls(getApiUrlForRequest(req)) }];
}
export const GET = createSmartRouteHandler({
metadata: {
hidden: true,
@ -79,8 +102,8 @@ export const GET = createSmartRouteHandler({
urls: yupArray(yupString().defined()).defined(),
}).defined(),
}),
handler: async () => {
const entries = getCachedConfig();
handler: async (_req, fullReq) => {
const entries = getConfiguredEntries() ?? getDefaultEntriesForRequest(fullReq);
const roll = Math.random();
let cumulative = 0;

View File

@ -20,7 +20,7 @@ describe('validateRedirectUrl', () => {
const stackKeys = [
"NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE",
"NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX",
"NEXT_PUBLIC_STACK_PORT_PREFIX",
"NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX",
] as const;
const hexclaveOf = (name: string) => name.replace("STACK_", "HEXCLAVE_");
const allKeys = [...stackKeys, ...stackKeys.map(hexclaveOf)];
@ -28,7 +28,7 @@ describe('validateRedirectUrl', () => {
const newValues: Record<string, string | undefined> = {
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE: values.hostedHandlerUrlTemplate,
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX: values.hostedHandlerDomainSuffix,
NEXT_PUBLIC_STACK_PORT_PREFIX: values.stackPortPrefix,
NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: values.stackPortPrefix,
};
for (const stackKey of stackKeys) {
newValues[hexclaveOf(stackKey)] = newValues[stackKey];
@ -69,7 +69,7 @@ describe('validateRedirectUrl', () => {
describe('exact domain matching', () => {
it('should implicitly validate hosted handler domains for the project', () => {
withHostedHandlerEnv({
hostedHandlerUrlTemplate: "http://{projectId}.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09/{hostedPath}",
hostedHandlerUrlTemplate: "http://{projectId}.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09/{hostedPath}",
stackPortPrefix: "92",
}, () => {
const tenancy = createMockTenancy({

View File

@ -9,7 +9,7 @@ export function getHostedHandlerTrustedDomain(projectId: string): string {
projectId,
hostedHandlerDomainSuffix: getProcessEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX"),
hostedHandlerUrlTemplate: getProcessEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE"),
stackPortPrefix: getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"),
stackPortPrefix: getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81"),
});
}

View File

@ -34,6 +34,7 @@ const corsAllowedRequestHeaders = [
// User auth
'x-stack-refresh-token',
'x-stack-access-token',
'x-stack-allow-restricted-user',
'x-stack-allow-anonymous-user',
// Sentry

View File

@ -75,9 +75,9 @@ export NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL=${NEXT_PUBLIC_STACK_DASHBOARD_URL
# is STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX, and the sentinel
# substitution loop below derives the env var name from the sentinel — so this
# MUST export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX or the sentinel never resolves.
# Accept the legacy NEXT_PUBLIC_STACK_PORT_PREFIX as input for back-compat with
# Accept the legacy NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX as input for back-compat with
# existing self-host configs.
export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}}
export NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}}
PORT_PREFIX=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX}
export NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL="http://localhost:${PORT_PREFIX}01"
export NEXT_PUBLIC_BROWSER_STACK_API_URL=${NEXT_PUBLIC_STACK_API_URL}

View File

@ -26,7 +26,7 @@ From the repository root:
pnpm -C docs-mintlify run dev
```
This starts Mintlify in `docs-mintlify` on `http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}04` (for example, `http://localhost:8104` with the default prefix).
This starts Mintlify in `docs-mintlify` on `http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04` (for example, `http://localhost:8104` with the default prefix).
From inside `docs-mintlify`, you can also run:

View File

@ -8,7 +8,7 @@ This is the documentation site for Stack Auth, built with [Next.js](https://next
pnpm dev
```
The docs server runs on port `8104` by default (or `${NEXT_PUBLIC_STACK_PORT_PREFIX}04`).
The docs server runs on port `8104` by default (or `${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX}04`).
## Project Structure

View File

@ -16,7 +16,7 @@ export const remindersPrompt = deindent`
- Take extra care to always have great error handling and loading states whenever necessary (including in button onClick handlers; Hexclave's code examples often use a special onClick class which handles loading states, but your own button may not). Hexclave's SDK tends to return errors that need to be handled explicitly in its return types.
- 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/stack (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.
- 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 \`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:

View File

@ -2,8 +2,12 @@ import type { BranchConfigNormalizedOverride } from "./config/schema";
type StackConfigObject = BranchConfigNormalizedOverride;
export const showOnboardingStackConfigValue = "show-onboarding";
/** @deprecated Use `HexclaveConfig` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackConfig = StackConfigObject | typeof showOnboardingStackConfigValue;
// Hexclave alias — same shape, declared separately so it doesn't inherit the deprecation tag.
export type HexclaveConfig = StackConfigObject | typeof showOnboardingStackConfigValue;
type StrictConfigShape<Actual, Expected> =
Expected extends readonly unknown[]
? Actual extends readonly unknown[]
@ -22,6 +26,12 @@ type StrictStackConfig<T extends StackConfig> =
? T & StrictConfigShape<T, StackConfigObject>
: T;
/** @deprecated Use `defineHexclaveConfig` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export function defineStackConfig<const T extends StackConfig>(config: StrictStackConfig<T>): T {
return config;
}
// Hexclave alias — separate function so it does not inherit the deprecation tag.
export function defineHexclaveConfig<const T extends HexclaveConfig>(config: StrictStackConfig<T>): T {
return config;
}

View File

@ -15,7 +15,7 @@ const hostedHandlerTemplateProjectIdB = "11111111-1111-4111-8111-111111111111";
function replaceStackPortPrefix(input: string | undefined, stackPortPrefix: string | undefined): string | undefined {
if (input == null) return undefined;
return stackPortPrefix ? input.replace(/\$\{NEXT_PUBLIC_STACK_PORT_PREFIX:-81\}/g, stackPortPrefix) : input;
return stackPortPrefix ? input.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, stackPortPrefix) : input;
}
function getHostedHandlerUrlFromTemplate(template: string, projectId: string, hostedPath: string): string {
@ -232,7 +232,7 @@ import.meta.vitest?.test("validateRedirectUrl trusts implicit hosted handler dom
allowLocalhost: false,
trustedDomains: getImplicitlyTrustedDomainsForProject({
projectId,
hostedHandlerUrlTemplate: "http://{projectId}.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09/{hostedPath}",
hostedHandlerUrlTemplate: "http://{projectId}.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09/{hostedPath}",
stackPortPrefix: "92",
}),
})).toBe(true);

View File

@ -6,7 +6,7 @@
import { BaseHandlerProps, StackHandlerClient } from "./stack-handler-client";
export default function StackHandler({ app, routeProps, params, searchParams, ...props }: BaseHandlerProps & { location?: string } & {
type StackHandlerProps = BaseHandlerProps & { location?: string } & {
/**
* @deprecated The app parameter is no longer necessary. You can safely remove it.
*/
@ -26,6 +26,19 @@ export default function StackHandler({ app, routeProps, params, searchParams, ..
* @deprecated The searchParams parameter is no longer necessary. You can safely remove it.
*/
searchParams?: any,
}) {
};
function HandlerImpl({ app, routeProps, params, searchParams, ...props }: StackHandlerProps) {
return <StackHandlerClient {...props} />;
}
// Non-deprecated Hexclave-branded export.
export const HexclaveHandler = HandlerImpl;
/** @deprecated Use `HexclaveHandler` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export const StackHandler = HandlerImpl;
// Default export preserved for backwards compatibility (legacy `as`-rename re-exports).
// Points at the deprecated alias so that `import StackHandler from ".../stack-handler"` still
// surfaces the deprecation. Internal re-exports in template/src/index.ts use the named exports.
export default StackHandler;

View File

@ -6,30 +6,21 @@ import "./internal/deprecation-warning";
export * from './lib/stack-app';
export { getConvexProvidersConfig } from "./integrations/convex";
// Hexclave aliases — same symbols under the new brand name
export type { StackConfig as HexclaveConfig } from "@hexclave/shared/config";
export { defineStackConfig as defineHexclaveConfig } from "@hexclave/shared/config";
/** @deprecated Use `HexclaveConfig` instead — same symbol, new brand name. */
export type { StackConfig } from "@hexclave/shared/config";
/** @deprecated Use `defineHexclaveConfig` instead — same symbol, new brand name. */
export { defineStackConfig } from "@hexclave/shared/config";
// Hexclave aliases and legacy Stack* names — @deprecated JSDoc lives on the original
// declarations in @hexclave/shared/config so it survives dts bundling
// (per-specifier JSDoc on re-exports does not).
export type { HexclaveConfig, StackConfig } from "@hexclave/shared/config";
export { defineHexclaveConfig, defineStackConfig } from "@hexclave/shared/config";
// IF_PLATFORM react-like
export type { AnalyticsOptions, AnalyticsReplayOptions } from "./lib/stack-app/apps/implementations/session-replay";
// Hexclave aliases — same symbols under the new brand name
export { default as HexclaveHandler } from "./components-page/stack-handler";
export { useStackApp as useHexclaveApp } from "./lib/hooks";
export { default as HexclaveProvider } from "./providers/stack-provider";
export { StackTheme as HexclaveTheme } from './providers/theme-provider';
/** @deprecated Use `HexclaveHandler` instead — same symbol, new brand name. */
export { default as StackHandler } from "./components-page/stack-handler";
/** @deprecated Use `useHexclaveApp` instead — same symbol, new brand name. */
export { useStackApp } from "./lib/hooks";
// Hexclave aliases and legacy Stack* names — @deprecated JSDoc lives on the original
// declarations in the source files (so it survives dts bundling).
export { HexclaveHandler, StackHandler } from "./components-page/stack-handler";
export { useHexclaveApp, useStackApp } from "./lib/hooks";
export { HexclaveProvider, StackProvider } from "./providers/stack-provider";
export { HexclaveTheme, StackTheme } from './providers/theme-provider';
export { useUser } from "./lib/hooks";
/** @deprecated Use `HexclaveProvider` instead — same symbol, new brand name. */
export { default as StackProvider } from "./providers/stack-provider";
/** @deprecated Use `HexclaveTheme` instead — same symbol, new brand name. */
export { StackTheme } from './providers/theme-provider';
export { AccountSettings } from "./components-page/account-settings";
export { AuthPage } from "./components-page/auth-page";

View File

@ -16,9 +16,9 @@ export function useUser(options: GetUserOptions & { or: 'redirect' | 'throw' }):
export function useUser(options: GetUserOptions & { projectIdMustMatch: "internal" }): CurrentInternalUser | null;
export function useUser(options?: GetUserOptions): CurrentUser | CurrentInternalUser | null;
export function useUser(options: GetUserOptions = {}): CurrentUser | CurrentInternalUser | null {
const stackApp = useStackApp(options);
const stackApp = useHexclaveApp(options);
if (options.projectIdMustMatch && stackApp.projectId !== options.projectIdMustMatch) {
throw new Error("Unexpected project ID in useStackApp: " + stackApp.projectId);
throw new Error("Unexpected project ID in useHexclaveApp: " + stackApp.projectId);
}
if (options.projectIdMustMatch === "internal") {
return stackApp.useUser(options) as CurrentInternalUser;
@ -27,22 +27,33 @@ export function useUser(options: GetUserOptions = {}): CurrentUser | CurrentInte
}
}
/**
* Returns the current Hexclave app associated with the HexclaveProvider.
*
* @returns the current Hexclave app
*/
export function useHexclaveApp<ProjectId extends string>(options: { projectIdMustMatch?: ProjectId } = {}): StackClientApp<true, ProjectId> {
if (typeof useContext !== "function") {
throw new Error("useHexclaveApp() can only be used in a React Client Component. Make sure you're not calling it from a Server Component, or any other environment.");
}
const context = useContext(StackContext);
if (context === null) {
throw new Error("useHexclaveApp must be used within a HexclaveProvider");
}
const stackApp = context.app;
if (options.projectIdMustMatch && stackApp.projectId !== options.projectIdMustMatch) {
throw new Error("Unexpected project ID in useHexclaveApp: " + stackApp.projectId);
}
return stackApp as StackClientApp<true, ProjectId>;
}
/**
* Returns the current Stack app associated with the StackProvider.
*
* @deprecated Use `useHexclaveApp` from the `@hexclave/*` package instead same symbol, new brand name. See https://docs.hexclave.com/migration.
*
* @returns the current Stack app
*/
export function useStackApp<ProjectId extends string>(options: { projectIdMustMatch?: ProjectId } = {}): StackClientApp<true, ProjectId> {
if (typeof useContext !== "function") {
throw new Error("useStackApp() can only be used in a React Client Component. Make sure you're not calling it from a Server Component, or any other environment.");
}
const context = useContext(StackContext);
if (context === null) {
throw new Error("useStackApp must be used within a StackProvider");
}
const stackApp = context.app;
if (options.projectIdMustMatch && stackApp.projectId !== options.projectIdMustMatch) {
throw new Error("Unexpected project ID in useStackApp: " + stackApp.projectId);
}
return stackApp as StackClientApp<true, ProjectId>;
return useHexclaveApp(options);
}

View File

@ -56,6 +56,7 @@ import type { AdminSessionReplay, ListSessionReplayChunksOptions, ListSessionRep
export type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplaysOptions, ListSessionReplaysResult, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, SessionReplayAllEventsResult } from "../../session-replays";
/** @deprecated Use `HexclaveAdminAppConstructorOptions` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = (
& StackServerAppConstructorOptions<HasTokenStore, ProjectId>
& {
@ -65,6 +66,7 @@ export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, Proje
);
/** @deprecated Use `HexclaveAdminApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
& AsyncStoreProperty<"project", [], AdminProject, false>
& AsyncStoreProperty<"internalApiKeys", [], InternalApiKey[], true>
@ -170,6 +172,7 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
}
& StackServerApp<HasTokenStore, ProjectId>
);
/** @deprecated Use `HexclaveAdminAppConstructor` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackAdminAppConstructor = {
new <
HasTokenStore extends boolean,
@ -181,4 +184,5 @@ export type HexclaveAdminAppConstructorOptions<HasTokenStore extends boolean, Pr
export type HexclaveAdminApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = StackAdminApp<HasTokenStore, ProjectId>;
export type HexclaveAdminAppConstructor = StackAdminAppConstructor;
export const HexclaveAdminApp: HexclaveAdminAppConstructor = _StackAdminAppImpl;
/** @deprecated Use `HexclaveAdminApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export const StackAdminApp: StackAdminAppConstructor = HexclaveAdminApp;

View File

@ -9,6 +9,7 @@ import { ProjectCurrentUser, SyncedPartialUser, TokenPartialUser } from "../../u
import { _StackClientAppImpl } from "../implementations";
import { AnalyticsOptions } from "../implementations/session-replay";
/** @deprecated Use `HexclaveClientAppConstructorOptions` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackClientAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = {
baseUrl?: string | { browser: string, server: string },
extraRequestHeaders?: Record<string, string>,
@ -46,11 +47,13 @@ export type StackClientAppConstructorOptions<HasTokenStore extends boolean, Proj
);
/** @deprecated Use `HexclaveClientAppJson` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & { inheritsFrom?: undefined } & {
uniqueIdentifier: string,
// note: if you add more fields here, make sure to ensure the checkString in the constructor has/doesn't have them
};
/** @deprecated Use `HexclaveClientApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
& {
readonly projectId: ProjectId,
@ -153,6 +156,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
& { [K in `redirectTo${Capitalize<keyof Omit<HandlerUrls, 'handler' | 'oauthCallback'>>}`]: (options?: RedirectToOptions) => Promise<void> }
& AuthLike<HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit }>
);
/** @deprecated Use `HexclaveClientAppConstructor` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackClientAppConstructor = {
new <
TokenStoreType extends string,
@ -172,4 +176,5 @@ export type HexclaveClientAppJson<HasTokenStore extends boolean, ProjectId exten
export type HexclaveClientApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = StackClientApp<HasTokenStore, ProjectId>;
export type HexclaveClientAppConstructor = StackClientAppConstructor;
export const HexclaveClientApp: HexclaveClientAppConstructor = _StackClientAppImpl;
/** @deprecated Use `HexclaveClientApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export const StackClientApp: StackClientAppConstructor = HexclaveClientApp;

View File

@ -11,10 +11,12 @@ import { _StackServerAppImpl } from "../implementations";
import { StackClientApp, StackClientAppConstructorOptions } from "./client-app";
/** @deprecated Use `HexclaveServerAppConstructorOptions` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackServerAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & {
secretServerKey?: string,
};
/** @deprecated Use `HexclaveServerApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackServerApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
& {
createTeam(data: ServerTeamCreateOptions): Promise<ServerTeam>,
@ -116,6 +118,7 @@ export type StackServerApp<HasTokenStore extends boolean = boolean, ProjectId ex
>
& StackClientApp<HasTokenStore, ProjectId>
);
/** @deprecated Use `HexclaveServerAppConstructor` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export type StackServerAppConstructor = {
new <
TokenStoreType extends string,
@ -128,4 +131,5 @@ export type HexclaveServerAppConstructorOptions<HasTokenStore extends boolean, P
export type HexclaveServerApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = StackServerApp<HasTokenStore, ProjectId>;
export type HexclaveServerAppConstructor = StackServerAppConstructor;
export const HexclaveServerApp: HexclaveServerAppConstructor = _StackServerAppImpl;
/** @deprecated Use `HexclaveServerApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export const StackServerApp: StackServerAppConstructor = HexclaveServerApp;

View File

@ -6,11 +6,10 @@ export {
// Legacy Stack* aliases — same runtime symbols, kept for backwards compatibility.
// Prefer the Hexclave* equivalents in new code.
/** @deprecated Use `HexclaveAdminApp` instead — same symbol, new brand name. */
// The @deprecated JSDoc lives on the original declarations in ./apps/interfaces/*.ts
// so it survives dts bundling (per-specifier JSDoc on re-exports does not).
export { StackAdminApp } from "./apps";
/** @deprecated Use `HexclaveClientApp` instead — same symbol, new brand name. */
export { StackClientApp } from "./apps";
/** @deprecated Use `HexclaveServerApp` instead — same symbol, new brand name. */
export { StackServerApp } from "./apps";
// HexclaveAdminApp / HexclaveClientApp / HexclaveServerApp are already exported above as values
@ -26,19 +25,14 @@ export type {
HexclaveServerAppConstructorOptions,
} from "./apps";
/** @deprecated Use `HexclaveAdminAppConstructor` instead — same symbol, new brand name. */
// Legacy Stack* type aliases — @deprecated tags live on the original declarations
// in ./apps/interfaces/*.ts (per-specifier JSDoc on re-exports doesn't survive dts bundling).
export type { StackAdminAppConstructor } from "./apps";
/** @deprecated Use `HexclaveAdminAppConstructorOptions` instead — same symbol, new brand name. */
export type { StackAdminAppConstructorOptions } from "./apps";
/** @deprecated Use `HexclaveClientAppConstructor` instead — same symbol, new brand name. */
export type { StackClientAppConstructor } from "./apps";
/** @deprecated Use `HexclaveClientAppConstructorOptions` instead — same symbol, new brand name. */
export type { StackClientAppConstructorOptions } from "./apps";
/** @deprecated Use `HexclaveClientAppJson` instead — same symbol, new brand name. */
export type { StackClientAppJson } from "./apps";
/** @deprecated Use `HexclaveServerAppConstructor` instead — same symbol, new brand name. */
export type { StackServerAppConstructor } from "./apps";
/** @deprecated Use `HexclaveServerAppConstructorOptions` instead — same symbol, new brand name. */
export type { StackServerAppConstructorOptions } from "./apps";
export type {

View File

@ -173,7 +173,7 @@ describe("handler URL targets", () => {
});
it("uses the full hosted handler URL template when configured", () => {
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE", "http://{projectId}.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09/{hostedPath}");
vi.stubEnv("NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE", "http://{projectId}.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09/{hostedPath}");
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "93");
const urls = resolveHandlerUrls({

View File

@ -90,10 +90,20 @@ function ReactStackProvider({
}
// END_PLATFORM
// Pick the platform-appropriate provider implementation. Only the active branch's
// line is preserved by the platform-stripping script when generating per-platform SDKs.
// The /* ... */ block hides the inactive branches from the template's TypeScript compiler.
// IF_PLATFORM next
export default NextStackProvider;
const ActiveProvider = NextStackProvider;
/* ELSE_IF_PLATFORM tanstack-start
export default TanStackStartStackProvider;
const ActiveProvider = TanStackStartStackProvider;
ELSE_PLATFORM
export default ReactStackProvider;
const ActiveProvider = ReactStackProvider;
END_PLATFORM */
// Named exports live outside the platform conditional so the @deprecated JSDoc can
// use a /** ... */ block without colliding with the outer comment terminator.
export const HexclaveProvider = ActiveProvider;
/** @deprecated Use `HexclaveProvider` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
export const StackProvider = ActiveProvider;
export default ActiveProvider;

View File

@ -77,7 +77,7 @@ function convertColorsToCSS(theme: Theme) {
}
export function StackTheme({
export function HexclaveTheme({
theme,
children,
nonce,
@ -109,3 +109,8 @@ export function StackTheme({
</>
);
}
/**
* @deprecated Use `HexclaveTheme` from the `@hexclave/*` package instead same symbol, new brand name. See https://docs.hexclave.com/migration.
*/
export const StackTheme = HexclaveTheme;

View File

@ -188,7 +188,7 @@ Tests use Swift Testing framework against a running backend.
swift test
```
The tests connect to `http://localhost:8102` (or `${NEXT_PUBLIC_STACK_PORT_PREFIX}02`).
The tests connect to `http://localhost:8102` (or `${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX}02`).
## API Reference

View File

@ -6,10 +6,10 @@ import FoundationNetworking
/// Shared test configuration
/// Set environment variables to customize test behavior:
/// - NEXT_PUBLIC_STACK_PORT_PREFIX: Port prefix for backend (default: "81")
/// - NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: Port prefix for backend (default: "81")
/// - STACK_SKIP_E2E_TESTS: Set to "true" to skip E2E tests
struct TestConfig {
static let portPrefix = ProcessInfo.processInfo.environment["NEXT_PUBLIC_STACK_PORT_PREFIX"] ?? "81"
static let portPrefix = ProcessInfo.processInfo.environment["NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX"] ?? "81"
static let baseUrl = "http://localhost:\(portPrefix)02"
static let skipE2E = ProcessInfo.processInfo.environment["STACK_SKIP_E2E_TESTS"] == "true"