69 lines
5.6 KiB
JavaScript
69 lines
5.6 KiB
JavaScript
// 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;
|
||
|
|
}
|