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: