Files
Multi-Debrid-Downloader/src/main/audit-log.ts
T
Sucukdeluxe 25ebc55f4f fix: strengthen live recovery and support diagnostics
Apply account and key changes to active queues without a restart and isolate provider attempt cancellation so fallback accounts remain usable. Preserve pause ownership, bound persisted HTTP 416 recovery, reconcile resets with authoritative state, and stabilize package ordering and live update cadence. Correlate rotation, conversion, resume, disk, queue-control, clipboard, and support-export events while redacting sensitive data at every persistent boundary and again in generated bundles. Release as v2.0.31 with updated English documentation and regression coverage.
2026-08-13 11:36:51 +02:00

122 lines
3.4 KiB
TypeScript

import fs from "node:fs";
import { logTimestamp } from "./log-timestamp";
import path from "node:path";
import { sanitizeDiagnosticFields, sanitizeDiagnosticText } from "./diagnostic-sanitizer";
type AuditLevel = "INFO" | "WARN" | "ERROR";
const AUDIT_LOG_MAX_FILE_BYTES = Number(process.env.RD_AUDIT_LOG_MAX_BYTES || 10 * 1024 * 1024);
const AUDIT_LOG_RETENTION_DAYS = Number(process.env.RD_AUDIT_LOG_RETENTION_DAYS || 30);
let auditLogPath: string | null = null;
function sanitizeFieldValue(value: unknown): string {
if (value === undefined || value === null) {
return "";
}
if (typeof value === "string") {
return value.replace(/\r?\n/g, "\\n");
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
try {
return JSON.stringify(value).replace(/\r?\n/g, "\\n");
} catch {
return String(value);
}
}
function formatFields(fields?: Record<string, unknown>): string {
const safeFields = sanitizeDiagnosticFields(fields);
if (!safeFields) {
return "";
}
const parts = Object.entries(safeFields)
.filter(([, value]) => value !== undefined && value !== null && sanitizeFieldValue(value) !== "")
.map(([key, value]) => `${key}=${sanitizeFieldValue(value)}`);
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function rotateIfNeeded(filePath: string): void {
try {
const stat = fs.statSync(filePath);
if (stat.size < AUDIT_LOG_MAX_FILE_BYTES) {
return;
}
const backup = `${filePath}.old`;
try {
fs.rmSync(backup, { force: true });
} catch {
}
fs.renameSync(filePath, backup);
} catch {
}
}
function cleanupOldBackup(filePath: string): void {
const backup = `${filePath}.old`;
try {
const stat = fs.statSync(backup);
const cutoff = Date.now() - AUDIT_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
if (stat.mtimeMs < cutoff) {
fs.rmSync(backup, { force: true });
}
} catch {
}
}
export function initAuditLog(baseDir: string): void {
auditLogPath = path.join(baseDir, "audit.log");
try {
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true });
cleanupOldBackup(auditLogPath);
if (!fs.existsSync(auditLogPath)) {
fs.writeFileSync(auditLogPath, "", "utf8");
}
rotateIfNeeded(auditLogPath);
if (!fs.existsSync(auditLogPath)) {
fs.writeFileSync(auditLogPath, "", "utf8");
}
fs.appendFileSync(auditLogPath, `=== Audit-Log Start: ${logTimestamp()} ===\n`, "utf8");
} catch {
auditLogPath = null;
}
}
export function logAuditEvent(level: AuditLevel, message: string, fields?: Record<string, unknown>): void {
if (!auditLogPath) {
return;
}
try {
rotateIfNeeded(auditLogPath);
if (!fs.existsSync(auditLogPath)) {
fs.writeFileSync(auditLogPath, "", "utf8");
}
fs.appendFileSync(
auditLogPath,
`${logTimestamp()} [${level}] ${sanitizeDiagnosticText(message)}${formatFields(fields)}\n`,
"utf8"
);
} catch {
}
}
export function getAuditLogPath(): string | null {
if (!auditLogPath) {
return null;
}
return fs.existsSync(auditLogPath) ? auditLogPath : null;
}
export function shutdownAuditLog(): void {
if (!auditLogPath) {
return;
}
try {
fs.appendFileSync(auditLogPath, `=== Audit-Log Ende: ${logTimestamp()} ===\n`, "utf8");
} catch {
}
auditLogPath = null;
}