diff --git a/.changeset/env-keys-error-copy.md b/.changeset/env-keys-error-copy.md new file mode 100644 index 00000000000..7bddf3b0186 --- /dev/null +++ b/.changeset/env-keys-error-copy.md @@ -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. diff --git a/integration/tests/next-middleware-keyless.test.ts b/integration/tests/next-middleware-keyless.test.ts index 4b726ec0116..d699b7790f8 100644 --- a/integration/tests/next-middleware-keyless.test.ts +++ b/integration/tests/next-middleware-keyless.test.ts @@ -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'); }); }); diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index f75845a384a..7ff28369c58 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -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'); }); diff --git a/packages/backend/src/__tests__/createRedirect.test.ts b/packages/backend/src/__tests__/createRedirect.test.ts index 481f80a043b..41fe6f5f9d1 100644 --- a/packages/backend/src/__tests__/createRedirect.test.ts +++ b/packages/backend/src/__tests__/createRedirect.test.ts @@ -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', ); }); }); @@ -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', ); }); diff --git a/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts index 8f4e8842cf4..da6dae6e201 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts @@ -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/); }); @@ -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/); }); }); diff --git a/packages/shared/src/__tests__/error.spec.ts b/packages/shared/src/__tests__/error.spec.ts index 0c644d295b7..2e4b7e09cea 100644 --- a/packages/shared/src/__tests__/error.spec.ts +++ b/packages/shared/src/__tests__/error.spec.ts @@ -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 diff --git a/packages/shared/src/__tests__/keys.spec.ts b/packages/shared/src/__tests__/keys.spec.ts index c9ec4d42acc..37f51fcf825 100644 --- a/packages/shared/src/__tests__/keys.spec.ts +++ b/packages/shared/src/__tests__/keys.spec.ts @@ -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', ); }); diff --git a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts index 7ee8d815c43..75304bb6dba 100644 --- a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts +++ b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts @@ -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', ); }); @@ -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', ); }); diff --git a/packages/shared/src/errors/errorThrower.ts b/packages/shared/src/errors/errorThrower.ts index 9375e994eff..589866d666c 100644 --- a/packages/shared/src/errors/errorThrower.ts +++ b/packages/shared/src/errors/errorThrower.ts @@ -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 component. Learn more: https://clerk.com/docs/components/clerk-provider`, }); diff --git a/packages/shared/src/errors/keySetupGuidance.ts b/packages/shared/src/errors/keySetupGuidance.ts new file mode 100644 index 00000000000..cda3ba1e7ca --- /dev/null +++ b/packages/shared/src/errors/keySetupGuidance.ts @@ -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}`; diff --git a/packages/shared/src/keys.ts b/packages/shared/src/keys.ts index 9949277f0da..378301ecd1f 100644 --- a/packages/shared/src/keys.ts +++ b/packages/shared/src/keys.ts @@ -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'; @@ -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 }, @@ -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; } @@ -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; }