mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
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:
parent
59547ef4ec
commit
b270c0f2ef
@ -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",
|
||||
|
||||
@ -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)}`;
|
||||
|
||||
@ -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)}`;
|
||||
|
||||
@ -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)}`;
|
||||
|
||||
24
apps/backend/src/lib/bulldozer/db/test-db-env.ts
Normal file
24
apps/backend/src/lib/bulldozer/db/test-db-env.ts
Normal 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;
|
||||
}
|
||||
@ -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)}`;
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
}],
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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`;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user