-
Notifications
You must be signed in to change notification settings - Fork 435
feat(nuxt): [WIP] 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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
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,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 |
|---|---|---|
| @@ -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', | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { createKeylessService } from '@clerk/shared/keyless'; | ||
| import type { H3Event } from 'h3'; | ||
|
|
||
| import { clerkClient } from '../clerkClient'; | ||
| import { createFileStorage } from './fileStorage'; | ||
|
|
||
| // Lazily initialized keyless service singleton | ||
| let keylessServiceInstance: ReturnType<typeof createKeylessService> | null = 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; | ||
| } | ||
| }, | ||
| async completeOnboarding(requestHeaders?: Headers) { | ||
| try { | ||
| return await clerkClient( | ||
| event, | ||
| ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ | ||
| requestHeaders, | ||
| }); | ||
| } catch { | ||
| return null; | ||
| } | ||
| }, | ||
| }, | ||
| framework: 'nuxt', | ||
| }); | ||
| } | ||
| return keylessServiceInstance; | ||
|
Comment on lines
+7
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Singleton captures the first request’s H3Event Line 10-38 creates a module-scoped singleton whose API closures close over the first 🔧 One safe fix (cache storage only, re-create service per request)-// Lazily initialized keyless service singleton
-let keylessServiceInstance: ReturnType<typeof createKeylessService> | null = null;
+// Cache storage only; service must be per-request to avoid leaking event context
+let storageInstance: ReturnType<typeof createFileStorage> | null = null;
+const getStorage = () => (storageInstance ??= createFileStorage());
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;
- }
- },
- async completeOnboarding(requestHeaders?: Headers) {
- try {
- return await clerkClient(
- event,
- ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({
- requestHeaders,
- });
- } catch {
- return null;
- }
- },
- },
- framework: 'nuxt',
- });
- }
- return keylessServiceInstance;
+ return createKeylessService({
+ storage: getStorage(),
+ 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',
+ });
}🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { getEnvVariable } from '@clerk/shared/getEnvVariable'; | ||
| import { isTruthy } from '@clerk/shared/underscore'; | ||
| import { isDevelopmentEnvironment } from '@clerk/shared/utils'; | ||
|
|
||
| const KEYLESS_DISABLED = | ||
| isTruthy(getEnvVariable('NUXT_PUBLIC_CLERK_KEYLESS_DISABLED')) || | ||
| isTruthy(getEnvVariable('CLERK_KEYLESS_DISABLED')) || | ||
| false; | ||
|
|
||
| export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check the structure and read the keyless index.ts file cat -n packages/astro/src/server/keyless/index.tsRepository: clerk/javascript
Length of output: 1606
🏁 Script executed:
# Check if file-storage.ts exists and what it imports cat -n packages/astro/src/server/keyless/file-storage.tsRepository: clerk/javascript
Length of output: 678
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 75
🏁 Script executed:
Repository: clerk/javascript
Length of output: 76
🏁 Script executed:
# Check package.json for runtime specifications and Astro version cat packages/astro/package.jsonRepository: clerk/javascript
Length of output: 3084
🏁 Script executed:
Repository: clerk/javascript
Length of output: 328
🏁 Script executed:
Repository: clerk/javascript
Length of output: 233
🏁 Script executed:
Repository: clerk/javascript
Length of output: 292
🏁 Script executed:
Repository: clerk/javascript
Length of output: 50375
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
Repository: clerk/javascript
Length of output: 875
🏁 Script executed:
Repository: clerk/javascript
Length of output: 863
🏁 Script executed:
Repository: clerk/javascript
Length of output: 918
🏁 Script executed:
Repository: clerk/javascript
Length of output: 2054
🏁 Script executed:
Repository: clerk/javascript
Length of output: 1008
🏁 Script executed:
# Check the keyless/utils.ts to see the wrapper cat -n packages/astro/src/server/keyless/utils.tsRepository: clerk/javascript
Length of output: 1006
🏁 Script executed:
# Check the feature flag that controls keyless cat -n packages/astro/src/utils/feature-flags.tsRepository: clerk/javascript
Length of output: 542
🏁 Script executed:
Repository: clerk/javascript
Length of output: 3108
Guard Node-only storage to prevent edge/serverless runtime crashes.
The
keyless()function at line 12–14 callscreateFileStorage(), which imports Node.jsfsandpathmodules (from file-storage.ts). WhenresolveKeysWithKeylessFallback()is invoked in the middleware (clerk-middleware.ts line 95), it unconditionally callskeyless(context), causing the import to execute even in edge runtimes (Cloudflare, Vercel Edge, etc.) where these modules are unavailable. AlthoughcanUseKeylessprevents the service from being used in non-development environments, it does not prevent the module from being loaded—and the import fails at load time.Add a runtime guard in the
keyless()function to throw a clear error when Node.js is unavailable, or move thefile-storageimport behind a dynamic import so the failure only occurs if keyless is actually used.Proposed fix
export function keyless(context: APIContext) { if (!keylessServiceInstance) { + if (typeof process === 'undefined' || !process.versions?.node) { + throw new Error( + 'Keyless mode for `@clerk/astro` requires a Node.js runtime with filesystem access. ' + + 'Use a Node adapter or disable keyless.' + ); + } keylessServiceInstance = createKeylessService({ storage: createFileStorage(),🤖 Prompt for AI Agents