Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 469
feat(nuxt): Add support for keyless mode#7844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
b94387301c26c4e80b974ac444eef8e080d2ae15e66e0497f7134248e476a004c3d66071566de587cf4f72fa4939bd3d891f723b65f8e1ccb5653faa1223fe836c54d9e8186d61070c1c7f39e93416006d29eba9ad8328eec0db85c37d62cFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@clerk/astro": patch | ||
| "@clerk/react-router": patch | ||
| --- | ||
| Simplified keyless service initialization. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof createKeylessService> | null = null; | ||
| let keylessInitPromise: Promise<ReturnType<typeof createKeylessService> | 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<ReturnType<typeof createKeylessService> | 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: { | ||
wobsoriano marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,41 @@ | ||
| import { createClerkClient } from '@clerk/backend'; | ||
| import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; | ||
| import { deprecated } from '@clerk/shared/deprecated'; | ||
| import { isTruthy } from '@clerk/shared/underscore'; | ||
| import type { H3Event } from 'h3'; | ||
| // @ts-expect-error: Nitro import. Handled by Nuxt. | ||
| import { useRuntimeConfig } from '#imports'; | ||
| function resolveApiUrl(runtimeConfig: ReturnType<typeof useRuntimeConfig>): string { | ||
| if (runtimeConfig.clerk.apiUrl) { | ||
| return runtimeConfig.clerk.apiUrl; | ||
| } | ||
| if (runtimeConfig.public.clerk.apiUrl) { | ||
| deprecated('NUXT_PUBLIC_CLERK_API_URL', 'Use `NUXT_CLERK_API_URL` instead.'); | ||
| return runtimeConfig.public.clerk.apiUrl; | ||
| } | ||
| return apiUrlFromPublishableKey(runtimeConfig.public.clerk.publishableKey); | ||
| } | ||
| function resolveApiVersion(runtimeConfig: ReturnType<typeof useRuntimeConfig>): string { | ||
| if (runtimeConfig.clerk.apiVersion) { | ||
| return runtimeConfig.clerk.apiVersion; | ||
| } | ||
| if (runtimeConfig.public.clerk.apiVersion) { | ||
| deprecated('NUXT_PUBLIC_CLERK_API_VERSION', 'Use `NUXT_CLERK_API_VERSION` instead.'); | ||
| return runtimeConfig.public.clerk.apiVersion; | ||
| } | ||
| return 'v1'; | ||
| } | ||
wobsoriano marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| export function clerkClient(event: H3Event) { | ||
| const runtimeConfig = useRuntimeConfig(event); | ||
| const publishableKey = runtimeConfig.public.clerk.publishableKey; | ||
| const apiUrl = runtimeConfig.public.clerk.apiUrl || apiUrlFromPublishableKey(publishableKey); | ||
| return createClerkClient({ | ||
| publishableKey, | ||
| apiUrl, | ||
| apiVersion: runtimeConfig.public.clerk.apiVersion, | ||
| publishableKey: runtimeConfig.public.clerk.publishableKey, | ||
| apiUrl: resolveApiUrl(runtimeConfig), | ||
| apiVersion: resolveApiVersion(runtimeConfig), | ||
| proxyUrl: runtimeConfig.public.clerk.proxyUrl, | ||
| domain: runtimeConfig.public.clerk.domain, | ||
| isSatellite: runtimeConfig.public.clerk.isSatellite, | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -5,7 +5,12 @@ import type { PendingSessionOptions } from '@clerk/shared/types'; | ||
| import type { EventHandler } from 'h3'; | ||
| import { createError, eventHandler, setResponseHeader } from 'h3'; | ||
| // @ts-expect-error: Nitro import. Handled by Nuxt. | ||
| import { useRuntimeConfig } from '#imports'; | ||
| 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 +87,35 @@ 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 { | ||
| 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 | ||
| } | ||
wobsoriano marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| 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); | ||
| }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import * as fs from 'node:fs'; | ||
| import * as path from 'node:path'; | ||
| import { createNodeFileStorage, type KeylessStorage } from '@clerk/shared/keyless'; | ||
| export type { KeylessStorage }; | ||
| export interface FileStorageOptions { | ||
| cwd?: () => string; | ||
| } | ||
| export function createFileStorage(options: FileStorageOptions = {}): KeylessStorage { | ||
| const { cwd = () => process.cwd() } = options; | ||
| return createNodeFileStorage(fs, path, { | ||
| cwd, | ||
| frameworkPackageName: '@clerk/nuxt', | ||
| }); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.