@@ -148,6 +148,62 @@ async function withProxyUriTemplateEnv<T>(
148148 }
149149}
150150
151+ const APP_PROXY_BASE_PATH = "/@u/ws/apps/mux" ;
152+ const APP_PROXY_BASE_PATH_ALT = "/@alice/dev/apps/mux" ;
153+
154+ function countOccurrences ( value : string , needle : string ) : number {
155+ return value . split ( needle ) . length - 1 ;
156+ }
157+
158+ async function createStaticTestServer (
159+ options : {
160+ files ?: Record < string , string > ;
161+ context ?: Partial < ORPCContext > ;
162+ authToken ?: string ;
163+ } = { }
164+ ) : Promise < {
165+ server : Awaited < ReturnType < typeof createOrpcServer > > ;
166+ tempDir : string ;
167+ close : ( ) => Promise < void > ;
168+ } > {
169+ const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "mux-static-app-proxy-" ) ) ;
170+ const files = {
171+ "index.html" :
172+ "<!doctype html><html><head><title>mux</title></head><body><div>ok</div></body></html>" ,
173+ ...options . files ,
174+ } ;
175+
176+ for ( const [ filePath , contents ] of Object . entries ( files ) ) {
177+ const absolutePath = path . join ( tempDir , filePath ) ;
178+ await fs . mkdir ( path . dirname ( absolutePath ) , { recursive : true } ) ;
179+ await fs . writeFile ( absolutePath , contents , "utf-8" ) ;
180+ }
181+
182+ let server : Awaited < ReturnType < typeof createOrpcServer > > | null = null ;
183+ try {
184+ server = await createOrpcServer ( {
185+ host : "127.0.0.1" ,
186+ port : 0 ,
187+ context : ( options . context ?? { } ) as ORPCContext ,
188+ authToken : options . authToken ,
189+ serveStatic : true ,
190+ staticDir : tempDir ,
191+ } ) ;
192+ } catch ( error ) {
193+ await fs . rm ( tempDir , { recursive : true , force : true } ) ;
194+ throw error ;
195+ }
196+
197+ return {
198+ server,
199+ tempDir,
200+ close : async ( ) => {
201+ await server ?. close ( ) ;
202+ await fs . rm ( tempDir , { recursive : true , force : true } ) ;
203+ } ,
204+ } ;
205+ }
206+
151207describe ( "createOrpcServer" , ( ) => {
152208 test ( "serveStatic fallback does not swallow /api routes" , async ( ) => {
153209 // Minimal context stub - router won't be exercised by this test.
@@ -185,6 +241,212 @@ describe("createOrpcServer", () => {
185241 }
186242 } ) ;
187243
244+ test ( "serves SPA base href from the detected public base path per request" , async ( ) => {
245+ const { server, close } = await createStaticTestServer ( ) ;
246+
247+ try {
248+ const rootRes = await fetch ( `${ server . baseUrl } /` ) ;
249+ expect ( rootRes . status ) . toBe ( 200 ) ;
250+ const rootHtml = await rootRes . text ( ) ;
251+ expect ( countOccurrences ( rootHtml , '<base href="/" />' ) ) . toBe ( 1 ) ;
252+
253+ const forwardedPrefixRes = await fetch ( `${ server . baseUrl } /some/spa/route` , {
254+ headers : { "X-Forwarded-Prefix" : APP_PROXY_BASE_PATH } ,
255+ } ) ;
256+ expect ( forwardedPrefixRes . status ) . toBe ( 200 ) ;
257+ const forwardedPrefixHtml = await forwardedPrefixRes . text ( ) ;
258+ expect ( forwardedPrefixHtml ) . toContain ( `<base href="${ APP_PROXY_BASE_PATH } /" />` ) ;
259+
260+ const originalUriRes = await fetch ( `${ server . baseUrl } /` , {
261+ headers : { "X-Original-Uri" : `${ APP_PROXY_BASE_PATH } /` } ,
262+ } ) ;
263+ expect ( originalUriRes . status ) . toBe ( 200 ) ;
264+ const originalUriHtml = await originalUriRes . text ( ) ;
265+ expect ( originalUriHtml ) . toContain ( `<base href="${ APP_PROXY_BASE_PATH } /" />` ) ;
266+
267+ const firstPrefixRes = await fetch ( `${ server . baseUrl } /one` , {
268+ headers : { "X-Forwarded-Prefix" : APP_PROXY_BASE_PATH } ,
269+ } ) ;
270+ const secondPrefixRes = await fetch ( `${ server . baseUrl } /two` , {
271+ headers : { "X-Forwarded-Prefix" : APP_PROXY_BASE_PATH_ALT } ,
272+ } ) ;
273+ expect ( await firstPrefixRes . text ( ) ) . toContain ( `<base href="${ APP_PROXY_BASE_PATH } /" />` ) ;
274+ expect ( await secondPrefixRes . text ( ) ) . toContain ( `<base href="${ APP_PROXY_BASE_PATH_ALT } /" />` ) ;
275+ } finally {
276+ await close ( ) ;
277+ }
278+ } ) ;
279+
280+ test ( "routes direct app-proxy HTTP requests to root-mounted handlers" , async ( ) => {
281+ const mainJs = "console.log('prefixed asset');" ;
282+ const authContext : Partial < ORPCContext > = {
283+ serverAuthService : {
284+ isGithubDeviceFlowEnabled : ( ) => true ,
285+ } as unknown as ORPCContext [ "serverAuthService" ] ,
286+ } ;
287+ const { server, close } = await createStaticTestServer ( {
288+ files : { "assets/main.js" : mainJs } ,
289+ context : authContext ,
290+ } ) ;
291+
292+ try {
293+ const rootAssetRes = await fetch ( `${ server . baseUrl } /assets/main.js` ) ;
294+ const prefixedAssetRes = await fetch (
295+ `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /assets/main.js`
296+ ) ;
297+ expect ( prefixedAssetRes . status ) . toBe ( 200 ) ;
298+ expect ( await prefixedAssetRes . text ( ) ) . toBe ( await rootAssetRes . text ( ) ) ;
299+
300+ const prefixedSpaRes = await fetch ( `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /settings` ) ;
301+ expect ( prefixedSpaRes . status ) . toBe ( 200 ) ;
302+ expect ( await prefixedSpaRes . text ( ) ) . toContain ( `<base href="${ APP_PROXY_BASE_PATH } /" />` ) ;
303+
304+ const specRes = await fetch ( `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /api/spec.json` ) ;
305+ expect ( specRes . status ) . toBe ( 200 ) ;
306+ expect ( specRes . headers . get ( "content-type" ) ) . toContain ( "application/json" ) ;
307+ const spec = ( await specRes . json ( ) ) as { servers ?: Array < { url ?: string } > } ;
308+ expect ( spec . servers ?. [ 0 ] ?. url ) . toBe ( `${ APP_PROXY_BASE_PATH } /api` ) ;
309+
310+ const docsRes = await fetch ( `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /api/docs` ) ;
311+ expect ( docsRes . status ) . toBe ( 200 ) ;
312+ expect ( await docsRes . text ( ) ) . toContain ( `${ APP_PROXY_BASE_PATH } /api/spec.json` ) ;
313+
314+ const authRes = await fetch (
315+ `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /auth/server-login/options`
316+ ) ;
317+ expect ( authRes . status ) . toBe ( 200 ) ;
318+ expect ( await authRes . json ( ) ) . toEqual ( { githubDeviceFlowEnabled : true } ) ;
319+
320+ const client = createHttpClient ( `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } ` ) ;
321+ const pingResult = await Promise . resolve ( client . general . ping ( "app-proxy" ) ) ;
322+ expect ( pingResult ) . toBe ( "Pong: app-proxy" ) ;
323+
324+ const falsePositiveRes = await fetch ( `${ server . baseUrl } /projects/apps/other` ) ;
325+ expect ( falsePositiveRes . status ) . toBe ( 200 ) ;
326+ expect ( await falsePositiveRes . text ( ) ) . toContain ( '<base href="/" />' ) ;
327+ } finally {
328+ await close ( ) ;
329+ }
330+ } ) ;
331+
332+ test ( "keeps origin validation active after direct app-proxy prefix stripping" , async ( ) => {
333+ const stubContext : Partial < ORPCContext > = { } ;
334+ let server : Awaited < ReturnType < typeof createOrpcServer > > | null = null ;
335+
336+ try {
337+ server = await createOrpcServer ( {
338+ host : "127.0.0.1" ,
339+ port : 0 ,
340+ context : stubContext as ORPCContext ,
341+ } ) ;
342+
343+ const rootResponse = await fetch ( `${ server . baseUrl } /orpc` , {
344+ headers : { Origin : "https://evil.example.com" } ,
345+ } ) ;
346+ const prefixedResponse = await fetch ( `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /orpc` , {
347+ headers : { Origin : "https://evil.example.com" } ,
348+ } ) ;
349+
350+ expect ( rootResponse . status ) . toBe ( 403 ) ;
351+ expect ( prefixedResponse . status ) . toBe ( rootResponse . status ) ;
352+ } finally {
353+ await server ?. close ( ) ;
354+ }
355+ } ) ;
356+
357+ test ( "serves Scalar docs with request-specific spec URLs" , async ( ) => {
358+ const { server, close } = await createStaticTestServer ( ) ;
359+
360+ try {
361+ const rootDocsRes = await fetch ( `${ server . baseUrl } /api/docs` ) ;
362+ expect ( rootDocsRes . status ) . toBe ( 200 ) ;
363+ expect ( await rootDocsRes . text ( ) ) . toContain ( 'url: "/api/spec.json"' ) ;
364+
365+ const forwardedDocsRes = await fetch ( `${ server . baseUrl } /api/docs` , {
366+ headers : { "X-Forwarded-Prefix" : APP_PROXY_BASE_PATH } ,
367+ } ) ;
368+ expect ( forwardedDocsRes . status ) . toBe ( 200 ) ;
369+ expect ( await forwardedDocsRes . text ( ) ) . toContain (
370+ `url: "${ APP_PROXY_BASE_PATH } /api/spec.json"`
371+ ) ;
372+
373+ const directDocsRes = await fetch ( `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /api/docs` ) ;
374+ expect ( directDocsRes . status ) . toBe ( 200 ) ;
375+ expect ( await directDocsRes . text ( ) ) . toContain ( `url: "${ APP_PROXY_BASE_PATH } /api/spec.json"` ) ;
376+ } finally {
377+ await close ( ) ;
378+ }
379+ } ) ;
380+
381+ test ( "accepts direct app-proxy WebSocket upgrades" , async ( ) => {
382+ const stubContext : Partial < ORPCContext > = { } ;
383+ let server : Awaited < ReturnType < typeof createOrpcServer > > | null = null ;
384+ let ws : WebSocket | null = null ;
385+
386+ try {
387+ server = await createOrpcServer ( {
388+ host : "127.0.0.1" ,
389+ port : 0 ,
390+ context : stubContext as ORPCContext ,
391+ } ) ;
392+
393+ ws = new WebSocket (
394+ `${ server . baseUrl . replace ( / ^ h t t p / , "ws" ) } ${ APP_PROXY_BASE_PATH } /orpc/ws?token=test-token`
395+ ) ;
396+
397+ await waitForWebSocketOpen ( ws ) ;
398+ await closeWebSocket ( ws ) ;
399+ ws = null ;
400+ } finally {
401+ ws ?. terminate ( ) ;
402+ await server ?. close ( ) ;
403+ }
404+ } ) ;
405+
406+ test ( "includes app-proxy base paths in OAuth redirect and callback return URLs" , async ( ) => {
407+ let muxGatewayRedirectUri = "" ;
408+ const stubContext : Partial < ORPCContext > = {
409+ muxGatewayOauthService : {
410+ startServerFlow : ( input : { redirectUri : string } ) => {
411+ muxGatewayRedirectUri = input . redirectUri ;
412+ return { authorizeUrl : "https://gateway.example.com/auth" , state : "state-gateway" } ;
413+ } ,
414+ handleServerCallbackAndExchange : ( ) => Promise . resolve ( { success : true , data : undefined } ) ,
415+ } as unknown as ORPCContext [ "muxGatewayOauthService" ] ,
416+ } ;
417+ let server : Awaited < ReturnType < typeof createOrpcServer > > | null = null ;
418+
419+ try {
420+ server = await createOrpcServer ( {
421+ host : "127.0.0.1" ,
422+ port : 0 ,
423+ context : stubContext as ORPCContext ,
424+ authToken : "test-token" ,
425+ } ) ;
426+
427+ const startResponse = await fetch ( `${ server . baseUrl } /auth/mux-gateway/start` , {
428+ headers : {
429+ Authorization : "Bearer test-token" ,
430+ "X-Forwarded-Prefix" : APP_PROXY_BASE_PATH ,
431+ } ,
432+ } ) ;
433+ expect ( startResponse . status ) . toBe ( 200 ) ;
434+ expect ( muxGatewayRedirectUri ) . toBe (
435+ `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /auth/mux-gateway/callback`
436+ ) ;
437+
438+ const callbackResponse = await fetch (
439+ `${ server . baseUrl } ${ APP_PROXY_BASE_PATH } /auth/mux-gateway/callback?state=test&code=test`
440+ ) ;
441+ expect ( callbackResponse . status ) . toBe ( 200 ) ;
442+ const callbackHtml = await callbackResponse . text ( ) ;
443+ expect ( callbackHtml ) . toContain ( `href="${ APP_PROXY_BASE_PATH } /"` ) ;
444+ expect ( callbackHtml ) . toContain ( `window.location.replace("${ APP_PROXY_BASE_PATH } /")` ) ;
445+ } finally {
446+ await server ?. close ( ) ;
447+ }
448+ } ) ;
449+
188450 test ( "injects proxy URI template into SPA fallback HTML when env is set" , async ( ) => {
189451 const stubContext : Partial < ORPCContext > = { } ;
190452
0 commit comments