@@ -18,6 +18,18 @@ const createMockSessionAuth = () => ({
1818
1919const authenticateRequestMock = vi . fn ( ) ;
2020
21+ const { mockClerkFrontendApiProxy } = vi . hoisted ( ( ) => ( {
22+ mockClerkFrontendApiProxy : vi . fn ( ) ,
23+ } ) ) ;
24+
25+ vi . mock ( '@clerk/backend/proxy' , async ( ) => {
26+ const actual = await vi . importActual ( '@clerk/backend/proxy' ) ;
27+ return {
28+ ...actual ,
29+ clerkFrontendApiProxy : mockClerkFrontendApiProxy ,
30+ } ;
31+ } ) ;
32+
2133vi . mock ( import ( '@clerk/backend' ) , async importOriginal => {
2234 const original = await importOriginal ( ) ;
2335
@@ -163,6 +175,211 @@ describe('clerkMiddleware()', () => {
163175 } ) ;
164176} ) ;
165177
178+ describe ( 'Frontend API proxy handling' , ( ) => {
179+ beforeEach ( ( ) => {
180+ vi . stubEnv ( 'CLERK_SECRET_KEY' , EnvVariables . CLERK_SECRET_KEY ) ;
181+ vi . stubEnv ( 'CLERK_PUBLISHABLE_KEY' , EnvVariables . CLERK_PUBLISHABLE_KEY ) ;
182+ authenticateRequestMock . mockReset ( ) ;
183+ mockClerkFrontendApiProxy . mockReset ( ) ;
184+ } ) ;
185+
186+ test ( 'intercepts proxy path requests' , async ( ) => {
187+ mockClerkFrontendApiProxy . mockResolvedValueOnce ( new Response ( 'proxied' , { status : 200 } ) ) ;
188+
189+ const app = new Hono ( ) ;
190+ app . use ( '*' , clerkMiddleware ( { frontendApiProxy : { enabled : true } } ) ) ;
191+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
192+
193+ const response = await app . request ( new Request ( 'http://localhost/__clerk/v1/client' ) ) ;
194+
195+ expect ( response . status ) . toEqual ( 200 ) ;
196+ expect ( await response . text ( ) ) . toEqual ( 'proxied' ) ;
197+ expect ( mockClerkFrontendApiProxy ) . toHaveBeenCalled ( ) ;
198+ expect ( authenticateRequestMock ) . not . toHaveBeenCalled ( ) ;
199+ } ) ;
200+
201+ test ( 'intercepts proxy path with query parameters' , async ( ) => {
202+ mockClerkFrontendApiProxy . mockResolvedValueOnce ( new Response ( 'proxied' , { status : 200 } ) ) ;
203+
204+ const app = new Hono ( ) ;
205+ app . use ( '*' , clerkMiddleware ( { frontendApiProxy : { enabled : true } } ) ) ;
206+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
207+
208+ const response = await app . request ( new Request ( 'http://localhost/__clerk?_clerk_js_version=5.0.0' ) ) ;
209+
210+ expect ( response . status ) . toEqual ( 200 ) ;
211+ expect ( await response . text ( ) ) . toEqual ( 'proxied' ) ;
212+ expect ( mockClerkFrontendApiProxy ) . toHaveBeenCalled ( ) ;
213+ expect ( authenticateRequestMock ) . not . toHaveBeenCalled ( ) ;
214+ } ) ;
215+
216+ test ( 'authenticates default path when custom proxy path is set' , async ( ) => {
217+ authenticateRequestMock . mockResolvedValueOnce ( {
218+ status : 'handshake' ,
219+ reason : 'auth-reason' ,
220+ message : 'auth-message' ,
221+ headers : new Headers ( {
222+ location : 'https://fapi.example.com/v1/clients/handshake' ,
223+ 'x-clerk-auth-message' : 'auth-message' ,
224+ 'x-clerk-auth-reason' : 'auth-reason' ,
225+ 'x-clerk-auth-status' : 'handshake' ,
226+ } ) ,
227+ toAuth : createMockSessionAuth ,
228+ } ) ;
229+
230+ const app = new Hono ( ) ;
231+ app . use ( '*' , clerkMiddleware ( { frontendApiProxy : { enabled : true , path : '/custom-proxy' } } ) ) ;
232+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
233+
234+ const response = await app . request (
235+ new Request ( 'http://localhost/__clerk/v1/client' , {
236+ headers : {
237+ Cookie : '__client_uat=1711618859;' ,
238+ 'Sec-Fetch-Dest' : 'document' ,
239+ } ,
240+ } ) ,
241+ ) ;
242+
243+ expect ( response . status ) . toEqual ( 307 ) ;
244+ expect ( response . headers . get ( 'x-clerk-auth-status' ) ) . toEqual ( 'handshake' ) ;
245+ expect ( mockClerkFrontendApiProxy ) . not . toHaveBeenCalled ( ) ;
246+ expect ( authenticateRequestMock ) . toHaveBeenCalled ( ) ;
247+ } ) ;
248+
249+ test ( 'does not intercept when enabled is false' , async ( ) => {
250+ authenticateRequestMock . mockResolvedValueOnce ( {
251+ headers : new Headers ( ) ,
252+ toAuth : createMockSessionAuth ,
253+ } ) ;
254+
255+ const app = new Hono ( ) ;
256+ app . use ( '*' , clerkMiddleware ( { frontendApiProxy : { enabled : false } } ) ) ;
257+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
258+
259+ const response = await app . request ( new Request ( 'http://localhost/__clerk/v1/client' ) ) ;
260+
261+ expect ( response . status ) . toEqual ( 200 ) ;
262+ expect ( mockClerkFrontendApiProxy ) . not . toHaveBeenCalled ( ) ;
263+ expect ( authenticateRequestMock ) . toHaveBeenCalled ( ) ;
264+ } ) ;
265+
266+ test ( 'does not intercept when frontendApiProxy is not configured' , async ( ) => {
267+ authenticateRequestMock . mockResolvedValueOnce ( {
268+ headers : new Headers ( ) ,
269+ toAuth : createMockSessionAuth ,
270+ } ) ;
271+
272+ const app = new Hono ( ) ;
273+ app . use ( '*' , clerkMiddleware ( ) ) ;
274+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
275+
276+ const response = await app . request ( new Request ( 'http://localhost/__clerk/v1/client' ) ) ;
277+
278+ expect ( response . status ) . toEqual ( 200 ) ;
279+ expect ( mockClerkFrontendApiProxy ) . not . toHaveBeenCalled ( ) ;
280+ expect ( authenticateRequestMock ) . toHaveBeenCalled ( ) ;
281+ } ) ;
282+
283+ test ( 'still authenticates non-proxy paths when proxy is configured' , async ( ) => {
284+ authenticateRequestMock . mockResolvedValueOnce ( {
285+ headers : new Headers ( ) ,
286+ toAuth : createMockSessionAuth ,
287+ } ) ;
288+
289+ const app = new Hono ( ) ;
290+ app . use ( '*' , clerkMiddleware ( { frontendApiProxy : { enabled : true } } ) ) ;
291+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
292+
293+ const response = await app . request ( new Request ( 'http://localhost/api/users' ) ) ;
294+
295+ expect ( response . status ) . toEqual ( 200 ) ;
296+ expect ( await response . text ( ) ) . toEqual ( 'OK' ) ;
297+ expect ( mockClerkFrontendApiProxy ) . not . toHaveBeenCalled ( ) ;
298+ expect ( authenticateRequestMock ) . toHaveBeenCalled ( ) ;
299+ } ) ;
300+
301+ test ( 'uses env vars for keys when only frontendApiProxy is passed' , async ( ) => {
302+ mockClerkFrontendApiProxy . mockResolvedValueOnce ( new Response ( 'proxied' , { status : 200 } ) ) ;
303+
304+ const app = new Hono ( ) ;
305+ app . use ( '*' , clerkMiddleware ( { frontendApiProxy : { enabled : true } } ) ) ;
306+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
307+
308+ const response = await app . request ( new Request ( 'http://localhost/__clerk/v1/client' ) ) ;
309+
310+ expect ( response . status ) . toEqual ( 200 ) ;
311+ expect ( mockClerkFrontendApiProxy ) . toHaveBeenCalledWith (
312+ expect . any ( Request ) ,
313+ expect . objectContaining ( {
314+ publishableKey : EnvVariables . CLERK_PUBLISHABLE_KEY ,
315+ secretKey : EnvVariables . CLERK_SECRET_KEY ,
316+ } ) ,
317+ ) ;
318+ } ) ;
319+
320+ test ( 'auto-derives proxyUrl from request when frontendApiProxy is enabled' , async ( ) => {
321+ authenticateRequestMock . mockResolvedValueOnce ( {
322+ headers : new Headers ( ) ,
323+ toAuth : createMockSessionAuth ,
324+ } ) ;
325+
326+ const app = new Hono ( ) ;
327+ app . use ( '*' , clerkMiddleware ( { frontendApiProxy : { enabled : true } } ) ) ;
328+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
329+
330+ const response = await app . request (
331+ new Request ( 'http://localhost/api/users' , {
332+ headers : {
333+ 'x-forwarded-proto' : 'https' ,
334+ 'x-forwarded-host' : 'myapp.com' ,
335+ } ,
336+ } ) ,
337+ ) ;
338+
339+ expect ( response . status ) . toEqual ( 200 ) ;
340+ expect ( authenticateRequestMock ) . toHaveBeenCalledWith (
341+ expect . any ( Request ) ,
342+ expect . objectContaining ( {
343+ proxyUrl : '/__clerk' ,
344+ } ) ,
345+ ) ;
346+ } ) ;
347+
348+ test ( 'does not override explicit proxyUrl' , async ( ) => {
349+ authenticateRequestMock . mockResolvedValueOnce ( {
350+ headers : new Headers ( ) ,
351+ toAuth : createMockSessionAuth ,
352+ } ) ;
353+
354+ const app = new Hono ( ) ;
355+ app . use (
356+ '*' ,
357+ clerkMiddleware ( {
358+ frontendApiProxy : { enabled : true } ,
359+ proxyUrl : 'https://explicit.example.com/__clerk' ,
360+ } ) ,
361+ ) ;
362+ app . get ( '/*' , c => c . text ( 'OK' ) ) ;
363+
364+ const response = await app . request (
365+ new Request ( 'http://localhost/api/users' , {
366+ headers : {
367+ 'x-forwarded-proto' : 'https' ,
368+ 'x-forwarded-host' : 'myapp.com' ,
369+ } ,
370+ } ) ,
371+ ) ;
372+
373+ expect ( response . status ) . toEqual ( 200 ) ;
374+ expect ( authenticateRequestMock ) . toHaveBeenCalledWith (
375+ expect . any ( Request ) ,
376+ expect . objectContaining ( {
377+ proxyUrl : 'https://explicit.example.com/__clerk' ,
378+ } ) ,
379+ ) ;
380+ } ) ;
381+ } ) ;
382+
166383describe ( 'getAuth()' , ( ) => {
167384 beforeEach ( ( ) => {
168385 vi . stubEnv ( 'CLERK_SECRET_KEY' , EnvVariables . CLERK_SECRET_KEY ) ;
0 commit comments