feat(nuxt): [WIP] Add support for keyless mode#7844
Conversation
🦋 Changeset detectedLatest commit: ac444ee The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis pull request introduces keyless mode support across the Clerk SDKs for Nuxt, Astro, and React Router. Changes include a new Nuxt keyless quickstart release, runtime middleware and plugin integration to resolve and inject keyless configuration, new keyless service implementations with file storage adapters, feature flag controls, type definitions for keyless context, and test coverage for Nuxt keyless functionality. The keyless service implementations in Astro and React Router are refactored from async promise-based initialization to synchronous lazy singletons. Configuration is conditionally populated from keyless resolution logic during request processing and made available to client-side initialization. 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Introduce Keyless quickstart for Nuxt, enabling use of the Clerk SDK without manual key setup.
@clerk/agent-toolkit
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/dev-cli
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@packages/astro/src/server/keyless/index.ts`:
- Around line 10-14: The keyless() factory unconditionally calls
createFileStorage(), which imports Node-only modules and crashes in edge
runtimes; add a runtime Node check at the start of keyless() (e.g., verify
typeof process !== 'undefined' && process.versions?.node) and if Node is not
available throw a clear error explaining that keyless storage requires a Node
runtime, so createFileStorage() is never invoked in non-Node environments; keep
references to keyless(), keylessServiceInstance, createFileStorage(),
resolveKeysWithKeylessFallback(), and canUseKeyless when making this change.
In `@packages/nuxt/src/runtime/server/keyless/index.ts`:
- Around line 7-39: The current module-level singleton keylessServiceInstance
closes over the first request's event causing stale request context; change to
cache only the storage (e.g., createFileStorage) at module scope and instantiate
createKeylessService per request inside the keyless(event) function so that api
methods call clerkClient(event) with the current H3Event; specifically, keep a
cached storage variable (instead of keylessServiceInstance), and in
keyless(event) call createKeylessService({ storage: cachedStorage, api: {
createAccountlessApplication: async (headers?) => clerkClient(event)...,
completeOnboarding: async (headers?) => clerkClient(event)... }, framework:
'nuxt' }) so no module-level closure captures the first event.
| export function keyless(context: APIContext) { | ||
| if (!keylessServiceInstance) { | ||
| keylessServiceInstance = createKeylessService({ | ||
| storage: createFileStorage(), | ||
| api: { |
There was a problem hiding this comment.
🧩 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:
# Look for any existing runtime guards or environment checks
rg -n "process\.versions|typeof.*undefined|canUseFileSystem|runtime|edge" packages/astro/src/server/keyless/ -g '*.ts'Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
# Check if there are tests or usage examples of keyless in edge contexts
find packages/astro -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.astro" | xargs rg -l "keyless" 2>/dev/null || echo "No test results"Repository: clerk/javascript
Length of output: 75
🏁 Script executed:
# Check what adapters or runtimes Astro supports and if edge is documented
rg -n "edge|adapter|runtime" packages/astro/README.md packages/astro/package.json 2>/dev/null || echo "No matches found"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:
# Look for any mentions of edge, runtime, or alternative storage implementations
rg -n "edge|runtime|serverless|storage" packages/astro/package.jsonRepository: clerk/javascript
Length of output: 328
🏁 Script executed:
# Check if there are any conditional imports or dynamic requires elsewhere in the codebase
rg -n "dynamic.*import|lazy.*load" packages/astro/src/ -g '*.ts'Repository: clerk/javascript
Length of output: 233
🏁 Script executed:
# Look for environment-specific code in the broader astro package
fd . packages/astro/src --type f -name "*.ts" | head -20 | xargs rg -l "process\|globalThis\|typeof"Repository: clerk/javascript
Length of output: 292
🏁 Script executed:
# Check if there's any reference to edge adapters in Astro documentation or config
find packages/astro -name "astro.config.*" -o -name "*.md" 2>/dev/null | xargs cat 2>/dev/null || echo "No config/docs found"Repository: clerk/javascript
Length of output: 50375
🏁 Script executed:
# Check if keyless is documented as supporting edge or if it's marked Node-only
find packages/astro -name "README*" -o -name "*.md" | xargs rg -l "keyless" 2>/dev/null | head -3Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
# Look for any comments or TODOs about edge/runtime support in the keyless module
rg -n "TODO|FIXME|NOTE|edge|serverless|runtime|workerd" packages/astro/src/server/keyless/ -g '*.ts'Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
# Check if keyless is exported from the main server export
rg -n "keyless" packages/astro/src/server/index.tsRepository: clerk/javascript
Length of output: 42
🏁 Script executed:
# Verify if there are any tests that use keyless in edge contexts
find packages/astro -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) -exec grep -l "keyless" {} \;Repository: clerk/javascript
Length of output: 42
🏁 Script executed:
# Check what is exported from packages/astro/src/server/index.ts
cat -n packages/astro/src/server/index.ts | head -30Repository: clerk/javascript
Length of output: 875
🏁 Script executed:
# Look for any import of keyless in the broader codebase to see actual usage
rg -n "from.*keyless|import.*keyless" packages/astro -g '*.ts' -g '*.tsx'Repository: clerk/javascript
Length of output: 863
🏁 Script executed:
# Verify if there's a guard pattern used elsewhere in the astro package for edge safety
rg -n "typeof.*undefined|process\.versions|globalThis" packages/astro/src -g '*.ts' -A 2Repository: clerk/javascript
Length of output: 918
🏁 Script executed:
# Check how keyless is used in clerk-middleware.ts
cat -n packages/astro/src/server/clerk-middleware.ts | head -50Repository: clerk/javascript
Length of output: 2054
🏁 Script executed:
# Look at the actual usage of resolveKeysWithKeylessFallback to understand when keyless() is called
rg -n "resolveKeysWithKeylessFallback" packages/astro/src/server/clerk-middleware.ts -B 5 -A 5Repository: 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:
# Check if the middleware is executed for all environments including edge
rg -n "clerkMiddleware|onRequest" packages/astro/src/server/ -B 3 -A 3 | head -40Repository: clerk/javascript
Length of output: 3108
Guard Node-only storage to prevent edge/serverless runtime crashes.
The keyless() function at line 12–14 calls createFileStorage(), which imports Node.js fs and path modules (from file-storage.ts). When resolveKeysWithKeylessFallback() is invoked in the middleware (clerk-middleware.ts line 95), it unconditionally calls keyless(context), causing the import to execute even in edge runtimes (Cloudflare, Vercel Edge, etc.) where these modules are unavailable. Although canUseKeyless prevents 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 the file-storage import 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
In `@packages/astro/src/server/keyless/index.ts` around lines 10 - 14, The
keyless() factory unconditionally calls createFileStorage(), which imports
Node-only modules and crashes in edge runtimes; add a runtime Node check at the
start of keyless() (e.g., verify typeof process !== 'undefined' &&
process.versions?.node) and if Node is not available throw a clear error
explaining that keyless storage requires a Node runtime, so createFileStorage()
is never invoked in non-Node environments; keep references to keyless(),
keylessServiceInstance, createFileStorage(), resolveKeysWithKeylessFallback(),
and canUseKeyless when making this change.
| // 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; |
There was a problem hiding this comment.
Singleton captures the first request’s H3Event
Line 10-38 creates a module-scoped singleton whose API closures close over the first event. Subsequent requests will reuse that stale event when calling clerkClient, which can leak request-specific context (headers/cookies) across requests. Make the keyless service per-request, or keep only storage cached and rebind API functions per call.
🔧 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
In `@packages/nuxt/src/runtime/server/keyless/index.ts` around lines 7 - 39, The
current module-level singleton keylessServiceInstance closes over the first
request's event causing stale request context; change to cache only the storage
(e.g., createFileStorage) at module scope and instantiate createKeylessService
per request inside the keyless(event) function so that api methods call
clerkClient(event) with the current H3Event; specifically, keep a cached storage
variable (instead of keylessServiceInstance), and in keyless(event) call
createKeylessService({ storage: cachedStorage, api: {
createAccountlessApplication: async (headers?) => clerkClient(event)...,
completeOnboarding: async (headers?) => clerkClient(event)... }, framework:
'nuxt' }) so no module-level closure captures the first event.
| 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; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check how other framework implementations handle the keyless pattern
echo "=== Checking Astro keyless implementation ==="
cat packages/astro/src/server/keyless/index.ts 2>/dev/null | head -50
echo -e "\n=== Checking Nuxt keyless implementation ==="
cat packages/nuxt/src/runtime/server/keyless/index.ts 2>/dev/null | head -50
echo -e "\n=== Check how keyless() is called in React Router middleware ==="
rg -n "keyless\(" packages/react-router/src/ -B3 -A3
echo -e "\n=== Check exports from index.ts ==="
cat packages/react-router/src/server/keyless/index.tsRepository: clerk/javascript
Length of output: 5506
Singleton captures stale request context from first invocation.
The args and options parameters are captured in the closure when the singleton is first created. However, keyless() is called on every request with new args and options (see packages/react-router/src/server/keyless/utils.ts:18). Since DataFunctionArgs contains request-specific middleware data, all API calls after the first request will use stale context from the initial request that triggered singleton creation, causing:
- Incorrect/stale request headers in API calls
- Wrong authentication context for subsequent requests
- Potential request data leakage across different users/requests
Refactor to avoid closing over request-specific values. Either:
- Pass
argsandoptionsto the API methods (not tocreateKeylessService) so they receive fresh values on each call - Recreate or reinitialize the service with current request context instead of using a global singleton
Description
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change
Summary by CodeRabbit
New Features
Tests