2026-10-13 16:58:15 +02:00
import inspectionStages from '../../packages/core/inspection-stages.cjs' ;
2026-10-13 10:33:31 +02:00
import { NodeSDK } from '@opentelemetry/sdk-node' ;
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' ;
2026-10-13 16:58:15 +02:00
import { trace , metrics , context , ROOT_CONTEXT , SpanStatusCode } from '@opentelemetry/api' ;
2026-10-13 10:33:31 +02:00
import { logs } from '@opentelemetry/api-logs' ;
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs' ;
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http' ;
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http' ;
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' ;
export function startTelemetry () {
if ( ! process . env . OTEL_EXPORTER_OTLP_ENDPOINT ) return null ;
2026-10-13 16:58:15 +02:00
const endpoint = new URL ( process . env . OTEL_EXPORTER_OTLP_ENDPOINT );
if ( endpoint . username || endpoint . password || endpoint . search || endpoint . hash ||! ( endpoint . protocol === 'https:' || endpoint . protocol === 'http:' && [ '127.0.0.1' , '[::1]' ]. includes ( endpoint . hostname ))) throw Error ( 'OTLP requires HTTPS or numeric loopback without URL credentials' );
2026-10-13 10:33:31 +02:00
const sdk = new NodeSDK ({ serviceName : 'safetynet-control-plane' , traceExporter : new OTLPTraceExporter (),
metricReaders : [ new PeriodicExportingMetricReader ({ exporter : new OTLPMetricExporter (), exportIntervalMillis : 10000 })],
logRecordProcessors : [ new BatchLogRecordProcessor ({ exporter : new OTLPLogExporter ()})]});
sdk . start (); return sdk ;
}
export const tracer = trace . getTracer ( 'safetynet' , '0.1.0' );
2026-10-13 16:58:15 +02:00
const stages = new Set ([ 'evidence.challenge' , 'evidence.decrypt' , 'evidence.store' , 'evidence.encrypt' , 'audit.verify' , 'startpage.news' , 'ai.gateway' , 'ai.inspect.input' , 'ai.inspect.output' , 'ai.provider.request' , 'ai.provider.read' , 'ai.provider.decode' , 'library.propose' , 'library.validate' , 'library.apply' , 'library.derive' , 'server.request' ]);
export async function traceOperation ( name , attributes , fn ){
if ( typeof fn !== 'function' ) throw TypeError ( 'Trace operation requires a function' );
if ( ! stages . has ( name )) return fn ();
const safe = {}; for ( const [ key , value ] of Object . entries ( attributes || {})) if ([ 'bytes' , 'count' , 'policyVersion' ]. includes ( key ) && Number . isSafeInteger ( value ) && value >= 0 ) safe [ `safetynet. ${ key } ` ] = value ;
return tracer . startActiveSpan ( name ,{ attributes : safe }, async span =>{ const start = performance . now (); try { return await fn ();} catch ( error ){ span . setStatus ({ code : SpanStatusCode . ERROR }); span . setAttribute ( 'error.type' , error instanceof TypeError ? 'TypeError' : error instanceof RangeError ? 'RangeError' : 'Error' ); throw error ;} finally { span . setAttribute ( 'safetynet.duration.ms' , performance . now () - start ); span . end ();}});
}
export function installRequestTelemetry ( app ){
app . use (( req , res , next )=>{
if ( ! req . path . startsWith ( '/api/' )) return next ();
const match = /^00-([a-f0-9]{32})-([a-f0-9]{16})-(0[01])$/ . exec ( req . headers . traceparent || '' );
const parent = match && match [ 1 ] !== '0' . repeat ( 32 ) && match [ 2 ] !== '0' . repeat ( 16 ) ? trace . setSpanContext ( ROOT_CONTEXT ,{ traceId : match [ 1 ], spanId : match [ 2 ], traceFlags : parseInt ( match [ 3 ], 16 ), isRemote : true }) : ROOT_CONTEXT ;
const span = tracer . startSpan ( 'server.request' ,{ attributes : { 'http.request.method' : [ 'GET' , 'POST' , 'PUT' , 'PATCH' , 'DELETE' , 'OPTIONS' ]. includes ( req . method ) ? req . method : 'OTHER' }}, parent ), start = performance . now (); let ended = false ;
function end (){ if ( ended ) return ; ended = true ; const route = req . route ? . path ; if ( typeof route === 'string' && route . length <= 120 ) span . setAttribute ( 'http.route' , route ); span . setAttribute ( 'http.response.status_code' , res . statusCode ); span . setAttribute ( 'safetynet.duration.ms' , performance . now () - start ); if ( res . statusCode >= 500 ) span . setStatus ({ code : SpanStatusCode . ERROR }); span . end ();}
res . once ( 'finish' , end ); res . once ( 'close' , end ); context . with ( trace . setSpan ( parent , span ), next );
});
}
2026-10-13 10:33:31 +02:00
export function recordSecurityEvent ( kind , metadata , eventId ){
// Only bounded, allowlisted fields. Never emit body text, rules, vectors or prompts.
const decision = metadata . decision ?? ( kind . includes ( 'block' ) ? 'block' : 'other' );
metrics . getMeter ( 'safetynet' ). createCounter ( 'safetynet.security.events' ). add ( 1 ,{ 'event.action' : kind , 'dlp.decision' : decision });
logs . getLogger ( 'safetynet.security' ). emit ({ severityNumber : kind . includes ( 'block' ) ? 13 : 9 , severityText : kind . includes ( 'block' ) ? 'WARN' : 'INFO' , body : kind ,
attributes : { 'event.id' : eventId , 'event.action' : kind , 'dlp.decision' : decision ,...( metadata . ruleId ? { 'rule.id' : metadata . ruleId } : {}),...( metadata . policyVersion ? { 'safetynet.policy.version' : metadata . policyVersion } : {})}});
}
2026-10-13 16:58:15 +02:00
export function importInspectionStages ( envelope , policy ){
try {
if ( policy ? . diagnostics ? . enabled !== true || policy . diagnostics . mode !== 'debug' ) return ;
const parent = context . active (), active = trace . getSpan ( parent ); if ( ! active ? . isRecording ()) return ;
const batch = inspectionStages . validateStages ( envelope ); if ( ! batch ) return ;
const parents = new Map (); for ( const row of batch . records ){ const scope = row . parentId === null ? parent : parents . get ( row . parentId ); if ( ! scope ) return ; const span = tracer . startSpan ( 'inspector.' + row . stage ,{ startTime : batch . baseUnixMs + row . startOffsetMs , attributes : { 'safetynet.component' : 'inspector' , 'safetynet.status' : row . status , 'safetynet.duration.ms' : row . durationMs }}, scope ); if ( row . status === 'error' ) span . setStatus ({ code : SpanStatusCode . ERROR }); parents . set ( row . id , trace . setSpan ( scope , span )); span . end ( batch . baseUnixMs + row . startOffsetMs + row . durationMs );}
} catch { /* Diagnostics cannot change a DLP decision. */ }
}