Fix env-rename gaps from PR review: prod-build conflict, dual-read holes

Addresses correctness/coverage issues found reviewing the STACK_*->HEXCLAVE_*
rename, including a confirmed production-breaking dashboard build failure.

- dashboard/.env: empty out non-empty committed NEXT_PUBLIC_HEXCLAVE_* values
  (ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS, HEAD_TAGS) that collided with the
  platform-set legacy NEXT_PUBLIC_STACK_* values at build time and threw in the
  inline conflict check; move the local-dev default to .env.development.
- backend polyfill: expand the ${PORT_PREFIX} sentinel for HEXCLAVE_/
  NEXT_PUBLIC_HEXCLAVE_ keys too (renamed DB/Svix/S3 URLs were being skipped).
- codegen-prisma: set only HEXCLAVE_DATABASE_CONNECTION_STRING (prefer existing
  HEXCLAVE/STACK, else placeholder) so it never diverges from a real STACK value
  and trips prisma.config.ts's conflict check.
- backend DB tests: centralize a dual-read resolveTestDatabaseConnectionString()
  and use it in bulldozer/payments suites (were legacy-STACK_-only).
- dashboard next.config: dual-read NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW for the
  X-Frame-Options gate.
- RDE manager: inject canonical HEXCLAVE_* names alongside legacy STACK_* ones.
- vite examples: restore VITE_HEXCLAVE_* || VITE_STACK_* fallback.
- cli auth: dual-read HEXCLAVE_API_URL / HEXCLAVE_DASHBOARD_URL.
- shared env: make getEnvVarWithHexclaveFallback two-way so canonical callers
  also fall back to the legacy name; add tests.
- convex example: replace non-null assertion with ?? throwErr(...).
This commit is contained in:
BilalG1 2026-06-16 11:39:46 -07:00
parent 59547ef4ec
commit b270c0f2ef
20 changed files with 106 additions and 63 deletions

View File

@ -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",

View File

@ -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)}`;

View File

@ -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)}`;

View File

@ -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)}`;

View File

@ -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;
}

View File

@ -22,6 +22,7 @@ import {
toQueryableSqlQuery,
} from "./index";
import { loadProcessQueueFunctionSql } from "./test-sql-loaders";
import { resolveTestDatabaseConnectionString } from "./test-db-env";
type SqlExpression<T> = { 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)}`;

View File

@ -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 = {

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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",
}],

View File

@ -219,6 +219,21 @@ function createInternalApp(apiBaseUrl: string, anonymousRefreshToken?: string) {
function envVarsForProject(project: RemoteDevelopmentEnvironmentProject): Record<string, string> {
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,

View File

@ -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),
}),
}

View File

@ -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",

View File

@ -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,
},

View File

@ -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,

View File

@ -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`;
}

View File

@ -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;
}

View File

@ -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/);
});
});

View File

@ -10,20 +10,6 @@ const ENV_VAR_RENAME: Record<string, string[] | undefined> = {
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];
}
/**