Files
safetynet/scripts/windows-package-plan.mjs
T

39 lines
3.0 KiB
JavaScript

// Not wired into package.mjs until a real packaged broker is available.
// Official CLI: https://www.electron.build/docs/cli/
// --dir produces only an unpacked app; --win nsis --prepackaged distributes
// that verified directory without silently rebuilding its application payload.
import path from 'node:path';
function fail(code){throw Object.assign(new Error(code),{code});}
function identity(value){
if(!value||!['sourceSha256','runtimeSha256','packageSha256'].every(key=>/^[a-f0-9]{64}$/.test(value[key])))fail('package_identity_invalid');
return Object.freeze(Object.fromEntries(['sourceSha256','runtimeSha256','packageSha256'].map(key=>[key,value[key]])));
}
function same(left,right){return Object.keys(left).every(key=>left[key]===right[key]);}
export function createWindowsPackagePlan({workspace,outputDirectory,electronDirectory,productName='SafetyNet',appId='enterprise.safetynet.browser',platform=process.platform}){
if(platform!=='win32')fail('package_platform_unsupported');
if(![workspace,outputDirectory,electronDirectory].every(value=>typeof value==='string'&&path.isAbsolute(value)))fail('package_path_invalid');
const root=path.resolve(workspace),output=path.resolve(outputDirectory),runtime=path.resolve(electronDirectory);
const relative=path.relative(root,output);
if(!relative||relative.startsWith('..'+path.sep)||relative==='..'||path.isAbsolute(relative))fail('package_output_unsafe');
if(!/^[\w .-]{1,40}$/.test(productName)||!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){2,}$/.test(appId))fail('package_brand_invalid');
const directory=path.join(output,'win-unpacked');
const common=[`--config.productName=${productName}`,`--config.appId=${appId}`,`--config.directories.output=${output}`,'--publish','never'];
return Object.freeze({
unpacked:Object.freeze({stage:'build-unpacked',args:Object.freeze(['--win','--dir',`--config.electronDist=${runtime}`,...common]),directory}),
verification:Object.freeze({stage:'verify-unpacked',required:Object.freeze(['fuses','resources','broker','runtimeAcl']),directory}),
async installer({expectedIdentity,readIdentity,verify}){
if(typeof readIdentity!=='function'||typeof verify!=='function')fail('package_verifier_required');
const expected=identity(expectedIdentity);
if(!same(expected,identity(await readIdentity(directory))))fail('package_identity_changed');
const receipt=await verify(directory,expected);
if(!receipt||!same(expected,identity(receipt.identity))||!['fuses','resources','broker','runtimeAcl'].every(key=>receipt[key]===true))fail('package_verification_incomplete');
if(!same(expected,identity(await readIdentity(directory))))fail('package_identity_changed');
// This plans arguments only. Caller must protect the verified directory
// against changes through invocation and inspect the real installer result.
return Object.freeze({stage:'build-installer',directory,identity:expected,
args:Object.freeze(['--win','nsis','--prepackaged',directory,...common])});
}
});
}