2026-10-13 10:33:31 +02:00
'use strict' ;
// This capability stays in the main process. Neither pages nor agents receive CDP access.
const owners = new WeakMap ();
function protectClipboardPrimitives (){
const prototype = Document . prototype , existing = Object . getOwnPropertyDescriptor ( prototype , 'execCommand' );
if ( existing ? . value ? . _managedClipboardGuard === true &&! existing . configurable ) return ;
const original = prototype . execCommand , apply = Reflect . apply , test = RegExp . prototype . test . bind ( /^(copy|cut|paste)/i );
function managedClipboardCommand ( command ,... args ){
if ( typeof command !== 'string' || test ( command )) return false ;
return apply ( original , this ,[ command ,... args ]);
}
Object . defineProperty ( managedClipboardCommand , '_managedClipboardGuard' ,{ value : true });
Object . defineProperty ( prototype , 'execCommand' ,{ value : managedClipboardCommand , writable : false , configurable : false });
const SecurityException = DOMException ;
const denyReplacement = ()=>{ throw new SecurityException ( 'Managed document replacement is disabled' , 'SecurityError' );};
for ( const name of [ 'open' , 'write' , 'writeln' ]) Object . defineProperty ( prototype , name ,{ value : denyReplacement , writable : false , configurable : false });
const deny = event =>{ event . preventDefault (); event . stopImmediatePropagation ();};
for ( const name of [ 'copy' , 'cut' , 'paste' , 'dragstart' , 'dragover' , 'drop' ]) window . addEventListener ( name , deny , true );
const transferred = RegExp . prototype . test . bind ( /^(insertFromPaste|insertFromDrop|deleteByCut)/ );
window . addEventListener ( 'beforeinput' , event =>{ if ( transferred ( event . inputType || '' )) deny ( event );}, true );
}
const source = `( ${ protectClipboardPrimitives . toString () } )();` ;
function deadline ( operation ){ let timer ; return Promise . race ([ operation , new Promise (( _ , reject )=>{ timer = setTimeout (()=> reject ( Error ( 'Page security initialization timed out' )), 5000 );})]). finally (()=> clearTimeout ( timer ));}
async function attachPageClipboardGuard ( contents , onFailure ){
const api = contents . debugger ;
if ( ! api || api . isAttached () || owners . has ( contents )) throw Error ( 'Page security initialization unavailable' );
2026-10-14 00:37:32 +02:00
const owner = { api , healthy : false , onFailure };
2026-10-13 10:33:31 +02:00
const detached = ()=>{ owners . delete ( contents ); owner . healthy = false ; if ( ! contents . isDestroyed ()) onFailure ();};
owner . detached = detached ;
api . on ( 'detach' , detached );
try {
api . attach ( '1.3' );
await deadline ( api . sendCommand ( 'Page.enable' ,{}));
const registration = await deadline ( api . sendCommand ( 'Page.addScriptToEvaluateOnNewDocument' ,{ source }));
if ( typeof registration ? . identifier !== 'string' ||! registration . identifier ||! api . isAttached ()) throw Error ( 'Page security initialization failed' );
owner . healthy = true ; owners . set ( contents , owner );
} catch {
api . removeListener ( 'detach' , detached );
if ( api . isAttached ()) api . detach ();
throw Error ( 'Page security initialization failed' );
}
}
function guardedScreenshot ( contents , parameters ){
const owner = owners . get ( contents );
if ( ! owner ) return null ;
if ( ! owner . healthy || owner . api !== contents . debugger ||! owner . api . isAttached ()) throw Error ( 'Page security connection unavailable' );
// Only the fixed screenshot command is shared. There is no general command dispatcher.
return owner . api . sendCommand ( 'Page.captureScreenshot' , parameters );
}
function disposePageClipboardGuard ( contents ){
const owner = owners . get ( contents ); if ( ! owner ) return ;
2026-10-14 00:37:32 +02:00
owner . releaseTransport ? .();
2026-10-13 10:33:31 +02:00
owners . delete ( contents ); owner . healthy = false ; owner . api . removeListener ( 'detach' , owner . detached );
if ( owner . api . isAttached ()) owner . api . detach ();
}
2026-10-14 00:37:32 +02:00
function createNativeTransportChannel ( contents ){
const owner = owners . get ( contents ), api = owner ? . api ;
if ( ! owner ? . healthy || api !== contents . debugger ||! api . isAttached () || owner . transportLease ) throw Error ( 'Private transport channel unavailable' );
const lease = Symbol ( 'native-transport' ), subscriptions = new Set (), targets = new Set (); let released = false ;
owner . transportLease = lease ;
const healthy = ()=> ! released && owners . get ( contents ) === owner && owner . healthy && owner . transportLease === lease &&! contents . isDestroyed () && api . isAttached ();
const session = value => value === undefined || typeof value === 'string' && value . length > 0 && value . length <= 128 && /^[a-zA-Z0-9._:-]+$/ . test ( value );
const command = ( method , parameters , sessionId )=>{ if ( ! healthy () ||! session ( sessionId )) return Promise . reject ( Error ( 'Private transport channel unavailable' )); return api . sendCommand ( method , parameters , sessionId );};
const subscribe = ( name , listener )=>{ if ( ! healthy () || typeof listener !== 'function' ) throw Error ( 'Private transport subscription unavailable' ); api . on ( name , listener ); let removed = false ; const remove = ()=>{ if ( removed ) return ; removed = true ; api . removeListener ( name , listener ); subscriptions . delete ( remove );}; subscriptions . add ( remove ); return remove ;};
const messages = new Set ([ 'Target.attachedToTarget' , 'Target.detachedFromTarget' , 'Network.webTransportCreated' , 'Network.webTransportConnectionEstablished' , 'Network.webTransportClosed' ]);
function release (){
if ( released ) return ; released = true ; for ( const remove of [... subscriptions ]) remove ();
if ( owner . transportLease === lease ){ owner . transportLease = null ; owner . releaseTransport = null ;}
// Release private observation without detaching the clipboard security owner.
// Fresh workers may still be waiting for startup; resume before dropping targets.
if ( api . isAttached ()){
const send = ( method , parameters , id )=>{ try { return Promise . resolve ( api . sendCommand ( method , parameters , id ));} catch { return Promise . reject ( Error ( 'Private transport cleanup unavailable' ));}};
const pending = []; for ( const id of targets ) pending . push ( send ( 'Runtime.runIfWaitingForDebugger' ,{}, id ));
pending . push ( send ( 'Target.setAutoAttach' ,{ autoAttach : false , waitForDebuggerOnStart : false , flatten : true }));
void Promise . allSettled ( pending ). then ( results =>{ if ( results . some ( result => result . status === 'rejected' ) && owners . get ( contents ) === owner && owner . healthy &&! contents . isDestroyed ()) owner . onFailure ();});
}
targets . clear ();
}
owner . releaseTransport = release ;
return Object . freeze ({ isHealthy : healthy ,
enableNetwork : sessionId => command ( 'Network.enable' ,{ maxPostDataSize : 0 }, sessionId ),
autoAttachTargets : sessionId => command ( 'Target.setAutoAttach' ,{ autoAttach : true , waitForDebuggerOnStart : true , flatten : true }, sessionId ),
resumeTarget : sessionId => sessionId === undefined ? Promise . reject ( Error ( 'Private transport target required' )) : command ( 'Runtime.runIfWaitingForDebugger' ,{}, sessionId ),
subscribeMessage : listener => subscribe ( 'message' ,( event , method , params , sessionId )=>{ if ( ! messages . has ( method )) return ; if ( method === 'Target.attachedToTarget' && session ( params ? . sessionId ) && params . sessionId !== undefined ) targets . add ( params . sessionId ); if ( method === 'Target.detachedFromTarget' ) targets . delete ( params ? . sessionId ); listener ( event , method , params , sessionId );}),
subscribeDetach : listener => subscribe ( 'detach' ,()=> listener ()), release });
}
2026-10-13 21:39:47 +02:00
async function guardedVisualStructure ( contents ){
const owner = owners . get ( contents );
if ( ! owner ? . healthy ||! owner . api . isAttached () || contents . isDestroyed ()) throw Error ( 'Managed visual export refused' );
// Native DOM data detects closed/declarative roots that isolated DOM queries
// cannot see. It is consumed privately and never logged or returned to agents.
const result = await deadline ( owner . api . sendCommand ( 'DOM.getDocument' ,{ depth :- 1 , pierce : true }));
if ( owners . get ( contents ) !== owner ||! owner . healthy ||! owner . api . isAttached ()) throw Error ( 'Managed visual export refused' );
const queue = [ result ? . root ]; let nodes = 0 , characters = 0 ;
while ( queue . length ){
const node = queue . pop (); if ( ! node ||++ nodes > 20000 ) throw Error ( 'Managed visual export refused' );
for ( const text of [ node . nodeName , node . nodeValue ,...( node . attributes || [])]){ if ( typeof text !== 'string' || ( characters += text . length ) > 1024 * 1024 ) throw Error ( 'Managed visual export refused' );}
const shadows = node . shadowRoots || [], children = node . children || [];
if ( ! Array . isArray ( shadows ) ||! Array . isArray ( children ) || nodes + queue . length + shadows . length + children . length > 20000 ) throw Error ( 'Managed visual export refused' );
for ( const root of shadows ){ if ( root . shadowRootType === 'closed' ) throw Error ( 'Managed visual export refused' ); queue . push ( root );}
if ( node . contentDocument ) throw Error ( 'Managed visual export refused' );
queue . push (... children );
}
return { safe : true , nodes };
}
2026-10-14 00:37:32 +02:00
module . exports = { attachPageClipboardGuard , guardedScreenshot , guardedVisualStructure , disposePageClipboardGuard , createNativeTransportChannel };