import type { AuditEntry } from "@repo/core-shared/audit"; import type { IAuditLog } from "./audit-log.interface"; /** * Fan-out wrapper. Delivers each entry to every inner sink with settle-all * semantics — one failing sink doesn't drop the audit entry from others. * * Failures emit a structured `audit-sink-error` JSON line to stderr. * Stderr (not via OTel/Sentry) avoids recursion: if Sentry is one of the * sinks failing and we routed the error back through Sentry's reporter, * we'd loop. Stderr is consumed by the same log shipper as audit entries * themselves, so the operator sees the failure in their aggregator. */ export class MultiSinkAuditLog implements IAuditLog { constructor(private readonly sinks: IAuditLog[]) {} async record(entry: AuditEntry): Promise { const results = await Promise.allSettled( this.sinks.map((s) => s.record(entry)), ); for (const r of results) { if (r.status === "rejected") { this.reportSinkError(r.reason); } } } async eraseSubject( actorId: string, mode: "pseudonymize" | "delete", ): Promise { const results = await Promise.allSettled( this.sinks.map((s) => s.eraseSubject(actorId, mode)), ); for (const r of results) { if (r.status === "rejected") { this.reportSinkError(r.reason); } } } private reportSinkError(reason: unknown): void { const line = JSON.stringify({ _type: "audit-sink-error", error: String(reason), at: new Date().toISOString(), }); process.stderr.write(line + "\n"); } }