const fs = require("fs"); const path = require("path"); class JsonlDiagnosticLog { constructor(directory, options = {}) { this.directory = directory; this.retentionMs = (options.retentionDays || 7) * 86400000; this.maxBytes = options.maxBytes || 256 * 1024 * 1024; this.includeCaptionText = options.includeCaptionText !== false; fs.mkdirSync(directory, { recursive: true }); } append(entry) { const safe = sanitize({ timestamp: new Date().toISOString(), ...entry }, this.includeCaptionText); fs.appendFileSync(this.fileForToday(), `${JSON.stringify(safe)}\n`, { encoding: "utf8", mode: 0o600 }); } cleanup(now = Date.now()) { const files = this.files(); let removed = 0; for (const file of files) { if (now - file.mtimeMs > this.retentionMs) { fs.rmSync(file.path, { force: true }); removed += 1; } } const retained = this.files(); let total = retained.reduce((sum, file) => sum + file.size, 0); for (const file of retained) { if (total <= this.maxBytes) break; fs.rmSync(file.path, { force: true }); total -= file.size; removed += 1; } return { removed, bytes: total }; } files() { return fs.readdirSync(this.directory).filter((name) => /^transcription-\d{4}-\d{2}-\d{2}\.jsonl$/.test(name)).map((name) => { const target = path.join(this.directory, name); const stat = fs.statSync(target); return { path: target, name, size: stat.size, mtimeMs: stat.mtimeMs }; }).sort((a, b) => a.mtimeMs - b.mtimeMs); } fileForToday() { return path.join(this.directory, `transcription-${new Date().toISOString().slice(0, 10)}.jsonl`); } } function sanitize(value, includeCaptionText) { if (Buffer.isBuffer(value)) return "[binary omitted]"; if (Array.isArray(value)) return value.map((entry) => sanitize(entry, includeCaptionText)); if (typeof value === "string") return sanitizeText(value); if (!value || typeof value !== "object") return value; const result = {}; for (const [key, child] of Object.entries(value)) { if (isSensitiveKey(key)) result[key] = "[redacted]"; else if (!includeCaptionText && /(?:stable|uncertain|caption)_text/i.test(key)) result[key] = "[caption text disabled]"; else result[key] = sanitize(child, includeCaptionText); } return result; } function isSensitiveKey(key) { return /(?:audio|pcm|credential|(?:^|[_-])(?:authorization|cookie|password|passwd|secret|signature|token|api[_-]?key|private[_-]?key)(?:$|[_-]))/i.test(String(key)); } function sanitizeText(value) { return String(value || "") .replace(/\b(Bearer|Basic|LumiDevice)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [redacted]") .replace(/\b((?:set-)?cookie\s*:\s*)[^\r\n]+/gi, "$1[redacted]") .replace(/([?&](?:token|key|secret|password|authorization)=)[^&#\s]+/gi, "$1[redacted]") .replace(/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|passwd|secret|signature|authorization)\s*[:=]\s*)[^\s,;}]+/gi, "$1[redacted]"); } module.exports = { JsonlDiagnosticLog, sanitize, sanitizeText };