// Encrypted developer telemetry only; this is not durable security evidence. import {lstatSync,realpathSync,readdirSync,existsSync,renameSync,unlinkSync,openSync,writeSync,fsyncSync,closeSync} from 'node:fs'; import {resolve,join,dirname} from 'node:path'; import {randomUUID} from 'node:crypto'; export function collectorStorage(directory,{segmentBytes=50*1024*1024,retiredSegments=2}={}) { if(!Number.isSafeInteger(segmentBytes)||segmentBytes<1024||!Number.isSafeInteger(retiredSegments)||retiredSegments<0||retiredSegments>10)throw Error('Invalid collector storage limits'); directory=resolve(directory); const active=join(directory,'signals.encrypted.ndjson'),legacy=join(directory,'signals.legacy.encrypted.ndjson'); let rotations=0,droppedSegments=0,droppedBytes=0,rejectedWrites=0; function guard(){ for(let current=directory;;current=dirname(current)){const s=lstatSync(current);if(!s.isDirectory()||s.isSymbolicLink()||realpathSync(current).toLowerCase()!==resolve(current).toLowerCase())throw Error('Unsafe collector directory');if(dirname(current)===current)break;} const names=readdirSync(directory); for(const name of names){if(!['collector-key.dpapi','signals.encrypted.ndjson','signals.legacy.encrypted.ndjson'].includes(name)&&!/^signals\.rolling\.[0-9]{13}\.[a-f0-9-]{36}\.encrypted\.ndjson$/.test(name))throw Error('Unexpected collector storage entry');const path=join(directory,name),s=lstatSync(path);if(!s.isFile()||s.isSymbolicLink()||s.nlink!==1||realpathSync(path).toLowerCase()!==path.toLowerCase())throw Error('Unsafe collector storage entry');} return names.filter(n=>n.startsWith('signals.rolling.')).sort(); } guard(); // The first old archive is pinned and never retention-deleted, including after restart. function prune(){let retired=guard();while(retired.length>retiredSegments){const name=retired.shift(),path=join(directory,name);guard();const size=lstatSync(path).size;unlinkSync(path);droppedSegments++;droppedBytes+=size;}} function rotate(){ guard(); if(existsSync(active)){ if(!existsSync(legacy))renameSync(active,legacy); else renameSync(active,join(directory,`signals.rolling.${Date.now()}.${randomUUID()}.encrypted.ndjson`)); rotations++; } prune(); } function append(line){ try{ guard();const bytes=Buffer.byteLength(line);if(bytes>segmentBytes)throw Error('Collector batch exceeds segment budget');prune(); const size=existsSync(active)?lstatSync(active).size:0;if(size+bytes>segmentBytes)rotate(); guard();const fd=openSync(active,existsSync(active)?'a':'wx',0o600); try{const buffer=Buffer.from(line);let offset=0;while(offsetn+lstatSync(join(directory,name)).size,0),retiredSegments:retired.length,segmentBytes,maxRollingBytes:segmentBytes*(retiredSegments+1),rotations,droppedSegments,droppedBytes,rejectedWrites};} return {append,summary}; }