'use strict'; // Runs only in the privileged isolated world; no preload IPC or page-world bridge. function humanInteractionDOM(mode,url,token){ const key='__safetynetHumanInteraction'; const previous=globalThis[key]; if(mode==='dispose'){if(previous?.token===token){previous.dispose();delete globalThis[key];}return null;} if(window.top!==window||location.href!==url)return null; if(mode==='drain'){ if(!previous||previous.token!==token)return null; previous.until=Date.now()+1500;const events=previous.queue.splice(0,16);return {events,dropped:previous.dropped}; } if(mode!=='install')return null; if(previous){previous.dispose();delete globalThis[key];} const state={token,queue:[],dropped:0,until:Date.now()+1500,tokens:20,last:Date.now(),gesture:null}; const sensitive=e=>{ let steps=0; for(let p=e;p;p=p.parentElement){ if(++steps>64)return true; if(p.hasAttribute?.('data-sensitive')||/password|passwd|secret|token|credential|api.?key|auth|one.?time|otp|master|credit.?card|cc-number|cc-csc/i.test(['name','id','autocomplete','aria-label'].map(a=>p.getAttribute?.(a)||'').join(' '))||/^(password|file|hidden)$/i.test(p.type||''))return true; } return false; }; const formSensitive=e=>{ const form=e?.tagName==='FORM'?e:e?.form||e?.closest?.('form'); if(!form)return false; const controls=form.elements;if(!controls||controls.length>128)return true; for(let i=0;i{ const now=Date.now();if(event.isTrusted!==true||now>state.until||location.href!==url||event.type==='submit'&&(navigator.userActivation?.isActive!==true||state.gesture===null||now-state.gesture>1000))return; const target=event.target?.nodeType===1?event.target:event.target?.parentElement; if(!target?.isConnected||sensitive(target)||formSensitive(target))return; if(event.type==='click')state.gesture=now; state.tokens=Math.min(20,state.tokens+Math.max(0,now-state.last)*0.01);state.last=now; if(state.tokens<1||state.queue.length>=16){state.dropped=Math.min(1000000,state.dropped+1);return;} state.tokens--; const tag=['A','BUTTON','INPUT','TEXTAREA','SELECT','DIV','SPAN','P','BODY'].includes(target.tagName)?target.tagName:'OTHER'; const facts={tag};const role=target.getAttribute?.('role'); if(['link','button','checkbox','radio','textbox','combobox','text','other','menuitem','option'].includes(role))facts.role=role; if(event.type==='click'&&Number.isFinite(event.clientX)&&Number.isFinite(event.clientY)&&event.clientX>=0&&event.clientY>=0&&event.clientX<=innerWidth&&event.clientY<=innerHeight){facts.x=Math.round(event.clientX);facts.y=Math.round(event.clientY);} state.queue.push({action:event.type,outcome:'observed',sensitiveField:false,target:facts}); }; const enter=event=>{if(event.isTrusted===true&&event.key==='Enter'&&Date.now()<=state.until&&location.href===url&&!sensitive(event.target)&&!formSensitive(event.target))state.gesture=Date.now();}; window.addEventListener('click',record,true);window.addEventListener('submit',record,true);window.addEventListener('keydown',enter,true); state.dispose=()=>{window.removeEventListener('click',record,true);window.removeEventListener('submit',record,true);window.removeEventListener('keydown',enter,true);state.queue.length=0;}; globalThis[key]=state;return {installed:true}; } function humanInteractionScript(mode,{url,token}){ if(!['install','drain','dispose'].includes(mode)||typeof url!=='string'||url.length>4096||typeof token!=='string'||!/^[a-f0-9]{32,64}$/.test(token))throw TypeError('Invalid interaction observer context'); return `(${humanInteractionDOM.toString()})(${JSON.stringify(mode)},${JSON.stringify(url)},${JSON.stringify(token)})`; } module.exports={humanInteractionScript}; function createHumanInteractionObserver({getContext,isCurrent,executeScript,onObserved,onDropped=()=>{},interval=250,deadline=1000}){ if([getContext,isCurrent,executeScript,onObserved,onDropped].some(fn=>typeof fn!=='function'))throw TypeError('Invalid interaction observer callbacks'); if(!Number.isSafeInteger(interval)||interval<100||interval>500||!Number.isSafeInteger(deadline)||deadline<100||deadline>1000)throw TypeError('Invalid interaction observer timing'); const {randomBytes}=require('node:crypto');let current=null,retired=null,busy=false,closed=false,epoch=0; const eligible=c=>!!c&&c.ready===true&&c.enabled===true&&c.foreground===true&&typeof c.sessionId==='string'&&typeof c.url==='string'&&typeof c.token==='string'&&Number.isSafeInteger(c.policyVersion)&&c.webContents&&!c.webContents.isDestroyed?.()&&/^https?:\/\//.test(c.url)&&isCurrent(c); const same=(a,b)=>a&&b&&a.sessionId===b.sessionId&&a.url===b.url&&a.token===b.token&&a.policyVersion===b.policyVersion&&a.webContents===b.webContents; const validBatch=result=>result&&typeof result==='object'&&!Array.isArray(result)&&Object.keys(result).every(k=>['events','dropped'].includes(k))&&Array.isArray(result.events)&&result.events.length<=16&&Number.isSafeInteger(result.dropped)&&result.dropped>=0&&result.dropped<=1000000; async function tick(){ if(closed||busy)return;busy=true;const generation=epoch;let timedOut=false;const timer=setTimeout(()=>{timedOut=true;epoch++;current=null;},deadline);timer.unref?.(); try{ if(retired){const previous=retired;retired=null;await executeScript(previous.context.webContents,humanInteractionScript('dispose',{url:previous.context.url,token:previous.secret}),'dispose');if(closed||timedOut||generation!==epoch)return;} const context=getContext(); if(!eligible(context)){const previous=current;current=null;if(previous)await executeScript(previous.context.webContents,humanInteractionScript('dispose',{url:previous.context.url,token:previous.secret}),'dispose');return;} if(!same(current?.context,context)){ const previous=current;current=null;if(previous)await executeScript(previous.context.webContents,humanInteractionScript('dispose',{url:previous.context.url,token:previous.secret}),'dispose'); if(closed||timedOut||generation!==epoch||!eligible(context))return; const secret=randomBytes(24).toString('hex');const result=await executeScript(context.webContents,humanInteractionScript('install',{url:context.url,token:secret}),'install'); if(!closed&&!timedOut&&generation===epoch&&eligible(context)&&result?.installed===true)current={context,secret,dropped:0}; else if(result?.installed===true)await executeScript(context.webContents,humanInteractionScript('dispose',{url:context.url,token:secret}),'dispose');return; } const selected=current;const result=await executeScript(context.webContents,humanInteractionScript('drain',{url:context.url,token:selected.secret}),'drain'); if(closed||timedOut||generation!==epoch||current!==selected||!eligible(context)||!validBatch(result))return; if(result.dropped>selected.dropped){await onDropped(result.dropped-selected.dropped,context);selected.dropped=result.dropped;} for(const event of result.events){ if(closed||timedOut||generation!==epoch||current!==selected||!eligible(context))return; // Never trust arbitrary renderer keys; the root validates the shared evidence schema too. if(!event||typeof event!=='object'||Array.isArray(event)||Object.keys(event).some(k=>!['action','outcome','sensitiveField','target'].includes(k))||!['click','submit'].includes(event.action)||event.outcome!=='observed'||event.sensitiveField!==false)continue; await onObserved(event,context); } }catch{current=null;}finally{clearTimeout(timer);busy=false;} } const timer=setInterval(()=>{void tick();},interval);timer.unref?.(); function refresh(){epoch++;if(current)retired=current;current=null;void tick();} function dispose(){if(closed)return;closed=true;epoch++;clearInterval(timer);const previous=current||retired;current=null;retired=null;if(previous)Promise.resolve().then(()=>executeScript(previous.context.webContents,humanInteractionScript('dispose',{url:previous.context.url,token:previous.secret}),'dispose')).catch(()=>{});} return {refresh,dispose,poll:tick}; } module.exports.createHumanInteractionObserver=createHumanInteractionObserver;