diff --git a/apps/backend/package.json b/apps/backend/package.json index ccf240188..12174e897 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -19,8 +19,8 @@ "build-self-host-migration-script": "tsdown --config scripts/db-migrations.tsdown.config.ts", "analyze-bundle": "next experimental-analyze", "start": "next start --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02", - "codegen-prisma": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate", - "codegen-prisma:watch": "STACK_DATABASE_CONNECTION_STRING=\"${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}\" pnpm run prisma generate --watch", + "codegen-prisma": "HEXCLAVE_DATABASE_CONNECTION_STRING=\"${HEXCLAVE_DATABASE_CONNECTION_STRING:-${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}}\" pnpm run prisma generate", + "codegen-prisma:watch": "HEXCLAVE_DATABASE_CONNECTION_STRING=\"${HEXCLAVE_DATABASE_CONNECTION_STRING:-${STACK_DATABASE_CONNECTION_STRING:-placeholder-database-connection-string}}\" pnpm run prisma generate --watch", "generate-private-sign-up-risk-engine": "pnpm run with-env tsx scripts/generate-private-sign-up-risk-engine.ts", "generate-private-sign-up-risk-engine:watch": "chokidar 'src/private/src/sign-up-risk-engine.ts' -c 'pnpm run generate-private-sign-up-risk-engine'", "codegen-route-info": "pnpm run with-env tsx scripts/generate-route-info.ts", diff --git a/apps/backend/src/lib/bulldozer/db/index.fuzz.test.ts b/apps/backend/src/lib/bulldozer/db/index.fuzz.test.ts index a508f3684..af96b120f 100644 --- a/apps/backend/src/lib/bulldozer/db/index.fuzz.test.ts +++ b/apps/backend/src/lib/bulldozer/db/index.fuzz.test.ts @@ -2,6 +2,7 @@ import { stringCompare } from "@hexclave/shared/dist/utils/strings"; import postgres from "postgres"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import type { Table } from "./index"; +import { resolveTestDatabaseConnectionString } from "./test-db-env"; import type { RowData } from "./utilities"; import { createBulldozerExecutionContext, @@ -57,11 +58,7 @@ type TestDb = { full: string, base: string }; const TEST_DB_PREFIX = "stack_bulldozer_db_fuzz_test"; function getTestDbUrls(): TestDb { - const env = Reflect.get(import.meta, "env"); - const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING"); - if (typeof connectionString !== "string" || connectionString.length === 0) { - throw new Error("Missing STACK_DATABASE_CONNECTION_STRING"); - } + const connectionString = resolveTestDatabaseConnectionString(); const base = connectionString.replace(/\/[^/]*(\?.*)?$/, ""); const query = connectionString.split("?")[1] ?? ""; const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`; diff --git a/apps/backend/src/lib/bulldozer/db/index.perf.test.ts b/apps/backend/src/lib/bulldozer/db/index.perf.test.ts index 197fadec3..ba1592296 100644 --- a/apps/backend/src/lib/bulldozer/db/index.perf.test.ts +++ b/apps/backend/src/lib/bulldozer/db/index.perf.test.ts @@ -2,6 +2,7 @@ import { stringCompare } from "@hexclave/shared/dist/utils/strings"; import postgres from "postgres"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { Table } from "./index"; +import { resolveTestDatabaseConnectionString } from "./test-db-env"; import type { RowData } from "./utilities"; import { createBulldozerExecutionContext, @@ -112,11 +113,7 @@ const LOAD_REDUCE_TABLE_INIT_MAX_MS = withCiPerfHeadroom(90_000); const LOAD_REDUCE_TABLE_COUNT_QUERY_MAX_MS = withCiPerfHeadroom(8_000); function getTestDbUrls(): TestDb { - const env = Reflect.get(import.meta, "env"); - const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING"); - if (typeof connectionString !== "string" || connectionString.length === 0) { - throw new Error("Missing STACK_DATABASE_CONNECTION_STRING"); - } + const connectionString = resolveTestDatabaseConnectionString(); const base = connectionString.replace(/\/[^/]*(\?.*)?$/, ""); const query = connectionString.split("?")[1] ?? ""; const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`; diff --git a/apps/backend/src/lib/bulldozer/db/index.test.ts b/apps/backend/src/lib/bulldozer/db/index.test.ts index 5b2b2aa77..85d1ade17 100644 --- a/apps/backend/src/lib/bulldozer/db/index.test.ts +++ b/apps/backend/src/lib/bulldozer/db/index.test.ts @@ -2,6 +2,7 @@ import { stringCompare, templateIdentity } from "@hexclave/shared/dist/utils/str import postgres from "postgres"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test } from "vitest"; import type { Table } from "./index"; +import { resolveTestDatabaseConnectionString } from "./test-db-env"; import { createBulldozerExecutionContext, declareCompactTable, @@ -26,11 +27,7 @@ type TestDb = { full: string, base: string }; const TEST_DB_PREFIX = "stack_bulldozer_db_test"; function getTestDbUrls(): TestDb { - const env = Reflect.get(import.meta, "env"); - const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING"); - if (typeof connectionString !== "string" || connectionString.length === 0) { - throw new Error("Missing STACK_DATABASE_CONNECTION_STRING"); - } + const connectionString = resolveTestDatabaseConnectionString(); const base = connectionString.replace(/\/[^/]*(\?.*)?$/, ""); const query = connectionString.split("?")[1] ?? ""; const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`; diff --git a/apps/backend/src/lib/bulldozer/db/test-db-env.ts b/apps/backend/src/lib/bulldozer/db/test-db-env.ts new file mode 100644 index 000000000..ae8b4a597 --- /dev/null +++ b/apps/backend/src/lib/bulldozer/db/test-db-env.ts @@ -0,0 +1,24 @@ +/** + * Resolve the test database connection string from the environment, preferring + * the canonical `HEXCLAVE_DATABASE_CONNECTION_STRING` and falling back to the + * legacy `STACK_DATABASE_CONNECTION_STRING`. Empty counts as unset. Throws when + * both names are set to different non-empty values, or when neither is set. + * + * Shared by the bulldozer/payments DB-backed vitest suites so the dual-read + * stays consistent with the rest of the Hexclave rebrand. + */ +export function resolveTestDatabaseConnectionString(): string { + const env = Reflect.get(import.meta, "env"); + const hexclaveRaw = Reflect.get(env, "HEXCLAVE_DATABASE_CONNECTION_STRING"); + const stackRaw = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING"); + const hexclaveValue = typeof hexclaveRaw === "string" && hexclaveRaw.length > 0 ? hexclaveRaw : undefined; + const stackValue = typeof stackRaw === "string" && stackRaw.length > 0 ? stackRaw : undefined; + if (hexclaveValue && stackValue && hexclaveValue !== stackValue) { + throw new Error("Environment variables HEXCLAVE_DATABASE_CONNECTION_STRING and STACK_DATABASE_CONNECTION_STRING are both set to different values. Remove one of them or set them to the same value."); + } + const value = hexclaveValue || stackValue; + if (!value) { + throw new Error("Missing environment variable HEXCLAVE_DATABASE_CONNECTION_STRING or STACK_DATABASE_CONNECTION_STRING."); + } + return value; +} diff --git a/apps/backend/src/lib/bulldozer/db/timefold-queue-downstream.test.ts b/apps/backend/src/lib/bulldozer/db/timefold-queue-downstream.test.ts index f40379437..ef92b00f1 100644 --- a/apps/backend/src/lib/bulldozer/db/timefold-queue-downstream.test.ts +++ b/apps/backend/src/lib/bulldozer/db/timefold-queue-downstream.test.ts @@ -22,6 +22,7 @@ import { toQueryableSqlQuery, } from "./index"; import { loadProcessQueueFunctionSql } from "./test-sql-loaders"; +import { resolveTestDatabaseConnectionString } from "./test-db-env"; type SqlExpression = { type: "expression", sql: string }; type SqlStatement = { type: "statement", sql: string, outputName?: string }; @@ -42,11 +43,7 @@ function predicate(sql: string): SqlPredicate { const TEST_DB_PREFIX = "stack_bulldozer_queue_downstream_test"; function getTestDbUrls() { - const env = Reflect.get(import.meta, "env"); - const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING"); - if (typeof connectionString !== "string" || connectionString.length === 0) { - throw new Error("Missing STACK_DATABASE_CONNECTION_STRING"); - } + const connectionString = resolveTestDatabaseConnectionString(); const base = connectionString.replace(/\/[^/]*(\?.*)?$/, ""); const query = connectionString.split("?")[1] ?? ""; const dbName = `${TEST_DB_PREFIX}_${Math.random().toString(16).slice(2, 12)}`; diff --git a/apps/backend/src/lib/payments/schema/__tests__/test-helpers.ts b/apps/backend/src/lib/payments/schema/__tests__/test-helpers.ts index d9af185bf..07baf4eac 100644 --- a/apps/backend/src/lib/payments/schema/__tests__/test-helpers.ts +++ b/apps/backend/src/lib/payments/schema/__tests__/test-helpers.ts @@ -13,17 +13,13 @@ import { toQueryableSqlQuery, } from "@/lib/bulldozer/db/index"; import { loadProcessQueueFunctionSql } from "@/lib/bulldozer/db/test-sql-loaders"; +import { resolveTestDatabaseConnectionString } from "@/lib/bulldozer/db/test-db-env"; type SqlStatement = { type: "statement", sql: string, outputName?: string }; type SqlQuery = { type: "query", sql: string, toStatement(outputName?: string): SqlStatement }; function getConnectionString(): string { - const env = Reflect.get(import.meta, "env"); - const connectionString = Reflect.get(env, "STACK_DATABASE_CONNECTION_STRING"); - if (typeof connectionString !== "string" || connectionString.length === 0) { - throw new Error("Missing STACK_DATABASE_CONNECTION_STRING"); - } - return connectionString; + return resolveTestDatabaseConnectionString(); } export type CreateTestDbOptions = { diff --git a/apps/backend/src/polyfills.tsx b/apps/backend/src/polyfills.tsx index 7a4d65234..cdcaf77e1 100644 --- a/apps/backend/src/polyfills.tsx +++ b/apps/backend/src/polyfills.tsx @@ -22,7 +22,7 @@ const sentryErrorSink = (location: string, error: unknown, level: "error" | "war export function ensurePolyfilled() { for (const [key, value] of Object.entries(process.env)) { - if (key.startsWith("STACK_") || key.startsWith("NEXT_PUBLIC_STACK_")) { + if (key.startsWith("STACK_") || key.startsWith("NEXT_PUBLIC_STACK_") || key.startsWith("HEXCLAVE_") || key.startsWith("NEXT_PUBLIC_HEXCLAVE_")) { const replaced = expandHexclavePortPrefix(value ?? undefined); if (replaced !== undefined) { // eslint-disable-next-line no-restricted-syntax diff --git a/apps/dashboard/.env b/apps/dashboard/.env index 5378b0119..b99c169b0 100644 --- a/apps/dashboard/.env +++ b/apps/dashboard/.env @@ -12,9 +12,9 @@ NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY=# enter your Stripe publishable key NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL=# For prod, leave it empty. For local development, use `http://localhost:8113` # Misc, optional -NEXT_PUBLIC_HEXCLAVE_HEAD_TAGS='[{ "tagName": "script", "attributes": {}, "innerHTML": "// insert head tags here" }]' +NEXT_PUBLIC_HEXCLAVE_HEAD_TAGS=# a JSON array of head tags to inject, e.g. '[{ "tagName": "script", "attributes": {}, "innerHTML": "..." }]'. Leave empty here so a platform-set legacy NEXT_PUBLIC_STACK_HEAD_TAGS value isn't treated as a conflict in prod builds. HEXCLAVE_DEVELOPMENT_TRANSLATION_LOCALE=# enter the locale to use for the translation provider here, for example: de-DE. Only works during development, not in production. Optional, by default don't translate -NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS='["internal"]' +NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS=# JSON array of project IDs that get development features (set to '["internal"]' in .env.development). Leave empty here so a platform-set legacy NEXT_PUBLIC_STACK_* value isn't treated as a conflict in prod builds. NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR=# set to true to open the debugger on assertion errors (set to true in .env.development) HEXCLAVE_FEATUREBASE_JWT_SECRET=# used for Featurebase SSO, you probably won't have to set this HEXCLAVE_CHANGELOG_URL=# Used for raw github link to root changelog.md file. diff --git a/apps/dashboard/.env.development b/apps/dashboard/.env.development index 8189e6966..3464e3957 100644 --- a/apps/dashboard/.env.development +++ b/apps/dashboard/.env.development @@ -11,5 +11,6 @@ NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_POR HEXCLAVE_ARTIFICIAL_DEVELOPMENT_DELAY_MS=50 NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR=false +NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS='["internal"]' HEXCLAVE_FEATUREBASE_JWT_SECRET=secret-value diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index 877eeb8d4..b70ff09a4 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -131,7 +131,7 @@ const nextConfig = { key: "X-Content-Type-Options", value: "nosniff", }, - ...process.env.NEXT_PUBLIC_STACK_IS_PREVIEW === "true" ? [] : [{ + ...resolveHexclaveStackEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW", "NEXT_PUBLIC_STACK_IS_PREVIEW") === "true" ? [] : [{ key: "X-Frame-Options", value: "SAMEORIGIN", }], diff --git a/apps/dashboard/src/lib/remote-development-environment/manager.ts b/apps/dashboard/src/lib/remote-development-environment/manager.ts index 26d4cc2ec..001dcbbc1 100644 --- a/apps/dashboard/src/lib/remote-development-environment/manager.ts +++ b/apps/dashboard/src/lib/remote-development-environment/manager.ts @@ -219,6 +219,21 @@ function createInternalApp(apiBaseUrl: string, anonymousRefreshToken?: string) { function envVarsForProject(project: RemoteDevelopmentEnvironmentProject): Record { return { + // Canonical Hexclave names (preferred by SDKs/examples post-rebrand)... + HEXCLAVE_PROJECT_ID: project.projectId, + NEXT_PUBLIC_HEXCLAVE_PROJECT_ID: project.projectId, + VITE_HEXCLAVE_PROJECT_ID: project.projectId, + EXPO_PUBLIC_HEXCLAVE_PROJECT_ID: project.projectId, + HEXCLAVE_PUBLISHABLE_CLIENT_KEY: project.publishableClientKey, + NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY: project.publishableClientKey, + VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY: project.publishableClientKey, + EXPO_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY: project.publishableClientKey, + HEXCLAVE_SECRET_SERVER_KEY: project.secretServerKey, + HEXCLAVE_API_URL: project.apiBaseUrl, + NEXT_PUBLIC_HEXCLAVE_API_URL: project.apiBaseUrl, + VITE_HEXCLAVE_API_URL: project.apiBaseUrl, + EXPO_PUBLIC_HEXCLAVE_API_URL: project.apiBaseUrl, + // ...plus the legacy Stack names so existing/copied setups keep working. STACK_PROJECT_ID: project.projectId, NEXT_PUBLIC_STACK_PROJECT_ID: project.projectId, VITE_STACK_PROJECT_ID: project.projectId, diff --git a/examples/convex/convex/auth.config.ts b/examples/convex/convex/auth.config.ts index 513788a2a..2d43cca44 100644 --- a/examples/convex/convex/auth.config.ts +++ b/examples/convex/convex/auth.config.ts @@ -1,5 +1,9 @@ import { getConvexProvidersConfig } from "@hexclave/next/convex-auth.config"; +function throwErr(message: string): never { + throw new Error(message); +} + function resolveRenamedEnvVar(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined { if (hexclaveValue && stackValue && hexclaveValue !== stackValue) { throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`); @@ -9,7 +13,7 @@ function resolveRenamedEnvVar(hexclaveName: string, stackName: string, hexclaveV export default { providers: getConvexProvidersConfig({ - projectId: resolveRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID", process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, process.env.NEXT_PUBLIC_STACK_PROJECT_ID)!, + projectId: resolveRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID", process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, process.env.NEXT_PUBLIC_STACK_PROJECT_ID) ?? throwErr("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID or NEXT_PUBLIC_STACK_PROJECT_ID must be set"), baseUrl: resolveRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL), }), } diff --git a/examples/js-example/hexclave.ts b/examples/js-example/hexclave.ts index 14ca9197e..ca1050c8e 100644 --- a/examples/js-example/hexclave.ts +++ b/examples/js-example/hexclave.ts @@ -3,9 +3,9 @@ import { StackClientApp } from "@hexclave/js"; export const hexclaveClientApp = new StackClientApp({ - baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL, - projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID, - publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY, + baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL, + projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID || import.meta.env.VITE_STACK_PROJECT_ID, + publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY, tokenStore: "cookie", urls: { oauthCallback: window.location.origin + "/oauth", diff --git a/examples/lovable-react-18-example/src/hexclave/client.ts b/examples/lovable-react-18-example/src/hexclave/client.ts index 2f47e522a..49ad4b002 100644 --- a/examples/lovable-react-18-example/src/hexclave/client.ts +++ b/examples/lovable-react-18-example/src/hexclave/client.ts @@ -3,9 +3,9 @@ import { useNavigate } from "react-router-dom"; export const hexclaveClientApp = new StackClientApp({ tokenStore: "cookie", - baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL, - projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID, - publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY, + baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL, + projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID || import.meta.env.VITE_STACK_PROJECT_ID, + publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY, redirectMethod: { useNavigate, }, diff --git a/examples/react-example/src/hexclave.ts b/examples/react-example/src/hexclave.ts index 91608be3e..2d3865621 100644 --- a/examples/react-example/src/hexclave.ts +++ b/examples/react-example/src/hexclave.ts @@ -4,9 +4,9 @@ import { StackClientApp } from "@hexclave/react"; import { useNavigate } from "react-router-dom"; export const hexclaveClientApp = new StackClientApp({ - projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID, - publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY, - baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL, + projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID || import.meta.env.VITE_STACK_PROJECT_ID, + publishableClientKey: import.meta.env.VITE_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY, + baseUrl: import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL, tokenStore: "cookie", redirectMethod: { useNavigate, diff --git a/examples/tanstack-start-demo/src/hexclave.ts b/examples/tanstack-start-demo/src/hexclave.ts index 09cf4fe90..7408316fc 100644 --- a/examples/tanstack-start-demo/src/hexclave.ts +++ b/examples/tanstack-start-demo/src/hexclave.ts @@ -9,7 +9,7 @@ function replaceHexclavePortPrefix(value: string): string { } function getStackApiUrl(): string { - const configured = import.meta.env.VITE_HEXCLAVE_API_URL as string | undefined; + const configured = (import.meta.env.VITE_HEXCLAVE_API_URL || import.meta.env.VITE_STACK_API_URL) as string | undefined; return configured ? replaceHexclavePortPrefix(configured) : `http://localhost:${getPortPrefix()}02`; } diff --git a/packages/cli/src/lib/auth.ts b/packages/cli/src/lib/auth.ts index 0ff465ca3..3ff3b387c 100644 --- a/packages/cli/src/lib/auth.ts +++ b/packages/cli/src/lib/auth.ts @@ -31,13 +31,13 @@ export type ProjectAuth = (ProjectAuthWithRefreshToken | ProjectAuthWithSecretSe }; function resolveApiUrl(): string { - return process.env.STACK_API_URL + return resolveHexclaveStackEnvVar("HEXCLAVE_API_URL", "STACK_API_URL") ?? readConfigValue("STACK_API_URL") ?? DEFAULT_API_URL; } function resolveDashboardUrl(): string { - return process.env.STACK_DASHBOARD_URL + return resolveHexclaveStackEnvVar("HEXCLAVE_DASHBOARD_URL", "STACK_DASHBOARD_URL") ?? readConfigValue("STACK_DASHBOARD_URL") ?? DEFAULT_DASHBOARD_URL; } diff --git a/packages/shared/src/utils/env.test.tsx b/packages/shared/src/utils/env.test.tsx index 943e146ef..9ec7f2852 100644 --- a/packages/shared/src/utils/env.test.tsx +++ b/packages/shared/src/utils/env.test.tsx @@ -40,4 +40,19 @@ describe("Hexclave/Stack env var dual-read", () => { it("returns undefined when both names are empty", () => { expect(resolveHexclaveStackEnvVarValue("HEXCLAVE_FOO", "STACK_FOO", "", "")).toBeUndefined(); }); + + it("falls back to the legacy Stack name when a canonical Hexclave name is looked up", () => { + vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test"); + + // Caller passes the canonical HEXCLAVE_ name but only the legacy value is set. + expect(getEnvVariable("NEXT_PUBLIC_HEXCLAVE_API_URL")).toBe("https://stack.example.test"); + expect(getProcessEnv("NEXT_PUBLIC_HEXCLAVE_API_URL")).toBe("https://stack.example.test"); + }); + + it("throws on a conflict when a canonical Hexclave name is looked up", () => { + vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://hexclave.example.test"); + vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test"); + + expect(() => getEnvVariable("NEXT_PUBLIC_HEXCLAVE_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/); + }); }); diff --git a/packages/shared/src/utils/env.tsx b/packages/shared/src/utils/env.tsx index 757849413..b95b58ab7 100644 --- a/packages/shared/src/utils/env.tsx +++ b/packages/shared/src/utils/env.tsx @@ -10,20 +10,6 @@ const ENV_VAR_RENAME: Record = { NEXT_PUBLIC_STACK_API_URL: ['STACK_BASE_URL', 'NEXT_PUBLIC_STACK_URL'], }; -/** - * Hexclave rebrand: compute the `HEXCLAVE_*`-prefixed equivalent of a `STACK_*` - * env var name by replacing the first `STACK_` occurrence with `HEXCLAVE_`. - * Covers `STACK_FOO`, `NEXT_PUBLIC_STACK_FOO`, `NEXT_PUBLIC_BROWSER_STACK_FOO`, - * `NEXT_PUBLIC_SERVER_STACK_FOO`, `VITE_STACK_FOO`. Returns `undefined` when the - * name has no `STACK_` segment (caller should behave exactly as before). - */ -function getHexclaveEnvVarName(name: string): string | undefined { - if (!name.includes("STACK_")) { - return undefined; - } - return name.replace("STACK_", "HEXCLAVE_"); -} - export function resolveHexclaveStackEnvVarValue(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined { if (hexclaveValue && stackValue && hexclaveValue !== stackValue) { throw new Error(`Environment variables ${hexclaveName} and ${stackName} are both set to different values. Remove one of them or set them to the same value.`); @@ -31,12 +17,26 @@ export function resolveHexclaveStackEnvVarValue(hexclaveName: string, stackName: return hexclaveValue || stackValue || undefined; } +/** + * Hexclave rebrand: resolve an env var by reading both the `HEXCLAVE_*` and + * `STACK_*` spellings, preferring the canonical Hexclave value and falling back + * to the legacy Stack value (empty counts as unset). Works in BOTH directions — + * whether the caller passes the legacy `STACK_FOO` name or the canonical + * `HEXCLAVE_FOO` name, the other spelling is still honored. Covers `STACK_FOO`, + * `NEXT_PUBLIC_STACK_FOO`, `NEXT_PUBLIC_BROWSER_STACK_FOO`, + * `NEXT_PUBLIC_SERVER_STACK_FOO`, `VITE_STACK_FOO` and their HEXCLAVE_ twins. + * Names with neither segment behave exactly as before. + */ function getEnvVarWithHexclaveFallback(name: string): string | undefined { - const hexclaveName = getHexclaveEnvVarName(name); - if (hexclaveName == null) { - return process.env[name]; + if (name.includes("STACK_")) { + const hexclaveName = name.replace("STACK_", "HEXCLAVE_"); + return resolveHexclaveStackEnvVarValue(hexclaveName, name, process.env[hexclaveName], process.env[name]); } - return resolveHexclaveStackEnvVarValue(hexclaveName, name, process.env[hexclaveName], process.env[name]); + if (name.includes("HEXCLAVE_")) { + const stackName = name.replace("HEXCLAVE_", "STACK_"); + return resolveHexclaveStackEnvVarValue(name, stackName, process.env[name], process.env[stackName]); + } + return process.env[name]; } /**