From b94387338d389ad41f93e74449873a40cdd10218 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 11 Feb 2026 09:32:08 -0800 Subject: [PATCH 1/4] feat(nuxt): Introduce keyless mode --- integration/tests/nuxt/keyless.test.ts | 55 ++++++++++++ packages/nuxt/src/module.ts | 5 +- packages/nuxt/src/runtime/plugin.ts | 14 +++ .../src/runtime/server/clerkMiddleware.ts | 42 +++++++++ packages/nuxt/src/runtime/server/index.ts | 2 +- .../src/runtime/server/keyless/fileStorage.ts | 29 +++++++ .../nuxt/src/runtime/server/keyless/index.ts | 86 +++++++++++++++++++ .../nuxt/src/runtime/server/keyless/utils.ts | 24 ++++++ packages/nuxt/src/runtime/server/types.ts | 8 ++ .../nuxt/src/runtime/utils/feature-flags.ts | 10 +++ 10 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 integration/tests/nuxt/keyless.test.ts create mode 100644 packages/nuxt/src/runtime/server/keyless/fileStorage.ts create mode 100644 packages/nuxt/src/runtime/server/keyless/index.ts create mode 100644 packages/nuxt/src/runtime/server/keyless/utils.ts create mode 100644 packages/nuxt/src/runtime/utils/feature-flags.ts diff --git a/integration/tests/nuxt/keyless.test.ts b/integration/tests/nuxt/keyless.test.ts new file mode 100644 index 00000000000..6a2cad13033 --- /dev/null +++ b/integration/tests/nuxt/keyless.test.ts @@ -0,0 +1,55 @@ +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.nuxt.node.clone(); + +test.describe('Keyless mode @nuxt', () => { + 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 () => { + // Keep files for debugging + await app?.teardown(); + }); + + test('Toggle collapse popover and claim.', async ({ page, context }) => { + await testToggleCollapsePopoverAndClaim({ page, context, app, dashboardUrl, framework: 'nuxt' }); + }); + + 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/nuxt/src/module.ts b/packages/nuxt/src/module.ts index c4ee151a95a..fe739ee33f8 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -115,11 +115,14 @@ export default defineNuxtModule({ addTypeTemplate( { filename: 'types/clerk.d.ts', - getContents: () => `import type { AuthFn } from '@clerk/nuxt/server'; + getContents: () => `import type { AuthFn, ClerkKeylessContext } from '@clerk/nuxt/server'; + import type { InitialState } from '@clerk/shared/types'; declare module 'h3' { interface H3EventContext { auth: AuthFn; + __clerk_initial_state?: InitialState; + __clerk_keyless?: ClerkKeylessContext; } } `, diff --git a/packages/nuxt/src/runtime/plugin.ts b/packages/nuxt/src/runtime/plugin.ts index 03fdeb75d25..699edcd5970 100644 --- a/packages/nuxt/src/runtime/plugin.ts +++ b/packages/nuxt/src/runtime/plugin.ts @@ -10,15 +10,28 @@ setClerkJSLoadingErrorPackageName(PACKAGE_NAME); export default defineNuxtPlugin(nuxtApp => { // SSR-friendly shared state const initialState = useState('clerk-initial-state', () => undefined); + const keylessContext = useState<{ claimUrl?: string; apiKeysUrl?: string } | undefined>( + 'clerk-keyless-context', + () => undefined, + ); if (import.meta.server) { // Save the initial state from server and pass it to the plugin initialState.value = nuxtApp.ssrContext?.event.context.__clerk_initial_state; + keylessContext.value = nuxtApp.ssrContext?.event.context.__clerk_keyless; } const runtimeConfig = useRuntimeConfig(); const clerkConfig = runtimeConfig.public.clerk ?? {}; + // Add keyless mode props if present + const keylessProps = keylessContext.value + ? { + __internal_keyless_claimKeylessApplicationUrl: keylessContext.value.claimUrl, + __internal_keyless_copyInstanceKeysUrl: keylessContext.value.apiKeysUrl, + } + : {}; + nuxtApp.vueApp.use(clerkPlugin as any, { ...clerkConfig, // Map jsUrl/uiUrl to clerkJSUrl/clerkUIUrl as expected by the Vue plugin @@ -32,5 +45,6 @@ export default defineNuxtPlugin(nuxtApp => { routerPush: (to: string) => navigateTo(to), routerReplace: (to: string) => navigateTo(to, { replace: true }), initialState: initialState.value, + ...keylessProps, }); }); diff --git a/packages/nuxt/src/runtime/server/clerkMiddleware.ts b/packages/nuxt/src/runtime/server/clerkMiddleware.ts index 555999938b5..7a62ef69dbd 100644 --- a/packages/nuxt/src/runtime/server/clerkMiddleware.ts +++ b/packages/nuxt/src/runtime/server/clerkMiddleware.ts @@ -5,7 +5,9 @@ import type { PendingSessionOptions } from '@clerk/shared/types'; import type { EventHandler } from 'h3'; import { createError, eventHandler, setResponseHeader } from 'h3'; +import { canUseKeyless } from '../utils/feature-flags'; import { clerkClient } from './clerkClient'; +import { resolveKeysWithKeylessFallback } from './keyless/utils'; import type { AuthFn, AuthOptions } from './types'; import { createInitialState, toWebRequest } from './utils'; @@ -82,6 +84,38 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => { return eventHandler(async event => { const clerkRequest = toWebRequest(event); + // Resolve keyless in development if keys are missing + let keylessClaimUrl: string | undefined; + let keylessApiKeysUrl: string | undefined; + + if (canUseKeyless) { + try { + // Get runtime config to access configured keys + // @ts-expect-error: Nitro import. Handled by Nuxt. + const { useRuntimeConfig } = await import('#imports'); + const runtimeConfig = useRuntimeConfig(event); + + const { publishableKey, secretKey, claimUrl, apiKeysUrl } = await resolveKeysWithKeylessFallback( + runtimeConfig.public.clerk.publishableKey, + runtimeConfig.clerk.secretKey, + event, + ); + + keylessClaimUrl = claimUrl; + keylessApiKeysUrl = apiKeysUrl; + + // Override runtime config with keyless values if returned + if (publishableKey) { + runtimeConfig.public.clerk.publishableKey = publishableKey; + } + if (secretKey) { + runtimeConfig.clerk.secretKey = secretKey; + } + } catch { + // Silently fail - continue without keyless + } + } + const requestState = await clerkClient(event).authenticateRequest(clerkRequest, { ...options, acceptsToken: 'any', @@ -117,6 +151,14 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => { // Internal serializable state that will be passed to the client event.context.__clerk_initial_state = createInitialState(authObjectFn()); + // Store keyless mode URLs in separate context property + if (canUseKeyless && keylessClaimUrl) { + event.context.__clerk_keyless = { + claimUrl: keylessClaimUrl, + apiKeysUrl: keylessApiKeysUrl, + }; + } + await handler?.(event); }); }; diff --git a/packages/nuxt/src/runtime/server/index.ts b/packages/nuxt/src/runtime/server/index.ts index 6ce2112d43c..f919e9e14b9 100644 --- a/packages/nuxt/src/runtime/server/index.ts +++ b/packages/nuxt/src/runtime/server/index.ts @@ -2,4 +2,4 @@ export * from '@clerk/backend'; export { clerkClient } from './clerkClient'; export { clerkMiddleware } from './clerkMiddleware'; export { createRouteMatcher } from './routeMatcher'; -export type { AuthFn } from './types'; +export type { AuthFn, ClerkKeylessContext } from './types'; diff --git a/packages/nuxt/src/runtime/server/keyless/fileStorage.ts b/packages/nuxt/src/runtime/server/keyless/fileStorage.ts new file mode 100644 index 00000000000..750731222bd --- /dev/null +++ b/packages/nuxt/src/runtime/server/keyless/fileStorage.ts @@ -0,0 +1,29 @@ +import { createNodeFileStorage, type KeylessStorage } from '@clerk/shared/keyless'; + +export type { KeylessStorage }; + +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; + + try { + const [fs, path] = await Promise.all([import('node:fs'), import('node:path')]); + + return createNodeFileStorage(fs, path, { + cwd, + frameworkPackageName: '@clerk/nuxt', + }); + } catch { + throw new Error( + 'Keyless mode requires a Node.js runtime with file system access. ' + + 'Set NUXT_PUBLIC_CLERK_KEYLESS_DISABLED=1 or CLERK_KEYLESS_DISABLED=1 to disable keyless mode.', + ); + } +} diff --git a/packages/nuxt/src/runtime/server/keyless/index.ts b/packages/nuxt/src/runtime/server/keyless/index.ts new file mode 100644 index 00000000000..120b56b2692 --- /dev/null +++ b/packages/nuxt/src/runtime/server/keyless/index.ts @@ -0,0 +1,86 @@ +import { createKeylessService } from '@clerk/shared/keyless'; +import type { H3Event } from 'h3'; + +import { clerkClient } from '../clerkClient'; +import { createFileStorage } from './fileStorage'; + +let keylessServiceInstance: ReturnType | null = null; +let keylessInitPromise: Promise | null> | null = null; + +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(event: H3Event): Promise | null> { + if (!canUseFileSystem()) { + return null; + } + + if (keylessServiceInstance) { + return keylessServiceInstance; + } + + if (keylessInitPromise) { + return keylessInitPromise; + } + + keylessInitPromise = (async () => { + try { + const storage = await createFileStorage(); + + const service = createKeylessService({ + storage, + api: { + async createAccountlessApplication(requestHeaders?: Headers) { + try { + return await clerkClient(event).__experimental_accountlessApplications.createAccountlessApplication({ + requestHeaders, + }); + } catch { + return null; + } + }, + async completeOnboarding(requestHeaders?: Headers) { + try { + return await clerkClient( + event, + ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: 'nuxt', + 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/nuxt/src/runtime/server/keyless/utils.ts b/packages/nuxt/src/runtime/server/keyless/utils.ts new file mode 100644 index 00000000000..3943211df9f --- /dev/null +++ b/packages/nuxt/src/runtime/server/keyless/utils.ts @@ -0,0 +1,24 @@ +import { resolveKeysWithKeylessFallback as sharedResolveKeysWithKeylessFallback } from '@clerk/shared/keyless'; +import type { H3Event } from 'h3'; + +import { canUseKeyless } from '../../utils/feature-flags'; +import { keyless } from './index'; + +export type { KeylessResult } from '@clerk/shared/keyless'; + +/** + * 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, + event: H3Event, +) { + const keylessService = await keyless(event); + return sharedResolveKeysWithKeylessFallback( + configuredPublishableKey, + configuredSecretKey, + keylessService, + canUseKeyless, + ); +} diff --git a/packages/nuxt/src/runtime/server/types.ts b/packages/nuxt/src/runtime/server/types.ts index c4369d57e23..0700b3531f3 100644 --- a/packages/nuxt/src/runtime/server/types.ts +++ b/packages/nuxt/src/runtime/server/types.ts @@ -7,3 +7,11 @@ export type AuthOptions = PendingSessionOptions & Pick Date: Fri, 13 Feb 2026 10:03:46 -0800 Subject: [PATCH 2/4] chore: clean up keyless logic --- packages/astro/src/server/keyless/index.ts | 106 +++++----------- packages/nuxt/src/runtime/plugin.ts | 16 ++- .../src/runtime/server/keyless/fileStorage.ts | 26 ++-- .../nuxt/src/runtime/server/keyless/index.ts | 106 +++++----------- .../react-router/src/server/keyless/index.ts | 114 +++++------------- 5 files changed, 107 insertions(+), 261 deletions(-) diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index 641fb961510..7c1bb31353e 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -4,83 +4,37 @@ import type { APIContext } from 'astro'; import { clerkClient } from '../clerk-client'; import { createFileStorage } from './file-storage.js'; +// Lazily initialized keyless service singleton let keylessServiceInstance: ReturnType | null = null; -let keylessInitPromise: Promise | null> | null = null; -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): Promise | null> { - if (!canUseFileSystem()) { - return null; - } - - if (keylessServiceInstance) { - return keylessServiceInstance; - } - - if (keylessInitPromise) { - return keylessInitPromise; - } - - keylessInitPromise = (async () => { - try { - const storage = await createFileStorage(); - - const service = createKeylessService({ - storage, - api: { - async createAccountlessApplication(requestHeaders?: Headers) { - try { - return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({ - requestHeaders, - }); - } catch { - return null; - } - }, - async completeOnboarding(requestHeaders?: Headers) { - try { - return await clerkClient( - context, - ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ - requestHeaders, - }); - } catch { - return null; - } - }, +export function keyless(context: APIContext) { + if (!keylessServiceInstance) { + keylessServiceInstance = createKeylessService({ + storage: createFileStorage(), + api: { + async createAccountlessApplication(requestHeaders?: Headers) { + try { + return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({ + 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; + async completeOnboarding(requestHeaders?: Headers) { + try { + return await clerkClient( + context, + ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: 'astro', + }); + } + return keylessServiceInstance; } diff --git a/packages/nuxt/src/runtime/plugin.ts b/packages/nuxt/src/runtime/plugin.ts index 699edcd5970..0e2a344c205 100644 --- a/packages/nuxt/src/runtime/plugin.ts +++ b/packages/nuxt/src/runtime/plugin.ts @@ -24,14 +24,6 @@ export default defineNuxtPlugin(nuxtApp => { const runtimeConfig = useRuntimeConfig(); const clerkConfig = runtimeConfig.public.clerk ?? {}; - // Add keyless mode props if present - const keylessProps = keylessContext.value - ? { - __internal_keyless_claimKeylessApplicationUrl: keylessContext.value.claimUrl, - __internal_keyless_copyInstanceKeysUrl: keylessContext.value.apiKeysUrl, - } - : {}; - nuxtApp.vueApp.use(clerkPlugin as any, { ...clerkConfig, // Map jsUrl/uiUrl to clerkJSUrl/clerkUIUrl as expected by the Vue plugin @@ -45,6 +37,12 @@ export default defineNuxtPlugin(nuxtApp => { routerPush: (to: string) => navigateTo(to), routerReplace: (to: string) => navigateTo(to, { replace: true }), initialState: initialState.value, - ...keylessProps, + // Add keyless mode props if present + ...(keylessContext.value + ? { + __internal_keyless_claimKeylessApplicationUrl: keylessContext.value.claimUrl, + __internal_keyless_copyInstanceKeysUrl: keylessContext.value.apiKeysUrl, + } + : {}), }); }); diff --git a/packages/nuxt/src/runtime/server/keyless/fileStorage.ts b/packages/nuxt/src/runtime/server/keyless/fileStorage.ts index 750731222bd..340b011dcef 100644 --- a/packages/nuxt/src/runtime/server/keyless/fileStorage.ts +++ b/packages/nuxt/src/runtime/server/keyless/fileStorage.ts @@ -1,3 +1,6 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + import { createNodeFileStorage, type KeylessStorage } from '@clerk/shared/keyless'; export type { KeylessStorage }; @@ -6,24 +9,11 @@ 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 { +export function createFileStorage(options: FileStorageOptions = {}): KeylessStorage { const { cwd = () => process.cwd() } = options; - try { - const [fs, path] = await Promise.all([import('node:fs'), import('node:path')]); - - return createNodeFileStorage(fs, path, { - cwd, - frameworkPackageName: '@clerk/nuxt', - }); - } catch { - throw new Error( - 'Keyless mode requires a Node.js runtime with file system access. ' + - 'Set NUXT_PUBLIC_CLERK_KEYLESS_DISABLED=1 or CLERK_KEYLESS_DISABLED=1 to disable keyless mode.', - ); - } + return createNodeFileStorage(fs, path, { + cwd, + frameworkPackageName: '@clerk/nuxt', + }); } diff --git a/packages/nuxt/src/runtime/server/keyless/index.ts b/packages/nuxt/src/runtime/server/keyless/index.ts index 120b56b2692..0ee1a4fac47 100644 --- a/packages/nuxt/src/runtime/server/keyless/index.ts +++ b/packages/nuxt/src/runtime/server/keyless/index.ts @@ -4,83 +4,37 @@ import type { H3Event } from 'h3'; import { clerkClient } from '../clerkClient'; import { createFileStorage } from './fileStorage'; +// Lazily initialized keyless service singleton let keylessServiceInstance: ReturnType | null = null; -let keylessInitPromise: Promise | null> | null = null; -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(event: H3Event): Promise | null> { - if (!canUseFileSystem()) { - return null; - } - - if (keylessServiceInstance) { - return keylessServiceInstance; - } - - if (keylessInitPromise) { - return keylessInitPromise; - } - - keylessInitPromise = (async () => { - try { - const storage = await createFileStorage(); - - const service = createKeylessService({ - storage, - api: { - async createAccountlessApplication(requestHeaders?: Headers) { - try { - return await clerkClient(event).__experimental_accountlessApplications.createAccountlessApplication({ - requestHeaders, - }); - } catch { - return null; - } - }, - async completeOnboarding(requestHeaders?: Headers) { - try { - return await clerkClient( - event, - ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ - requestHeaders, - }); - } catch { - return null; - } - }, +export function keyless(event: H3Event) { + if (!keylessServiceInstance) { + keylessServiceInstance = createKeylessService({ + storage: createFileStorage(), + api: { + async createAccountlessApplication(requestHeaders?: Headers) { + try { + return await clerkClient(event).__experimental_accountlessApplications.createAccountlessApplication({ + requestHeaders, + }); + } catch { + return null; + } }, - framework: 'nuxt', - 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; + async completeOnboarding(requestHeaders?: Headers) { + try { + return await clerkClient( + event, + ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: 'nuxt', + }); + } + return keylessServiceInstance; } diff --git a/packages/react-router/src/server/keyless/index.ts b/packages/react-router/src/server/keyless/index.ts index a0ec434b19a..2874e022481 100644 --- a/packages/react-router/src/server/keyless/index.ts +++ b/packages/react-router/src/server/keyless/index.ts @@ -5,90 +5,40 @@ import type { DataFunctionArgs } from '../loadOptions'; import type { ClerkMiddlewareOptions } from '../types'; import { createFileStorage } from './fileStorage'; +// Lazily initialized keyless service singleton let keylessServiceInstance: ReturnType | null = null; -let keylessInitPromise: Promise | null> | null = null; -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( - args: DataFunctionArgs, - options?: ClerkMiddlewareOptions, -): Promise | null> { - if (!canUseFileSystem()) { - return null; - } - - if (keylessServiceInstance) { - return keylessServiceInstance; - } - - if (keylessInitPromise) { - return keylessInitPromise; - } - - keylessInitPromise = (async () => { - try { - const storage = await createFileStorage(); - - const service = createKeylessService({ - storage, - api: { - async createAccountlessApplication(requestHeaders?: Headers) { - try { - return await clerkClient( - args, - options, - ).__experimental_accountlessApplications.createAccountlessApplication({ - requestHeaders, - }); - } catch { - return null; - } - }, - async completeOnboarding(requestHeaders?: Headers) { - try { - return await clerkClient( - args, - options, - ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ +export function keyless(args: DataFunctionArgs, options?: ClerkMiddlewareOptions) { + if (!keylessServiceInstance) { + keylessServiceInstance = createKeylessService({ + storage: createFileStorage(), + api: { + async createAccountlessApplication(requestHeaders?: Headers) { + try { + return await clerkClient(args, options).__experimental_accountlessApplications.createAccountlessApplication( + { requestHeaders, - }); - } catch { - return null; - } - }, + }, + ); + } catch { + return null; + } }, - framework: 'react-router', - 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; + async completeOnboarding(requestHeaders?: Headers) { + try { + return await clerkClient( + args, + options, + ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + requestHeaders, + }); + } catch { + return null; + } + }, + }, + framework: 'react-router', + }); + } + return keylessServiceInstance; } From e80b974d2bacf820f1c6616763ced9498640de36 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 13 Feb 2026 10:08:36 -0800 Subject: [PATCH 3/4] chore: remove unused type --- packages/nuxt/src/module.ts | 4 +--- packages/nuxt/src/runtime/server/index.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index fe739ee33f8..d8b574c7f4a 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -115,14 +115,12 @@ export default defineNuxtModule({ addTypeTemplate( { filename: 'types/clerk.d.ts', - getContents: () => `import type { AuthFn, ClerkKeylessContext } from '@clerk/nuxt/server'; + getContents: () => `import type { AuthFn } from '@clerk/nuxt/server'; import type { InitialState } from '@clerk/shared/types'; declare module 'h3' { interface H3EventContext { auth: AuthFn; - __clerk_initial_state?: InitialState; - __clerk_keyless?: ClerkKeylessContext; } } `, diff --git a/packages/nuxt/src/runtime/server/index.ts b/packages/nuxt/src/runtime/server/index.ts index f919e9e14b9..6ce2112d43c 100644 --- a/packages/nuxt/src/runtime/server/index.ts +++ b/packages/nuxt/src/runtime/server/index.ts @@ -2,4 +2,4 @@ export * from '@clerk/backend'; export { clerkClient } from './clerkClient'; export { clerkMiddleware } from './clerkMiddleware'; export { createRouteMatcher } from './routeMatcher'; -export type { AuthFn, ClerkKeylessContext } from './types'; +export type { AuthFn } from './types'; From ac444eeb3418e22dae9f31970457dfe2df373bc7 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 13 Feb 2026 10:12:05 -0800 Subject: [PATCH 4/4] chore: add changeset Introduce Keyless quickstart for Nuxt, enabling use of the Clerk SDK without manual key setup. --- .changeset/lazy-turkeys-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lazy-turkeys-switch.md diff --git a/.changeset/lazy-turkeys-switch.md b/.changeset/lazy-turkeys-switch.md new file mode 100644 index 00000000000..dcd5a400652 --- /dev/null +++ b/.changeset/lazy-turkeys-switch.md @@ -0,0 +1,5 @@ +--- +"@clerk/nuxt": minor +--- + +Introduce Keyless quickstart for Nuxt. This allows the Clerk SDK to be used without having to sign up and paste your keys manually.