'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};