2026-10-13 10:33:31 +02:00
'use strict' ;
// Runs only in the isolated main-frame world. No arbitrary selectors or scripts from callers.
2026-10-13 21:00:41 +02:00
function fillDocument ( expectedUrl , origin , username , password , expiresAt , approvedForms ){
2026-10-13 10:33:31 +02:00
if ( Date . now () >= expiresAt || window . top !== window ||! window . isSecureContext || location . href !== expectedUrl || location . origin !== origin ) throw Error ( 'Login destination changed' );
const visible = element =>{
2026-10-13 21:00:41 +02:00
if ( element . disabled || element . readOnly || element . closest ( '[hidden],[inert],[aria-hidden="true"],[aria-disabled="true"],fieldset[disabled]' ) || element . getClientRects (). length === 0 ) return false ;
for ( let node = element ; node ; node = node . parentElement ){ const style = getComputedStyle ( node ); if ( style . display === 'none' || style . visibility === 'hidden' || Number ( style . opacity ) === 0 ) return false ;} return true ;
2026-10-13 10:33:31 +02:00
};
2026-10-13 21:00:41 +02:00
const passwords = [... document . querySelectorAll ( 'input[type="password"]' )]. filter ( element => visible ( element ) &&! String ( element . autocomplete || '' ). toLowerCase (). split ( /\s+/ ). includes ( 'new-password' ));
2026-10-13 10:33:31 +02:00
if ( passwords . length !== 1 ) throw Error ( 'A single visible login password field is required' );
const target = passwords [ 0 ], form = target . form ;
if ( ! form || form . method . toLowerCase () !== 'post' ||! [ '' , '_self' ]. includes ( form . target . toLowerCase ())) throw Error ( 'A same-window POST login form is required' );
const action = new URL ( form . action || location . href , location . href );
if ( action . protocol !== 'https:' || action . origin !== origin || action . username || action . password ) throw Error ( 'Login form sends to a different destination' );
for ( const control of [...( form . elements || form . querySelectorAll ( 'button,input' ))]){
if ( control . disabled || typeof control . getAttribute !== 'function' ) continue ;
const actionOverride = control . getAttribute ( 'formaction' ), methodOverride = control . getAttribute ( 'formmethod' ), targetOverride = control . getAttribute ( 'formtarget' );
if ( actionOverride !== null ){ const destination = new URL ( actionOverride || location . href , location . href ); if ( destination . protocol !== 'https:' || destination . origin !== origin || destination . username || destination . password ) throw Error ( 'Login submit destination differs' );}
if ( methodOverride !== null && methodOverride . toLowerCase () !== 'post' ) throw Error ( 'Login submit method differs' );
if ( targetOverride !== null &&! [ '' , '_self' ]. includes ( targetOverride . toLowerCase ())) throw Error ( 'Login submit window differs' );
}
const inputs = [... form . querySelectorAll ( 'input' )]. filter ( element => visible ( element ) && [ 'text' , 'email' ]. includes ( element . type ));
const named = inputs . filter ( element => element . autocomplete === 'username' || /^(?:username|user|email|login)$/i . test ( element . name || element . id ));
const candidates = named . length ? named : inputs ;
if ( username && candidates . length !== 1 ) throw Error ( 'A single login username field is required' );
const user = username ? candidates [ 0 ] : null ;
if ( user && user . form !== form ) throw Error ( 'Login fields belong to different forms' );
2026-10-13 21:00:41 +02:00
let approved ;
if ( approvedForms !== undefined ){
if ( ! Array . isArray ( approvedForms ) || approvedForms . length > 32 ||! user || form . enctype . toLowerCase () !== 'application/x-www-form-urlencoded' ) throw Error ( 'Controlled login form is unsupported' );
const matches = approvedForms . filter ( rule => rule . actionUrl === action . href && rule . usernameField === user . name && rule . passwordField === target . name );
if ( matches . length !== 1 || [...( form . elements || [])]. filter ( element => element . name === user . name ). length !== 1 || [...( form . elements || [])]. filter ( element => element . name === target . name ). length !== 1 ) throw Error ( 'Login form is not explicitly approved' );
approved = matches [ 0 ];
for ( const control of [...( form . elements || [])]){ if ( control . disabled || typeof control . getAttribute !== 'function' ) continue ; const destination = control . getAttribute ( 'formaction' ), encoding = control . getAttribute ( 'formenctype' ); if ( destination !== null && new URL ( destination || location . href , location . href ). href !== action . href || encoding !== null && encoding . toLowerCase () !== 'application/x-www-form-urlencoded' ) throw Error ( 'Controlled login submit differs' );}
}
2026-10-13 10:33:31 +02:00
const setter = Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ). set ;
// Recheck inside the target renderer, after queued native dispatch. A hidden
// or blurred document must not receive credentials after a tab switch.
if ( document . visibilityState !== 'visible' ||! document . hasFocus ()) throw Error ( 'Login tab is no longer foreground' );
// Set both values before dispatching events: page handlers cannot move focus between writes.
if ( user ) setter . call ( user , username ); setter . call ( target , password );
if ( user ) user . dispatchEvent ( new Event ( 'input' ,{ bubbles : true }));
target . dispatchEvent ( new Event ( 'input' ,{ bubbles : true }));
2026-10-13 21:00:41 +02:00
return { completed : true ,...( approved ? { formId : approved . id } : {})};
2026-10-13 10:33:31 +02:00
}
function fillScript ( expectedUrl , credential , expiresAt = Date . now () + 10000 ){
const url = new URL ( expectedUrl );
if ( url . protocol !== 'https:' || url . username || url . password || url . origin !== credential . origin ) throw Error ( 'Saved login requires its exact HTTPS origin' );
if ( ! Number . isFinite ( expiresAt ) || expiresAt <= Date . now ()) throw Error ( 'Password policy expired' );
return `( ${ fillDocument . toString () } )( ${ JSON . stringify ( url . href ) } , ${ JSON . stringify ( credential . origin ) } , ${ JSON . stringify ( credential . username ) } , ${ JSON . stringify ( credential . password ) } , ${ JSON . stringify ( expiresAt ) } )` ;
}
module . exports = { fillDocument , fillScript };
2026-10-13 21:00:41 +02:00
function prepareFillScript ( expectedUrl , credential , forms , expiresAt = Date . now () + 10000 ){
const url = new URL ( expectedUrl ); if ( url . protocol !== 'https:' || url . username || url . password || url . origin !== credential . origin ||! Array . isArray ( forms ) ||! Number . isFinite ( expiresAt ) || expiresAt <= Date . now ()) throw Error ( 'Approved controlled login required' );
const rules = forms . filter ( row => new URL ( row . actionUrl ). origin === credential . origin ). map (({ id , actionUrl , usernameField , passwordField })=>({ id , actionUrl , usernameField , passwordField }));
if ( ! rules . length ) throw Error ( 'No approved login endpoint' );
return `( ${ fillDocument . toString () } )( ${ JSON . stringify ( url . href ) } , ${ JSON . stringify ( credential . origin ) } , ${ JSON . stringify ( credential . username ) } , ${ JSON . stringify ( credential . password ) } , ${ JSON . stringify ( expiresAt ) } , ${ JSON . stringify ( rules ) } )` ;
}
module . exports . prepareFillScript = prepareFillScript ;