2026-10-13 23:19:22 +02:00
import { hardenedElectronFuses , verifyPackagedFuses } from './package-fuses.mjs' ;
2026-10-14 14:18:40 +02:00
import { spawnSync } from 'node:child_process' ;
import { readFileSync , readdirSync , writeFileSync , createReadStream } from 'node:fs' ;
import { createHash , randomUUID } from 'node:crypto' ;
2026-10-13 10:33:31 +02:00
import { createRequire } from 'node:module' ;
import path from 'node:path' ;
2026-10-14 14:18:40 +02:00
import { provisionBrokerRuntime , BROKER_RUNTIME } from './provision-broker-runtime.mjs' ;
import { prepareBrokerResources , verifyPackagedBroker , embeddedBrokerManifestReader , snapshotScannerResources , verifyScannerResources } from './package-broker-resources.mjs' ;
import { createWindowsPackagePlan } from './windows-package-plan.mjs' ;
import { buildVerifiedInstaller } from './verified-installer.mjs' ;
const platform = process . argv [ 2 ];
if ( platform !== 'win' || process . platform !== 'win32' || process . argv . slice ( 3 ). some ( value => value !== '--unpacked-only' )) throw Error ( 'Windows is the active build target; only --unpacked-only is additionally supported' );
const workspace = path . resolve ( '.' );
for ( const args of [[ 'scripts/provision-windows-runtime.py' ],[ 'scripts/provision-semantic-runtime.py' , '--verify' ]]){
const result = spawnSync ( process . env . SAFETYNET_BUILD_PYTHON || 'python' , args ,{ stdio : 'inherit' }); if ( result . status !== 0 ) process . exit ( result . status ?? 1 );
}
await provisionBrokerRuntime ( workspace );
2026-10-13 10:33:31 +02:00
const name = process . env . BRAND_NAME || 'SafetyNet' , id = process . env . BRAND_APP_ID || 'enterprise.safetynet.browser' ;
2026-10-14 14:18:40 +02:00
const configuration = JSON . parse ( readFileSync ( 'package.json' , 'utf8' )), output = path . resolve ( configuration . build . directories . output );
2026-10-13 10:33:31 +02:00
// Reuse the pinned installed distribution already exercised by native tests.
// This avoids a second archive extraction and Windows directory-rename locks.
const electronDirectory = path . dirname ( createRequire ( import . meta . url )( 'electron' ));
if ( readFileSync ( path . join ( electronDirectory , 'version' ), 'utf8' ). trim (). replace ( /^v/ , '' ) !== configuration . devDependencies . electron ) throw Error ( 'Installed Electron does not match the build pin' );
2026-10-14 14:18:40 +02:00
const plan = createWindowsPackagePlan ({ workspace , outputDirectory : output , electronDirectory , productName : name , appId : id });
const broker = await prepareBrokerResources ({ root : workspace , build : configuration . build });
const scannerSnapshot = await snapshotScannerResources ( workspace );
const artifactName = ` ${ name } -Setup- ${ configuration . version } .exe` ;
if ( ! /^[\w .-]+\.exe$/ . test ( artifactName )) throw Error ( 'Invalid installer artifact name' );
const buildConfiguration = {... broker . configuration , artifactName , electronFuses : hardenedElectronFuses };
const configPath = path . join ( workspace , '.runtime/windows' , `package-build- ${ randomUUID () } .json` );
writeFileSync ( configPath , JSON . stringify ( buildConfiguration , null , 2 ) + '\n' ,{ flag : 'wx' });
const configArgs = [ `--config= ${ configPath } ` ];
function sourceSnapshot (){
const entries = [];
function walk ( directory ){ for ( const item of readdirSync ( directory ,{ withFileTypes : true })){ const file = path . join ( directory , item . name ); if ( item . isSymbolicLink ()) throw Error ( 'Build source reparse point rejected' ); if ( item . isDirectory ()) walk ( file ); else if ( item . isFile ()) entries . push ([ file , createHash ( 'sha256' ). update ( readFileSync ( file )). digest ( 'hex' )]);}}
for ( const directory of [ 'apps/browser' , 'apps/network-broker' , 'apps/inspector' , 'packages/core' , 'scripts' ]) walk ( directory );
for ( const file of [ 'package.json' , 'package-lock.json' , configuration . build . win . icon , configPath ]) entries . push ([ file , createHash ( 'sha256' ). update ( readFileSync ( file )). digest ( 'hex' )]);
return createHash ( 'sha256' ). update ( JSON . stringify ( entries . sort (( a , b )=> a [ 0 ]. localeCompare ( b [ 0 ])))). digest ( 'hex' );
}
2026-10-13 10:33:31 +02:00
const sourcesBefore = sourceSnapshot ();
2026-10-14 14:18:40 +02:00
const run = args => spawnSync ( process . execPath ,[ 'node_modules/electron-builder/out/cli/cli.js' ,... args ,... configArgs ],{ stdio : 'inherit' , env : process . env });
const result = run ( plan . unpacked . args ); if ( result . status !== 0 ) process . exit ( result . status ?? 1 );
await broker . verifySources (); if ( sourceSnapshot () !== sourcesBefore ) throw Error ( 'Build sources changed; discard this package' );
const resources = path . join ( plan . unpacked . directory , 'resources' ), archive = path . join ( resources , 'app.asar' ), executable = path . join ( plan . unpacked . directory , ` ${ name } .exe` );
async function verify (){
await broker . verifySources (); if ( sourceSnapshot () !== sourcesBefore ) throw Error ( 'Build sources changed; discard this package' );
await verifyPackagedFuses ( executable , archive );
await verifyPackagedBroker ({ resources , expectedManifest : broker . manifest , readEmbeddedManifest : embeddedBrokerManifestReader ( archive )});
await verifyScannerResources ( resources , scannerSnapshot );
const acl = spawnSync ( path . join ( resources , 'python/python.exe' ),[ path . join ( resources , 'inspector/provision_runtime_acl.py' )],{ stdio : 'inherit' , windowsHide : true }); if ( acl . status !== 0 ) throw Error ( 'Packaged scanner ACL verification failed' );
return { identity : await readIdentity (), fuses : true , resources : true , broker : true , runtimeAcl : true };
}
// This verifies/provisions the unpacked build location only. NSIS extraction
// does not preserve these ACEs: a reviewed installer post-install ACL step and
// actual standard-user Program Files acceptance remain required. Browser
// startup must NEVER silently provision or broaden installation permissions.
const acl = spawnSync ( path . join ( resources , 'python/python.exe' ),[ path . join ( resources , 'inspector/provision_runtime_acl.py' ), '--provision' ],{ stdio : 'inherit' , windowsHide : true }); if ( acl . status !== 0 ) process . exit ( acl . status ?? 1 );
async function treeHash ( directory ){
const hash = createHash ( 'sha256' );
async function walk ( current ){ for ( const item of readdirSync ( current ,{ withFileTypes : true }). sort (( a , b )=> a . name . localeCompare ( b . name ))){ const file = path . join ( current , item . name ); if ( item . isSymbolicLink ()) throw Error ( 'Packaged reparse point rejected' ); if ( item . isDirectory ()) await walk ( file ); else if ( item . isFile ()){ const content = createHash ( 'sha256' ); let bytes = 0 ; for await ( const chunk of createReadStream ( file )){ content . update ( chunk ); bytes += chunk . length ;} hash . update ( JSON . stringify ([ path . relative ( directory , file ). replaceAll ( '\\' , '/' ), bytes , content . digest ( 'hex' )]) + '\n' );}}}
await walk ( directory ); return hash . digest ( 'hex' );
}
async function readIdentity (){ await broker . verifySources (); const scanners = await snapshotScannerResources ( workspace ); if ( JSON . stringify ( scanners ) !== JSON . stringify ( scannerSnapshot )) throw Error ( 'Scanner sources changed; discard this package' ); return { sourceSha256 : sourceSnapshot (), runtimeSha256 : createHash ( 'sha256' ). update ( JSON . stringify ({ electron : configuration . devDependencies . electron , broker : BROKER_RUNTIME , scanners })). digest ( 'hex' ), packageSha256 : await treeHash ( plan . unpacked . directory )};}
await verify ();
console . log ( 'Unpacked browser, embedded broker manifest, complete broker payload, security fuses and scanner ACLs verified.' );
if ( ! process . argv . includes ( '--unpacked-only' )){
const installer = await buildVerifiedInstaller ({ plan , expectedIdentity : await readIdentity (), readIdentity , verify , run , installerPath : path . join ( output , artifactName )});
console . log ( JSON . stringify ({ installer : installer . path , bytes : installer . bytes , sha256 : installer . sha256 , brokerIncluded : true , productionActivationVerified : false }));
}