Files
safetynet/apps/network-broker/lifecycle-audit.cjs
T

57 lines
4.7 KiB
JavaScript

'use strict';
const {randomUUID}=require('node:crypto');
const {GateError}=require('./http-request-gate.cjs');
function reasonFor(code,scope){
if(code==='inspection-deadline')return 'broker-scan-timeout';
if(code==='authority-expired')return 'broker-authority-expired';
if(code==='authentication-required')return scope==='connect'?'broker-auth-denied':'broker-framing-block';
if(['deadline','certificate-deadline','forward-deadline'].includes(code))return 'broker-request-timeout';
if(['aborted','incomplete-body','request-error'].includes(code))return 'broker-request-aborted';
if(code==='body-too-large')return 'broker-size-block';
if(['invalid-upstream-response','unsupported-upstream-encoding'].includes(code))return 'broker-response-invalid';
if(code==='upstream-failed')return 'broker-upstream-failed';
if(['invalid-headers','duplicate-header','unsupported-framing','unsupported-method','invalid-destination','authority-mismatch','invalid-length','unsupported-connect-framing','missing-host','duplicate-host','invalid-target','missing-sni','sni-mismatch'].includes(code))return 'broker-framing-block';
return scope==='connect'?'broker-connect-denied':'broker-inspection-fault';
}
/** One bounded state object per request. UUIDs express audit causation, never
* invented OpenTelemetry traces. A failed/uncertain commit is never retried.
*/
function createBrokerLifecycle(audit){
if(!audit||typeof audit.record!=='function')throw new GateError('audit-configuration-invalid');
return Object.freeze({begin(connectionId){
if(connectionId!==undefined&&!/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(connectionId))throw new GateError('audit-configuration-invalid');
const requestId=randomUUID();let decisionWork,decisionType,authorizedEventId,terminalWork,responseWork,responseDecision,responseCommitted=false;
const broker=(phase,reason)=>({requestId,...(connectionId?{connectionId}:{}),phase,reason,...(phase==='forward.fault'||phase.startsWith('response.')?{authorizedEventId}: {})});
return Object.freeze({requestId,
decision(decision,facts){
if(decisionWork)return decisionWork;if(terminalWork)throw new GateError('aborted');
decisionType=decision;
const reason=facts.reason||(decision==='allow'?'broker-approved':decision==='block'?'broker-policy-block':'broker-inspection-fault');
const phase=decision==='allow'?'request.authorized':decision==='block'?'request.block':'request.fault';
decisionWork=audit.record(decision,{...facts,reason,broker:broker(phase,reason)}).then(receipt=>{if(decision==='allow')authorizedEventId=receipt.eventId;return receipt;},error=>{if(decision==='allow'&&error?.committedEventId)authorizedEventId=error.committedEventId;throw error;});
return decisionWork;
},
response(decision,facts){
if(responseWork)return responseWork;if(terminalWork)throw new GateError('aborted');responseDecision=decision;
responseWork=(async()=>{await decisionWork;if(!authorizedEventId)throw new GateError('authority-expired');const reason=decision==='allow'?'broker-download-approved':facts.reason||'broker-download-inspection-block';const safeFacts={...facts};delete safeFacts.reason;return audit.recordDownload(decision,{...safeFacts,broker:broker(decision==='allow'?'response.scanned':'response.block',reason)}).then(receipt=>{responseCommitted=true;return receipt;},error=>{responseCommitted=!!error?.committedEventId;throw error;});})();return responseWork;
},
reject(code,facts,scope='request'){
if(terminalWork)return terminalWork;
terminalWork=(async()=>{
if(decisionWork){
try {await decisionWork;} catch {if(!authorizedEventId)return {persisted:false,uncertain:true};}
if(decisionType!=='allow')return {persisted:true,alreadyRecorded:true};
}
if(responseWork){try{await responseWork;}catch{if(!responseCommitted)return {persisted:false,uncertain:true};}if(responseDecision==='block')return {persisted:true,alreadyRecorded:true};}
let reason=reasonFor(code,scope);
const phase=authorizedEventId?'forward.fault':scope==='connect'?'connect.block':reason==='broker-inspection-fault'?'request.fault':'request.block';
if(phase==='forward.fault'&&!['broker-upstream-failed','broker-response-invalid','broker-request-aborted','broker-request-timeout','broker-authority-expired','broker-scan-timeout'].includes(reason))reason='broker-upstream-failed';
return audit.record(phase==='forward.fault'||phase==='request.fault'?'fault':'block',{...facts,reason,broker:broker(phase,reason)});
})();
return terminalWork;
}
});
}});
}
module.exports={createBrokerLifecycle};