diff --git a/.changeset/proud-animals-happen.md b/.changeset/proud-animals-happen.md new file mode 100644 index 00000000000..b8bfad4b712 --- /dev/null +++ b/.changeset/proud-animals-happen.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/expo': patch +--- + +Ensure clerk-js accepts `proxyUrl` and `domain` in non-browser environments. diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 4d9fb15bc5b..eb1381d231c 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2477,6 +2477,19 @@ describe('Clerk singleton', () => { expect(sut.getFapiClient().buildUrl({ path: '/me' }).href).toContain('https://clerk.satellite.com/v1/me'); }); + + mockNativeRuntime(() => { + test('fapiClient should use Clerk.domain as its baseUrl in non-browser runtimes', async () => { + const sut = new Clerk(productionPublishableKey, { + domain: 'satellite.com', + }); + await sut.load({ + isSatellite: true, + }); + + expect(sut.getFapiClient().buildUrl({ path: '/me' }).href).toContain('https://satellite.com/v1/me'); + }); + }); }); }); @@ -2490,6 +2503,17 @@ describe('Clerk singleton', () => { expect(sut.getFapiClient().buildUrl({ path: '/me' }).href).toContain('https://proxy.com/api/__clerk/v1/me'); }); + + mockNativeRuntime(() => { + test('fapiClient should use Clerk.proxyUrl as its baseUrl in non-browser runtimes', async () => { + const sut = new Clerk(productionPublishableKey, { + proxyUrl: 'https://proxy.com/api/__clerk', + }); + await sut.load({}); + + expect(sut.getFapiClient().buildUrl({ path: '/me' }).href).toContain('https://proxy.com/api/__clerk/v1/me'); + }); + }); }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 1a362b2edac..8d8da3b6cd0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -342,7 +342,13 @@ export class Clerk implements ClerkInterface { } return strippedDomainString; } - return ''; + + if (typeof this.#domain === 'function') { + logger.warnOnce(warnings.domainAsFunctionNotSupported); + return ''; + } + + return this.#domain || ''; } get proxyUrl(): string { @@ -353,7 +359,13 @@ export class Clerk implements ClerkInterface { } return proxyUrlToAbsoluteURL(_unfilteredProxy); } - return ''; + + if (typeof this.#proxyUrl === 'function') { + logger.warnOnce(warnings.proxyUrlAsFunctionNotSupported); + return ''; + } + + return this.#proxyUrl || ''; } get frontendApi(): string { diff --git a/packages/expo/src/provider/singleton/__tests__/createClerkInstance.test.ts b/packages/expo/src/provider/singleton/__tests__/createClerkInstance.test.ts index 24753a1d894..cc5edec6fde 100644 --- a/packages/expo/src/provider/singleton/__tests__/createClerkInstance.test.ts +++ b/packages/expo/src/provider/singleton/__tests__/createClerkInstance.test.ts @@ -220,4 +220,23 @@ describe('createClerkInstance', () => { domain: undefined, }); }); + + test('throws when proxyUrl is not absolute', async () => { + const createClerkInstance = await loadCreateClerkInstance(); + const getClerkInstance = createClerkInstance(MockClerk as unknown as typeof Clerk); + + expect(() => + getClerkInstance({ + publishableKey: 'pk_test_123', + proxyUrl: '/api/__clerk', + }), + ).toThrow(/`proxyUrl` must be an absolute URL/); + + expect(() => + getClerkInstance({ + publishableKey: 'pk_test_123', + proxyUrl: () => '/api/__clerk', + }), + ).toThrow(/`proxyUrl` must be a string/); + }); }); diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts index 29bb8dc1ee4..2a361bad54a 100644 --- a/packages/expo/src/provider/singleton/createClerkInstance.ts +++ b/packages/expo/src/provider/singleton/createClerkInstance.ts @@ -20,7 +20,7 @@ import { import { MemoryTokenCache } from '../../cache/MemoryTokenCache'; import { CLERK_CLIENT_JWT_KEY } from '../../constants'; import { errorThrower } from '../../errorThrower'; -import { isNative } from '../../utils'; +import { assertValidProxyUrl, isNative } from '../../utils'; import type { BuildClerkOptions } from './types'; /** @@ -100,12 +100,15 @@ export function createClerkInstance(ClerkClass: typeof Clerk) { } if (!__internal_clerk || hasConfigChanged) { + assertValidProxyUrl(proxyUrl); + if (hasConfigChanged) { tokenCache.clearToken?.(CLERK_CLIENT_JWT_KEY); } const getToken = tokenCache.getToken; const saveToken = tokenCache.saveToken; + __internal_clerkOptions = { publishableKey, proxyUrl, domain }; __internal_clerk = new ClerkClass(publishableKey, { proxyUrl, domain }) as unknown as BrowserClerk; diff --git a/packages/expo/src/utils/errors.ts b/packages/expo/src/utils/errors.ts index 70bbe8fdae2..25aed259038 100644 --- a/packages/expo/src/utils/errors.ts +++ b/packages/expo/src/utils/errors.ts @@ -1,3 +1,23 @@ import { buildErrorThrower } from '@clerk/shared/error'; +import { isHttpOrHttps } from '@clerk/shared/proxy'; +import type { DomainOrProxyUrl } from '@clerk/shared/types'; + +import { isNative } from './runtime'; export const errorThrower = buildErrorThrower({ packageName: PACKAGE_NAME }); + +export const assertValidProxyUrl = (proxyUrl: DomainOrProxyUrl['proxyUrl']) => { + if (!proxyUrl) { + return; + } + + if (isNative()) { + if (typeof proxyUrl !== 'string') { + return errorThrower.throw('`proxyUrl` must be a string in non-browser environments.'); + } + + if (!isHttpOrHttps(proxyUrl)) { + errorThrower.throw('`proxyUrl` must be an absolute URL in non-browser environments.'); + } + } +}; diff --git a/packages/shared/src/internal/clerk-js/warnings.ts b/packages/shared/src/internal/clerk-js/warnings.ts index c6d0f7f8885..2ec160247d8 100644 --- a/packages/shared/src/internal/clerk-js/warnings.ts +++ b/packages/shared/src/internal/clerk-js/warnings.ts @@ -22,7 +22,15 @@ const createMessageForDisabledBilling = (componentName: 'PricingTable' | 'Checko ); }; +const propertyAsFunctionNotSupported = (property: 'proxyUrl' | 'domain') => { + return formatWarning( + `${property} as a function is not supported in this environment. The value will be ignored. Provide an absolute URL instead.`, + ); +}; + const warnings = { + proxyUrlAsFunctionNotSupported: propertyAsFunctionNotSupported('proxyUrl'), + domainAsFunctionNotSupported: propertyAsFunctionNotSupported('domain'), cannotRenderComponentWhenSessionExists: 'The and components cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the Home URL instead.', cannotRenderSignUpComponentWhenSessionExists: