'use strict'; const {randomUUID}=require('node:crypto'); const phases=Object.freeze({'Network.webTransportCreated':'created','Network.webTransportClosed':'closed','Network.webTransportConnectionEstablished':'established'}); const uuid=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; const opaque=value=>typeof value==='string'&&value.length>0&&value.length<=128&&/^[a-zA-Z0-9._:-]+$/.test(value); function transportHost(value){if(typeof value!=='string'||value.length>8192)throw Error('Transport metadata refused');const u=new URL(value);if(u.protocol!=='https:'||u.username||u.password||u.hostname.length>253)throw Error('Transport metadata refused');return u.hostname.toLowerCase();} // Observability only: Chromium's mandatory private proxy remains the release // boundary. No generic command runner, raw protocol events or page bridge escape. function createNativeTransportObserver({channel,getContext,isCurrent,audit,onFailure,onEstablished,traceOperation=(_stage,fn)=>fn(),commandTimeoutMs=5000,maxTargets=64,maxAttempts=256,maxAuditQueue=256}={}){ if(!channel||['isHealthy','enableNetwork','autoAttachTargets','resumeTarget','subscribeMessage','subscribeDetach','release'].some(name=>typeof channel[name]!=='function')||[getContext,isCurrent,audit,onFailure,onEstablished,traceOperation].some(fn=>typeof fn!=='function')||!Number.isInteger(commandTimeoutMs)||commandTimeoutMs<50||commandTimeoutMs>10000||[maxTargets,maxAttempts,maxAuditQueue].some(n=>!Number.isInteger(n)||n<1||n>256))throw TypeError('Native transport observer callbacks required'); const targets=new Map(),attempts=new Map(),setups=new Set();let owned=false,installed=false,disposed=false,failed=false,installPromise,auditTail=Promise.resolve(),queued=0,unsubscribeMessage,unsubscribeDetach; const active=()=>owned&&!disposed&&!failed; function failure(reason='transport-observer-unavailable'){if(disposed||failed)return;failed=true;installed=false;attempts.clear();targets.clear();try{Promise.resolve(traceOperation('transport.monitor-unavailable',()=>onFailure(reason))).catch(()=>{});}catch{} } async function command(method,_parameters={},sessionId){if(!active())throw Error('Native transport monitor unavailable');let timer;try{return await Promise.race([(method==='Network.enable'?channel.enableNetwork(sessionId):method==='Target.setAutoAttach'?channel.autoAttachTargets(sessionId):channel.resumeTarget(sessionId)),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Object.assign(Error('Native transport command timeout'),{reason:'transport-observer-command-timeout'})),commandTimeoutMs);})]);}finally{clearTimeout(timer);}} function context(){try{const value=getContext();if(!value)return null;const copy=Object.freeze({...value});if(!uuid.test(copy.sessionId)||!Number.isSafeInteger(copy.policyVersion)||copy.policyVersion<1||!isCurrent(copy))return null;return copy;}catch{return null;}} function enqueue(row,phase){if(queued>=maxAuditQueue){failure('transport-observer-overflow');return;}const facts=Object.freeze({transportId:row.id,protocol:'webtransport',phase,realm:row.realm,sessionId:row.context.sessionId,connectionEstablished:row.established,host:row.host});queued++; // Metadata retains the creation policy/owner across later navigation/policy // changes. Rechecking release authority here would erase legitimate audit. auditTail=auditTail.then(async()=>{await traceOperation('transport.'+phase,()=>audit(facts,row.context),row.context);}).catch(()=>failure('transport-observer-audit-failed')).finally(()=>queued--); } function event(_event,method,params,sessionId){if(!active())return; if(method==='Target.attachedToTarget'){ const id=params?.sessionId,type=params?.targetInfo?.type; if(!opaque(id)||targets.has(id)){failure();return;}if(targets.size>=maxTargets){failure('transport-observer-overflow');return;} const realm=['worker','shared_worker','service_worker'].includes(type)?'worker':type==='iframe'?'iframe':null; if(!realm){failure();return;}targets.set(id,{realm}); const work=Promise.resolve().then(()=>traceOperation('transport.target',async()=>{ // Issue setup before resume, but do not await responses while the fresh // target is waiting for startup. Chromium may defer them until resume. const network=command('Network.enable',{maxPostDataSize:0},id),children=command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true},id); await Promise.all([network,children,command('Runtime.runIfWaitingForDebugger',{},id)]); })).catch(error=>failure(error.reason==='transport-observer-command-timeout'?'transport-observer-command-timeout':'transport-observer-unavailable')).finally(()=>setups.delete(work));setups.add(work);return; } // Target detachment can precede its final Network close event. Keep the // already captured bounded attempt correlation until that event or disposal. if(method==='Target.detachedFromTarget'){targets.delete(params?.sessionId);return;} const phase=phases[method];if(!phase)return; if(sessionId&&!targets.has(sessionId)&&phase==='created'){failure();return;}if(!opaque(params?.transportId)){failure();return;} const key=(sessionId||'window')+':'+params.transportId; if(phase==='created'){ if(attempts.has(key)){failure();return;}if(attempts.size>=maxAttempts){failure('transport-observer-overflow');return;}const owner=context();if(!owner)return; try{const row={id:randomUUID(),context:owner,host:transportHost(params.url),realm:sessionId?targets.get(sessionId).realm:'window',target:sessionId,established:false};attempts.set(key,row);enqueue(row,phase);}catch{failure();}return; } const row=attempts.get(key);if(!row)return; if(phase==='established'){if(row.established){failure();return;}row.established=true;try{onEstablished(row.context);}catch{failure();}enqueue(row,phase);return;} attempts.delete(key);enqueue(row,phase); } function detach(){if(!disposed&&owned)failure('transport-observer-detached');owned=false;installed=false;} async function install(){if(installPromise)return installPromise;if(disposed||failed||!channel.isHealthy())throw Error('Private transport observer unavailable'); // eslint-disable-next-line preserve-caught-error -- Expose only a fixed error; protocol causes may contain private URLs or target data. installPromise=Promise.resolve(traceOperation('transport.install',async()=>{try{unsubscribeMessage=channel.subscribeMessage(event);unsubscribeDetach=channel.subscribeDetach(detach);owned=true;await command('Network.enable',{maxPostDataSize:0});await command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true});if(!active())throw Error('Native transport monitor unavailable');installed=true;}catch(error){failure(error.reason==='transport-observer-command-timeout'?'transport-observer-command-timeout':'transport-observer-unavailable');throw Error('Native transport monitor installation failed');}}));return installPromise; } function dispose(){if(disposed)return;disposed=true;installed=false;attempts.clear();targets.clear();try{unsubscribeMessage?.();}catch{}try{unsubscribeDetach?.();}catch{}try{channel.release();}catch{}owned=false;} async function flush(){await Promise.allSettled([...setups]);await auditTail;} return {install,dispose,flush,status:()=>Object.freeze({installed,failed,disposed,attached:owned&&channel.isHealthy(),targets:targets.size,pending:attempts.size,queued})}; } module.exports={createNativeTransportObserver};