2026-10-13 16:58:15 +02:00
// Local development receiver. This is not a production SIEM or audit store.
import { createServer } from 'node:http' ;
import { mkdirSync , existsSync , readFileSync , writeFileSync } from 'node:fs' ;
import { collectorStorage } from './debug-collector-storage.mjs' ;
import { normalizeCollectorBatch } from './debug-collector-batch.mjs' ;
2026-10-14 01:18:23 +02:00
import { collectorAdmission } from './debug-collector-admission.mjs' ;
import { protectCollectorDirectory } from './debug-collector-acl.mjs' ;
import { monitorEventLoopDelay } from 'node:perf_hooks' ;
2026-10-13 16:58:15 +02:00
import { resolve } from 'node:path' ;
import { spawnSync } from 'node:child_process' ;
import { randomBytes , createCipheriv } from 'node:crypto' ;
const port = Number ( process . env . DEBUG_COLLECTOR_PORT || 4318 ), directory = resolve ( process . env . DEBUG_COLLECTOR_DIRECTORY || 'data/observability' );
if ( process . platform !== 'win32' ) throw Error ( 'Local protected collector currently requires Windows DPAPI' );
mkdirSync ( directory ,{ recursive : true });
const storage = collectorStorage ( directory ,{ segmentBytes : Number ( process . env . DEBUG_COLLECTOR_SEGMENT_BYTES || 50 * 1024 * 1024 ), retiredSegments : Number ( process . env . DEBUG_COLLECTOR_RETIRED_SEGMENTS || 2 )});
2026-10-14 01:18:23 +02:00
protectCollectorDirectory ( directory );
2026-10-13 16:58:15 +02:00
function protect ( operation , value ){ const result = spawnSync ( resolve ( '.runtime/windows/python/python.exe' ),[ resolve ( 'apps/inspector/dpapi.py' ), operation ],{ input : value , encoding : 'utf8' , windowsHide : true , timeout : 5000 , env : { SystemRoot : process . env . SystemRoot , USERPROFILE : process . env . USERPROFILE , LOCALAPPDATA : process . env . LOCALAPPDATA , PYTHONDONTWRITEBYTECODE : '1' }}); if ( result . status !== 0 ) throw Error ( 'Collector DPAPI key protection failed' ); return result . stdout . trim ();}
const keyFile = resolve ( directory , 'collector-key.dpapi' );
let key ; if ( existsSync ( keyFile )){ const value = protect ( 'unprotect' , readFileSync ( keyFile , 'utf8' )); if ( ! value . startsWith ( 'observability-v1:' )) throw Error ( 'Collector key binding rejected' ); key = Buffer . from ( value . slice ( 17 ), 'base64' );} else { key = randomBytes ( 32 ); writeFileSync ( keyFile , protect ( 'protect' , 'observability-v1:' + key . toString ( 'base64' )),{ flag : 'wx' , mode : 0o600 });}
if ( key . length !== 32 ) throw Error ( 'Invalid collector key' );
2026-10-14 01:18:23 +02:00
const admission = collectorAdmission ({ maxRequests : Number ( process . env . DEBUG_COLLECTOR_MAX_REQUESTS || 8 ), maxBufferedBytes : Number ( process . env . DEBUG_COLLECTOR_MAX_BUFFERED_BYTES || 8 * 1024 * 1024 )});
const eventLoop = monitorEventLoopDelay ({ resolution : 20 }); eventLoop . enable ();
const lag = ()=>({ meanMs : Number . isFinite ( eventLoop . mean ) ? eventLoop . mean / 1e6 : 0 , maxMs : eventLoop . max / 1e6 , p99Ms : eventLoop . percentile ( 99 ) / 1e6 });
2026-10-13 16:58:15 +02:00
const counts = { traces : 0 , logs : 0 , metrics : 0 }, recent = []; let requests = 0 ;
2026-10-14 01:18:23 +02:00
function summary (){ return { service : 'safetynet-local-debug' , protectedStorage : 'Windows DPAPI + AES-256-GCM' , requests , counts , admission : admission . summary (), eventLoopLag : lag (),... storage . summary (), retainedSpans : recent . length , slowest : recent . slice (). sort (( a , b )=> b . durationMs - a . durationMs ). slice ( 0 , 15 )};}
2026-10-13 16:58:15 +02:00
const server = createServer (( req , res )=>{
res . setHeader ( 'cache-control' , 'no-store' ); res . setHeader ( 'x-content-type-options' , 'nosniff' );
const origin = `http://127.0.0.1: ${ port } ` ;
if ( req . headers . host !== `127.0.0.1: ${ port } ` || ( req . headers . origin && req . headers . origin !== origin )){ res . writeHead ( 403 ); res . end (); return ;}
if ( req . method === 'GET' && [ '/' , '/health' , '/api/summary' ]. includes ( req . url )){ res . setHeader ( 'content-type' , 'application/json' ); try { res . end ( JSON . stringify ( summary (), null , 2 ));} catch { res . writeHead ( 503 ); res . end ( '{"error":"Protected collector storage unavailable"}' );} return ;}
if ( req . method === 'GET' && /^\/api\/trace\/[a-f0-9]{32}$/ . test ( req . url )){ res . setHeader ( 'content-type' , 'application/json' ); res . end ( JSON . stringify ({ traceId : req . url . slice ( 11 ), spans : recent . filter ( span => span . traceId === req . url . slice ( 11 ))}, null , 2 )); return ;}
if ( req . method !== 'POST' ||! [ '/v1/traces' , '/v1/logs' , '/v1/metrics' ]. includes ( req . url ) || req . headers [ 'content-encoding' ]){ res . writeHead ( 404 ); res . end (); return ;}
2026-10-14 01:18:23 +02:00
const lease = admission . acquire ();
const rejectBusy = ()=>{ res . setHeader ( 'connection' , 'close' ); res . setHeader ( 'retry-after' , '1' ); res . writeHead ( 503 ); res . end ( '{"error":"Collector busy"}' );};
if ( ! lease ){ rejectBusy (); return ;}
const chunks = []; let finished = false ;
const release = ()=>{ lease . release (); chunks . length = 0 ;};
const fail = ( status , body )=>{ if ( finished ) return ; finished = true ; release (); res . setHeader ( 'connection' , 'close' ); res . writeHead ( status ); res . end ( body );};
req . once ( 'aborted' ,()=>{ finished = true ; release ();}); req . once ( 'error' ,()=>{ finished = true ; release ();});
res . once ( 'close' ,()=>{ if ( ! finished ){ finished = true ; release ();}});
req . setTimeout ( 10000 ,()=> fail ( 408 , '{"error":"Collector request timed out"}' ));
req . on ( 'data' , chunk =>{ if ( finished ) return ; const rejected = lease . add ( chunk . length ); if ( rejected ){ if ( rejected === 'body-limit' ) fail ( 413 , '{"error":"Telemetry batch too large"}' ); else { finished = true ; release (); rejectBusy ();}} else chunks . push ( chunk );});
req . on ( 'end' ,()=>{ if ( finished ) return ; req . setTimeout ( 0 );
void lease . enqueue (()=>{ if ( finished ) return ;
try {
const payload = JSON . parse ( Buffer . concat ( chunks ). toString ( 'utf8' )), signal = req . url . slice ( 4 ); if ( ! payload || typeof payload !== 'object' || Array . isArray ( payload )) throw Error ( 'Invalid OTLP' );
const normalized = normalizeCollectorBatch ( signal , payload );
const nonce = randomBytes ( 12 ), cipher = createCipheriv ( 'aes-256-gcm' , key , nonce ); cipher . setAAD ( Buffer . from ( 'safetynet-debug-v1' ));
const plain = Buffer . from ( JSON . stringify ({ signal , receivedAt : new Date (). toISOString (), payload })), encrypted = Buffer . concat ([ cipher . update ( plain ), cipher . final ()]); plain . fill ( 0 );
const line = JSON . stringify ({ v : 1 , nonce : nonce . toString ( 'base64' ), tag : cipher . getAuthTag (). toString ( 'base64' ), ciphertext : encrypted . toString ( 'base64' )}) + '\n' ;
// HTTP success remains after the guarded encrypted append and fsync.
try { storage . append ( line );} catch { fail ( 507 , '{"error":"Protected collector storage unavailable"}' ); throw Error ( 'Collector commit failed' );}
requests ++ ; counts . traces += normalized . spans . length ; counts . logs += normalized . logs ; counts . metrics += normalized . metrics ; recent . push (... normalized . spans ); while ( recent . length > 5000 ) recent . shift ();
finished = true ; res . setHeader ( 'content-type' , 'application/json' ); res . end ( '{}' );
} catch { fail ( 400 , '{"error":"Invalid telemetry batch"}' ); throw Error ( 'Collector batch failed' );}
}). catch (()=>{ if ( ! finished ) fail ( 503 , '{"error":"Collector busy"}' );}). finally ( release );
});
2026-10-13 16:58:15 +02:00
});
server . listen ( port , '127.0.0.1' ,()=> console . log ( `Local debug collector: http://127.0.0.1: ${ port } /api/summary` ));
2026-10-14 01:18:23 +02:00
function stop (){ eventLoop . disable (); server . closeAllConnections (); server . close (()=>{ key . fill ( 0 ); process . exit ( 0 );});} process . on ( 'SIGINT' , stop ); process . on ( 'SIGTERM' , stop );