@@ -477,9 +477,15 @@ test("browser setup --open opens only the public handoff URL by argv", async ()
477477 await rm ( result . configDir , { recursive : true , force : true } ) ;
478478} ) ;
479479
480- test ( "browser setup fails closed on extra delivery fields without echoing them" , async ( ) => {
480+ const BROWSER_CLAIM_SCREEN = {
481+ id : "scr_browser_link" ,
482+ public_id : "browser-link-screen" ,
483+ state : "pairing_pending" as const ,
484+ public_url : "https://play.screenrig.ai/s/browser-link-screen" ,
485+ } ;
486+
487+ function browserSetupClaimTransport ( body : unknown ) : FakeTransport {
481488 const transport = new FakeTransport ( ) ;
482- const secret = `https://play.screenrig.ai/s/browser-link-screen#provision=${ "S" . repeat ( 43 ) } ` ;
483489 transport . on ( "POST" , "/api/v1/enrollments" , ( ) => ( {
484490 status : 201 ,
485491 headers : { "cache-control" : "private, no-store" } ,
@@ -494,24 +500,61 @@ test("browser setup fails closed on extra delivery fields without echoing them",
494500 transport . on ( "POST" , "/api/v1/account/browser-links/claim" , ( ) => ( {
495501 status : 201 ,
496502 headers : { "cache-control" : "private, no-store" } ,
497- body : {
498- session_id : "bls_fixture" ,
499- status : "claimed" ,
500- screen : {
501- id : "scr_browser_link" ,
502- public_id : "browser-link-screen" ,
503- state : "pairing_pending" ,
504- public_url : "https://play.screenrig.ai/s/browser-link-screen" ,
503+ body,
504+ } ) ) ;
505+ return transport ;
506+ }
507+
508+ test ( "browser setup ignores extra claim and screen keys without echoing them" , async ( ) => {
509+ const secret = `https://play.screenrig.ai/s/browser-link-screen#provision=${ "S" . repeat ( 43 ) } ` ;
510+ const transport = browserSetupClaimTransport ( {
511+ session_id : "bls_fixture" ,
512+ status : "claimed" ,
513+ screen : {
514+ ...BROWSER_CLAIM_SCREEN ,
515+ timezone : "America/Los_Angeles" ,
516+ observation : {
517+ observed_at : "2026-08-14T17:00:00.000Z" ,
518+ surfaces : [ { id : "primary" , width : 1920 , height : 1080 , pixel_ratio : 1 , presentation : "output" } ] ,
505519 } ,
506- provisioning_url : secret ,
507520 } ,
508- } ) ) ;
521+ provisioning_url : secret ,
522+ } ) ;
509523 const result = await withRuntime ( [ "--json" , "browser" , "setup" , "--code" , "ABC234" ] , transport ) ;
510- assert . equal ( result . code , ExitCode . Usage ) ;
511- assert . doesNotMatch ( result . stdout , / # p r o v i s i o n = | p r o v i s i o n i n g _ u r l | S S S S S S / i) ;
524+ assert . equal ( result . code , 0 , result . stdout ) ;
525+ const envelope = JSON . parse ( result . stdout ) as { data : Record < string , unknown > } ;
526+ assert . deepEqual ( envelope . data , {
527+ code : "ABC-234" ,
528+ status : "claimed" ,
529+ player_public_url : "https://play.screenrig.ai/s/browser-link-screen" ,
530+ } ) ;
531+ assert . doesNotMatch ( result . stdout , / # p r o v i s i o n = | p r o v i s i o n i n g _ u r l | S S S S S S | t i m e z o n e | o b s e r v a t i o n | A m e r i c a \/ L o s _ A n g e l e s / i) ;
512532 await rm ( result . configDir , { recursive : true , force : true } ) ;
513533} ) ;
514534
535+ test ( "browser setup rejects missing required claim fields, bad status, and unsafe public URLs" , async ( ) => {
536+ const cases : Array < { name : string ; body : Record < string , unknown > } > = [
537+ { name : "missing session_id" , body : { status : "claimed" , screen : BROWSER_CLAIM_SCREEN } } ,
538+ { name : "bad status" , body : { session_id : "bls_fixture" , status : "waiting" , screen : BROWSER_CLAIM_SCREEN } } ,
539+ { name : "missing screen" , body : { session_id : "bls_fixture" , status : "claimed" } } ,
540+ { name : "missing screen id" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , id : "" } } } ,
541+ { name : "missing public_id" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , public_id : "" } } } ,
542+ { name : "bad screen state" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , state : "active" } } } ,
543+ { name : "missing public_url" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , public_url : "" } } } ,
544+ { name : "hash in public_url" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , public_url : "https://play.screenrig.ai/s/browser-link-screen#provision=SSS" } } } ,
545+ { name : "search in public_url" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , public_url : "https://play.screenrig.ai/s/browser-link-screen?q=1" } } } ,
546+ { name : "userinfo in public_url" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , public_url : "https://user:pass@play.screenrig.ai/s/browser-link-screen" } } } ,
547+ { name : "wrong origin" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , public_url : "https://example.invalid/s/browser-link-screen" } } } ,
548+ { name : "pathname mismatch" , body : { session_id : "bls_fixture" , status : "claimed" , screen : { ...BROWSER_CLAIM_SCREEN , public_url : "https://play.screenrig.ai/s/other-id" } } } ,
549+ ] ;
550+ for ( const item of cases ) {
551+ const result = await withRuntime ( [ "--json" , "browser" , "setup" , "--code" , "ABC234" ] , browserSetupClaimTransport ( item . body ) ) ;
552+ assert . equal ( result . code , ExitCode . Usage , item . name ) ;
553+ assert . doesNotMatch ( result . stdout , / # p r o v i s i o n = | u s e r : p a s s | t o k e n | c o o k i e | p r o o f / i, item . name ) ;
554+ await rm ( result . configDir , { recursive : true , force : true } ) ;
555+ }
556+ } ) ;
557+
515558test ( "browser setup rejects malformed codes before claim and keeps exact ambiguous retry state" , async ( ) => {
516559 const invalid = await withRuntime ( [ "--json" , "browser" , "setup" , "--code" , "ABC10I" ] , memoryBackend ( ) ) ;
517560 assert . equal ( invalid . code , ExitCode . Usage ) ;
@@ -2693,6 +2736,153 @@ test("screen update cannot send observation", async () => {
26932736 await rm ( configDir , { recursive : true , force : true } ) ;
26942737} ) ;
26952738
2739+ const DOCUMENTATION_IPV4 = "192.0.2.10" ;
2740+ const DOCUMENTATION_IPV6 = "2001:db8::1" ;
2741+
2742+ test ( "screen show includes online and optional last_online_at and last_ip" , async ( ) => {
2743+ const transport = new FakeTransport ( ) ;
2744+ const screen = {
2745+ content_access_generation : 1 ,
2746+ created_at : "2026-08-14T17:00:00.000Z" ,
2747+ id : "scr_PAIRINGAAAAAAAAAAAAAAAA" ,
2748+ label : "Lobby" ,
2749+ last_ip : DOCUMENTATION_IPV4 ,
2750+ last_online_at : "2026-08-19T12:00:00Z" ,
2751+ manifest_revision : 1 ,
2752+ online : true ,
2753+ public_id : "scr_public_pairing" ,
2754+ revision : 1 ,
2755+ state : "active" as const ,
2756+ updated_at : "2026-08-14T17:00:00.000Z" ,
2757+ } ;
2758+ transport . on ( "GET" , "/api/v1/screens/scr_PAIRINGAAAAAAAAAAAAAAAA" , ( ) => ( {
2759+ status : 200 ,
2760+ headers : { "x-request-id" : "req_show_online" } ,
2761+ body : screen ,
2762+ } ) ) ;
2763+ const configDir = await testTemp ( "screen-show-online-" ) ;
2764+ const fsLike = { mkdir, open, rename, rm, chmod, stat, homedir : ( ) => configDir , env : { XDG_CONFIG_HOME : configDir } } ;
2765+ await writeConfigAtomic (
2766+ path . join ( configDir , "screenrig" , "config.json" ) ,
2767+ { api_url : "https://api.screenrig.ai" , token : "sr_live_existing_secret" } ,
2768+ fsLike ,
2769+ ) ;
2770+
2771+ const jsonResult = await withRuntime (
2772+ [ "--json" , "screen" , "show" , "scr_PAIRINGAAAAAAAAAAAAAAAA" ] ,
2773+ transport ,
2774+ { fs : fsLike } ,
2775+ ) ;
2776+ assert . equal ( jsonResult . code , ExitCode . Success , jsonResult . stdout ) ;
2777+ const envelope = JSON . parse ( jsonResult . stdout ) as {
2778+ ok : boolean ;
2779+ data : { online ?: boolean ; last_online_at ?: string ; last_ip ?: string } ;
2780+ } ;
2781+ assert . equal ( envelope . ok , true ) ;
2782+ assert . equal ( envelope . data . online , true ) ;
2783+ assert . equal ( envelope . data . last_online_at , "2026-08-19T12:00:00Z" ) ;
2784+ assert . equal ( envelope . data . last_ip , DOCUMENTATION_IPV4 ) ;
2785+
2786+ const humanResult = await withRuntime (
2787+ [ "screen" , "show" , "scr_PAIRINGAAAAAAAAAAAAAAAA" ] ,
2788+ transport ,
2789+ { fs : fsLike } ,
2790+ ) ;
2791+ assert . equal ( humanResult . code , ExitCode . Success , humanResult . stdout ) ;
2792+ assert . match ( humanResult . stdout , / ^ S c r e e n \n / ) ;
2793+ assert . match ( humanResult . stdout , / " o n l i n e " : t r u e / ) ;
2794+ assert . match ( humanResult . stdout , / " l a s t _ o n l i n e _ a t " : " 2 0 2 6 - 0 8 - 1 9 T 1 2 : 0 0 : 0 0 Z " / ) ;
2795+ assert . match ( humanResult . stdout , / " l a s t _ i p " : " 1 9 2 .0 .2 .1 0 " / ) ;
2796+
2797+ screen . last_ip = DOCUMENTATION_IPV6 ;
2798+ const v6Result = await withRuntime (
2799+ [ "--json" , "screen" , "show" , "scr_PAIRINGAAAAAAAAAAAAAAAA" ] ,
2800+ transport ,
2801+ { fs : fsLike } ,
2802+ ) ;
2803+ assert . equal ( v6Result . code , ExitCode . Success , v6Result . stdout ) ;
2804+ const v6Envelope = JSON . parse ( v6Result . stdout ) as { ok : boolean ; data : { last_ip ?: string } } ;
2805+ assert . equal ( v6Envelope . data . last_ip , DOCUMENTATION_IPV6 ) ;
2806+ await rm ( configDir , { recursive : true , force : true } ) ;
2807+ } ) ;
2808+
2809+ test ( "screen show still works when last_online_at and last_ip are absent" , async ( ) => {
2810+ const transport = new FakeTransport ( ) ;
2811+ const screen = {
2812+ content_access_generation : 1 ,
2813+ created_at : "2026-08-14T17:00:00.000Z" ,
2814+ id : "scr_PAIRINGAAAAAAAAAAAAAAAA" ,
2815+ label : "Lobby" ,
2816+ manifest_revision : 1 ,
2817+ online : false ,
2818+ public_id : "scr_public_pairing" ,
2819+ revision : 1 ,
2820+ state : "pairing_pending" as const ,
2821+ updated_at : "2026-08-14T17:00:00.000Z" ,
2822+ } ;
2823+ transport . on ( "GET" , "/api/v1/screens/scr_PAIRINGAAAAAAAAAAAAAAAA" , ( ) => ( {
2824+ status : 200 ,
2825+ headers : { "x-request-id" : "req_show_offline" } ,
2826+ body : screen ,
2827+ } ) ) ;
2828+ const configDir = await testTemp ( "screen-show-offline-" ) ;
2829+ const fsLike = { mkdir, open, rename, rm, chmod, stat, homedir : ( ) => configDir , env : { XDG_CONFIG_HOME : configDir } } ;
2830+ await writeConfigAtomic (
2831+ path . join ( configDir , "screenrig" , "config.json" ) ,
2832+ { api_url : "https://api.screenrig.ai" , token : "sr_live_existing_secret" } ,
2833+ fsLike ,
2834+ ) ;
2835+
2836+ const result = await withRuntime (
2837+ [ "--json" , "screen" , "show" , "scr_PAIRINGAAAAAAAAAAAAAAAA" ] ,
2838+ transport ,
2839+ { fs : fsLike } ,
2840+ ) ;
2841+ assert . equal ( result . code , ExitCode . Success , result . stdout ) ;
2842+ const envelope = JSON . parse ( result . stdout ) as {
2843+ ok : boolean ;
2844+ data : { id : string ; online ?: boolean ; last_online_at ?: string ; last_ip ?: string } ;
2845+ } ;
2846+ assert . equal ( envelope . ok , true ) ;
2847+ assert . equal ( envelope . data . id , "scr_PAIRINGAAAAAAAAAAAAAAAA" ) ;
2848+ assert . equal ( envelope . data . online , false ) ;
2849+ assert . equal ( envelope . data . last_online_at , undefined ) ;
2850+ assert . equal ( envelope . data . last_ip , undefined ) ;
2851+ assert . equal ( JSON . stringify ( envelope . data ) . includes ( "last_online_at" ) , false ) ;
2852+ assert . equal ( JSON . stringify ( envelope . data ) . includes ( "last_ip" ) , false ) ;
2853+ await rm ( configDir , { recursive : true , force : true } ) ;
2854+ } ) ;
2855+
2856+ test ( "screen update cannot send online, last_online_at, or last_ip" , async ( ) => {
2857+ const transport = memoryBackend ( ) ;
2858+ const { code, configDir } = await withRuntime ( [ "--json" , "screen" , "pair" , "ABC234" ] , transport ) ;
2859+ assert . equal ( code , ExitCode . Success ) ;
2860+ const fsLike = { mkdir, open, rename, rm, chmod, stat, homedir : ( ) => configDir , env : { XDG_CONFIG_HOME : configDir } } ;
2861+ transport . calls . length = 0 ;
2862+
2863+ const result = await withRuntime (
2864+ [
2865+ "--json" , "screen" , "update" , "scr_PAIRINGAAAAAAAAAAAAAAAA" ,
2866+ "--name" , "Lobby" ,
2867+ "--if-match" , "1" ,
2868+ "--online" , "true" ,
2869+ "--last-online-at" , "2026-08-19T12:00:00Z" ,
2870+ "--last-ip" , DOCUMENTATION_IPV4 ,
2871+ ] ,
2872+ transport ,
2873+ { fs : fsLike } ,
2874+ ) ;
2875+ assert . equal ( result . code , ExitCode . Success , result . stdout ) ;
2876+ const patch = transport . calls . find ( ( call ) => call . method === "PATCH" ) ;
2877+ assert . equal ( patch ?. path , "/api/v1/screens/scr_PAIRINGAAAAAAAAAAAAAAAA" ) ;
2878+ assert . deepEqual ( patch ?. body , { name : "Lobby" } ) ;
2879+ const patchJson = JSON . stringify ( patch ?. body ) ;
2880+ assert . equal ( patchJson . includes ( "online" ) , false ) ;
2881+ assert . equal ( patchJson . includes ( "last_online_at" ) , false ) ;
2882+ assert . equal ( patchJson . includes ( "last_ip" ) , false ) ;
2883+ await rm ( configDir , { recursive : true , force : true } ) ;
2884+ } ) ;
2885+
26962886test ( "adding a schedule to a playlist an unzoned screen already runs is refused" , async ( ) => {
26972887 const transport = memoryBackend ( ) ;
26982888 const { configDir, fsLike, file } = await scheduledPlaylistFixture ( false , "tz-playlist-update-" ) ;
0 commit comments