diff --git a/packages/stack-shared/src/schema-fields.ts b/packages/stack-shared/src/schema-fields.ts index a34ddf308..0087fe84e 100644 --- a/packages/stack-shared/src/schema-fields.ts +++ b/packages/stack-shared/src/schema-fields.ts @@ -3,6 +3,7 @@ import { KnownErrors } from "./known-errors"; import { isBase64 } from "./utils/bytes"; import { SUPPORTED_CURRENCIES, type Currency, type MoneyAmount } from "./utils/currency-constants"; import type { DayInterval, Interval } from "./utils/dates"; +import { getProcessEnv } from "./utils/env"; import { StackAssertionError } from "./utils/errors"; import { decodeBasicAuthorizationHeader } from "./utils/http"; import { allProviders } from "./utils/oauth"; @@ -881,7 +882,7 @@ export const neonAuthorizationHeaderSchema = basicAuthorizationHeaderSchema.test const decoded = decodeBasicAuthorizationHeader(value); if (decoded === null) return true; const [clientId, clientSecret] = decoded; - for (const neonClientConfig of JSON.parse(process.env.STACK_INTEGRATION_CLIENTS_CONFIG || '[]')) { + for (const neonClientConfig of JSON.parse(getProcessEnv("STACK_INTEGRATION_CLIENTS_CONFIG") || '[]')) { if (clientId === neonClientConfig.client_id && clientSecret === neonClientConfig.client_secret) return true; } return false; diff --git a/packages/stack-shared/src/utils/env.tsx b/packages/stack-shared/src/utils/env.tsx index 0d6b0e9d5..d92f53bc7 100644 --- a/packages/stack-shared/src/utils/env.tsx +++ b/packages/stack-shared/src/utils/env.tsx @@ -76,3 +76,22 @@ export function getNextRuntime() { export function getNodeEnvironment() { return getEnvVariable("NODE_ENV", ""); } + +/** + * Browser-safe access to `process.env` for server-only or genuinely dynamic + * env-var lookups. Returns `undefined` when `process` is not defined (e.g. in + * a Vite browser bundle without a `process` shim). + * + * Note: uses `process.env[name]` (bracket form), which is NOT recognized by + * Next.js / webpack DefinePlugin for compile-time inlining. If you need + * build-time inlining for a `NEXT_PUBLIC_*` var, use the literal dot-form at + * the call site, guarded with `typeof process`: + * + * const value = (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_FOO : undefined); + */ +export function getProcessEnv(name: string): string | undefined { + if (typeof process === "undefined" || typeof process.env === "undefined") { + return undefined; + } + return process.env[name]; +} diff --git a/packages/stack-shared/src/utils/errors.tsx b/packages/stack-shared/src/utils/errors.tsx index e14e7e800..13fdbce7a 100644 --- a/packages/stack-shared/src/utils/errors.tsx +++ b/packages/stack-shared/src/utils/errors.tsx @@ -78,7 +78,9 @@ export class StackAssertionError extends Error { enumerable: false, }); - if (process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR === "true") { + // 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_STACK_DEBUGGER_ON_ASSERTION_ERROR : undefined) === "true") { debugger; } } diff --git a/packages/stack-shared/src/utils/esbuild.tsx b/packages/stack-shared/src/utils/esbuild.tsx index dff9c14c5..fee3925cb 100644 --- a/packages/stack-shared/src/utils/esbuild.tsx +++ b/packages/stack-shared/src/utils/esbuild.tsx @@ -1,6 +1,6 @@ import * as esbuild from 'esbuild-wasm/lib/browser.js'; import { join } from 'path'; -import { isBrowserLike } from './env'; +import { getProcessEnv, isBrowserLike } from './env'; import { captureError, StackAssertionError, throwErr } from "./errors"; import { createGlobalAsync } from './globals'; import { ignoreUnhandledRejection, runAsynchronously } from './promises'; @@ -13,7 +13,7 @@ import { traceSpan, withTraceSpan } from './telemetry'; let esbuildInitializePromise: Promise | null = null; -if (process.env.NODE_ENV === 'development' && typeof process !== "undefined" && typeof process.exit === "function") { +if (typeof process !== "undefined" && typeof process.exit === "function" && getProcessEnv("NODE_ENV") === 'development') { // On development Node.js servers, initialize ESBuild as soon as the module is imported so we have to wait less on the first request runAsynchronously(async () => { try { diff --git a/packages/stack-shared/src/utils/promises.tsx b/packages/stack-shared/src/utils/promises.tsx index 4cfe07cf4..bd056fbc5 100644 --- a/packages/stack-shared/src/utils/promises.tsx +++ b/packages/stack-shared/src/utils/promises.tsx @@ -1,4 +1,5 @@ import { KnownError } from ".."; +import { getProcessEnv } from "./env"; import { StackAssertionError, captureError, concatStacktraces, errorToNiceString } from "./errors"; import { DependenciesMap } from "./maps"; import { Result } from "./results"; @@ -318,10 +319,11 @@ export function runAsynchronouslyWithAlert(...args: Parameters { - if (KnownError.isKnownError(error) && typeof process !== "undefined" && (process.env.NODE_ENV as any)?.includes("production")) { + const nodeEnv = getProcessEnv("NODE_ENV"); + if (KnownError.isKnownError(error) && nodeEnv?.includes("production")) { alert(error.message); } else { - alert(`An unhandled error occurred. Please ${process.env.NODE_ENV === "development" ? `check the browser console for the full error.` : "report this to the developer."}\n\n${error}`); + alert(`An unhandled error occurred. Please ${nodeEnv === "development" ? `check the browser console for the full error.` : "report this to the developer."}\n\n${error}`); } args[1]?.onError?.(error); },