From 521f12a779e49addecc2a6bd64ba0edc023aee5e Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 19 Feb 2026 15:00:50 -0500 Subject: [PATCH 1/3] fix(astro): Fix compatibility with Astro v6 Cloudflare adapter Astro v6's Cloudflare adapter removed `locals.runtime.env`, which causes `@clerk/astro` to crash. This adds a fallback that imports env from `cloudflare:workers` when `locals.runtime.env` is unavailable, while maintaining backwards compatibility with Astro v4/v5. Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-astro-v6-cloudflare.md | 5 +++ packages/astro/package.json | 2 +- packages/astro/src/env.d.ts | 2 +- packages/astro/src/server/clerk-middleware.ts | 4 +- packages/astro/src/server/get-safe-env.ts | 44 +++++++++++++++++-- 5 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-astro-v6-cloudflare.md diff --git a/.changeset/fix-astro-v6-cloudflare.md b/.changeset/fix-astro-v6-cloudflare.md new file mode 100644 index 00000000000..da723bf6278 --- /dev/null +++ b/.changeset/fix-astro-v6-cloudflare.md @@ -0,0 +1,5 @@ +--- +'@clerk/astro': patch +--- + +Fix compatibility with Astro v6 Cloudflare adapter by using `cloudflare:workers` env when `locals.runtime.env` is unavailable diff --git a/packages/astro/package.json b/packages/astro/package.json index 629a0a770f5..72db319e94e 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -100,7 +100,7 @@ "astro": "^5.17.1" }, "peerDependencies": { - "astro": "^4.15.0 || ^5.0.0" + "astro": "^4.15.0 || ^5.0.0 || ^6.0.0" }, "engines": { "node": ">=20.9.0" diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts index 205d514c786..dc5c6d3303d 100644 --- a/packages/astro/src/env.d.ts +++ b/packages/astro/src/env.d.ts @@ -30,7 +30,7 @@ interface ImportMeta { declare namespace App { interface Locals { - runtime: { env: InternalEnv }; + runtime?: { env: InternalEnv }; keylessClaimUrl?: string; keylessApiKeysUrl?: string; keylessPublishableKey?: string; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 1583fdb5fc2..808c540aa05 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -28,7 +28,7 @@ import { canUseKeyless } from '../utils/feature-flags'; import { buildClerkHotloadScript } from './build-clerk-hotload-script'; import { clerkClient } from './clerk-client'; import { createCurrentUser } from './current-user'; -import { getClientSafeEnv, getSafeEnv } from './get-safe-env'; +import { getClientSafeEnv, getSafeEnv, initCloudflareEnv } from './get-safe-env'; import { resolveKeysWithKeylessFallback } from './keyless/utils'; import { serverRedirectWithAuth } from './server-redirect-with-auth'; import type { @@ -79,6 +79,8 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { return next(); } + await initCloudflareEnv(); + const clerkRequest = createClerkRequest(context.request); // Resolve keyless URLs per-request in development diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 76600aac7c0..d9b1dece172 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -3,6 +3,34 @@ import type { APIContext } from 'astro'; type ContextOrLocals = APIContext | APIContext['locals']; +/** + * Cached env object from `cloudflare:workers` for Astro v6+ Cloudflare adapter. + * - `undefined`: not yet attempted + * - `null`: attempted but not available (non-Cloudflare environment) + * - object: the env object from `cloudflare:workers` + */ +let cloudflareEnv: Record | null | undefined; + +/** + * @internal + * Attempts to import env from `cloudflare:workers` and caches the result. + * This is needed for Astro v6+ where `locals.runtime.env` is no longer available. + * Safe to call in non-Cloudflare environments — will no-op. + */ +async function initCloudflareEnv(): Promise { + if (cloudflareEnv !== undefined) { + return; + } + try { + // Use a variable to prevent TypeScript from resolving the module specifier + const moduleName = 'cloudflare:workers'; + const mod = await import(/* @vite-ignore */ moduleName); + cloudflareEnv = mod.env; + } catch { + cloudflareEnv = null; + } +} + /** * @internal * Isomorphic handler for reading environment variables defined from Vite or are injected in the request context (CF Pages) @@ -10,8 +38,18 @@ type ContextOrLocals = APIContext | APIContext['locals']; function getContextEnvVar(envVarName: keyof InternalEnv, contextOrLocals: ContextOrLocals): string | undefined { const locals = 'locals' in contextOrLocals ? contextOrLocals.locals : contextOrLocals; - if (locals?.runtime?.env) { - return locals.runtime.env[envVarName]; + // Astro v4/v5 Cloudflare adapter: env is on locals.runtime.env + try { + if (locals?.runtime?.env) { + return locals.runtime.env[envVarName]; + } + } catch { + // Astro v6 Cloudflare adapter throws when accessing locals.runtime.env + } + + // Astro v6 Cloudflare adapter: env from cloudflare:workers + if (cloudflareEnv) { + return cloudflareEnv[envVarName]; } return import.meta.env[envVarName]; @@ -72,4 +110,4 @@ function getClientSafeEnv(context: ContextOrLocals) { }; } -export { getSafeEnv, getClientSafeEnv }; +export { getSafeEnv, getClientSafeEnv, initCloudflareEnv }; From 467fcc4e7aebe022df663079db9a2bed2791b289 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 19 Feb 2026 15:15:19 -0500 Subject: [PATCH 2/3] Create get-safe-env.test.ts --- .../src/server/__tests__/get-safe-env.test.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 packages/astro/src/server/__tests__/get-safe-env.test.ts 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..0ebb2d56901 --- /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'; + +type GetSafeEnvModule = typeof import('../get-safe-env'); + +describe('get-safe-env', () => { + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('initCloudflareEnv', () => { + it('caches env from cloudflare:workers when available', async () => { + vi.doMock('cloudflare:workers', () => ({ + env: { CLERK_SECRET_KEY: 'sk_test_cf' }, + })); + + const { initCloudflareEnv, getSafeEnv } = await import('../get-safe-env'); + + await initCloudflareEnv(); + + const env = getSafeEnv({ locals: {} } as any); + expect(env.sk).toBe('sk_test_cf'); + }); + + it('sets cache to null when cloudflare:workers is not available', async () => { + vi.doMock('cloudflare:workers', () => { + throw new Error('Module not found'); + }); + + const { initCloudflareEnv, getSafeEnv } = await import('../get-safe-env'); + + await initCloudflareEnv(); + + // Should fall through to import.meta.env (undefined in test) + const env = getSafeEnv({ locals: {} } as any); + expect(env.sk).toBeUndefined(); + }); + + it('only imports once (caches result)', async () => { + let importCount = 0; + vi.doMock('cloudflare:workers', () => { + importCount++; + return { env: { CLERK_SECRET_KEY: 'sk_test_cf' } }; + }); + + const { initCloudflareEnv } = await import('../get-safe-env'); + + await initCloudflareEnv(); + await initCloudflareEnv(); + await initCloudflareEnv(); + + expect(importCount).toBe(1); + }); + + it('only imports once even when cloudflare:workers throws', async () => { + let importCount = 0; + vi.doMock('cloudflare:workers', () => { + importCount++; + throw new Error('Module not found'); + }); + + const { initCloudflareEnv } = await import('../get-safe-env'); + + await initCloudflareEnv(); + await initCloudflareEnv(); + + expect(importCount).toBe(1); + }); + }); + + describe('getContextEnvVar fallback chain', () => { + it('reads from locals.runtime.env (Astro v4/v5)', async () => { + const { getSafeEnv } = await import('../get-safe-env'); + const locals = { runtime: { env: { CLERK_SECRET_KEY: 'sk_from_runtime' } } }; + + const env = getSafeEnv({ locals } as any); + expect(env.sk).toBe('sk_from_runtime'); + }); + + it('falls back to cloudflareEnv when locals.runtime.env is absent', async () => { + vi.doMock('cloudflare:workers', () => ({ + env: { CLERK_SECRET_KEY: 'sk_from_cf_workers' }, + })); + + const { initCloudflareEnv, getSafeEnv } = await import('../get-safe-env'); + await initCloudflareEnv(); + + const env = getSafeEnv({ locals: {} } as any); + expect(env.sk).toBe('sk_from_cf_workers'); + }); + + it('falls back to cloudflareEnv when locals.runtime throws (Astro v6)', async () => { + vi.doMock('cloudflare:workers', () => ({ + env: { CLERK_SECRET_KEY: 'sk_from_cf_workers' }, + })); + + const { initCloudflareEnv, getSafeEnv } = await import('../get-safe-env'); + await initCloudflareEnv(); + + // Simulate Astro v6 behavior: accessing runtime throws + const locals = new Proxy( + {}, + { + get(_, prop) { + if (prop === 'runtime') { + throw new Error('locals.runtime is not available in Astro v6 Cloudflare'); + } + return undefined; + }, + }, + ); + + const env = getSafeEnv({ locals } as any); + expect(env.sk).toBe('sk_from_cf_workers'); + }); + + it('prefers locals.runtime.env over cloudflareEnv', async () => { + vi.doMock('cloudflare:workers', () => ({ + env: { CLERK_SECRET_KEY: 'sk_from_cf_workers' }, + })); + + const { initCloudflareEnv, getSafeEnv } = await import('../get-safe-env'); + await initCloudflareEnv(); + + const locals = { runtime: { env: { CLERK_SECRET_KEY: 'sk_from_runtime' } } }; + const env = getSafeEnv({ locals } as any); + expect(env.sk).toBe('sk_from_runtime'); + }); + }); +}); From 3c68b4da399d1d4299b8058df02f44361d5f5a8a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 19 Feb 2026 15:32:26 -0500 Subject: [PATCH 3/3] Update get-safe-env.test.ts --- packages/astro/src/server/__tests__/get-safe-env.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/astro/src/server/__tests__/get-safe-env.test.ts b/packages/astro/src/server/__tests__/get-safe-env.test.ts index 0ebb2d56901..4a710a7d41b 100644 --- a/packages/astro/src/server/__tests__/get-safe-env.test.ts +++ b/packages/astro/src/server/__tests__/get-safe-env.test.ts @@ -1,7 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -type GetSafeEnvModule = typeof import('../get-safe-env'); - describe('get-safe-env', () => { beforeEach(() => { vi.resetModules();