diff --git a/.changeset/cli-error-copy.md b/.changeset/cli-error-copy.md new file mode 100644 index 00000000000..ba3025b265f --- /dev/null +++ b/.changeset/cli-error-copy.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': patch +--- + +Update missing and invalid key error messages to recommend the Clerk CLI: `npx clerk@latest init` to create an application, `npx clerk@latest env pull` to fetch the keys of an existing one, and `npx clerk@latest deploy` to provision a production instance. The Dashboard link is kept for manual key copying. diff --git a/.changeset/nextjs-cli-key-errors.md b/.changeset/nextjs-cli-key-errors.md new file mode 100644 index 00000000000..dd16263e23e --- /dev/null +++ b/.changeset/nextjs-cli-key-errors.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +`clerkMiddleware()` now points key misconfiguration at the Clerk CLI. Missing keys throw an error recommending `npx clerk@latest init`, with `npx clerk@latest deploy` / `npx clerk@latest env pull --instance prod` guidance for production (`code=missing_env_keys`). The publishable key format is also validated upfront (`code=invalid_env_keys`) instead of failing later with `Publishable key not valid.` The messages are the same in development and production. diff --git a/packages/backend/src/__tests__/createRedirect.test.ts b/packages/backend/src/__tests__/createRedirect.test.ts index 0877146bb89..b585d135ad6 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. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/backend: Missing publishableKey. To set up Clerk for this project, run:\n\n npx clerk@latest init', ); }); }); @@ -258,7 +258,7 @@ describe('redirect(redirectAdapter)', () => { }); expect(() => redirectToSignUp({ returnBackUrl })).toThrowError( - '@clerk/backend: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/backend: Missing publishableKey. To set up Clerk for this project, run:\n\n npx clerk@latest init', ); }); diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 31757419d0f..2d6085ee140 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -266,7 +266,7 @@ describe('clerkMiddleware(params)', () => { it('propagates middleware dynamic keys to the next request', async () => { const options = { secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', - publishableKey: 'pk_test_xxxxxxxxxxxxx', + publishableKey: 'pk_test_ZHluYW1pYy1rZXlzLmNsZXJrLmFjY291bnRzLmRldiQ', signInUrl: '/foo', signUpUrl: '/bar', }; @@ -286,7 +286,7 @@ describe('clerkMiddleware(params)', () => { it('with synchronous callback', async () => { const options = { secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', - publishableKey: 'pk_test_xxxxxxxxxxxxx', + publishableKey: 'pk_test_ZHluYW1pYy1rZXlzLmNsZXJrLmFjY291bnRzLmRldiQ', signInUrl: '/foo', signUpUrl: '/bar', }; @@ -313,7 +313,7 @@ describe('clerkMiddleware(params)', () => { it('with asynchronous callback', async () => { const options = { secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', - publishableKey: 'pk_test_xxxxxxxxxxxxx', + publishableKey: 'pk_test_ZHluYW1pYy1rZXlzLmNsZXJrLmFjY291bnRzLmRldiQ', signInUrl: '/foo', signUpUrl: '/bar', }; diff --git a/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts new file mode 100644 index 00000000000..d6e6d61a4d6 --- /dev/null +++ b/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts @@ -0,0 +1,57 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; +import type { NextFetchEvent } from 'next/server'; +import { NextRequest } from 'next/server'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// The mock SHOULD exist before the imports: keys are present but not parseable as Clerk keys, so +// the invalid-key error path is reachable. +vi.mock(import('../constants.js'), async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + PUBLISHABLE_KEY: 'pk_test_placeholder', + SECRET_KEY: 'sk_test_placeholder', + }; +}); + +describe('clerkMiddleware when Clerk env vars are invalid', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + vi.resetModules(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + const runMiddleware = async () => { + const { clerkMiddleware } = await import('../clerkMiddleware.js'); + const request = new NextRequest('https://example.com/protected'); + return clerkMiddleware()(request, {} as NextFetchEvent); + }; + + it('throws the invalid-key error pointing at the CLI', async () => { + await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest init/); + await expect(runMiddleware()).rejects.toThrow(/\(code=invalid_env_keys\)/); + }); + + 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(/\(code=invalid_env_keys\)/); + }); + + it('names the env var, the expected key format, and all three CLI commands', async () => { + const { invalidEnvKeys } = await import('../errors.js'); + expect(invalidEnvKeys).toContain('NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'); + expect(invalidEnvKeys).toContain('pk_test_'); + expect(invalidEnvKeys).toContain('npx clerk@latest init'); + expect(invalidEnvKeys).toContain('npx clerk@latest env pull'); + }); +}); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 9e6699de60d..1906c472b4f 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -39,7 +39,7 @@ import { canUseKeyless } from '../utils/feature-flags'; import { clerkClient } from './clerkClient'; import { DOMAIN, PROXY_URL, PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { type ContentSecurityPolicyOptions, createContentSecurityPolicyHeaders } from './content-security-policy'; -import { errorThrower } from './errorThrower'; +import { invalidEnvKeys, missingEnvVars } from './errors'; import { getHeader } from './headers-utils'; import { getKeylessCookieValue } from './keyless'; import { clerkMiddlewareRequestDataStorage, clerkMiddlewareRequestDataStore } from './middleware-storage'; @@ -155,12 +155,18 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl const publishableKey = assertKey( resolvedParams.publishableKey || PUBLISHABLE_KEY || keyless?.publishableKey, - () => errorThrower.throwMissingPublishableKeyError(), + () => { + throw new Error(missingEnvVars); + }, ); - const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey, () => - errorThrower.throwMissingSecretKeyError(), - ); + const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey, () => { + throw new Error(missingEnvVars); + }); + + if (!parsePublishableKey(publishableKey)) { + throw new Error(invalidEnvKeys); + } // Handle Frontend API proxy requests early, before authentication const requestUrl = new URL(request.nextUrl.href); diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 2cc8a8f1c7a..478d82e30f2 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -34,6 +34,22 @@ For more details, see https://clerk.com/err/auth-middleware `; }; +export const missingEnvVars = `Clerk: Missing environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY). + +To set up Clerk for this project, run: + + npx clerk@latest init + +The command is non-interactive and requires no Clerk account or login: it provisions temporary development keys automatically and writes them to your .env.local file. Then restart your dev server. If you already have a Clerk application, run \`npx clerk@latest env pull\` to write its keys, or copy them from https://dashboard.clerk.com into .env.local. Deploying to production? Run \`npx clerk@latest deploy\` to provision a production instance, or \`npx clerk@latest env pull --instance prod\` to use an existing one, and set the keys in your deployment environment. (code=missing_env_keys)`; + +export const invalidEnvKeys = `Clerk: Invalid environment keys. The publishable key (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY or the publishableKey option) is set but is not a valid Clerk key (expected format: pk_test_... or pk_live_...). + +To set up Clerk for this project with valid keys, run: + + npx clerk@latest init + +The command is non-interactive and requires no Clerk account or login: it provisions temporary development keys automatically and writes them to your .env.local file. Then restart your dev server. If you already have a Clerk application, run \`npx clerk@latest env pull\` to write its keys (\`--instance prod\` for production keys), or copy them from https://dashboard.clerk.com into .env.local. (code=invalid_env_keys)`; + export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/reference/nextjs/clerk-middleware. (code=auth_signature_invalid)`; export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. For more information, see: https://clerk.com/docs/reference/nextjs/clerk-middleware#dynamic-keys. (code=encryption_key_invalid)`; diff --git a/packages/shared/src/__tests__/error.spec.ts b/packages/shared/src/__tests__/error.spec.ts index 47981be4a4e..0ad0ebb4e9b 100644 --- a/packages/shared/src/__tests__/error.spec.ts +++ b/packages/shared/src/__tests__/error.spec.ts @@ -16,13 +16,13 @@ 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. You can get your Publishable key at https://dashboard.clerk.com/last-active?path=api-keys. (key=whatever)', + '@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, run:\n\n npx 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. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/test-package: Missing publishableKey. To set up Clerk for this project, run:\n\n npx clerk@latest init', ); }); diff --git a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts index 81191d47072..162ccdaa1f7 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. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/react: Missing publishableKey. To set up Clerk for this project, run:\n\n npx 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. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + '@clerk/react: Missing publishableKey. To set up Clerk for this project, run:\n\n npx clerk@latest init', ); }); diff --git a/packages/shared/src/errors/errorThrower.ts b/packages/shared/src/errors/errorThrower.ts index 030e1b68948..e78c70dedef 100644 --- a/packages/shared/src/errors/errorThrower.ts +++ b/packages/shared/src/errors/errorThrower.ts @@ -1,8 +1,20 @@ 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. You can get your Publishable key at https://dashboard.clerk.com/last-active?path=api-keys. (key={{key}})`, - MissingPublishableKeyErrorMessage: `Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, - MissingSecretKeyErrorMessage: `Missing secretKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, + 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, run: + + npx clerk@latest init + +The command is non-interactive and requires no Clerk account or login. If you already have a Clerk application, run \`npx clerk@latest env pull\` to write its keys to your env file (\`--instance prod\` for production keys), or copy its Publishable key from https://dashboard.clerk.com/last-active?path=api-keys.`, + MissingPublishableKeyErrorMessage: `Missing publishableKey. To set up Clerk for this project, run: + + npx clerk@latest init + +The command is non-interactive and requires no Clerk account or login: it creates a Clerk application and writes the required keys to your env file. If you already have a Clerk application, run \`npx clerk@latest env pull\` to write its keys, or copy them from https://dashboard.clerk.com/last-active?path=api-keys. Deploying to production? Run \`npx clerk@latest deploy\` to provision a production instance, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`, + MissingSecretKeyErrorMessage: `Missing secretKey. To set up Clerk for this project, run: + + npx clerk@latest init + +The command is non-interactive and requires no Clerk account or login: it creates a Clerk application and writes the required keys to your env file. If you already have a Clerk application, run \`npx clerk@latest env pull\` to write its keys, or copy them from https://dashboard.clerk.com/last-active?path=api-keys. Deploying to production? Run \`npx clerk@latest deploy\` to provision a production instance, or \`npx clerk@latest env pull --instance prod\` to use an existing one.`, MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, });