import { createServer } from 'node:http'; import { isIP } from 'node:net'; import { monitorEventLoopDelay } from 'node:perf_hooks'; import { stat,statfs } from 'node:fs/promises'; import { join } from 'node:path'; import type { AddressInfo } from 'node:net'; /** No hop-count/boolean trust. Match only explicit proxy addresses/subnets. The edge must * overwrite forwarded headers and prevent direct backend access from trusted addresses. */ export function trustedProxies(env: NodeJS.ProcessEnv = process.env): string[] { const raw = env.WIKIDECK_TRUSTED_PROXIES; if (!raw) return []; const values=raw.split(',').map(s=>s.trim()); if(values.length>16)throw new Error('At most 16 trusted proxy addresses/subnets'); for(const value of values){ const [ip,prefix,...extra]=value.split('/'),family=isIP(ip); if(!family||extra.length||(prefix!==undefined&&(!/^\d{1,3}$/.test(prefix)||Number(prefix)<(family===4?8:32)||Number(prefix)>(family===4?32:128)))) throw new Error('Trusted proxies must be literal IPs or bounded CIDRs, never booleans, names or hop counts'); } return [...new Set(values)]; } export function operationsPort(env: NodeJS.ProcessEnv = process.env): number | undefined { if(env.WIKIDECK_OPS_PORT===undefined||env.WIKIDECK_OPS_PORT==='')return undefined; const n=Number(env.WIKIDECK_OPS_PORT); if(!/^\d+$/.test(env.WIKIDECK_OPS_PORT)||!Number.isInteger(n)||n<1||n>65535)throw new Error('Invalid WIKIDECK_OPS_PORT'); return n; } /** Separate loopback listener, disabled by default. No bearer tokens or details on public RPC. * Local users/processes share this trust boundary; never forward/publish this port. */ export function startOperations(port: number | undefined, snapshot: ()=>unknown, directory?:string) { if(port===undefined)return {close() {}}; const delay=monitorEventLoopDelay({resolution:20});delay.enable(); let disk:unknown=null,lastDiskSample=0; const sampleDisk=async()=>{ if(!directory)return; try { const fs=await statfs(directory); const sizes=await Promise.all(['ledger.sqlite','ledger.sqlite-wal'].map(async name=>{ try{return {name,bytes:(await stat(join(directory,name))).size};} catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return {name,bytes:0};throw error;} })); disk={availableBytes:fs.bavail*fs.bsize,files:sizes};lastDiskSample=Date.now(); }catch{disk={available:false,error:'Disk sample failed; inspect local filesystem and permissions.'};lastDiskSample=Date.now();} }; void sampleDisk();const diskTimer=setInterval(()=>{void sampleDisk();},30000);diskTimer.unref(); const server=createServer((req,res)=>{ res.setHeader('Cache-Control','no-store');res.setHeader('Content-Type','application/json'); if(req.method!=='GET'||req.url!=='/status'){res.writeHead(404).end('{"error":"Not found"}');return;} try { const data=JSON.stringify({schema:'wikideck-operations-v1',observedAt:new Date().toISOString(), uptimeSeconds:process.uptime(),nodeRssBytes:process.memoryUsage().rss, eventLoop:{meanMs:Number.isFinite(delay.mean)?delay.mean/1e6:0,maxMs:delay.max/1e6}, disk,diskSampledAt:lastDiskSample||null,...snapshot() as object}); if(Buffer.byteLength(data)>64*1024)throw new Error('Diagnostics size'); res.end(data); }catch{res.writeHead(503).end('{"error":"Diagnostics unavailable"}');} }); server.maxConnections=4;server.headersTimeout=5000;server.requestTimeout=5000;server.keepAliveTimeout=1000; server.setTimeout(5000,socket=>socket.destroy()); server.on('error',()=>{clearInterval(diskTimer);delay.disable();console.error('Private operations listener failed; check local monitoring configuration.');}); server.listen(port,'127.0.0.1'); return {close(){clearInterval(diskTimer);delay.disable();server.closeAllConnections();server.close();},address:()=>server.address() as AddressInfo|null}; }