@@ -352,127 +352,76 @@ describe('adapters/handler', () => {
352352 }
353353 } )
354354
355- it ( 'a hostile first request never becomes the OTP-link origin; a later loopback one does' , async ( ) => {
356- const wsPort = await getPort ( { port : 18180 , host : '127.0.0.1' } )
355+ // The auth-link origin is derived from the served request's URL (the fetch
356+ // handler ignores the `Host` header — that path is `nodeMiddleware`'s), so
357+ // each case just points a request at the origin under test and inspects the
358+ // one-time banner (`console.log`).
359+ async function withBannerSpy (
360+ id : string ,
361+ extra : Partial < Parameters < typeof initDevframe > [ 1 ] > ,
362+ run : ( devtools : ReturnType < typeof initDevframe > , spy : ReturnType < typeof vi . spyOn > ) => Promise < void > ,
363+ ) : Promise < void > {
364+ const wsPort = await getPort ( { host : '127.0.0.1' } )
357365 const spy = vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } )
358- const devtools = initDevframe ( defineTestDef ( 'handler-poison' ) , { base : '/__handler-poison/' , host : '127.0.0.1' , ws : { port : wsPort } } )
359-
366+ const devtools = initDevframe ( defineTestDef ( id ) , { base : `/__${ id } /` , host : '127.0.0.1' , ws : { port : wsPort } , ...extra } )
360367 try {
361368 await devtools . ready
362- // A first request forging a non-loopback Host must not print, adopt, or
363- // register that authority as the magic-link origin.
364- await devtools . handler ( new Request ( 'http://evil.example.com/__handler-poison/__connection.json' , {
365- headers : { host : 'evil.example.com' } ,
366- } ) )
367- expect ( spy ) . not . toHaveBeenCalled ( )
368-
369- // A later loopback request is trusted, adopted, and prints exactly one
370- // link pointing at that origin — the rejected candidate never locked it
371- // out.
372- await devtools . handler ( new Request ( 'http://localhost:4321/__handler-poison/__connection.json' ) )
373- expect ( spy ) . toHaveBeenCalledTimes ( 1 )
374- const link = String ( spy . mock . calls [ 0 ] )
375- expect ( link ) . toContain ( 'http://localhost:4321/#' )
376- expect ( link ) . not . toContain ( 'evil.example.com' )
377- // The credential rides the fragment; assert only its presence.
378- expect ( link ) . toContain ( '#devframe_otp=' )
379-
380- // The first-valid origin is pinned: a second loopback request neither
381- // re-prints nor moves it.
382- await devtools . handler ( new Request ( 'http://127.0.0.1:9999/__handler-poison/__connection.json' ) )
383- expect ( spy ) . toHaveBeenCalledTimes ( 1 )
369+ await run ( devtools , spy )
384370 }
385371 finally {
386372 spy . mockRestore ( )
387373 await devtools . close ( )
388374 }
389- } )
390-
391- it ( 'adopts an exactly allow-listed non-loopback origin, but rejects a prefix/suffix near-match' , async ( ) => {
392- const wsPort = await getPort ( { port : 18181 , host : '127.0.0.1' } )
393- const spy = vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } )
394- const devtools = initDevframe ( defineTestDef ( 'handler-allow' ) , {
395- base : '/__handler-allow/' ,
396- host : '127.0.0.1' ,
397- ws : { port : wsPort } ,
398- allowedOrigins : [ 'https://tools.example.com' ] ,
399- } )
400-
401- try {
402- await devtools . ready
403- // Only prefix/suffix-matches the allow-list entry — never adopted.
404- await devtools . handler ( new Request ( 'https://tools.example.com.evil.com/__handler-allow/__connection.json' , {
405- headers : { host : 'tools.example.com.evil.com' } ,
406- } ) )
407- await devtools . handler ( new Request ( 'https://evil.tools.example.com/__handler-allow/__connection.json' , {
408- headers : { host : 'evil.tools.example.com' } ,
409- } ) )
375+ }
376+ const hit = ( devtools : ReturnType < typeof initDevframe > , origin : string ) : Promise < Response > =>
377+ devtools . handler ( new Request ( `${ origin } /__connection.json` ) )
378+
379+ it ( 'a hostile first request never becomes the OTP-link origin; a later loopback one does' , ( ) =>
380+ withBannerSpy ( 'h-poison' , { } , async ( devtools , spy ) => {
381+ // A forged non-loopback origin is not adopted and prints nothing.
382+ await hit ( devtools , 'http://evil.example.com/__h-poison' )
410383 expect ( spy ) . not . toHaveBeenCalled ( )
384+ // A later loopback origin is adopted and prints exactly one OTP link
385+ // (the credential rides the fragment) — the reject never locked it out.
386+ await hit ( devtools , 'http://localhost:4321/__h-poison' )
387+ expect ( spy ) . toHaveBeenCalledTimes ( 1 )
388+ expect ( String ( spy . mock . calls [ 0 ] ) ) . toContain ( 'http://localhost:4321/#devframe_otp=' )
389+ expect ( String ( spy . mock . calls [ 0 ] ) ) . not . toContain ( 'evil.example.com' )
390+ // First-valid origin is pinned: a second loopback request doesn't move it.
391+ await hit ( devtools , 'http://127.0.0.1:9999/__h-poison' )
392+ expect ( spy ) . toHaveBeenCalledTimes ( 1 )
393+ } ) )
411394
412- // The exact allow-listed origin is adopted.
413- await devtools . handler ( new Request ( 'https://tools.example.com/__handler-allow/__connection.json' , {
414- headers : { host : 'tools.example.com' } ,
415- } ) )
395+ it ( 'adopts an exactly allow-listed non-loopback origin, but rejects a near-match' , ( ) =>
396+ withBannerSpy ( 'h-allow' , { allowedOrigins : [ 'https://tools.example.com' ] } , async ( devtools , spy ) => {
397+ // Prefix/suffix near-matches of the allow-list entry are never adopted.
398+ await hit ( devtools , 'https://tools.example.com.evil.com/__h-allow' )
399+ await hit ( devtools , 'https://evil.tools.example.com/__h-allow' )
400+ expect ( spy ) . not . toHaveBeenCalled ( )
401+ // The exact allow-listed origin is.
402+ await hit ( devtools , 'https://tools.example.com/__h-allow' )
416403 expect ( spy ) . toHaveBeenCalledTimes ( 1 )
417404 expect ( String ( spy . mock . calls [ 0 ] ) ) . toContain ( 'https://tools.example.com/#' )
418- }
419- finally {
420- spy . mockRestore ( )
421- await devtools . close ( )
422- }
423- } )
405+ } ) )
424406
425- it ( 'an explicit origin wins regardless of the inbound Host' , async ( ) => {
426- const wsPort = await getPort ( { port : 18182 , host : '127.0.0.1' } )
427- const spy = vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } )
428- const devtools = initDevframe ( defineTestDef ( 'handler-pinned' ) , {
429- base : '/__handler-pinned/' ,
430- host : '127.0.0.1' ,
431- ws : { port : wsPort } ,
432- origin : 'https://pinned.example.com' ,
433- } )
434-
435- try {
436- await devtools . ready
437- // A pinned origin needs no request: the banner points at it from the
438- // start, ignoring whatever Host a request forges.
407+ it ( 'an explicit origin wins over any request' , ( ) =>
408+ withBannerSpy ( 'h-pinned' , { origin : 'https://pinned.example.com' } , async ( devtools , spy ) => {
409+ // Pinned: the banner points at it before any request, and a forged
410+ // request can't move it.
439411 expect ( spy ) . toHaveBeenCalledTimes ( 1 )
440412 expect ( String ( spy . mock . calls [ 0 ] ) ) . toContain ( 'https://pinned.example.com/#' )
441-
442- await devtools . handler ( new Request ( 'http://evil.example.com/__handler-pinned/__connection.json' , {
443- headers : { host : 'evil.example.com' } ,
444- } ) )
413+ await hit ( devtools , 'http://evil.example.com/__h-pinned' )
445414 expect ( spy ) . toHaveBeenCalledTimes ( 1 )
446- expect ( String ( spy . mock . calls [ 0 ] ) ) . toContain ( 'https://pinned.example.com/#' )
447415 expect ( String ( spy . mock . calls [ 0 ] ) ) . not . toContain ( 'evil.example.com' )
448- }
449- finally {
450- spy . mockRestore ( )
451- await devtools . close ( )
452- }
453- } )
454-
455- it ( 'canonicalizes the protocol and default port of an adopted origin' , async ( ) => {
456- const wsPort = await getPort ( { port : 18183 , host : '127.0.0.1' } )
457- const spy = vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } )
458- const devtools = initDevframe ( defineTestDef ( 'handler-canon' ) , { base : '/__handler-canon/' , host : '127.0.0.1' , ws : { port : wsPort } } )
416+ } ) )
459417
460- try {
461- await devtools . ready
462- // An explicit :80 default port canonicalizes away in the advertised
463- // origin, so the link carries no redundant port.
464- await devtools . handler ( new Request ( 'http://localhost:80/__handler-canon/__connection.json' , {
465- headers : { host : 'localhost:80' } ,
466- } ) )
418+ it ( 'canonicalizes an adopted origin, dropping the default port' , ( ) =>
419+ withBannerSpy ( 'h-canon' , { } , async ( devtools , spy ) => {
420+ await hit ( devtools , 'http://localhost:80/__h-canon' )
467421 expect ( spy ) . toHaveBeenCalledTimes ( 1 )
468422 expect ( String ( spy . mock . calls [ 0 ] ) ) . toContain ( 'http://localhost/#' )
469423 expect ( String ( spy . mock . calls [ 0 ] ) ) . not . toContain ( 'localhost:80' )
470- }
471- finally {
472- spy . mockRestore ( )
473- await devtools . close ( )
474- }
475- } )
424+ } ) )
476425
477426 it ( 'bridge mode: without a distDir only meta + WS are served' , async ( ) => {
478427 const wsPort = await getPort ( { port : 18160 , host : '127.0.0.1' } )
0 commit comments