mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
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:
parent
9d3ee6a0d6
commit
59547ef4ec
@ -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({
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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: "/(.*)",
|
||||
|
||||
54
apps/dashboard/src/lib/env.test.tsx
Normal file
54
apps/dashboard/src/lib/env.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
6
docs/src/lib/env.ts
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
|
||||
43
packages/shared/src/utils/env.test.tsx
Normal file
43
packages/shared/src/utils/env.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user