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/env-keys-error-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/shared': patch
---

Missing and invalid key errors now list the Clerk CLI commands that fix them: `npx clerk@latest init` for a new app, `npx clerk@latest link` and `npx clerk@latest env pull` for an existing one, and `npx clerk@latest env pull --instance prod` for production keys. The missing secret key error skips `init`, since the publishable key already points to an existing app.
2 changes: 1 addition & 1 deletion integration/tests/next-middleware-keyless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test.describe('Keyless mode | middleware authorization @nextjs', () => {
const response = await page.goto(`${app.serverUrl}/protected`);
expect(response?.status()).toBe(500);
const content = await page.content();
expect(content).toContain('Missing publishableKey');
expect(content).toContain('Clerk keys are missing from your environment');
expect(content).toContain('npx clerk@latest init');
});
});
2 changes: 1 addition & 1 deletion integration/tests/next-quickstart-keyless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test.describe('Keyless mode @quickstart', () => {
const response = await page.goto(`${app.serverUrl}/`);
expect(response?.status()).toBe(500);
const content = await page.content();
expect(content).toContain('Missing publishableKey');
expect(content).toContain('Clerk keys are missing from your environment');
expect(content).toContain('npx clerk@latest init');
});

Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/__tests__/createRedirect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('redirect(redirectAdapter)', () => {
} as any);

expect(() => redirectToSignIn({ returnBackUrl })).toThrowError(
'@clerk/backend: Missing publishableKey. To set up Clerk for this project, in your terminal run:\n\nnpx clerk@latest init',
'@clerk/backend: Clerk keys are missing from your environment.\n\nTo create a new Clerk app, run:\nnpx clerk@latest init',
);
});
});
Expand Down Expand Up @@ -258,7 +258,7 @@ describe('redirect(redirectAdapter)', () => {
});

expect(() => redirectToSignUp({ returnBackUrl })).toThrowError(
'@clerk/backend: Missing publishableKey. To set up Clerk for this project, in your terminal run:\n\nnpx clerk@latest init',
'@clerk/backend: Clerk keys are missing from your environment.\n\nTo create a new Clerk app, run:\nnpx clerk@latest init',
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('clerkMiddleware when Clerk env vars are missing', () => {
};

it('throws the missing key error pointing at the CLI instead of bootstrapping keyless', async () => {
await expect(runMiddleware()).rejects.toThrow(/Missing publishableKey/);
await expect(runMiddleware()).rejects.toThrow(/Clerk keys are missing from your environment/);
await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest init/);
});

Expand All @@ -49,6 +49,6 @@ describe('clerkMiddleware when Clerk env vars are missing', () => {
it('throws the same error regardless of NODE_ENV', async () => {
vi.stubEnv('NODE_ENV', 'production');
await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest init/);
await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest deploy/);
await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest env pull --instance prod/);
});
});
11 changes: 9 additions & 2 deletions packages/shared/src/__tests__/error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ describe('ErrorThrower', () => {

it('throws the correct error message and interpolates pkg and known parameters', () => {
expect(() => errorThrower.throwInvalidPublishableKeyError({ key: 'whatever' })).toThrow(
'@clerk/test-package: The publishableKey passed to Clerk is invalid (key=whatever, expected format: pk_test_... or pk_live_...). To create a Clerk application with valid keys, in your terminal run:\n\nnpx clerk@latest init',
'@clerk/test-package: The publishableKey passed to Clerk is invalid (key=whatever, expected format: pk_test_... or pk_live_...).\n\nTo create a new Clerk app, run:\nnpx clerk@latest init',
);
});

it('throws the correct error message and interpolates pkg if no parameters are provided', () => {
expect(() => errorThrower.throwMissingPublishableKeyError()).toThrow(
'@clerk/test-package: Missing publishableKey. To set up Clerk for this project, in your terminal run:\n\nnpx clerk@latest init',
'@clerk/test-package: Clerk keys are missing from your environment.\n\nTo create a new Clerk app, run:\nnpx clerk@latest init',
);
});

it('names the missing key so the secret key error is distinguishable from the publishable key error', () => {
expect(() => errorThrower.throwMissingSecretKeyError()).toThrow(
'@clerk/test-package: Missing secretKey.\n\nTo use an existing Clerk app, run:\nnpx clerk@latest link\nnpx clerk@latest env pull',
);
expect(() => errorThrower.throwMissingSecretKeyError()).not.toThrow(/npx clerk@latest init/);
});

it('throws a custom error message and interpolates pkg and known parameters', () => {
expect(() =>
errorThrower
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/__tests__/keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('parsePublishableKey(key)', () => {

it('throws an error if the publishable key is missing, when fatal: true', () => {
expect(() => parsePublishableKey(undefined, { fatal: true })).toThrowError(
'Publishable key is missing. To create a Clerk application with valid keys, in your terminal run:\n\nnpx clerk@latest init',
'Publishable key is missing.\n\nTo create a new Clerk app, run:\nnpx clerk@latest init',
);
});

Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/__tests__/loadClerkJsScript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('loadClerkJsScript(options)', () => {

test('throws error when publishableKey is missing', async () => {
await expect(loadClerkJsScript({} as any)).rejects.toThrow(
'@clerk/react: Missing publishableKey. To set up Clerk for this project, in your terminal run:\n\nnpx clerk@latest init',
'@clerk/react: Clerk keys are missing from your environment.\n\nTo create a new Clerk app, run:\nnpx clerk@latest init',
);
});

Expand Down Expand Up @@ -310,7 +310,7 @@ describe('loadClerkUIScript(options)', () => {

test('throws error when publishableKey is missing', async () => {
await expect(loadClerkUIScript({} as any)).rejects.toThrow(
'@clerk/react: Missing publishableKey. To set up Clerk for this project, in your terminal run:\n\nnpx clerk@latest init',
'@clerk/react: Clerk keys are missing from your environment.\n\nTo create a new Clerk app, run:\nnpx clerk@latest init',
);
});

Expand Down
26 changes: 8 additions & 18 deletions packages/shared/src/errors/errorThrower.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import { existingAppKeyGuidance, keySetupGuidance } from './keySetupGuidance';

const DefaultMessages = Object.freeze({
InvalidProxyUrlErrorMessage: `The proxyUrl passed to Clerk is invalid. The expected value for proxyUrl is an absolute URL or a relative path with a leading '/'. (key={{url}})`,
InvalidPublishableKeyErrorMessage: `The publishableKey passed to Clerk is invalid (key={{key}}, expected format: pk_test_... or pk_live_...). To create a Clerk application with valid keys, in your terminal run:

npx clerk@latest init

\`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive.

If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys (\`--instance prod\` for production keys). Or copy its Publishable key from https://dashboard.clerk.com/~/api-keys.`,
MissingPublishableKeyErrorMessage: `Missing publishableKey. To set up Clerk for this project, in your terminal run:

npx clerk@latest init

\`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive.

If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys. Or copy them from https://dashboard.clerk.com/~/api-keys. Deploy a production instance by running \`npx clerk@latest deploy\`, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`,
MissingSecretKeyErrorMessage: `Missing secretKey. To set up Clerk for this project, in your terminal run:
InvalidPublishableKeyErrorMessage: `The publishableKey passed to Clerk is invalid (key={{key}}, expected format: pk_test_... or pk_live_...).

npx clerk@latest init
${keySetupGuidance}`,
MissingPublishableKeyErrorMessage: `Clerk keys are missing from your environment.

\`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive.
${keySetupGuidance}`,
MissingSecretKeyErrorMessage: `Missing secretKey.

If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys. Or copy them from https://dashboard.clerk.com/~/api-keys. Deploy a production instance by running \`npx clerk@latest deploy\`, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`,
${existingAppKeyGuidance}`,
MissingClerkProvider: `{{source}} can only be used within the <ClerkProvider /> component. Learn more: https://clerk.com/docs/components/clerk-provider`,
});

Expand Down
19 changes: 19 additions & 0 deletions packages/shared/src/errors/keySetupGuidance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const existingAppSteps = `To use an existing Clerk app, run:
npx clerk@latest link
npx clerk@latest env pull

For production keys, run:
npx clerk@latest env pull --instance prod`;

const dashboardFallback = `Or copy keys from https://dashboard.clerk.com/~/api-keys into your .env file.`;

export const keySetupGuidance = `To create a new Clerk app, run:
npx clerk@latest init

${existingAppSteps}

${dashboardFallback}`;

export const existingAppKeyGuidance = `${existingAppSteps}

${dashboardFallback}`;
19 changes: 7 additions & 12 deletions packages/shared/src/keys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DEV_OR_STAGING_SUFFIXES, LEGACY_DEV_INSTANCE_SUFFIXES } from './constants';
import { keySetupGuidance } from './errors/keySetupGuidance';
import { isomorphicAtob } from './isomorphicAtob';
import { isomorphicBtoa } from './isomorphicBtoa';
import type { PublishableKey } from './types';
Expand Down Expand Up @@ -98,14 +99,6 @@ function isValidDecodedPublishableKey(decoded: string): boolean {
return withoutTrailing.includes('.');
}

const fatalKeyGuidance = `To create a Clerk application with valid keys, in your terminal run:

npx clerk@latest init

\`npx clerk@latest init\` creates a Clerk application and writes keys to your .env file. No Clerk account or login required and the command is non-interactive.

If you have a Clerk application, run \`npx clerk@latest env pull\` to write the keys (\`--instance prod\` for production keys). Or copy them from https://dashboard.clerk.com/~/api-keys.`;

export function parsePublishableKey(
key: string | undefined,
options: ParsePublishableKeyOptions & { fatal: true },
Expand Down Expand Up @@ -135,10 +128,12 @@ export function parsePublishableKey(

if (!key || !isPublishableKey(key)) {
if (options.fatal && !key) {
throw new Error(`Publishable key is missing. ${fatalKeyGuidance}`);
throw new Error(`Publishable key is missing.\n\n${keySetupGuidance}`);
}
if (options.fatal && !isPublishableKey(key)) {
throw new Error(`Publishable key not valid (expected format: pk_test_... or pk_live_...). ${fatalKeyGuidance}`);
throw new Error(
`Publishable key not valid (expected format: pk_test_... or pk_live_...).\n\n${keySetupGuidance}`,
);
}
return null;
}
Expand All @@ -150,14 +145,14 @@ export function parsePublishableKey(
decodedFrontendApi = isomorphicAtob(key.split('_')[2]);
} catch {
if (options.fatal) {
throw new Error(`Publishable key not valid: Failed to decode key. ${fatalKeyGuidance}`);
throw new Error(`Publishable key not valid: Failed to decode key.\n\n${keySetupGuidance}`);
}
return null;
}

if (!isValidDecodedPublishableKey(decodedFrontendApi)) {
if (options.fatal) {
throw new Error(`Publishable key not valid: Decoded key has invalid format. ${fatalKeyGuidance}`);
throw new Error(`Publishable key not valid: Decoded key has invalid format.\n\n${keySetupGuidance}`);
}
return null;
}
Expand Down
Loading