PR 4: Rename Stack -> Hexclave: examples config module, app-internal symbols, crypto docs (#1534)

## What

Continues the **Stack Auth → Hexclave** rename for a set of safe,
internal-only surfaces. This intentionally avoids public-contract names.

### Changes
- **Examples** — renamed the user-facing config module
`stack.ts`/`stack.tsx` (and the `convex` / `lovable` `stack/`
directories) to `hexclave`, and updated every importer across
`.ts`/`.tsx`/`.jsx`. The public `app/handler/[...stack]/` route segment
is left unchanged.
- **apps/{dashboard,backend,internal-tool}** — renamed app-local
SDK-init symbols `stackClientApp → hexclaveClientApp` and
`getStackServerApp → getHexclaveServerApp`, and the dashboard
`StackCompanion` component → `HexclaveCompanion` (incl.
`useStackCompanion`, context types). The public
`StackClientApp`/`StackServerApp` SDK classes are **unchanged**.
- **packages/stack-shared** — added comments to the crypto / JWT / vault
`stack-*` literals documenting that they must **not** be renamed (key
derivation / JWKS / KMS-alias stability). The literals are
byte-identical.

### Deliberately excluded
- **`STACK_*` → `HEXCLAVE_*` env-var rename** — `HEXCLAVE_*` already
resolves via the dual-read layers (SDK env, dashboard `_inlineEnvVars`,
`getEnvVariable`). The remaining holdout is the docker post-build
sentinel path, which the codebase authors explicitly deferred and which
is tightly coupled to `entrypoint.sh` + untestable here. A blind rename
there risks silently breaking self-host/emulator bootstrap for ~zero
functional gain.
- **All public-contract names** — SDK class names, env vars, HTTP
headers (`x-stack-*`), and the `/handler` route convention.

## Verification
- `pnpm lint` — **29/29 passing**.
- `pnpm typecheck` — **28/29 passing**; the only failure is
`@hexclave/docs` (pre-existing missing fumadocs `.source` codegen,
untouched by this PR).
- Two rounds of adversarial multi-agent review; findings fixed:
string-literal collateral from the symbol sweep (CLI test fixtures + an
AI-prompt template) reverted, and a missed `.jsx` importer in
`examples/cjs-test` corrected.

## Notes
- Based on a `dev` snapshot from when the branch was cut (a couple
commits behind tip); the diff contains only the changes above.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Complete the internal “Stack” → “Hexclave” rename across examples,
app-local code, config tooling, and setup docs, and standardize env
output to HEXCLAVE_* with correct default API URL handling. Public SDK
classes, handler routes, and legacy env names keep working.

- **Refactors**
- Examples/config: `stack.*` files and `stack/` dirs →
`hexclave.*`/`hexclave/`; imports updated; keep `app/handler/[...stack]`
route.
- Apps: backend/dashboard/internal-tool now use `getHexclaveServerApp`
and `hexclaveClientApp`; dashboard `StackCompanion` →
`HexclaveCompanion`. Public `StackClientApp`/`StackServerApp` unchanged.
- Env/setup: Next.js and CLI generators write HEXCLAVE_* and omit API
URL when using https://api.stack-auth.com; CLI `doctor` and auth
resolution prefer HEXCLAVE_* (e.g. `HEXCLAVE_SECRET_SERVER_KEY`,
`HEXCLAVE_PROJECT_ID`) with `STACK_*` fallback.
- Config tooling: `stack-config-file` → `hexclave-config-file`, emitting
`HexclaveConfig`; imports updated across backend/dashboard/tooling.
- Shared/docs: added “do not rename” notes for crypto/JWT/vault
`stack-*` literals; regenerated setup prompt/docs to use
`hexclave.config.ts`, `hexclave dev`, and `src/hexclave/`.
- Tests: updated snapshots/assertions to expect `HexclaveConfig` and
HEXCLAVE_* env names.

- **Migration**
  - No action required. SDK and CLI read both HEXCLAVE_* and STACK_*.

<sup>Written for commit 8a891b4f6c.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1534?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

* **Refactor**
* Renamed internal app/client/server instances and companion/provider
components to the new product name across backend, dashboard, examples,
and tooling; imports updated accordingly.
* Updated generated environment variable names and CLI init/doctor
outputs to prefer the new product prefix.

* **Documentation**
* Added clarifying notes about vault/encryption and JWT/key labels to
avoid breaking existing encrypted data.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
BilalG1 2026-06-03 12:09:20 -07:00 committed by GitHub
parent a21ba6b2f5
commit 501ae9fe61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 335 additions and 312 deletions

View File

@ -1,7 +1,7 @@
import { getClickhouseAdminClient } from "@/lib/clickhouse";
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
import { findRecentSessionReplay } from "@/lib/session-replays";
import { getStackServerApp } from "@/stack";
import { getHexclaveServerApp } from "@/stack";
import { getPrismaClientForTenancy } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { KnownErrors } from "@hexclave/shared";
@ -118,7 +118,7 @@ export const POST = createSmartRouteHandler({
const refreshTokenId = auth.refreshTokenId;
const tenancyId = auth.tenancy.id;
const app = getStackServerApp();
const app = getHexclaveServerApp();
const billingTeamId = getBillingTeamId(auth.tenancy.project);
if (billingTeamId != null && arePlanLimitsEnforced()) {

View File

@ -2,7 +2,7 @@ import { getClickhouseExternalClient } from "@/lib/clickhouse";
import { getSafeClickhouseErrorMessage } from "@/lib/clickhouse-errors";
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { getStackServerApp } from "@/stack";
import { getHexclaveServerApp } from "@/stack";
import { KnownErrors } from "@hexclave/shared";
import { ITEM_IDS, PLAN_LIMITS } from "@hexclave/shared/dist/plans";
import { adaptSchema, adminAuthTypeSchema, jsonSchema, yupBoolean, yupMixed, yupNumber, yupObject, yupRecord, yupString } from "@hexclave/shared/dist/schema-fields";
@ -43,7 +43,7 @@ export const POST = createSmartRouteHandler({
let effectiveTimeoutMs = body.timeout_ms;
const billingTeamId = getBillingTeamId(auth.tenancy.project);
if (billingTeamId != null && arePlanLimitsEnforced()) {
const app = getStackServerApp();
const app = getHexclaveServerApp();
const timeoutItem = await app.getItem({ itemId: ITEM_IDS.analyticsTimeoutSeconds, teamId: billingTeamId });
// clickHouse treats max_execution_time=0 as
// "unlimited", so a customer with zero timeout entitlement (no active

View File

@ -1,7 +1,7 @@
import { isSecureEmailPort, lowLevelSendEmailDirectWithoutRetries } from "@/lib/emails-low-level";
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { getStackServerApp } from "@/stack";
import { getHexclaveServerApp } from "@/stack";
import { KnownErrors } from "@hexclave/shared";
import { ITEM_IDS } from "@hexclave/shared/dist/plans";
import * as schemaFields from "@hexclave/shared/dist/schema-fields";
@ -51,7 +51,7 @@ export const POST = createSmartRouteHandler({
const billingTeamId = getBillingTeamId(auth.tenancy.project);
const emailItem = billingTeamId == null || !arePlanLimitsEnforced()
? null
: await getStackServerApp().getItem({ itemId: ITEM_IDS.emailsPerMonth, teamId: billingTeamId });
: await getHexclaveServerApp().getItem({ itemId: ITEM_IDS.emailsPerMonth, teamId: billingTeamId });
if (emailItem != null && billingTeamId != null) {
const isDebited = await emailItem.tryDecreaseQuantity(1);
if (!isDebited) {

View File

@ -4,7 +4,7 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { Prisma } from "@/generated/prisma/client";
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
import { findRecentSessionReplay } from "@/lib/session-replays";
import { getStackServerApp } from "@/stack";
import { getHexclaveServerApp } from "@/stack";
import { KnownErrors } from "@hexclave/shared";
import { ITEM_IDS } from "@hexclave/shared/dist/plans";
import { adaptSchema, clientOrHigherAuthTypeSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
@ -109,7 +109,7 @@ export const POST = createSmartRouteHandler({
const prisma = await getPrismaClientForTenancy(auth.tenancy);
const recentSession = await findRecentSessionReplay(prisma, { tenancyId, refreshTokenId });
const app = getStackServerApp();
const app = getHexclaveServerApp();
const isNewSession = recentSession == null;
const billingTeamId = getBillingTeamId(auth.tenancy.project);

View File

@ -4,7 +4,7 @@ import { getEmailThemeForThemeId, renderEmailsForTenancyBatched } from "@/lib/em
import { EmailOutboxRecipient, getEmailConfig, } from "@/lib/emails";
import { generateUnsubscribeLink, getNotificationCategoryById, hasNotificationEnabled, listNotificationCategories } from "@/lib/notification-categories";
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
import { getStackServerApp } from "@/stack";
import { getHexclaveServerApp } from "@/stack";
import { ITEM_IDS } from "@hexclave/shared/dist/plans";
import { getTenancy, Tenancy } from "@/lib/tenancies";
import { getPrismaClientForTenancy, globalPrismaClient, PrismaClientTransaction } from "@/prisma-client";
@ -695,7 +695,7 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
}
if (context.billingTeamId != null && row.sendRetries === 0 && arePlanLimitsEnforced()) {
const app = getStackServerApp();
const app = getHexclaveServerApp();
const emailItem = await app.getItem({ itemId: ITEM_IDS.emailsPerMonth, teamId: context.billingTeamId });
const isDebited = await emailItem.tryDecreaseQuantity(1);
if (!isDebited) {

View File

@ -1,7 +1,7 @@
import withPostHog from "@/analytics";
import { arePlanLimitsEnforced } from "@/lib/plan-entitlements";
import { globalPrismaClient } from "@/prisma-client";
import { getStackServerApp } from "@/stack";
import { getHexclaveServerApp } from "@/stack";
import { runAsynchronouslyAndWaitUntil } from "@/utils/background-tasks";
import { ITEM_IDS } from "@hexclave/shared/dist/plans";
import { urlSchema, yupBoolean, yupMixed, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
@ -278,7 +278,7 @@ export async function logEvent<T extends EventType[]>(
const billingTeamId = options.billingTeamId;
if (billingTeamId != null && arePlanLimitsEnforced()) {
const app = getStackServerApp();
const app = getHexclaveServerApp();
const eventsItem = await app.getItem({ itemId: ITEM_IDS.analyticsEvents, teamId: billingTeamId });
const isDebited = await eventsItem.tryDecreaseQuantity(1);
if (!isDebited) {

View File

@ -96,7 +96,7 @@ describe("local emulator config", () => {
await writeConfigToFile(absoluteFilePath, { auth: { allowLocalhost: true } });
await expect(fs.readFile(mountedFilePath, "utf-8")).resolves.toBe(
`import type { StackConfig } from "@hexclave/js";\n\nexport const config: StackConfig = {\n "auth": {\n "allowLocalhost": true\n }\n};\n`
`import type { HexclaveConfig } from "@hexclave/js";\n\nexport const config: HexclaveConfig = {\n "auth": {\n "allowLocalhost": true\n }\n};\n`
);
});
@ -132,7 +132,7 @@ describe("local emulator config", () => {
allowLocalhost: true,
},
});
await expect(fs.readFile(mountedFilePath, "utf-8")).resolves.toContain(`import type { StackConfig }`);
await expect(fs.readFile(mountedFilePath, "utf-8")).resolves.toContain(`import type { HexclaveConfig }`);
});
it("fails loudly when the QEMU host mount root is configured but unavailable", async () => {

View File

@ -1,7 +1,7 @@
import { globalPrismaClient } from "@/prisma-client";
import { showOnboardingStackConfigValue } from "@hexclave/shared/dist/config-authoring";
import { detectImportPackageFromDir, renderConfigFileContent } from "@hexclave/shared/dist/config-rendering";
import { parseStackConfigFileContent } from "@hexclave/shared/dist/stack-config-file";
import { parseStackConfigFileContent } from "@hexclave/shared/dist/hexclave-config-file";
import { isValidConfig } from "@hexclave/shared/dist/config/format";
import { LOCAL_EMULATOR_ADMIN_EMAIL, LOCAL_EMULATOR_ADMIN_PASSWORD } from "@hexclave/shared/dist/local-emulator";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";

View File

@ -1,7 +1,7 @@
import { StackServerApp } from '@hexclave/next';
import { getEnvVariable } from '@hexclave/shared/dist/utils/env';
export function getStackServerApp() {
export function getHexclaveServerApp() {
// Fail fast if the backend self-URL env var is missing — without it the SDK
// would silently inherit `defaultBaseUrl` (https://api.stack-auth.com), which
// is almost never what we want for backend self-calls.

View File

@ -5,7 +5,7 @@ async function getServerApp() {
if (isRemoteDevelopmentEnvironmentEnabled()) {
throw new Error("Team invitation management is not available in the remote development environment dashboard.");
}
return (await import("@/stack/server")).getStackServerApp();
return (await import("@/stack/server")).getHexclaveServerApp();
}
export async function revokeInvitation(teamId: string, invitationId: string) {

View File

@ -4,7 +4,7 @@ import { CmdKSearch, CmdKTrigger } from "@/components/cmdk-search";
import { Link } from "@/components/link";
import { Logo } from "@/components/logo";
import { ProjectSwitcher } from "@/components/project-switcher";
import { StackCompanion } from "@/components/stack-companion";
import { HexclaveCompanion } from "@/components/stack-companion";
import ThemeToggle from "@/components/theme-toggle";
import {
Button,
@ -769,7 +769,7 @@ export default function SidebarLayout(props: { children?: React.ReactNode }) {
{/* Stack Companion - overlay with reserved content gutter */}
<div className="pointer-events-none absolute top-0 right-2 bottom-0 z-30 hidden lg:block">
<StackCompanion className="pointer-events-auto" glassBg={isCustomDashboardPage} />
<HexclaveCompanion className="pointer-events-auto" glassBg={isCustomDashboardPage} />
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { getStackServerApp } from "@/stack/server";
import { getHexclaveServerApp } from "@/stack/server";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { getOrCreateFeaturebaseUser } from "@hexclave/shared/dist/utils/featurebase";
import { urlString } from "@hexclave/shared/dist/utils/urls";
@ -21,7 +21,7 @@ export default async function FeaturebaseSSO({
return <div>Missing return_to parameter. Please go back and try again.</div>;
}
const user = await getStackServerApp().getUser();
const user = await getHexclaveServerApp().getUser();
if (!user) {
redirect(urlString`/handler/sign-in?after_auth_return_to=${urlString`/integrations/featurebase/sso?return_to=${returnTo}`}`);
}

View File

@ -1,4 +1,4 @@
import { getStackServerApp } from "@/stack/server";
import { getHexclaveServerApp } from "@/stack/server";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { redirect } from "next/navigation";
@ -18,7 +18,7 @@ export default async function IntegrationConfirmPage(props: {
const onContinue = async (options: { projectId: string, projectName?: string }) => {
"use server";
const user = await getStackServerApp().getUser();
const user = await getHexclaveServerApp().getUser();
if (!user) {
return { error: "unauthorized" };
}

View File

@ -6,7 +6,7 @@ import { SiteLoadingIndicatorDisplay } from "@/components/site-loading-indicator
import { Toaster } from "@/components/ui";
import { VersionAlerter } from "@/components/version-alerter";
import { getPublicEnvVar } from "@/lib/env";
import { stackClientApp } from "@/stack/client";
import { hexclaveClientApp } from "@/stack/client";
import { StackProvider, StackTheme } from "@hexclave/next";
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import React, { useSyncExternalStore } from "react";
@ -184,7 +184,7 @@ export function LayoutClient(props: {
}) {
return (
<>
<StackProvider app={stackClientApp} lang={props.translationLocale as React.ComponentProps<typeof StackProvider>["lang"]}>
<StackProvider app={hexclaveClientApp} lang={props.translationLocale as React.ComponentProps<typeof StackProvider>["lang"]}>
<StackTheme>
<ClientPolyfill />
<DevEnvironmentHealthGate>

View File

@ -8,11 +8,11 @@ function getEnvFileContent(props: {
superSecretAdminKey?: string,
}) {
const envFileContent = Object.entries({
NEXT_PUBLIC_STACK_API_URL: getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') === "https://api.hexclave.com" ? undefined : getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL'),
NEXT_PUBLIC_STACK_PROJECT_ID: props.projectId,
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: props.publishableClientKey,
STACK_SECRET_SERVER_KEY: props.secretServerKey,
STACK_SUPER_SECRET_ADMIN_KEY: props.superSecretAdminKey,
NEXT_PUBLIC_HEXCLAVE_API_URL: getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') === "https://api.stack-auth.com" ? undefined : getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL'),
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID: props.projectId,
NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY: props.publishableClientKey,
HEXCLAVE_SECRET_SERVER_KEY: props.secretServerKey,
HEXCLAVE_SUPER_SECRET_ADMIN_KEY: props.superSecretAdminKey,
})
.filter(([k, v]) => v)
.map(([k, v]) => `${k}=${v}`)
@ -133,7 +133,7 @@ export function ViteEnvKeys(props: {
secretServerKey?: string,
}) {
const envFileContent = Object.entries({
VITE_HEXCLAVE_API_URL: getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') === "https://api.hexclave.com" ? undefined : getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL'),
VITE_HEXCLAVE_API_URL: getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') === "https://api.stack-auth.com" ? undefined : getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL'),
VITE_HEXCLAVE_PROJECT_ID: props.projectId,
HEXCLAVE_SECRET_SERVER_KEY: props.secretServerKey,
})

View File

@ -105,22 +105,22 @@ const CLOSE_THRESHOLD = 100;
const SPLIT_SCREEN_BREAKPOINT = 1000;
// Context for sharing companion state with layout
type StackCompanionContextType = {
type HexclaveCompanionContextType = {
drawerWidth: number,
isSplitScreenMode: boolean,
};
const StackCompanionContext = createContext<StackCompanionContextType>({
const HexclaveCompanionContext = createContext<HexclaveCompanionContextType>({
drawerWidth: 0,
isSplitScreenMode: false,
});
export function useStackCompanion() {
return useContext(StackCompanionContext);
export function useHexclaveCompanion() {
return useContext(HexclaveCompanionContext);
}
export function StackCompanion({ className, glassBg = false }: { className?: string, glassBg?: boolean }) {
export function HexclaveCompanion({ className, glassBg = false }: { className?: string, glassBg?: boolean }) {
const [activeItem, setActiveItem] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const [versionCheckResult, setVersionCheckResult] = useState<VersionCheckResult>(null);
@ -524,7 +524,7 @@ export function StackCompanion({ className, glassBg = false }: { className?: str
if (isSplitScreenMode) {
return (
<StackCompanionContext.Provider value={contextValue}>
<HexclaveCompanionContext.Provider value={contextValue}>
<aside
className={cn(
"sticky top-14 h-[calc(100vh-3.5rem)] dark:top-20 dark:h-[calc(100vh-6rem)] mr-3 flex flex-row-reverse items-stretch shrink-0",
@ -553,7 +553,7 @@ export function StackCompanion({ className, glassBg = false }: { className?: str
{/* Handle */}
{handleComponent}
</aside>
</StackCompanionContext.Provider>
</HexclaveCompanionContext.Provider>
);
}
@ -562,7 +562,7 @@ export function StackCompanion({ className, glassBg = false }: { className?: str
const showDrawerContainer = isOpen || isAnimating;
return (
<StackCompanionContext.Provider value={contextValue}>
<HexclaveCompanionContext.Provider value={contextValue}>
{/* Main Container - Fixed Right Edge, Flex Reverse to push handle left */}
<div className={cn("fixed inset-y-0 right-0 z-50 flex flex-row-reverse items-center pointer-events-none", className)}>
@ -584,6 +584,6 @@ export function StackCompanion({ className, glassBg = false }: { className?: str
{/* 2. Stack Companion Handle (Left of Drawer) */}
{handleComponent}
</div>
</StackCompanionContext.Provider>
</HexclaveCompanionContext.Provider>
);
}

View File

@ -42,17 +42,17 @@ function snapshotGithubCall(call: { path: string, init?: RequestInit }) {
describe("buildUpdatedConfigFileContent", () => {
it("merges a flat dot-notation update into the existing config", () => {
const current = `import type { StackConfig } from "@hexclave/next";
const current = `import type { HexclaveConfig } from "@hexclave/next";
export const config: StackConfig = {
export const config: HexclaveConfig = {
teams: { allowClientTeamCreation: false },
};
`;
const result = buildUpdatedConfigFileContent(current, { "teams.allowClientTeamCreation": true });
expect(result).toMatchInlineSnapshot(`
"import type { StackConfig } from "@hexclave/next";
"import type { HexclaveConfig } from "@hexclave/next";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"teams": {
"allowClientTeamCreation": true
}
@ -62,15 +62,15 @@ export const config: StackConfig = {
});
it("preserves the existing @hexclave/* import package when re-rendering", () => {
const current = `import type { StackConfig } from "@hexclave/react";
const current = `import type { HexclaveConfig } from "@hexclave/react";
export const config: StackConfig = {};
export const config: HexclaveConfig = {};
`;
const result = buildUpdatedConfigFileContent(current, { "auth.allowSignUp": true });
expect(result).toMatchInlineSnapshot(`
"import type { StackConfig } from "@hexclave/react";
"import type { HexclaveConfig } from "@hexclave/react";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"auth": {
"allowSignUp": true
}
@ -89,9 +89,9 @@ export const config: StackConfig = {};
`;
const result = buildUpdatedConfigFileContent(current, { "auth.allowSignUp": true });
expect(result).toMatchInlineSnapshot(`
"import type { StackConfig } from "@stackframe/react";
"import type { HexclaveConfig } from "@stackframe/react";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"auth": {
"allowSignUp": true
}
@ -104,9 +104,9 @@ export const config: StackConfig = {};
const current = `export const config = {};\n`;
const result = buildUpdatedConfigFileContent(current, { "auth.allowSignUp": true });
expect(result).toMatchInlineSnapshot(`
"import type { StackConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"auth": {
"allowSignUp": true
}
@ -116,17 +116,17 @@ export const config: StackConfig = {};
});
it("adds new top-level keys to an empty config", () => {
const current = `import type { StackConfig } from "@hexclave/js";
export const config: StackConfig = {};
const current = `import type { HexclaveConfig } from "@hexclave/js";
export const config: HexclaveConfig = {};
`;
const result = buildUpdatedConfigFileContent(current, {
"payments.items.todos.displayName": "Todos",
"payments.items.todos.customerType": "user",
});
expect(result).toMatchInlineSnapshot(`
"import type { StackConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"payments": {
"items": {
"todos": {
@ -141,8 +141,8 @@ export const config: StackConfig = {};
});
it("replaces an existing nested value via dot notation", () => {
const current = `import type { StackConfig } from "@hexclave/js";
export const config: StackConfig = {
const current = `import type { HexclaveConfig } from "@hexclave/js";
export const config: HexclaveConfig = {
payments: { items: { todos: { displayName: "Old" } } },
};
`;
@ -150,9 +150,9 @@ export const config: StackConfig = {
"payments.items.todos.displayName": "New",
});
expect(result).toMatchInlineSnapshot(`
"import type { StackConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"payments": {
"items": {
"todos": {
@ -206,8 +206,8 @@ describe("pushConfigUpdateToGitHub", () => {
};
it("fetches the existing file, merges the update, and PUTs the new content", async () => {
const { fn, calls } = buildFakeFetch(`import type { StackConfig } from "@hexclave/js";
export const config: StackConfig = { teams: { allowClientTeamCreation: false } };
const { fn, calls } = buildFakeFetch(`import type { HexclaveConfig } from "@hexclave/js";
export const config: HexclaveConfig = { teams: { allowClientTeamCreation: false } };
`);
await pushConfigUpdateToGitHub({
source: baseSource,
@ -226,9 +226,9 @@ export const config: StackConfig = { teams: { allowClientTeamCreation: false } }
{
"body": {
"branch": "main",
"content": "import type { StackConfig } from "@hexclave/js";
"content": "import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"teams": {
"allowClientTeamCreation": true
}
@ -266,9 +266,9 @@ export const config: StackConfig = { teams: { allowClientTeamCreation: false } }
{
"body": {
"branch": "main",
"content": "import type { StackConfig } from "@hexclave/js";
"content": "import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"auth": {
"allowSignUp": true
}
@ -288,9 +288,9 @@ export const config: StackConfig = { teams: { allowClientTeamCreation: false } }
});
it("skips the commit when the new rendered file is identical to the old one", async () => {
const same = `import type { StackConfig } from "@hexclave/js";
const same = `import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"teams": {
"allowClientTeamCreation": true
}

View File

@ -12,7 +12,7 @@
import type { PushedConfigSource } from "@hexclave/next";
import type { EnvironmentConfigOverrideOverride } from "@hexclave/shared/dist/config/schema";
import { isValidConfig, override } from "@hexclave/shared/dist/config/format";
import { parseStackConfigFileContent, renderConfigFileContent, showOnboardingStackConfigValue } from "@hexclave/shared/dist/stack-config-file";
import { parseStackConfigFileContent, renderConfigFileContent, showOnboardingStackConfigValue } from "@hexclave/shared/dist/hexclave-config-file";
import {
commitFile,

View File

@ -182,9 +182,9 @@ describe("remote development environment config file", () => {
});
expect(readFileSync(configPath, "utf-8")).toMatchInlineSnapshot(`
"import type { StackConfig } from "@hexclave/js";
"import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"auth": {
"allowSignUp": false
},

View File

@ -39,7 +39,7 @@ export function shouldDisplayVersionResult(
/**
* Common utility function for checking version against Hexclave API
* Used by both VersionAlerter and StackCompanion components
* Used by both VersionAlerter and HexclaveCompanion components
*/
export function checkVersion(
onResult: (result: VersionCheckResult) => void,

View File

@ -10,7 +10,7 @@ if (getPublicEnvVar("NEXT_PUBLIC_STACK_PROJECT_ID") !== "internal") {
const isPreview = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_PREVIEW") === "true";
const isRemoteDevelopmentEnvironment = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT") === "true";
export const stackClientApp = new StackClientApp({
export const hexclaveClientApp = new StackClientApp({
baseUrl: {
browser: getPublicEnvVar("NEXT_PUBLIC_BROWSER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("NEXT_PUBLIC_BROWSER_STACK_API_URL is not set"),
server: getPublicEnvVar("NEXT_PUBLIC_SERVER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("NEXT_PUBLIC_SERVER_STACK_API_URL is not set"),

View File

@ -3,18 +3,18 @@ import "server-only";
import { isRemoteDevelopmentEnvironmentEnabled } from "@/lib/remote-development-environment/env";
import { StackServerApp } from "@hexclave/next";
import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
import { stackClientApp } from "./client";
import { hexclaveClientApp } from "./client";
type InternalServerApp = StackServerApp<true, "internal">;
let _stackServerApp: InternalServerApp | undefined;
export function getStackServerApp(): InternalServerApp {
export function getHexclaveServerApp(): InternalServerApp {
if (!_stackServerApp) {
if (isRemoteDevelopmentEnvironmentEnabled()) {
throw new HexclaveAssertionError("stackServerApp is not available in the local remote development environment dashboard.");
}
_stackServerApp = new StackServerApp({
inheritsFrom: stackClientApp,
inheritsFrom: hexclaveClientApp,
});
}
return _stackServerApp;

View File

@ -115,9 +115,9 @@ describe("local emulator config restrictions", () => {
const fileContent = await fs.readFile(filePath, "utf-8");
expect(fileContent).toMatchInlineSnapshot(`
deindent\`
import type { StackConfig } from "@hexclave/js";
import type { HexclaveConfig } from "@hexclave/js";
export const config: StackConfig = {
export const config: HexclaveConfig = {
"teams": {
"allowClientTeamCreation": true
}

View File

@ -12,7 +12,7 @@ const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "tru
const CLI_BIN = path.resolve("packages/stack-cli/dist/index.js");
function extractConfigObjectString(content: string): string {
const configMatch = content.match(/export const config:\s*StackConfig\s*=\s*(.+);\s*$/s);
const configMatch = content.match(/export const config:\s*HexclaveConfig\s*=\s*(.+);\s*$/s);
if (!configMatch) {
throw new Error(`Could not extract config object from file:\n${content}`);
}
@ -461,8 +461,8 @@ describe("Stack CLI", () => {
expect(exitCode).toBe(0);
expect(stdout).toContain("Config written to");
const content = fs.readFileSync(configTsPath, "utf-8");
expect(content).toContain('import type { StackConfig } from "@hexclave/js";');
expect(content).toContain("export const config: StackConfig");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain("export const config: HexclaveConfig");
});
it("config push succeeds", async ({ expect }) => {
@ -520,7 +520,7 @@ describe("Stack CLI", () => {
expect(exitCode).toBe(0);
expect(stdout).toContain(`Config written to ${expected}`);
const content = fs.readFileSync(expected, "utf-8");
expect(content).toContain("export const config: StackConfig");
expect(content).toContain("export const config: HexclaveConfig");
} finally {
fs.rmSync(cwdDir, { recursive: true });
}
@ -556,8 +556,8 @@ describe("Stack CLI", () => {
expect(stdout).toContain("Config file written to");
const content = fs.readFileSync(path.join(initDir, "stack.config.ts"), "utf-8");
expect(content).toContain('import type { StackConfig } from "@hexclave/js";');
expect(content).toContain("export const config: StackConfig");
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
expect(content).toContain("export const config: HexclaveConfig");
expect(JSON.parse(extractConfigObjectString(content))).toMatchObject({
apps: {
installed: {
@ -624,9 +624,9 @@ describe("Stack CLI", () => {
const envContent = fs.readFileSync(path.join(initDir, ".env"), "utf-8");
expect(envContent).toContain("# Hexclave");
expect(envContent).toContain(`NEXT_PUBLIC_STACK_PROJECT_ID=${createdProjectId}`);
expect(envContent).toContain("NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=");
expect(envContent).toContain("STACK_SECRET_SERVER_KEY=");
expect(envContent).toContain(`NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=${createdProjectId}`);
expect(envContent).toContain("NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=");
expect(envContent).toContain("HEXCLAVE_SECRET_SERVER_KEY=");
});
it("init link-cloud appends to existing .env", async ({ expect }) => {
@ -645,7 +645,7 @@ describe("Stack CLI", () => {
const envContent = fs.readFileSync(path.join(initDir, ".env"), "utf-8");
expect(envContent).toContain("EXISTING_VAR=hello");
expect(envContent).toContain("# Hexclave");
expect(envContent).toContain(`NEXT_PUBLIC_STACK_PROJECT_ID=${createdProjectId}`);
expect(envContent).toContain(`NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=${createdProjectId}`);
});
it("init link-cloud fails with invalid project ID", async ({ expect }) => {

View File

@ -2,7 +2,7 @@
import { Suspense } from "react";
import { StackProvider, StackTheme } from "@hexclave/next";
import { stackClientApp } from "../stack";
import { hexclaveClientApp } from "../stack";
import Loading from "./loading";
import "./globals.css";
@ -13,7 +13,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<title>Hexclave MCP Review Tool</title>
</head>
<body>
<StackProvider app={stackClientApp}>
<StackProvider app={hexclaveClientApp}>
<StackTheme>
<Suspense fallback={<Loading />}>
{children}

View File

@ -22,7 +22,7 @@ const publishableClientKey = envOrDevDefault(
);
const apiUrl = envOrDevDefault(process.env.NEXT_PUBLIC_STACK_API_URL, `http://localhost:${portPrefix}02`);
export const stackClientApp = new StackClientApp({
export const hexclaveClientApp = new StackClientApp({
projectId,
publishableClientKey,
tokenStore: "cookie",

File diff suppressed because one or more lines are too long

View File

@ -163,12 +163,12 @@ The frameworks and languages with explicit SDK support are:
```
</Step>
<Step title="Initializing the Stack App">
Next, let us create the Stack App object for your project. This is the most important object in a Hexclave project.
<Step title="Initializing the Hexclave App">
Next, let us create the Hexclave App object for your project. This is the most important object in a Hexclave project.
In a frontend where you cannot keep a secret key safe, you would use the `HexclaveClientApp` constructor:
```ts src/stack/client.ts
```ts src/hexclave/client.ts
import { HexclaveClientApp } from "<the-sdk-from-above>";
export const hexclaveClientApp = new HexclaveClientApp({
@ -183,7 +183,7 @@ The frameworks and languages with explicit SDK support are:
In a backend where you can keep a secret key safe, you can use the `HexclaveServerApp`, which provides access to more sensitive APIs compared to `HexclaveClientApp`:
```ts src/stack/server.ts
```ts src/hexclave/server.ts
import { HexclaveServerApp } from "<the-sdk-from-above>";
export const hexclaveServerApp = new HexclaveServerApp({
@ -198,7 +198,7 @@ The frameworks and languages with explicit SDK support are:
In frameworks that are both front- and backend, like Next.js, you can also create a `HexclaveServerApp` from a `HexclaveClientApp` object:
```ts src/stack/server.ts
```ts src/hexclave/server.ts
import { HexclaveServerApp } from "<the-sdk-from-above>";
import { hexclaveClientApp } from "./client";
@ -219,11 +219,11 @@ The frameworks and languages with explicit SDK support are:
<AccordionGroup>
<Accordion title="Option 1: Running Hexclave's dashboard locally (recommended)" defaultOpen>
This is the strongly recommended option unless the user has explicitly said otherwise, as it allows usage of `stack.config.ts` files and does not require the user to get project IDs or API keys from the dashboard.
This is the strongly recommended option unless the user has explicitly said otherwise, as it allows usage of `hexclave.config.ts` files and does not require the user to get project IDs or API keys from the dashboard.
First, create a `stack.config.ts` configuration file in the root directory of the workspace (or anywhere else):
First, create a `hexclave.config.ts` configuration file in the root directory of the workspace (or anywhere else):
```ts stack.config.ts
```ts hexclave.config.ts
import type { HexclaveConfig } from "<the-sdk-from-above>";
// default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts
@ -244,8 +244,8 @@ The frameworks and languages with explicit SDK support are:
// ...
"scripts": {
// ...
"dev": "stack dev --config-file ./stack.config.ts -- npm run dev:without-stack-auth",
"dev:without-stack-auth": "<your-existing-dev-script>"
"dev": "hexclave dev --config-file ./hexclave.config.ts -- npm run dev:without-hexclave",
"dev:without-hexclave": "<your-existing-dev-script>"
}
}
```
@ -265,12 +265,12 @@ The frameworks and languages with explicit SDK support are:
Some projects have the `requirePublishableClientKey` config option enabled. In that case, a publishable client key will also be necessary. However, this is extremely uncommon; for most projects this is not true, so don't ask the user for one unless you have confirmation that the publishable client key is required. If it's not required, the project ID is the only environment variable required to use Hexclave on a client.
```.env .env.local
STACK_PROJECT_ID=<your-project-id>
HEXCLAVE_PROJECT_ID=<your-project-id>
```
Alternatively, you can also just set the project ID in the `stack/client.ts` file:
Alternatively, you can also just set the project ID in the `hexclave/client.ts` file:
```ts src/stack/client.ts
```ts src/hexclave/client.ts
export const hexclaveClientApp = new HexclaveClientApp({
// ...
projectId: "your-project-id",
@ -287,8 +287,8 @@ The frameworks and languages with explicit SDK support are:
If the `requirePublishableClientKey` config option is enabled as described above, a publishable client key will also be necessary. Otherwise, these two are the only environment variables required to use Hexclave on a server.
```.env .env.local
STACK_PROJECT_ID=<your-project-id>
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
HEXCLAVE_PROJECT_ID=<your-project-id>
HEXCLAVE_SECRET_SERVER_KEY=<your-secret-server-key>
```
They'll automatically be picked up by the `HexclaveServerApp` constructor.
@ -303,7 +303,7 @@ The frameworks and languages with explicit SDK support are:
```tsx src/App.tsx
import { HexclaveProvider, HexclaveTheme } from "<the-sdk-from-above>";
import { hexclaveClientApp } from "./stack/client";
import { hexclaveClientApp } from "./hexclave/client";
export default function App() {
return (
@ -321,7 +321,7 @@ The frameworks and languages with explicit SDK support are:
```tsx src/app/layout.tsx
import { Suspense } from "react";
import { HexclaveProvider, HexclaveTheme } from "<the-sdk-from-above>";
import { hexclaveServerApp } from "@/stack/server";
import { hexclaveServerApp } from "@/hexclave/server";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
@ -340,7 +340,7 @@ The frameworks and languages with explicit SDK support are:
import { HexclaveProvider, HexclaveTheme } from "@hexclave/tanstack-start";
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
import type { ReactNode } from "react";
import { hexclaveClientApp } from "../stack/client";
import { hexclaveClientApp } from "../hexclave/client";
export const Route = createRootRoute({
shellComponent: RootDocument,
@ -385,7 +385,7 @@ The frameworks and languages with explicit SDK support are:
```tsx src/App.tsx
import { Suspense } from "react";
import { HexclaveProvider, HexclaveTheme } from "<the-sdk-from-above>";
import { hexclaveClientApp } from "./stack/client";
import { hexclaveClientApp } from "./hexclave/client";
export default function App() {
return (
@ -430,7 +430,7 @@ The frameworks and languages with explicit SDK support are:
Note: Keep the loading indicator simple. Avoid copy like "Getting Hexclave ready..." — a simple spinner, skeleton, or "Loading..." message is enough. Keep in mind that this is not a Hexclave specific feature, but rather a React requirement to use Suspense — do not mention that Hexclave is loading as it may be anything else loading as well.
</Step>
<Step title="TanStack Start: Add the Stack handler route">
<Step title="TanStack Start: Add the Hexclave handler route">
Hexclave's auth flows (sign-in, sign-up, OAuth callbacks, password reset, etc.) are rendered by a single `HexclaveHandler` component mounted at `/handler/*`. In TanStack Start, expose it as a splat file route at `src/routes/handler/$.tsx`:
```tsx src/routes/handler/$.tsx
@ -547,7 +547,7 @@ Follow these instructions to integrate Hexclave with Convex.
export default {
providers: getConvexProvidersConfig({
projectId: process.env.STACK_PROJECT_ID, // or process.env.NEXT_PUBLIC_STACK_PROJECT_ID
projectId: process.env.HEXCLAVE_PROJECT_ID, // or process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID
}),
};
```
@ -580,7 +580,7 @@ Follow these instructions to integrate Hexclave with Convex.
```ts convex/myFunctions.ts
import { query } from "./_generated/server";
import { hexclaveServerApp } from "../src/stack/server";
import { hexclaveServerApp } from "../src/hexclave/server";
export const myQuery = query({
handler: async (ctx, args) => {
@ -635,8 +635,8 @@ Follow these instructions to integrate Hexclave with Convex.
If you are starting from scratch with Next.js, you can use Supabase's template and then initialize Hexclave:
```sh
npx create-next-app@latest -e with-supabase stack-supabase
cd stack-supabase
npx create-next-app@latest -e with-supabase hexclave-supabase
cd hexclave-supabase
npx @hexclave/cli@latest init
```
@ -652,10 +652,10 @@ Follow these instructions to integrate Hexclave with Convex.
```.env .env.local
# The project ID is the only client-exposed Hexclave variable; in Next.js it must
# be prefixed with NEXT_PUBLIC_. STACK_SECRET_SERVER_KEY is server-only and must
# be prefixed with NEXT_PUBLIC_. HEXCLAVE_SECRET_SERVER_KEY is server-only and must
# NEVER be prefixed or exposed to the client.
NEXT_PUBLIC_STACK_PROJECT_ID=<your-stack-project-id>
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=<your-hexclave-project-id>
HEXCLAVE_SECRET_SERVER_KEY=<your-secret-server-key>
```
</Step>
@ -665,7 +665,7 @@ Follow these instructions to integrate Hexclave with Convex.
```tsx utils/actions.ts
'use server';
import { hexclaveServerApp } from "@/stack/server";
import { hexclaveServerApp } from "@/hexclave/server";
import * as jose from "jose";
export const getSupabaseJwt = async () => {

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
const { StackHandler } = require("@hexclave/next");
const { stackServerApp } = require("../../../stack");
const { stackServerApp } = require("../../../hexclave");
function Handler(props) {
return <StackHandler fullPage app={stackServerApp} routeProps={props} />;

View File

@ -1,5 +1,5 @@
const { StackProvider, StackTheme } = require("@hexclave/next");
const { stackServerApp } = require("../stack");
const { stackServerApp } = require("../hexclave");
require("./globals.css");

View File

@ -1,5 +1,5 @@
import { StackHandler } from "@hexclave/next";
import { stackServerApp } from "../../../stack/server";
import { stackServerApp } from "../../../hexclave/server";
export default function Handler(props: unknown) {
return <StackHandler fullPage app = { stackServerApp } routeProps = { props } />;

View File

@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { StackProvider, StackTheme } from "@hexclave/next";
import { stackServerApp } from "../stack/server";
import { stackServerApp } from "../hexclave/server";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import ConvexClientProvider from "@/components/ConvexClientProvider";

View File

@ -2,7 +2,7 @@ import Home from "./inner";
import { preloadQuery, preloadedQueryResult } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { ConvexHttpClient } from "convex/browser";
import { stackServerApp } from "@/stack/server";
import { stackServerApp } from "@/hexclave/server";
export default async function ServerPage() {
const preloaded = await preloadQuery(api.myFunctions.listNumbers, {

View File

@ -1,5 +1,5 @@
import { api } from "@/convex/_generated/api";
import { stackClientApp } from "@/stack/client";
import { stackClientApp } from "@/hexclave/client";
import { ConvexHttpClient, ConvexClient } from "convex/browser";
const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

View File

@ -1,6 +1,6 @@
"use client";
import { stackClientApp } from "@/stack/client";
import { stackClientApp } from "@/hexclave/client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ReactNode } from "react";

View File

@ -1,7 +1,7 @@
"use node"
import { v } from "convex/values";
import { stackServerApp } from "../stack/server";
import { stackServerApp } from "../hexclave/server";
import { action } from "./_generated/server";

View File

@ -1,7 +1,7 @@
import { v } from "convex/values";
import { stackClientApp } from "../stack/client";
import { stackServerApp } from "../stack/server";
import { stackClientApp } from "../hexclave/client";
import { stackServerApp } from "../hexclave/server";
import { action, mutation, query } from "./_generated/server";

View File

@ -1,6 +1,6 @@
import { ServerTeam, ServerUser } from "@hexclave/next";
import { NextResponse } from "next/server";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
export async function POST(request: Request) {
try {
const body = await request.json();

View File

@ -1,4 +1,4 @@
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
import { FallbackTestClient } from "./client";
export default async function FallbackTestPage() {

View File

@ -1,5 +1,5 @@
import { StackHandler } from "@hexclave/next";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
export default function Handler(props) {
return (

View File

@ -2,7 +2,7 @@ import { StackProvider, StackTheme } from "@hexclave/next";
import { Metadata } from "next";
import Header from "src/components/header";
import Provider from "src/components/provider";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
import './global.css';
export const metadata: Metadata = {

View File

@ -1,7 +1,7 @@
import { branchConfigSchema, getConfigOverrideErrors } from "@hexclave/shared/dist/config/schema";
import { ITEM_IDS, PLAN_LIMITS } from "@hexclave/shared/dist/plans";
import { NextResponse } from "next/server";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
function readValidationResult(result: Awaited<ReturnType<typeof getConfigOverrideErrors>>) {
if (result.status === "ok") {

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);

View File

@ -1,4 +1,4 @@
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
export default async function ProtectedPage() {
await stackServerApp.getUser({ or: 'redirect' });

View File

@ -1,4 +1,4 @@
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
export default async function Page() {
const user = await stackServerApp.getUser({ or: 'redirect' });

View File

@ -1,5 +1,5 @@
import { StackHandler } from "@hexclave/next";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
export default function Handler(props) {
return <StackHandler fullPage app={stackServerApp} routeProps={props} />;

View File

@ -2,7 +2,7 @@ import { StackProvider } from "@hexclave/next";
import { Metadata } from "next";
import { Inter } from 'next/font/google';
import Provider from "src/components/provider";
import { stackServerApp } from "src/stack";
import { stackServerApp } from "src/hexclave";
import './global.css';
const inter = Inter({ subsets: ['latin'] });

View File

@ -3,7 +3,7 @@ import "server-only";
import Link from 'next/link';
import { revalidatePath } from "next/cache";
import { randomUUID } from "crypto";
import { stackServerApp } from '@/stack';
import { stackServerApp } from '@/hexclave';
import { Product, Shop } from "@/shop";
export default async function EditShop() {

View File

@ -1,5 +1,5 @@
import { StackHandler } from "@hexclave/next";
import { stackServerApp } from "../../../stack";
import { stackServerApp } from "../../../hexclave";
export default function Handler(props: any) {
return <StackHandler fullPage app={stackServerApp} routeProps={props} />;

View File

@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { StackProvider, StackTheme } from "@hexclave/next";
import { Inter } from "next/font/google";
import { stackServerApp } from "../stack";
import { stackServerApp } from "../hexclave";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });

View File

@ -1,6 +1,6 @@
import { ServerUser } from "@hexclave/next";
import { Shop } from "@/shop";
import { stackServerApp } from "@/stack";
import { stackServerApp } from "@/hexclave";
export default async function Home() {
const users = await stackServerApp.listUsers();

View File

@ -1,4 +1,4 @@
import { stackClientApp } from "./stack";
import { stackClientApp } from "./hexclave";
const updateUIState = (user: any | null) => {
const authOptions = document.getElementById("authOptions");

View File

@ -1,4 +1,4 @@
import { stackClientApp } from "./stack";
import { stackClientApp } from "./hexclave";
// Check if user is already signed in
stackClientApp.getUser().then((user) => {

View File

@ -1,4 +1,4 @@
import { stackClientApp } from "./stack";
import { stackClientApp } from "./hexclave";
// Check if user is already signed in
stackClientApp.getUser().then((user) => {

View File

@ -1,4 +1,4 @@
import { stackClientApp } from "./stack";
import { stackClientApp } from "./hexclave";
// Check if user is already signed in
stackClientApp.getUser().then((user) => {

View File

@ -1,4 +1,4 @@
import { stackClientApp } from "./stack";
import { stackClientApp } from "./hexclave";
// Check if user is already signed in
stackClientApp.getUser().then((user) => {

View File

@ -6,7 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
import Index from "./pages/Index";
import NotFound from "./pages/NotFound";
import { stackClientApp } from "./stack/client";
import { stackClientApp } from "./hexclave/client";
const queryClient = new QueryClient();

View File

@ -1,5 +1,5 @@
import { StackHandler } from "@hexclave/next";
import { stackServerApp } from "../../../stack";
import { stackServerApp } from "../../../hexclave";
export default function Handler(props: any) {
return (

View File

@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { StackProvider, StackTheme } from "@hexclave/next";
import { stackServerApp } from "../stack";
import { stackServerApp } from "../hexclave";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });

View File

@ -1,4 +1,4 @@
import { stackServerApp } from "@/stack";
import { stackServerApp } from "@/hexclave";
import Link from "next/link";
export default async function Home() {

View File

@ -1,4 +1,4 @@
import { stackServerApp } from "@/stack";
import { stackServerApp } from "@/hexclave";
import Link from "next/link";
export default function Home() {

View File

@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { stackServerApp } from './stack';
import { stackServerApp } from './hexclave';
export async function middleware(request: NextRequest) {
console.log('Running middleware on URL: ', request.url);

View File

@ -1,7 +1,7 @@
import { StackHandler, StackProvider, StackTheme } from "@hexclave/react";
import { Suspense } from "react";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
import { stackClientApp } from "./stack";
import { stackClientApp } from "./hexclave";
function HandlerRoutes() {
const location = useLocation();

View File

@ -1,5 +1,5 @@
import { StackHandler } from "@hexclave/next";
import { stackServerApp } from "../../../stack";
import { stackServerApp } from "../../../hexclave";
export default function Handler(props: any) {
return <StackHandler fullPage app={stackServerApp} routeProps={props} />;

View File

@ -1,5 +1,5 @@
import { StackProvider, StackTheme } from "@hexclave/next";
import { stackServerApp } from "../stack";
import { stackServerApp } from "../hexclave";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (

View File

@ -1,6 +1,6 @@
'use server';
import { stackServerApp } from "@/stack";
import { stackServerApp } from "@/hexclave";
import * as jose from "jose";
/*

View File

@ -6,7 +6,7 @@ import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-r
import type { ReactNode } from "react";
import { Suspense, useMemo } from "react";
import { Header } from "~/components/header";
import { createStackApp } from "~/stack";
import { createStackApp } from "~/hexclave";
export const Route = createRootRoute({
head: () => ({

View File

@ -222,9 +222,9 @@ const NEXT_CHECKS: CheckSpec[] = [
], "Create app/handler/[...stack]/page.tsx that renders <StackHandler fullPage app={stackServerApp} routeProps={props} />."),
layoutWrapsStackProviderCheck(),
envVarsCheck([
{ names: ["NEXT_PUBLIC_STACK_PROJECT_ID"], severity: "fail" },
{ names: ["NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"], severity: "warn" },
{ names: ["STACK_SECRET_SERVER_KEY"], severity: "fail" },
{ names: ["NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID"], severity: "fail" },
{ names: ["NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"], severity: "warn" },
{ names: ["HEXCLAVE_SECRET_SERVER_KEY", "STACK_SECRET_SERVER_KEY"], severity: "fail" },
]),
configFileCheck(),
];
@ -235,8 +235,8 @@ const REACT_CHECKS: CheckSpec[] = [
"stack/client.ts", "stack/client.tsx", "stack/client.js", "stack/client.jsx",
]),
envVarsCheck([
{ names: ["VITE_STACK_PROJECT_ID"], severity: "fail" },
{ names: ["VITE_STACK_PUBLISHABLE_CLIENT_KEY"], severity: "warn" },
{ names: ["VITE_HEXCLAVE_PROJECT_ID", "VITE_STACK_PROJECT_ID"], severity: "fail" },
{ names: ["VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "VITE_STACK_PUBLISHABLE_CLIENT_KEY"], severity: "warn" },
]),
configFileCheck(),
];
@ -249,10 +249,11 @@ const JS_CHECKS: CheckSpec[] = [
]),
envVarsCheck([
// PUBLIC_* aliases cover SvelteKit / Astro, which require that prefix
// to expose vars to client code.
{ names: ["STACK_PROJECT_ID", "PUBLIC_STACK_PROJECT_ID"], severity: "fail" },
{ names: ["STACK_PUBLISHABLE_CLIENT_KEY", "PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"], severity: "warn" },
{ names: ["STACK_SECRET_SERVER_KEY"], severity: "fail" },
// to expose vars to client code. HEXCLAVE_* names are preferred; the
// legacy STACK_* / PUBLIC_STACK_* names remain accepted as a fallback.
{ names: ["HEXCLAVE_PROJECT_ID", "PUBLIC_HEXCLAVE_PROJECT_ID", "STACK_PROJECT_ID", "PUBLIC_STACK_PROJECT_ID"], severity: "fail" },
{ names: ["HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "STACK_PUBLISHABLE_CLIENT_KEY", "PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"], severity: "warn" },
{ names: ["HEXCLAVE_SECRET_SERVER_KEY", "STACK_SECRET_SERVER_KEY"], severity: "fail" },
]),
configFileCheck(),
];

View File

@ -235,9 +235,9 @@ async function writeProjectKeysToEnv(
const envLines = [
"# Hexclave",
`NEXT_PUBLIC_STACK_PROJECT_ID=${project.id}`,
`NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${publishableClientKey}`,
`STACK_SECRET_SERVER_KEY=${secretServerKey}`,
`NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=${project.id}`,
`NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY=${publishableClientKey}`,
`HEXCLAVE_SECRET_SERVER_KEY=${secretServerKey}`,
].join("\n");
const envPath = path.resolve(outputDir, ".env");

View File

@ -72,12 +72,16 @@ describe("localEmulatorReadyTimeoutMs", () => {
describe("resolveProjectId", () => {
const SAVED = process.env.STACK_PROJECT_ID;
const SAVED_HEXCLAVE = process.env.HEXCLAVE_PROJECT_ID;
beforeEach(() => {
delete process.env.STACK_PROJECT_ID;
delete process.env.HEXCLAVE_PROJECT_ID;
});
afterEach(() => {
if (SAVED === undefined) delete process.env.STACK_PROJECT_ID;
else process.env.STACK_PROJECT_ID = SAVED;
if (SAVED_HEXCLAVE === undefined) delete process.env.HEXCLAVE_PROJECT_ID;
else process.env.HEXCLAVE_PROJECT_ID = SAVED_HEXCLAVE;
});
it("uses the --cloud-project-id option when provided", () => {
@ -100,6 +104,6 @@ describe("resolveProjectId", () => {
});
it("throws a CliError with help text when neither is provided", () => {
expect(() => resolveProjectId(undefined)).toThrow(/STACK_PROJECT_ID/);
expect(() => resolveProjectId(undefined)).toThrow(/HEXCLAVE_PROJECT_ID/);
});
});

View File

@ -52,7 +52,7 @@ function resolveRefreshToken(): string {
}
function resolveSecretServerKey(): string | null {
return process.env.STACK_SECRET_SERVER_KEY ?? null;
return process.env.HEXCLAVE_SECRET_SERVER_KEY ?? process.env.STACK_SECRET_SERVER_KEY ?? null;
}
export function resolveLoginConfig(): LoginConfig {
@ -87,15 +87,16 @@ export function resolveAuth(projectId: string): ProjectAuth {
}
// Resolve the cloud project ID from the `--cloud-project-id` option, falling
// back to the STACK_PROJECT_ID environment variable. Empty strings are treated
// as absent so callers can pass through optional option values directly.
// back to the HEXCLAVE_PROJECT_ID environment variable (and the legacy
// STACK_PROJECT_ID name). Empty strings are treated as absent so callers can
// pass through optional option values directly.
export function resolveProjectId(projectIdOption?: string): string {
for (const candidate of [projectIdOption, process.env.STACK_PROJECT_ID]) {
for (const candidate of [projectIdOption, process.env.HEXCLAVE_PROJECT_ID, process.env.STACK_PROJECT_ID]) {
if (candidate != null && candidate !== "") {
return candidate;
}
}
throw new CliError("No project ID provided. Pass --cloud-project-id <id> or set the STACK_PROJECT_ID environment variable.");
throw new CliError("No project ID provided. Pass --cloud-project-id <id> or set the HEXCLAVE_PROJECT_ID environment variable.");
}
export function isProjectAuthWithSecretServerKey(auth: ProjectAuth): auth is ProjectAuthWithSecretServerKey {

View File

@ -45,7 +45,7 @@ export const convexSetupPrompt = deindent`
export default {
providers: getConvexProvidersConfig({
projectId: process.env.STACK_PROJECT_ID, // or process.env.NEXT_PUBLIC_STACK_PROJECT_ID
projectId: process.env.HEXCLAVE_PROJECT_ID, // or process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID
}),
};
\`\`\`
@ -78,7 +78,7 @@ export const convexSetupPrompt = deindent`
\`\`\`ts convex/myFunctions.ts
import { query } from "./_generated/server";
import { hexclaveServerApp } from "../src/stack/server";
import { hexclaveServerApp } from "../src/hexclave/server";
export const myQuery = query({
handler: async (ctx, args) => {
@ -135,8 +135,8 @@ export const supabaseSetupPrompt = deindent`
If you are starting from scratch with Next.js, you can use Supabase's template and then initialize Hexclave:
\`\`\`sh
npx create-next-app@latest -e with-supabase stack-supabase
cd stack-supabase
npx create-next-app@latest -e with-supabase hexclave-supabase
cd hexclave-supabase
npx @hexclave/cli@latest init
\`\`\`
@ -152,10 +152,10 @@ export const supabaseSetupPrompt = deindent`
\`\`\`.env .env.local
# The project ID is the only client-exposed Hexclave variable; in Next.js it must
# be prefixed with NEXT_PUBLIC_. STACK_SECRET_SERVER_KEY is server-only and must
# be prefixed with NEXT_PUBLIC_. HEXCLAVE_SECRET_SERVER_KEY is server-only and must
# NEVER be prefixed or exposed to the client.
NEXT_PUBLIC_STACK_PROJECT_ID=<your-stack-project-id>
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
NEXT_PUBLIC_HEXCLAVE_PROJECT_ID=<your-hexclave-project-id>
HEXCLAVE_SECRET_SERVER_KEY=<your-secret-server-key>
\`\`\`
</Step>
@ -165,7 +165,7 @@ export const supabaseSetupPrompt = deindent`
\`\`\`tsx utils/actions.ts
'use server';
import { hexclaveServerApp } from "@/stack/server";
import { hexclaveServerApp } from "@/hexclave/server";
import * as jose from "jose";
export const getSupabaseJwt = async () => {
@ -450,13 +450,13 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
\`\`\`
</Step>
<Step title="Initializing the Stack App">
Next, let us create the Stack App object for your project. This is the most important object in a Hexclave project.
<Step title="Initializing the Hexclave App">
Next, let us create the Hexclave App object for your project. This is the most important object in a Hexclave project.
${isMaybeFrontend ? deindent`
In a frontend where you cannot keep a secret key safe, you would use the \`HexclaveClientApp\` constructor:
\`\`\`ts src/stack/client.ts
\`\`\`ts src/hexclave/client.ts
import { HexclaveClientApp } from "${packageName}";
export const hexclaveClientApp = new HexclaveClientApp({
@ -474,7 +474,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
In a backend where you can keep a secret key safe, you can use the \`HexclaveServerApp\`, which provides access to more sensitive APIs compared to \`HexclaveClientApp\`:
${!isDefinitelyFrontend ? deindent`
\`\`\`ts src/stack/server.ts
\`\`\`ts src/hexclave/server.ts
import { HexclaveServerApp } from "${packageName}";
export const hexclaveServerApp = new HexclaveServerApp({
@ -493,7 +493,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
` : ""}
${isMaybeFrontend ? deindent`
\`\`\`ts src/stack/server.ts
\`\`\`ts src/hexclave/server.ts
import { HexclaveServerApp } from "${packageName}";
import { hexclaveClientApp } from "./client";
@ -520,11 +520,11 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
<AccordionGroup>
<Accordion title="Option 1: Running Hexclave's dashboard locally (recommended)" defaultOpen>
${isAiPrompt ? "This is the strongly recommended option unless the user has explicitly said otherwise, as it allows usage of \`stack.config.ts\` files and does not require the user to get project IDs or API keys from the dashboard." : ""}
First, create a \`stack.config.ts\` configuration file in the root directory of the workspace (or anywhere else):
${isAiPrompt ? "This is the strongly recommended option unless the user has explicitly said otherwise, as it allows usage of \`hexclave.config.ts\` files and does not require the user to get project IDs or API keys from the dashboard." : ""}
\`\`\`ts stack.config.ts
First, create a \`hexclave.config.ts\` configuration file in the root directory of the workspace (or anywhere else):
\`\`\`ts hexclave.config.ts
import type { HexclaveConfig } from "${packageName}";
// default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts
@ -545,8 +545,8 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
// ...
"scripts": {
// ...
"dev": "stack dev --config-file ./stack.config.ts -- npm run dev:without-stack-auth",
"dev:without-stack-auth": "<your-existing-dev-script>"
"dev": "hexclave dev --config-file ./hexclave.config.ts -- npm run dev:without-hexclave",
"dev:without-hexclave": "<your-existing-dev-script>"
}
}
\`\`\`
@ -568,12 +568,12 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
${isAiPrompt ? `${deindent`
Some projects have the \`requirePublishableClientKey\` config option enabled. In that case, a publishable client key will also be necessary. However, this is extremely uncommon; for most projects this is not true, so don't ask the user for one unless you have confirmation that the publishable client key is required. If it's not required, the project ID is the only environment variable required to use Hexclave on a client.
`}\n\n` : ""}\`\`\`.env .env.local
STACK_PROJECT_ID=<your-project-id>
HEXCLAVE_PROJECT_ID=<your-project-id>
\`\`\`
Alternatively, you can also just set the project ID in the \`stack/client.ts\` file:
Alternatively, you can also just set the project ID in the \`hexclave/client.ts\` file:
\`\`\`ts src/stack/client.ts
\`\`\`ts src/hexclave/client.ts
export const hexclaveClientApp = new HexclaveClientApp({
// ...
projectId: "your-project-id",
@ -590,8 +590,8 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
${isAiPrompt ? `${deindent`
If the \`requirePublishableClientKey\` config option is enabled as described above, a publishable client key will also be necessary. Otherwise, these two are the only environment variables required to use Hexclave on a server.
`}\n\n` : ""}\`\`\`.env .env.local
STACK_PROJECT_ID=<your-project-id>
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
HEXCLAVE_PROJECT_ID=<your-project-id>
HEXCLAVE_SECRET_SERVER_KEY=<your-secret-server-key>
\`\`\`
They'll automatically be picked up by the \`HexclaveServerApp\` constructor.
@ -608,7 +608,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
\`\`\`tsx src/App.tsx
import { HexclaveProvider, HexclaveTheme } from "${packageName}";
import { hexclaveClientApp } from "./stack/client";
import { hexclaveClientApp } from "./hexclave/client";
export default function App() {
return (
@ -628,7 +628,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
\`\`\`tsx src/app/layout.tsx
import { Suspense } from "react";
import { HexclaveProvider, HexclaveTheme } from "${packageName}";
import { hexclaveServerApp } from "@/stack/server";
import { hexclaveServerApp } from "@/hexclave/server";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
@ -649,7 +649,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
import { HexclaveProvider, HexclaveTheme } from "${isDefinitelyTanstackStart ? packageName : "@hexclave/tanstack-start"}";
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
import type { ReactNode } from "react";
import { hexclaveClientApp } from "../stack/client";
import { hexclaveClientApp } from "../hexclave/client";
export const Route = createRootRoute({
shellComponent: RootDocument,
@ -696,7 +696,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
\`\`\`tsx src/App.tsx
import { Suspense } from "react";
import { HexclaveProvider, HexclaveTheme } from "${packageName}";
import { hexclaveClientApp } from "./stack/client";
import { hexclaveClientApp } from "./hexclave/client";
export default function App() {
return (
@ -749,7 +749,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
</Step>
${isMaybeTanstackStart ? deindent`
<Step title="${!isDefinitelyTanstackStart ? "TanStack Start: " : ""}Add the Stack handler route">
<Step title="${!isDefinitelyTanstackStart ? "TanStack Start: " : ""}Add the Hexclave handler route">
Hexclave's auth flows (sign-in, sign-up, OAuth callbacks, password reset, etc.) are rendered by a single \`HexclaveHandler\` component mounted at \`/handler/*\`. In TanStack Start, expose it as a splat file route at \`src/routes/handler/$.tsx\`:
\`\`\`tsx src/routes/handler/$.tsx

View File

@ -1,10 +1,10 @@
import { existsSync, readFileSync } from "fs";
import path from "path";
import { parseStackConfigFileContent, renderConfigFileContent } from "./stack-config-file";
import { parseStackConfigFileContent, renderConfigFileContent } from "./hexclave-config-file";
export { parseStackConfigFileContent, renderConfigFileContent };
/**
* Packages that export the `StackConfig` type, in priority order.
* Packages that export the `HexclaveConfig` type, in priority order.
* The first match found in a project's dependencies wins. Hexclave-branded
* packages come first (canonical); the legacy `@stackframe/*` names remain
* so projects pinned to the last legacy release still render a config file
@ -22,7 +22,7 @@ const CONFIG_IMPORT_PACKAGES = [
/**
* Given a list of dependency names (from package.json), returns the SDK
* package that should be used for the `StackConfig` import, or `undefined`
* package that should be used for the `HexclaveConfig` import, or `undefined`
* if none of the known packages are installed.
*/
export function detectConfigImportPackage(dependencies: string[]): string | undefined {
@ -36,7 +36,7 @@ export function detectConfigImportPackage(dependencies: string[]): string | unde
/**
* Walks up from `dir` to find the nearest `package.json` and returns the
* best SDK package to use for the `StackConfig` type import.
* best SDK package to use for the `HexclaveConfig` type import.
*/
export function detectImportPackageFromDir(dir: string): string | undefined {
let current = dir;
@ -65,7 +65,7 @@ import.meta.vitest?.test("renderConfigFileContent normalizes config exports", ({
expect(renderConfigFileContent({
"payments.items.todos.displayName": "Todo Slots",
"payments.items.todos.customerType": "user",
})).toContain(`export const config: StackConfig = {
})).toContain(`export const config: HexclaveConfig = {
"payments": {
"items": {
"todos": {
@ -119,12 +119,12 @@ import.meta.vitest?.test("renderConfigFileContent rejects invalid config exports
import.meta.vitest?.test("renderConfigFileContent uses custom import package", ({ expect }) => {
const content = renderConfigFileContent({}, "@hexclave/next");
expect(content).toContain('import type { StackConfig } from "@hexclave/next";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/next";');
});
import.meta.vitest?.test("renderConfigFileContent defaults to @hexclave/js", ({ expect }) => {
const content = renderConfigFileContent({});
expect(content).toContain('import type { StackConfig } from "@hexclave/js";');
expect(content).toContain('import type { HexclaveConfig } from "@hexclave/js";');
});
import.meta.vitest?.test("detectConfigImportPackage picks first matching package by priority", ({ expect }) => {

View File

@ -1,6 +1,9 @@
import { decodeBase64, encodeBase64 } from "../../utils/bytes";
import { decrypt, encrypt, hash, iteratedHash } from "../../utils/crypto";
// NOTE (Hexclave rebrand): do NOT rename these "stack-*" purpose tags. They are mixed into the
// vault's key derivation and value encryption; renaming any of them would make every
// previously-stored vault value undecryptable. Internal constants, never user-visible.
const hashPurpose = "stack-data-vault-client-side-encryption-key-hash";
const encryptionSecretPurpose = "stack-data-vault-client-side-encryption-value-encryption-key-hash";
const encryptionValuePurpose = "stack-data-vault-client-side-encryption-value-encryption-value-encryption";

View File

@ -79,6 +79,9 @@ async function getKmsClient() {
}
async function getOrCreateKekId(): Promise<string> {
// NOTE (Hexclave rebrand): do NOT rename this "stack-*" alias. It is the AWS KMS key alias for
// the existing key-encryption key; renaming it would point at a non-existent key and break
// wrap/unwrap of all stored data keys. Internal infrastructure constant, never user-visible.
const id = "alias/stack-data-vault-server-side-kek";
const kms = await getKmsClient();
try {
@ -123,6 +126,8 @@ export async function encryptWithKms(value: string) {
const { dekBytes, edkBytes } = await genDEK();
try {
const ciphertext = await encrypt({
// NOTE (Hexclave rebrand): do NOT rename this "stack-*" purpose tag — it must match the one
// used in decryptWithKms below, or already-encrypted values become undecryptable.
purpose: "stack-data-vault-server-side-encryption",
secret: dekBytes,
value: new TextEncoder().encode(value),

View File

@ -28,8 +28,8 @@ export function renderConfigFileContent(config: unknown, importPackage?: string)
throw new Error(`Config has conflicting keys that would be dropped during normalization: ${droppedKeys.map(k => JSON.stringify(k)).join(", ")}`);
}
const pkg = importPackage ?? DEFAULT_CONFIG_IMPORT_PACKAGE;
const importLine = `import type { StackConfig } from "${pkg}";`;
return `${importLine}\n\nexport const config: StackConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
const importLine = `import type { HexclaveConfig } from "${pkg}";`;
return `${importLine}\n\nexport const config: HexclaveConfig = ${JSON.stringify(normalizedConfig, null, 2)};\n`;
}
type ParsedStackConfig = Record<string, unknown> | typeof showOnboardingStackConfigValue;

View File

@ -34,6 +34,9 @@ async function getDerivedSymmetricKey(purpose: string, secret: string | Uint8Arr
name: "HKDF",
salt: toArrayBufferBacked(salt),
hash: "SHA-256",
// NOTE (Hexclave rebrand): do NOT rename this "stack-*" literal. It is a domain-separation
// tag baked into HKDF key derivation; changing it would derive different keys and make all
// previously-encrypted data undecryptable. It is an internal constant, never user-visible.
info: new TextEncoder().encode(JSON.stringify([
"stack-crypto-helper-derived-symmetric-key",
purpose,
@ -126,6 +129,9 @@ export async function iteratedHash(options: HashOptions & { iterations: number }
);
return new Uint8Array(await crypto.subtle.deriveBits({
name: "PBKDF2",
// NOTE (Hexclave rebrand): do NOT rename this "stack-*" literal. It is a domain-separation tag
// mixed into the PBKDF2 salt; changing it would change every hash output and break verification
// of all previously-hashed values. It is an internal constant, never user-visible.
salt: new TextEncoder().encode(JSON.stringify([
"stack-crypto-helper-iterated-hash",
options.purpose,

View File

@ -111,6 +111,9 @@ export async function getPrivateJwks(options: {
}]))
.digest()
);
// NOTE (Hexclave rebrand): do NOT rename these "stack-*" literals. They are hashed into the
// per-audience JWT signing secret and key id (kid); renaming them would rotate every project's
// JWKS and invalidate all already-issued access tokens. Internal constants, never user-visible.
const perAudienceSecret = getHashOfJwkInfo("stack-jwk-audience-secret");
const perAudienceKid = getHashOfJwkInfo("stack-jwk-kid").slice(0, 12);