@@ -83,14 +83,21 @@ describe('proxy', () => {
8383 describe ( 'clerkFrontendApiProxy' , ( ) => {
8484 const mockFetch = vi . fn ( ) ;
8585 const originalFetch = global . fetch ;
86+ const originalFapiUrl = process . env . CLERK_FAPI_URL ;
8687
8788 beforeEach ( ( ) => {
8889 global . fetch = mockFetch ;
8990 mockFetch . mockReset ( ) ;
91+ delete process . env . CLERK_FAPI_URL ;
9092 } ) ;
9193
9294 afterEach ( ( ) => {
9395 global . fetch = originalFetch ;
96+ if ( originalFapiUrl === undefined ) {
97+ delete process . env . CLERK_FAPI_URL ;
98+ } else {
99+ process . env . CLERK_FAPI_URL = originalFapiUrl ;
100+ }
94101 } ) ;
95102
96103 it ( 'returns error when publishableKey is missing' , async ( ) => {
@@ -197,6 +204,77 @@ describe('proxy', () => {
197204 expect ( response . status ) . toBe ( 200 ) ;
198205 } ) ;
199206
207+ it ( 'uses the configured FAPI URL over CLERK_FAPI_URL and the publishable key' , async ( ) => {
208+ process . env . CLERK_FAPI_URL = 'https://frontend-api.clerkstage.dev' ;
209+ mockFetch . mockResolvedValue ( new Response ( JSON . stringify ( { client : { } } ) , { status : 200 } ) ) ;
210+
211+ const request = new Request ( 'https://example.com/__clerk/v1/client' ) ;
212+
213+ await clerkFrontendApiProxy ( request , {
214+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
215+ secretKey : 'sk_test_xxx' ,
216+ fapiUrl : 'http://localhost:8001/' ,
217+ } ) ;
218+
219+ const [ url , options ] = mockFetch . mock . calls [ 0 ] ;
220+ expect ( url ) . toBe ( 'http://localhost:8001/v1/client' ) ;
221+ expect ( options . headers . get ( 'Host' ) ) . toBe ( 'localhost:8001' ) ;
222+ } ) ;
223+
224+ it ( 'uses CLERK_FAPI_URL when no FAPI URL is configured' , async ( ) => {
225+ process . env . CLERK_FAPI_URL = 'https://frontend-api.clerkstage.dev' ;
226+ mockFetch . mockResolvedValue ( new Response ( JSON . stringify ( { client : { } } ) , { status : 200 } ) ) ;
227+
228+ const request = new Request ( 'https://example.com/__clerk/v1/client' ) ;
229+
230+ await clerkFrontendApiProxy ( request , {
231+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
232+ secretKey : 'sk_test_xxx' ,
233+ } ) ;
234+
235+ expect ( mockFetch . mock . calls [ 0 ] [ 0 ] ) . toBe ( 'https://frontend-api.clerkstage.dev/v1/client' ) ;
236+ } ) ;
237+
238+ it . each ( [
239+ [ 'unsupported scheme' , 'ftp://frontend-api.clerk.dev' , undefined , 'FAPI URL must use http or https' ] ,
240+ [
241+ 'credentials' ,
242+ 'https://user:password@frontend-api.clerk.dev' ,
243+ undefined ,
244+ 'FAPI URL must not include credentials, a query string, or a hash' ,
245+ ] ,
246+ [
247+ 'query string' ,
248+ 'https://frontend-api.clerk.dev?env=staging' ,
249+ undefined ,
250+ 'FAPI URL must not include credentials, a query string, or a hash' ,
251+ ] ,
252+ [
253+ 'fragment' ,
254+ 'https://frontend-api.clerk.dev#staging' ,
255+ undefined ,
256+ 'FAPI URL must not include credentials, a query string, or a hash' ,
257+ ] ,
258+ [ 'invalid environment value' , undefined , 'not-a-url' , 'Invalid URL' ] ,
259+ ] ) ( 'returns a configuration error for an invalid FAPI URL with %s' , async ( _case , fapiUrl , envFapiUrl , message ) => {
260+ if ( envFapiUrl ) {
261+ process . env . CLERK_FAPI_URL = envFapiUrl ;
262+ }
263+ const request = new Request ( 'https://example.com/__clerk/v1/client' ) ;
264+
265+ const response = await clerkFrontendApiProxy ( request , {
266+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
267+ secretKey : 'sk_test_xxx' ,
268+ ...( fapiUrl ? { fapiUrl } : { } ) ,
269+ } ) ;
270+
271+ expect ( response . status ) . toBe ( 500 ) ;
272+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
273+ await expect ( response . json ( ) ) . resolves . toMatchObject ( {
274+ errors : [ { code : 'proxy_configuration_error' , message } ] ,
275+ } ) ;
276+ } ) ;
277+
200278 it ( 'forwards POST request with body' , async ( ) => {
201279 const mockResponse = new Response ( JSON . stringify ( { success : true } ) , {
202280 status : 200 ,
@@ -477,6 +555,27 @@ describe('proxy', () => {
477555 expect ( response . headers . get ( 'Location' ) ) . toBe ( 'https://example.com/__clerk/v1/oauth/callback?code=123' ) ;
478556 } ) ;
479557
558+ it ( 'rewrites redirects from a configured FAPI URL' , async ( ) => {
559+ const mockResponse = new Response ( null , {
560+ status : 302 ,
561+ headers : {
562+ Location : 'http://localhost:8001/v1/oauth/callback?code=123' ,
563+ } ,
564+ } ) ;
565+ mockFetch . mockResolvedValue ( mockResponse ) ;
566+
567+ const request = new Request ( 'https://example.com/__clerk/v1/oauth/authorize' ) ;
568+
569+ const response = await clerkFrontendApiProxy ( request , {
570+ publishableKey : 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' ,
571+ secretKey : 'sk_test_xxx' ,
572+ fapiUrl : 'http://localhost:8001' ,
573+ } ) ;
574+
575+ expect ( response . status ) . toBe ( 302 ) ;
576+ expect ( response . headers . get ( 'Location' ) ) . toBe ( 'https://example.com/__clerk/v1/oauth/callback?code=123' ) ;
577+ } ) ;
578+
480579 it ( 'does not rewrite Location header for external redirects' , async ( ) => {
481580 const mockResponse = new Response ( null , {
482581 status : 302 ,
0 commit comments