// Mandatory component packaging only; this does not activate a broker or // install/attest its Windows network boundary. import {createRequire} from 'node:module'; import {randomUUID} from 'node:crypto'; import {createHash} from 'node:crypto'; import {createReadStream} from 'node:fs'; import {readdir,lstat} from 'node:fs/promises'; import path from 'node:path'; import {stageBrokerPayload,BROKER_PAYLOAD,verifySourceSnapshot} from './stage-broker-payload.mjs'; import {verifyBrokerRuntime} from './provision-broker-runtime.mjs'; import {verifyBrokerPackage} from './broker-package-contract.mjs'; const require=createRequire(import.meta.url); const RESOURCE_FROM='.runtime/windows/broker-payload',MANIFEST_FROM=RESOURCE_FROM+'.manifest.json'; const EMBEDDED_MANIFEST='apps/browser/broker-package-manifest.json'; function fail(code){throw Object.assign(new Error(code),{code});} export function brokerResourceConfiguration(build,{payloadRoot,manifestPath}){ if(!build||!Array.isArray(build.extraResources)||!Array.isArray(build.files))fail('broker_package_configuration_missing'); const destination=value=>typeof value==='string'?value.replaceAll('\\','/').replace(/\/+$/,'').toLowerCase():''; const resources=build.extraResources.filter(item=>item&&typeof item==='object'&&(destination(item.to)==='broker'||destination(item.to).startsWith('broker/'))); const manifests=build.files.filter(item=>item&&typeof item==='object'&&destination(item.to)===EMBEDDED_MANIFEST); if(resources.length!==1||resources[0].from!==RESOURCE_FROM||manifests.length!==1||manifests[0].from!==MANIFEST_FROM)fail('broker_package_configuration_missing'); if(!path.isAbsolute(payloadRoot)||!path.isAbsolute(manifestPath))fail('broker_package_configuration_invalid'); return {...structuredClone(build),extraResources:build.extraResources.map(item=>item===resources[0]?{from:payloadRoot,to:'broker',filter:['**/*']}:structuredClone(item)),files:build.files.map(item=>item===manifests[0]?{from:manifestPath,to:EMBEDDED_MANIFEST}:structuredClone(item))}; } export async function prepareBrokerResources({root,build,dependencyArchive}){ await verifyBrokerRuntime(path.join(root,'.runtime/windows/broker')); const destination=path.join(root,'.runtime/windows','broker-payload-package-'+randomUUID()); const staged=await stageBrokerPayload({root,destination,dependencyArchive}); const configuration=brokerResourceConfiguration(build,{payloadRoot:staged.root,manifestPath:staged.root+'.manifest.json'}); return {configuration,manifest:staged.manifest,sourceSnapshot:staged.sources, verifySources:()=>verifySourceSnapshot(root,staged.sources)}; } export async function verifyPackagedBroker({resources,expectedManifest,readEmbeddedManifest}){ if(typeof readEmbeddedManifest!=='function')fail('broker_package_manifest_reader_required'); // Production supplies the ASAR extractor after embedded ASAR integrity/fuse // verification. Never read expected state from resources/broker metadata. let embedded;try{const bytes=await readEmbeddedManifest(EMBEDDED_MANIFEST);if(!Buffer.isBuffer(bytes)||bytes.length>1048576)fail('broker_package_manifest_invalid');embedded=JSON.parse(bytes.toString('utf8'));}catch{fail('broker_package_manifest_invalid');} if(JSON.stringify(embedded)!==JSON.stringify(expectedManifest))fail('broker_package_manifest_mismatch'); return verifyBrokerPackage(path.join(resources,'broker'),embedded,BROKER_PAYLOAD); } export function embeddedBrokerManifestReader(archive){ const {extractFile}=require('@electron/asar'); return relative=>extractFile(archive,path.normalize(relative)); } const scannerRoots=[['apps/inspector','inspector'],['.runtime/windows/python','python'],['.runtime/windows/semantic','semantic']]; async function fileDigest(file){const stat=await lstat(file);if(stat.isSymbolicLink()||!stat.isFile())fail('scanner_package_path_unsafe');const hash=createHash('sha256');for await(const chunk of createReadStream(file))hash.update(chunk);const after=await lstat(file);if(stat.size!==after.size||stat.mtimeMs!==after.mtimeMs||stat.ctimeMs!==after.ctimeMs)fail('scanner_package_changed');return {bytes:stat.size,sha256:hash.digest('hex')};} async function scannerInventory(base,component,filter){ const records=[]; async function walk(directory,prefix=''){ for(const entry of await readdir(directory,{withFileTypes:true})){ if(filter&&(entry.name==='__pycache__'||component==='inspector'&&(entry.name.startsWith('test')||entry.name==='appcontainer_probe.py')))continue; const file=path.join(directory,entry.name),relative=prefix+entry.name; if(entry.isSymbolicLink())fail('scanner_package_path_unsafe'); if(entry.isDirectory())await walk(file,relative+'/');else if(entry.isFile())records.push({path:component+'/'+relative,...await fileDigest(file)}); else fail('scanner_package_path_unsafe'); if(records.length>20000)fail('scanner_package_too_large'); } } const stat=await lstat(base);if(stat.isSymbolicLink()||!stat.isDirectory())fail('scanner_package_path_unsafe'); await walk(base);return records.sort((a,b)=>a.path.localeCompare(b.path)); } export async function snapshotScannerResources(root){ const records=[];for(const [from,to] of scannerRoots)records.push(...await scannerInventory(path.join(root,...from.split('/')),to,to!=='semantic')); return records; } export async function verifyScannerResources(resources,expected){ const actual=[];for(const [,component] of scannerRoots)actual.push(...await scannerInventory(path.join(resources,component),component,false)); if(JSON.stringify(actual)!==JSON.stringify(expected))fail('scanner_package_integrity_mismatch');return true; }