Files

64 lines
5.5 KiB
JavaScript

'use strict';
const {randomUUID}=require('node:crypto');
const {BrokerAuditEventSchema}=require('../../packages/core/broker-audit.cjs');
const {createProductionBroker}=require('./production-factory.cjs');
const UUID=/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
const exact=(value,keys)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).sort().join(',')===keys.sort().join(',');
/** Inherited IPC is the only bootstrap/control surface. No CLI, environment
* credential, public management listener, dynamic module or private key IPC.
*/
function runProductionProcess(factory=createProductionBroker,channel=process){
if(typeof factory!=='function'||typeof channel.send!=='function'||!channel.connected)throw Error('broker-process-invalid');
let state='waiting',context,broker,construction,initialization,closing,cleanup,sequence=0;const pending=new Map();
const closeBroker=created=>{if(!cleanup){cleanup=Promise.resolve().then(()=>created.close());cleanup.catch(()=>{});}return cleanup;};
const close=()=>{
if(closing)return closing;state='closing';for(const value of pending.values()){clearTimeout(value.timer);value.reject(Error('broker-parent-unavailable'));}pending.clear();
closing=Promise.resolve().then(async()=>{
// Construction and listen retain ownership until their ORIGINAL promises
// settle. A late factory result cannot become ready or escape cleanup.
let created=broker;try{created=created||await construction;}catch{}
try{if(created)await closeBroker(created);}finally{await initialization?.catch(()=>{});}
}).finally(()=>{if(channel.connected)channel.disconnect();});
// Event-driven callers may discard the promise; explicit close callers still
// receive the real sticky cleanup failure, never a fabricated confirmation.
closing.catch(()=>{});return closing;
};
const send=message=>{if(!channel.connected||state==='closing')throw Error('broker-parent-unavailable');channel.send(message,error=>{if(error)void close();});};
const rpc=(type,payload)=>new Promise((resolve,reject)=>{
if(pending.size>=8||state==='closing'||!context){reject(Error('broker-parent-unavailable'));return;}
if(sequence>=Number.MAX_SAFE_INTEGER){reject(Error('broker-parent-unavailable'));void close();return;}
const nextSequence=++sequence,requestId=randomUUID(),timer=setTimeout(()=>{void close();},30000);
pending.set(requestId,{type,sequence:nextSequence,eventId:payload.event?.eventId,resolve,reject,timer});
try{send({type,...context,requestId,sequence:nextSequence,...payload});}catch{void close();}
});
channel.once('disconnect',()=>{void close();});
channel.on('message',message=>{
try{
const bytes=Buffer.byteLength(JSON.stringify(message));if(!message||bytes>(state==='waiting'?2097152:65536))throw Error();
if(state==='waiting'){
if(!exact(message,['type','version','generation','policyVersion','config'])||message.type!=='start'||message.version!==1||!UUID.test(message.generation)||!Number.isSafeInteger(message.policyVersion)||message.policyVersion<1)throw Error();
context={version:1,generation:message.generation,policyVersion:message.policyVersion};state='starting';
construction=Promise.resolve().then(()=>factory(message.config,{expectedPolicyVersion:context.policyVersion,persistAudit:async event=>{
BrokerAuditEventSchema.parse(event);if(event.policyVersion!==context.policyVersion)throw Error('broker-parent-unavailable');
return rpc('audit',{event});
},acknowledgeCertificate:async({authority,cert})=>rpc('certificate',{authority,cert})}));
initialization=construction.then(async created=>{
broker=created;if(state==='closing')return;
const port=await broker.listen();if(state==='closing')return;
if(created.authMode!=='basic'||port!==message.config.brokerPort||!/^safetynet-[a-f0-9]{32}$/.test(created.credentials?.username)||!/^[A-Za-z0-9_-]{43}$/.test(created.credentials?.password))throw Error();
state='ready';send({type:'ready',...context,sequence:0,authMode:'basic',port,credentials:{...created.credentials}});
}).catch(error=>{const codes=new Set(['broker-bootstrap-invalid','broker-route-unsupported','broker-startup-inspector','broker-startup-certificate','broker-startup-transport','broker-startup-managed']);const code=codes.has(error?.message)?error.message:'broker-startup-failed';try{send({type:'startup-fault',...context,code});}catch{}void close();});return;
}
if(message.version!==1||message.generation!==context.generation||message.policyVersion!==context.policyVersion)throw Error();
if(message.type==='stop'&&exact(message,['type','version','generation','policyVersion'])){void close();return;}
const value=pending.get(message.requestId);if(!value||message.sequence!==value.sequence)throw Error();
if(value.type==='audit'&&exact(message,['type','version','generation','policyVersion','requestId','sequence','receipt'])&&message.type==='audit-receipt'&&exact(message.receipt,['persisted','eventId'])&&message.receipt.persisted===true&&message.receipt.eventId===value.eventId){clearTimeout(value.timer);pending.delete(message.requestId);value.resolve(Object.freeze({...message.receipt}));return;}
if(value.type==='certificate'&&exact(message,['type','version','generation','policyVersion','requestId','sequence','accepted'])&&message.type==='certificate-receipt'&&message.accepted===true){clearTimeout(value.timer);pending.delete(message.requestId);value.resolve(true);return;}
throw Error();
}catch{void close();}
});
return Object.freeze({close});
}
if(require.main===module)runProductionProcess();
module.exports={runProductionProcess};