mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Summary: Detects conflicting non-empty HEXCLAVE_* and STACK_* values
across shared env helpers, dashboard public envs, generated SDK env
access, Docker scripts, CLI/docs/examples, and related tests.
Verification: pnpm test run packages/shared/src/utils/env.test.tsx
apps/dashboard/src/lib/env.test.tsx packages/cli/src/lib/auth.test.ts;
targeted lint/typecheck across touched workspaces; bash -n/node --check
for changed scripts; node
docker/local-emulator/generate-env-development.mjs --check.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Detects and blocks conflicting `HEXCLAVE_*` and `STACK_*` env vars
across the monorepo. Prefers `HEXCLAVE_*`, falls back to `STACK_*` when
empty, and fails fast when both are set to different values.
- **New Features**
- Added conflict-aware env resolvers used across apps, CLI, docs,
examples, and Docker (build/runtime).
- Validates critical vars (e.g., database connection, API/dashboard
URLs, emulator flags, tokens) and ignores post-build sentinel values.
- Prisma, Next.js, and Docker startup now error on mismatched values;
CLI enforces project ID/key conflicts; tests added.
- **Migration**
- If both names are set with different values, builds/tests/scripts will
error. Set only `HEXCLAVE_*` or make both equal.
- Update `.env`, CI secrets, and Docker envs to use `HEXCLAVE_*`. Keep
`STACK_*` only as a temporary fallback.
<sup>Written for commit 4d63fa3bad.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1604?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
134 lines
5.2 KiB
TypeScript
134 lines
5.2 KiB
TypeScript
import { HexclaveAssertionError, throwErr } from "./errors";
|
|
import { deindent } from "./strings";
|
|
|
|
export function isBrowserLike() {
|
|
return typeof window !== "undefined" && typeof document !== "undefined" && typeof document.createElement !== "undefined";
|
|
}
|
|
|
|
// newName: oldName
|
|
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.`);
|
|
}
|
|
return hexclaveValue || stackValue || undefined;
|
|
}
|
|
|
|
function getEnvVarWithHexclaveFallback(name: string): string | undefined {
|
|
const hexclaveName = getHexclaveEnvVarName(name);
|
|
if (hexclaveName == null) {
|
|
return process.env[name];
|
|
}
|
|
return resolveHexclaveStackEnvVarValue(hexclaveName, name, process.env[hexclaveName], process.env[name]);
|
|
}
|
|
|
|
/**
|
|
* Returns the environment variable with the given name, returning the default (if given) or throwing an error (otherwise) if it's undefined or the empty string.
|
|
*/
|
|
export function getEnvVariable(name: string, defaultValue?: string | undefined): string {
|
|
if (isBrowserLike()) {
|
|
throw new Error(deindent`
|
|
Can't use getEnvVariable on the client because Next.js transpiles expressions of the kind process.env.XYZ at build-time on the client.
|
|
|
|
Use process.env.XYZ directly instead.
|
|
`);
|
|
}
|
|
if (name === "NEXT_RUNTIME") {
|
|
throw new Error(deindent`
|
|
Can't use getEnvVariable to access the NEXT_RUNTIME environment variable because it's compiled into the client bundle.
|
|
|
|
Use getNextRuntime() instead.
|
|
`);
|
|
}
|
|
|
|
// throw error if the old name is used as the retrieve key
|
|
for (const [newName, oldNames] of Object.entries(ENV_VAR_RENAME)) {
|
|
if (oldNames?.includes(name)) {
|
|
throwErr(`Environment variable ${name} has been renamed to ${newName}. Please update your configuration to use the new name.`);
|
|
}
|
|
}
|
|
|
|
// Hexclave rebrand: prefer the HEXCLAVE_*-prefixed equivalent, fall back to the STACK_* name.
|
|
// Treat the empty string as unset — the checked-in .env templates define empty
|
|
// HEXCLAVE_* placeholders, which must not shadow a real value under the legacy name.
|
|
let value = getEnvVarWithHexclaveFallback(name);
|
|
|
|
// check the key under the old name if the new name is not found
|
|
const renamedNames = ENV_VAR_RENAME[name];
|
|
if (!value && renamedNames != null) {
|
|
for (const oldName of renamedNames) {
|
|
value = getEnvVarWithHexclaveFallback(oldName);
|
|
if (value) break;
|
|
}
|
|
}
|
|
|
|
if (!value) {
|
|
if (defaultValue !== undefined) {
|
|
value = defaultValue;
|
|
} else {
|
|
throwErr(`Missing environment variable: ${name}`);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
export function getEnvBoolean(name: string): boolean {
|
|
const value = getEnvVariable(name, "false");
|
|
if (value === "true") {
|
|
return true;
|
|
} else if (value === "false") {
|
|
return false;
|
|
} else {
|
|
throw new HexclaveAssertionError(`Environment variable ${name} must be either "true" or "false": found ${JSON.stringify(value)}`);
|
|
}
|
|
}
|
|
|
|
export function getNextRuntime() {
|
|
// This variable is compiled into the client bundle, so we can't use getEnvVariable here.
|
|
return process.env.NEXT_RUNTIME || throwErr("Missing environment variable: NEXT_RUNTIME");
|
|
}
|
|
|
|
export function getNodeEnvironment() {
|
|
return getEnvVariable("NODE_ENV", "");
|
|
}
|
|
|
|
/**
|
|
* Browser-safe access to `process.env` for server-only or genuinely dynamic
|
|
* env-var lookups. Returns `undefined` when `process` is not defined (e.g. in
|
|
* a Vite browser bundle without a `process` shim).
|
|
*
|
|
* Note: uses `process.env[name]` (bracket form), which is NOT recognized by
|
|
* Next.js / webpack DefinePlugin for compile-time inlining. If you need
|
|
* build-time inlining for a `NEXT_PUBLIC_*` var, use the literal dot-form at
|
|
* the call site, guarded with `typeof process`:
|
|
*
|
|
* const value = (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_FOO : undefined);
|
|
*/
|
|
export function getProcessEnv(name: string): string | undefined {
|
|
if (typeof process === "undefined" || typeof process.env === "undefined") {
|
|
return undefined;
|
|
}
|
|
// Hexclave rebrand: prefer the HEXCLAVE_*-prefixed equivalent, fall back to the STACK_* name.
|
|
// Empty counts as unset — the checked-in .env templates define empty HEXCLAVE_* placeholders,
|
|
// which must not shadow a real value under the legacy name.
|
|
return getEnvVarWithHexclaveFallback(name);
|
|
}
|