diff --git a/.changeset/tanstack-keyless-cli-init-error.md b/.changeset/tanstack-keyless-cli-init-error.md new file mode 100644 index 00000000000..d321f6d87cb --- /dev/null +++ b/.changeset/tanstack-keyless-cli-init-error.md @@ -0,0 +1,5 @@ +--- +'@clerk/tanstack-react-start': minor +--- + +In development, missing Clerk keys no longer activate keyless mode. When `VITE_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` are not set, the SDK now fails with an error directing you to run `npx clerk@latest init`, which provisions a Clerk application and writes the keys to `.env.local`. Keys that keyless mode stored in the `.clerk/` directory are no longer read, and `VITE_CLERK_KEYLESS_DISABLED` and `CLERK_KEYLESS_DISABLED` no longer have any effect. Existing apps with configured keys are unaffected. diff --git a/integration/models/application.ts b/integration/models/application.ts index c74dab234f9..b8ca49851e0 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -66,6 +66,7 @@ export const application = ( const stdoutFilePath = path.resolve(appDirPath, `e2e.${now}.log`); const stderrFilePath = path.resolve(appDirPath, `e2e.${now}.err.log`); let buildOutput = ''; + let devOutput = ''; let serveOutput = ''; const self = { @@ -133,7 +134,12 @@ export const application = ( detached: opts.detached, stdout: opts.detached ? fs.openSync(stdoutFilePath, 'a') : undefined, stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined, - log: opts.detached ? undefined : log, + log: opts.detached + ? undefined + : (msg: string) => { + devOutput += `\n${msg}`; + log(msg); + }, }); const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0; @@ -199,6 +205,9 @@ export const application = ( get buildOutput() { return buildOutput; }, + get devOutput() { + return devOutput; + }, get serveOutput() { return serveOutput; }, diff --git a/integration/tests/tanstack-start/keyless.test.ts b/integration/tests/tanstack-start/keyless.test.ts index a2605806778..90309c9c72a 100644 --- a/integration/tests/tanstack-start/keyless.test.ts +++ b/integration/tests/tanstack-start/keyless.test.ts @@ -1,16 +1,11 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import type { Application } from '../../models/application'; import { appConfigs } from '../../presets'; -import { - testClaimedAppWithMissingKeys, - testKeylessRemovedAfterEnvAndRestart, - testToggleCollapsePopoverAndClaim, -} from '../../testUtils/keylessHelpers'; -const commonSetup = appConfigs.reactRouter.reactRouterNode.clone(); +const commonSetup = appConfigs.tanstack.reactStart.clone(); -test.describe('Keyless mode @react-router', () => { +test.describe('Keyless mode @tanstack-react-start', () => { test.describe.configure({ mode: 'serial' }); test.setTimeout(90_000); @@ -21,35 +16,25 @@ test.describe('Keyless mode @react-router', () => { }); 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(); + // Without keys the app 500s on every request, so readiness can't wait for a 2xx + await app.dev({ acceptAnyResponse: true }); }); 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: 'react-router' }); - }); - - test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({ + test('Without keys, requests fail with the missing env vars error instead of keyless bootstrap.', 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 }); + const response = await page.goto(`${app.serverUrl}/`); + expect(response?.status()).toBe(500); + expect(app.devOutput).toContain('Missing secretKey'); + expect(app.devOutput).toContain('npx clerk@latest init'); }); }); diff --git a/packages/tanstack-react-start/src/client/ClerkProvider.tsx b/packages/tanstack-react-start/src/client/ClerkProvider.tsx index 9231da2a554..f73a3310e1b 100644 --- a/packages/tanstack-react-start/src/client/ClerkProvider.tsx +++ b/packages/tanstack-react-start/src/client/ClerkProvider.tsx @@ -31,23 +31,13 @@ export function ClerkProvider({ const clerkInitState = isClient() ? (window as any).__clerk_init_state : clerkInitialState; - const { clerkSsrState, __keylessClaimUrl, __keylessApiKeysUrl, ...restInitState } = pickFromClerkInitState( - clerkInitState?.__internal_clerk_state, - ); + const { clerkSsrState, ...restInitState } = pickFromClerkInitState(clerkInitState?.__internal_clerk_state); const mergedProps = { ...mergeWithPublicEnvs(restInitState), ...providerProps, }; - // Add keyless mode props if present - const keylessProps = __keylessClaimUrl - ? { - __internal_keyless_claimKeylessApplicationUrl: __keylessClaimUrl, - __internal_keyless_copyInstanceKeysUrl: __keylessApiKeysUrl, - } - : {}; - return ( <> {`window.__clerk_init_state = ${htmlSafeJson(clerkInitialState)};`} @@ -74,7 +64,6 @@ export function ClerkProvider({ }); }} {...mergedProps} - {...keylessProps} > {children} diff --git a/packages/tanstack-react-start/src/client/utils.ts b/packages/tanstack-react-start/src/client/utils.ts index fd9bcea0afd..4198618b81e 100644 --- a/packages/tanstack-react-start/src/client/utils.ts +++ b/packages/tanstack-react-start/src/client/utils.ts @@ -10,8 +10,6 @@ export const pickFromClerkInitState = ( ): TanStackProviderAndInitialProps & InternalClerkScriptProps & { clerkSsrState: any; - __keylessClaimUrl?: string; - __keylessApiKeysUrl?: string; } => { const { __clerk_ssr_state, @@ -32,8 +30,6 @@ export const pickFromClerkInitState = ( __signUpForceRedirectUrl, __signInFallbackRedirectUrl, __signUpFallbackRedirectUrl, - __keylessClaimUrl, - __keylessApiKeysUrl, __prefetchUI, } = clerkInitState || {}; @@ -59,8 +55,6 @@ export const pickFromClerkInitState = ( signUpForceRedirectUrl: __signUpForceRedirectUrl, signInFallbackRedirectUrl: __signInFallbackRedirectUrl, signUpFallbackRedirectUrl: __signUpFallbackRedirectUrl, - __keylessClaimUrl, - __keylessApiKeysUrl, }; }; diff --git a/packages/tanstack-react-start/src/server/__tests__/loadOptions.test.ts b/packages/tanstack-react-start/src/server/__tests__/loadOptions.test.ts new file mode 100644 index 00000000000..09ff5aa0294 --- /dev/null +++ b/packages/tanstack-react-start/src/server/__tests__/loadOptions.test.ts @@ -0,0 +1,17 @@ +import type { ClerkRequest } from '@clerk/backend/internal'; +import { describe, expect, it, vi } from 'vitest'; + +import { loadOptions } from '../loadOptions'; + +vi.mock('../constants', () => ({ commonEnvs: () => ({}) })); + +const request = {} as ClerkRequest; + +describe('loadOptions', () => { + it.each([ + ['only the secret key is missing', { publishableKey: 'pk_test_Zm9vLWJhci0xMi5jbGVyay5hY2NvdW50cy5kZXYk' }], + ['both keys are missing', {}], + ])('throws the CLI-pointing error when %s', (_, overrides) => { + expect(() => loadOptions(request, overrides)).toThrow(/Missing secretKey[\s\S]*npx clerk@latest init/); + }); +}); diff --git a/packages/tanstack-react-start/src/server/clerkMiddleware.ts b/packages/tanstack-react-start/src/server/clerkMiddleware.ts index 5ba42112a7c..dfac9f37e7f 100644 --- a/packages/tanstack-react-start/src/server/clerkMiddleware.ts +++ b/packages/tanstack-react-start/src/server/clerkMiddleware.ts @@ -6,9 +6,7 @@ import type { PendingSessionOptions } from '@clerk/shared/types'; import type { AnyRequestMiddleware } from '@tanstack/react-start'; import { createMiddleware } from '@tanstack/react-start'; -import { canUseKeyless } from '../utils/feature-flags'; import { clerkClient } from './clerkClient'; -import { resolveKeysWithKeylessFallback } from './keyless/utils'; import { loadOptions } from './loadOptions'; import type { ClerkMiddlewareOptions, ClerkMiddlewareOptionsCallback } from './types'; import { getResponseClerkState } from './utils'; @@ -22,27 +20,7 @@ export const clerkMiddleware = ( // Resolve options: if function, call it with context object; otherwise use as-is const resolvedOptions = typeof options === 'function' ? await options({ url: clerkRequest.clerkUrl }) : options; - // Load options with resolved keys - const loadedOptions = loadOptions(clerkRequest, { - ...resolvedOptions, - publishableKey: resolvedOptions?.publishableKey, - secretKey: resolvedOptions?.secretKey, - }); - - // Get keys - either from options, env, or keyless mode - const { - publishableKey, - secretKey, - claimUrl: keylessClaimUrl, - apiKeysUrl: keylessApiKeysUrl, - } = await resolveKeysWithKeylessFallback(loadedOptions.publishableKey, loadedOptions.secretKey); - - if (publishableKey) { - loadedOptions.publishableKey = publishableKey; - } - if (secretKey) { - loadedOptions.secretKey = secretKey; - } + const loadedOptions = loadOptions(clerkRequest, resolvedOptions); const requestState = await clerkClient().authenticateRequest(clerkRequest, { ...loadedOptions, @@ -67,15 +45,6 @@ export const clerkMiddleware = ( const clerkInitialState = getResponseClerkState(requestState as RequestState, loadedOptions); - // Include keyless mode URLs if applicable - if (canUseKeyless && keylessClaimUrl) { - (clerkInitialState as Record).__internal_clerk_state = { - ...((clerkInitialState as Record).__internal_clerk_state as Record), - __keylessClaimUrl: keylessClaimUrl, - __keylessApiKeysUrl: keylessApiKeysUrl, - }; - } - const result = await next({ context: { clerkInitialState, diff --git a/packages/tanstack-react-start/src/server/keyless/fileStorage.ts b/packages/tanstack-react-start/src/server/keyless/fileStorage.ts deleted file mode 100644 index 24929cc7ebd..00000000000 --- a/packages/tanstack-react-start/src/server/keyless/fileStorage.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/tanstack-react-start', - }); -} diff --git a/packages/tanstack-react-start/src/server/keyless/index.ts b/packages/tanstack-react-start/src/server/keyless/index.ts deleted file mode 100644 index bb91fbedfa9..00000000000 --- a/packages/tanstack-react-start/src/server/keyless/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createKeylessService } from '@clerk/shared/keyless'; - -import { clerkClient } from '../clerkClient'; -import { createFileStorage } from './fileStorage'; - -// Lazily initialized keyless service singleton -let keylessServiceInstance: ReturnType | null = null; - -export function keyless() { - if (!keylessServiceInstance) { - keylessServiceInstance = createKeylessService({ - storage: createFileStorage(), - api: { - async createAccountlessApplication(requestHeaders?: Headers, source?: string) { - try { - return await clerkClient().__experimental_accountlessApplications.createAccountlessApplication({ - requestHeaders, - source, - }); - } catch { - return null; - } - }, - async completeOnboarding(requestHeaders?: Headers, source?: string) { - try { - return await clerkClient().__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ - requestHeaders, - source, - }); - } catch { - return null; - } - }, - }, - framework: 'tanstack-react-start', - }); - } - return keylessServiceInstance; -} diff --git a/packages/tanstack-react-start/src/server/keyless/utils.ts b/packages/tanstack-react-start/src/server/keyless/utils.ts deleted file mode 100644 index ab4896cadb5..00000000000 --- a/packages/tanstack-react-start/src/server/keyless/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -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'; - -/** - * Resolves Clerk keys, falling back to keyless mode in development if configured keys are missing. - * - * @param configuredPublishableKey - The publishable key from options or environment - * @param configuredSecretKey - The secret key from options or environment - * @returns The resolved keys (either configured or from keyless mode) - */ -export function resolveKeysWithKeylessFallback( - configuredPublishableKey: string | undefined, - configuredSecretKey: string | undefined, -) { - return sharedResolveKeysWithKeylessFallback(configuredPublishableKey, configuredSecretKey, keyless(), canUseKeyless); -} diff --git a/packages/tanstack-react-start/src/server/loadOptions.ts b/packages/tanstack-react-start/src/server/loadOptions.ts index 694494a8c6d..cbb72cdb125 100644 --- a/packages/tanstack-react-start/src/server/loadOptions.ts +++ b/packages/tanstack-react-start/src/server/loadOptions.ts @@ -5,7 +5,6 @@ import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy'; import { errorThrower } from '../utils'; -import { canUseKeyless } from '../utils/feature-flags'; import { commonEnvs } from './constants'; import type { LoaderOptions } from './types'; @@ -30,10 +29,8 @@ export const loadOptions = (request: ClerkRequest, overrides: LoaderOptions = {} proxyUrl = relativeOrAbsoluteProxyUrl; } - // In keyless mode, don't throw if secretKey is missing - ClerkProvider will handle it - if (!secretKey && !canUseKeyless) { - // eslint-disable-next-line @typescript-eslint/only-throw-error - throw errorThrower.throw('Clerk: no secret key provided'); + if (!secretKey) { + errorThrower.throwMissingSecretKeyError(); } if (isSatellite && !proxyUrl && !domain) { diff --git a/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts b/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts deleted file mode 100644 index 693278f61f9..00000000000 --- a/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { automatedEnvironmentVariables } from '@clerk/shared/utils'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -async function loadCanUseKeyless() { - vi.resetModules(); - const { canUseKeyless } = await import('../feature-flags.js'); - return canUseKeyless; -} - -describe('canUseKeyless', () => { - beforeEach(() => { - vi.stubEnv('NODE_ENV', 'development'); - vi.stubEnv('VITE_CLERK_KEYLESS_DISABLED', undefined); - vi.stubEnv('CLERK_KEYLESS_DISABLED', undefined); - automatedEnvironmentVariables.forEach(name => { - vi.stubEnv(name, undefined); - vi.stubGlobal(name, undefined); - }); - }); - - afterEach(() => { - vi.unstubAllEnvs(); - vi.unstubAllGlobals(); - vi.resetModules(); - }); - - it('enables keyless in development when automation signals are absent', async () => { - await expect(loadCanUseKeyless()).resolves.toBe(true); - }); - - it('disables keyless in CI even when the app runs in development mode', async () => { - vi.stubEnv('CI', 'true'); - - await expect(loadCanUseKeyless()).resolves.toBe(false); - }); - - it('disables keyless outside development mode', async () => { - vi.stubEnv('NODE_ENV', 'production'); - - await expect(loadCanUseKeyless()).resolves.toBe(false); - }); - - it('disables keyless when explicitly disabled', async () => { - vi.stubEnv('VITE_CLERK_KEYLESS_DISABLED', 'true'); - - await expect(loadCanUseKeyless()).resolves.toBe(false); - }); -}); diff --git a/packages/tanstack-react-start/src/utils/feature-flags.ts b/packages/tanstack-react-start/src/utils/feature-flags.ts deleted file mode 100644 index 4d129d8887c..00000000000 --- a/packages/tanstack-react-start/src/utils/feature-flags.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getEnvVariable } from '@clerk/shared/getEnvVariable'; -import { isTruthy } from '@clerk/shared/underscore'; -import { isAutomatedEnvironment, isDevelopmentEnvironment } from '@clerk/shared/utils'; - -// Support both Vite-style and generic env var names for disabling keyless mode -const KEYLESS_DISABLED = - isTruthy(getEnvVariable('VITE_CLERK_KEYLESS_DISABLED')) || - isTruthy(getEnvVariable('CLERK_KEYLESS_DISABLED')) || - false; - -/** - * Whether keyless mode can be used in the current environment. - * Keyless mode is only available in development, when not explicitly disabled, - * and when not running in an automated/CI environment. - * - * To disable keyless mode, set either: - * - `VITE_CLERK_KEYLESS_DISABLED=1` (for Vite-based projects) - * - `CLERK_KEYLESS_DISABLED=1` (generic) - */ -export const canUseKeyless = isDevelopmentEnvironment() && !isAutomatedEnvironment() && !KEYLESS_DISABLED;