diff --git a/.changeset/tame-parents-drive.md b/.changeset/tame-parents-drive.md new file mode 100644 index 00000000000..7b708c487dc --- /dev/null +++ b/.changeset/tame-parents-drive.md @@ -0,0 +1,5 @@ +--- +'@clerk/astro': patch +--- + +Fix `PUBLIC_CLERK_PUBLISHABLE_KEY` not readable from runtime environment when using the Astro Node adapter. Added `process.env` as a fallback in `getContextEnvVar()` for cases where `import.meta.env.PUBLIC_*` is statically replaced at build time by Vite. 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..aa2bf7aeb23 --- /dev/null +++ b/packages/astro/src/server/__tests__/get-safe-env.test.ts @@ -0,0 +1,133 @@ +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 import.meta.env when runtime.env is not available', () => { + vi.stubEnv('PUBLIC_CLERK_PUBLISHABLE_KEY', 'pk_from_meta'); + vi.stubEnv('CLERK_SECRET_KEY', 'sk_from_meta'); + + const locals = createLocals({ runtime: { env: undefined as unknown as InternalEnv } }); + const env = getSafeEnv(locals); + + expect(env.pk).toBe('pk_from_meta'); + expect(env.sk).toBe('sk_from_meta'); + }); + + it('falls back to process.env when import.meta.env has no value', () => { + 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', () => { + // Clean process.env so the fallback finds nothing + 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('falls back to 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('falls back to 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..0809926aafe 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -14,7 +14,18 @@ function getContextEnvVar(envVarName: keyof InternalEnv, contextOrLocals: Contex return locals.runtime.env[envVarName]; } - return import.meta.env[envVarName]; + const envValue = import.meta.env[envVarName]; + if (envValue) { + return envValue; + } + + // Fallback to process.env for runtime environments (e.g., Node.js adapter) + // where import.meta.env.PUBLIC_* is statically replaced at build time by Vite + if (typeof process !== 'undefined' && process.env) { + return process.env[envVarName]; + } + + return undefined; } /**