Skip to content

Commit b3d0ab6

Browse files
BilalG1claude
andauthored
fix(stack-shared): make process.env access browser-safe (#1391)
## Summary - Bare `process.env.X` accesses in `stack-shared` throw `ReferenceError: process is not defined` when the package is bundled into a browser app without a `process` shim (e.g. a plain Vite app). The most reachable offenders are in `StackAssertionError`'s constructor and `schema-fields.ts`'s Neon Basic-auth validator, both of which can run on the client during normal sign-in flows with `@stackframe/react`. - Extracted a zero-dependency `getProcessEnv` helper at `packages/stack-shared/src/utils/process-env.tsx` and routed the bare references through it. Returns `undefined` when `process` is not defined; otherwise behaves like a normal `process.env[name]` read, so Next.js/webpack inlining is unchanged on the server. - Touched: `schema-fields.ts:884` (`STACK_INTEGRATION_CLIENTS_CONFIG`), `utils/errors.tsx:81` (`NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR`), `utils/promises.tsx` (`NODE_ENV` in `runAsynchronouslyWithAlert`), `utils/esbuild.tsx:16` (`NODE_ENV`, also reordered the `typeof process` guard so the env access is unreachable in browsers). ## Why a separate helper module `utils/env.tsx` already exists but its `getEnvVariable` explicitly throws in the browser, so it can't be reused here. The new module has zero imports so it can be safely consumed from low-level utilities like `errors.tsx` without creating a cycle (env.tsx ↔ errors.tsx). ## Test plan - [x] `pnpm lint` passes - [x] `pnpm typecheck` passes - [ ] Reproduced the original failure in a Vite + `@stackframe/react` app: sign-in flow logged `ReferenceError: process is not defined` from `StackAssertionError`, plus `clientSecret must not be empty` cascading from the same path - [ ] Verify the same flow in a Vite app no longer throws once `@stackframe/react` is rebuilt against this `stack-shared` change - [ ] Confirm Next.js consumer behavior is unchanged (env vars still inlined at build time for `NEXT_PUBLIC_*`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Refactor** * Improved environment variable handling across shared utilities for enhanced browser compatibility and safety. Introduced a new utility for dynamic, browser-safe environment variable access that prevents errors in non-Node.js environments. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 65d87a4 commit b3d0ab6

5 files changed

Lines changed: 30 additions & 6 deletions

File tree

packages/stack-shared/src/schema-fields.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { KnownErrors } from "./known-errors";
33
import { isBase64 } from "./utils/bytes";
44
import { SUPPORTED_CURRENCIES, type Currency, type MoneyAmount } from "./utils/currency-constants";
55
import type { DayInterval, Interval } from "./utils/dates";
6+
import { getProcessEnv } from "./utils/env";
67
import { StackAssertionError } from "./utils/errors";
78
import { decodeBasicAuthorizationHeader } from "./utils/http";
89
import { allProviders } from "./utils/oauth";
@@ -881,7 +882,7 @@ export const neonAuthorizationHeaderSchema = basicAuthorizationHeaderSchema.test
881882
const decoded = decodeBasicAuthorizationHeader(value);
882883
if (decoded === null) return true;
883884
const [clientId, clientSecret] = decoded;
884-
for (const neonClientConfig of JSON.parse(process.env.STACK_INTEGRATION_CLIENTS_CONFIG || '[]')) {
885+
for (const neonClientConfig of JSON.parse(getProcessEnv("STACK_INTEGRATION_CLIENTS_CONFIG") || '[]')) {
885886
if (clientId === neonClientConfig.client_id && clientSecret === neonClientConfig.client_secret) return true;
886887
}
887888
return false;

packages/stack-shared/src/utils/env.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,22 @@ export function getNextRuntime() {
7676
export function getNodeEnvironment() {
7777
return getEnvVariable("NODE_ENV", "");
7878
}
79+
80+
/**
81+
* Browser-safe access to `process.env` for server-only or genuinely dynamic
82+
* env-var lookups. Returns `undefined` when `process` is not defined (e.g. in
83+
* a Vite browser bundle without a `process` shim).
84+
*
85+
* Note: uses `process.env[name]` (bracket form), which is NOT recognized by
86+
* Next.js / webpack DefinePlugin for compile-time inlining. If you need
87+
* build-time inlining for a `NEXT_PUBLIC_*` var, use the literal dot-form at
88+
* the call site, guarded with `typeof process`:
89+
*
90+
* const value = (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_FOO : undefined);
91+
*/
92+
export function getProcessEnv(name: string): string | undefined {
93+
if (typeof process === "undefined" || typeof process.env === "undefined") {
94+
return undefined;
95+
}
96+
return process.env[name];
97+
}

packages/stack-shared/src/utils/errors.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export class StackAssertionError extends Error {
7878
enumerable: false,
7979
});
8080

81-
if (process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR === "true") {
81+
// Use literal dot-form (guarded with `typeof process`) so Next.js / webpack
82+
// DefinePlugin can inline the value at build time. See getProcessEnv in ./env.
83+
if ((typeof process !== "undefined" ? process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR : undefined) === "true") {
8284
debugger;
8385
}
8486
}

packages/stack-shared/src/utils/esbuild.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as esbuild from 'esbuild-wasm/lib/browser.js';
22
import { join } from 'path';
3-
import { isBrowserLike } from './env';
3+
import { getProcessEnv, isBrowserLike } from './env';
44
import { captureError, StackAssertionError, throwErr } from "./errors";
55
import { createGlobalAsync } from './globals';
66
import { ignoreUnhandledRejection, runAsynchronously } from './promises';
@@ -13,7 +13,7 @@ import { traceSpan, withTraceSpan } from './telemetry';
1313

1414
let esbuildInitializePromise: Promise<void> | null = null;
1515

16-
if (process.env.NODE_ENV === 'development' && typeof process !== "undefined" && typeof process.exit === "function") {
16+
if (typeof process !== "undefined" && typeof process.exit === "function" && getProcessEnv("NODE_ENV") === 'development') {
1717
// On development Node.js servers, initialize ESBuild as soon as the module is imported so we have to wait less on the first request
1818
runAsynchronously(async () => {
1919
try {

packages/stack-shared/src/utils/promises.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { KnownError } from "..";
2+
import { getProcessEnv } from "./env";
23
import { StackAssertionError, captureError, concatStacktraces, errorToNiceString } from "./errors";
34
import { DependenciesMap } from "./maps";
45
import { Result } from "./results";
@@ -318,10 +319,11 @@ export function runAsynchronouslyWithAlert(...args: Parameters<typeof runAsynchr
318319
{
319320
...args[1],
320321
onError: error => {
321-
if (KnownError.isKnownError(error) && typeof process !== "undefined" && (process.env.NODE_ENV as any)?.includes("production")) {
322+
const nodeEnv = getProcessEnv("NODE_ENV");
323+
if (KnownError.isKnownError(error) && nodeEnv?.includes("production")) {
322324
alert(error.message);
323325
} else {
324-
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}`);
326+
alert(`An unhandled error occurred. Please ${nodeEnv === "development" ? `check the browser console for the full error.` : "report this to the developer."}\n\n${error}`);
325327
}
326328
args[1]?.onError?.(error);
327329
},

0 commit comments

Comments
 (0)