import "reflect-metadata"; import type { Container } from "inversify"; import { getPayload as _getPayload, type SanitizedConfig } from "payload"; import { NoopAuditLog } from "../noop-audit-log"; import { PayloadAuditLog } from "../payload-audit-log"; import { StdoutJsonAuditLog } from "../stdout-json-audit-log"; import { MultiSinkAuditLog } from "../multi-sink-audit-log"; import type { IAuditLog } from "../audit-log.interface"; import { AUDIT_SYMBOLS } from "./symbols"; import { TraceIdEnrichingAuditLog } from "../trace-id-enriching-audit-log"; export type BindAuditOpts = { /** Payload config; required if "payload" is in sinks. */ payloadConfig?: SanitizedConfig; /** Sink selection. Default ["payload", "stdout"]. */ sinks?: ("payload" | "stdout")[]; }; /** * Binds an `IAuditLog` impl to the container under `AUDIT_SYMBOLS.IAuditLog`. * * Default sink set: ["payload", "stdout"] — Payload local cache + structured * JSON to stdout (operator wires a log shipper to the centralized aggregator). * * In production, AUDIT_PSEUDONYM_SALT env var MUST be set. Boot fails fast * if not — better to refuse to start than to ship audit data with a dev-fallback * salt that an attacker could reverse. * * The returned auditLog is wrapped in TraceIdEnrichingAuditLog * so all sinks receive AuditEntry.correlationId auto-populated from the * active OTel span. The inner sink/fan-out is accessible via `.inner`. */ export function bindAudit( container: Container, opts: BindAuditOpts = {}, ): { auditLog: IAuditLog } { if ( process.env.NODE_ENV === "production" && !process.env.AUDIT_PSEUDONYM_SALT ) { throw new Error( "AUDIT_PSEUDONYM_SALT environment variable is required in production. " + "Generate via `openssl rand -hex 32` and store in your secrets manager.", ); } const sinkList = opts.sinks ?? ["payload", "stdout"]; const sinks: IAuditLog[] = []; if (sinkList.includes("payload") && opts.payloadConfig) { // eslint-disable-next-line @typescript-eslint/no-explicit-any sinks.push(new PayloadAuditLog(opts.payloadConfig, _getPayload as any)); } if (sinkList.includes("stdout")) { sinks.push(new StdoutJsonAuditLog()); } const inner: IAuditLog = sinks.length > 1 ? new MultiSinkAuditLog(sinks) : sinks.length === 1 ? sinks[0]! : new NoopAuditLog(); const auditLog: IAuditLog = new TraceIdEnrichingAuditLog(inner); if (container.isBound(AUDIT_SYMBOLS.IAuditLog)) { container.unbind(AUDIT_SYMBOLS.IAuditLog); } container.bind(AUDIT_SYMBOLS.IAuditLog).toConstantValue(auditLog); return { auditLog }; }