From 9546d96c90a4a069a2ed9da128f34e3990b3975f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 09:17:37 -0800 Subject: [PATCH 01/13] feat(astro): Add support for keyless mode - Add keyless service with file storage adapter - Middleware resolves keyless URLs per-request (runtime, not compile-time) - Store keyless data in context.locals - Inject keyless URLs via __CLERK_ASTRO_SAFE_VARS__ script tag - Client reads from params (server-injected) instead of import.meta.env - Add feature flag for keyless mode - Add integration test using shared keyless helpers - Add @clerk/shared as dependency for shared keyless utilities Follows runtime pattern from React Router and TanStack Start: - No browser cache issues - Instant updates when switching modes - Clean separation: Integration for build, Middleware for runtime --- integration/tests/astro/keyless.test.ts | 54 +++++++++++++ packages/astro/package.json | 3 +- packages/astro/src/env.d.ts | 4 + .../src/integration/create-integration.ts | 28 ++++++- .../src/internal/create-clerk-instance.ts | 5 ++ .../internal/merge-env-vars-with-params.ts | 8 +- packages/astro/src/server/clerk-middleware.ts | 43 ++++++++++- packages/astro/src/server/get-safe-env.ts | 15 +++- .../astro/src/server/keyless/file-storage.ts | 20 +++++ packages/astro/src/server/keyless/index.ts | 50 ++++++++++++ packages/astro/src/server/keyless/utils.ts | 76 +++++++++++++++++++ packages/astro/src/utils/feature-flags.ts | 17 +++++ packages/astro/tsup.config.ts | 10 ++- 13 files changed, 324 insertions(+), 9 deletions(-) create mode 100644 integration/tests/astro/keyless.test.ts 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/integration/tests/astro/keyless.test.ts b/integration/tests/astro/keyless.test.ts new file mode 100644 index 00000000000..c0808437173 --- /dev/null +++ b/integration/tests/astro/keyless.test.ts @@ -0,0 +1,54 @@ +import { test } from '@playwright/test'; + +import type { Application } from '../../models/application'; +import { appConfigs } from '../../presets'; +import { + testClaimedAppWithMissingKeys, + testKeylessRemovedAfterEnvAndRestart, + testToggleCollapsePopoverAndClaim, +} 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 }) => { + await testToggleCollapsePopoverAndClaim({ page, context, app, dashboardUrl, framework: 'astro' }); + }); + + test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({ + page, + context, + }) => { + await testClaimedAppWithMissingKeys({ page, context, app, dashboardUrl }); + }); + + test('Keyless popover is removed after adding keys to .env and restarting.', async ({ page, context }) => { + await testKeylessRemovedAfterEnvAndRestart({ page, context, app }); + }); +}); diff --git a/packages/astro/package.json b/packages/astro/package.json index 86a88fd1341..d103413e2d4 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -97,7 +97,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/env.d.ts b/packages/astro/src/env.d.ts index 933fc2aea3f..205d514c786 100644 --- a/packages/astro/src/env.d.ts +++ b/packages/astro/src/env.d.ts @@ -21,6 +21,7 @@ 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_DISABLED?: string; } interface ImportMeta { @@ -30,6 +31,9 @@ interface ImportMeta { declare namespace App { interface Locals { runtime: { env: InternalEnv }; + keylessClaimUrl?: string; + keylessApiKeysUrl?: string; + keylessPublishableKey?: string; } } diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index dd566287a1f..0583c02dbea 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -28,17 +28,22 @@ 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 isDev = command === 'dev'; + + // Note: Keyless mode is now handled by middleware per-request, not here + // Keys are read directly from process.env by server code - no vite.define injection needed + const internalParams: ClerkOptions = { ...params, sdkMetadata: { version: packageVersion, name: packageName, - environment: command === 'dev' ? 'development' : 'production', + environment: isDev ? 'development' : 'production', }, }; @@ -64,6 +69,8 @@ function createIntegration() prefetchUI === false || hasUI ? 'false' : undefined, 'PUBLIC_CLERK_PREFETCH_UI', ), + // Keys read directly from process.env by server code - no vite.define needed + // Keyless handled by middleware at runtime, not compile-time }, ssr: { @@ -166,7 +173,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 }), @@ -179,7 +186,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 1f84cd4a311..db6c2d91059 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() ui: { ...options?.ui, ClerkUI }, + ...(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 bcecc86d790..5c8b97f036b 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -48,7 +48,9 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish isSatellite: paramSatellite || import.meta.env.PUBLIC_CLERK_IS_SATELLITE, proxyUrl: paramProxy || import.meta.env.PUBLIC_CLERK_PROXY_URL, domain: paramDomain || import.meta.env.PUBLIC_CLERK_DOMAIN, - publishableKey: paramPublishableKey || import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY || '', + // In keyless mode, use server-injected publishableKey from params + publishableKey: + paramPublishableKey || (params as any)?.publishableKey || import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY || '', clerkJSUrl: paramClerkJSUrl || import.meta.env.PUBLIC_CLERK_JS_URL, clerkJSVersion: paramClerkJSVersion || import.meta.env.PUBLIC_CLERK_JS_VERSION, clerkUIUrl: paramClerkUIUrl || import.meta.env.PUBLIC_CLERK_UI_URL, @@ -58,6 +60,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__) + // 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 fa0500f2197..72d7792b175 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,38 @@ 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); + + 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 +135,16 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { decorateAstroLocal(clerkRequest, authObjectFn, context, requestState); + // 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; + } + } + /** * 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 ebf98b99136..0d65b905fb6 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -21,11 +21,14 @@ 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', 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), @@ -39,6 +42,9 @@ 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)), + // Read from locals (set by middleware) instead of env vars + keylessClaimUrl: locals.keylessClaimUrl, + keylessApiKeysUrl: locals.keylessApiKeysUrl, }; } @@ -50,12 +56,19 @@ function getSafeEnv(context: ContextOrLocals) { * This is a way to get around it. */ function getClientSafeEnv(context: ContextOrLocals) { + const locals = ('locals' in context ? context.locals : context) as any; + 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), + // 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, }; } 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..af2035bb94b --- /dev/null +++ b/packages/astro/src/server/keyless/index.ts @@ -0,0 +1,50 @@ +import { createClerkClient } from '@clerk/backend'; +import { createKeylessService } from '@clerk/shared/keyless'; + +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 client = createClerkClient({ + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + }); + return await client.__experimental_accountlessApplications.createAccountlessApplication({ + requestHeaders, + }); + } catch { + return null; + } + }, + async completeOnboarding(requestHeaders?: Headers) { + try { + const client = createClerkClient({ + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + }); + return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: '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..92ac8b888c1 --- /dev/null +++ b/packages/astro/src/server/keyless/utils.ts @@ -0,0 +1,76 @@ +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; +} + +/** + * 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, +): Promise { + let publishableKey = configuredPublishableKey; + let secretKey = configuredSecretKey; + let claimUrl: string | undefined; + let apiKeysUrl: string | undefined; + + if (!canUseKeyless) { + return { publishableKey, secretKey, claimUrl, apiKeysUrl }; + } + + // If both keys are explicitly configured, skip all keyless logic + if (publishableKey && secretKey) { + 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..70856d6609e --- /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(process.env.PUBLIC_CLERK_KEYLESS_DISABLED) || isTruthy(process.env.CLERK_KEYLESS_DISABLED) || false; + +export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED && hasFileSystemSupport(); 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', + ], }; }); From 517a81553513669752762cdd946eed3f94701326 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 10:21:12 -0800 Subject: [PATCH 02/13] refactor(astro): Use shared keyless utility for DRY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor packages/astro/src/server/keyless/utils.ts to use the shared resolveKeysWithKeylessFallback utility from @clerk/shared/keyless, matching the pattern used by React Router and TanStack Start. This removes ~70 lines of duplicated code and ensures consistent behavior across all frameworks. Changes: - Import sharedResolveKeysWithKeylessFallback from @clerk/shared/keyless - Re-export KeylessResult type from shared package - Simplify wrapper to create keylessService and call shared utility - Remove duplicated logic (claimed keys flow, cache management, etc.) Benefits: ✅ Keeps codebase DRY ✅ Ensures consistent behavior across frameworks ✅ Makes maintenance easier (single source of truth) ✅ Reduces test surface area --- packages/astro/package.json | 3 +- packages/astro/src/server/keyless/utils.ts | 73 +++------------------- 2 files changed, 10 insertions(+), 66 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index d103413e2d4..ed5e051a1cf 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -97,8 +97,7 @@ }, "devDependencies": { "@clerk/ui": "workspace:^", - "astro": "^5.15.9", - "vite": "^7.1.0" + "astro": "^5.17.1" }, "peerDependencies": { "astro": "^4.15.0 || ^5.0.0" diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index 92ac8b888c1..89976f2da15 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -1,76 +1,21 @@ -import type { AccountlessApplication } from '@clerk/shared/keyless'; -import { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } from '@clerk/shared/keyless'; +import { resolveKeysWithKeylessFallback as sharedResolveKeysWithKeylessFallback } from '@clerk/shared/keyless'; +export type { KeylessResult } 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; -} - /** * 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, -): Promise { - let publishableKey = configuredPublishableKey; - let secretKey = configuredSecretKey; - let claimUrl: string | undefined; - let apiKeysUrl: string | undefined; - - if (!canUseKeyless) { - return { publishableKey, secretKey, claimUrl, apiKeysUrl }; - } - - // If both keys are explicitly configured, skip all keyless logic - if (publishableKey && secretKey) { - 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 }; + return sharedResolveKeysWithKeylessFallback( + configuredPublishableKey, + configuredSecretKey, + keylessService, + canUseKeyless, + ); } From 3bc54de6494c86ecdcefc596d566ab93a5d090e2 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 10:38:52 -0800 Subject: [PATCH 03/13] refactor(astro): Match React Router keyless patterns and fix type safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns Astro's keyless implementation with React Router/TanStack Start patterns for consistency and maintainability. Changes: 1. file-storage.ts: - Use static imports without .default destructuring - Pass entire module namespace to createNodeFileStorage - Add error handling with helpful message - Match React Router's implementation pattern 2. keyless/index.ts: - Accept context and options parameters (like React Router) - Add canUseFileSystem() runtime check - Add init promise to prevent race conditions - Add resetKeylessService() for testing - Use createClerkClient directly instead of inline creation - Add proper error handling and logging 3. keyless/utils.ts: - Pass context and options to keyless service - Update function signature to match new pattern 4. feature-flags.ts: - Use getEnvVariable helper instead of direct process.env - Remove hasFileSystemSupport() (handled by keyless service) - Simplify to match React Router's cleaner pattern 5. get-safe-env.ts: - Remove 'as any' type cast (line 59) - Use proper App.Locals type from env.d.ts - Improve type safety throughout 6. clerk-middleware.ts: - Export ClerkAstroMiddlewareOptions type - Pass context and options to resolveKeysWithKeylessFallback Benefits: ✅ Consistent patterns across React Router/TanStack Start/Astro ✅ Better type safety (no 'any' casts) ✅ Cleaner separation of concerns ✅ Race condition protection with init promise ✅ Better error handling and logging ✅ More testable with resetKeylessService() ✅ Follows shared utility patterns (DRY) --- packages/astro/src/server/clerk-middleware.ts | 9 +- packages/astro/src/server/get-safe-env.ts | 2 +- .../astro/src/server/keyless/file-storage.ts | 25 ++-- packages/astro/src/server/keyless/index.ts | 132 ++++++++++++------ packages/astro/src/server/keyless/utils.ts | 6 +- packages/astro/src/utils/feature-flags.ts | 17 +-- 6 files changed, 127 insertions(+), 64 deletions(-) diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 72d7792b175..91513164517 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -51,7 +51,7 @@ type ClerkAstroMiddlewareHandler = ( next: AstroMiddlewareNextParam, ) => AstroMiddlewareReturn | undefined; -type ClerkAstroMiddlewareOptions = AuthenticateRequestOptions; +export type ClerkAstroMiddlewareOptions = AuthenticateRequestOptions; /** * Middleware for Astro that handles authentication and authorization with Clerk. @@ -92,7 +92,12 @@ 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); + const keylessResult = await resolveKeysWithKeylessFallback( + configuredPublishableKey, + configuredSecretKey, + context, + options, + ); keylessClaimUrl = keylessResult.claimUrl; keylessApiKeysUrl = keylessResult.apiKeysUrl; diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index 0d65b905fb6..76600aac7c0 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -56,7 +56,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) as any; + const locals = 'locals' in context ? context.locals : context; return { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), diff --git a/packages/astro/src/server/keyless/file-storage.ts b/packages/astro/src/server/keyless/file-storage.ts index 0df8973418d..f7f93c00b6b 100644 --- a/packages/astro/src/server/keyless/file-storage.ts +++ b/packages/astro/src/server/keyless/file-storage.ts @@ -1,4 +1,4 @@ -import type { KeylessStorage } from '@clerk/shared/keyless'; +import { createNodeFileStorage, type KeylessStorage } from '@clerk/shared/keyless'; export type { KeylessStorage }; @@ -6,15 +6,24 @@ export interface FileStorageOptions { cwd?: () => string; } +/** + * Creates a file-based storage adapter for keyless mode. + * Uses dynamic imports to avoid bundler issues with edge runtimes. + */ 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')]); + try { + const [fs, 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', - }); + return createNodeFileStorage(fs, path, { + cwd, + frameworkPackageName: '@clerk/astro', + }); + } catch { + throw new Error( + 'Keyless mode requires a Node.js runtime with file system access. ' + + 'Set PUBLIC_CLERK_KEYLESS_DISABLED=1 or CLERK_KEYLESS_DISABLED=1 to disable keyless mode.', + ); + } } diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index af2035bb94b..afa67ec2827 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -1,50 +1,102 @@ import { createClerkClient } from '@clerk/backend'; import { createKeylessService } from '@clerk/shared/keyless'; +import type { APIContext } from 'astro'; +import type { ClerkAstroMiddlewareOptions } from '../clerk-middleware'; import { createFileStorage } from './file-storage.js'; let keylessServiceInstance: ReturnType | null = null; +let keylessInitPromise: Promise | null> | null = null; -export async function keyless() { - if (!keylessServiceInstance) { - const storage = await createFileStorage(); - - keylessServiceInstance = createKeylessService({ - storage, - api: { - async createAccountlessApplication(requestHeaders?: Headers) { - try { - const client = createClerkClient({ - secretKey: process.env.CLERK_SECRET_KEY, - publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, - apiUrl: process.env.CLERK_API_URL, - }); - return await client.__experimental_accountlessApplications.createAccountlessApplication({ - requestHeaders, - }); - } catch { - return null; - } - }, - async completeOnboarding(requestHeaders?: Headers) { - try { - const client = createClerkClient({ - secretKey: process.env.CLERK_SECRET_KEY, - publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, - apiUrl: process.env.CLERK_API_URL, - }); - return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ - requestHeaders, - }); - } catch { - return null; - } - }, - }, - framework: 'astro', - frameworkVersion: PACKAGE_VERSION, - }); +function canUseFileSystem(): boolean { + try { + return typeof process !== 'undefined' && typeof process.cwd === 'function'; + } catch { + return false; + } +} + +/** + * Gets or creates the keyless service singleton. + * Returns null for non-Node.js runtimes (e.g., Cloudflare Workers). + */ +export async function keyless( + context: APIContext, + options?: ClerkAstroMiddlewareOptions, +): Promise | null> { + if (!canUseFileSystem()) { + return null; + } + + if (keylessServiceInstance) { + return keylessServiceInstance; } - return keylessServiceInstance; + if (keylessInitPromise) { + return keylessInitPromise; + } + + keylessInitPromise = (async () => { + try { + const storage = await createFileStorage(); + + const service = createKeylessService({ + storage, + api: { + async createAccountlessApplication(requestHeaders?: Headers) { + try { + // Reuse existing clerkClient factory with keys from process.env + const client = createClerkClient({ + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, + }); + return await client.__experimental_accountlessApplications.createAccountlessApplication({ + requestHeaders, + }); + } catch { + return null; + } + }, + async completeOnboarding(requestHeaders?: Headers) { + try { + // Reuse existing clerkClient factory with keys from process.env + const client = createClerkClient({ + secretKey: process.env.CLERK_SECRET_KEY, + publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, + apiUrl: process.env.CLERK_API_URL, + userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, + }); + return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: 'astro', + frameworkVersion: PACKAGE_VERSION, + }); + + keylessServiceInstance = service; + return service; + } catch (error) { + console.warn('[Clerk] Failed to initialize keyless service:', error); + return null; + } finally { + keylessInitPromise = null; + } + })(); + + return keylessInitPromise; +} + +/** + * @internal + */ +export function resetKeylessService(): void { + keylessServiceInstance = null; + keylessInitPromise = null; } diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index 89976f2da15..a91fbc36e71 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -1,6 +1,8 @@ import { resolveKeysWithKeylessFallback as sharedResolveKeysWithKeylessFallback } from '@clerk/shared/keyless'; +import type { APIContext } from 'astro'; export type { KeylessResult } from '@clerk/shared/keyless'; +import type { ClerkAstroMiddlewareOptions } from '../clerk-middleware'; import { canUseKeyless } from '../../utils/feature-flags'; import { keyless } from './index'; @@ -10,8 +12,10 @@ import { keyless } from './index'; export async function resolveKeysWithKeylessFallback( configuredPublishableKey: string | undefined, configuredSecretKey: string | undefined, + context: APIContext, + options?: ClerkAstroMiddlewareOptions, ) { - const keylessService = await keyless(); + const keylessService = await keyless(context, options); return sharedResolveKeysWithKeylessFallback( configuredPublishableKey, configuredSecretKey, diff --git a/packages/astro/src/utils/feature-flags.ts b/packages/astro/src/utils/feature-flags.ts index 70856d6609e..94421cb9937 100644 --- a/packages/astro/src/utils/feature-flags.ts +++ b/packages/astro/src/utils/feature-flags.ts @@ -1,17 +1,10 @@ +import { getEnvVariable } from '@clerk/shared/getEnvVariable'; 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(process.env.PUBLIC_CLERK_KEYLESS_DISABLED) || isTruthy(process.env.CLERK_KEYLESS_DISABLED) || false; + isTruthy(getEnvVariable('PUBLIC_CLERK_KEYLESS_DISABLED')) || + isTruthy(getEnvVariable('CLERK_KEYLESS_DISABLED')) || + false; -export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED && hasFileSystemSupport(); +export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED; From 2ed5c1a0b35cd1db34120b13daaee7292cecb341 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 10:47:56 -0800 Subject: [PATCH 04/13] refactor(astro): Reuse clerk-client.ts in keyless service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace manual createClerkClient calls with the existing clerkClient helper, matching React Router's pattern of reusing the framework's client factory. Changes: - Import clerkClient from '../clerk-client' instead of createClerkClient from @clerk/backend - Use clerkClient(context) in createAccountlessApplication and completeOnboarding - Remove manual client configuration (secretKey, publishableKey, apiUrl, userAgent) - Client now properly inherits all configuration from getSafeEnv(context) Benefits: ✅ DRY - reuses existing client configuration logic ✅ Consistent - all API calls use same client setup ✅ Maintainable - client config changes apply everywhere ✅ Matches React Router pattern exactly ✅ Inherits telemetry, sdkMetadata, and all other configs automatically --- packages/astro/src/server/keyless/index.ts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index afa67ec2827..b02f8695a56 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -1,7 +1,7 @@ -import { createClerkClient } from '@clerk/backend'; import { createKeylessService } from '@clerk/shared/keyless'; import type { APIContext } from 'astro'; +import { clerkClient } from '../clerk-client'; import type { ClerkAstroMiddlewareOptions } from '../clerk-middleware'; import { createFileStorage } from './file-storage.js'; @@ -45,14 +45,7 @@ export async function keyless( api: { async createAccountlessApplication(requestHeaders?: Headers) { try { - // Reuse existing clerkClient factory with keys from process.env - const client = createClerkClient({ - secretKey: process.env.CLERK_SECRET_KEY, - publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, - apiUrl: process.env.CLERK_API_URL, - userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, - }); - return await client.__experimental_accountlessApplications.createAccountlessApplication({ + return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, }); } catch { @@ -61,14 +54,9 @@ export async function keyless( }, async completeOnboarding(requestHeaders?: Headers) { try { - // Reuse existing clerkClient factory with keys from process.env - const client = createClerkClient({ - secretKey: process.env.CLERK_SECRET_KEY, - publishableKey: process.env.PUBLIC_CLERK_PUBLISHABLE_KEY, - apiUrl: process.env.CLERK_API_URL, - userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, - }); - return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + return await clerkClient( + context, + ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, }); } catch { From f07722ef1341b659b01ea6645472faa552f08538 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 10:50:34 -0800 Subject: [PATCH 05/13] fix(astro): Pass options to clerkClient and remove unused parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix unused parameters and ensure options are properly passed through the call chain, matching React Router's pattern exactly. Changes: 1. clerk-client.ts: - Update clerkClient signature to accept optional options parameter - Now matches React Router: clerkClient(context, options?) - Allows keyless service to override client configuration 2. keyless/index.ts: - Pass options parameter to clerkClient() calls - Options now used in createAccountlessApplication - Options now used in completeOnboarding - Fixes unused parameter warning 3. clerk-middleware.ts: - Remove unused 'error' variable in catch block - Use empty catch block (matches React Router pattern) 4. keyless/utils.ts: - Fix import sorting (ESLint auto-fix) Benefits: ✅ No unused parameters ✅ Options properly passed through call chain ✅ clerkClient can be overridden with custom config ✅ Matches React Router pattern exactly ✅ All ESLint errors resolved ✅ All TypeScript type checks pass --- packages/astro/src/server/clerk-client.ts | 2 +- packages/astro/src/server/clerk-middleware.ts | 2 +- packages/astro/src/server/keyless/index.ts | 6 +++++- packages/astro/src/server/keyless/utils.ts | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/astro/src/server/clerk-client.ts b/packages/astro/src/server/clerk-client.ts index 10b53c03a27..55d841483c8 100644 --- a/packages/astro/src/server/clerk-client.ts +++ b/packages/astro/src/server/clerk-client.ts @@ -29,6 +29,6 @@ const createClerkClientWithOptions: CreateClerkClientWithOptions = (context, opt ...options, }); -const clerkClient = (context: APIContext) => createClerkClientWithOptions(context); +const clerkClient = (context: APIContext, options?: ClerkOptions) => createClerkClientWithOptions(context, options); export { clerkClient }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 91513164517..242f100a5e9 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -110,7 +110,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { ...(keylessResult.secretKey && { secretKey: keylessResult.secretKey }), }; } - } catch (error) { + } catch { // Silently fail - continue without keyless } } diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index b02f8695a56..a131863fcb9 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -45,7 +45,10 @@ export async function keyless( api: { async createAccountlessApplication(requestHeaders?: Headers) { try { - return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({ + return await clerkClient( + context, + options, + ).__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, }); } catch { @@ -56,6 +59,7 @@ export async function keyless( try { return await clerkClient( context, + options, ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, }); diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index a91fbc36e71..1bb8f61be4b 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -2,8 +2,8 @@ import { resolveKeysWithKeylessFallback as sharedResolveKeysWithKeylessFallback import type { APIContext } from 'astro'; export type { KeylessResult } from '@clerk/shared/keyless'; -import type { ClerkAstroMiddlewareOptions } from '../clerk-middleware'; import { canUseKeyless } from '../../utils/feature-flags'; +import type { ClerkAstroMiddlewareOptions } from '../clerk-middleware'; import { keyless } from './index'; /** From a14541be7b597369610d1968a067f7fc08fe58a6 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 11:11:22 -0800 Subject: [PATCH 06/13] chore: reinstall deps --- packages/astro/tsup.config.ts | 10 +--------- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/astro/tsup.config.ts b/packages/astro/tsup.config.ts index 80849d46a6c..ade6b396caa 100644 --- a/packages/astro/tsup.config.ts +++ b/packages/astro/tsup.config.ts @@ -26,14 +26,6 @@ export default defineConfig(() => { bundle: true, sourcemap: true, format: ['esm'], - external: [ - 'astro', - 'react', - 'react-dom', - 'node:async_hooks', - '#async-local-storage', - 'astro:transitions/client', - 'vite', - ], + external: ['astro', 'react', 'react-dom', 'node:async_hooks', '#async-local-storage', 'astro:transitions/client'], }; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adb035f97a6..9e46088d355 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -386,7 +386,7 @@ importers: specifier: workspace:^ version: link:../ui astro: - specifier: ^5.15.9 + specifier: ^5.17.1 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) packages/backend: From 3b9cc57b793a493f45620a38027cd4f809a238d8 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 11:16:15 -0800 Subject: [PATCH 07/13] refactor(astro): Remove unused options parameter from clerkClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The options parameter in clerkClient was not providing any value because getSafeEnv(context) already reads all necessary configuration from both process.env and context.locals (which includes keyless values set by middleware). Changes: 1. clerk-client.ts: - Remove optional options parameter from clerkClient signature - Keep createClerkClientWithOptions internal signature for potential future use - Client configuration fully driven by getSafeEnv(context) 2. keyless/index.ts: - Remove ClerkAstroMiddlewareOptions import (no longer needed) - Remove options parameter from keyless() function - Remove options from clerkClient() calls - Simplify function signature 3. keyless/utils.ts: - Remove ClerkAstroMiddlewareOptions import - Remove options parameter from resolveKeysWithKeylessFallback() - Update keyless() call to not pass options 4. clerk-middleware.ts: - Remove options parameter from resolveKeysWithKeylessFallback() call Why this is correct: - getSafeEnv(context) reads from process.env via getContextEnvVar() - getSafeEnv(context) reads keyless values from context.locals - All necessary config (keys, urls, telemetry) is already available - The spread ...options at end of createClerkClient was never overriding anything useful - Simplifies the API and removes unnecessary parameters Benefits: ✅ Simpler API surface ✅ No unused parameters ✅ Clear single source of truth (getSafeEnv) ✅ Less confusing for developers ✅ All type checks pass --- packages/astro/src/server/clerk-client.ts | 2 +- packages/astro/src/server/clerk-middleware.ts | 1 - packages/astro/src/server/keyless/index.ts | 12 ++---------- packages/astro/src/server/keyless/utils.ts | 4 +--- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/astro/src/server/clerk-client.ts b/packages/astro/src/server/clerk-client.ts index 55d841483c8..10b53c03a27 100644 --- a/packages/astro/src/server/clerk-client.ts +++ b/packages/astro/src/server/clerk-client.ts @@ -29,6 +29,6 @@ const createClerkClientWithOptions: CreateClerkClientWithOptions = (context, opt ...options, }); -const clerkClient = (context: APIContext, options?: ClerkOptions) => createClerkClientWithOptions(context, options); +const clerkClient = (context: APIContext) => createClerkClientWithOptions(context); export { clerkClient }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 242f100a5e9..1583fdb5fc2 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -96,7 +96,6 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { configuredPublishableKey, configuredSecretKey, context, - options, ); keylessClaimUrl = keylessResult.claimUrl; diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index a131863fcb9..641fb961510 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -2,7 +2,6 @@ import { createKeylessService } from '@clerk/shared/keyless'; import type { APIContext } from 'astro'; import { clerkClient } from '../clerk-client'; -import type { ClerkAstroMiddlewareOptions } from '../clerk-middleware'; import { createFileStorage } from './file-storage.js'; let keylessServiceInstance: ReturnType | null = null; @@ -20,10 +19,7 @@ function canUseFileSystem(): boolean { * Gets or creates the keyless service singleton. * Returns null for non-Node.js runtimes (e.g., Cloudflare Workers). */ -export async function keyless( - context: APIContext, - options?: ClerkAstroMiddlewareOptions, -): Promise | null> { +export async function keyless(context: APIContext): Promise | null> { if (!canUseFileSystem()) { return null; } @@ -45,10 +41,7 @@ export async function keyless( api: { async createAccountlessApplication(requestHeaders?: Headers) { try { - return await clerkClient( - context, - options, - ).__experimental_accountlessApplications.createAccountlessApplication({ + return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, }); } catch { @@ -59,7 +52,6 @@ export async function keyless( try { return await clerkClient( context, - options, ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, }); diff --git a/packages/astro/src/server/keyless/utils.ts b/packages/astro/src/server/keyless/utils.ts index 1bb8f61be4b..f0f0d0e0898 100644 --- a/packages/astro/src/server/keyless/utils.ts +++ b/packages/astro/src/server/keyless/utils.ts @@ -3,7 +3,6 @@ import type { APIContext } from 'astro'; export type { KeylessResult } from '@clerk/shared/keyless'; import { canUseKeyless } from '../../utils/feature-flags'; -import type { ClerkAstroMiddlewareOptions } from '../clerk-middleware'; import { keyless } from './index'; /** @@ -13,9 +12,8 @@ export async function resolveKeysWithKeylessFallback( configuredPublishableKey: string | undefined, configuredSecretKey: string | undefined, context: APIContext, - options?: ClerkAstroMiddlewareOptions, ) { - const keylessService = await keyless(context, options); + const keylessService = await keyless(context); return sharedResolveKeysWithKeylessFallback( configuredPublishableKey, configuredSecretKey, From 813e17e0a5bcff8101ee4310a73009c4c535f9d7 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Tue, 10 Feb 2026 11:56:06 -0800 Subject: [PATCH 08/13] chore: add changeset --- .changeset/perfect-chicken-scream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-chicken-scream.md diff --git a/.changeset/perfect-chicken-scream.md b/.changeset/perfect-chicken-scream.md new file mode 100644 index 00000000000..552257a3977 --- /dev/null +++ b/.changeset/perfect-chicken-scream.md @@ -0,0 +1,5 @@ +--- +"@clerk/astro": minor +--- + +Introduce Keyless quickstart for Astro. This allows the Clerk SDK to be used without having to sign up and paste your keys manually. From 1d79e234affe5b1ac9fea9719b9dcbc1bb49bb79 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 11:24:29 -0800 Subject: [PATCH 09/13] chore: remove unused async keyword --- packages/astro/src/integration/create-integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 0583c02dbea..da41e9a5c4d 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -28,7 +28,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.'); } From eaa509f11d69128b8e38c2b0b89a9c046315388a Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 10 Feb 2026 16:47:08 -0800 Subject: [PATCH 10/13] chore: remove unused changes --- packages/astro/src/integration/create-integration.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index da41e9a5c4d..eb198243e73 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -33,17 +33,12 @@ function createIntegration() logger.error('Missing adapter, please update your Astro config to use one.'); } - const isDev = command === 'dev'; - - // Note: Keyless mode is now handled by middleware per-request, not here - // Keys are read directly from process.env by server code - no vite.define injection needed - const internalParams: ClerkOptions = { ...params, sdkMetadata: { version: packageVersion, name: packageName, - environment: isDev ? 'development' : 'production', + environment: command === 'dev' ? 'development' : 'production', }, }; From 5268f12326fa1de52cd2e1e245fa76f0a3306285 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 11 Feb 2026 06:20:17 -0800 Subject: [PATCH 11/13] chore: clean up --- packages/astro/src/integration/create-integration.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts index 17d92dab79b..48d86c126de 100644 --- a/packages/astro/src/integration/create-integration.ts +++ b/packages/astro/src/integration/create-integration.ts @@ -64,8 +64,6 @@ function createIntegration() prefetchUI === false || hasUI ? 'false' : undefined, 'PUBLIC_CLERK_PREFETCH_UI', ), - // Keys read directly from process.env by server code - no vite.define needed - // Keyless handled by middleware at runtime, not compile-time }, ssr: { From e6d87acf71fa028ab673ab09fec6d1381db28c2d Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 11 Feb 2026 06:21:09 -0800 Subject: [PATCH 12/13] chore: dedupe --- pnpm-lock.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b149ffd9896..35a8bffa8db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2475,7 +2475,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -8867,11 +8867,13 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.0: @@ -8880,11 +8882,11 @@ packages: glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -10103,7 +10105,6 @@ packages: keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -13581,12 +13582,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} From 0b4fe759045aa3a96ee680d41bc0382896544fe5 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 11 Feb 2026 06:29:50 -0800 Subject: [PATCH 13/13] chore: add internal params types --- .../src/internal/create-clerk-instance.ts | 7 ++--- .../internal/merge-env-vars-with-params.ts | 12 +++++---- packages/astro/src/types.ts | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index db6c2d91059..1a2455fa79d 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -9,7 +9,7 @@ import type { Ui } from '@clerk/ui/internal'; import { $clerkStore } from '../stores/external'; import { $clerk, $csrState } from '../stores/internal'; -import type { AstroClerkCreateInstanceParams, AstroClerkUpdateOptions } from '../types'; +import type { AstroClerkCreateInstanceParams, AstroClerkUpdateOptions, InternalRuntimeOptions } from '../types'; import { invokeClerkAstroJSFunctions } from './invoke-clerk-astro-js-functions'; import { mountAllClerkAstroJSComponents } from './mount-clerk-astro-js-components'; import { runOnce } from './run-once'; @@ -54,8 +54,9 @@ async function createClerkInstanceInternal(options?: AstroC $clerk.set(clerkJSInstance); } - const keylessClaimUrl = (options as any)?.__internal_keylessClaimUrl; - const keylessApiKeysUrl = (options as any)?.__internal_keylessApiKeysUrl; + const internalOptions = options as AstroClerkCreateInstanceParams & InternalRuntimeOptions; + const keylessClaimUrl = internalOptions.__internal_keylessClaimUrl; + const keylessApiKeysUrl = internalOptions.__internal_keylessApiKeysUrl; const clerkOptions = { routerPush: createNavigationHandler(window.history.pushState.bind(window.history)), 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 5c8b97f036b..39dbfd9820b 100644 --- a/packages/astro/src/internal/merge-env-vars-with-params.ts +++ b/packages/astro/src/internal/merge-env-vars-with-params.ts @@ -1,6 +1,6 @@ import { isTruthy } from '@clerk/shared/underscore'; -import type { AstroClerkIntegrationParams } from '../types'; +import type { AstroClerkIntegrationParams, InternalRuntimeOptions } from '../types'; /** * Merges `prefetchUI` param with env vars. @@ -25,7 +25,7 @@ function mergePrefetchUIConfig(paramPrefetchUI: AstroClerkIntegrationParams['pre /** * @internal */ -const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publishableKey?: string }) => { +const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & InternalRuntimeOptions) => { const { signInUrl: paramSignIn, signUpUrl: paramSignUp, @@ -42,6 +42,8 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish ...rest } = params || {}; + const internalOptions = params; + return { signInUrl: paramSignIn || import.meta.env.PUBLIC_CLERK_SIGN_IN_URL, signUpUrl: paramSignUp || import.meta.env.PUBLIC_CLERK_SIGN_UP_URL, @@ -50,7 +52,7 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish domain: paramDomain || import.meta.env.PUBLIC_CLERK_DOMAIN, // In keyless mode, use server-injected publishableKey from params publishableKey: - paramPublishableKey || (params as any)?.publishableKey || import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY || '', + paramPublishableKey || internalOptions?.publishableKey || import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY || '', clerkJSUrl: paramClerkJSUrl || import.meta.env.PUBLIC_CLERK_JS_URL, clerkJSVersion: paramClerkJSVersion || import.meta.env.PUBLIC_CLERK_JS_VERSION, clerkUIUrl: paramClerkUIUrl || import.meta.env.PUBLIC_CLERK_UI_URL, @@ -62,8 +64,8 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish }, // 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, + __internal_keylessClaimUrl: internalOptions?.keylessClaimUrl, + __internal_keylessApiKeysUrl: internalOptions?.keylessApiKeysUrl, ...rest, }; }; diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts index b0fff02dbb9..a3b051262bf 100644 --- a/packages/astro/src/types.ts +++ b/packages/astro/src/types.ts @@ -51,6 +51,33 @@ type AstroClerkCreateInstanceParams = AstroClerkIntegration publishableKey: string; }; +/** + * @internal + * Internal runtime options injected by the server for keyless mode support. + */ +export type InternalRuntimeOptions = { + /** + * Server-injected publishable key from keyless mode or context.locals + */ + publishableKey?: string; + /** + * Keyless claim URL injected by middleware for the client-side banner + */ + keylessClaimUrl?: string; + /** + * Keyless API keys URL injected by middleware for the client-side banner + */ + keylessApiKeysUrl?: string; + /** + * Internal keyless claim URL passed to Clerk.load() + */ + __internal_keylessClaimUrl?: string; + /** + * Internal keyless API keys URL passed to Clerk.load() + */ + __internal_keylessApiKeysUrl?: string; +}; + // Copied from `@clerk/react` export interface HeadlessBrowserClerk extends Clerk { load: (opts?: Without) => Promise;