56 lines
4.7 KiB
JavaScript
56 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};
|