Skip to content

feat(shared): Enhance publishable key validation #6266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 8, 2025
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/smooth-papayas-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/shared': patch
---

Enhancing publishable key parsing and validation logic to validate expected format
9 changes: 9 additions & 0 deletions .typedoc/__tests__/__snapshots__/file-structure.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"types/without.mdx",
"shared/api-url-from-publishable-key.mdx",
"shared/build-clerk-js-script-attributes.mdx",
"shared/build-publishable-key.mdx",
"shared/camel-to-snake.mdx",
"shared/clerk-js-script-url.mdx",
"shared/clerk-runtime-error.mdx",
"shared/create-dev-or-staging-url-cache.mdx",
"shared/create-path-matcher.mdx",
"shared/deep-camel-to-snake.mdx",
"shared/deep-snake-to-camel.mdx",
Expand All @@ -87,14 +89,20 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"shared/extract-dev-browser-jwt-from-url.mdx",
"shared/fast-deep-merge-and-replace.mdx",
"shared/get-clerk-js-major-version-or-tag.mdx",
"shared/get-cookie-suffix.mdx",
"shared/get-env-variable.mdx",
"shared/get-non-undefined-values.mdx",
"shared/get-script-url.mdx",
"shared/get-suffixed-cookie-name.mdx",
"shared/icon-image-url.mdx",
"shared/in-browser.mdx",
"shared/is-browser-online.mdx",
"shared/is-clerk-runtime-error.mdx",
"shared/is-development-from-publishable-key.mdx",
"shared/is-development-from-secret-key.mdx",
"shared/is-ipv4-address.mdx",
"shared/is-production-from-publishable-key.mdx",
"shared/is-production-from-secret-key.mdx",
"shared/is-publishable-key.mdx",
"shared/is-staging.mdx",
"shared/is-truthy.mdx",
Expand All @@ -105,6 +113,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"shared/pages-or-infinite-options.mdx",
"shared/paginated-hook-config.mdx",
"shared/paginated-resources.mdx",
"shared/parse-publishable-key.mdx",
"shared/read-json-file.mdx",
"shared/set-clerk-js-loading-error-package-name.mdx",
"shared/snake-to-camel.mdx",
Expand Down
85 changes: 68 additions & 17 deletions packages/shared/src/__tests__/keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

describe('buildPublishableKey(frontendApi)', () => {
const cases = [
['example.clerk.accounts.dev', 'pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk'],
['fake-clerk-test.clerk.accounts.dev', 'pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ='],
['foo-bar-13.clerk.accounts.dev', 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'],
['clerk.boring.sawfly-91.lcl.dev', 'pk_test_Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2wuZGV2JA=='],
['clerk.boring.sawfly-91.lclclerk.com', 'pk_test_Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2xjbGVyay5jb20k'],
Expand All @@ -34,12 +34,8 @@ describe('parsePublishableKey(key)', () => {
['', null],
['whatever', null],
[
'pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk',
{ instanceType: 'production', frontendApi: 'example.clerk.accounts.dev' },
],
[
'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk',
{ instanceType: 'development', frontendApi: 'foo-bar-13.clerk.accounts.dev' },
'pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=',
{ instanceType: 'production', frontendApi: 'fake-clerk-test.clerk.accounts.dev' },
],
[
'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk',
Expand All @@ -54,6 +50,30 @@ describe('parsePublishableKey(key)', () => {
expect(result).toEqual(expectedPublishableKey);
});

it('returns null for keys with extra characters after $', () => {
expect(parsePublishableKey('pk_live_ZmFrZS1jbGVyay1tYWxmb3JtZWQuY2xlcmsuYWNjb3VudHMuZGV2JGV4dHJh')).toBeNull();
});

it('throws an error for keys with extra characters after $ when fatal: true', () => {
expect(() =>
parsePublishableKey('pk_live_ZmFrZS1jbGVyay1tYWxmb3JtZWQuY2xlcmsuYWNjb3VudHMuZGV2JGV4dHJh', { fatal: true }),
).toThrowError('Publishable key not valid.');
});

it('returns null for keys with multiple $ characters', () => {
expect(parsePublishableKey('pk_live_ZmFrZS1jbGVyay1tdWx0aXBsZS5jbGVyay5hY2NvdW50cy5kZXYkJA==')).toBeNull();
});

it('returns null for keys without proper domain format', () => {
expect(parsePublishableKey('pk_live_aW52YWxpZGtleSQ=')).toBeNull();
});

it('throws an error if the key cannot be decoded when fatal: true', () => {
expect(() => parsePublishableKey('pk_live_invalid!@#$', { fatal: true })).toThrowError(
'Publishable key not valid.',
);
});

it('throws an error if the key is not a valid publishable key, when fatal: true', () => {
expect(() => parsePublishableKey('fake_pk', { fatal: true })).toThrowError('Publishable key not valid.');
});
Expand All @@ -65,15 +85,22 @@ describe('parsePublishableKey(key)', () => {
});

it('applies the proxyUrl if provided', () => {
expect(parsePublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==', { proxyUrl: 'example.com/__clerk' })).toEqual({
expect(
parsePublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', {
proxyUrl: 'example.com/__clerk',
}),
).toEqual({
frontendApi: 'example.com/__clerk',
instanceType: 'production',
});
});

it('applies the domain if provided for production keys and isSatellite is true', () => {
expect(
parsePublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==', { domain: 'example.com', isSatellite: true }),
parsePublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', {
domain: 'example.com',
isSatellite: true,
}),
).toEqual({
frontendApi: 'clerk.example.com',
instanceType: 'production',
Expand All @@ -82,9 +109,12 @@ describe('parsePublishableKey(key)', () => {

it('ignores domain for production keys when isSatellite is false', () => {
expect(
parsePublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==', { domain: 'example.com', isSatellite: false }),
parsePublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', {
domain: 'example.com',
isSatellite: false,
}),
).toEqual({
frontendApi: 'clerk.clerk.dev',
frontendApi: 'fake-clerk-test.clerk.accounts.dev',
instanceType: 'production',
});
});
Expand All @@ -101,12 +131,33 @@ describe('parsePublishableKey(key)', () => {

describe('isPublishableKey(key)', () => {
it('returns true if the key is a valid publishable key', () => {
expect(isPublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==')).toBe(true);
expect(isPublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=')).toBe(true);
expect(isPublishableKey('pk_test_Y2xlcmsuY2xlcmsuZGV2JA==')).toBe(true);
});

it('returns false if the key is not a valid publishable key', () => {
expect(isPublishableKey('clerk.clerk.com')).toBe(false);
});

it('returns false if the key has invalid structure', () => {
expect(isPublishableKey('pk_live')).toBe(false);
expect(isPublishableKey('pk_live_')).toBe(false);
expect(isPublishableKey('pk_live_invalid')).toBe(false);
});

it('returns false if the decoded key has extra characters after $', () => {
expect(isPublishableKey('pk_live_ZmFrZS1jbGVyay1tYWxmb3JtZWQuY2xlcmsuYWNjb3VudHMuZGV2JGV4dHJh')).toBe(false);
expect(isPublishableKey('pk_test_Y2xlcmsuY2xlcmsuZGV2JGV4dHJh')).toBe(false);
});

it('returns false if the decoded key has multiple $ characters', () => {
expect(isPublishableKey('pk_live_ZmFrZS1jbGVyay1tdWx0aXBsZS5jbGVyay5hY2NvdW50cy5kZXYkJA==')).toBe(false);
expect(isPublishableKey('pk_live_JGZha2UtY2xlcmstcHJlZml4LmNsZXJrLmFjY291bnRzLmRldiQ=')).toBe(false);
});

it('returns false if the decoded key does not look like a domain', () => {
expect(isPublishableKey('pk_live_aW52YWxpZGtleSQ=')).toBe(false);
});
});

describe('isDevOrStagingUrl(url)', () => {
Expand Down Expand Up @@ -138,9 +189,9 @@ describe('isDevOrStagingUrl(url)', () => {

describe('isDevelopmentFromPublishableKey(key)', () => {
const cases: Array<[string, boolean]> = [
['pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', false],
['pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', false],
['pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', true],
['live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', false],
['live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', false],
['test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', true],
];

Expand All @@ -152,9 +203,9 @@ describe('isDevelopmentFromPublishableKey(key)', () => {

describe('isProductionFromPublishableKey(key)', () => {
const cases: Array<[string, boolean]> = [
['pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', true],
['pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', true],
['pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', false],
['live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', true],
['live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', true],
['test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', false],
];

Expand Down Expand Up @@ -194,7 +245,7 @@ describe('isProductionFromSecretKey(key)', () => {

describe('getCookieSuffix(publishableKey, subtle?)', () => {
const cases: Array<[string, string]> = [
['pk_live_Y2xlcmsuY2xlcmsuZGV2JA', '1Z8AzTQD'],
['pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', 'qReyu04C'],
['pk_test_Y2xlcmsuY2xlcmsuZGV2JA', 'QvfNY2dr'],
];

Expand Down
Loading
Loading