@@ -114,13 +114,17 @@ const makeRes = () => {
114114const makeReq = ( init : {
115115 hostname : string ;
116116 path ?: string ;
117+ originalUrl ?: string ;
117118 protocol ?: string ;
118119 headers ?: Record < string , string > ;
119120 cookies ?: Record < string , string > ;
120121} ) : Request =>
121122 ( {
122123 hostname : init . hostname ,
123124 path : init . path ?? '/' ,
125+ // Redirect helpers use originalUrl (preserves query string).
126+ // Default to `path` when caller doesn't care to distinguish.
127+ originalUrl : init . originalUrl ?? init . path ?? '/' ,
124128 protocol : init . protocol ?? 'http' ,
125129 headers : init . headers ?? { } ,
126130 cookies : init . cookies ?? { } ,
@@ -358,12 +362,15 @@ describe('createPuterSiteMiddleware — subdomain lookup', () => {
358362 } ) ;
359363} ) ;
360364
361- // ── Private hosting domain refusal ─────────────── ───────────────────
365+ // ── Private hosting domain → public-host redirect ───────────────────
362366
363367describe ( 'createPuterSiteMiddleware — private hosting domain' , ( ) => {
364- it ( '404s a subdomain on the private host that has no private app (prevents public-site leak via private host )' , async ( ) => {
368+ it ( '302s a subdomain on the private host with no private app to the equivalent puter.site URL (covers freed-paid-app bookmarks + plain hosted sites )' , async ( ) => {
365369 // Owner exists, subdomain exists, but it has no associated
366- // private app. On the *private* host this must refuse, not serve.
370+ // private app. On the *private* host this used to 404; now it
371+ // mirrors the public→private redirect so a paid app whose price
372+ // dropped to 0 still resolves on `puter.site` when accessed via
373+ // its old `puter.app` URL.
367374 const owner = await makeUser ( ) ;
368375 const sub = `leak-${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
369376 await server . stores . subdomain . create ( {
@@ -376,15 +383,18 @@ describe('createPuterSiteMiddleware — private hosting domain', () => {
376383 makeReq ( {
377384 // Note: app.puter.localhost is the *private* hosting domain.
378385 hostname : `${ sub } .app.puter.localhost` ,
386+ path : '/some/deep/path.html' ,
379387 } ) ,
380388 ) ;
381- expect ( out . statusCode ) . toBe ( 404 ) ;
382- expect ( out . body ) . toBe ( 'Subdomain not found' ) ;
389+ expect ( out . redirected ) . toEqual ( {
390+ status : 302 ,
391+ url : `http://${ sub } .site.puter.localhost/some/deep/path.html` ,
392+ } ) ;
383393 } ) ;
384394
385- it ( 'uses the alt private hosting domain when configured' , async ( ) => {
395+ it ( 'uses the alt private hosting domain when configured (same redirect to the public host) ' , async ( ) => {
386396 // Coverage for the `private_app_hosting_domain_alt` slot — same
387- // refusal logic, but via the alternate host that the deployment
397+ // redirect logic, but via the alternate host that the deployment
388398 // can use for legacy traffic.
389399 const owner = await makeUser ( ) ;
390400 const sub = `altleak-${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
@@ -399,8 +409,40 @@ describe('createPuterSiteMiddleware — private hosting domain', () => {
399409 mw ,
400410 makeReq ( { hostname : `${ sub } .apps.alt.localhost` } ) ,
401411 ) ;
412+ expect ( out . redirected ) . toEqual ( {
413+ status : 302 ,
414+ url : `http://${ sub } .site.puter.localhost/` ,
415+ } ) ;
416+ } ) ;
417+
418+ it ( 'falls back to 404 when no public hosting domain is configured (no leak)' , async ( ) => {
419+ // Without a static_hosting_domain to redirect to we have no safe
420+ // target; the original refusal must still apply so a public-app
421+ // subdomain doesn't accidentally serve via the private host.
422+ const owner = await makeUser ( ) ;
423+ const sub = `nopub-${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
424+ await server . stores . subdomain . create ( {
425+ userId : owner . id ,
426+ subdomain : sub ,
427+ } ) ;
428+ const mw = createPuterSiteMiddleware (
429+ {
430+ ...hostingConfig ,
431+ static_hosting_domain : null ,
432+ } as unknown as IConfig ,
433+ {
434+ clients : server . clients ,
435+ stores : server . stores ,
436+ services : server . services ,
437+ } ,
438+ ) ;
439+ const { out } = await runMiddleware (
440+ mw ,
441+ makeReq ( { hostname : `${ sub } .app.puter.localhost` } ) ,
442+ ) ;
402443 expect ( out . statusCode ) . toBe ( 404 ) ;
403444 expect ( out . body ) . toBe ( 'Subdomain not found' ) ;
445+ expect ( out . redirected ) . toBeUndefined ( ) ;
404446 } ) ;
405447} ) ;
406448
@@ -466,12 +508,7 @@ describe('createPuterSiteMiddleware — file serving', () => {
466508 rootDirId : homeEntry ! . id ,
467509 } ) ;
468510 const body = Buffer . from ( '<html>hi</html>' ) ;
469- await writeFile (
470- owner . id ,
471- `${ homePath } /index.html` ,
472- body ,
473- 'text/html' ,
474- ) ;
511+ await writeFile ( owner . id , `${ homePath } /index.html` , body , 'text/html' ) ;
475512
476513 const mw = buildMiddleware ( ) ;
477514 const { res, out } = makeRes ( ) ;
@@ -546,12 +583,7 @@ describe('createPuterSiteMiddleware — file serving', () => {
546583 rootDirId : homeEntry ! . id ,
547584 } ) ;
548585 const body = Buffer . from ( 'default doc' ) ;
549- await writeFile (
550- owner . id ,
551- `${ homePath } /index.html` ,
552- body ,
553- 'text/html' ,
554- ) ;
586+ await writeFile ( owner . id , `${ homePath } /index.html` , body , 'text/html' ) ;
555587
556588 const mw = buildMiddleware ( ) ;
557589 const { res, out } = makeRes ( ) ;
@@ -657,11 +689,7 @@ describe('createPuterSiteMiddleware — file serving', () => {
657689 const body = Buffer . from ( 'not a directory' ) ;
658690 // Write a file and use ITS id as the subdomain's root_dir_id —
659691 // the middleware must reject because root must be a directory.
660- await writeFile (
661- owner . id ,
662- `${ homePath } /Documents/somefile.txt` ,
663- body ,
664- ) ;
692+ await writeFile ( owner . id , `${ homePath } /Documents/somefile.txt` , body ) ;
665693 const fileEntry = await server . stores . fsEntry . getEntryByPath (
666694 `${ homePath } /Documents/somefile.txt` ,
667695 ) ;
@@ -733,12 +761,7 @@ describe('createPuterSiteMiddleware — file serving', () => {
733761 rootDirId : homeEntry ! . id ,
734762 } ) ;
735763 const body = Buffer . from ( 'inside' ) ;
736- await writeFile (
737- owner . id ,
738- `${ homePath } /safe.txt` ,
739- body ,
740- 'text/plain' ,
741- ) ;
764+ await writeFile ( owner . id , `${ homePath } /safe.txt` , body , 'text/plain' ) ;
742765
743766 const mw = buildMiddleware ( ) ;
744767 const { res, out } = makeRes ( ) ;
0 commit comments