From 5995193e42ac0f0f8ba54c5be8769f20b52ef12f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 19:28:59 -0800 Subject: [PATCH 01/13] feat(astro): Add support for keyless mode --- packages/astro/src/env.d.ts | 3 + .../src/integration/create-integration.ts | 45 ++++++++++-- .../src/internal/create-clerk-instance.ts | 5 ++ .../internal/merge-env-vars-with-params.ts | 2 + packages/astro/src/server/get-safe-env.ts | 4 ++ .../astro/src/server/keyless/file-storage.ts | 20 ++++++ packages/astro/src/server/keyless/index.ts | 51 ++++++++++++++ packages/astro/src/server/keyless/utils.ts | 68 +++++++++++++++++++ packages/astro/src/utils/feature-flags.ts | 17 +++++ 9 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 packages/astro/src/server/keyless/file-storage.ts create mode 100644 packages/astro/src/server/keyless/index.ts create mode 100644 packages/astro/src/server/keyless/utils.ts create mode 100644 packages/astro/src/utils/feature-flags.ts diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts index 1240941e1e2..ee31ce4b0e6 100644 --- a/packages/astro/src/env.d.ts +++ b/packages/astro/src/env.d.ts @@ -20,6 +20,9 @@ interface InternalEnv { readonly PUBLIC_CLERK_SIGN_UP_URL?: string; readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string; readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string; + readonly PUBLIC_CLERK_KEYLESS_CLAIM_URL?: string; + readonly PUBLIC_CLERK_KEYLESS_API_KEYS_URL?: string; + readonly PUBLIC_CLERK_KEYLESS_DISABLED?: string; } interface ImportMeta { diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index fc19a8371c3..cd0d8621733 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -3,6 +3,7 @@ import type { AstroIntegration } from 'astro'; import { envField } from 'astro/config'; import { name as packageName, version as packageVersion } from '../../package.json'; +import { resolveKeysWithKeylessFallback } from '../server/keyless/utils'; import type { AstroClerkIntegrationParams } from '../types'; import { vitePluginAstroConfig } from './vite-plugin-astro-config'; @@ -26,17 +27,36 @@ function createIntegration() return { name: '@clerk/astro/integration', hooks: { - 'astro:config:setup': ({ config, injectScript, updateConfig, logger, command }) => { + 'astro:config:setup': async ({ config, injectScript, updateConfig, logger, command }) => { if (['server', 'hybrid'].includes(config.output) && !config.adapter) { logger.error('Missing adapter, please update your Astro config to use one.'); } + const envPublishableKey = process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + const envSecretKey = process.env.CLERK_SECRET_KEY; + + const isDev = command === 'dev'; + let resolvedKeys = { + publishableKey: envPublishableKey, + secretKey: envSecretKey, + claimUrl: undefined as string | undefined, + apiKeysUrl: undefined as string | undefined, + }; + + if (isDev) { + try { + resolvedKeys = await resolveKeysWithKeylessFallback(envPublishableKey, envSecretKey); + } catch { + logger.warn('Keyless mode initialization failed, using configured keys'); + } + } + const internalParams: ClerkOptions = { ...params, sdkMetadata: { version: packageVersion, name: packageName, - environment: command === 'dev' ? 'development' : 'production', + environment: isDev ? 'development' : 'production', }, }; @@ -58,6 +78,10 @@ function createIntegration() ...buildEnvVarFromOption(clerkJSUrl, 'PUBLIC_CLERK_JS_URL'), ...buildEnvVarFromOption(clerkJSVersion, 'PUBLIC_CLERK_JS_VERSION'), ...buildEnvVarFromOption(prefetchUI === false ? 'false' : undefined, 'PUBLIC_CLERK_PREFETCH_UI'), + ...buildEnvVarFromOption(resolvedKeys.publishableKey, 'PUBLIC_CLERK_PUBLISHABLE_KEY'), + ...buildEnvVarFromOption(resolvedKeys.secretKey, 'CLERK_SECRET_KEY'), + ...buildEnvVarFromOption(resolvedKeys.claimUrl, 'PUBLIC_CLERK_KEYLESS_CLAIM_URL'), + ...buildEnvVarFromOption(resolvedKeys.apiKeysUrl, 'PUBLIC_CLERK_KEYLESS_API_KEYS_URL'), }, ssr: { @@ -157,7 +181,7 @@ function createIntegration() function createClerkEnvSchema() { return { - PUBLIC_CLERK_PUBLISHABLE_KEY: envField.string({ context: 'client', access: 'public' }), + PUBLIC_CLERK_PUBLISHABLE_KEY: envField.string({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_SIGN_IN_URL: envField.string({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_SIGN_UP_URL: envField.string({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_IS_SATELLITE: envField.boolean({ context: 'client', access: 'public', optional: true }), @@ -169,7 +193,20 @@ function createClerkEnvSchema() { PUBLIC_CLERK_UI_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }), PUBLIC_CLERK_TELEMETRY_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_TELEMETRY_DEBUG: envField.boolean({ context: 'client', access: 'public', optional: true }), - CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret' }), + PUBLIC_CLERK_KEYLESS_CLAIM_URL: envField.string({ + context: 'client', + access: 'public', + optional: true, + url: true, + }), + PUBLIC_CLERK_KEYLESS_API_KEYS_URL: envField.string({ + context: 'client', + access: 'public', + optional: true, + url: true, + }), + PUBLIC_CLERK_KEYLESS_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }), + CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), CLERK_MACHINE_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), CLERK_JWT_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), }; diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index 675e405382c..0eb4a71b582 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -54,12 +54,17 @@ async function createClerkInstanceInternal(options?: AstroC $clerk.set(clerkJSInstance); } + const keylessClaimUrl = (options as any)?.__internal_keylessClaimUrl; + const keylessApiKeysUrl = (options as any)?.__internal_keylessApiKeysUrl; + const clerkOptions = { routerPush: createNavigationHandler(window.history.pushState.bind(window.history)), routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)), ...options, // Pass the clerk-ui constructor promise to clerk.load() clerkUICtor, + ...(keylessClaimUrl && { __internal_keyless_claimKeylessApplicationUrl: keylessClaimUrl }), + ...(keylessApiKeysUrl && { __internal_keyless_copyInstanceKeysUrl: keylessApiKeysUrl }), } as unknown as ClerkOptions; initOptions = clerkOptions; diff --git a/packages/astro/src/internal/merge-env-vars-with-params.ts b/packages/astro/src/internal/merge-env-vars-with-params.ts index 1cac532cc6f..30df74de785 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -56,6 +56,8 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish disabled: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DISABLED), debug: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DEBUG), }, + __internal_keylessClaimUrl: import.meta.env.PUBLIC_CLERK_KEYLESS_CLAIM_URL, + __internal_keylessApiKeysUrl: import.meta.env.PUBLIC_CLERK_KEYLESS_API_KEYS_URL, ...rest, }; }; diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 9d17c86321d..66c498cb683 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -38,6 +38,8 @@ function getSafeEnv(context: ContextOrLocals) { apiUrl: getContextEnvVar('CLERK_API_URL', context), telemetryDisabled: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DISABLED', context)), telemetryDebug: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DEBUG', context)), + keylessClaimUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_CLAIM_URL', context), + keylessApiKeysUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_API_KEYS_URL', context), }; } @@ -55,6 +57,8 @@ function getClientSafeEnv(context: ContextOrLocals) { proxyUrl: getContextEnvVar('PUBLIC_CLERK_PROXY_URL', context), signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context), signUpUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_UP_URL', context), + keylessClaimUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_CLAIM_URL', context), + keylessApiKeysUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_API_KEYS_URL', context), }; } diff --git a/packages/astro/src/server/keyless/file-storage.ts b/packages/astro/src/server/keyless/file-storage.ts new file mode 100644 index 00000000000..0df8973418d --- /dev/null +++ b/packages/astro/src/server/keyless/file-storage.ts @@ -0,0 +1,20 @@ +import type { KeylessStorage } from '@clerk/shared/keyless'; + +export type { KeylessStorage }; + +export interface FileStorageOptions { + cwd?: () => string; +} + +export async function createFileStorage(options: FileStorageOptions = {}): Promise { + const { cwd = () => process.cwd() } = options; + + const [{ default: fs }, { default: path }] = await Promise.all([import('node:fs'), import('node:path')]); + + const { createNodeFileStorage } = await import('@clerk/shared/keyless'); + + return createNodeFileStorage(fs, path, { + cwd, + frameworkPackageName: '@clerk/astro', + }); +} diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts new file mode 100644 index 00000000000..4fd22255a7c --- /dev/null +++ b/packages/astro/src/server/keyless/index.ts @@ -0,0 +1,51 @@ +import { createKeylessService } from '@clerk/shared/keyless'; +import type { APIContext } from 'astro'; + +import { clerkClient } from '../clerk-client'; +import { createFileStorage } from './file-storage.js'; + +let keylessServiceInstance: ReturnType | null = null; + +export async function keyless() { + if (!keylessServiceInstance) { + const storage = await createFileStorage(); + + keylessServiceInstance = createKeylessService({ + storage, + api: { + async createAccountlessApplication(requestHeaders?: Headers) { + try { + const mockContext = { + locals: { runtime: { env: {} } }, + } as unknown as APIContext; + + return await clerkClient(mockContext).__experimental_accountlessApplications.createAccountlessApplication({ + requestHeaders, + }); + } catch { + return null; + } + }, + async completeOnboarding(requestHeaders?: Headers) { + try { + const mockContext = { + locals: { runtime: { env: {} } }, + } as unknown as APIContext; + + return await clerkClient( + mockContext, + ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: '@clerk/astro', + frameworkVersion: PACKAGE_VERSION, + }); + } + + return keylessServiceInstance; +} diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts new file mode 100644 index 00000000000..59e09b4daea --- /dev/null +++ b/packages/astro/src/server/keyless/utils.ts @@ -0,0 +1,68 @@ +import type { AccountlessApplication } from '@clerk/shared/keyless'; +import { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } from '@clerk/shared/keyless'; + +import { canUseKeyless } from '../../utils/feature-flags'; +import { keyless } from './index'; + +export interface KeylessResult { + publishableKey: string | undefined; + secretKey: string | undefined; + claimUrl: string | undefined; + apiKeysUrl: string | undefined; +} + +export async function resolveKeysWithKeylessFallback( + configuredPublishableKey: string | undefined, + configuredSecretKey: string | undefined, +): Promise { + let publishableKey = configuredPublishableKey; + let secretKey = configuredSecretKey; + let claimUrl: string | undefined; + let apiKeysUrl: string | undefined; + + if (!canUseKeyless) { + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; + } + + const keylessService = await keyless(); + const locallyStoredKeys = keylessService.readKeys(); + + const runningWithClaimedKeys = + Boolean(configuredPublishableKey) && configuredPublishableKey === locallyStoredKeys?.publishableKey; + + if (runningWithClaimedKeys && locallyStoredKeys) { + try { + await clerkDevelopmentCache?.run(() => keylessService.completeOnboarding(), { + cacheKey: `${locallyStoredKeys.publishableKey}_complete`, + onSuccessStale: 24 * 60 * 60 * 1000, + }); + } catch { + // noop + } + + clerkDevelopmentCache?.log({ + cacheKey: `${locallyStoredKeys.publishableKey}_claimed`, + msg: createConfirmationMessage(), + }); + + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; + } + + if (!publishableKey && !secretKey) { + const keylessApp: AccountlessApplication | null = await keylessService.getOrCreateKeys(); + + if (keylessApp) { + publishableKey = keylessApp.publishableKey; + secretKey = keylessApp.secretKey; + claimUrl = keylessApp.claimUrl; + apiKeysUrl = keylessApp.apiKeysUrl; + + clerkDevelopmentCache?.log({ + cacheKey: keylessApp.publishableKey, + msg: createKeylessModeMessage(keylessApp), + }); + } + } + + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; +} diff --git a/packages/astro/src/utils/feature-flags.ts b/packages/astro/src/utils/feature-flags.ts new file mode 100644 index 00000000000..e32d8dc6577 --- /dev/null +++ b/packages/astro/src/utils/feature-flags.ts @@ -0,0 +1,17 @@ +import { isTruthy } from '@clerk/shared/underscore'; +import { isDevelopmentEnvironment } from '@clerk/shared/utils'; + +function hasFileSystemSupport(): boolean { + if (typeof process === 'undefined' || !process?.versions?.node) { + return false; + } + if (typeof window !== 'undefined') { + return false; + } + return true; +} + +const KEYLESS_DISABLED = + isTruthy(import.meta.env.PUBLIC_CLERK_KEYLESS_DISABLED) || isTruthy(import.meta.env.CLERK_KEYLESS_DISABLED) || false; + +export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED && hasFileSystemSupport(); From da2bc6f80906fc0388c1e3b3c89de212ab2b1538 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 19:39:44 -0800 Subject: [PATCH 02/13] chore: use process.env in config phase --- integration/tests/astro/keyless.test.ts | 115 ++++++++++++++++++++++ packages/astro/src/utils/feature-flags.ts | 2 +- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 integration/tests/astro/keyless.test.ts diff --git a/integration/tests/astro/keyless.test.ts b/integration/tests/astro/keyless.test.ts new file mode 100644 index 00000000000..85cd4e0c17b --- /dev/null +++ b/integration/tests/astro/keyless.test.ts @@ -0,0 +1,115 @@ +import { expect, test } from '@playwright/test'; + +import type { Application } from '../../models/application'; +import { appConfigs } from '../../presets'; +import { createTestUtils } from '../../testUtils'; +import { mockClaimedInstanceEnvironmentCall } from '../../testUtils/keylessHelpers'; + +const commonSetup = appConfigs.astro.node.clone(); + +test.describe('Keyless mode @astro', () => { + test.describe.configure({ mode: 'serial' }); + test.setTimeout(90_000); + + test.use({ + extraHTTPHeaders: { + 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET || '', + }, + }); + + let app: Application; + let dashboardUrl = 'https://dashboard.clerk.com/'; + + test.beforeAll(async () => { + app = await commonSetup.commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withKeyless); + if (appConfigs.envs.withKeyless.privateVariables.get('CLERK_API_URL')?.includes('clerkstage')) { + dashboardUrl = 'https://dashboard.clerkstage.dev/'; + } + await app.dev(); + }); + + test.afterAll(async () => { + await app?.teardown(); + }); + + test('Toggle collapse popover and claim.', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToAppHome(); + await u.page.waitForClerkJsLoaded(); + await u.po.expect.toBeSignedOut(); + + await u.po.keylessPopover.waitForMounted(); + + expect(await u.po.keylessPopover.isExpanded()).toBe(false); + await u.po.keylessPopover.toggle(); + expect(await u.po.keylessPopover.isExpanded()).toBe(true); + + const claim = await u.po.keylessPopover.promptsToClaim(); + + const [newPage] = await Promise.all([context.waitForEvent('page'), claim.click()]); + + await newPage.waitForLoadState(); + + await newPage.waitForURL(url => { + const urlToReturnTo = `${dashboardUrl}apps/claim?token=`; + + const signUpForceRedirectUrl = url.searchParams.get('sign_up_force_redirect_url'); + + const signUpForceRedirectUrlCheck = + signUpForceRedirectUrl?.startsWith(urlToReturnTo) || + (signUpForceRedirectUrl?.startsWith(`${dashboardUrl}prepare-account`) && + signUpForceRedirectUrl?.includes(encodeURIComponent('apps/claim?token='))); + + return ( + url.pathname === '/apps/claim/sign-in' && + url.searchParams.get('sign_in_force_redirect_url')?.startsWith(urlToReturnTo) && + signUpForceRedirectUrlCheck + ); + }); + }); + + test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({ + page, + context, + }) => { + await mockClaimedInstanceEnvironmentCall(page); + const u = createTestUtils({ app, page, context }); + await u.page.goToAppHome(); + await u.page.waitForClerkJsLoaded(); + + await u.po.keylessPopover.waitForMounted(); + expect(await u.po.keylessPopover.isExpanded()).toBe(true); + await expect(u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible(); + + const [newPage] = await Promise.all([ + context.waitForEvent('page'), + u.po.keylessPopover.promptToUseClaimedKeys().click(), + ]); + + await newPage.waitForLoadState(); + await newPage.waitForURL(url => { + return url.href.startsWith(`${dashboardUrl}sign-in?redirect_url=${encodeURIComponent(dashboardUrl)}apps%2Fapp_`); + }); + }); + + test('Keyless popover is removed after adding keys to .env and restarting.', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToAppHome(); + + await u.po.keylessPopover.waitForMounted(); + expect(await u.po.keylessPopover.isExpanded()).toBe(false); + + // Copy keys from keyless.json to .env + await app.keylessToEnv(); + + // Restart the dev server to pick up new env vars (Astro doesn't hot-reload .env) + await app.restart(); + + await u.page.goToAppHome(); + + // Keyless popover should no longer be present since we now have explicit keys + await u.po.keylessPopover.waitForUnmounted(); + }); +}); diff --git a/packages/astro/src/utils/feature-flags.ts b/packages/astro/src/utils/feature-flags.ts index e32d8dc6577..70856d6609e 100644 --- a/packages/astro/src/utils/feature-flags.ts +++ b/packages/astro/src/utils/feature-flags.ts @@ -12,6 +12,6 @@ function hasFileSystemSupport(): boolean { } const KEYLESS_DISABLED = - isTruthy(import.meta.env.PUBLIC_CLERK_KEYLESS_DISABLED) || isTruthy(import.meta.env.CLERK_KEYLESS_DISABLED) || false; + isTruthy(process.env.PUBLIC_CLERK_KEYLESS_DISABLED) || isTruthy(process.env.CLERK_KEYLESS_DISABLED) || false; export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED && hasFileSystemSupport(); From 69a311944f312c4205b16cf848a9a66a8debb075 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 19:41:46 -0800 Subject: [PATCH 03/13] fix missing test helper --- integration/testUtils/keylessHelpers.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 integration/testUtils/keylessHelpers.ts diff --git a/integration/testUtils/keylessHelpers.ts b/integration/testUtils/keylessHelpers.ts new file mode 100644 index 00000000000..4c941e64f62 --- /dev/null +++ b/integration/testUtils/keylessHelpers.ts @@ -0,0 +1,20 @@ +import type { Page } from '@playwright/test'; + +/** + * Mocks the environment API call to return a claimed instance. + * Used in keyless mode tests to simulate an instance that has been claimed. + */ +export const mockClaimedInstanceEnvironmentCall = async (page: Page): Promise => { + await page.route('*/**/v1/environment*', async route => { + const response = await route.fetch(); + const json = await response.json(); + const newJson = { + ...json, + auth_config: { + ...json.auth_config, + claimed_at: Date.now(), + }, + }; + await route.fulfill({ response, json: newJson }); + }); +}; From 4d0ee39f901908cfc8ac2c4faee1ac8e28050ea4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 19:54:59 -0800 Subject: [PATCH 04/13] fix dev mode --- .../src/integration/create-integration.ts | 14 ++++++++-- packages/astro/src/server/keyless/index.ts | 26 ++++++++++--------- packages/astro/src/server/keyless/utils.ts | 3 ++- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index cd0d8621733..5e9df762a49 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -45,12 +45,22 @@ function createIntegration() if (isDev) { try { - resolvedKeys = await resolveKeysWithKeylessFallback(envPublishableKey, envSecretKey); - } catch { + resolvedKeys = await resolveKeysWithKeylessFallback(envPublishableKey, envSecretKey, isDev); + if (resolvedKeys.publishableKey) { + logger.info(`Clerk: Using ${resolvedKeys.claimUrl ? 'keyless' : 'configured'} keys`); + } + } catch (error) { logger.warn('Keyless mode initialization failed, using configured keys'); + logger.debug(`Keyless error: ${error}`); } } + if (!resolvedKeys.publishableKey && !resolvedKeys.secretKey) { + logger.error( + 'Missing Clerk keys. Set PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY environment variables, or let keyless mode generate them automatically.', + ); + } + const internalParams: ClerkOptions = { ...params, sdkMetadata: { diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index 4fd22255a7c..202b264e245 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -10,16 +10,20 @@ export async function keyless() { if (!keylessServiceInstance) { const storage = await createFileStorage(); + const mockContext = { + locals: { runtime: { env: {} } }, + } as unknown as APIContext; + keylessServiceInstance = createKeylessService({ storage, api: { async createAccountlessApplication(requestHeaders?: Headers) { try { - const mockContext = { - locals: { runtime: { env: {} } }, - } as unknown as APIContext; - - return await clerkClient(mockContext).__experimental_accountlessApplications.createAccountlessApplication({ + return await clerkClient(mockContext, { + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + }).__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, }); } catch { @@ -28,13 +32,11 @@ export async function keyless() { }, async completeOnboarding(requestHeaders?: Headers) { try { - const mockContext = { - locals: { runtime: { env: {} } }, - } as unknown as APIContext; - - return await clerkClient( - mockContext, - ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + return await clerkClient(mockContext, { + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + }).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, }); } catch { diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index 59e09b4daea..d272a5316ec 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -14,13 +14,14 @@ export interface KeylessResult { export async function resolveKeysWithKeylessFallback( configuredPublishableKey: string | undefined, configuredSecretKey: string | undefined, + isDev: boolean = false, ): Promise { let publishableKey = configuredPublishableKey; let secretKey = configuredSecretKey; let claimUrl: string | undefined; let apiKeysUrl: string | undefined; - if (!canUseKeyless) { + if (!isDev || !canUseKeyless) { return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } From 490699de84afd97ce2404ef42f98a521bce20859 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 20:23:43 -0800 Subject: [PATCH 05/13] fix types --- packages/astro/src/server/keyless/index.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index 202b264e245..e26335a9901 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -1,7 +1,6 @@ +import { createClerkClient } from '@clerk/backend'; import { createKeylessService } from '@clerk/shared/keyless'; -import type { APIContext } from 'astro'; -import { clerkClient } from '../clerk-client'; import { createFileStorage } from './file-storage.js'; let keylessServiceInstance: ReturnType | null = null; @@ -10,20 +9,17 @@ export async function keyless() { if (!keylessServiceInstance) { const storage = await createFileStorage(); - const mockContext = { - locals: { runtime: { env: {} } }, - } as unknown as APIContext; - keylessServiceInstance = createKeylessService({ storage, api: { async createAccountlessApplication(requestHeaders?: Headers) { try { - return await clerkClient(mockContext, { + const client = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, apiUrl: process.env.CLERK_API_URL, - }).__experimental_accountlessApplications.createAccountlessApplication({ + }); + return await client.__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, }); } catch { @@ -32,11 +28,12 @@ export async function keyless() { }, async completeOnboarding(requestHeaders?: Headers) { try { - return await clerkClient(mockContext, { + const client = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, apiUrl: process.env.CLERK_API_URL, - }).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + }); + return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, }); } catch { From e1bf0ac9015e7677a5b3271e208a8b6b15754539 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 20:59:28 -0800 Subject: [PATCH 06/13] fix: proper cache invalidation when switching between keyless and configured modes --- packages/astro/src/integration/create-integration.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 5e9df762a49..8831721a957 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -8,7 +8,10 @@ import type { AstroClerkIntegrationParams } from '../types'; import { vitePluginAstroConfig } from './vite-plugin-astro-config'; const buildEnvVarFromOption = (valueToBeStored: unknown, envName: keyof InternalEnv) => { - return valueToBeStored ? { [`import.meta.env.${envName}`]: JSON.stringify(valueToBeStored) } : {}; + // Always return a value to ensure Vite properly replaces previous definitions + // For undefined values, use the literal 'undefined' identifier instead of a string + const value = valueToBeStored !== undefined ? JSON.stringify(valueToBeStored) : 'undefined'; + return { [`import.meta.env.${envName}`]: value }; }; type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & { From f7d0ce921256a75f2be51d8ae278638edebaa4a6 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 21:15:59 -0800 Subject: [PATCH 07/13] debug logging --- .../src/integration/create-integration.ts | 5 +-- packages/astro/src/server/keyless/utils.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 8831721a957..5e9df762a49 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -8,10 +8,7 @@ import type { AstroClerkIntegrationParams } from '../types'; import { vitePluginAstroConfig } from './vite-plugin-astro-config'; const buildEnvVarFromOption = (valueToBeStored: unknown, envName: keyof InternalEnv) => { - // Always return a value to ensure Vite properly replaces previous definitions - // For undefined values, use the literal 'undefined' identifier instead of a string - const value = valueToBeStored !== undefined ? JSON.stringify(valueToBeStored) : 'undefined'; - return { [`import.meta.env.${envName}`]: value }; + return valueToBeStored ? { [`import.meta.env.${envName}`]: JSON.stringify(valueToBeStored) } : {}; }; type HotloadAstroClerkIntegrationParams = AstroClerkIntegrationParams & { diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index d272a5316ec..9d64b414dc3 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -21,16 +21,37 @@ export async function resolveKeysWithKeylessFallback( let claimUrl: string | undefined; let apiKeysUrl: string | undefined; + console.log('[Keyless Debug] Input:', { + hasPublishableKey: Boolean(configuredPublishableKey), + hasSecretKey: Boolean(configuredSecretKey), + isDev, + canUseKeyless, + }); + if (!isDev || !canUseKeyless) { + console.log('[Keyless Debug] Early return - not dev or keyless disabled'); + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; + } + + // If both keys are explicitly configured, skip all keyless logic + if (publishableKey && secretKey) { + console.log('[Keyless Debug] Both keys configured, skipping keyless'); return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } const keylessService = await keyless(); const locallyStoredKeys = keylessService.readKeys(); + console.log('[Keyless Debug] Stored keys:', { + hasStoredKeys: Boolean(locallyStoredKeys), + storedPublishableKey: locallyStoredKeys?.publishableKey?.substring(0, 20) + '...', + }); + const runningWithClaimedKeys = Boolean(configuredPublishableKey) && configuredPublishableKey === locallyStoredKeys?.publishableKey; + console.log('[Keyless Debug] Running with claimed keys:', runningWithClaimedKeys); + if (runningWithClaimedKeys && locallyStoredKeys) { try { await clerkDevelopmentCache?.run(() => keylessService.completeOnboarding(), { @@ -46,10 +67,14 @@ export async function resolveKeysWithKeylessFallback( msg: createConfirmationMessage(), }); + console.log('[Keyless Debug] Returning claimed keys (no keyless URLs)'); return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } + console.log('[Keyless Debug] Checking if need to create keyless app'); + if (!publishableKey && !secretKey) { + console.log('[Keyless Debug] Creating keyless app (no keys configured)'); const keylessApp: AccountlessApplication | null = await keylessService.getOrCreateKeys(); if (keylessApp) { @@ -58,12 +83,23 @@ export async function resolveKeysWithKeylessFallback( claimUrl = keylessApp.claimUrl; apiKeysUrl = keylessApp.apiKeysUrl; + console.log('[Keyless Debug] Keyless app created/retrieved with URLs'); + clerkDevelopmentCache?.log({ cacheKey: keylessApp.publishableKey, msg: createKeylessModeMessage(keylessApp), }); } + } else { + console.log('[Keyless Debug] Skipping keyless app creation (keys already configured)'); } + console.log('[Keyless Debug] Final return:', { + hasPublishableKey: Boolean(publishableKey), + hasSecretKey: Boolean(secretKey), + hasClaimUrl: Boolean(claimUrl), + hasApiKeysUrl: Boolean(apiKeysUrl), + }); + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } From d3a360a7c28d7e892e24a0fcd2f60a6859234e8e Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 21:29:09 -0800 Subject: [PATCH 08/13] use loadenv --- packages/astro/package.json | 3 ++- .../src/integration/create-integration.ts | 21 ++++++++++++++++--- packages/astro/tsup.config.ts | 10 ++++++++- pnpm-lock.yaml | 3 +++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index 24a5fd9641b..74e6c4422b8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -96,7 +96,8 @@ }, "devDependencies": { "@clerk/ui": "workspace:^", - "astro": "^5.15.9" + "astro": "^5.15.9", + "vite": "^7.1.0" }, "peerDependencies": { "astro": "^4.15.0 || ^5.0.0" diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 5e9df762a49..1054f9ca1ba 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -1,6 +1,7 @@ import type { ClerkOptions } from '@clerk/shared/types'; import type { AstroIntegration } from 'astro'; import { envField } from 'astro/config'; +import { loadEnv } from 'vite'; import { name as packageName, version as packageVersion } from '../../package.json'; import { resolveKeysWithKeylessFallback } from '../server/keyless/utils'; @@ -32,10 +33,24 @@ function createIntegration() logger.error('Missing adapter, please update your Astro config to use one.'); } - const envPublishableKey = process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; - const envSecretKey = process.env.CLERK_SECRET_KEY; - const isDev = command === 'dev'; + + // Load environment variables from .env files + // Astro's integration hook runs before .env is loaded, so we need to do it manually + const mode = isDev ? 'development' : 'production'; + const envDir = config.root.pathname; + const loadedEnv = loadEnv(mode, envDir, ''); + + console.log('[Integration Debug] Loaded env from .env files:', { + PUBLIC_CLERK_PUBLISHABLE_KEY: loadedEnv.PUBLIC_CLERK_PUBLISHABLE_KEY?.substring(0, 20) + '...', + CLERK_SECRET_KEY: loadedEnv.CLERK_SECRET_KEY?.substring(0, 20) + '...', + allClerkKeys: Object.keys(loadedEnv).filter(k => k.includes('CLERK')), + }); + + // Try .env files first, then fall back to process.env + const envPublishableKey = loadedEnv.PUBLIC_CLERK_PUBLISHABLE_KEY || process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + const envSecretKey = loadedEnv.CLERK_SECRET_KEY || process.env.CLERK_SECRET_KEY; + let resolvedKeys = { publishableKey: envPublishableKey, secretKey: envSecretKey, diff --git a/packages/astro/tsup.config.ts b/packages/astro/tsup.config.ts index ade6b396caa..80849d46a6c 100644 --- a/packages/astro/tsup.config.ts +++ b/packages/astro/tsup.config.ts @@ -26,6 +26,14 @@ export default defineConfig(() => { bundle: true, sourcemap: true, format: ['esm'], - external: ['astro', 'react', 'react-dom', 'node:async_hooks', '#async-local-storage', 'astro:transitions/client'], + external: [ + 'astro', + 'react', + 'react-dom', + 'node:async_hooks', + '#async-local-storage', + 'astro:transitions/client', + 'vite', + ], }; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adb035f97a6..4a28034e8b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -388,6 +388,9 @@ importers: astro: specifier: ^5.15.9 version: 5.17.1(@types/node@22.19.0)(db0@0.3.4)(idb-keyval@6.2.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.1)(terser@5.44.1)(tsx@4.20.6)(typescript@5.8.3)(yaml@2.8.1) + vite: + specifier: ^7.1.0 + version: 7.2.2(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) packages/backend: dependencies: From fd376a2c6aac7b77b43726f25fa3b0c74e82e437 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 21:46:01 -0800 Subject: [PATCH 09/13] chore: clean up --- .../src/integration/create-integration.ts | 6 ---- packages/astro/src/server/keyless/utils.ts | 31 ------------------- 2 files changed, 37 deletions(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 1054f9ca1ba..f9b1f0e1ca1 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -41,12 +41,6 @@ function createIntegration() const envDir = config.root.pathname; const loadedEnv = loadEnv(mode, envDir, ''); - console.log('[Integration Debug] Loaded env from .env files:', { - PUBLIC_CLERK_PUBLISHABLE_KEY: loadedEnv.PUBLIC_CLERK_PUBLISHABLE_KEY?.substring(0, 20) + '...', - CLERK_SECRET_KEY: loadedEnv.CLERK_SECRET_KEY?.substring(0, 20) + '...', - allClerkKeys: Object.keys(loadedEnv).filter(k => k.includes('CLERK')), - }); - // Try .env files first, then fall back to process.env const envPublishableKey = loadedEnv.PUBLIC_CLERK_PUBLISHABLE_KEY || process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; const envSecretKey = loadedEnv.CLERK_SECRET_KEY || process.env.CLERK_SECRET_KEY; diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index 9d64b414dc3..0c2c6951431 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -21,37 +21,21 @@ export async function resolveKeysWithKeylessFallback( let claimUrl: string | undefined; let apiKeysUrl: string | undefined; - console.log('[Keyless Debug] Input:', { - hasPublishableKey: Boolean(configuredPublishableKey), - hasSecretKey: Boolean(configuredSecretKey), - isDev, - canUseKeyless, - }); - if (!isDev || !canUseKeyless) { - console.log('[Keyless Debug] Early return - not dev or keyless disabled'); return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } // If both keys are explicitly configured, skip all keyless logic if (publishableKey && secretKey) { - console.log('[Keyless Debug] Both keys configured, skipping keyless'); return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } const keylessService = await keyless(); const locallyStoredKeys = keylessService.readKeys(); - console.log('[Keyless Debug] Stored keys:', { - hasStoredKeys: Boolean(locallyStoredKeys), - storedPublishableKey: locallyStoredKeys?.publishableKey?.substring(0, 20) + '...', - }); - const runningWithClaimedKeys = Boolean(configuredPublishableKey) && configuredPublishableKey === locallyStoredKeys?.publishableKey; - console.log('[Keyless Debug] Running with claimed keys:', runningWithClaimedKeys); - if (runningWithClaimedKeys && locallyStoredKeys) { try { await clerkDevelopmentCache?.run(() => keylessService.completeOnboarding(), { @@ -67,14 +51,10 @@ export async function resolveKeysWithKeylessFallback( msg: createConfirmationMessage(), }); - console.log('[Keyless Debug] Returning claimed keys (no keyless URLs)'); return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } - console.log('[Keyless Debug] Checking if need to create keyless app'); - if (!publishableKey && !secretKey) { - console.log('[Keyless Debug] Creating keyless app (no keys configured)'); const keylessApp: AccountlessApplication | null = await keylessService.getOrCreateKeys(); if (keylessApp) { @@ -83,23 +63,12 @@ export async function resolveKeysWithKeylessFallback( claimUrl = keylessApp.claimUrl; apiKeysUrl = keylessApp.apiKeysUrl; - console.log('[Keyless Debug] Keyless app created/retrieved with URLs'); - clerkDevelopmentCache?.log({ cacheKey: keylessApp.publishableKey, msg: createKeylessModeMessage(keylessApp), }); } - } else { - console.log('[Keyless Debug] Skipping keyless app creation (keys already configured)'); } - console.log('[Keyless Debug] Final return:', { - hasPublishableKey: Boolean(publishableKey), - hasSecretKey: Boolean(secretKey), - hasClaimUrl: Boolean(claimUrl), - hasApiKeysUrl: Boolean(apiKeysUrl), - }); - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; } From 458c600a59a6f08e711d6014e4aa66e4918d8172 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 22:09:04 -0800 Subject: [PATCH 10/13] chore: move to runtime check --- packages/astro/src/env.d.ts | 2 + .../src/integration/create-integration.ts | 36 +++------------- .../internal/merge-env-vars-with-params.ts | 6 ++- packages/astro/src/server/clerk-middleware.ts | 43 ++++++++++++++++++- packages/astro/src/server/get-safe-env.ts | 16 ++++--- 5 files changed, 65 insertions(+), 38 deletions(-) diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts index ee31ce4b0e6..521ef2bf5f7 100644 --- a/packages/astro/src/env.d.ts +++ b/packages/astro/src/env.d.ts @@ -32,6 +32,8 @@ interface ImportMeta { declare namespace App { interface Locals { runtime: { env: InternalEnv }; + keylessClaimUrl?: string; + keylessApiKeysUrl?: string; } } diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index f9b1f0e1ca1..5f99ef57f4e 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -4,7 +4,6 @@ import { envField } from 'astro/config'; import { loadEnv } from 'vite'; import { name as packageName, version as packageVersion } from '../../package.json'; -import { resolveKeysWithKeylessFallback } from '../server/keyless/utils'; import type { AstroClerkIntegrationParams } from '../types'; import { vitePluginAstroConfig } from './vite-plugin-astro-config'; @@ -28,7 +27,7 @@ function createIntegration() return { name: '@clerk/astro/integration', hooks: { - 'astro:config:setup': async ({ config, injectScript, updateConfig, logger, command }) => { + 'astro:config:setup': ({ config, injectScript, updateConfig, logger, command }) => { if (['server', 'hybrid'].includes(config.output) && !config.adapter) { logger.error('Missing adapter, please update your Astro config to use one.'); } @@ -45,30 +44,8 @@ function createIntegration() const envPublishableKey = loadedEnv.PUBLIC_CLERK_PUBLISHABLE_KEY || process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; const envSecretKey = loadedEnv.CLERK_SECRET_KEY || process.env.CLERK_SECRET_KEY; - let resolvedKeys = { - publishableKey: envPublishableKey, - secretKey: envSecretKey, - claimUrl: undefined as string | undefined, - apiKeysUrl: undefined as string | undefined, - }; - - if (isDev) { - try { - resolvedKeys = await resolveKeysWithKeylessFallback(envPublishableKey, envSecretKey, isDev); - if (resolvedKeys.publishableKey) { - logger.info(`Clerk: Using ${resolvedKeys.claimUrl ? 'keyless' : 'configured'} keys`); - } - } catch (error) { - logger.warn('Keyless mode initialization failed, using configured keys'); - logger.debug(`Keyless error: ${error}`); - } - } - - if (!resolvedKeys.publishableKey && !resolvedKeys.secretKey) { - logger.error( - 'Missing Clerk keys. Set PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY environment variables, or let keyless mode generate them automatically.', - ); - } + // Note: Keyless mode is now handled by middleware, not integration hook + // Keys missing error removed - middleware will handle keyless fallback const internalParams: ClerkOptions = { ...params, @@ -97,10 +74,9 @@ function createIntegration() ...buildEnvVarFromOption(clerkJSUrl, 'PUBLIC_CLERK_JS_URL'), ...buildEnvVarFromOption(clerkJSVersion, 'PUBLIC_CLERK_JS_VERSION'), ...buildEnvVarFromOption(prefetchUI === false ? 'false' : undefined, 'PUBLIC_CLERK_PREFETCH_UI'), - ...buildEnvVarFromOption(resolvedKeys.publishableKey, 'PUBLIC_CLERK_PUBLISHABLE_KEY'), - ...buildEnvVarFromOption(resolvedKeys.secretKey, 'CLERK_SECRET_KEY'), - ...buildEnvVarFromOption(resolvedKeys.claimUrl, 'PUBLIC_CLERK_KEYLESS_CLAIM_URL'), - ...buildEnvVarFromOption(resolvedKeys.apiKeysUrl, 'PUBLIC_CLERK_KEYLESS_API_KEYS_URL'), + ...buildEnvVarFromOption(envPublishableKey, 'PUBLIC_CLERK_PUBLISHABLE_KEY'), + ...buildEnvVarFromOption(envSecretKey, 'CLERK_SECRET_KEY'), + // Keyless URLs are now handled by middleware, not vite.define }, ssr: { diff --git a/packages/astro/src/internal/merge-env-vars-with-params.ts b/packages/astro/src/internal/merge-env-vars-with-params.ts index 30df74de785..da232a1678c 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -56,8 +56,10 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish disabled: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DISABLED), debug: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DEBUG), }, - __internal_keylessClaimUrl: import.meta.env.PUBLIC_CLERK_KEYLESS_CLAIM_URL, - __internal_keylessApiKeysUrl: import.meta.env.PUBLIC_CLERK_KEYLESS_API_KEYS_URL, + // Read from params (server-injected via __CLERK_ASTRO_SAFE_VARS__) with fallback to import.meta.env + __internal_keylessClaimUrl: (params as any)?.keylessClaimUrl || import.meta.env.PUBLIC_CLERK_KEYLESS_CLAIM_URL, + __internal_keylessApiKeysUrl: + (params as any)?.keylessApiKeysUrl || import.meta.env.PUBLIC_CLERK_KEYLESS_API_KEYS_URL, ...rest, }; }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index fa0500f2197..a1d74999bfe 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -24,10 +24,12 @@ import type { APIContext } from 'astro'; import { authAsyncStorage } from '#async-local-storage'; +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 { resolveKeysWithKeylessFallback } from './keyless/utils'; import { serverRedirectWithAuth } from './server-redirect-with-auth'; import type { AstroMiddleware, @@ -79,9 +81,42 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { const clerkRequest = createClerkRequest(context.request); + // Resolve keyless URLs per-request in development + let keylessClaimUrl: string | undefined; + let keylessApiKeysUrl: string | undefined; + let keylessOptions = options; + + if (canUseKeyless) { + try { + const env = getSafeEnv(context); + const configuredPublishableKey = options?.publishableKey || env.pk; + const configuredSecretKey = options?.secretKey || env.sk; + + const keylessResult = await resolveKeysWithKeylessFallback( + configuredPublishableKey, + configuredSecretKey, + true, // isDev - keyless only works in dev + ); + + keylessClaimUrl = keylessResult.claimUrl; + keylessApiKeysUrl = keylessResult.apiKeysUrl; + + // Override keys with keyless values if returned + if (keylessResult.publishableKey || keylessResult.secretKey) { + keylessOptions = { + ...options, + ...(keylessResult.publishableKey && { publishableKey: keylessResult.publishableKey }), + ...(keylessResult.secretKey && { secretKey: keylessResult.secretKey }), + }; + } + } catch (error) { + // Silently fail - continue without keyless + } + } + const requestState = await clerkClient(context).authenticateRequest( clerkRequest, - createAuthenticateRequestOptions(clerkRequest, options, context), + createAuthenticateRequestOptions(clerkRequest, keylessOptions, context), ); const locationHeader = requestState.headers.get(constants.Headers.Location); @@ -104,6 +139,12 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { decorateAstroLocal(clerkRequest, authObjectFn, context, requestState); + // Store keyless URLs for injection into client + if (keylessClaimUrl || keylessApiKeysUrl) { + context.locals.keylessClaimUrl = keylessClaimUrl; + context.locals.keylessApiKeysUrl = keylessApiKeysUrl; + } + /** * ALS is crucial for guaranteeing SSR in UI frameworks like React. * This currently powers the `useAuth()` React hook and any other hook or Component that depends on it. diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 66c498cb683..4cb1751a936 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -21,6 +21,8 @@ function getContextEnvVar(envVarName: keyof InternalEnv, contextOrLocals: Contex * @internal */ function getSafeEnv(context: ContextOrLocals) { + const locals = 'locals' in context ? context.locals : context; + return { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), isSatellite: getContextEnvVar('PUBLIC_CLERK_IS_SATELLITE', context) === 'true', @@ -35,11 +37,12 @@ function getSafeEnv(context: ContextOrLocals) { clerkUIUrl: getContextEnvVar('PUBLIC_CLERK_UI_URL', context), prefetchUI: getContextEnvVar('PUBLIC_CLERK_PREFETCH_UI', context) === 'false' ? false : undefined, apiVersion: getContextEnvVar('CLERK_API_VERSION', context), - apiUrl: getContextEnvVar('CLERK_API_URL', context), + apiUrl: getContextEnvVar('PUBLIC_CLERK_API_URL', context), telemetryDisabled: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DISABLED', context)), telemetryDebug: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DEBUG', context)), - keylessClaimUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_CLAIM_URL', context), - keylessApiKeysUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_API_KEYS_URL', context), + // Read from locals (set by middleware) instead of env vars + keylessClaimUrl: locals.keylessClaimUrl, + keylessApiKeysUrl: locals.keylessApiKeysUrl, }; } @@ -51,14 +54,17 @@ function getSafeEnv(context: ContextOrLocals) { * This is a way to get around it. */ function getClientSafeEnv(context: ContextOrLocals) { + const locals = 'locals' in context ? context.locals : context; + return { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), isSatellite: getContextEnvVar('PUBLIC_CLERK_IS_SATELLITE', context) === 'true', proxyUrl: getContextEnvVar('PUBLIC_CLERK_PROXY_URL', context), signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context), signUpUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_UP_URL', context), - keylessClaimUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_CLAIM_URL', context), - keylessApiKeysUrl: getContextEnvVar('PUBLIC_CLERK_KEYLESS_API_KEYS_URL', context), + // Read from locals (set by middleware) instead of env vars + keylessClaimUrl: locals.keylessClaimUrl, + keylessApiKeysUrl: locals.keylessApiKeysUrl, }; } From 238f5d309d49c0c65b66c983fcb66bcc9df86067 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 22:11:37 -0800 Subject: [PATCH 11/13] fix types --- .../astro/src/integration/create-integration.ts | 17 ++++------------- packages/astro/src/server/get-safe-env.ts | 4 ++-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 5f99ef57f4e..823bb7d2774 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -1,7 +1,6 @@ import type { ClerkOptions } from '@clerk/shared/types'; import type { AstroIntegration } from 'astro'; import { envField } from 'astro/config'; -import { loadEnv } from 'vite'; import { name as packageName, version as packageVersion } from '../../package.json'; import type { AstroClerkIntegrationParams } from '../types'; @@ -34,18 +33,10 @@ function createIntegration() const isDev = command === 'dev'; - // Load environment variables from .env files - // Astro's integration hook runs before .env is loaded, so we need to do it manually - const mode = isDev ? 'development' : 'production'; - const envDir = config.root.pathname; - const loadedEnv = loadEnv(mode, envDir, ''); - - // Try .env files first, then fall back to process.env - const envPublishableKey = loadedEnv.PUBLIC_CLERK_PUBLISHABLE_KEY || process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; - const envSecretKey = loadedEnv.CLERK_SECRET_KEY || process.env.CLERK_SECRET_KEY; - - // Note: Keyless mode is now handled by middleware, not integration hook - // Keys missing error removed - middleware will handle keyless fallback + // Read keys from process.env for vite.define injection + // Note: Keyless mode is now handled by middleware per-request, not here + const envPublishableKey = process.env.PUBLIC_CLERK_PUBLISHABLE_KEY; + const envSecretKey = process.env.CLERK_SECRET_KEY; const internalParams: ClerkOptions = { ...params, diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 4cb1751a936..7c12de9068e 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -37,7 +37,7 @@ function getSafeEnv(context: ContextOrLocals) { clerkUIUrl: getContextEnvVar('PUBLIC_CLERK_UI_URL', context), prefetchUI: getContextEnvVar('PUBLIC_CLERK_PREFETCH_UI', context) === 'false' ? false : undefined, apiVersion: getContextEnvVar('CLERK_API_VERSION', context), - apiUrl: getContextEnvVar('PUBLIC_CLERK_API_URL', context), + apiUrl: getContextEnvVar('CLERK_API_URL', context), telemetryDisabled: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DISABLED', context)), telemetryDebug: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DEBUG', context)), // Read from locals (set by middleware) instead of env vars @@ -54,7 +54,7 @@ function getSafeEnv(context: ContextOrLocals) { * This is a way to get around it. */ function getClientSafeEnv(context: ContextOrLocals) { - const locals = 'locals' in context ? context.locals : context; + const locals = ('locals' in context ? context.locals : context) as any; return { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), From 7abc1bdf5cec9b44f1534516368c2ad30fe3e2e4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 6 Feb 2026 22:18:07 -0800 Subject: [PATCH 12/13] fix: pass keyless keys to client --- packages/astro/src/env.d.ts | 1 + packages/astro/src/server/clerk-middleware.ts | 6 +++++- packages/astro/src/server/get-safe-env.ts | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts index 521ef2bf5f7..34489ee4a0a 100644 --- a/packages/astro/src/env.d.ts +++ b/packages/astro/src/env.d.ts @@ -34,6 +34,7 @@ declare namespace App { 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 a1d74999bfe..324fd0ffc83 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -139,10 +139,14 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { decorateAstroLocal(clerkRequest, authObjectFn, context, requestState); - // Store keyless URLs for injection into client + // Store keyless data for injection into client if (keylessClaimUrl || keylessApiKeysUrl) { context.locals.keylessClaimUrl = keylessClaimUrl; context.locals.keylessApiKeysUrl = keylessApiKeysUrl; + // Also store the resolved publishable key so client can use it + if (keylessOptions?.publishableKey) { + context.locals.keylessPublishableKey = keylessOptions.publishableKey; + } } /** diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 7c12de9068e..2c679edc59e 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -27,7 +27,8 @@ function getSafeEnv(context: ContextOrLocals) { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), isSatellite: getContextEnvVar('PUBLIC_CLERK_IS_SATELLITE', context) === 'true', proxyUrl: getContextEnvVar('PUBLIC_CLERK_PROXY_URL', context), - pk: getContextEnvVar('PUBLIC_CLERK_PUBLISHABLE_KEY', context), + // Use keyless publishable key if available, otherwise read from env + pk: locals.keylessPublishableKey || getContextEnvVar('PUBLIC_CLERK_PUBLISHABLE_KEY', context), sk: getContextEnvVar('CLERK_SECRET_KEY', context), machineSecretKey: getContextEnvVar('CLERK_MACHINE_SECRET_KEY', context), signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context), @@ -62,6 +63,8 @@ function getClientSafeEnv(context: ContextOrLocals) { proxyUrl: getContextEnvVar('PUBLIC_CLERK_PROXY_URL', context), signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context), signUpUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_UP_URL', context), + // In keyless mode, pass the resolved publishable key to client + publishableKey: locals.keylessPublishableKey || getContextEnvVar('PUBLIC_CLERK_PUBLISHABLE_KEY', context), // Read from locals (set by middleware) instead of env vars keylessClaimUrl: locals.keylessClaimUrl, keylessApiKeysUrl: locals.keylessApiKeysUrl, From 11ed1eecbed3f67900ead40b136dc8f1df8877f5 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Sun, 8 Feb 2026 11:53:20 -0800 Subject: [PATCH 13/13] chore: remove redundant code --- packages/astro/src/env.d.ts | 2 -- .../astro/src/integration/create-integration.ts | 13 +------------ .../src/internal/merge-env-vars-with-params.ts | 8 ++++---- packages/astro/src/server/clerk-middleware.ts | 6 +----- packages/astro/src/server/keyless/index.ts | 2 +- packages/astro/src/server/keyless/utils.ts | 6 ++++-- 6 files changed, 11 insertions(+), 26 deletions(-) diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts index 34489ee4a0a..e1778007d82 100644 --- a/packages/astro/src/env.d.ts +++ b/packages/astro/src/env.d.ts @@ -20,8 +20,6 @@ interface InternalEnv { readonly PUBLIC_CLERK_SIGN_UP_URL?: string; readonly PUBLIC_CLERK_TELEMETRY_DISABLED?: string; readonly PUBLIC_CLERK_TELEMETRY_DEBUG?: string; - readonly PUBLIC_CLERK_KEYLESS_CLAIM_URL?: string; - readonly PUBLIC_CLERK_KEYLESS_API_KEYS_URL?: string; readonly PUBLIC_CLERK_KEYLESS_DISABLED?: string; } diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 823bb7d2774..c213e9653b8 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -179,18 +179,7 @@ function createClerkEnvSchema() { PUBLIC_CLERK_UI_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }), PUBLIC_CLERK_TELEMETRY_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }), PUBLIC_CLERK_TELEMETRY_DEBUG: envField.boolean({ context: 'client', access: 'public', optional: true }), - PUBLIC_CLERK_KEYLESS_CLAIM_URL: envField.string({ - context: 'client', - access: 'public', - optional: true, - url: true, - }), - PUBLIC_CLERK_KEYLESS_API_KEYS_URL: envField.string({ - context: 'client', - access: 'public', - optional: true, - url: true, - }), + // Note: KEYLESS_CLAIM_URL and KEYLESS_API_KEYS_URL are dynamically resolved by middleware, not user-configurable PUBLIC_CLERK_KEYLESS_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }), CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), CLERK_MACHINE_SECRET_KEY: envField.string({ context: 'server', access: 'secret', optional: true }), diff --git a/packages/astro/src/internal/merge-env-vars-with-params.ts b/packages/astro/src/internal/merge-env-vars-with-params.ts index da232a1678c..edc4b360f2b 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -56,10 +56,10 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish disabled: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DISABLED), debug: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DEBUG), }, - // Read from params (server-injected via __CLERK_ASTRO_SAFE_VARS__) with fallback to import.meta.env - __internal_keylessClaimUrl: (params as any)?.keylessClaimUrl || import.meta.env.PUBLIC_CLERK_KEYLESS_CLAIM_URL, - __internal_keylessApiKeysUrl: - (params as any)?.keylessApiKeysUrl || import.meta.env.PUBLIC_CLERK_KEYLESS_API_KEYS_URL, + // Read from params (server-injected via __CLERK_ASTRO_SAFE_VARS__) + // These are dynamically resolved by middleware, not from env vars + __internal_keylessClaimUrl: (params as any)?.keylessClaimUrl, + __internal_keylessApiKeysUrl: (params as any)?.keylessApiKeysUrl, ...rest, }; }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 324fd0ffc83..72d7792b175 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -92,11 +92,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { const configuredPublishableKey = options?.publishableKey || env.pk; const configuredSecretKey = options?.secretKey || env.sk; - const keylessResult = await resolveKeysWithKeylessFallback( - configuredPublishableKey, - configuredSecretKey, - true, // isDev - keyless only works in dev - ); + const keylessResult = await resolveKeysWithKeylessFallback(configuredPublishableKey, configuredSecretKey); keylessClaimUrl = keylessResult.claimUrl; keylessApiKeysUrl = keylessResult.apiKeysUrl; diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index e26335a9901..af2035bb94b 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -41,7 +41,7 @@ export async function keyless() { } }, }, - framework: '@clerk/astro', + framework: 'astro', frameworkVersion: PACKAGE_VERSION, }); } diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index 0c2c6951431..92ac8b888c1 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -11,17 +11,19 @@ export interface KeylessResult { apiKeysUrl: string | undefined; } +/** + * Resolves Clerk keys, falling back to keyless mode in development if configured keys are missing. + */ export async function resolveKeysWithKeylessFallback( configuredPublishableKey: string | undefined, configuredSecretKey: string | undefined, - isDev: boolean = false, ): Promise { let publishableKey = configuredPublishableKey; let secretKey = configuredSecretKey; let claimUrl: string | undefined; let apiKeysUrl: string | undefined; - if (!isDev || !canUseKeyless) { + if (!canUseKeyless) { return { publishableKey, secretKey, claimUrl, apiKeysUrl }; }