64 lines
5.5 KiB
JavaScript
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};
|