Detect conflicting Hexclave and Stack env vars (#1604)

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. -->
This commit is contained in:
BilalG1 2026-06-16 10:57:59 -07:00 committed by GitHub
parent 9d3ee6a0d6
commit 59547ef4ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 455 additions and 96 deletions

View File

@ -1,6 +1,15 @@
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
function getDatabaseConnectionStringEnvVarName() {
const hexclaveValue = process.env.HEXCLAVE_DATABASE_CONNECTION_STRING;
const stackValue = process.env.STACK_DATABASE_CONNECTION_STRING;
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.");
}
return hexclaveValue ? 'HEXCLAVE_DATABASE_CONNECTION_STRING' : 'STACK_DATABASE_CONNECTION_STRING';
}
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
@ -11,7 +20,7 @@ export default defineConfig({
// Hexclave rebrand: prefer the canonical name, fall back to the legacy one
// (empty counts as unset — the checked-in .env templates define empty placeholders).
// eslint-disable-next-line no-restricted-properties
url: env(process.env.HEXCLAVE_DATABASE_CONNECTION_STRING ? 'HEXCLAVE_DATABASE_CONNECTION_STRING' : 'STACK_DATABASE_CONNECTION_STRING'),
url: env(getDatabaseConnectionStringEnvVarName()),
},
experimental: {
externalTables: true,
@ -27,4 +36,3 @@ export default defineConfig({
],
},
})

View File

@ -19,6 +19,7 @@ import { DEFAULT_EMAIL_THEME_ID } from '@hexclave/shared/dist/helpers/emails';
import { AdminUserProjectsCrud } from '@hexclave/shared/dist/interface/crud/projects';
import { ITEM_IDS, PLAN_LIMITS } from '@hexclave/shared/dist/plans';
import { DayInterval } from '@hexclave/shared/dist/utils/dates';
import { getEnvVariable } from '@hexclave/shared/dist/utils/env';
import { throwErr } from '@hexclave/shared/dist/utils/errors';
import { typedEntries, typedFromEntries } from '@hexclave/shared/dist/utils/objects';
@ -60,17 +61,18 @@ export async function seed() {
console.log('Seeding database...');
// Optional default admin user
const adminEmail = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_USER_EMAIL || process.env.STACK_SEED_INTERNAL_PROJECT_USER_EMAIL);
const adminPassword = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_USER_PASSWORD || process.env.STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD);
const adminInternalAccess = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS || process.env.STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS) === 'true';
const adminGithubId = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_USER_GITHUB_ID || process.env.STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID);
const adminEmail = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_EMAIL", "");
const adminPassword = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD", "");
const adminInternalAccess = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS", "") === 'true';
const adminGithubId = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID", "");
// dashboard settings
const dashboardDomain = (process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL || process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL);
const oauthProviderIds = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS || process.env.STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS)?.split(',') ?? [];
const otpEnabled = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_OTP_ENABLED || process.env.STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED) === 'true';
const signUpEnabled = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED || process.env.STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED) === 'true';
const allowLocalhost = (process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST || process.env.STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST) === 'true';
const dashboardDomain = getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL", "");
const rawOauthProviderIds = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS", "");
const oauthProviderIds = rawOauthProviderIds ? rawOauthProviderIds.split(',') : [];
const otpEnabled = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED", "") === 'true';
const signUpEnabled = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED", "") === 'true';
const allowLocalhost = getEnvVariable("STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST", "") === 'true';
const localEmulatorEnabled = isLocalEmulatorEnabled();
@ -366,8 +368,8 @@ export async function seed() {
// seed, email/svix, clickhouse). The emulator CLI authenticates against the
// internal project using the pck stored here, so it must land before the rest
// of the seed even if something later fails.
const isLocalEmulator = (process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR || process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR) === 'true';
const rawPck = (process.env.HEXCLAVE_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY || process.env.STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY);
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === 'true';
const rawPck = getEnvVariable("STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY", "");
if (isLocalEmulator && !rawPck) {
// Emulator images build before a per-VM pck is available. Runtime boots set
// HEXCLAVE_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
@ -377,11 +379,11 @@ export async function seed() {
const keySet = {
publishableClientKey: rawPck || throwErr('HEXCLAVE_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
secretServerKey: isLocalEmulator
? ((process.env.HEXCLAVE_INTERNAL_PROJECT_SECRET_SERVER_KEY || process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY) ?? null)
: ((process.env.HEXCLAVE_INTERNAL_PROJECT_SECRET_SERVER_KEY || process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY) || throwErr('HEXCLAVE_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
? (getEnvVariable("STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY", "") || null)
: (getEnvVariable("STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY", "") || throwErr('HEXCLAVE_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
superSecretAdminKey: isLocalEmulator
? ((process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY) ?? null)
: ((process.env.HEXCLAVE_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY) || throwErr('HEXCLAVE_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),
? (getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY", "") || null)
: (getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY", "") || throwErr('HEXCLAVE_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),
};
await globalPrismaClient.apiKeySet.upsert({
@ -401,7 +403,7 @@ export async function seed() {
console.log('Updated internal API key set');
}
const shouldSeedDummyProject = (process.env.HEXCLAVE_SEED_ENABLE_DUMMY_PROJECT || process.env.STACK_SEED_ENABLE_DUMMY_PROJECT) === 'true';
const shouldSeedDummyProject = getEnvVariable("STACK_SEED_ENABLE_DUMMY_PROJECT", "") === 'true';
if (shouldSeedDummyProject) {
await seedDummyProject({
projectId: DUMMY_PROJECT_ID,

View File

@ -6,11 +6,25 @@ import { applyMigrations, runMigrationNeeded } from "./index";
const TEST_DB_PREFIX = 'stack_auth_test_db';
const getDatabaseConnectionString = (): string => {
// @ts-ignore - ImportMeta.env is provided by Vite
const hexclaveValue: string | undefined = import.meta.env.HEXCLAVE_DATABASE_CONNECTION_STRING;
// @ts-ignore - ImportMeta.env is provided by Vite
const stackValue: string | undefined = import.meta.env.STACK_DATABASE_CONNECTION_STRING;
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;
};
const getTestDbURL = (testDbName: string) => {
// @ts-ignore - ImportMeta.env is provided by Vite
const base = (import.meta.env.HEXCLAVE_DATABASE_CONNECTION_STRING || import.meta.env.STACK_DATABASE_CONNECTION_STRING).replace(/\/[^/]*$/, '');
// @ts-ignore - ImportMeta.env is provided by Vite
const query = (import.meta.env.HEXCLAVE_DATABASE_CONNECTION_STRING || import.meta.env.STACK_DATABASE_CONNECTION_STRING).split('?')[1] ?? '';
const connString = getDatabaseConnectionString();
const base = connString.replace(/\/[^/]*$/, '');
const query = connString.split('?')[1] ?? '';
return {
full: `${base}/${testDbName}`,
base,

View File

@ -12,9 +12,23 @@ const MIGRATIONS_DIR = path.resolve(__dirname, '../../prisma/migrations');
const TEST_DB_PREFIX = 'stack_migration_test';
const getTestDbURL = (testDbName: string) => {
const getDatabaseConnectionString = (): string => {
// @ts-ignore - ImportMeta.env is provided by Vite
const connString: string = (import.meta.env.HEXCLAVE_DATABASE_CONNECTION_STRING || import.meta.env.STACK_DATABASE_CONNECTION_STRING);
const hexclaveValue: string | undefined = import.meta.env.HEXCLAVE_DATABASE_CONNECTION_STRING;
// @ts-ignore - ImportMeta.env is provided by Vite
const stackValue: string | undefined = import.meta.env.STACK_DATABASE_CONNECTION_STRING;
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;
};
const getTestDbURL = (testDbName: string) => {
const connString = getDatabaseConnectionString();
const base = connString.replace(/\/[^/]*(\?.*)?$/, '');
const query = connString.split('?')[1] ?? '';
return { full: `${base}/${testDbName}`, base, query };

View File

@ -43,6 +43,15 @@ const withConfiguredSentryConfig = (nextConfig) =>
}
);
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
const hexclaveValue = process.env[hexclaveName];
const stackValue = process.env[stackName];
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;
}
/** @type {import('next').NextConfig} */
const nextConfig = {
// optionally set output to "standalone" for Docker builds
@ -100,7 +109,7 @@ const nextConfig = {
},
async headers() {
const isLocalEmulator = (process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR || process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR) === "true";
const isLocalEmulator = resolveHexclaveStackEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR", "NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true";
return [
{
source: "/(.*)",

View File

@ -0,0 +1,54 @@
import { afterEach, describe, expect, it, vi } from "vitest";
async function loadEnvModule() {
vi.resetModules();
return await import("./env");
}
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});
describe("dashboard public env var dual-read", () => {
it("falls back to the legacy Stack name when the Hexclave value is empty", async () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "");
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
const { getPublicEnvVar } = await loadEnvModule();
expect(getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
});
it("allows both names when they have the same non-empty value", async () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://api.example.test");
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://api.example.test");
const { getPublicEnvVar } = await loadEnvModule();
expect(getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL")).toBe("https://api.example.test");
});
it("throws when both names are non-empty and different", async () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://hexclave.example.test");
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
await expect(loadEnvModule()).rejects.toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/);
});
it("does not treat unreplaced post-build sentinels as a conflict", async () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_API_URL");
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_API_URL");
await expect(loadEnvModule()).resolves.toBeDefined();
});
it("prefers a real value over a sentinel value", async () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_HEXCLAVE_API_URL");
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
const { getPublicEnvVar } = await loadEnvModule();
expect(getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
});
});

View File

@ -8,40 +8,57 @@ export function expandHexclavePortPrefix(value?: string | null) {
return prefix ? value.replace(/\$\{NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81\}/g, prefix as string) : value;
}
function isPostBuildEnvVarSentinel(value: string): boolean {
return value.startsWith("STACK_ENV_VAR_SENTINEL");
}
function resolveInlineRenamedEnvVar(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined {
const usableHexclaveValue = hexclaveValue && !isPostBuildEnvVarSentinel(hexclaveValue) ? hexclaveValue : undefined;
const usableStackValue = stackValue && !isPostBuildEnvVarSentinel(stackValue) ? stackValue : undefined;
if (
usableHexclaveValue
&& usableStackValue
&& usableHexclaveValue !== usableStackValue
) {
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 usableHexclaveValue || usableStackValue || hexclaveValue || stackValue || undefined;
}
// Hexclave rebrand: each entry prefers the NEXT_PUBLIC_HEXCLAVE_* literal, falling back
// to the legacy NEXT_PUBLIC_*STACK_* literal. Both operands must stay literal
// `process.env.NEXT_PUBLIC_*` references so Next.js can inline them at build time.
const _inlineEnvVars = {
NEXT_PUBLIC_STACK_API_URL: process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL,
NEXT_PUBLIC_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL || process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL,
NEXT_PUBLIC_STACK_SVIX_SERVER_URL: process.env.NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL || process.env.NEXT_PUBLIC_STACK_SVIX_SERVER_URL,
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR || process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR,
NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT: process.env.NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT || process.env.NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT,
NEXT_PUBLIC_STACK_IS_PREVIEW: process.env.NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW || process.env.NEXT_PUBLIC_STACK_IS_PREVIEW,
NEXT_PUBLIC_STACK_API_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL),
NEXT_PUBLIC_STACK_DASHBOARD_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL),
NEXT_PUBLIC_STACK_SVIX_SERVER_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL", "NEXT_PUBLIC_STACK_SVIX_SERVER_URL", process.env.NEXT_PUBLIC_HEXCLAVE_SVIX_SERVER_URL, process.env.NEXT_PUBLIC_STACK_SVIX_SERVER_URL),
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR", "NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR, process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR),
NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT", "NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT", process.env.NEXT_PUBLIC_HEXCLAVE_IS_REMOTE_DEVELOPMENT_ENVIRONMENT, process.env.NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT),
NEXT_PUBLIC_STACK_IS_PREVIEW: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW", "NEXT_PUBLIC_STACK_IS_PREVIEW", process.env.NEXT_PUBLIC_HEXCLAVE_IS_PREVIEW, process.env.NEXT_PUBLIC_STACK_IS_PREVIEW),
NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT: process.env.NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT,
NEXT_PUBLIC_STACK_PROJECT_ID: process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID || process.env.NEXT_PUBLIC_STACK_PROJECT_ID,
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
NEXT_PUBLIC_STACK_URL: process.env.NEXT_PUBLIC_HEXCLAVE_URL || process.env.NEXT_PUBLIC_STACK_URL,
NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: process.env.NEXT_PUBLIC_HEXCLAVE_INBUCKET_WEB_URL || process.env.NEXT_PUBLIC_STACK_INBUCKET_WEB_URL,
NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS: process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS || process.env.NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS,
NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY,
NEXT_PUBLIC_STACK_PROJECT_ID: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID", process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, process.env.NEXT_PUBLIC_STACK_PROJECT_ID),
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY", process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY, process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY),
NEXT_PUBLIC_STACK_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_URL", "NEXT_PUBLIC_STACK_URL", process.env.NEXT_PUBLIC_HEXCLAVE_URL, process.env.NEXT_PUBLIC_STACK_URL),
NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_INBUCKET_WEB_URL", "NEXT_PUBLIC_STACK_INBUCKET_WEB_URL", process.env.NEXT_PUBLIC_HEXCLAVE_INBUCKET_WEB_URL, process.env.NEXT_PUBLIC_STACK_INBUCKET_WEB_URL),
NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS", "NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS", process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS, process.env.NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS),
NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY", "NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY", process.env.NEXT_PUBLIC_HEXCLAVE_STRIPE_PUBLISHABLE_KEY, process.env.NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY),
// Hexclave rebrand: port-prefix var renamed outright (no dual-read).
NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX: process.env.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX,
NEXT_PUBLIC_STACK_DOCS_BASE_URL: process.env.NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL || process.env.NEXT_PUBLIC_STACK_DOCS_BASE_URL,
NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT: process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_REACT_SCAN_IN_DEVELOPMENT || process.env.NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT,
NEXT_PUBLIC_STACK_DOCS_BASE_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL", "NEXT_PUBLIC_STACK_DOCS_BASE_URL", process.env.NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL, process.env.NEXT_PUBLIC_STACK_DOCS_BASE_URL),
NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_ENABLE_REACT_SCAN_IN_DEVELOPMENT", "NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT", process.env.NEXT_PUBLIC_HEXCLAVE_ENABLE_REACT_SCAN_IN_DEVELOPMENT, process.env.NEXT_PUBLIC_STACK_ENABLE_REACT_SCAN_IN_DEVELOPMENT),
// TODO: NEXT_PUBLIC_BROWSER_STACK_API_URL should be renamed to NEXT_PUBLIC_STACK_BROWSER_API_URL
NEXT_PUBLIC_BROWSER_STACK_API_URL: process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_BROWSER_STACK_API_URL,
NEXT_PUBLIC_BROWSER_STACK_API_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_BROWSER_HEXCLAVE_API_URL", "NEXT_PUBLIC_BROWSER_STACK_API_URL", process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_BROWSER_STACK_API_URL),
// TODO: NEXT_PUBLIC_SERVER_STACK_API_URL should be renamed to NEXT_PUBLIC_STACK_SERVER_API_URL
NEXT_PUBLIC_SERVER_STACK_API_URL: process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_SERVER_STACK_API_URL,
NEXT_PUBLIC_SERVER_STACK_API_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL", "NEXT_PUBLIC_SERVER_STACK_API_URL", process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_SERVER_STACK_API_URL),
// TODO: NEXT_PUBLIC_SENTRY_DSN should be renamed to NEXT_PUBLIC_STACK_SENTRY_DSN
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
// TODO: NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY should be renamed to NEXT_PUBLIC_STACK_VERSION_ALERTER_SEVERE_ONLY
NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY: process.env.NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY,
// TODO: NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL should be renamed to NEXT_PUBLIC_STACK_BROWSER_DASHBOARD_URL
NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_DASHBOARD_URL || process.env.NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL,
NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_BROWSER_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_BROWSER_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL),
// TODO: NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL should be renamed to NEXT_PUBLIC_STACK_SERVER_DASHBOARD_URL
NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL: process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_DASHBOARD_URL || process.env.NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL,
NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL: resolveInlineRenamedEnvVar("NEXT_PUBLIC_SERVER_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_SERVER_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL),
// TODO: NEXT_PUBLIC_POSTHOG_KEY should be renamed to NEXT_PUBLIC_STACK_POSTHOG_KEY
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
} as const;

View File

@ -1,10 +1,13 @@
import { generateSecureRandomString } from "@hexclave/shared/dist/utils/crypto";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import type { MailboxMessage } from "../../../../../../helpers";
import { it } from "../../../../../../helpers";
import { Auth, InternalApiKey, Project, ProjectApiKey, Team, backendContext, bumpEmailAddress, niceBackendFetch } from "../../../../../backend-helpers";
// TODO re-enable these tests when we re-enable credential scanning email notifications
const isSourceOfTruthTest = () => getEnvVariable("STACK_TEST_SOURCE_OF_TRUTH", "") === "true";
it("should send email notification to user when revoking an API key through credential scanning", async ({ expect }: { expect: any }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true, allow_team_api_keys: true, allow_user_api_keys: true } });
@ -44,7 +47,7 @@ it("should send email notification to user when revoking an API key through cred
},
});
if ((process.env.HEXCLAVE_TEST_SOURCE_OF_TRUTH || process.env.STACK_TEST_SOURCE_OF_TRUTH) === "true") {
if (isSourceOfTruthTest()) {
expect(revokeResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 404,
@ -180,7 +183,7 @@ it("should send email notification to team members when revoking a team API key
},
});
if ((process.env.HEXCLAVE_TEST_SOURCE_OF_TRUTH || process.env.STACK_TEST_SOURCE_OF_TRUTH) === "true") {
if (isSourceOfTruthTest()) {
expect(revokeResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 404,
@ -353,7 +356,7 @@ it("should handle already revoked API keys gracefully", async ({ expect }: { exp
},
});
if ((process.env.HEXCLAVE_TEST_SOURCE_OF_TRUTH || process.env.STACK_TEST_SOURCE_OF_TRUTH) === "true") {
if (isSourceOfTruthTest()) {
expect(revokeResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 404,

View File

@ -1,11 +1,12 @@
import { randomUUID } from "crypto";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import fs from "fs/promises";
import path from "path";
import { describe } from "vitest";
import { it } from "../../../../../helpers";
import { backendContext, niceBackendFetch } from "../../../../backend-helpers";
const isLocalEmulator = (process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR || process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR) === "true";
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
const blockedMessage = "cannot be changed in a development environment";
const localEmulatorProjectEndpoint = "/api/v1/internal/local-emulator/project";

View File

@ -1,3 +1,4 @@
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { wait } from "@hexclave/shared/dist/utils/promises";
import { stringCompare } from "@hexclave/shared/dist/utils/strings";
import { describe, expect } from "vitest";
@ -13,6 +14,8 @@ type FailedEmailsBatch = {
type DigestResponse = Awaited<ReturnType<typeof niceBackendFetch>>;
const isSourceOfTruthTest = () => getEnvVariable("STACK_TEST_SOURCE_OF_TRUTH", "") === "true";
// Always uses dry_run=true: the only callers are the polling helper below
// (which must be side-effect-free since it fires repeatedly) and the snapshot
// assertion (which uses the same SELECT result regardless of dry_run).
@ -168,7 +171,7 @@ describe("with valid credentials", () => {
const { response, batches: mockProjectFailedEmails } = await waitForFailedEmailsDigest(2);
expect(response.status).toBe(200);
if ((process.env.HEXCLAVE_TEST_SOURCE_OF_TRUTH || process.env.STACK_TEST_SOURCE_OF_TRUTH) === "true") {
if (isSourceOfTruthTest()) {
expect(mockProjectFailedEmails).toMatchInlineSnapshot(`[]`);
} else {
expect(mockProjectFailedEmails).toMatchInlineSnapshot(`
@ -460,7 +463,7 @@ describe("with valid credentials", () => {
(batch: any) => batch.project_id === projectId
);
if ((process.env.HEXCLAVE_TEST_SOURCE_OF_TRUTH || process.env.STACK_TEST_SOURCE_OF_TRUTH) === "true") {
if (isSourceOfTruthTest()) {
expect(currentResponses).toMatchInlineSnapshot(`[]`);
} else {
expect(currentResponses.length).toBe(1);

View File

@ -1,9 +1,10 @@
import { randomUUID } from "crypto";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { describe } from "vitest";
import { it } from "../../../../../helpers";
import { Auth, backendContext, createMailbox, niceBackendFetch, waitForOutboxEmailWithStatus } from "../../../../backend-helpers";
const isLocalEmulator = (process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR || process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR) === "true";
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
const supportConversationsPath = "/api/v1/internal/dogfood/support/conversations";
describe("POST /api/v1/internal/feedback", () => {

View File

@ -1,4 +1,5 @@
import { randomUUID } from "crypto";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import fs from "fs/promises";
import path from "path";
import { describe } from "vitest";
@ -7,7 +8,7 @@ import { backendContext, niceBackendFetch } from "../../../../backend-helpers";
const LOCAL_EMULATOR_PROJECT_ENDPOINT = "/api/v1/internal/local-emulator/project";
const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428";
const isLocalEmulator = (process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR || process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR) === "true";
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
async function createTempConfigFile(): Promise<string> {
const filePath = `/tmp/${randomUUID()}/stack.config.ts`;

View File

@ -1,4 +1,5 @@
import { createHmac } from "node:crypto";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { expect } from "vitest";
import { it } from "../../../../../helpers";
import { Auth, Payments as PaymentsHelper, Project, Team, User, niceBackendFetch } from "../../../../backend-helpers";
@ -72,7 +73,7 @@ async function createPurchaseCodeForCustomer(options: { customerType: "user" | "
return code as string;
}
const stripeWebhookSecret = (process.env.HEXCLAVE_STRIPE_WEBHOOK_SECRET || process.env.STACK_STRIPE_WEBHOOK_SECRET) ?? "mock_stripe_webhook_secret";
const stripeWebhookSecret = getEnvVariable("STACK_STRIPE_WEBHOOK_SECRET", "mock_stripe_webhook_secret");
async function sendStripeWebhook(payload: unknown) {
const timestamp = Math.floor(Date.now() / 1000);

View File

@ -3,11 +3,12 @@ import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { StackAdminApp } from "@hexclave/js";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
import { Result } from "@hexclave/shared/dist/utils/results";
import { describe, beforeAll, afterAll } from "vitest";
import { it, niceFetch, STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_SERVER_KEY, STACK_INTERNAL_PROJECT_ADMIN_KEY } from "../helpers";
const isLocalEmulator = (process.env.NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR || process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR) === "true";
const isLocalEmulator = getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
const CLI_BIN = path.resolve("packages/cli/dist/index.js");

View File

@ -241,7 +241,7 @@ export class Mailbox {
};
this.waitForMessagesWithSubjectCount = async (subject: string, minCount: number, options?: { noBody?: boolean }) => {
const timeoutMs = Number((process.env.HEXCLAVE_MAILBOX_WAIT_TIMEOUT_MS || process.env.STACK_MAILBOX_WAIT_TIMEOUT_MS) ?? 60000);
const timeoutMs = Number(getEnvVariable("STACK_MAILBOX_WAIT_TIMEOUT_MS", "60000"));
const intervalMs = 500;
const deadline = Date.now() + timeoutMs;
let messages: MailboxMessage[] = [];
@ -324,7 +324,7 @@ export const STACK_MCP_BASE_URL = getEnvVariable("STACK_MCP_BASE_URL");
* fallback port. Always thread this through to SDK constructors instead of
* hardcoding `STACK_BACKEND_BASE_URL`.
*/
export const SDK_BASE_URL: string | undefined = (process.env.HEXCLAVE_TEST_SDK_FALLBACK || process.env.STACK_TEST_SDK_FALLBACK)
export const SDK_BASE_URL: string | undefined = getEnvVariable("STACK_TEST_SDK_FALLBACK", "")
? undefined
: STACK_BACKEND_BASE_URL;
export const STACK_INTERNAL_PROJECT_ID = getEnvVariable("STACK_INTERNAL_PROJECT_ID");

View File

@ -50,7 +50,12 @@ function useProjectIdFromHostname(): string | null | undefined {
}
function getApiBaseUrlFromEnv(): string | undefined {
return import.meta.env.VITE_HEXCLAVE_API_URL ?? import.meta.env.VITE_STACK_API_URL ?? undefined;
const hexclaveValue = import.meta.env.VITE_HEXCLAVE_API_URL;
const stackValue = import.meta.env.VITE_STACK_API_URL;
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
throw new Error("Environment variables VITE_HEXCLAVE_API_URL and VITE_STACK_API_URL are both set to different values. Remove one of them or set them to the same value.");
}
return hexclaveValue || stackValue || undefined;
}
function isTrustedNavigationTarget(to: string): boolean {

View File

@ -6,10 +6,20 @@ import { spawnSync } from "node:child_process";
const target = process.argv[2]; // "local" or "prod"
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
const hexclaveValue = process.env[hexclaveName];
const stackValue = process.env[stackName];
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;
}
/** HTTP API for 'spacetime publish' (matches docker/dependencies/docker.compose.yaml host port ...39). */
function localPublishServerUrl() {
if ((process.env.HEXCLAVE_SPACETIME_PUBLISH_URL || process.env.STACK_SPACETIME_PUBLISH_URL)) {
return (process.env.HEXCLAVE_SPACETIME_PUBLISH_URL || process.env.STACK_SPACETIME_PUBLISH_URL);
const publishUrl = resolveHexclaveStackEnvVar("HEXCLAVE_SPACETIME_PUBLISH_URL", "STACK_SPACETIME_PUBLISH_URL");
if (publishUrl) {
return publishUrl;
}
const prefix = process.env.NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX ?? "81";
return `http://127.0.0.1:${prefix}39`;
@ -36,7 +46,7 @@ if (!args) {
process.exit(1);
}
if (target === "prod" && !(process.env.HEXCLAVE_MCP_LOG_TOKEN || process.env.STACK_MCP_LOG_TOKEN)) {
if (target === "prod" && !resolveHexclaveStackEnvVar("HEXCLAVE_MCP_LOG_TOKEN", "STACK_MCP_LOG_TOKEN")) {
console.error("Error: HEXCLAVE_MCP_LOG_TOKEN (or legacy STACK_MCP_LOG_TOKEN) must be set for prod publish");
process.exit(1);
}

View File

@ -11,8 +11,17 @@ const PLACEHOLDER = "__SPACETIMEDB_LOG_TOKEN__";
const action = process.argv[2];
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
const hexclaveValue = process.env[hexclaveName];
const stackValue = process.env[stackName];
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;
}
if (action === "inject") {
const token = (process.env.HEXCLAVE_MCP_LOG_TOKEN || process.env.STACK_MCP_LOG_TOKEN) || "change-me";
const token = resolveHexclaveStackEnvVar("HEXCLAVE_MCP_LOG_TOKEN", "STACK_MCP_LOG_TOKEN") || "change-me";
if (existsSync(BACKUP)) {
console.error("Refusing to inject: backup already exists. Run restore first.");
process.exit(1);

View File

@ -12,6 +12,13 @@ import type { McpCallLogRow } from "../types";
type Tab = "calls" | "knowledge" | "analytics";
function resolveInlineRenamedEnvVar(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;
}
export default function App() {
const user = useUser({ or: process.env.NODE_ENV === "development" ? "redirect" : "return-null" });
const [selectedRow, setSelectedRow] = useState<McpCallLogRow | null>(null);
@ -26,7 +33,12 @@ export default function App() {
<h1 className="text-lg font-semibold text-gray-900 mb-2">MCP Review Tool</h1>
<p className="text-sm text-gray-500 mb-4">
Sign in to the{" "}
<a href={(process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL || process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL)} className="text-blue-600 underline" target="_blank" rel="noreferrer">
<a
href={resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL", "NEXT_PUBLIC_STACK_DASHBOARD_URL", process.env.NEXT_PUBLIC_HEXCLAVE_DASHBOARD_URL, process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL)}
className="text-blue-600 underline"
target="_blank"
rel="noreferrer"
>
Hexclave Dashboard
</a>
{" "}first, then reload this page.

View File

@ -47,9 +47,16 @@ const dashboardEnv = parseEnvFile(dashboardEnvPath);
const toCanonicalKey = (key) => key.includes("STACK_") ? key.replace("STACK_", "HEXCLAVE_") : key;
const getRequiredEnvValue = (sourceName, envMap, key) => {
const value = envMap.get(toCanonicalKey(key)) ?? envMap.get(key);
const canonicalKey = toCanonicalKey(key);
const canonicalValue = envMap.get(canonicalKey);
const legacyValue = canonicalKey === key ? undefined : envMap.get(key);
if (canonicalValue && legacyValue && canonicalValue !== legacyValue) {
throw new Error(`${sourceName} defines both ${canonicalKey} and ${key} with different non-empty values. Remove one of them or set them to the same value.`);
}
const nonEmptyValue = canonicalValue || legacyValue;
const value = nonEmptyValue !== undefined ? nonEmptyValue : canonicalValue ?? legacyValue;
if (value == null) {
throw new Error(`Missing ${toCanonicalKey(key)} in ${sourceName}; update the generator or source env file.`);
throw new Error(`Missing ${canonicalKey} in ${sourceName}; update the generator or source env file.`);
}
return value;
};

View File

@ -24,6 +24,10 @@ WORK_DIR="${STACK_RUNTIME_WORK_DIR:-/app}"
# Hexclave rebrand: the container env may carry the canonical HEXCLAVE_ name
# instead of the legacy STACK_ one this script reads.
if [ -n "${STACK_DATABASE_CONNECTION_STRING:-}" ] && [ -n "${HEXCLAVE_DATABASE_CONNECTION_STRING:-}" ] && [ "$STACK_DATABASE_CONNECTION_STRING" != "$HEXCLAVE_DATABASE_CONNECTION_STRING" ]; then
echo "ERROR: STACK_DATABASE_CONNECTION_STRING and HEXCLAVE_DATABASE_CONNECTION_STRING are both set to different non-empty values. Remove one of them or set them to the same value." >&2
exit 1
fi
STACK_DATABASE_CONNECTION_STRING="${STACK_DATABASE_CONNECTION_STRING:-${HEXCLAVE_DATABASE_CONNECTION_STRING:-}}"
PLACEHOLDER_PCK="00000000000000000000000000000000ffffffffffffffffffffffffffffffff"

View File

@ -28,6 +28,10 @@ mirror_hexclave_stack_env() {
*STACK_*) _twin=${_name/STACK_/HEXCLAVE_} ;;
*) continue ;;
esac
if [ -n "${!_name:-}" ] && [ -n "${!_twin:-}" ] && [ "${!_name}" != "${!_twin}" ]; then
echo "ERROR: $_name and $_twin are both set to different non-empty values. Remove one of them or set them to the same value." >&2
exit 1
fi
if [ -z "${!_twin:-}" ] && [ -n "${!_name:-}" ]; then
export "$_twin=${!_name}"
fi

View File

@ -5,6 +5,7 @@ import { ArrowRight, Check, Code, Copy, Play, Send, Settings, Zap } from 'lucide
import { useCallback, useEffect, useState } from 'react';
import type { OpenAPIOperation, OpenAPIParameter, OpenAPISchema, OpenAPISpec } from '../../lib/openapi-types';
import { resolveSchema } from '../../lib/openapi-utils';
import { resolveInlineRenamedEnvVar } from '../../lib/env';
import { useAPIPageContext } from './api-page-wrapper';
import { Button } from '../mdx/button';
@ -45,6 +46,9 @@ const HTTP_METHOD_COLORS = {
DELETE: 'from-red-500 to-red-600 text-white shadow-red-500/25',
} as const;
function getLocalApiUrlFromEnv(): string | undefined {
return resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL);
}
export function EnhancedAPIPage({ document, operations, description }: EnhancedAPIPageProps) {
const apiContext = useAPIPageContext();
@ -230,7 +234,7 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
}
// Use local API URL in development, production URL from OpenAPI spec otherwise
const defaultBaseUrl = spec?.servers?.[0]?.url || '';
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL;
const localApiUrl = getLocalApiUrlFromEnv();
const baseUrl = localApiUrl ? localApiUrl + '/api/v1' : defaultBaseUrl;
let url = baseUrl + path;
@ -439,7 +443,7 @@ function ModernAPIPlayground({
const generateCurlCommand = useCallback(() => {
// Use local API URL in development, production URL otherwise
const defaultBaseUrl = spec.servers?.[0]?.url || '';
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL;
const localApiUrl = getLocalApiUrlFromEnv();
const baseUrl = localApiUrl
? localApiUrl + '/api/v1'
: defaultBaseUrl;
@ -492,7 +496,7 @@ function ModernAPIPlayground({
const generateJavaScriptCode = useCallback(() => {
// Use local API URL in development, production URL otherwise
const defaultBaseUrl = spec.servers?.[0]?.url || '';
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL;
const localApiUrl = getLocalApiUrlFromEnv();
const baseUrl = localApiUrl
? localApiUrl + '/api/v1'
: defaultBaseUrl;
@ -554,7 +558,7 @@ function ModernAPIPlayground({
const generatePythonCode = useCallback(() => {
// Use local API URL in development, production URL otherwise
const defaultBaseUrl = spec.servers?.[0]?.url || '';
const localApiUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL;
const localApiUrl = getLocalApiUrlFromEnv();
const baseUrl = localApiUrl
? localApiUrl + '/api/v1'
: defaultBaseUrl;

View File

@ -6,6 +6,7 @@ import { runAsynchronously } from '@hexclave/shared/dist/utils/promises';
import { convertToModelMessages, DefaultChatTransport, type DynamicToolUIPart } from 'ai';
import { ChevronDown, ChevronUp, ExternalLink, FileText, Maximize2, Minimize2, Send, X } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { resolveInlineRenamedEnvVar } from '../../lib/env';
import { useSidebar } from '../layouts/sidebar-context';
import { MessageFormatter } from './message-formatter';
@ -351,7 +352,7 @@ export function AIChatDrawer() {
const height = isHomePage && isScrolled ? 'h-[calc(100vh-1.5rem)]' : 'h-[calc(100vh-1.5rem)]';
const [input, setInput] = useState('');
const apiBaseUrl = process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL || throwErr("NEXT_PUBLIC_HEXCLAVE_API_URL or NEXT_PUBLIC_STACK_API_URL is not set");
const apiBaseUrl = resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL) || throwErr("NEXT_PUBLIC_HEXCLAVE_API_URL or NEXT_PUBLIC_STACK_API_URL is not set");
const {
messages,

6
docs/src/lib/env.ts Normal file
View File

@ -0,0 +1,6 @@
export function resolveInlineRenamedEnvVar(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;
}

View File

@ -1,13 +1,14 @@
import { StackServerApp } from '@hexclave/next';
import "server-only";
import { resolveInlineRenamedEnvVar } from './lib/env';
// Explicitly configure Stack Auth for docs app
export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
projectId: process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID || process.env.NEXT_PUBLIC_STACK_PROJECT_ID,
publishableClientKey: process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY || process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
secretServerKey: process.env.HEXCLAVE_SECRET_SERVER_KEY || process.env.STACK_SECRET_SERVER_KEY,
baseUrl: process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL,
projectId: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PROJECT_ID", "NEXT_PUBLIC_STACK_PROJECT_ID", process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, process.env.NEXT_PUBLIC_STACK_PROJECT_ID),
publishableClientKey: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY", "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY", process.env.NEXT_PUBLIC_HEXCLAVE_PUBLISHABLE_CLIENT_KEY, process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY),
secretServerKey: resolveInlineRenamedEnvVar("HEXCLAVE_SECRET_SERVER_KEY", "STACK_SECRET_SERVER_KEY", process.env.HEXCLAVE_SECRET_SERVER_KEY, process.env.STACK_SECRET_SERVER_KEY),
baseUrl: resolveInlineRenamedEnvVar("NEXT_PUBLIC_HEXCLAVE_API_URL", "NEXT_PUBLIC_STACK_API_URL", process.env.NEXT_PUBLIC_HEXCLAVE_API_URL, process.env.NEXT_PUBLIC_STACK_API_URL),
analytics: {
replays: {
enabled: true,

View File

@ -1,8 +1,15 @@
import { getConvexProvidersConfig } from "@hexclave/next/convex-auth.config";
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.`);
}
return hexclaveValue || stackValue || undefined;
}
export default {
providers: getConvexProvidersConfig({
projectId: (process.env.NEXT_PUBLIC_HEXCLAVE_PROJECT_ID || process.env.NEXT_PUBLIC_STACK_PROJECT_ID)!,
baseUrl: (process.env.NEXT_PUBLIC_HEXCLAVE_API_URL || process.env.NEXT_PUBLIC_STACK_API_URL),
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)!,
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

@ -2,8 +2,17 @@
/** Minimal `stack login` flow for local demos. Usage: `node cli-sim.mjs` */
const API_URL = (process.env.HEXCLAVE_API_URL || process.env.STACK_API_URL) || "http://localhost:8102";
const APP_URL = (process.env.HEXCLAVE_APP_URL || process.env.STACK_APP_URL) || "http://localhost:8103";
function resolveHexclaveStackEnvVar(hexclaveName, stackName) {
const hexclaveValue = process.env[hexclaveName];
const stackValue = process.env[stackName];
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;
}
const API_URL = resolveHexclaveStackEnvVar("HEXCLAVE_API_URL", "STACK_API_URL") || "http://localhost:8102";
const APP_URL = resolveHexclaveStackEnvVar("HEXCLAVE_APP_URL", "STACK_APP_URL") || "http://localhost:8103";
const PROJECT_ID = "internal";
const PUBLISHABLE_KEY = "this-publishable-client-key-is-for-local-development-only";

View File

@ -93,11 +93,28 @@ describe("resolveProjectId", () => {
expect(resolveProjectId(undefined)).toBe("proj_from_env");
});
it("uses the HEXCLAVE_PROJECT_ID env var", () => {
process.env.HEXCLAVE_PROJECT_ID = "proj_from_hexclave_env";
expect(resolveProjectId(undefined)).toBe("proj_from_hexclave_env");
});
it("throws when the Hexclave and Stack env vars disagree", () => {
process.env.HEXCLAVE_PROJECT_ID = "proj_from_hexclave_env";
process.env.STACK_PROJECT_ID = "proj_from_stack_env";
expect(() => resolveProjectId(undefined)).toThrow(/HEXCLAVE_PROJECT_ID.*STACK_PROJECT_ID.*different values/);
});
it("prefers the option over the env var", () => {
process.env.STACK_PROJECT_ID = "proj_from_env";
expect(resolveProjectId("proj_from_flag")).toBe("proj_from_flag");
});
it("does not inspect conflicting env vars when the option is present", () => {
process.env.HEXCLAVE_PROJECT_ID = "proj_from_hexclave_env";
process.env.STACK_PROJECT_ID = "proj_from_stack_env";
expect(resolveProjectId("proj_from_flag")).toBe("proj_from_flag");
});
it("treats an empty option string as absent and falls back to the env var", () => {
process.env.STACK_PROJECT_ID = "proj_from_env";
expect(resolveProjectId("")).toBe("proj_from_env");

View File

@ -51,8 +51,17 @@ function resolveRefreshToken(): string {
return token;
}
function resolveHexclaveStackEnvVar(hexclaveName: string, stackName: string): string | undefined {
const hexclaveValue = process.env[hexclaveName];
const stackValue = process.env[stackName];
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
throw new CliError(`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 resolveSecretServerKey(): string | null {
return process.env.HEXCLAVE_SECRET_SERVER_KEY || process.env.STACK_SECRET_SERVER_KEY || null;
return resolveHexclaveStackEnvVar("HEXCLAVE_SECRET_SERVER_KEY", "STACK_SECRET_SERVER_KEY") || null;
}
export function resolveLoginConfig(): LoginConfig {
@ -91,10 +100,12 @@ export function resolveAuth(projectId: string): ProjectAuth {
// 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.HEXCLAVE_PROJECT_ID, process.env.STACK_PROJECT_ID]) {
if (candidate != null && candidate !== "") {
return candidate;
}
if (projectIdOption != null && projectIdOption !== "") {
return projectIdOption;
}
const projectIdFromEnv = resolveHexclaveStackEnvVar("HEXCLAVE_PROJECT_ID", "STACK_PROJECT_ID");
if (projectIdFromEnv != null && projectIdFromEnv !== "") {
return projectIdFromEnv;
}
throw new CliError("No project ID provided. Pass --cloud-project-id <id> or set the HEXCLAVE_PROJECT_ID environment variable.");
}

View File

@ -0,0 +1,43 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { getEnvVariable, getProcessEnv, resolveHexclaveStackEnvVarValue } from "./env";
afterEach(() => {
vi.unstubAllEnvs();
});
describe("Hexclave/Stack env var dual-read", () => {
it("falls back to the legacy Stack name when the Hexclave value is empty", () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "");
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://stack.example.test");
expect(getEnvVariable("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
expect(getProcessEnv("NEXT_PUBLIC_STACK_API_URL")).toBe("https://stack.example.test");
});
it("allows both names when they have the same non-empty value", () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_API_URL", "https://api.example.test");
vi.stubEnv("NEXT_PUBLIC_STACK_API_URL", "https://api.example.test");
expect(getEnvVariable("NEXT_PUBLIC_STACK_API_URL")).toBe("https://api.example.test");
expect(getProcessEnv("NEXT_PUBLIC_STACK_API_URL")).toBe("https://api.example.test");
});
it("throws when both names are non-empty and different", () => {
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_STACK_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/);
expect(() => getProcessEnv("NEXT_PUBLIC_STACK_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_API_URL.*NEXT_PUBLIC_STACK_API_URL.*different values/);
});
it("checks renamed legacy aliases when falling back", () => {
vi.stubEnv("NEXT_PUBLIC_HEXCLAVE_URL", "https://hexclave-url.example.test");
vi.stubEnv("NEXT_PUBLIC_STACK_URL", "https://stack-url.example.test");
expect(() => getEnvVariable("NEXT_PUBLIC_STACK_API_URL")).toThrow(/NEXT_PUBLIC_HEXCLAVE_URL.*NEXT_PUBLIC_STACK_URL.*different values/);
});
it("returns undefined when both names are empty", () => {
expect(resolveHexclaveStackEnvVarValue("HEXCLAVE_FOO", "STACK_FOO", "", "")).toBeUndefined();
});
});

View File

@ -6,7 +6,7 @@ export function isBrowserLike() {
}
// newName: oldName
const ENV_VAR_RENAME: Record<string, string[]> = {
const ENV_VAR_RENAME: Record<string, string[] | undefined> = {
NEXT_PUBLIC_STACK_API_URL: ['STACK_BASE_URL', 'NEXT_PUBLIC_STACK_URL'],
};
@ -24,6 +24,21 @@ function getHexclaveEnvVarName(name: string): string | 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.
*/
@ -45,7 +60,7 @@ export function getEnvVariable(name: string, defaultValue?: string | undefined):
// 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)) {
if (oldNames?.includes(name)) {
throwErr(`Environment variable ${name} has been renamed to ${newName}. Please update your configuration to use the new name.`);
}
}
@ -53,15 +68,13 @@ export function getEnvVariable(name: string, defaultValue?: string | undefined):
// 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.
const hexclaveName = getHexclaveEnvVarName(name);
let value = (hexclaveName ? process.env[hexclaveName] : undefined) || process.env[name];
let value = getEnvVarWithHexclaveFallback(name);
// check the key under the old name if the new name is not found
if (!value && ENV_VAR_RENAME[name] as any) {
for (const oldName of ENV_VAR_RENAME[name]) {
// Hexclave rebrand: also accept the HEXCLAVE_*-prefixed equivalent of each old alias.
const hexclaveOldName = getHexclaveEnvVarName(oldName);
value = (hexclaveOldName ? process.env[hexclaveOldName] : undefined) || process.env[oldName];
const renamedNames = ENV_VAR_RENAME[name];
if (!value && renamedNames != null) {
for (const oldName of renamedNames) {
value = getEnvVarWithHexclaveFallback(oldName);
if (value) break;
}
}
@ -116,6 +129,5 @@ export function getProcessEnv(name: string): string | 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.
const hexclaveName = getHexclaveEnvVarName(name);
return (hexclaveName ? process.env[hexclaveName] : undefined) || process.env[name];
return getEnvVarWithHexclaveFallback(name);
}

View File

@ -80,7 +80,12 @@ export class HexclaveAssertionError extends Error {
// Use literal dot-form (guarded with `typeof process`) so Next.js / webpack
// DefinePlugin can inline the value at build time. See getProcessEnv in ./env.
if ((typeof process !== "undefined" ? (process.env.NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR || process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR) : undefined) === "true") {
const hexclaveDebuggerValue = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR : undefined;
const stackDebuggerValue = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR : undefined;
if (hexclaveDebuggerValue && stackDebuggerValue && hexclaveDebuggerValue !== stackDebuggerValue) {
throw new Error("Environment variables NEXT_PUBLIC_HEXCLAVE_DEBUGGER_ON_ASSERTION_ERROR and NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR are both set to different values. Remove one of them or set them to the same value.");
}
if ((hexclaveDebuggerValue || stackDebuggerValue) === "true") {
debugger;
}
}

View File

@ -63,20 +63,76 @@ const envVarsConfig: Record<string, { allowPublic?: boolean, deprecatedLegacyNam
},
};
function getHexclaveEnvVarName(name: string): string | undefined {
if (!name.includes("STACK_")) {
return undefined;
}
return name.replace("STACK_", "HEXCLAVE_");
}
function getStackEnvVarName(name: string): string | undefined {
if (!name.includes("HEXCLAVE_")) {
return undefined;
}
return name.replace("HEXCLAVE_", "STACK_");
}
function getEnvVarSnippet(variableName: string) {
return deindent`
((typeof process !== "undefined" ? process.env.${variableName} : undefined) || import.meta.env?.${variableName})
`;
}
function getEnvVarCandidateSnippets(allVariables: string[]) {
const allVariablesSet = new Set(allVariables);
const emittedVariables = new Set<string>();
const snippets: string[] = [];
for (const variableName of allVariables) {
if (emittedVariables.has(variableName)) {
continue;
}
const stackName = getStackEnvVarName(variableName);
if (stackName != null && allVariablesSet.has(stackName)) {
emittedVariables.add(variableName);
emittedVariables.add(stackName);
snippets.push(deindent`
resolveHexclaveStackEnvVar("${variableName}", "${stackName}", ${getEnvVarSnippet(variableName)}, ${getEnvVarSnippet(stackName)})
`);
continue;
}
const hexclaveName = getHexclaveEnvVarName(variableName);
if (hexclaveName != null && allVariablesSet.has(hexclaveName)) {
emittedVariables.add(hexclaveName);
emittedVariables.add(variableName);
snippets.push(deindent`
resolveHexclaveStackEnvVar("${hexclaveName}", "${variableName}", ${getEnvVarSnippet(hexclaveName)}, ${getEnvVarSnippet(variableName)})
`);
continue;
}
emittedVariables.add(variableName);
snippets.push(getEnvVarSnippet(variableName));
}
return snippets;
}
function generateEnvVarsConstSnippet() {
const getters: string[] = [];
for (const [key, config] of Object.entries(envVarsConfig)) {
const allVariables = [key, ...(config.deprecatedLegacyNames ?? [])]
.flatMap(k => k.startsWith("HEXCLAVE_") ? [k, k.replace("HEXCLAVE_", "STACK_")] : [k])
.flatMap(k => config.allowPublic ? [k, `NEXT_PUBLIC_${k}`, `VITE_${k}`] : [k]);
const candidateSnippets = getEnvVarCandidateSnippets(allVariables);
// Use || (not ??) between candidates: empty-string env vars (e.g. the empty
// HEXCLAVE_* placeholders in checked-in .env templates) must not shadow a
// real value under a legacy STACK_* name further down the chain.
getters.push(deindent`
get ${key}() {
return ${allVariables.map(variableName => deindent`
((typeof process !== "undefined" ? process.env.${variableName} : undefined) || import.meta.env?.${variableName})
`).join("\n || ")} || undefined;
return ${candidateSnippets.join("\n || ")} || undefined;
},
`);
}
@ -85,6 +141,13 @@ function generateEnvVarsConstSnippet() {
// DO NOT EDIT IT BY HAND.
/* eslint-disable no-restricted-properties */
function resolveHexclaveStackEnvVar(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;
}
export const envVars = {
${getters.join("\n")}
};