2026-10-13 10:33:31 +02:00
const { app , BrowserWindow , WebContentsView , ipcMain , session , safeStorage , shell , dialog , powerMonitor , clipboard , Menu } = require ( 'electron' );
const { assertSecureStartup } = require ( './startup-security.cjs' );
const { assertPolicyAssignment } = require ( './policy-assignment.cjs' );
assertSecureStartup ( app . commandLine );
// Force Chromium sandboxing globally before any renderer is created. The subframe
// preload option below never grants a Node engine: native proof tests verify this.
app . enableSandbox ();
const fs = require ( 'node:fs' );
const path = require ( 'node:path' );
const os = require ( 'node:os' );
const { pathToFileURL } = require ( 'node:url' );
const { createDiagnostics } = require ( './diagnostics.cjs' );
const diagnostics = createDiagnostics ();
function diagnosticContext ( extra = {}){ return { policyVersion : policy ? . version || 0 ,...( device ? . id ? { deviceId : device . id } : {}),... extra };}
function diagnosticSpan ( name , attributes , operation ){
// Instrumentation never replaces, skips or retries a security operation.
let started = false ; const run = ()=>{ started = true ; return Promise . resolve ( operation ()). then ( result =>{ if ([ 'allow' , 'block' , 'deny' , 'unknown' ]. includes ( result ? . decision )){ try { diagnostics . event ( name , diagnosticContext ({... attributes , decision : result . decision }));} catch {}} return result ;});};
try { return diagnostics . span ( name , diagnosticContext ( attributes ), run );} catch ( error ){ if ( started ) throw error ; return run ();}
}
let diagnosticsConfiguration = '' ;
function configureDiagnostics ( config ){ const value = config || { enabled : false }, context = diagnosticContext (), key = JSON . stringify ([ value , context ]); if ( key === diagnosticsConfiguration ) return ; try { diagnostics . configure ( value , context ); diagnosticsConfiguration = key ;} catch { diagnosticsConfiguration = '' ; try { diagnostics . configure ({ enabled : false });} catch {}}}
function diagnosticCorrelation (){ try { const ids = diagnostics . correlation ? .(); return /^[a-f0-9]{32}$/ . test ( ids ? . traceId || '' ) && /[1-9a-f]/ . test ( ids . traceId ) && /^[a-f0-9]{16}$/ . test ( ids ? . spanId || '' ) && /[1-9a-f]/ . test ( ids . spanId ) ? { traceId : ids . traceId , spanId : ids . spanId } : {};} catch { return {};}}
2026-10-13 16:58:15 +02:00
let win , view , browserSession , managementSession , core , device , policy , ready = false , syncing = false , aiPanel = false , vaultPanel = false , vault , vaultTimer , fileUploadGate , uploadLeaseFatal = false ;
2026-10-13 10:33:31 +02:00
let chromeHeight = 104 , leftSidebar = false , startPage = false , overviewPanel = false , toolsOpen = false , pageTitle = '' , pageColor = '#edf1f6' ;
const { createTabRegistry } = require ( './tab-registry.cjs' );
let tabs , tabsRevision = 0 ;
function activeTab (){ return tabs ? . active ();}
function tabContext ( id ,{ foreground = true } = {}){ const tab = id ? tabs . get ( id ) : activeTab (); if ( ! tab ) throw Error ( 'No native browser tab' ); return tabs . context ( tab . id ,{ foreground });}
function currentTabContext ( token ){ return ready && token ? . policyVersion === policy ? . version && tabs . current ( token ) && Date . parse ( policy ? . expiresAt ) > Date . now ();}
function selectTab ( id ){ const target = tabs . get ( id ); if ( ! target . guarded ) throw Error ( 'Native tab security is still initializing' ); const changed = target !== activeTab (); const tab = tabs . activate ( id ); if ( changed ){ win . webContents . focus (); vault ? . lock (); vaultPanel = false ;} view = tab . view ; startPage = tab . startPage ; pageTitle = tab . title ; pageColor = tab . color ; layout (); return tab ;}
function blockedTab (){ return tabs ? . all (). find ( tab => tab . security . visualBlocked || tab . security . contentBlocked );}
function stickyBlock (){ const tab = blockedTab (); return tab && ( tab . security . visualBlocked || tab . security . contentBlocked );}
const { startInspector } = require ( './inspection.cjs' );
const { icapRequired , scanEnterprise , scanDownloadEnterprise } = require ( './enterprise-inspection.cjs' );
const { createDownloadGate } = require ( './download-gate.cjs' );
const { resolverOptions , createNetworkPosture } = require ( './network-posture.cjs' );
const networkPosture = createNetworkPosture (); let networkTimer , networkChecking = false ;
const { startAgentControl } = require ( './agent-control.cjs' );
const { createAgentAdapters } = require ( './agent-adapters.cjs' );
const { createVault } = require ( './vault.cjs' );
const { createDeveloperToolsGuard , developerAssetRequestAllowed } = require ( './developer-tools.cjs' );
const { settingsState , sectionVisible , sectionAllowed , featureAllowed , requireFeature , requireSection , validateAppearance } = require ( './managed-settings.cjs' );
const { bookmarkState , validateMutation : validateBookmarkMutation , libraryVisible } = require ( './bookmarks.cjs' );
2026-10-13 16:58:15 +02:00
let bookmarkBusy = false , personalBookmarks = { revision : 0 , items : []}, bookmarkSnapshotJSON = '' , libraryOrganizer ;
2026-10-13 10:33:31 +02:00
function effectiveBookmarkPolicy (){ return policy ? {... policy , browser : {... policy . browser , bookmarks : {... policy . browser . bookmarks , personal : personalBookmarks }}} : null ;}
let appearancePreferences = {};
function managedSettings (){ return settingsState ({ policy , ready , preferences : appearancePreferences });}
function requireManagedSection ( section ){ requireSection ( managedSettings (), section );}
function requireFeatureSection ( section ){ requireFeature ( managedSettings (), section );}
function loadAppearance (){ try { appearancePreferences = validateAppearance ( JSON . parse ( fs . readFileSync ( path . join ( app . getPath ( 'userData' ), 'appearance.json' ), 'utf8' )));} catch { appearancePreferences = {};}}
function persistAppearance (){ fs . writeFileSync ( path . join ( app . getPath ( 'userData' ), 'appearance.json' ), JSON . stringify ( appearancePreferences ),{ mode : 0o600 });}
const { fillScript } = require ( './password-fill.cjs' );
const { createVisualPhishing , describeScript } = require ( './visual-phishing.cjs' );
const { fingerprintPNG } = require ( '../../packages/core/visual-image.cjs' );
const { hammingDistance } = require ( '../../packages/core/visual-hash.cjs' );
const { capturePNG } = require ( './visual-capture.cjs' );
const { createClipboardGuard } = require ( './clipboard-guard.cjs' );
const { clipboardScript } = require ( './clipboard-dom.cjs' );
const { attachPageClipboardGuard , disposePageClipboardGuard } = require ( './page-clipboard-guard.cjs' );
let clipboardGuard , clipboardCore , clipboardFatal = false ;
function installClipboardControls ( contents ){
contents . on ( 'before-input-event' ,( event , input )=>{
if ( input . type !== 'keyDown' ) return ;
const key = input . key . toLowerCase (), modifier = ( input . control || input . meta ) &&! input . alt ;
const operation = modifier && key === 'c' || input . control && key === 'insert' ? 'copy' : modifier && key === 'x' || input . shift && key === 'delete' ? 'cut' : modifier && key === 'v' || input . shift && key === 'insert' ? 'paste' : null ;
if ( operation ){ event . preventDefault (); diagnosticSpan ( 'browser.clipboard' ,{ operation },()=> clipboardGuard ? . perform ( operation , contents )). catch (()=>{});}
});
contents . on ( 'context-menu' ,( _event , params )=>{
const actions = params . isEditable ? [ 'copy' , 'cut' , 'paste' ] : params . selectionText ? [ 'copy' ] : [];
if ( ! actions . length ) return ;
Menu . buildFromTemplate ( actions . map ( operation =>({ label : operation [ 0 ]. toUpperCase () + operation . slice ( 1 ), click : ()=> diagnosticSpan ( 'browser.clipboard' ,{ operation },()=> clipboardGuard ? . perform ( operation , contents )). catch (()=>{})}))). popup ({ window : win });
});
}
let managementHealthy = false ;
function visualDeadline ( operation ){ let timeout ; return Promise . race ([ operation , new Promise (( _ , reject )=>{ timeout = setTimeout (()=> reject ( Error ( 'Visual check timed out' )), 5000 );})]). finally (()=> clearTimeout ( timeout ));}
function checkVisual (... args ){ return diagnosticSpan ( 'browser.visual' , args [ 0 ] ? . id ? { sessionId : args [ 0 ]. id } : {},()=> checkVisualImpl (... args ));}
async function checkVisualImpl ( tab = activeTab ()){
if ( ! tab || tab . view . webContents . isDestroyed ()) return { decision : 'unknown' };
if ( ! policy ? . security . visualPhishing . enabled ) return { decision : 'allow' };
const security = tab . security ;
if ( ! security . visualPending ){ const operation = security . visualScanner . check ({ force : true }); security . visualPending = operation ; operation . finally (()=>{ if ( security . visualPending === operation ) security . visualPending = null ;}). catch (()=>{});}
const generation = tab . navigationId , version = policy . version , url = tab . view . webContents . getURL (), result = await security . visualPending ;
if ( ready && generation === tab . navigationId && version === policy . version &&! tab . view . webContents . isDestroyed () && url === tab . view . webContents . getURL () && result . decision === 'allow' ) security . visualApproval = { generation , version , url };
if ( ! tab . view . webContents . isDestroyed () && generation === tab . navigationId && version === policy . version && url === tab . view . webContents . getURL ()){ if ( ready ){ if ( result . decision === 'unknown' ) message = 'Login protection could not approve this view. Credential actions are held.' ; emit ();} return result ;}
return { decision : 'unknown' , reason : 'stale-context' };
}
async function configureVisualPage ( enabled , tab = activeTab ()){
if ( ! tab ) return ; tabs . navigation ( tab . id ); const contents = tab . view . webContents ;
if ( contents . isDestroyed () ||! /^https?:\/\// . test ( contents . getURL ())) return ;
const token = require ( 'node:crypto' ). randomUUID ();
await new Promise (( resolve , reject )=>{
const handler = ( event , value )=>{ if ( event . sender === contents && event . senderFrame === contents . mainFrame && value === token ){ clearTimeout ( timeout ); ipcMain . removeListener ( 'managed:visual-configured' , handler ); resolve ();}};
const timeout = setTimeout (()=>{ ipcMain . removeListener ( 'managed:visual-configured' , handler ); enabled ? reject ( Error ( 'Visual protection could not initialize' )) : resolve ();}, 2000 );
ipcMain . on ( 'managed:visual-configured' , handler ); contents . send ( 'managed:visual-config' ,{ enabled , token });
});
}
2026-10-13 16:58:15 +02:00
let companyNews = [], companyNewsKey = '' , companyNewsFetchedAt = 0 ;
2026-10-13 10:33:31 +02:00
let message = 'Enroll this browser to receive company policy.' , syncTimer , localInspector , auditQueue = [], flushing = false , agentService , agentStarting = false , securityCore ;
const recentRequests = new Map ();
const agentFile = ()=> path . join ( app . getPath ( 'userData' ), 'agent-connection.json' );
2026-10-13 16:58:15 +02:00
const agentGrants = require ( './agent-grants.cjs' );
const agentActorStorage = new ( require ( 'node:async_hooks' ). AsyncLocalStorage )();
let agentServiceMode ;
function effectiveAgentPolicy (){ if ( ! policy ) return policy ; const { grants } = agentGrants . reconcileGenerationFloor ( policy . agents . grants || [], device ? . agentGenerationFloor || {}); return {... policy , agents : {... policy . agents , grants }};}
function approvedAgentGrants (){ return agentGrants . activeGrants ( effectiveAgentPolicy ());}
function actorGrantAllowed ( actor , action , url ){ return agentGrants . authorizeGrant ({ policy : effectiveAgentPolicy (), ready : ready && featureAllowed ( managedSettings (), 'agents' ), principalId : actor ? . principalId , generation : actor ? . generation , action , url , hostMatches : core . hostMatches }). allowed ;}
function agentRequestOwnerAllowed ( source ){ return policy ? . agents ? . identityMode !== 'individual' ||!! source ? . guarded ;}
function actorDestinationAllowed ( url , actor = agentActorStorage . getStore ()){ if ( ! actor ? . action ) return core . destinationAllowed ( url , policy ); return core . destinationAllowed ( url , policy ) && actorGrantAllowed ( actor , actor . action , url );}
async function reconcileAgentPolicy (){
const result = agentGrants . reconcileGenerationFloor ( policy . agents . grants || [], device . agentGenerationFloor || {});
if ( JSON . stringify ( result . floor ) !== JSON . stringify ( device . agentGenerationFloor || {})){ device . agentGenerationFloor = result . floor ; saveDevice ( device );}
const mode = policy . agents . identityMode ;
if ( agentService && ( agentServiceMode !== mode ||! policy . agents . enabled || policy . browser . settings . hiddenSections . includes ( 'agents' ) || policy . browser . settings . lockedSections . includes ( 'agents' ))){ agentService . revoke (); await agentService . close (); agentService = null ; agentServiceMode = undefined ;}
if ( mode === 'individual' ){ try { fs . unlinkSync ( agentFile ());} catch ( error ){ if ( error . code !== 'ENOENT' ) throw error ;} agentService ? . reconcilePrincipals ( result . grants );}
}
async function pairIndividualAgent ( principalId ){
if ( typeof principalId !== 'string' ) throw Error ( 'Select an approved agent' );
const grant = approvedAgentGrants (). find ( g => g . id === principalId ), initial = policy , service = agentService ;
if ( ! grant ||! service || agentServiceMode !== 'individual' ) throw Error ( 'Agent grant unavailable' );
const connection = service . issuePrincipal ( grant . id , grant . generation , grant . expiresAt );
const credentialPath = await agentGrants . persistConnection ({ filePath : agentGrants . descriptorPath ( app . getPath ( 'userData' ), grant . id ), connection , protect : value => require ( './dpapi.cjs' ). capabilityCrypto ( 'protect' , value ), isCurrent : ()=> ready && policy === initial && agentService === service && featureAllowed ( managedSettings (), 'agents' ) && actorGrantAllowed ({ principalId : grant . id , generation : grant . generation }, grant . scopes [ 0 ])});
return { address : service . address , credentialPath , principalId : grant . id , generation : grant . generation };
}
2026-10-13 10:33:31 +02:00
app . commandLine . appendSwitch ( 'disable-http-cache' );
const uiUrl = pathToFileURL ( path . join ( __dirname , 'index.html' )). href ;
const configPath = ()=> path . join ( app . getPath ( 'userData' ), 'enrollment.json' );
const cliRequest = app . commandLine . getSwitchValue ( 'agent-action' ), cliConnection = app . commandLine . getSwitchValue ( 'agent-connection' );
if ( cliRequest ){ try { require ( './agent-client.cjs' ). configureClient ( cliConnection );} catch { app . exit ( 1 );}}
const ownsBrowserInstance =!! cliRequest || app . requestSingleInstanceLock ();
if ( ! ownsBrowserInstance ) app . quit ();
else if ( ! cliRequest ) app . on ( 'second-instance' ,()=>{ if ( win &&! win . isDestroyed ()){ if ( win . isMinimized ()) win . restore (); win . focus ();}});
function requireEncryption (){
if ( ! safeStorage . isEncryptionAvailable () || ( process . platform === 'linux' && safeStorage . getSelectedStorageBackend () === 'basic_text' )) throw Error ( 'An OS credential store is required. Configure Keychain, Windows DPAPI, or Linux Secret Service.' );
}
function saveDevice ( value ){ requireEncryption (); fs . writeFileSync ( configPath (), JSON . stringify ({ encryptedEnrollment : safeStorage . encryptString ( JSON . stringify ( value )). toString ( 'base64' )}),{ mode : 0o600 });}
function loadDevice (){
if ( ! fs . existsSync ( configPath ())) return ;
requireEncryption (); const d = JSON . parse ( fs . readFileSync ( configPath (), 'utf8' )); return JSON . parse ( safeStorage . decryptString ( Buffer . from ( d . encryptedEnrollment , 'base64' )));
}
function managementUrl ( value ){
const u = new URL ( value );
if ( u . username || u . password || u . search || u . hash || u . pathname !== '/' ) throw Error ( 'Enter a management server origin without path or credentials' );
if ( u . protocol !== 'https:' && ! ( u . protocol === 'http:' && [ 'localhost' , '127.0.0.1' , '[::1]' ]. includes ( u . hostname ))) throw Error ( 'Management requires HTTPS; local development may use loopback HTTP.' );
if ( process . env . SAFETYNET_MANAGEMENT_URL && new URL ( process . env . SAFETYNET_MANAGEMENT_URL ). origin !== u . origin ) throw Error ( 'Enrollment must use the administrator-deployed management server' );
return u . origin ;
}
const managementRequests = new Map ();
2026-10-13 18:04:02 +02:00
const evidenceRequestTypes = new Map (), evidenceSendStorage = new ( require ( 'node:async_hooks' ). AsyncLocalStorage )();
2026-10-13 10:33:31 +02:00
function request (... args ){ return diagnosticSpan ( 'browser.management' ,{ operation : args [ 1 ] === '/api/device/ai' ? 'ai' : args [ 1 ] === '/api/device/enroll' ? 'enroll' : 'sync' },()=> requestImpl (... args ));}
async function requestImpl ( server , route , body , credential ){
2026-10-13 16:58:15 +02:00
if ( route === '/api/device/evidence' ){
if ( ! device || device . server !== server || device . token !== credential ) throw Error ( 'Protected evidence identity unavailable' );
2026-10-13 18:04:02 +02:00
const identity = device , type = body ? . type ; if ( ! [ 'download' , 'interaction' ]. includes ( type )) throw Error ( 'Invalid protected evidence type' );
const assertCurrent = ()=>{ if ( device !== identity ||! require ( './evidence-delivery.cjs' ). policyAllows ({ policy }, type )) throw Object . assign ( Error ( 'Protected evidence collection disabled' ),{ status : 403 }); if ( ! ready ) throw Error ( 'Evidence delivery paused until managed context is ready' );};
assertCurrent (); return evidenceSendStorage . run ({ type },()=> require ( './management-channel.cjs' ). protectedEvidenceRequest ({ device , request , save : saveDevice , payload : body , assertCurrent , traceOperation : ( operation , fn )=> diagnosticSpan ( 'browser.evidence' ,{ operation }, fn )}));
2026-10-13 16:58:15 +02:00
}
2026-10-13 10:33:31 +02:00
if ( route === '/api/device/policy' ){
if ( ! device || device . server !== server || device . token !== credential ) throw Error ( 'Protected management identity unavailable' );
return require ( './management-channel.cjs' ). protectedPolicyRequest ({ device , request , save : saveDevice });
}
const headers = {}; if ( body ) headers [ 'content-type' ] = 'application/json' ; if ( credential ) headers . authorization = `Bearer ${ credential } ` ;
2026-10-13 16:58:15 +02:00
const correlation = diagnosticCorrelation (); if ( correlation . traceId && correlation . spanId ) headers . traceparent = `00- ${ correlation . traceId } - ${ correlation . spanId } -01` ;
2026-10-13 10:33:31 +02:00
if ( route === '/api/device/enroll' || route === '/api/device/sso/poll' ) headers [ 'x-safetynet-pq-bootstrap' ] = '1' ;
2026-10-13 18:04:02 +02:00
const controller = new AbortController (), timer = setTimeout (()=> controller . abort (), route . endsWith ( '/ai' ) || route === '/api/device/library/propose' ? 65000 : 10000 ); managementRequests . set ( controller , route ); if ( evidenceSendStorage . getStore () ? . type && route . startsWith ( '/api/device/channel/evidence' )) evidenceRequestTypes . set ( controller , evidenceSendStorage . getStore (). type );
2026-10-13 10:33:31 +02:00
try {
const fetcher = managementSession ? managementSession . fetch . bind ( managementSession ) : fetch ;
const r = await fetcher ( ` ${ server }${ route } ` ,{ method : body ? 'POST' : 'GET' , headers , body : body ? JSON . stringify ( body ) : undefined , credentials : 'omit' , redirect : 'error' , signal : controller . signal });
const chunks = []; let size = 0 ; for await ( const part of r . body ){ size += part . length ; if ( size > 5 * 1024 * 1024 ) throw Error ( 'Management response exceeds its limit' ); chunks . push ( part );}
const data = JSON . parse ( Buffer . concat ( chunks ). toString ( 'utf8' )); if ( ! r . ok && r . status !== 202 ){ const e = Error ( 'Management request refused' ); e . status = r . status ; throw e ;} return data ;
2026-10-13 18:04:02 +02:00
} finally { clearTimeout ( timer ); managementRequests . delete ( controller ); evidenceRequestTypes . delete ( controller );}
2026-10-13 10:33:31 +02:00
}
2026-10-13 16:58:15 +02:00
function state (){ return { startPage : {...( policy ? . startPage || {}), news : ready ? companyNews : []}, libraryAI : { suggestionsEnabled : ready &&!! policy ? . ai . enabled &&!! policy ? . library ? . aiSuggestionsEnabled && sectionAllowed ( managedSettings (), 'library' ) && featureAllowed ( managedSettings (), 'ai' ), autoApplyEnabled : ready &&!! policy ? . library ? . aiAutoApplyEnabled && policy ? . ai ? . trustMode === 'local' , busy : libraryOrganizer ? . isBusy () || false }, bookmarks : bookmarkState ({ policy : effectiveBookmarkPolicy (), ready , settings : managedSettings ()}), settings : managedSettings (), enrolled :!! device , deviceId : device ? . id || '' , ready , clipboardPending : clipboardGuard ? . isPending () || false ,... tabs ? . summary (), tabsRevision :++ tabsRevision , canNavigate : ready ||!! ( stickyBlock () && managementHealthy ), loginCheckPending : ready &&!! policy ? . security . visualPhishing . enabled && ( ! activeTab () ? . security . visualApproval || activeTab (). security . visualApproval . generation !== activeTab (). navigationId ), message , brand : policy ? . brand || { name : 'SafetyNet' , color : '#137c66' }, policyVersion : policy ? . version || 0 , server : device ? . server || '' , url : view ? . webContents . getURL () || '' , aiEnabled : ready &&!! policy ? . ai . enabled , documentScanEnabled : ready && policy ? . inspection ? . documentAnalysisEnabled !== false , passwordManagerEnabled : ready &&!! policy ? . passwordManager ? . enabled , vaultLocked : vault ? . status (). locked ?? true , vaultExists : vault ? . status (). exists ?? fs . existsSync ( path . join ( app . getPath ( 'userData' ), 'password-vault.json' )), agentIdentityMode : policy ? . agents . identityMode || 'legacy' , agentGrants : ready && featureAllowed ( managedSettings (), 'agents' ) ? approvedAgentGrants (). map (({ id , name , generation , expiresAt })=>({ id , name , generation , expiresAt })) : [], agentsEnabled : ready &&!! policy ? . agents . enabled , agentConnected :!! agentService };}
2026-10-13 10:33:31 +02:00
function emit (){ if ( win &&! win . isDestroyed ()){ const settings = managedSettings (); if ( ! settings . visible ) toolsOpen = false ; if ( ! sectionAllowed ( settings , 'appearance' )) chromeHeight = settings . appearance . tabMode === 'compact' ? 64 : 104 ; if ( ! featureAllowed ( settings , 'ai' )) aiPanel = false ; if ( ! featureAllowed ( settings , 'passwords' )){ vaultPanel = false ; vault ? . lock ();} if ( ! libraryVisible ( settings )) leftSidebar = false ; layout (); win . webContents . send ( 'managed:state' ,{... state (), pageTitle , pageColor , windowMaximized : win . isMaximized ()}); win . setTitle ( `S Browser · ${ state (). brand . name } ` );}}
function layout (){ const tab = activeTab (); if ( ! win ||! tab ) return ; view = tab . view ; startPage = tab . startPage ; const [ width , height ] = win . getContentSize (), left = leftSidebar ? 240 : 0 ; view . setBounds ({ x : left , y : chromeHeight , width : Math . max ( 0 , width - left - (( aiPanel || vaultPanel ) ? 360 : 0 )), height : Math . max ( 0 , height - chromeHeight )}); view . setVisible ( ready && tab . guarded &&! startPage &&! overviewPanel &&! toolsOpen );}
async function pageTheme ( tab = activeTab ()){
if ( ! tab ||! ready ) return ; const contents = tab . view . webContents , url = contents . getURL (), context = tabContext ( tab . id ,{ foreground : false }); if ( ! /^https?:\/\// . test ( url )) return ;
try { const result = await contents . executeJavaScriptInIsolatedWorld ( 999 ,[{ code : `(()=>{if(location.href!== ${ JSON . stringify ( url ) } )return null;return {color:(document.querySelector('meta[name="theme-color"]')?.content||'').slice(0,32),background:getComputedStyle(document.body||document.documentElement).backgroundColor};})()` }], false );
if ( ! currentTabContext ( context ) ||! result ) return ; const hex = /^#[a-f0-9]{6}$/i . test ( result . color ) ? result . color : null , rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/ . exec ( result . background || '' ); tab . color = hex || ( rgb && rgb . slice ( 1 ). every ( v => + v <= 255 ) ? '#' + rgb . slice ( 1 ). map ( v =>( + v ). toString ( 16 ). padStart ( 2 , '0' )). join ( '' ) : '#edf1f6' ); tab . title = contents . getTitle (). slice ( 0 , 200 ); if ( tab === activeTab ()){ pageColor = tab . color ; pageTitle = tab . title ;} emit ();
} catch {}
}
2026-10-13 21:00:41 +02:00
function lockVault (){ const unlocked = vault &&! vault . status (). locked ; credentialBroker ? . revoke (); vault ? . lock (); if ( unlocked ) audit ( 'vault.locked' ); emit ();}
2026-10-13 16:58:15 +02:00
function quarantine ( reason ){ ready = false ; fileUploadGate ? . cancelAll (); if ( policy && Date . parse ( policy . expiresAt ) <= Date . now ()){ configureDiagnostics ({ enabled : false }); const expiredService = agentService ; agentService = null ; agentServiceMode = undefined ; expiredService ? . revoke (); void expiredService ? . close (). catch (()=>{});} for ( const [ controller , route ] of managementRequests ) if ( route === '/api/device/ai' ) controller . abort (); enforceEvidencePolicy (); message = reason ; vaultPanel = false ; vault ? . lock (); tabs ? . quarantine (); emit ();}
2026-10-13 21:00:41 +02:00
function vaultSettings (){ return { enabled : ready &&!! policy ? . passwordManager ? . enabled , allowSaving :!! policy ? . passwordManager ? . allowSaving , allowAutofill :!! policy ? . passwordManager ? . allowAutofill , allowControlledSubmission :!! policy ? . passwordManager ? . controlledSubmission ? . enabled , idleLockSeconds : policy ? . passwordManager ? . idleLockSeconds || 300 };}
2026-10-13 10:33:31 +02:00
function vaultStatus (){ return {...( vault ? . status () || { exists : fs . existsSync ( path . join ( app . getPath ( 'userData' ), 'password-vault.json' )), locked : true }),... vaultSettings ()};}
function requireVault (){
requireFeatureSection ( 'passwords' );
const settings = vaultSettings (); if ( ! settings . enabled || Date . parse ( policy . expiresAt ) <= Date . now ()) throw Error ( 'Password manager is disabled or browser access is paused' );
requireEncryption ();
if ( ! vault || vault . status (). idleSeconds !== settings . idleLockSeconds ){ vault ? . lock (); vault = createVault ({ filePath : path . join ( app . getPath ( 'userData' ), 'password-vault.json' ), idleSeconds : settings . idleLockSeconds , protect : bytes => safeStorage . encryptString ( bytes . toString ( 'base64' )), unprotect : bytes => Buffer . from ( safeStorage . decryptString ( bytes ), 'base64' )});}
return vault ;
}
2026-10-13 16:58:15 +02:00
function persistAudit (){
requireEncryption (); const target = path . join ( app . getPath ( 'userData' ), 'audit-queue.bin' ), temporary = target + '.' + require ( 'node:crypto' ). randomUUID () + '.tmp' ; let fd ;
2026-10-13 19:41:27 +02:00
const stage = ( operation , fn )=> diagnostics . spanSync ( 'browser.security' , diagnosticContext ({ operation }), fn );
try { const serialized = stage ( 'audit.serialize' ,()=> JSON . stringify ( auditQueue )); const encrypted = stage ( 'audit.encrypt' ,()=> safeStorage . encryptString ( serialized )); stage ( 'audit.write' ,()=>{ fd = fs . openSync ( temporary , 'wx' , 0o600 ); fs . writeFileSync ( fd , encrypted );}); stage ( 'audit.fsync' ,()=> fs . fsyncSync ( fd )); stage ( 'audit.commit' ,()=>{ fs . closeSync ( fd ); fd = undefined ; fs . renameSync ( temporary , target );});} finally { if ( fd !== undefined ) fs . closeSync ( fd ); try { fs . unlinkSync ( temporary );} catch {}}
2026-10-13 16:58:15 +02:00
}
function enforceEvidencePolicy (){
2026-10-13 18:04:02 +02:00
if ( ! policy ) return ; const delivery = require ( './evidence-delivery.cjs' );
for ( const [ controller , type ] of evidenceRequestTypes ) if ( ! delivery . policyAllows ({ policy }, type )) controller . abort ();
const next = delivery . stripPendingEvidence ( auditQueue ,{ policy });
if ( next . some (( record , index )=> record !== auditQueue [ index ])){ auditQueue = next ; persistAudit ();}
2026-10-13 16:58:15 +02:00
}
2026-10-13 10:33:31 +02:00
async function flushAudit (){
if ( ! device || flushing ||! auditQueue . length ) return ; flushing = true ;
2026-10-13 18:24:44 +02:00
try { for ( let n = 0 ; n < 100 && auditQueue . length ; n ++ ){ const record = auditQueue [ 0 ]; await diagnostics . withCorrelation ( record ,()=> diagnosticSpan ( 'browser.management' ,{ operation : 'audit.deliver' },()=> require ( './evidence-delivery.cjs' ). deliverAuditRecord ({ record , send : ( route , body )=> request ( device . server , route , body , device . token ), getContext : ()=>({ ready , policy , deviceId : device . id })}))); auditQueue . shift (); persistAudit ();}}
2026-10-13 10:33:31 +02:00
catch {} finally { flushing = false ;}
}
2026-10-13 21:00:41 +02:00
async function audit ( kind , ruleId , host , fileFacts , requestFacts , evidencePayload , agentFacts , interactionFacts , origin ){
2026-10-13 10:33:31 +02:00
if ( ! device ||! policy ) return ;
// Operational correlation contains no rule text, hostname or forensic payload.
2026-10-13 16:58:15 +02:00
try { diagnostics . event ( 'browser.security' , diagnosticContext ({ decision : kind . endsWith ( '.block' ) ? 'block' : kind . endsWith ( '.denied' ) ? 'deny' : kind . endsWith ( '.authorized' ) || kind . endsWith ( '.released' ) ? 'allow' : 'unknown' , reason : ruleId }));} catch {}
2026-10-13 10:33:31 +02:00
if ( auditQueue . length >= 10000 ){ quarantine ( 'Security event queue is full. Contact your administrator.' ); return ;}
2026-10-13 21:00:41 +02:00
try { const file = fileFacts ? require ( '../../packages/core/transfer-facts.cjs' ). transferFacts ( fileFacts ) : undefined ; const request = requestFacts ? require ( '../../packages/core/request-transfer-facts.cjs' ). requestTransferFacts ( requestFacts ) : undefined ; const agent = agentFacts ? require ( '../../packages/core/agent-audit.cjs' ). agentAuditFacts ( agentFacts ) : undefined ; const interaction = interactionFacts ? require ( '../../packages/core/interaction-facts.cjs' ). InteractionFactsSchema . parse ( interactionFacts ) : undefined ; const evidenceType = evidencePayload ? . action ? 'interaction' : 'download' ; auditQueue . push ({ eventId : require ( 'node:crypto' ). randomUUID (), kind ,...( ruleId ? { ruleId } : {}),...( host ? { host } : {}),...( file ? { file } : {}),...( request ? { request } : {}),...( agent ? { agent } : {}),...( interaction ? { interaction } : {}),...( evidencePayload && require ( './evidence-delivery.cjs' ). collectionAllowed ({ ready , policy , type : evidenceType }) ? { protectedEvidence : { deviceId : device . id , type : evidenceType , payload : require ( './evidence-delivery.cjs' ). validatedEvidence ( evidencePayload , evidenceType )}} : {}),... diagnosticCorrelation (), policyVersion : Number . isSafeInteger ( origin ? . policyVersion ) && origin . policyVersion > 0 ? origin . policyVersion : policy . version }); persistAudit (); const eventId = auditQueue . at ( - 1 ). eventId ; flushAudit (); return eventId ;}
2026-10-13 10:33:31 +02:00
catch { quarantine ( 'Security events cannot be persisted. Contact your administrator.' );}
}
2026-10-13 16:58:15 +02:00
function auditAgent ( event ){
const action = event . action === 'agent.read' ? 'read' : event . action || 'agent-request' , outcome = event . outcome === 'blocked' ? 'denied' : event . outcome ;
const helper = require ( '../../packages/core/agent-audit.cjs' ), session = helper . AgentAuditSchema . shape . sessionId . safeParse ( event . sessionId );
const agent = helper . agentAuditFacts ({ action , outcome ,...( event . scope ? { scope : event . scope } : {}),...( session . success && session . data ? { sessionId : session . data } : {}),...( event . principalId ? { principalId : event . principalId , credentialGeneration : event . generation } : {})});
2026-10-13 18:04:02 +02:00
return audit ([ 'denied' , 'failed' ]. includes ( outcome ) ? 'agent.denied' : 'agent.action' , ` ${ event . action || 'agent-request' } : ${ event . outcome } ` , undefined , undefined , undefined , event . protectedInteraction , agent );
2026-10-13 16:58:15 +02:00
}
2026-10-13 10:33:31 +02:00
function applyPolicy ( envelope ){ return diagnosticSpan ( 'browser.policy' ,{},()=> applyPolicyImpl ( envelope ));}
async function applyPolicyImpl ( envelope ){
2026-10-13 18:04:02 +02:00
const next = await diagnosticSpan ( 'browser.policy' ,{ operation : 'policy.verify' },()=> assertPolicyAssignment ( core . verifyPolicy ( envelope , device . publicKey , device . minimumVersion || 0 ), device . id ));
const { nextBookmarks , nextBookmarkJSON } = await diagnosticSpan ( 'browser.policy' ,{ operation : 'policy.bookmarks' }, async ()=>{
2026-10-13 10:33:31 +02:00
const nextBookmarks = ( await import ( '../../packages/core/bookmarks.mjs' )). verifyBookmarkSnapshot ( envelope . bookmarks , device . publicKey ,{ deviceId : device . id , policyVersion : next . version , minimumRevision : device . bookmarkRevision || 0 });
2026-10-13 18:04:02 +02:00
2026-10-13 10:33:31 +02:00
const nextBookmarkJSON = JSON . stringify ( nextBookmarks . items );
if ( nextBookmarks . revision === ( device . bookmarkRevision || 0 ) && device . bookmarkDigest && require ( 'node:crypto' ). createHash ( 'sha256' ). update ( nextBookmarkJSON ). digest ( 'hex' ) !== device . bookmarkDigest || nextBookmarks . revision === personalBookmarks . revision && bookmarkSnapshotJSON && nextBookmarkJSON !== bookmarkSnapshotJSON ) throw Error ( 'Bookmarks changed without a new revision' );
2026-10-13 18:04:02 +02:00
return { nextBookmarks , nextBookmarkJSON };
});
2026-10-13 10:33:31 +02:00
const acceptBookmarks = ()=>{ personalBookmarks = { revision : nextBookmarks . revision , items : nextBookmarks . items }; bookmarkSnapshotJSON = nextBookmarkJSON ; device . bookmarkRevision = nextBookmarks . revision ; device . bookmarkDigest = require ( 'node:crypto' ). createHash ( 'sha256' ). update ( nextBookmarkJSON ). digest ( 'hex' ); saveDevice ( device );};
if ( process . env . SAFETYNET_POLICY_KEY_FILE && fs . readFileSync ( process . env . SAFETYNET_POLICY_KEY_FILE , 'utf8' ). trim () !== device . publicKey . trim ()) throw Error ( 'Policy signing key does not match deployment pin' );
if ( policy && next . version === policy . version ){
// Same versions must describe identical policy; reject equivocation.
2026-10-13 18:04:02 +02:00
if ( JSON . stringify ( next ) !== JSON . stringify ( policy )) throw Error ( 'Policy changed without a new version' ); acceptBookmarks (); await diagnosticSpan ( 'browser.policy' ,{ operation : 'policy.agent-reconcile' },()=> reconcileAgentPolicy ()); enforceEvidencePolicy (); if ( Date . parse ( policy . expiresAt ) > Date . now ()) configureDiagnostics ( policy . diagnostics ); emit (); return ;
2026-10-13 10:33:31 +02:00
}
configureDiagnostics ({ enabled : false }); quarantine ( 'Applying company policy…' );
const proxy = next . proxy ;
const settings = { mode : proxy . mode ,...( proxy . mode === 'fixed_servers' ? { proxyRules : proxy . rules , proxyBypassRules : proxy . bypass } : {}),...( proxy . mode === 'pac_script' ? { pacScript : proxy . pacUrl } : {})};
await browserSession . closeAllConnections (); await browserSession . setProxy ( settings );
await managementSession . closeAllConnections (); await managementSession . setProxy ( settings );
app . configureHostResolver ( resolverOptions ( next . enterprise ? . dns ));
await browserSession . clearHostResolverCache (); recentRequests . clear ();
await managementSession . clearHostResolverCache ();
2026-10-13 21:00:41 +02:00
credentialBroker ? . revoke (); policy = next ; enforceEvidencePolicy (); device . minimumVersion = next . version ; acceptBookmarks (); configureDiagnostics ( next . diagnostics );
2026-10-13 18:04:02 +02:00
await diagnosticSpan ( 'browser.policy' ,{ operation : 'policy.page-configure' }, async ()=>{ for ( const tab of tabs . all ()){ await configureVisualPage ( next . security . visualPhishing . enabled , tab ); if ( ! next . browser . allowDevTools ){ tab . security . developerToolsGuard ? . close (); tab . view . webContents . closeDevTools ();}}});
await diagnosticSpan ( 'browser.policy' ,{ operation : 'policy.agent-reconcile' },()=> reconcileAgentPolicy ());
await diagnosticSpan ( 'browser.policy' ,{ operation : 'policy.audit' },()=> audit ( 'policy.applied' ));
2026-10-13 10:33:31 +02:00
}
async function sync (){
if ( ! device || syncing ) return ; syncing = true ;
try {
const envelope = await request ( device . server , '/api/device/policy' , null , device . token ); await applyPolicy ( envelope );
await request ( device . server , '/api/device/heartbeat' ,{ policyVersion : policy . version }, device . token );
await flushAudit (); if ( auditQueue . length >= 10000 ) throw Error ( 'Security event queue is full' );
// Mandatory local inspection must be attested before unlocking the browser;
// first user gestures must not consume their scan budget waiting for ACL setup.
if ( policy . security . yara . enabled || policy . dlp . semantic . enabled ){
if ( ! localInspector ) throw Error ( 'Required private content inspector is unavailable' );
await diagnosticSpan ( 'browser.parser' ,{ operation : 'scan' },()=> localInspector . waitReady ());
}
if ( Date . parse ( policy . expiresAt ) <= Date . now ()) throw Error ( 'Company policy expired' );
if ( clipboardFatal ) throw Error ( 'Managed renderer security failed; restart the browser' );
2026-10-13 16:58:15 +02:00
if ( uploadLeaseFatal ) throw Error ( 'Upload protection failed; restart the browser' );
2026-10-13 10:33:31 +02:00
const posture = await diagnosticSpan ( 'browser.network' ,{ operation : 'scan' },()=> networkPosture . check ( policy . enterprise ? . vpn ));
if ( ! posture . allowed ) throw Error ( 'Required corporate network is unavailable. Connect your company VPN.' );
managementHealthy = true ;
const blocked = stickyBlock ();
if ( blocked && blocked . policyVersion === policy . version && ( blockedTab () ? . security . visualBlocked ? policy . security . visualPhishing . enabled : policy . security . content . blockedPhrases . length )){ message = blocked . message ; ready = false ; emit (); layout (); return ;}
for ( const tab of tabs . all ()){ tab . security . visualBlocked = null ; tab . security . contentBlocked = null ;}
2026-10-13 16:58:15 +02:00
const recovering =! ready ; ready = true ; flushAudit (); tabs . resume ( policy . version ); message = `Managed · policy v ${ policy . version } ` ; emit (); layout ();
2026-10-13 10:33:31 +02:00
await ensureAgentAPI ();
2026-10-13 16:58:15 +02:00
void refreshCompanyNews ();
2026-10-13 10:33:31 +02:00
if ( recovering ){ for ( const tab of tabs . all ()){ const current = tab . view . webContents . getURL (); if ( /^https?:\/\// . test ( current ) && core . destinationAllowed ( current , policy )) tab . view . webContents . reload (); else if ( current !== 'about:blank' || tabs . all (). length === 1 ) await navigate ({ url : policy . browser . homeUrl , tabId : tab . id });} emit ();}
} catch ( e ){
// A deliberate page quarantine aborts in-flight management requests too.
// Preserve that stronger, sticky verdict instead of replacing it with an
// expected cancellation error or treating it as a management outage.
const blocked = stickyBlock ();
if ( e . name === 'AbortError' &&! ready && blocked && message === blocked . message ){ emit (); return ;}
managementHealthy = false ; quarantine ( e . status === 401 ? 'Device revoked or credentials rejected. Contact your administrator.' : `Browsing paused: ${ e . message } ` ); if ( e . status === 401 ){ configureDiagnostics ({ enabled : false }); agentService ? . revoke (); await agentService ? . close (); agentService = null ; try { fs . unlinkSync ( agentFile ());} catch {} await browserSession . closeAllConnections (); await browserSession . clearStorageData ();}
}
finally { syncing = false ;}
}
2026-10-13 16:58:15 +02:00
async function refreshCompanyNews (){
const p = policy , key = JSON . stringify ([ p ? . version , p ? . startPage ]); if ( ! ready ||! p ? . startPage ? . newsEnabled ){ companyNews = []; companyNewsKey = '' ; return ;} if ( key === companyNewsKey && Date . now () - companyNewsFetchedAt < 300000 ) return ; companyNewsKey = key ; companyNewsFetchedAt = Date . now (); companyNews = []; emit (); try { const result = await request ( device . server , '/api/device/start-page/news' , undefined , device . token ); if ( ! ready || policy !== p || result . policyVersion !== p . version ) return ; companyNews = Array . isArray ( result . news ) ? result . news . slice ( 0 , 12 ). filter ( item => typeof item . title === 'string' && item . title . length <= 200 && typeof item . url === 'string' && /^https:\/\// . test ( item . url ) &&! new URL ( item . url ). username &&! new URL ( item . url ). password ). map ( item =>({ title : item . title , url : item . url , publishedAt : item . publishedAt || null })) : []; emit ();} catch { if ( policy === p ){ companyNews = []; emit ();}}
}
2026-10-13 10:33:31 +02:00
async function completeEnrollment ( server , result ){
if ( device ) throw Error ( 'This installation is already enrolled' );
const candidate = { server , id : result . id , token : result . token , publicKey : result . publicKey , minimumVersion : 0 };
candidate . channelSignatureKey = require ( '../../packages/core/policy-channel.cjs' ). verifyIdentity ( result . managementChannel , candidate . publicKey );
const channelPin = require ( './management-channel.cjs' ). deploymentPin (); if ( channelPin && candidate . channelSignatureKey . trim () !== channelPin ) throw Error ( 'Protected management signing pin mismatch' );
// Optional deployment-time key pin strengthens first enrollment beyond TLS trust.
if ( process . env . SAFETYNET_POLICY_KEY_FILE && fs . readFileSync ( process . env . SAFETYNET_POLICY_KEY_FILE , 'utf8' ). trim () !== candidate . publicKey . trim ()) throw Error ( 'Management signing key does not match deployment pin' );
const initialPolicy = await require ( './management-channel.cjs' ). protectedPolicyRequest ({ device : candidate , request , save : ()=>{}});
assertPolicyAssignment ( core . verifyPolicy ( initialPolicy , candidate . publicKey ), candidate . id );
saveDevice ( candidate ); device = candidate ; await sync (); return state ();
}
function navigate (... args ){ return diagnosticSpan ( 'browser.navigation' ,{},()=> navigateImpl (... args ));}
async function navigateImpl ( input , captured , options = {}){
const address = typeof input === 'string' ? input : input ? . url ;
if ( typeof address !== 'string' || address . length > 4096 ) throw Error ( 'Invalid address' );
const tab = tabs . get ( captured ? . sessionId || input ? . tabId || activeTab (). id ), contents = tab . view . webContents ;
if ( captured && ( ! currentTabContext ( captured . token ) || captured . webContents !== contents )) throw Error ( 'Agent context changed' );
const url = address . includes ( '://' ) ? address : `https:// ${ address } ` ;
2026-10-13 16:58:15 +02:00
if ( ! policy ||! [ 'https:' , 'http:' ]. includes ( new URL ( url ). protocol ) ||! actorDestinationAllowed ( url )) throw Error ( 'Destination is blocked by company policy' );
2026-10-13 10:33:31 +02:00
const blocked = stickyBlock (), documentURL = value =>{ const parsed = new URL ( value ); parsed . hash = '' ; return parsed . href ;};
if ( blocked && managementHealthy && Date . parse ( policy . expiresAt ) > Date . now () && blockedTab () === tab && documentURL ( url ) !== documentURL ( blocked . checkedURL )){
for ( const record of tabs . all ()){ record . security . visualBlocked = null ; record . security . contentBlocked = null ;} ready = true ; tabs . resume ( policy . version ); message = 'Checking destination…' ;
}
if ( ! ready ) throw Error ( 'Browser is not managed and ready' );
if ( options . signal ? . aborted ) throw Error ( 'Agent action cancelled' );
2026-10-13 16:58:15 +02:00
if ( ! agentActorStorage . getStore () ? . principalId &&! captured ) tab . security . agentAuthority = null ;
2026-10-13 10:33:31 +02:00
const initial = policy ; tab . startPage = false ; if ( tab === activeTab ()) startPage = false ; layout ();
const abort = ()=> contents . stop (); options . signal ? . addEventListener ( 'abort' , abort ,{ once : true });
try { await contents . loadURL ( url );} finally { options . signal ? . removeEventListener ( 'abort' , abort );}
if ( options . signal ? . aborted ||! ready || policy !== initial || contents . isDestroyed () || tabs . fromContents ( contents . id ) !== tab ) throw Error ( 'Navigation context changed' );
const refreshed = captured ? agentContext ( tab . id ) : null ;
if ( captured ? . token . foreground && ( ! refreshed . foreground || captured . token . activation !== refreshed . token . activation )) throw Error ( 'Agent foreground changed during navigation' );
emit (); return refreshed || state ();
}
2026-10-13 16:58:15 +02:00
async function agentHistory ( action , captured ,{ signal } = {}){
if ( ! currentTabContext ( captured . token ) || captured . foreground !== true ) throw Error ( 'Agent context changed' );
const tab = tabs . get ( captured . sessionId ), contents = tab . view . webContents , initial = policy , activation = captured . token . activation ;
const current = ( phase = 'after' )=> ready && policy === initial &&! signal ? . aborted &&! contents . isDestroyed () && tabs . fromContents ( contents . id ) === tab && activeTab () === tab && tabContext ( tab . id ,{ foreground : true }). activation === activation && ( phase !== 'before' || currentTabContext ( captured . token ));
const allowed = url => actorDestinationAllowed ( url ) && ( ! initial . agents . allowedHosts . length || initial . agents . allowedHosts . some ( pattern => core . hostMatches ( new URL ( url ). hostname , pattern )));
const result = await diagnosticSpan ( 'browser.navigation' ,{ operation : action },()=> require ( './history-navigation.cjs' ). performHistory ({ contents , action , allowed , current , signal }));
if ( ! current ()) throw Error ( 'History context changed' ); emit (); return { completed : result . completed , context : agentContext ( tab . id )};
}
2026-10-13 18:48:40 +02:00
function nativeHistorySnapshot ( contents ){
return require ( './history-snapshot.cjs' ). readHistorySnapshot ( contents );
}
function getAgentHistory ( captured ){ if ( ! currentTabContext ( captured . token ) || captured . webContents . isDestroyed ()) throw Error ( 'Agent context changed' ); return nativeHistorySnapshot ( captured . webContents );}
async function goAgentHistory ( index , captured ,{ signal , historyBinding } = {}){
if ( ! currentTabContext ( captured . token ) || captured . foreground !== true ) throw Error ( 'Agent context changed' );
const tab = tabs . get ( captured . sessionId ), contents = tab . contents , initial = policy , activation = captured . token . activation ;
const current = ( phase = 'after' )=> ready && policy === initial &&! signal ? . aborted &&! contents . isDestroyed () && tabs . fromContents ( contents . id ) === tab && activeTab () === tab && tabContext ( tab . id ). activation === activation && ( phase !== 'before' || currentTabContext ( captured . token ));
const allowed = url => actorDestinationAllowed ( url ) && ( ! initial . agents . allowedHosts . length || initial . agents . allowedHosts . some ( pattern => core . hostMatches ( new URL ( url ). hostname , pattern )));
const result = await diagnosticSpan ( 'browser.navigation' ,{ operation : 'history:go' },()=> require ( './history-navigation.cjs' ). performHistory ({ contents , action : 'index' , index , getSnapshot : ()=> nativeHistorySnapshot ( contents ), historyBinding , allowed , current , signal }));
if ( ! current ()) throw Error ( 'History context changed' ); emit (); return { completed : result . completed , context : agentContext ( tab . id )};
}
function agentContext ( id ){ const tab = id ? tabs . get ( id ) : activeTab (), foreground = tab === activeTab (), actor = agentActorStorage . getStore (); return { ready : ready && featureAllowed ( managedSettings (), 'agents' ), interactive : foreground &&! tab . startPage &&! overviewPanel &&! toolsOpen , policy , policyVersion : policy ? . version , sessionId : tab . id , foreground , token : tabContext ( tab . id ,{ foreground }), webContents : tab . view . webContents ,...( actor ? . principalId ? { actor : Object . freeze ({ agentId : actor . principalId , generation : actor . generation })} : {}), destinationAllowed : url => actorDestinationAllowed ( url )};}
2026-10-13 10:33:31 +02:00
function managedAI (... args ){ return diagnosticSpan ( 'browser.ai' ,{},()=> managedAIImpl (... args ));}
async function managedAIImpl ( prompt ){
requireFeatureSection ( 'ai' );
if ( ! ready ||! policy ? . ai . enabled ) throw Error ( 'AI access is disabled by company policy' );
if ( typeof prompt !== 'string' ||! prompt . trim () || prompt . length > 100000 ) throw Error ( 'Enter a prompt of up to 100,000 characters' );
const initial = policy , verdict = core . inspectForAI ( Buffer . from ( prompt ), initial );
if ( verdict . decision === 'block' ){ await audit ( 'dlp.block' , verdict . ruleId ); throw Error ( `Blocked by company policy: ${ verdict . ruleId } ` );}
if ( initial . dlp . semantic . enabled || initial . security . yara . enabled ){ if ( ! localInspector ) throw Error ( 'Local inspector unavailable' ); const result = await diagnosticSpan ( 'browser.inspect.local' ,{ bytes : Buffer . byteLength ( prompt )},()=> localInspector . inspect ( Buffer . from ( prompt ), initial )); if ( result . decision !== 'allow' &&! core . allowsConfidentialLocal ( result , initial )) throw Error ( 'Local content policy blocked this prompt' );}
const external = await diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : Buffer . byteLength ( prompt )},()=> scanEnterprise ( Buffer . from ( prompt ), initial , 'ai' )); if ( external . decision !== 'allow' ){ await audit ( 'dlp.block' , external . ruleId ); throw Error ( 'Company external scanner blocked this prompt' );}
if ( ! ready || policy . version !== initial . version ) throw Error ( 'Policy changed during inspection' );
const result = await request ( device . server , '/api/device/ai' ,{ prompt }, device . token );
if ( ! ready || policy . version !== initial . version ) throw Error ( 'Policy changed during AI request' ); return result ;
}
async function mutateManagedBookmarks ( input ){
2026-10-13 16:58:15 +02:00
if ( bookmarkBusy || syncing || libraryOrganizer ? . isBusy ()) throw Object . assign ( Error ( 'Management update in progress. Try again.' ),{ status : 409 , code : 'browser-management-busy' });
2026-10-13 10:33:31 +02:00
const initial = policy , identity = device ;
const payload = validateBookmarkMutation ( input ,{ policy : effectiveBookmarkPolicy (), ready , settings : managedSettings (), destinationAllowed : core . destinationAllowed });
bookmarkBusy = true ;
try {
const result = await request ( identity . server , '/api/device/bookmarks' , payload , identity . token );
if ( policy !== initial || device !== identity ) throw Error ( 'Managed policy changed during bookmark update' );
const deadline = Date . now () + 12000 ; while ( syncing ){ if ( Date . now () >= deadline ) throw Error ( 'Bookmark update awaits management synchronization' ); await new Promise ( resolve => setTimeout ( resolve , 50 ));}
await sync ();
if ( ! ready || device !== identity || personalBookmarks . revision !== result . revision || policy . version !== result . version ) throw Error ( 'Bookmark update requires a fresh signed policy' );
return state ();
} finally { bookmarkBusy = false ;}
}
2026-10-13 16:58:15 +02:00
function requireLibraryAI (){
requireManagedSection ( 'library' ); requireFeatureSection ( 'ai' );
if ( ! ready ||! policy ? . ai . enabled ||! policy . library ? . aiSuggestionsEnabled || Date . parse ( policy . expiresAt ) <= Date . now ()) throw Error ( 'AI library organization is disabled by company policy' );
if ( ! policy . browser . bookmarks . allowUserEdit ) throw Error ( 'Bookmark organization is controlled by your administrator' );
}
function nativeLibraryRows (){ return tabs . all (). filter ( tab => ! tab . startPage && /^https?:\/\// . test ( tab . view . webContents . getURL ())). map ( tab =>({ id : tab . id , title : String ( tab . title ). replace ( /[\u0000-\u001f\u007f]/g , ' ' ). slice ( 0 , 120 ), url : tab . view . webContents . getURL ()}));}
function nativeLibraryEpoch (){ return JSON . stringify ( tabs . all (). map ( tab =>[ tab . id , tab . navigationId , tab . title , tab . view . webContents . getURL (), tab . group ]));}
function assertAgentLibraryDestinations ( snapshot ){ if ( agentActorStorage . getStore () ? . principalId && [...( snapshot . tabs || []),...( snapshot . bookmarks || [])]. some ( row => ! actorDestinationAllowed ( row . url ))) throw Error ( 'Agent destination denied' );}
function getLibraryOrganizer (){
if ( libraryOrganizer ) return libraryOrganizer ;
libraryOrganizer = require ( './library-organization.cjs' ). createLibraryOrganization ({
capture : async ()=>{ const library = await import ( '../../packages/core/library.mjs' ); requireLibraryAI (); if ( bookmarkBusy || syncing ) throw Error ( 'Management update in progress' ); const rows = library . LibraryTabsSchema . parse ( nativeLibraryRows ()); if ( agentActorStorage . getStore () ? . principalId && [... rows ,... personalBookmarks . items ]. some ( row => ! actorDestinationAllowed ( row . url ))) throw Error ( 'Agent destination denied' ); return { policy , identity : device , policyVersion : policy . version , bookmarkRevision : personalBookmarks . revision , bookmarks : structuredClone ( personalBookmarks . items ), tabs : rows , snapshotHash : library . librarySnapshotHash ( rows ), epoch : nativeLibraryEpoch ()};},
assertCurrent : ( snapshot ,{ afterCommit = false , result } = {})=>{ requireLibraryAI (); assertAgentLibraryDestinations ( snapshot ); if ( policy !== snapshot . policy || device !== snapshot . identity || nativeLibraryEpoch () !== snapshot . epoch || personalBookmarks . revision !== snapshot . bookmarkRevision || afterCommit && ( result . version !== policy . version || result . snapshotHash !== snapshot . snapshotHash )) throw Error ( 'Library context changed. Generate a fresh proposal.' );},
inspect : async snapshot =>{ const library = await import ( '../../packages/core/library.mjs' ); const bytes = Buffer . from ( JSON . stringify ({ bookmarks : library . libraryMetadata ( snapshot . bookmarks ), tabs : library . libraryMetadata ( snapshot . tabs )})); let verdict = core . inspectForAI ( bytes , snapshot . policy ); if ( verdict . decision !== 'allow' ) throw Error ( 'Company policy blocks library metadata' ); if ( snapshot . policy . dlp . semantic . enabled || snapshot . policy . security . yara . enabled ){ if ( ! localInspector ) throw Error ( 'Local inspector unavailable' ); verdict = await localInspector . inspect ( bytes , snapshot . policy ); if ( verdict . decision !== 'allow' &&! core . allowsConfidentialLocal ( verdict , snapshot . policy )) throw Error ( 'Local policy blocks library metadata' );} verdict = await scanEnterprise ( bytes , snapshot . policy , 'ai' ); if ( verdict . decision !== 'allow' ) throw Error ( 'Enterprise scanner blocks library metadata' );},
propose : snapshot => request ( snapshot . identity . server , '/api/device/library/propose' ,{ policyVersion : snapshot . policyVersion , bookmarkRevision : snapshot . bookmarkRevision , tabs : snapshot . tabs , snapshotHash : snapshot . snapshotHash }, snapshot . identity . token ),
commit : async ( proposalId , snapshot , automatic )=>{ const result = await request ( snapshot . identity . server , '/api/device/library/apply' ,{ proposalId , policyVersion : snapshot . policyVersion , expectedRevision : snapshot . bookmarkRevision , snapshotHash : snapshot . snapshotHash , mode : automatic ? 'automatic' : 'manual' }, snapshot . identity . token ); const signed = ( await import ( '../../packages/core/bookmarks.mjs' )). verifyBookmarkSnapshot ( result . bookmarks , snapshot . identity . publicKey ,{ deviceId : snapshot . identity . id , policyVersion : snapshot . policyVersion , minimumRevision : snapshot . bookmarkRevision }); if ( signed . revision !== result . revision ||! [ snapshot . bookmarkRevision , snapshot . bookmarkRevision + 1 ]. includes ( signed . revision )) throw Error ( 'Invalid signed organization revision' ); if ( signed . revision === snapshot . bookmarkRevision && JSON . stringify ( signed . items ) !== JSON . stringify ( snapshot . bookmarks )) throw Error ( 'Bookmarks changed without a new revision' ); return {... result , verifiedBookmarks : signed };},
applyTabs : ( assignments , result )=>{ for ( const row of assignments ) tabs . get ( row . id ); const signed = result . verifiedBookmarks ; personalBookmarks = { revision : signed . revision , items : signed . items }; bookmarkSnapshotJSON = JSON . stringify ( signed . items ); device . bookmarkRevision = signed . revision ; device . bookmarkDigest = require ( 'node:crypto' ). createHash ( 'sha256' ). update ( bookmarkSnapshotJSON ). digest ( 'hex' ); saveDevice ( device ); for ( const row of assignments ) tabs . get ( row . id ). group = row . group ;},
autoAllowed : ()=> policy ? . library ? . aiAutoApplyEnabled === true && policy ? . ai ? . trustMode === 'local' ,
audit , changed : emit
}); return libraryOrganizer ;
}
async function organizeLibrary ( options ){ return getLibraryOrganizer (). plan ( options );}
async function applyLibraryOrganization ( input , options ){
const result = await getLibraryOrganizer (). apply ( input , options );
await sync (); return result ;
}
2026-10-13 19:14:32 +02:00
async function inspectManagedAgentText ( text , context ){
const aiContext = context . action === 'ai' ;
let verdict = aiContext ? core . inspectForAI ( Buffer . from ( text ), context . policy ) : core . inspect ( Buffer . from ( text ), context . policy ); if ( verdict . decision === 'block' ) return verdict ;
if ( context . policy . dlp . semantic . enabled || context . policy . security . yara . enabled ){
if ( ! localInspector ) return { decision : 'block' , ruleId : 'inspection-unavailable' , coverage : { complete : false }};
const result = await diagnosticSpan ( 'browser.inspect.local' ,{ bytes : Buffer . byteLength ( text )},()=> localInspector . inspect ( Buffer . from ( text ), context . policy ));
verdict = aiContext && core . allowsConfidentialLocal ( result , context . policy ) ? {... result , decision : 'allow' , classification : 'local-only' } : result ;
}
if ( verdict . decision !== 'allow' ) return verdict ;
const external = await diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : Buffer . byteLength ( text )},()=> scanEnterprise ( Buffer . from ( text ), context . policy , aiContext ? 'ai' : 'upload' )); return external . decision === 'allow' ? verdict : external ;
}
2026-10-13 10:33:31 +02:00
async function ensureAgentAPI (){
if ( ! ready ||! policy . agents . enabled ||! featureAllowed ( managedSettings (), 'agents' ) || agentService || agentStarting ) return ; agentStarting = true ;
try {
const getContext = agentContext ;
2026-10-13 18:48:40 +02:00
const actions = createAgentAdapters ({ getContext , appControls : agentAppControls (), getSessions : ()=> tabs . all (). filter ( tab => ! agentActorStorage . getStore () ? . principalId || actorDestinationAllowed ( tab . view . webContents . getURL ())). map ( tab =>({ id : tab . id , title : tab . title , foreground : tab === activeTab ()})), contextCurrent : currentTabContext , navigate , history : agentHistory , getHistory : getAgentHistory , goHistory : goAgentHistory , ai : managedAI ,
2026-10-13 19:14:32 +02:00
inspect : inspectManagedAgentText , audit : auditAgent , onInteraction : async metadata =>{
2026-10-13 18:04:02 +02:00
const payload = await diagnosticSpan ( 'browser.evidence' ,{ operation : 'evidence.capture' , sessionId : metadata . sessionId },()=> require ( './evidence-capture.cjs' ). buildInteractionEvidence ({... metadata , url : metadata . pageUrl , captureSource : 'agent-adapter' }, policy )); if ( ! payload ) return ;
const actor = agentActorStorage . getStore (); await auditAgent ({ action : 'click' , outcome : 'completed' , sessionId : metadata . sessionId ,...( actor ? . principalId ? { principalId : actor . principalId , generation : actor . generation } : {}), protectedInteraction : payload });
}});
2026-10-13 19:41:27 +02:00
Object . assign ( actions , agentSessionControls ());
2026-10-13 16:58:15 +02:00
for ( const action of [ 'click' , 'type' , 'select' , 'check' ]){ const original = actions [ action ]; actions [ action ] = async (... args )=>{ const tab = tabs . get ( args [ 0 ] ? . sessionId || activeTab (). id ), context = tabContext ( tab . id ); const checked = await checkVisual ( tab ); if ( ! currentTabContext ( context ) || checked . decision !== 'allow' ) throw Error ( 'Visual login protection has not approved this page' ); return original (... args );};}
for ( const action of Object . keys ( actions )){ const original = actions [ action ]; actions [ action ] = ( args , context = {})=> agentActorStorage . run ({... context , action },()=> diagnosticSpan ( 'browser.agent' ,{ operation : action }, async ()=>{
2026-10-13 21:00:41 +02:00
if ( context . principalId && [ 'navigate' , 'back' , 'forward' , 'reload' , 'history:go' , 'vault:fill' , 'vault:prepare-submit' , 'click' , 'type' , 'select' , 'check' ]. includes ( action )){ const tab = tabs . get ( args . sessionId || activeTab (). id ); tab . security . agentAuthority = { principalId : context . principalId , generation : context . generation , action };}
2026-10-13 16:58:15 +02:00
return original ( args , context );
}));}
agentServiceMode = policy . agents . identityMode ; agentService = await startAgentControl ({ actions , principalMode : agentServiceMode , principals : agentServiceMode === 'individual' ? effectiveAgentPolicy (). agents . grants : [],
traceOperation : ( operation , fn )=> diagnosticSpan ( 'browser.agent' ,{ operation }, fn ),
2026-10-13 10:33:31 +02:00
onError : error =>{ if ( ! app . isPackaged && process . env . SAFETYNET_SMOKE_ARTIFACTS ) fs . appendFileSync ( path . join ( process . env . SAFETYNET_SMOKE_ARTIFACTS , 'smoke-entry.txt' ), `Agent action diagnostic: ${ error . reason } \n` );},
2026-10-13 16:58:15 +02:00
authorize : ({ action , args , scope , principalId , generation })=>{
const actor = { principalId , generation , action };
if ( ! actorGrantAllowed ( actor , action )) return { allowed : false };
const checkURL = url => actorDestinationAllowed ( url , actor );
2026-10-13 19:41:27 +02:00
if ( action === 'sessions' || [ 'sessions:save' , 'sessions:list' , 'sessions:restore' ]. includes ( action )) return { allowed : true , role : 'agent' , scopes : [ scope ]};
2026-10-13 16:58:15 +02:00
if ( action === 'bookmarks:mutate' && args . bookmark ? . url &&! checkURL ( args . bookmark . url )) return { allowed : false };
if ( action === 'bookmarks:mutate' && args . action !== 'add' ){ const existing = personalBookmarks . items . find ( row => row . id === ( args . id || args . bookmark ? . id )); if ( ! existing ||! checkURL ( existing . url )) return { allowed : false };}
if ( action === 'tabs:create' && args . url &&! checkURL ( args . url )) return { allowed : false };
if ([ 'tabs:activate' , 'tabs:close' , 'tabs:group' ]. includes ( action )){ let tab ; try { tab = tabs . get ( args . sessionId );} catch { return { allowed : false };} const url = tab . view . webContents . getURL (); if ( url !== 'about:blank' &&! checkURL ( url )) return { allowed : false };}
if ( Object . hasOwn ( agentAppControls (), action )) return { allowed : true , role : 'agent' , scopes : [ scope ]};
2026-10-13 10:33:31 +02:00
let tab ; try { tab = args . sessionId ? tabs . get ( args . sessionId ) : activeTab ();} catch { return { allowed : false };}
const url = action === 'navigate' ? args . url : tab . view . webContents . getURL ();
2026-10-13 16:58:15 +02:00
if ( ! checkURL ( url )) return { allowed : false };
2026-10-13 10:33:31 +02:00
return { allowed : true , role : 'agent' , scopes : [ scope ]};
2026-10-13 16:58:15 +02:00
}, audit : auditAgent ,
persistCredential : connection => agentGrants . persistConnection ({ filePath : agentFile (), connection , protect : value => require ( './dpapi.cjs' ). capabilityCrypto ( 'protect' , value ), isCurrent : ()=> ready && policy ? . agents . identityMode === 'legacy' && policy . agents . enabled })
2026-10-13 10:33:31 +02:00
}); emit ();
} finally { agentStarting = false ;}
}
function dnsGate (... args ){ return diagnosticSpan ( 'browser.dns' ,{},()=> dnsGateImpl (... args ));}
async function dnsGateImpl ( url , currentPolicy ){
const settings = currentPolicy . security . dns ;
if ( ! settings . enabled ||! settings . blockedIPs . length ) return ;
const host = new URL ( url ). hostname ;
if ( securityCore . dnsAddressBlocked ( host . replace ( /^\[|\]$/g , '' ), currentPolicy )) throw Error ( 'dns-ip-blocklist' );
let timer ; const resolved = await Promise . race ([ browserSession . resolveHost ( host ,{ cacheUsage : 'allowed' }), new Promise (( _ , reject )=>{ timer = setTimeout (()=> reject ( Error ( 'dns-resolution-timeout' )), 5000 );})]). finally (()=> clearTimeout ( timer ));
const addresses = resolved . endpoints ? . map ( v => v . address ) || [];
if ( ! addresses . length || addresses . some ( ip => securityCore . dnsAddressBlocked ( ip , currentPolicy ))) throw Error ( 'dns-ip-blocklist' );
}
function hooks (){
browserSession . setPermissionRequestHandler (( _wc , _permission , callback )=> callback ( false ));
browserSession . setPermissionCheckHandler (()=> false );
2026-10-13 16:58:15 +02:00
const leaseRoot = app . isPackaged ? process . resourcesPath : path . join ( __dirname , '../..' );
fileUploadGate = require ( './file-upload-gate.cjs' ). createFileUploadGate ({
maxBytes : context => context . policy . dlp . maxBytes ,
isCurrent : context => ! uploadLeaseFatal && ready && policy === context . policy && Date . parse ( context . policy . expiresAt ) > Date . now () && context . pending () && ( ! context . source || currentTabContext ( context . source )),
// Phase callbacks cancel denied requests. Live handles remain held until a
// native terminal event; stop()/quarantine alone is not proof of termination.
abort : ()=>{},
createLease : options => diagnosticSpan ( 'browser.upload.lease' ,{ count : options . paths . length },()=> require ( './upload-lease.cjs' ). acquireUploadLease ({... options ,
pythonPath : path . resolve ( leaseRoot , app . isPackaged ? 'python/python.exe' : '.runtime/windows/python/python.exe' ),
helperPath : path . resolve ( leaseRoot , app . isPackaged ? 'inspector/upload_lease.py' : 'apps/inspector/upload_lease.py' ),
onLost : ()=>{ uploadLeaseFatal = true ; options . onLost (); quarantine ( 'Upload protection failed. Restart the browser.' ); diagnosticSpan ( 'browser.upload.fault' ,{ decision : 'block' , reason : 'upload-lease-lost' },()=> audit ( 'upload.block' , 'upload-lease-lost' )). catch (()=>{});}})),
inspectPart : ( bytes ,{ kind , context , filename })=> diagnosticSpan ( 'browser.upload.part' ,{ bytes : bytes . length }, async ()=>{
const p = context . policy ;
if ( kind === 'file' ){
const transfer = require ( '../../packages/core/file-transfer.cjs' ). inspectFileTransfer ( bytes , p , 'upload' ,{ filename });
if ( transfer . decision !== 'allow' ) return transfer ;
}
const verdict = core . inspect ( bytes , p );
if ( verdict . decision !== 'allow' && verdict . ruleId !== 'uninspectable' ) return verdict ;
const needsLocal = verdict . ruleId === 'uninspectable' || p . dlp . semantic . enabled || p . security . yara . enabled ;
if ( needsLocal ){
if ( ! localInspector ) return { decision : 'block' , ruleId : 'inspector-unavailable' };
const local = await diagnosticSpan ( 'browser.inspect.local' ,{ bytes : bytes . length },()=> localInspector . inspect ( bytes , p ));
if ( local . decision !== 'allow' || local . coverage ? . complete !== true ) return local ;
}
return diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : bytes . length },()=> scanEnterprise ( bytes , p , 'upload' ));
})});
for ( const event of [ 'onCompleted' , 'onErrorOccurred' ]) browserSession . webRequest [ event ]({ urls : [ '<all_urls>' ]}, details =>{
2026-10-13 21:00:41 +02:00
credentialBroker ? . complete ( details . id ); const record = credentialRequestCorrelations . get ( details . id ); credentialRequestCorrelations . delete ( details . id );
if ( record ? . released && record . deviceId === device ? . id ) diagnostics . withCorrelation ( record . ids ,()=> diagnosticSpan ( 'browser.credential' ,{ operation : event === 'onCompleted' ? 'credential.complete' : 'credential.failure' , policyVersion : record . policyVersion , sessionId : record . sessionId },()=> audit ( event === 'onCompleted' ? 'vault.submission-completed' : 'vault.submission.failure' , undefined , undefined , undefined , undefined , undefined , undefined , undefined , record ))). catch (()=>{});
2026-10-13 16:58:15 +02:00
if ( ! fileUploadGate . has ( details . id )) return ;
diagnosticSpan ( 'browser.upload.release' ,{},()=> fileUploadGate . terminal ( details . id )). catch (()=>{ uploadLeaseFatal = true ; quarantine ( 'Upload protection cleanup failed. Restart the browser.' ); diagnosticSpan ( 'browser.upload.fault' ,{ decision : 'block' , reason : 'upload-lease-release-failed' },()=> audit ( 'upload.block' , 'upload-lease-release-failed' )). catch (()=>{});});
});
2026-10-13 21:00:41 +02:00
browserSession . webRequest . onBeforeRedirect ({ urls : [ '<all_urls>' ]}, details => credentialBroker ? . redirect ( details . id ));
2026-10-13 10:33:31 +02:00
const download = createDownloadGate ({ getContext : id =>{ const tab = tabs . fromContents ( id ); return tab ? { ready , policy , webContentsId : id , token : tabContext ( tab . id ,{ foreground : false })} : null ;}, isCurrent : context => !! context && currentTabContext ( context . token ) && policy === context . policy ,
2026-10-13 16:58:15 +02:00
fetchResponse : ( url , options )=> diagnosticSpan ( 'browser.request' ,{ operation : 'download' },()=> browserSession . fetch ( url , options )), scan : ( bytes , p )=> diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : bytes . length },()=> scanDownloadEnterprise ( bytes , p , localInspector ? { inspect : ( data , pol )=> diagnosticSpan ( 'browser.inspect.local' ,{ bytes : data . length },()=> localInspector . inspect ( data , pol )), scanDownload : ( data , pol )=> diagnosticSpan ( 'browser.yara' ,{ operation : 'scan' , bytes : data . length },()=> localInspector . scanDownload ( data , pol ))} : null )),
2026-10-13 10:33:31 +02:00
choosePath : async name =>{ const chosen = await dialog . showSaveDialog ( win ,{ title : 'Save scanned download (choose a new file)' , defaultPath : path . join ( app . getPath ( 'downloads' ), name )}); return chosen . canceled ? null : chosen . filePath ;},
writeFile : async ( file , bytes , current )=>{
if ( ! current () ||! path . isAbsolute ( file ) || file . includes ( '\\\\?\\' ) || file . slice ( 2 ). includes ( ':' )) throw Error ( 'Save path rejected' );
const handle = await fs . promises . open ( file , 'wx' , 0o600 ); let release = false ;
try { if ( ! current ()) throw Error ( 'Policy changed' ); await handle . writeFile ( bytes ); await handle . sync (); if ( ! current ()) throw Error ( 'Policy changed' ); release = true ;}
finally { await handle . close (); if ( ! release ) await fs . promises . unlink ( file ). catch (()=>{});}
2026-10-13 16:58:15 +02:00
}, audit : ( kind , rule , host , facts , evidence )=> audit ( kind , rule , host , facts , undefined , evidence ), status : text =>{ message = text ; emit ();}});
2026-10-13 10:33:31 +02:00
browserSession . on ( 'will-download' ,( event , item , contents )=>{
const url = item . getURL (), name = item . getFilename (), tracked = recentRequests . get ( ` ${ contents ? . id } : ${ url } ` ); event . preventDefault ();
if ( ! /^https?:\/\// . test ( url )){ audit ( 'download.block' , 'unsupported-download' ); return ;}
diagnosticSpan ( 'browser.download' ,{},()=> download ({ url , name , method : tracked ? . method , webContentsId : contents ? . id })). catch (()=> audit ( 'download.block' , 'download-inspection-failed' ));
});
browserSession . webRequest . onBeforeRequest ({ urls : [ '<all_urls>' ]},( details , callback )=>{
// A delegated native frontend may load its bundled Chromium UI only. Its HTTP
// requests receive no exemption, and neither pages nor trusted chrome qualify.
if ( developerAssetRequestAllowed ( details ,{ ready , policy , tabs : tabs . all ()})){ callback ({ cancel : false }); return ;}
let called = false ; const finish = cancel =>{ if ( ! called ){ called = true ; callback ({ cancel });}};
const initialPolicy = policy , source = details . webContentsId > 0 ? tabs . fromContents ( details . webContentsId ) : null , sourceContext = source ? tabContext ( source . id ,{ foreground : false }) : null ;
2026-10-13 16:58:15 +02:00
const sourceCurrent = ()=> require ( './request-source.cjs' ). requestSourceCurrent ({ details , source , token : sourceContext , registry : tabs , currentDocument : currentTabContext }); let timer ;
2026-10-13 10:33:31 +02:00
// Async gates complete before Chromium may transmit the request.
2026-10-13 21:00:41 +02:00
requestDiagnostic ( details ,{ requestType : details . resourceType ,...( source ? { sessionId : source . id } : {})}, async ()=>{ try {
2026-10-13 16:58:15 +02:00
timer = setTimeout (()=>{ if ( called ) return ; finish ( true ); audit ( 'navigation.block' , 'request-inspection-timeout' , new URL ( details . url ). hostname ). catch (()=>{});}, 40000 );
if ( ! ready ||! policy || Date . parse ( policy . expiresAt ) <= Date . now () ||! agentRequestOwnerAllowed ( source ) || ( details . webContentsId > 0 && ( ! source ||! source . guarded ))){ if ( ready && policy && ( ! agentRequestOwnerAllowed ( source ) || details . webContentsId > 0 && ( ! source ||! source . guarded ))) await audit ( 'navigation.block' , 'invalid-source-owner' , new URL ( details . url ). hostname ); finish ( true ); return ;}
if ( ! core . destinationAllowed ( details . url , policy ) || ( source ? . security . agentAuthority &&! actorDestinationAllowed ( details . url , source . security . agentAuthority ))){ audit ( 'navigation.block' , null , new URL ( details . url ). hostname ); finish ( true ); return ;}
2026-10-13 21:00:41 +02:00
if ( credentialBroker && source ){ try { credentialBroker . guardIssued ( details , credentialContext ( source )); credentialBroker . validateRedirect ( details , credentialContext ( source ));} catch { await audit ( 'vault.submission-denied' ); message = 'Login submission blocked by company policy. Contact your administrator.' ; emit (); finish ( true ); return ;}}
2026-10-13 10:33:31 +02:00
recentRequests . set ( ` ${ details . webContentsId } : ${ details . url } ` ,{ method : details . method , time : Date . now ()}); if ( recentRequests . size > 2000 ) for ( const [ url , value ] of recentRequests ) if ( value . time < Date . now () - 120000 || recentRequests . size > 2000 ) recentRequests . delete ( url );
try { await dnsGate ( details . url , initialPolicy );} catch ( error ){ audit ( 'dns.block' , error . message , new URL ( details . url ). hostname ); finish ( true ); return ;}
2026-10-13 16:58:15 +02:00
if ( called ||! ready || policy !== initialPolicy ||! sourceCurrent ()){ if ( ! called && ready && policy === initialPolicy &&! sourceCurrent ()) await audit ( 'navigation.block' , 'request-source-context-changed' , new URL ( details . url ). hostname ); finish ( true ); return ;}
2026-10-13 10:33:31 +02:00
if ( details . resourceType === 'webSocket' &&! policy . browser . allowWebSockets ){ audit ( 'dlp.block' , 'websocket-disabled' , new URL ( details . url ). hostname ); finish ( true ); return ;}
const address = new URL ( details . url ), query = address . pathname + address . search ;
const queryVerdict = core . inspect ( Buffer . from ( query ), policy );
if ( queryVerdict . decision === 'block' ){ audit ( 'dlp.block' , queryVerdict . ruleId , new URL ( details . url ). hostname ); finish ( true ); return ;}
const uploads = details . uploadData || [];
if ( policy . security . visualPhishing . enabled && ( uploads . length || ( address . search && details . resourceType === 'xhr' ) || details . resourceType === 'webSocket' )){
const checked = source ? await checkVisual ( source ) : { decision : 'unknown' }; if ( checked . decision !== 'allow' ||! ready || policy !== initialPolicy ||! sourceCurrent ()){ finish ( true ); return ;}
}
2026-10-13 16:58:15 +02:00
if ( uploads . some ( part => part . type === 'file' )){
const context = { policy : initialPolicy , source : sourceContext , pending : ()=> ! called };
const verdict = await fileUploadGate . inspect ( details , context );
if ( verdict . decision !== 'allow' ){ await audit ( 'upload.block' , verdict . ruleId || 'inspection-incomplete' , address . hostname ); finish ( true ); return ;}
await audit ( 'upload.authorized' , null , address . hostname , undefined , verdict . request );
finish ( uploadLeaseFatal ||! ready || policy !== initialPolicy ||! sourceCurrent ()); return ;
}
// Data-pipe/blob bodies still lack an immutable bounded native snapshot.
2026-10-13 10:33:31 +02:00
if ( uploads . some ( part => ! part . bytes || part . file || part . blobUUID )){ audit ( 'dlp.block' , 'uninspectable-upload' , new URL ( details . url ). hostname ); finish ( true ); return ;}
if ( ! [ 'GET' , 'HEAD' , 'OPTIONS' ]. includes ( details . method ) &&! uploads . length ){ audit ( 'dlp.block' , 'missing-request-body' , new URL ( details . url ). hostname ); finish ( true ); return ;}
if ( uploads . length ){
if ( uploads . reduce (( n , p )=> n + p . bytes . length , 0 ) > policy . dlp . maxBytes ){ audit ( 'dlp.block' , 'size-limit' , new URL ( details . url ). hostname ); finish ( true ); return ;}
const requestBytes = Buffer . concat ( uploads . map ( p => Buffer . from ( p . bytes )));
const transfer = require ( '../../packages/core/file-transfer.cjs' ). inspectFileTransfer ( requestBytes , policy , 'upload' );
2026-10-13 16:58:15 +02:00
// An arbitrary request may contain credentials even when its magic bytes look binary.
// Keep size/type for analysts; never fingerprint request bodies in ordinary audit.
const requestFacts = require ( '../../packages/core/request-transfer-facts.cjs' ). requestFactsForTransfer ( transfer );
const releaseUpload = async ()=>{ if ( called ) return ; await audit ( 'upload.authorized' , null , new URL ( details . url ). hostname , undefined , requestFacts ); finish ( ! ready || policy !== initialPolicy ||! sourceCurrent ());};
if ( transfer . decision !== 'allow' ){ await audit ( 'upload.block' , transfer . ruleId , new URL ( details . url ). hostname , undefined , requestFacts ); requestBytes . fill ( 0 ); finish ( true ); return ;}
2026-10-13 21:00:41 +02:00
let claim ;
try { claim = credentialBroker && source ? await diagnosticSpan ( 'browser.credential' ,{ operation : 'credential.claim' },()=> credentialBroker . claim ( details , requestBytes , credentialContext ( source ))) : null ; if ( claim ){ for ( const [ id , row ] of credentialRequestCorrelations ) if ( ! row . released && row . expires <= Date . now ()) credentialRequestCorrelations . delete ( id ); if ( credentialRequestCorrelations . size >= 256 ){ credentialBroker . cancel ( credentialContext ( source )); throw Error ( 'Controlled login audit capacity reached' );} credentialRequestCorrelations . set ( details . id ,{ ids : diagnosticCorrelation (), policyVersion : initialPolicy . version , deviceId : device . id , sessionId : source . id , expires : Date . now () + 60000 , released : false });}} catch { requestBytes . fill ( 0 ); claim ? . residual . fill ( 0 ); await audit ( 'vault.submission-denied' ); message = 'Login submission blocked by company policy. Contact your administrator.' ; emit (); finish ( true ); return ;}
try { const scanBytes = claim ? claim . residual : requestBytes ;
const verdict = core . inspect ( scanBytes , policy ); requestBytes . fill ( 0 );
if ( verdict . decision === 'block' && verdict . ruleId !== 'uninspectable' ){ claim ? . residual . fill ( 0 ); const reference = await audit ( 'dlp.block' , verdict . ruleId , new URL ( details . url ). hostname ); if ( claim ) await audit ( 'vault.submission-denied' ); if ( details . resourceType === 'mainFrame' && details . method === 'POST' ){ message = 'Submission blocked by company data policy.' + ( reference ? ' Event: ' + reference : '' ); emit ();} finish ( true ); return ;}
2026-10-13 10:33:31 +02:00
if ( verdict . ruleId === 'uninspectable' || policy . dlp . semantic . enabled || policy . security . yara . enabled || icapRequired ( policy , 'upload' )){
const needsLocal = verdict . ruleId === 'uninspectable' || policy . dlp . semantic . enabled || policy . security . yara . enabled ;
2026-10-13 21:00:41 +02:00
if ( needsLocal &&! localInspector ){ claim ? . residual . fill ( 0 ); audit ( 'dlp.block' , 'inspector-unavailable' , new URL ( details . url ). hostname ); finish ( true ); return ;}
const snapshot = claim ? Buffer . from ( claim . residual ) : Buffer . concat ( uploads . map ( p => Buffer . from ( p . bytes ))), currentPolicy = policy ; claim ? . residual . fill ( 0 );
2026-10-13 10:33:31 +02:00
try { let result = needsLocal ? await diagnosticSpan ( 'browser.inspect.local' ,{ bytes : snapshot . length },()=> localInspector . inspect ( snapshot , currentPolicy )) : { decision : 'allow' }; if ( result . decision === 'allow' ) result = await diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : snapshot . length },()=> scanEnterprise ( snapshot , currentPolicy , 'upload' ));
2026-10-13 16:58:15 +02:00
const cancel = called ||! ready || policy . version !== currentPolicy . version ||! sourceCurrent () || result . decision !== 'allow' ;
2026-10-13 10:33:31 +02:00
if ( cancel ){ audit ( 'dlp.block' , result . ruleId || 'policy-changed' , new URL ( details . url ). hostname ); finish ( true );} else await releaseUpload ();
} catch { audit ( 'dlp.block' , 'inspection-incomplete' , new URL ( details . url ). hostname ); finish ( true );} finally { snapshot . fill ( 0 );} return ;
2026-10-13 21:00:41 +02:00
} else { claim ? . residual . fill ( 0 ); await releaseUpload (); return ;}
} finally { requestBytes . fill ( 0 ); claim ? . residual . fill ( 0 );}
2026-10-13 10:33:31 +02:00
}
finish ( false );
} catch { finish ( true );} finally { clearTimeout ( timer );}}). catch (()=> finish ( true ));
});
2026-10-13 16:58:15 +02:00
browserSession . webRequest . onBeforeSendHeaders ({ urls : [ '<all_urls>' ]},( details , callback )=>{
if ( developerAssetRequestAllowed ( details ,{ ready , policy , tabs : tabs . all ()})){ callback ({ cancel : false }); return ;}
let called = false ; const finish = ( cancel , requestHeaders )=>{ if ( called ) return ; called = true ; callback ( cancel ? { cancel : true } : { cancel : false , requestHeaders });};
const initialPolicy = policy , source = details . webContentsId > 0 ? tabs . fromContents ( details . webContentsId ) : null , sourceContext = source ? tabContext ( source . id ,{ foreground : false }) : null ;
const sourceCurrent = ()=> require ( './request-source.cjs' ). requestSourceCurrent ({ details , source , token : sourceContext , registry : tabs , currentDocument : currentTabContext });
const current = ()=> ! called && ready && policy === initialPolicy && Date . parse ( initialPolicy ? . expiresAt ) > Date . now () && sourceCurrent ();
let timer ;
2026-10-13 21:00:41 +02:00
requestDiagnostic ( details ,{ requestType : details . resourceType ,...( source ? { sessionId : source . id } : {})}, async ()=>{
2026-10-13 16:58:15 +02:00
timer = setTimeout (()=>{ if ( called ) return ; finish ( true ); audit ( 'dlp.block' , 'headers-inspection-timeout' , new URL ( details . url ). hostname ). catch (()=>{});}, 40000 );
try {
if ( ! current () ||! agentRequestOwnerAllowed ( source ) || ( details . webContentsId > 0 && ( ! source ||! source . guarded )) ||! core . destinationAllowed ( details . url , initialPolicy ) || ( source ? . security . agentAuthority &&! actorDestinationAllowed ( details . url , source . security . agentAuthority ))){ if ( ready && policy === initialPolicy && ( ! agentRequestOwnerAllowed ( source ) || details . webContentsId > 0 && ( ! source ||! source . guarded ))) await audit ( 'navigation.block' , 'invalid-source-owner' , new URL ( details . url ). hostname ); finish ( true ); return ;}
2026-10-13 21:00:41 +02:00
const controlled = credentialBroker ? . has ( details . id ) === true ;
if ( credentialBroker && source ) await diagnosticSpan ( 'browser.credential' ,{ operation : controlled ? 'credential.headers' : 'credential.redirect' },()=>{ if ( controlled ) return credentialBroker . validateHeaders ( details , credentialContext ( source )); credentialBroker . guardIssued ( details , credentialContext ( source )); return credentialBroker . validateRedirect ( details , credentialContext ( source ));});
2026-10-13 16:58:15 +02:00
const { snapshotHeaders , inspectHeaders } = require ( './header-inspection.cjs' );
let snapshot ; try { snapshot = snapshotHeaders ( details . requestHeaders , initialPolicy );} catch ( error ){ await audit ( 'dlp.block' , error . message === 'headers-size-limit' ? 'headers-size-limit' : 'headers-uninspectable' , new URL ( details . url ). hostname ); finish ( true ); return ;}
const verdict = await diagnosticSpan ( 'browser.request.headers' ,{ bytes : snapshot . bytes , count : snapshot . count , requestType : details . resourceType },()=> inspectHeaders ( snapshot ,{ policy : initialPolicy , inspect : core . inspect , isCurrent : current ,
inspectLocal : localInspector ? ( bytes , p )=> diagnosticSpan ( 'browser.inspect.local' ,{ bytes : bytes . length },()=> localInspector . inspect ( bytes , p )) : null ,
inspectEnterprise : ( bytes , p )=> diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : bytes . length },()=> scanEnterprise ( bytes , p , 'upload' ))}));
if ( ! current ()){ if ( ready && policy === initialPolicy &&! sourceCurrent ()) await audit ( 'navigation.block' , 'header-source-context-changed' , new URL ( details . url ). hostname ); finish ( true ); return ;}
if ( verdict . decision !== 'allow' ){ await audit ( 'dlp.block' , verdict . ruleId || 'headers-inspection-incomplete' , new URL ( details . url ). hostname ); finish ( true ); return ;}
2026-10-13 21:00:41 +02:00
if ( controlled ){ await audit ( 'vault.submission-authorized' ); if ( ! current ()) throw Error ( 'Controlled login state changed' ); diagnostics . spanSync ( 'browser.credential' , diagnosticContext ({ operation : 'credential.authorize' }),()=>{ if ( ! current ()) throw Error ( 'Controlled login state changed' ); credentialBroker . finalize ( details . id , credentialContext ( source ), details ); finish ( false , snapshot . headers ); const record = credentialRequestCorrelations . get ( details . id ); if ( record ) record . released = true ;}); return ;}
2026-10-13 16:58:15 +02:00
// Chromium receives the exact immutable snapshot that was approved.
finish ( false , snapshot . headers );
} catch { if ( current ()) await audit ( 'dlp.block' , 'headers-inspection-incomplete' , new URL ( details . url ). hostname ); finish ( true );}
finally { clearTimeout ( timer );}
}). catch (()=> finish ( true ));
});
// Session request, header and permission gates are installed exactly once.
2026-10-13 10:33:31 +02:00
}
2026-10-13 18:24:44 +02:00
function interactionObserverContext ( tab ){
const contents = tab . contents || tab . view . webContents , nativeContext = tabContext ( tab . id );
return { ready : ready && currentTabContext ( nativeContext ), enabled : policy ? . evidence ? . enabled === true && policy . evidence . captureInteractions === true , policyVersion : policy ? . version || 0 , sessionId : tab . id , url : contents . getURL (), token : ` ${ tab . security . interactionDocumentGeneration } : ${ nativeContext . activation } ` , foreground : tab === activeTab () &&! tab . startPage &&! overviewPanel &&! toolsOpen , webContents : contents , nativeContext , documentGeneration : tab . security . interactionDocumentGeneration };
}
function currentInteractionObserverContext ( context ){
const tab = activeTab ();
if ( ! context ||! ready ||! tab || policy ? . evidence ? . enabled !== true || policy . evidence . captureInteractions !== true || context . policyVersion !== policy . version || Date . parse ( policy . expiresAt ) <= Date . now () || context . sessionId !== tab . id || context . webContents !== tab . contents || tab . contents . isDestroyed () || context . url !== tab . contents . getURL () || context . documentGeneration !== tab . security . interactionDocumentGeneration || tab . startPage || overviewPanel || toolsOpen ) return false ;
const live = tabs . context ( tab . id );
return ! live . paused && live . activation === context . nativeContext . activation ;
}
2026-10-13 10:33:31 +02:00
function installSiteHooks ( tab ){
const contents = tab . view . webContents , security = tab . security ;
2026-10-13 18:24:44 +02:00
security . interactionDocumentGeneration = 0 ;
security . interactionObserver = require ( './human-interaction.cjs' ). createHumanInteractionObserver ({
getContext : ()=> interactionObserverContext ( tab ), isCurrent : currentInteractionObserverContext ,
executeScript : ( target , code , operation )=> diagnosticSpan ( 'browser.evidence' ,{ operation : `evidence.observer. ${ operation } ` , sessionId : tab . id },()=> target . executeJavaScriptInIsolatedWorld ( 998 ,[{ code }], false )),
onObserved : ( event , context )=> diagnosticSpan ( 'browser.evidence' ,{ operation : 'evidence.capture' , sessionId : context . sessionId }, async ()=>{
if ( ! currentInteractionObserverContext ( context )) return ;
const payload = require ( './evidence-capture.cjs' ). buildInteractionEvidence ({... event , sessionId : context . sessionId , url : context . url , captureSource : 'human-observer' }, policy );
if ( ! payload ||! currentInteractionObserverContext ( context )) return ;
await audit ( 'browser.interaction' , undefined , new URL ( payload . pageUrl ). hostname , undefined , undefined , payload , undefined ,{ action : payload . action , sessionId : payload . sessionId , outcome : 'observed' });
}),
onDropped : async ( count , context )=>{ if ( currentInteractionObserverContext ( context )){ diagnostics . event ( 'browser.evidence' , diagnosticContext ({ operation : 'evidence.capture' , sessionId : context . sessionId , count , status : 'error' })); await audit ( 'browser.interaction.dropped' , 'interaction-capture-bounded' );}}
});
contents . on ( 'did-finish-load' ,()=> security . interactionObserver ? . refresh ());
contents . once ( 'destroyed' ,()=> security . interactionObserver ? . dispose ());
2026-10-13 10:33:31 +02:00
const developerTools = createDeveloperToolsGuard ({ contents , getContext : ()=>({ ready , foreground : tab === activeTab () &&! tab . startPage &&! overviewPanel &&! toolsOpen , policy , url : contents . getURL (), token : tabContext ( tab . id )}), isCurrent : context => currentTabContext ( context . token ), audit : kind => audit ( kind ), createFrontend : ()=>{ const frontend = new BrowserWindow ({ show : false , width : 1000 , height : 750 , title : 'S Browser · Privileged developer tools' , webPreferences : { session : browserSession , nodeIntegration : false , contextIsolation : true , sandbox : true , devTools : false }}); security . developerFrontend = frontend . webContents ; frontend . webContents . setWindowOpenHandler (()=>({ action : 'deny' })); return frontend ;}});
security . developerToolsGuard = developerTools ;
2026-10-13 16:58:15 +02:00
contents . setWindowOpenHandler (({ url })=>{ createNativeTab ({ url , agentAuthority : tab . security . agentAuthority }). catch ( e =>{ message = e . message ; emit ();}); return { action : 'deny' };});
contents . on ( 'will-navigate' ,( event , url )=>{ if ( ! ready ||! core . destinationAllowed ( url , policy ) || ( tab . security . agentAuthority &&! actorDestinationAllowed ( url , tab . security . agentAuthority ))) event . preventDefault ();});
2026-10-13 10:33:31 +02:00
contents . on ( 'will-attach-webview' , event => event . preventDefault ());
contents . on ( 'before-input-event' ,( event , input )=>{ if ( developerTools . handleInput ( event , input )) return ; const approval = security . visualApproval ; if ( policy ? . security . visualPhishing . enabled && ( ! approval || approval . generation !== tab . navigationId || approval . version !== policy . version || approval . url !== contents . getURL ())){ event . preventDefault (); checkVisual ( tab ). catch (()=>{});}});
2026-10-13 18:24:44 +02:00
contents . on ( 'did-start-navigation' ,( _event , _url , _inPlace , isMainFrame )=>{ if ( isMainFrame ){ if ( ! _inPlace ){ security . interactionDocumentGeneration ++ ; security . interactionObserver ? . refresh ();} tabs . navigation ( tab . id ); if ( _url !== 'about:blank' ) tab . startPage = false ;}});
2026-10-13 10:33:31 +02:00
contents . on ( 'did-navigate' ,()=> emit ()); contents . on ( 'did-navigate-in-page' ,()=> emit ());
contents . on ( 'did-finish-load' ,()=> pageTheme ( tab )); contents . on ( 'page-title-updated' ,()=>{ tab . title = contents . getTitle (). slice ( 0 , 200 ); if ( tab === activeTab ()) pageTitle = tab . title ; emit ();});
installClipboardControls ( contents );
security . visualScanner = createVisualPhishing ({ getContext : ()=>({ ready , policy , webContents : contents , url : contents . getURL (), navigationId : tab . navigationId }),
describe : url => visualDeadline ( contents . executeJavaScriptInIsolatedWorld ( 999 ,[{ code : describeScript ( url )}], false )),
capture : async ( scope , description )=>{ let rect ; if ( scope === 'login-form' ){ const r = description . formRect ; if ( ! r ) throw Error ( 'Login region unavailable' ); rect = { x : Math . floor ( r . x ), y : Math . floor ( r . y ), width : Math . ceil ( r . width ), height : Math . ceil ( r . height )};} const bytes = await visualDeadline ( capturePNG ( contents , rect )); if ( bytes . length < 24 ) throw Error ( 'Capture dimensions unavailable' ); return { bytes , width : bytes . readUInt32BE ( 16 ), height : bytes . readUInt32BE ( 20 )};},
fingerprint : capture =>{ try { return fingerprintPNG ( capture . bytes );} finally { capture . bytes . fill ( 0 );}}, compare : hammingDistance ,
audit : event => audit ( event . kind , event . ruleId , event . host ), block : result =>{ security . visualBlocked = {... result , message : result . reason === 'credential-brand-impersonation' ? 'Blocked: this login resembles a protected brand on an unapproved origin.' : 'Blocked: this login could not be inspected safely.' }; security . visualApproval = null ; quarantine ( security . visualBlocked . message );}
});
}
function createNativeTab ( input = {}){ return diagnosticSpan ( 'browser.tab' ,{ operation : 'create' },()=> createNativeTabImpl ( input ));}
2026-10-13 19:41:27 +02:00
async function createNativeTabImpl ( input = {}, controls = {}){
2026-10-13 10:33:31 +02:00
if ( ! input || typeof input !== 'object' || Object . keys ( input ). some ( key => ! [ 'group' , 'url' ]. includes ( key ))) throw Error ( 'Invalid native tab request' );
if ( input . group ) requireManagedSection ( 'library' );
2026-10-13 19:41:27 +02:00
controls . assertCurrent ? .(); const tab = tabs . create ({ group : input . group || '' });
2026-10-13 10:33:31 +02:00
try {
2026-10-13 19:41:27 +02:00
controls . onCreated ? .( tab ); tab . security . agentAuthority = agentActorStorage . getStore () ? . principalId && {... agentActorStorage . getStore ()}; installSiteHooks ( tab );
2026-10-13 10:33:31 +02:00
await tab . view . webContents . loadURL ( 'about:blank' );
2026-10-13 19:41:27 +02:00
controls . assertCurrent ? .();
2026-10-13 10:33:31 +02:00
await diagnosticSpan ( 'browser.clipboard' ,{ operation : 'create' , sessionId : tab . id },()=> attachPageClipboardGuard ( tab . view . webContents ,()=>{ clipboardFatal = true ; quarantine ( 'Managed clipboard protection failed; restart the browser' ); audit ( 'clipboard.block' , 'clipboard-primitive-unavailable' ); app . quit ();}));
2026-10-13 19:41:27 +02:00
controls . assertCurrent ? .(); tab . guarded = true ; selectTab ( tab . id ); controls . onActivated ? .( tab ); controls . assertCurrent ? .();
if ( input . url ){ if ( tab . security . agentAuthority ){ await agentActorStorage . run ( tab . security . agentAuthority ,()=> navigate ({ url : input . url , tabId : tab . id }, undefined ,{ signal : controls . signal }));} else await navigate ({ url : input . url , tabId : tab . id }, undefined ,{ signal : controls . signal });} controls . assertCurrent ? .(); emit (); return state ();
2026-10-13 10:33:31 +02:00
} catch ( error ){ if ( tabs . all (). includes ( tab )){ tabs . remove ( tab . id ); if ( tabs . active () ? . guarded ) selectTab ( tabs . active (). id );} throw error ;}
}
function updateManagedAppearance ( input ){ requireManagedSection ( 'appearance' ); appearancePreferences = {... appearancePreferences ,... validateAppearance ( input )}; persistAppearance (); emit (); return managedSettings ();}
async function closeNativeTab ( id ){ tabs . remove ( id ); if ( ! tabs . all (). length ) await createNativeTab (); else selectTab ( tabs . active (). id ); emit (); return state ();}
2026-10-13 16:58:15 +02:00
function groupNativeTab ( input ){ requireManagedSection ( 'library' ); if ( libraryOrganizer ? . isBusy ()) throw Error ( 'Library organization is running' ); if ( ! input || typeof input . group !== 'string' || input . group . length > 40 ) throw Error ( 'Invalid native tab group' ); tabs . get ( input . id ). group = input . group ; emit (); return state ();}
2026-10-13 19:41:27 +02:00
let sessionSnapshotStore ;
2026-10-13 21:00:41 +02:00
let credentialBroker ;
const credentialRequestCorrelations = new Map ();
function requestDiagnostic ( details , attributes , fn ){ const record = credentialRequestCorrelations . get ( details . id ); return record ? diagnostics . withCorrelation ( record . ids ,()=> diagnosticSpan ( 'browser.request' ,{... attributes , policyVersion : record . policyVersion }, fn )) : diagnosticSpan ( 'browser.request' , attributes , fn );}
function credentialContext ( tab ){
const contents = tab . contents , authority = tab . security . agentAuthority , actor = authority ? . principalId ? { agentId : authority . principalId , generation : authority . generation } : tab . security . credentialActor || { agentId : 'human-ui' , generation : 0 };
return { ready , policy , token : tabContext ( tab . id ), sessionId : tab . id , webContents : contents , mainFrame : contents . mainFrame , sourceURL : contents . getURL (), actor , vault };
}
function credentialAuthorityCurrent ( context ,{ phase }, details ){
try {
const tab = tabs . get ( context . sessionId ), contents = tab . contents , live = tabContext ( tab . id ), settings = managedSettings ();
if ( ! ready || policy !== context . policy || Date . parse ( policy . expiresAt ) <= Date . now () ||! policy . passwordManager . enabled ||! policy . passwordManager . allowAutofill ||! policy . passwordManager . controlledSubmission . enabled ||! featureAllowed ( settings , 'passwords' ) || tab !== activeTab () || contents !== context . webContents || contents . isDestroyed () || contents . mainFrame !== context . mainFrame || contents . mainFrame . processId !== context . frameProcessId || contents . mainFrame . routingId !== context . frameRoutingId || contents . getURL () !== context . sourceURL || live . activation !== context . token . activation || live . paused || vault !== context . vault || vault ? . status (). locked ) return false ;
if ( JSON . stringify ( credentialContext ( tab ). actor ) !== JSON . stringify ( context . actor )) return false ;
if ( context . actor . agentId !== 'human-ui' &&! actorGrantAllowed ( context . authority , 'vault:prepare-submit' , details ? . url || context . sourceURL )) return false ;
if ( phase === 'issued' ) return currentTabContext ( context . token );
if ( phase === 'watch' ) return tab . security . interactionDocumentGeneration === context . documentGeneration || tab . security . interactionDocumentGeneration === context . documentGeneration + 1 ;
return phase === 'redirect' ? tab . security . interactionDocumentGeneration >= context . documentGeneration + 1 : tab . security . interactionDocumentGeneration === context . documentGeneration + 1 ;
} catch { return false ;}
}
function getCredentialBroker (){ return credentialBroker ??= require ( './credential-submission.cjs' ). createCredentialSubmissionBroker ({ isCurrent : credentialAuthorityCurrent , traceOperation : ( operation , fn )=> diagnostics . spanSync ( 'browser.credential' , diagnosticContext ({ operation }), fn )});}
async function armCredentialSubmission ({ context , formId , credential , vault : current , vaultGeneration }){
const tab = tabs . get ( context . sessionId || context . token . tabId ), actor = context . actor || { agentId : 'human-ui' , generation : 0 }; tab . security . credentialActor = Object . freeze ({... actor });
const snapshot = {... credentialContext ( tab ), documentGeneration : tab . security . interactionDocumentGeneration , frameProcessId : tab . contents . mainFrame . processId , frameRoutingId : tab . contents . mainFrame . routingId , vault : current , authority : actor . agentId === 'human-ui' ? undefined : actor . agentId === 'legacy-paired-agent' ? { action : 'vault:prepare-submit' } : { principalId : actor . agentId , generation : actor . generation , action : 'vault:prepare-submit' }};
const result = await diagnosticSpan ( 'browser.credential' ,{ operation : 'credential.issue' , sessionId : tab . id },()=> getCredentialBroker (). issue ({ context : snapshot , formId , username : credential . username , password : credential . password , vault : current , vaultGeneration }));
await audit ( 'vault.submission-prepared' ); if ( ! credentialAuthorityCurrent ( snapshot ,{ phase : 'issued' })){ getCredentialBroker (). cancel ( snapshot ); throw Error ( 'Controlled login state changed' );} return {... result , cancel : ()=> getCredentialBroker (). cancel ( snapshot )};
}
async function handoffVault (){
const token = require ( 'node:crypto' ). randomUUID ();
await new Promise (( resolve , reject )=>{ const handler = ( event , value )=>{ if ( win &&! win . isDestroyed () && event . sender === win . webContents && event . senderFrame === win . webContents . mainFrame && event . senderFrame ? . url === uiUrl && value === token ){ clearTimeout ( timer ); ipcMain . removeListener ( 'managed:vault-handoff-ack' , handler ); resolve ();}}; const timer = setTimeout (()=>{ ipcMain . removeListener ( 'managed:vault-handoff-ack' , handler ); reject ( Error ( 'Trusted vault interface unavailable' ));}, 1500 ); ipcMain . on ( 'managed:vault-handoff-ack' , handler ); win . webContents . send ( 'managed:vault-handoff' , token );});
}
2026-10-13 19:41:27 +02:00
function agentSessionControls (){
requireEncryption (); sessionSnapshotStore ??= require ( './session-snapshot.cjs' ). createSessionSnapshotStore ({ filePath : path . join ( app . getPath ( 'userData' ), 'managed-sessions.bin' ), protect : bytes => safeStorage . encryptString ( bytes . toString ( 'base64' )), unprotect : bytes => Buffer . from ( safeStorage . decryptString ( bytes ), 'base64' )});
const sessionContext = id =>{ const context = agentContext ( id ), actor = agentActorStorage . getStore (); return {... context , ready : context . ready && actorGrantAllowed ( actor , actor ? . action ), actor : context . actor || Object . freeze ({ agentId : 'legacy-paired-agent' , generation : 1 })};};
return require ( './agent-session-adapters.cjs' ). createAgentSessionAdapters ({ getContext : sessionContext , getSessions : ()=> tabs . all (). map ( tab =>({ id : tab . id , title : tab . title , group : tab . group })), contextCurrent : currentTabContext ,
getOwner : context =>({ deviceId : device . id , ownerId : JSON . stringify ([ policy . agents . identityMode , context . actor . agentId , context . actor . generation ])}), store : sessionSnapshotStore , inspect : inspectManagedAgentText ,
traceOperation : ( operation , fn )=> diagnosticSpan ( 'browser.session' ,{ operation : 'sessions.' + operation }, fn ),
restore : ( collection , options )=> require ( './session-restore.cjs' ). restoreNativeSession ( collection , options ,{ registry : tabs , store : sessionSnapshotStore ,
create : async ( input , controls )=>{ const result = await diagnosticSpan ( 'browser.tab' ,{ operation : 'create' },()=> createNativeTabImpl ( input , controls )); return result . activeTabId ;}, activate : id =>{ selectTab ( id ); emit ();}, traceOperation : ( operation , fn )=> diagnosticSpan ( 'browser.session' ,{ operation : 'sessions.' + operation }, fn )})
});
}
2026-10-13 10:33:31 +02:00
function agentAppControls (){
2026-10-13 18:48:40 +02:00
const actorIdentity = ()=>{ const actor = agentActorStorage . getStore (); return actor ? . principalId ? ` ${ actor . principalId } : ${ actor . generation } ` : 'legacy' ;};
const windowControls = require ( './window-controls.cjs' ). createWindowControls ({ getWindow : ()=> win , getContext : ()=>({ ready : ready && featureAllowed ( managedSettings (), 'agents' ), policy , policyVersion : policy ? . version , deviceId : device ? . id , actor : actorIdentity ()}), isCurrent : context => ready && policy === context . policy && device ? . id === context . deviceId && actorIdentity () === context . actor , authorize : action => actorGrantAllowed ( agentActorStorage . getStore (), action ), traceOperation : ( operation , fn )=> diagnosticSpan ( 'browser.window' ,{ operation }, fn )});
2026-10-13 19:14:32 +02:00
const vaultControls = require ( './vault-agent-controls.cjs' ). createVaultAgentControls ({
getContext : id =>{ const context = agentContext ( id ); return {... context , ready : context . ready && featureAllowed ( managedSettings (), 'passwords' ), actor : context . actor || Object . freeze ({ agentId : 'legacy-paired-agent' , generation : 1 })};},
contextCurrent : currentTabContext , getVault : requireVault , checkVisual : context => checkVisual ( tabs . get ( context . sessionId )), inspect : inspectManagedAgentText ,
2026-10-13 21:00:41 +02:00
beforeFill : handoffVault , afterFill : ()=>{ vaultPanel = false ; emit ();}, prepareSubmission : armCredentialSubmission ,
2026-10-13 19:14:32 +02:00
audit : event => audit ( event . action ), traceOperation : ( operation , fn )=> diagnosticSpan ( 'browser.vault' ,{ operation }, fn )
});
2026-10-13 10:33:31 +02:00
const checked = ( operation )=> async ( args ,{ signal , policyVersion } = {})=>{
if ( signal ? . aborted ||! ready || policy ? . version !== policyVersion ||! featureAllowed ( managedSettings (), 'agents' )) throw Error ( 'Managed application changed' );
2026-10-13 16:58:15 +02:00
return operation ( args ,{ signal });
2026-10-13 10:33:31 +02:00
};
return {
2026-10-13 19:14:32 +02:00
'vault:status' : checked (( args , options )=> vaultControls . status ( args , options )),
'vault:list' : checked (( args , options )=> vaultControls . list ( args , options )),
'vault:fill' : checked (( args , options )=> vaultControls . fill ( args , options )),
2026-10-13 21:00:41 +02:00
'vault:prepare-submit' : checked (( args , options )=> vaultControls . prepare ( args , options )),
2026-10-13 18:48:40 +02:00
'window:state' : checked (( args , options )=> windowControls . state ( args , options )),
'window:control' : checked (( args , options )=> windowControls . control ( args , options )),
2026-10-13 10:33:31 +02:00
'application:state' : checked (()=>({ ready , policyVersion : policy . version , activeTabId : activeTab () ? . id , tabCount : tabs . all (). length })),
'settings:get' : checked (()=> managedSettings ()),
'settings:update' : checked ( args => updateManagedAppearance ( args . appearance )),
2026-10-13 16:58:15 +02:00
'bookmarks:list' : checked (()=>{ const result = bookmarkState ({ policy : effectiveBookmarkPolicy (), ready , settings : managedSettings ()}); if ( agentActorStorage . getStore () ? . principalId ){ result . managed = result . managed . filter ( row => actorDestinationAllowed ( row . url )); result . personal . items = result . personal . items . filter ( row => actorDestinationAllowed ( row . url ));} return {... result , policyVersion : policy . version };}),
2026-10-13 10:33:31 +02:00
'bookmarks:mutate' : checked ( async args =>{ await mutateManagedBookmarks ( args ); return { completed : true };}),
2026-10-13 16:58:15 +02:00
'library:organize-plan' : checked (( _args , options )=> organizeLibrary ( options )),
'library:organize-apply' : checked (( args , options )=> applyLibraryOrganization ( args , options )),
2026-10-13 10:33:31 +02:00
'tabs:create' : checked ( async args =>{ const result = await createNativeTab ( args ); return { completed : true , sessionId : result . activeTabId };}),
'tabs:activate' : checked ( args =>{ selectTab ( args . sessionId ); emit (); return { completed : true , sessionId : args . sessionId };}),
'tabs:close' : checked ( async args =>{ await closeNativeTab ( args . sessionId ); return { completed : true };}),
'tabs:group' : checked ( args =>{ groupNativeTab ({ id : args . sessionId , group : args . group }); return { completed : true , sessionId : args . sessionId };})
};
}
function ipc ( channel , fn ){ ipcMain . handle ( channel , async ( event ,... args )=>{
if ( event . sender !== win . webContents || event . senderFrame !== win . webContents . mainFrame || event . senderFrame ? . url !== uiUrl ) throw Error ( 'Untrusted IPC sender' );
try { return { ok : true , data : await diagnosticSpan ( 'browser.ipc' ,{ operation : channel . slice ( 'managed:' . length )},()=> fn (... args ))};} catch ( e ){ return { ok : false , error : e . message };}
});}
app . whenReady (). then ( async ()=>{
if ( ! ownsBrowserInstance ) return ;
if ( cliRequest ){ try { await require ( './agent-client.cjs' ). runClient ( cliConnection , cliRequest );} catch { console . error ( 'Local agent action failed. Check policy and encrypted pairing file.' ); app . exit ( 1 );} return ;}
core = await import ( '../../packages/core/policy.mjs' );
clipboardCore = await import ( '../../packages/core/clipboard.mjs' );
ipcMain . on ( 'managed:clipboard-sandbox-failure' , event =>{ if ( tabs ? . fromContents ( event . sender . id ) || event . sender === win ? . webContents ){ clipboardFatal = true ; quarantine ( 'Managed renderer security failed. Restart this browser.' );}});
powerMonitor . on ( 'lock-screen' , lockVault ); powerMonitor . on ( 'suspend' , lockVault );
securityCore = await import ( '../../packages/core/security.mjs' );
2026-10-13 16:58:15 +02:00
localInspector = startInspector ( app . isPackaged ? process . resourcesPath : path . join ( __dirname , '../..' ), app . isPackaged ,{ onStages : stages => diagnostics . importStages ( stages )});
2026-10-13 10:33:31 +02:00
browserSession = session . fromPartition ( 'persist:safetynet-managed' ,{ cache : false });
// Management has its own cookie jar and no website request gate. It uses the
// same company proxy and Chromium DNS configuration as managed website traffic.
managementSession = session . fromPartition ( 'persist:safetynet-management' ,{ cache : false });
browserSession . registerPreloadScript ({ type : 'frame' , id : 'managed-clipboard-deny' , filePath : path . join ( __dirname , 'clipboard-deny.cjs' )});
session . defaultSession . registerPreloadScript ({ type : 'frame' , id : 'shell-clipboard-deny' , filePath : path . join ( __dirname , 'clipboard-deny.cjs' )});
Menu . setApplicationMenu ( null );
win = new BrowserWindow ({ width : 1320 , height : 900 , minWidth : 800 , minHeight : 600 , frame : false , roundedCorners : true , hasShadow : true , backgroundMaterial : 'acrylic' , backgroundColor : '#eef1f5' , autoHideMenuBar : true , webPreferences : { preload : path . join ( __dirname , 'preload.cjs' ), nodeIntegration : false , contextIsolation : true , sandbox : true , devTools : false }});
win . removeMenu ();
2026-10-13 18:24:44 +02:00
tabs = createTabRegistry ({ createView : ()=> new WebContentsView ({ webPreferences : { session : browserSession , preload : path . join ( __dirname , 'site-preload.cjs' ), nodeIntegration : false , nodeIntegrationInSubFrames : true , contextIsolation : true , sandbox : true , webSecurity : true , allowRunningInsecureContent : false }}), attach : v => win . contentView . addChildView ( v ), detach : v => win . contentView . removeChildView ( v ), dispose : tab =>{ tab . security . interactionObserver ? . dispose (); disposePageClipboardGuard ( tab . contents );}});
2026-10-13 10:33:31 +02:00
const executeClipboard = ( mode , args )=> args . webContents . executeJavaScriptInIsolatedWorld ( 999 ,[{ code : clipboardScript ( mode , args )}], false );
clipboardGuard = createClipboardGuard ({
getContext : contents =>{ const target = contents || view ? . webContents , tab = target && tabs . fromContents ( target . id ), site =!! tab , url = target ? . getURL () || '' , token = site ? tabContext ( tab . id ) : null ; return { ready : ready && target ? . isFocused () && ( site ? tab === activeTab () &&! tab . startPage &&! overviewPanel &&! toolsOpen && core . destinationAllowed ( url , policy ) : target === win . webContents && url === uiUrl ), policy , webContents : target , url , navigationId : site ? tab . navigationId : 0 , tabId : tab ? . id , activation : token ? . activation };},
stage : async args =>{ const tab = tabs . fromContents ( args . webContents . id ); if ( tab && args . operation === 'paste' && policy ? . security . visualPhishing . enabled && ( await checkVisual ( tab )). decision !== 'allow' ) throw Error ( 'Login protection holds paste' ); return executeClipboard ( 'stage' , args );},
validate : args => executeClipboard ( 'validate' , args ), commit : args => executeClipboard ( 'commit' , args ),
discard : args => executeClipboard ( 'discard' ,{... args , url : '' , operation : 'copy' , limit : 0 }),
readClipboard : async limit =>{ const items = await clipboard . read (); if ( items . length !== 1 ) throw Error ( 'Clipboard format unsupported' ); const item = items [ 0 ]; if ( ! item . types . includes ( 'text/plain' ) || item . types . some ( type => type !== 'text/plain' &&! /^electron application\/osclipboard;format="(?:CF_TEXT|CF_UNICODETEXT|CF_LOCALE)"$/ . test ( type ))) throw Error ( 'Rich clipboard denied' ); const blob = await item . getType ( 'text/plain' ); if ( blob . size > limit ) throw Error ( 'Clipboard size limit' ); return blob . text ();},
writeClipboard : text => clipboard . writeText ( text ),
inspect : async ( bytes , currentPolicy , operation )=>{ let verdict = clipboardCore . inspectClipboard ( bytes , currentPolicy , operation ); if ( verdict . decision !== 'allow' ) return verdict ; if ( currentPolicy . dlp . semantic . enabled || currentPolicy . security . yara . enabled ){ if ( ! localInspector ) return { decision : 'block' , ruleId : 'clipboard-inspector-unavailable' }; verdict = await diagnosticSpan ( 'browser.inspect.local' ,{ bytes : bytes . length },()=> localInspector . inspect ( bytes , currentPolicy ));} if ( verdict . decision !== 'allow' ) return verdict ; const external = await diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : bytes . length },()=> scanEnterprise ( bytes , currentPolicy , 'clipboard' )); return external . decision === 'allow' ? verdict : external ;},
2026-10-13 18:04:02 +02:00
onCompleted : async metadata =>{
const payload = await diagnosticSpan ( 'browser.evidence' ,{ operation : 'evidence.capture' , sessionId : metadata . sessionId },()=> require ( './evidence-capture.cjs' ). buildInteractionEvidence ({... metadata , url : metadata . pageUrl , captureSource : 'guarded-clipboard' }, policy ));
await audit ( `clipboard. ${ metadata . action } ` , undefined , undefined , undefined , undefined , payload , undefined ,{ action : metadata . action , sessionId : metadata . sessionId , outcome : 'completed' });
},
audit : async ( kind , ruleId , facts )=>{ const parsed = require ( '../../packages/core/interaction-facts.cjs' ). InteractionFactsSchema . safeParse ( facts ); await audit ( kind , ruleId , undefined , undefined , undefined , undefined , undefined , parsed . success ? parsed . data : undefined ); if ( kind === 'clipboard.block' && ready ){ message = `Clipboard blocked by company policy ( ${ ruleId } ).` ; emit ();}}
2026-10-13 10:33:31 +02:00
});
installClipboardControls ( win . webContents ); hooks ();
ipcMain . on ( 'managed:visual-policy' , event =>{ event . returnValue = { enabled :!! tabs . fromContents ( event . sender . id ) &&!! policy ? . security . visualPhishing . enabled };});
ipcMain . handle ( 'managed:visual-check' , async ( event , input )=>{
const tab = tabs . fromContents ( event . sender . id ); if ( ! tab || event . senderFrame !== event . sender . mainFrame ||! input || typeof input . token !== 'string' || input . token . length > 80 ||! Number . isSafeInteger ( input . revision ) || input . revision < 0 || input . url !== event . sender . getURL ()) return { decision : 'unknown' };
const security = tab . security ; if ( input . token !== security . visualToken || input . revision !== security . visualRevision ){ tabs . navigation ( tab . id ); security . visualToken = input . token ; security . visualRevision = input . revision ;}
try { return await checkVisual ( tab );} catch { return { decision : 'unknown' };}
});
await createNativeTab ();
win . webContents . setWindowOpenHandler (()=>({ action : 'deny' })); win . webContents . on ( 'will-navigate' , event => event . preventDefault ());
ipc ( 'managed:state' ,()=>({... state (), pageTitle , pageColor , windowMaximized : win . isMaximized ()}));
ipc ( 'managed:window-control' , action =>{ if ( action === 'minimize' ) win . minimize (); else if ( action === 'maximize' ){ win . isMaximized () ? win . unmaximize () : win . maximize ();} else if ( action === 'close' ) win . close (); else throw Error ( 'Unknown window action' );});
ipc ( 'managed:authorize-setting' , section =>{ requireManagedSection ( section ); return { authorized : true , policyVersion : policy . version };});
ipc ( 'managed:bookmarks-mutate' , mutateManagedBookmarks );
2026-10-13 16:58:15 +02:00
ipc ( 'managed:library-organize' , organizeLibrary );
ipc ( 'managed:library-apply' , applyLibraryOrganization );
2026-10-13 10:33:31 +02:00
ipc ( 'managed:settings-update' , updateManagedAppearance );
ipc ( 'managed:chrome-layout' , async input =>{
if ( ! input ||! [ 64 , 104 ]. includes ( input . height ) || [ 'sidebarOpen' , 'startPageOpen' , 'overviewOpen' ]. some ( key => typeof input [ key ] !== 'boolean' )) throw Error ( 'Unsupported browser layout' );
if ( managedSettings (). available &&! sectionAllowed ( managedSettings (), 'appearance' ) && input . height !== ( managedSettings (). appearance . tabMode === 'compact' ? 64 : 104 )) throw Error ( 'Tab layout is controlled by your administrator' );
if ( input . toolsOpen &&! managedSettings (). visible ) throw Error ( 'Settings access is disabled by your administrator' );
if ( input . sidebarOpen &&! libraryVisible ( managedSettings ())) throw Error ( 'Library access is controlled by your administrator' );
if ( input . toolsOpen !== undefined && typeof input . toolsOpen !== 'boolean' ) throw Error ( 'Unsupported tools layout' ); chromeHeight = input . height ; leftSidebar = input . sidebarOpen ; startPage = activeTab () ? . startPage ?? true ; overviewPanel = input . overviewOpen ; toolsOpen =!! input . toolsOpen ; layout ();
if ( policy ? . security . visualPhishing . enabled ) await configureVisualPage ( true , activeTab ()); return { height : chromeHeight };
});
ipc ( 'managed:share-url' , async ()=>{
if ( ! ready ) throw Error ( 'Browser access paused' ); const initial = policy , context = tabContext (), contents = context . webContents , pageURL = contents . getURL (), url = new URL ( pageURL );
if ( ! [ 'https:' , 'http:' ]. includes ( url . protocol ) || url . username || url . password ) throw Error ( 'Cannot share this page' ); url . search = '' ; url . hash = '' ; const text = url . toString (), bytes = Buffer . from ( text );
let verdict = clipboardCore . inspectClipboard ( bytes , initial , 'copy' );
if ( verdict . decision === 'allow' && ( initial . dlp . semantic . enabled || initial . security . yara . enabled )) verdict = localInspector ? await diagnosticSpan ( 'browser.inspect.local' ,{ bytes : bytes . length },()=> localInspector . inspect ( bytes , initial )) : { decision : 'block' , ruleId : 'clipboard-inspector-unavailable' };
if ( verdict . decision === 'allow' ) verdict = await diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : bytes . length },()=> scanEnterprise ( bytes , initial , 'clipboard' ));
if ( verdict . decision !== 'allow' ){ await audit ( 'clipboard.block' , verdict . ruleId ); throw Error ( 'Company policy blocks sharing this link' );}
await audit ( 'clipboard.copy' , 'authorized-link' );
if ( ! ready || policy !== initial || Date . parse ( initial . expiresAt ) <= Date . now () ||! currentTabContext ( context )) throw Error ( 'Browser state changed during link inspection' );
await clipboard . writeText ( text ); return { copied : true };
});
ipc ( 'managed:enroll' , async input =>{
if ( device ) throw Error ( 'Already enrolled' ); requireEncryption ();
if ( ! input || typeof input . server !== 'string' || typeof input . code !== 'string' || input . code . length > 200 ) throw Error ( 'Invalid enrollment' );
const server = managementUrl ( input . server ), result = await request ( server , '/api/device/enroll' ,{ code : input . code , name : os . hostname (), platform : process . platform });
return completeEnrollment ( server , result );
});
ipc ( 'managed:sso' , async input =>{
if ( device ) throw Error ( 'Already enrolled' ); requireEncryption (); const server = managementUrl ( input . server );
const ticket = await request ( server , '/api/device/sso/start' ,{});
const loginUrl = new URL ( ticket . url ); if ( loginUrl . origin !== server || loginUrl . pathname !== '/auth/login' ) throw Error ( 'Invalid login URL' );
await shell . openExternal ( loginUrl . toString ());
message = 'Complete enterprise sign-in in your system browser…' ; emit ();
for ( let attempt = 0 ; attempt < 100 ; attempt ++ ){
await new Promise ( resolve => setTimeout ( resolve , 3000 ));
const result = await request ( server , '/api/device/sso/poll' ,{ id : ticket . id , secret : ticket . secret , name : os . hostname (), platform : process . platform });
if ( ! result . pending ) return completeEnrollment ( server , result );
}
throw Error ( 'Enterprise sign-in expired' );
});
ipc ( 'managed:tab-create' , createNativeTab );
ipc ( 'managed:tab-activate' , id =>{ selectTab ( id ); emit (); return state ();});
ipc ( 'managed:tab-close' , closeNativeTab );
ipc ( 'managed:tab-group' , groupNativeTab );
ipc ( 'managed:navigate' , navigate );
ipc ( 'managed:back' ,()=>{ if ( ready && view . webContents . navigationHistory . canGoBack ()) view . webContents . navigationHistory . goBack ();});
ipc ( 'managed:forward' ,()=>{ if ( ready && view . webContents . navigationHistory . canGoForward ()) view . webContents . navigationHistory . goForward ();});
ipc ( 'managed:reload' ,()=>{ if ( ready ) view . webContents . reload ();});
ipc ( 'managed:ai-panel' , open =>{ if ( open ) requireFeatureSection ( 'ai' ); aiPanel =!! open ; if ( aiPanel ){ vaultPanel = false ; lockVault ();} layout ();});
ipc ( 'managed:vault-panel' , open =>{ if ( open ) requireVault (); vaultPanel =!! open ; if ( vaultPanel ) aiPanel = false ; else lockVault (); layout ();});
ipc ( 'managed:vault-status' ,()=>{ requireFeatureSection ( 'passwords' ); return vaultStatus ();});
ipc ( 'managed:vault-create' , async master =>{ requireVault (). create ( master ); await audit ( 'vault.created' ); if ( ! ready ){ lockVault (); throw Error ( 'Browser access paused' );} emit (); return vaultStatus ();});
ipc ( 'managed:vault-unlock' , async master =>{ requireVault (). unlock ( master ); await audit ( 'vault.unlocked' ); if ( ! ready ){ lockVault (); throw Error ( 'Browser access paused' );} emit (); return vaultStatus ();});
ipc ( 'managed:vault-lock' ,()=>{ lockVault (); return vaultStatus ();});
ipc ( 'managed:vault-list' ,()=> requireVault (). list ());
ipc ( 'managed:vault-save' , async entry =>{ if ( ! vaultSettings (). allowSaving ) throw Error ( 'Saving passwords is disabled by company policy' ); const result = requireVault (). upsert ( entry ); await audit ( 'vault.saved' ); emit (); return result ;});
ipc ( 'managed:vault-delete' , async id =>{ if ( ! vaultSettings (). allowSaving ) throw Error ( 'Password changes are disabled by company policy' ); const result = requireVault (). delete ( id ); await audit ( 'vault.deleted' ); return result ;});
ipc ( 'managed:vault-generate' ,()=>{ const current = requireVault (); if ( ! vaultSettings (). allowSaving || current . status (). locked ) throw Error ( 'Unlock the vault before generating a password' ); return current . generatePassword ();});
2026-10-13 21:00:41 +02:00
const fillManagedVault = async ( id , controlled = false )=>{
2026-10-13 10:33:31 +02:00
const context = tabContext (), tab = tabs . get ( context . tabId ), contents = context . webContents ;
if ( startPage || overviewPanel || toolsOpen ) throw Error ( 'Show the saved login website before filling credentials' );
if (( await checkVisual ( tab )). decision !== 'allow' ||! currentTabContext ( context )) throw Error ( 'Visual login protection has not approved this page' );
if ( startPage || overviewPanel || toolsOpen ) throw Error ( 'The login website is no longer visible' );
const current = requireVault (), initial = policy , url = context . url ;
if ( ! initial . passwordManager . allowAutofill ||! core . destinationAllowed ( url , initial ) || new URL ( url ). protocol !== 'https:' ) throw Error ( 'Password filling requires an approved HTTPS destination' );
2026-10-13 21:00:41 +02:00
if ( controlled &&! initial . passwordManager . controlledSubmission . enabled ) throw Error ( 'Controlled login submission is disabled by company policy' );
2026-10-13 10:33:31 +02:00
let credential ;
try {
const origin = new URL ( url ). origin ; credential = {... current . credential ( id , origin ), origin };
await audit ( 'vault.fill-requested' );
if ( ! currentTabContext ( context ) || startPage || overviewPanel || toolsOpen || policy !== initial || Date . parse ( initial . expiresAt ) <= Date . now () || current . status (). locked ) throw Error ( 'Browser state changed' );
2026-10-13 21:00:41 +02:00
await handoffVault (); if ( ! currentTabContext ( context ) || policy !== initial || current . status (). locked ) throw Error ( 'Browser state changed' ); tab . security . agentAuthority = null ; tab . security . credentialActor = Object . freeze ({ agentId : 'human-ui' , generation : 0 });
2026-10-13 10:33:31 +02:00
contents . focus (); if ( ! currentTabContext ( context )) throw Error ( 'Browser state changed' );
2026-10-13 21:00:41 +02:00
const code = controlled ? require ( './password-fill.cjs' ). prepareFillScript ( url , credential , initial . passwordManager . controlledSubmission . forms , Date . parse ( initial . expiresAt )) : fillScript ( url , credential , Date . parse ( initial . expiresAt ));
const result = await diagnosticSpan ( 'browser.password.fill' ,{ sessionId : context . tabId },()=> contents . executeJavaScriptInIsolatedWorld ( 999 ,[{ code }], false ));
2026-10-13 10:33:31 +02:00
if ( ! currentTabContext ( context ) || startPage || overviewPanel || toolsOpen || policy !== initial || current . status (). locked || result ? . completed !== true ) throw Error ( 'Browser state changed' );
2026-10-13 21:00:41 +02:00
await audit ( 'vault.filled' ); if ( ! currentTabContext ( context ) || policy !== initial || current . status (). locked ) throw Error ( 'Browser state changed' );
if ( controlled ) await armCredentialSubmission ({ context : { sessionId : tab . id , token : context }, formId : result . formId , credential , vault : current , vaultGeneration : current . accessGeneration ()});
vaultPanel = false ; emit (); return { completed : true ,...( controlled ? { prepared : true } : {})};
} catch { credentialBroker ? . revoke (); vaultPanel = false ; emit (); throw Error ( 'Cannot fill this login. Unlock the vault and use its exact approved HTTPS origin with a single visible same-origin POST login form.' );}
2026-10-13 10:33:31 +02:00
finally { if ( credential ){ credential . username = '' ; credential . password = '' ;}}
2026-10-13 21:00:41 +02:00
};
ipc ( 'managed:vault-fill' , id => fillManagedVault ( id ));
ipc ( 'managed:vault-prepare-submit' , id => fillManagedVault ( id , true ));
2026-10-13 10:33:31 +02:00
ipc ( 'managed:ai' , managedAI );
2026-10-13 16:58:15 +02:00
ipc ( 'managed:agent-connection' , async principalId =>{
2026-10-13 10:33:31 +02:00
requireFeatureSection ( 'agents' );
2026-10-13 16:58:15 +02:00
if ( ! ready ||! policy . agents . enabled ||! agentService ) throw Error ( 'Agent access is disabled by company policy' ); if ( policy . agents . identityMode === 'individual' ) return pairIndividualAgent ( principalId ); if ( principalId !== undefined ) throw Error ( 'Individual agent selection requires individual mode' ); return { address : agentService . address , credentialPath : agentFile ()};
2026-10-13 10:33:31 +02:00
});
ipcMain . on ( 'managed:page-content' , async ( event , input )=>{
const tab = tabs . fromContents ( event . sender . id ); if ( ! tab || event . senderFrame !== event . sender . mainFrame ||! ready || typeof input ? . text !== 'string' || input . text . length > 250000 ) return ;
const initial = policy , context = tabContext ( tab . id ,{ foreground : false }); if ( ! initial . security . content . blockedPhrases . length ) return ;
const result = core . inspect ( Buffer . from ( input . text ), initial ); if ( result . decision === 'block' && result . ruleId ? . startsWith ( 'content-' ) && currentTabContext ( context ) && policy === initial ){ await audit ( 'content.block' , result . ruleId ); if ( ! currentTabContext ( context )) return ; tab . security . contentBlocked = { checkedURL : context . url , policyVersion : initial . version , message : 'This page contains content blocked by company policy.' }; quarantine ( tab . security . contentBlocked . message );}
});
ipc ( 'managed:inspect-file' , async ()=>{
requireFeatureSection ( 'documentScan' );
if ( ! ready ||! policy ) throw Error ( 'Device must be managed before inspecting files' );
if ( policy . inspection ? . documentAnalysisEnabled === false ) throw Error ( 'Document analysis is disabled for this browser by your administrator' );
if ( ! localInspector ) throw Error ( 'Local content inspector is not installed' );
const currentPolicy = policy ;
const selection = await dialog . showOpenDialog ( win ,{ title : icapRequired ( policy , 'upload' ) ? 'Inspect content with company security services' : 'Inspect company content locally' , properties : [ 'openFile' ]}); if ( selection . canceled ) return null ;
requireFeatureSection ( 'documentScan' );
if ( ! ready || policy !== currentPolicy ) throw Error ( 'Policy changed during file selection; inspect again' );
const file = selection . filePaths [ 0 ], handle = await fs . promises . open ( file , 'r' ); let bytes ;
try { const info = await handle . stat (); if ( ! info . isFile () || info . size > policy . dlp . maxBytes ) throw Error ( 'File exceeds company inspection limit' );
// Read a bounded immutable snapshot, never release the subsequently mutable original.
const chunks = []; let size = 0 ; for await ( const chunk of handle . createReadStream ()){ size += chunk . length ; if ( size > policy . dlp . maxBytes ) throw Error ( 'File exceeds company inspection limit' ); chunks . push ( chunk );} bytes = Buffer . concat ( chunks );
} finally { await handle . close ();}
if ( ! ready || policy !== currentPolicy ){ bytes . fill ( 0 ); throw Error ( 'Policy changed during file selection; inspect again' );}
let result ;
try { result = await diagnosticSpan ( 'browser.inspect.local' ,{ bytes : bytes . length },()=> localInspector . inspect ( bytes , currentPolicy )); if ( result . decision === 'allow' && icapRequired ( currentPolicy , 'upload' )){ const external = await diagnosticSpan ( 'browser.inspect.enterprise' ,{ bytes : bytes . length },()=> scanEnterprise ( bytes , currentPolicy , 'upload' )); if ( external . decision !== 'allow' ) result = external ;}} finally { bytes . fill ( 0 );}
if ( ! ready || currentPolicy . version !== policy . version ) throw Error ( 'Policy changed during inspection; inspect again' );
if ( result . decision === 'block' ) await audit ( 'dlp.block' , result . ruleId );
return { name : path . basename ( file ),... result };
});
win . on ( 'close' ,()=> tabs . close ());
2026-10-13 21:00:41 +02:00
win . on ( 'resize' , layout ); win . on ( 'maximize' , emit ); win . on ( 'unmaximize' , emit ); win . on ( 'blur' , lockVault ); win . on ( 'minimize' , lockVault ); win . on ( 'closed' ,()=>{ credentialBroker ? . close (); vault ? . lock (); clearInterval ( vaultTimer ); win = null ; clearInterval ( syncTimer ); localInspector ? . stop (); agentService ? . close ();});
2026-10-13 10:33:31 +02:00
loadAppearance ();
await win . loadFile ( path . join ( __dirname , 'index.html' )); layout ();
2026-10-13 16:58:15 +02:00
try { device = loadDevice (); const queuePath = path . join ( app . getPath ( 'userData' ), 'audit-queue.bin' ); if ( device && fs . existsSync ( queuePath )){ auditQueue = require ( '../../packages/core/request-transfer-facts.cjs' ). migrateQueuedRequestFacts ( JSON . parse ( safeStorage . decryptString ( fs . readFileSync ( queuePath )))); persistAudit ();} if ( device ){ await sync (); await audit ( 'browser.started' );}} catch ( e ){ quarantine ( `Enrollment could not load: ${ e . message } ` );}
2026-10-13 10:33:31 +02:00
const refreshMs =! app . isPackaged && process . env . SAFETYNET_SMOKE_USERDATA === app . getPath ( 'userData' ) ? 1000 : 15000 ;
syncTimer = setInterval (()=>{ if ( policy && Date . parse ( policy . expiresAt ) <= Date . now ()) quarantine ( 'Company policy expired' ); sync ();}, refreshMs );
networkTimer = setInterval ( async ()=>{
if ( ! policy ? . enterprise ? . vpn . required || networkChecking ) return ; networkChecking = true ;
const checkedPolicy = policy ;
try { const posture = await diagnosticSpan ( 'browser.network' ,{ operation : 'scan' },()=> networkPosture . check ( checkedPolicy . enterprise . vpn )); if ( policy === checkedPolicy &&! posture . allowed ){ managementHealthy = false ; quarantine ( 'Required corporate network is unavailable. Connect your company VPN.' ); await Promise . all ([ browserSession . closeAllConnections (), managementSession . closeAllConnections ()]);}}
finally { networkChecking = false ;}
}, 5000 );
let wasLocked = true ; vaultTimer = setInterval (()=>{ const locked = vault ? . status (). locked ?? true ; if ( locked !== wasLocked ){ if ( locked ) audit ( 'vault.locked' ); wasLocked = locked ; emit ();}}, 1000 );
}). catch ( error =>{ console . error ( error . stack ); app . quit ();});
app . on ( 'window-all-closed' ,()=> app . quit ());
2026-10-13 16:58:15 +02:00
app . on ( 'before-quit' ,()=>{ fileUploadGate ? . dispose (); tabs ? . close ();});
2026-10-13 10:33:31 +02:00
app . on ( 'will-quit' ,()=>{ Promise . resolve ( diagnostics . shutdown ()). catch (()=>{}); vault ? . lock (); clearInterval ( vaultTimer ); clearInterval ( networkTimer ); localInspector ? . stop (); agentService ? . close ();});
app . on ( 'certificate-error' ,( event , _wc , _url , _error , _certificate , callback )=>{ event . preventDefault (); callback ( false );});
app . on ( 'login' ,( event , _wc , _details , authInfo , callback )=>{
event . preventDefault ();
// Proxy credentials are supplied by deployment environment, never a page or policy bundle.
if ( authInfo . isProxy && process . env . SAFETYNET_PROXY_USER && process . env . SAFETYNET_PROXY_PASSWORD ) callback ( process . env . SAFETYNET_PROXY_USER , process . env . SAFETYNET_PROXY_PASSWORD );
else callback ();
});