import test from 'node:test';
import assert from 'node:assert/strict';
import {fork} from 'node:child_process';
import {once} from 'node:events';
import fs from 'node:fs';
import {createRequire} from 'node:module';
import {fileURLToPath} from 'node:url';
import https from 'node:https';
import {X509Certificate,generateKeyPairSync} from 'node:crypto';
import {defaultPolicy,signPolicy} from '../packages/core/policy.mjs';
const require=createRequire(import.meta.url),{createManagedProxy}=require('../apps/network-broker/managed-proxy.cjs'),{createPolicyAuthority}=require('../apps/network-broker/policy-authority.cjs'),{createHttpsForwarder}=require('../apps/network-broker/https-forwarder.cjs');
const {createSyntheticAudit}=require('./fixtures/broker-durable-audit.cjs');
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
function bounded(promise,ms=15000){let timer;return Promise.race([promise,new Promise((_,reject)=>timer=setTimeout(()=>reject(Error('interop-deadline')),ms))]).finally(()=>clearTimeout(timer));}
test('stock Electron authenticates Basic proxy and sends only signed-policy approved browser content',{
skip:process.platform!=='win32'||process.env.SAFETYNET_NATIVE_BROKER_ELECTRON!=='1',timeout:60000
},async t=>{
assert.equal(process.execPath.toLowerCase(),fileURLToPath(new URL('../.runtime/windows/broker/node.exe',import.meta.url)).toLowerCase());
const received=[];let leakedProxyHeader=false,connections=0;
const site=https.createServer({key,cert},async(req,res)=>{
leakedProxyHeader ||= !!req.headers['proxy-authorization'];
const chunks=[];for await(const chunk of req)chunks.push(chunk);
if(req.method==='POST'){received.push(Buffer.concat(chunks).toString());res.writeHead(201,{'content-type':'text/plain'});res.end('accepted');}
else {res.setHeader('content-type','text/html');res.end('
Local interop fixture
');}
});site.on('connection',()=>connections++);site.listen(0,'127.0.0.1');await once(site,'listening');
const port=site.address().port,destination=`localhost:${port}`,origin=`https://${destination}`;
t.after(async()=>{site.closeAllConnections();await new Promise(resolve=>site.close(resolve));});
const keys=generateKeyPairSync('ed25519'),publicKey=keys.publicKey.export({type:'spki',format:'pem'}),privateKey=keys.privateKey.export({type:'pkcs8',format:'pem'});
const p=defaultPolicy();p.assignment={deviceId:'interop-device',groups:[]};p.dlp.rules=[{id:'company-secret',label:'Sensitive',kind:'literal',value:'PROJECT SECRET',action:'block'}];
const authority=await createPolicyAuthority({publicKey,deviceId:'interop-device'}),snapshot=authority.accept(signPolicy(p,privateKey));
const forward=createHttpsForwarder({authority:snapshot.token,current:authority.current,resolveRoute:()=>({approved:true,address:'127.0.0.1',port}),ca:cert,maxResponseBytes:16777216});
const audit=createSyntheticAudit(t);
const proxy=await createManagedProxy({policyAuthority:authority,persistAudit:audit.persist,resolveCertificate:binding=>binding.authority===destination?{authority:destination,key,cert}:undefined,forward});
const proxyPort=await proxy.listen();t.after(()=>proxy.close());
const artifactRoot=fileURLToPath(new URL('../artifacts/broker-electron-interop/',import.meta.url));fs.mkdirSync(artifactRoot,{recursive:true});
const userData=fs.mkdtempSync(artifactRoot+'profile-');
const cleanEnvironment=Object.fromEntries(['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','HOMEDRIVE','HOMEPATH','LOCALAPPDATA','APPDATA','PATH'].filter(name=>process.env[name]!==undefined).map(name=>[name,process.env[name]]));
const child=fork(fileURLToPath(new URL('./fixtures/broker-electron-interop.cjs',import.meta.url)),[],{execPath:require('electron'),execArgv:[],env:cleanEnvironment,windowsHide:true,stdio:['ignore','ignore','ignore','ipc']});
t.after(()=>{if(child.exitCode===null)child.kill();});
const ready=bounded(once(child,'message'));child.send({type:'initialize',config:{proxyPort,origin,userData,credentials:proxy.credentials,fingerprint:new X509Certificate(cert).fingerprint256}});
const [initialized]=await ready;assert.equal(initialized.type,'ready');assert.ok(initialized.loginCount>=1);assert.match(initialized.electron,/^44\./);
const operation=async type=>{const result=bounded(once(child,'message'));child.send({type});const [message]=await result;assert.equal(message.type,'result');return message.result;};
assert.deepEqual(await operation('public'),{status:201,body:'accepted'});assert.deepEqual(received,['public browser content']);
const beforeSecret=connections;
const blocked=await operation('secret');assert.ok(blocked.blocked||blocked.status===403);assert.deepEqual(received,['public browser content']);assert.equal(connections,beforeSecret);
const next=structuredClone(p);next.version=2;authority.accept(signPolicy(next,privateKey));
const revoked=await operation('revoked');assert.ok(revoked.blocked||revoked.status===403);assert.deepEqual(received,['public browser content']);assert.equal(leakedProxyHeader,false);assert.equal(connections,beforeSecret);
const exit=bounded(once(child,'exit'));child.send({type:'stop'});await exit;
fs.mkdirSync('artifacts/broker-electron-interop',{recursive:true});
fs.writeFileSync(`artifacts/broker-electron-interop/proof-${new Date().toISOString().replace(/:/g,'-')}.json`,JSON.stringify({schemaVersion:1,electron:initialized.electron,chromium:initialized.chromium,brokerNode:process.version,proxyAuthentication:'Basic',loginEvents:initialized.loginCount,authorizedPosts:received.length,authorizedBytes:Buffer.byteLength(received[0]),secretPosts:0,revokedPosts:0,upstreamConnectionsBeforeSecret:beforeSecret,upstreamConnectionsAfterBlocks:connections,proxyCredentialsLeaked:leakedProxyHeader,isolatedFixture:true},null,2));
});