diff --git a/.changeset/tame-parents-drive.md b/.changeset/tame-parents-drive.md new file mode 100644 index 00000000000..cdc37de4137 --- /dev/null +++ b/.changeset/tame-parents-drive.md @@ -0,0 +1,5 @@ +--- +'@clerk/astro': major +--- + +Changed environment variable resolution order in `getContextEnvVar()` to prefer `process.env` over `import.meta.env`. Runtime environment variables (e.g., set in the Node.js adapter) now take precedence over build-time values statically replaced by Vite. This ensures that environment variables set at runtime behave as expected when deploying with the Astro Node adapter or similar runtime environments. diff --git a/packages/astro/src/server/__tests__/get-safe-env.test.ts b/packages/astro/src/server/__tests__/get-safe-env.test.ts new file mode 100644 index 00000000000..ff7e0301d6d --- /dev/null +++ b/packages/astro/src/server/__tests__/get-safe-env.test.ts @@ -0,0 +1,121 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { getClientSafeEnv, getSafeEnv } from '../get-safe-env'; + +function createLocals(overrides: Partial = {}): App.Locals { + return { + runtime: { env: {} as InternalEnv }, + ...overrides, + } as unknown as App.Locals; +} + +describe('getSafeEnv', () => { + beforeEach(() => { + vi.stubEnv('PUBLIC_CLERK_PUBLISHABLE_KEY', ''); + vi.stubEnv('CLERK_SECRET_KEY', ''); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('reads from locals.runtime.env first (Cloudflare)', () => { + const locals = createLocals({ + runtime: { + env: { + PUBLIC_CLERK_PUBLISHABLE_KEY: 'pk_from_runtime', + CLERK_SECRET_KEY: 'sk_from_runtime', + } as InternalEnv, + }, + }); + + // Also set process.env to verify runtime.env takes priority + process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process'; + process.env.CLERK_SECRET_KEY = 'sk_from_process'; + + const env = getSafeEnv(locals); + + expect(env.pk).toBe('pk_from_runtime'); + expect(env.sk).toBe('sk_from_runtime'); + + delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + delete process.env.CLERK_SECRET_KEY; + }); + + it('reads from process.env when runtime.env is not available', () => { + process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process'; + process.env.CLERK_SECRET_KEY = 'sk_from_process'; + + const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } }); + const env = getSafeEnv(locals); + + expect(env.pk).toBe('pk_from_process'); + expect(env.sk).toBe('sk_from_process'); + + delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + delete process.env.CLERK_SECRET_KEY; + }); + + it('returns undefined when no env source has the value', () => { + delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + delete process.env.CLERK_SECRET_KEY; + + const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } }); + const env = getSafeEnv(locals); + + expect(env.pk).toBeUndefined(); + expect(env.sk).toBeUndefined(); + }); + + it('prefers keylessPublishableKey over all env sources', () => { + process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process'; + + const locals = createLocals({ + runtime: { env: undefined as unknown as InternalEnv }, + keylessPublishableKey: 'pk_keyless', + }); + const env = getSafeEnv(locals); + + expect(env.pk).toBe('pk_keyless'); + + delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + }); +}); + +describe('getClientSafeEnv', () => { + beforeEach(() => { + vi.stubEnv('PUBLIC_CLERK_PUBLISHABLE_KEY', ''); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('reads from process.env for publishableKey', () => { + process.env.PUBLIC_CLERK_PUBLISHABLE_KEY = 'pk_from_process'; + + const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } }); + const env = getClientSafeEnv(locals); + + expect(env.publishableKey).toBe('pk_from_process'); + + delete process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + }); + + it('reads from process.env for all public env vars', () => { + process.env.PUBLIC_CLERK_DOMAIN = 'test.domain.com'; + process.env.PUBLIC_CLERK_SIGN_IN_URL = '/sign-in'; + process.env.PUBLIC_CLERK_SIGN_UP_URL = '/sign-up'; + + const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } }); + const env = getClientSafeEnv(locals); + + expect(env.domain).toBe('test.domain.com'); + expect(env.signInUrl).toBe('/sign-in'); + expect(env.signUpUrl).toBe('/sign-up'); + + delete process.env.PUBLIC_CLERK_DOMAIN; + delete process.env.PUBLIC_CLERK_SIGN_IN_URL; + delete process.env.PUBLIC_CLERK_SIGN_UP_URL; + }); +}); diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 76600aac7c0..573ddabd053 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -14,7 +14,14 @@ function getContextEnvVar(envVarName: keyof InternalEnv, contextOrLocals: Contex return locals.runtime.env[envVarName]; } - return import.meta.env[envVarName]; + // Prefer process.env for runtime environments (e.g., Node.js adapter) + // where import.meta.env.PUBLIC_* is statically replaced at build time by Vite. + // Runtime values should take precedence over build-time values. + if (typeof process !== 'undefined' && process.env?.[envVarName]) { + return process.env[envVarName]; + } + + return import.meta.env[envVarName] || undefined; } /** diff --git a/packages/upgrade/src/versions/core-3/changes/astro-env-var-precedence.md b/packages/upgrade/src/versions/core-3/changes/astro-env-var-precedence.md new file mode 100644 index 00000000000..6b2551c36e1 --- /dev/null +++ b/packages/upgrade/src/versions/core-3/changes/astro-env-var-precedence.md @@ -0,0 +1,22 @@ +--- +title: 'Runtime environment variables now take precedence over build-time values' +packages: ['astro'] +matcher: + - 'PUBLIC_CLERK_PUBLISHABLE_KEY' + - 'PUBLIC_CLERK_DOMAIN' + - 'PUBLIC_CLERK_SIGN_IN_URL' + - 'PUBLIC_CLERK_SIGN_UP_URL' + - 'import.meta.env' +category: 'behavior-change' +warning: true +--- + +Environment variable resolution in `@clerk/astro` now prefers `process.env` over `import.meta.env`. This means runtime environment variables (e.g., set in the Node.js adapter or container) take precedence over values statically replaced by Vite at build time. + +The new resolution order is: + +1. `locals.runtime.env` (Cloudflare Workers) +2. `process.env` (Node.js runtime) +3. `import.meta.env` (Vite build-time static replacement) + +Previously, `import.meta.env` was checked before `process.env`. If you rely on build-time `PUBLIC_*` values that differ from your runtime `process.env`, you may need to update your configuration to ensure the correct values are set in `process.env` at runtime.