Skip to content

Commit 2c01129

Browse files
committed
feat(clerk-js): Support Reverse Proxy
1 parent 9815c6c commit 2c01129

8 files changed

Lines changed: 115 additions & 10 deletions

File tree

packages/clerk-js/src/core/clerk.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ declare global {
9191
Clerk?: Clerk;
9292
__clerk_frontend_api?: string;
9393
__clerk_publishable_key?: string;
94+
__clerk_proxy_url?: Pick<ClerkInterface, 'proxyUrl'>['proxyUrl'];
9495
}
9596
}
9697

@@ -100,6 +101,8 @@ const defaultOptions: ClerkOptions = {
100101
touchSession: true,
101102
};
102103

104+
type ClerkConstructorOptions = Pick<ClerkInterface, 'proxyUrl'>;
105+
103106
export default class Clerk implements ClerkInterface {
104107
public static mountComponentRenderer?: MountComponentRenderer;
105108
public static version: string = packageJSON.version;
@@ -109,6 +112,7 @@ export default class Clerk implements ClerkInterface {
109112
public user?: UserResource | null;
110113
public frontendApi: string;
111114
public publishableKey?: string;
115+
public proxyUrl?: ClerkConstructorOptions['proxyUrl'];
112116

113117
#authService: AuthenticationService | null = null;
114118
#broadcastChannel: LocalStorageBroadcastChannel<ClerkCoreBroadcastChannelEvent> | null = null;
@@ -132,9 +136,11 @@ export default class Clerk implements ClerkInterface {
132136
return this.#isReady;
133137
}
134138

135-
public constructor(key: string) {
139+
public constructor(key: string, options?: unknown) {
136140
key = (key || '').trim();
137141

142+
this.proxyUrl = (options as ClerkConstructorOptions | undefined)?.proxyUrl;
143+
138144
if (isLegacyFrontendApiKey(key)) {
139145
if (!validateFrontendApi(key)) {
140146
errorThrower.throwInvalidFrontendApiError({ key });

packages/clerk-js/src/core/fapiClient.test.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@ const fapiClient = createFapiClient({
1010
},
1111
} as Clerk);
1212

13+
const proxyUrl = 'example.com/__clerk';
14+
15+
const fapiClientWithProxy = createFapiClient({
16+
frontendApi: 'clerk.example.com',
17+
proxyUrl: `example.com`,
18+
version: '42.0.0',
19+
session: {
20+
id: 'deadbeef',
21+
},
22+
} as Clerk);
23+
24+
const fapiClientWithProxyPath = createFapiClient({
25+
frontendApi: 'clerk.example.com',
26+
proxyUrl,
27+
version: '42.0.0',
28+
session: {
29+
id: 'deadbeef',
30+
},
31+
} as Clerk);
32+
33+
const fapiClientWithProxyPath2 = createFapiClient({
34+
frontendApi: 'clerk.example.com',
35+
proxyUrl: 'example.com/api/__clerk',
36+
version: '42.0.0',
37+
session: {
38+
id: 'deadbeef',
39+
},
40+
} as Clerk);
41+
1342
type RecursivePartial<T> = {
1443
[P in keyof T]?: RecursivePartial<T[P]>;
1544
};
@@ -68,6 +97,23 @@ describe('buildUrl(options)', () => {
6897
);
6998
});
7099

100+
it('returns the full frontend API URL using proxy url', () => {
101+
expect(fapiClientWithProxy.buildUrl({ path: '/foo' }).href).toBe(
102+
`https://example.com/v1/foo?_clerk_js_version=42.0.0`,
103+
);
104+
});
105+
106+
it('returns the full frontend API URL using proxy url path', () => {
107+
expect(fapiClientWithProxyPath.buildUrl({ path: '/foo' }).href).toBe(
108+
`https://${proxyUrl}/v1/foo?_clerk_js_version=42.0.0`,
109+
);
110+
});
111+
it('returns the full frontend API URL using proxy url path 2', () => {
112+
expect(fapiClientWithProxyPath2.buildUrl({ path: '/foo' }).href).toBe(
113+
`https://example.com/api/__clerk/v1/foo?_clerk_js_version=42.0.0`,
114+
);
115+
});
116+
71117
it('adds _clerk_session_id as a query parameter if provided and path does not start with client', () => {
72118
expect(fapiClient.buildUrl({ path: '/foo', sessionId: 'sess_42' }).href).toBe(
73119
'https://clerk.example.com/v1/foo?_clerk_js_version=42.0.0&_clerk_session_id=sess_42',
@@ -100,8 +146,6 @@ describe('buildUrl(options)', () => {
100146

101147
describe('request', () => {
102148
it('invokes global.fetch', async () => {
103-
window.location.href = 'http://clerk.example.com';
104-
105149
await fapiClient.request({
106150
path: '/foo',
107151
});
@@ -116,6 +160,21 @@ describe('request', () => {
116160
);
117161
});
118162

163+
it('invokes global.fetch with proxy', async () => {
164+
await fapiClientWithProxy.request({
165+
path: '/foo',
166+
});
167+
168+
expect(fetch).toHaveBeenCalledWith(
169+
'https://example.com/v1/foo?_clerk_js_version=42.0.0&_clerk_session_id=deadbeef',
170+
expect.objectContaining({
171+
credentials: 'include',
172+
method: 'GET',
173+
path: '/foo',
174+
}),
175+
);
176+
});
177+
119178
describe('for production instances', () => {
120179
it.todo('does not append the __dev_session cookie value to the query string');
121180
it.todo('does not set the __dev_session cookie from the response Clerk-Cookie header');

packages/clerk-js/src/core/fapiClient.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export interface FapiClient {
5858
request<T>(requestInit: FapiRequestInit): Promise<FapiResponse<T>>;
5959
}
6060

61-
export default function createFapiClient(clerkInstance: Clerk): FapiClient {
61+
export default function createFapiClient(clerkInstance: Omit<Clerk, 'proxyUrl'>): FapiClient {
6262
const onBeforeRequestCallbacks: Array<FapiRequestCallback<unknown>> = [];
6363
const onAfterResponseCallbacks: Array<FapiRequestCallback<unknown>> = [];
6464

@@ -123,6 +123,19 @@ export default function createFapiClient(clerkInstance: Clerk): FapiClient {
123123
function buildUrl(requestInit: FapiRequestInit): URL {
124124
const { path } = requestInit;
125125

126+
if ((clerkInstance as Clerk).proxyUrl) {
127+
const proxyBase = new URL(`https://${(clerkInstance as Clerk).proxyUrl}`);
128+
const proxyPath = proxyBase.pathname.slice(1, proxyBase.pathname.length);
129+
return buildUrlUtil(
130+
{
131+
base: proxyBase.origin,
132+
pathname: `${proxyPath}/v1${path}`,
133+
search: buildQueryString(requestInit),
134+
},
135+
{ stringify: false },
136+
);
137+
}
138+
126139
return buildUrlUtil(
127140
{
128141
base: `https://${clerkInstance.frontendApi}`,

packages/clerk-js/src/index.browser.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ const frontendApi =
1515
window.__clerk_frontend_api ||
1616
'';
1717

18-
window.Clerk = new Clerk(publishableKey || frontendApi);
18+
const proxyUrl =
19+
document.querySelector('script[data-clerk-proxy-url]')?.getAttribute('data-clerk-proxy-url') ||
20+
window.__clerk_proxy_url ||
21+
'';
22+
23+
window.Clerk = new Clerk(publishableKey || frontendApi, {
24+
proxyUrl,
25+
});
1926

2027
if (module.hot) {
2128
module.hot.accept();

packages/clerk-js/src/index.headless.browser.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ const frontendApi =
1212
window.__clerk_frontend_api ||
1313
'';
1414

15-
window.Clerk = new Clerk(publishableKey || frontendApi);
15+
const proxyUrl =
16+
document.querySelector('script[data-clerk-proxy-url]')?.getAttribute('data-clerk-proxy-url') ||
17+
window.__clerk_proxy_url ||
18+
'';
19+
20+
window.Clerk = new Clerk(publishableKey || frontendApi, {
21+
proxyUrl,
22+
});
1623

1724
if (module.hot) {
1825
module.hot.accept();

packages/react/src/isomorphicClerk.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { OrganizationProfileProps, OrganizationSwitcherProps } from '@clerk/type
2525
import type {
2626
BrowserClerk,
2727
BrowserClerkConstructor,
28+
ClerkConstructorOptions,
2829
ClerkProp,
2930
HeadlessBrowserClerk,
3031
HeadlessBrowserClerkConstrutor,
@@ -47,6 +48,7 @@ export default class IsomorphicClerk {
4748
private mode: 'browser' | 'server';
4849
private frontendApi?: string;
4950
private publishableKey?: string;
51+
private proxyUrl?: ClerkConstructorOptions['proxyUrl'];
5052
private options: IsomorphicClerkOptions;
5153
private Clerk: ClerkProp;
5254
private clerkjs: BrowserClerk | HeadlessBrowserClerk | null = null;
@@ -83,9 +85,10 @@ export default class IsomorphicClerk {
8385
}
8486

8587
constructor(options: IsomorphicClerkOptions) {
86-
const { Clerk = null, frontendApi, publishableKey, ...rest } = options || {};
88+
const { Clerk = null, frontendApi, publishableKey } = options || {};
8789
this.frontendApi = frontendApi;
8890
this.publishableKey = publishableKey;
91+
this.proxyUrl = (options as ClerkConstructorOptions | undefined)?.proxyUrl;
8992
this.options = options;
9093
this.Clerk = Clerk;
9194
this.mode = inClientSide() ? 'browser' : 'server';
@@ -109,6 +112,7 @@ export default class IsomorphicClerk {
109112
// - https://github.com/facebook/react/issues/24430
110113
window.__clerk_frontend_api = this.frontendApi;
111114
window.__clerk_publishable_key = this.publishableKey;
115+
window.__clerk_proxy_url = this.proxyUrl;
112116

113117
try {
114118
if (this.Clerk) {
@@ -117,7 +121,9 @@ export default class IsomorphicClerk {
117121

118122
if (isConstructor<BrowserClerkConstructor | HeadlessBrowserClerkConstrutor>(this.Clerk)) {
119123
// Construct a new Clerk object if a constructor is passed
120-
c = new this.Clerk(this.publishableKey || this.frontendApi || '');
124+
c = new this.Clerk(this.publishableKey || this.frontendApi || '', {
125+
proxyUrl: this.proxyUrl,
126+
});
121127
await c.load(this.options);
122128
} else {
123129
// Otherwise use the instantiated Clerk object
@@ -134,6 +140,7 @@ export default class IsomorphicClerk {
134140
await loadScript({
135141
frontendApi: this.frontendApi,
136142
publishableKey: this.publishableKey,
143+
proxyUrl: this.proxyUrl,
137144
scriptUrl: this.options.clerkJSUrl,
138145
scriptVariant: this.options.clerkJSVariant,
139146
});

packages/react/src/utils/scriptLoader.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,14 @@ export interface LoadScriptParams {
6868
*/
6969
frontendApi?: string;
7070
publishableKey?: string;
71+
proxyUrl?: string;
7172
scriptUrl?: string;
7273
scriptVariant?: ScriptVariant;
7374
}
7475

7576
export async function loadScript(params: LoadScriptParams): Promise<HTMLScriptElement | null> {
7677
return new Promise((resolve, reject) => {
77-
const { frontendApi, publishableKey } = params;
78+
const { frontendApi, publishableKey, proxyUrl } = params;
7879

7980
if (global.Clerk) {
8081
resolve(null);
@@ -91,7 +92,9 @@ export async function loadScript(params: LoadScriptParams): Promise<HTMLScriptEl
9192
} else if (frontendApi) {
9293
script.setAttribute('data-clerk-frontend-api', frontendApi);
9394
}
94-
95+
if (proxyUrl) {
96+
script.setAttribute('data-clerk-proxy-url', proxyUrl);
97+
}
9598
script.setAttribute('crossorigin', 'anonymous');
9699
script.async = true;
97100

packages/types/src/clerk.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export interface Clerk {
6464
/** Clerk Publishable Key string. */
6565
publishableKey?: string;
6666

67+
/** Clerk Proxy url string. */
68+
proxyUrl?: string;
69+
6770
instanceType?: InstanceType;
6871

6972
/** Client handling most Clerk operations. */

0 commit comments

Comments
 (0)