Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tanstack-keyless-cli-init-error.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 10 additions & 1 deletion integration/models/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -199,6 +205,9 @@ export const application = (
get buildOutput() {
return buildOutput;
},
get devOutput() {
return devOutput;
},
get serveOutput() {
return serveOutput;
},
Expand Down
35 changes: 10 additions & 25 deletions integration/tests/tanstack-start/keyless.test.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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');
});
});
13 changes: 1 addition & 12 deletions packages/tanstack-react-start/src/client/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,13 @@ export function ClerkProvider<TUi extends Ui = Ui>({

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 (
<>
<ScriptOnce>{`window.__clerk_init_state = ${htmlSafeJson(clerkInitialState)};`}</ScriptOnce>
Expand All @@ -74,7 +64,6 @@ export function ClerkProvider<TUi extends Ui = Ui>({
});
}}
{...mergedProps}
{...keylessProps}
>
{children}
</ReactClerkProvider>
Expand Down
6 changes: 0 additions & 6 deletions packages/tanstack-react-start/src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ export const pickFromClerkInitState = (
): TanStackProviderAndInitialProps &
InternalClerkScriptProps & {
clerkSsrState: any;
__keylessClaimUrl?: string;
__keylessApiKeysUrl?: string;
} => {
const {
__clerk_ssr_state,
Expand All @@ -32,8 +30,6 @@ export const pickFromClerkInitState = (
__signUpForceRedirectUrl,
__signInFallbackRedirectUrl,
__signUpFallbackRedirectUrl,
__keylessClaimUrl,
__keylessApiKeysUrl,
__prefetchUI,
} = clerkInitState || {};

Expand All @@ -59,8 +55,6 @@ export const pickFromClerkInitState = (
signUpForceRedirectUrl: __signUpForceRedirectUrl,
signInFallbackRedirectUrl: __signInFallbackRedirectUrl,
signUpFallbackRedirectUrl: __signUpFallbackRedirectUrl,
__keylessClaimUrl,
__keylessApiKeysUrl,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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/);
});
});
33 changes: 1 addition & 32 deletions packages/tanstack-react-start/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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<string, unknown>).__internal_clerk_state = {
...((clerkInitialState as Record<string, unknown>).__internal_clerk_state as Record<string, unknown>),
__keylessClaimUrl: keylessClaimUrl,
__keylessApiKeysUrl: keylessApiKeysUrl,
};
}

const result = await next({
context: {
clerkInitialState,
Expand Down
19 changes: 0 additions & 19 deletions packages/tanstack-react-start/src/server/keyless/fileStorage.ts

This file was deleted.

39 changes: 0 additions & 39 deletions packages/tanstack-react-start/src/server/keyless/index.ts

This file was deleted.

19 changes: 0 additions & 19 deletions packages/tanstack-react-start/src/server/keyless/utils.ts

This file was deleted.

7 changes: 2 additions & 5 deletions packages/tanstack-react-start/src/server/loadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) {
Expand Down

This file was deleted.

Loading
Loading