feat(core-audit): wrap bound auditLog with TraceIdEnrichingAuditLog

bindAudit now wraps the inner sink/fan-out with TraceIdEnrichingAuditLog so
all sinks receive AuditEntry.correlationId auto-populated from the active
OTel span. bind-audit.test.ts assertions updated to check instanceof
TraceIdEnrichingAuditLog first, then .inner for the concrete sink type.
TraceIdEnrichingAuditLog exported from the package barrel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:31:19 +02:00
parent 07548c4620
commit 55993a2c93
3 changed files with 16 additions and 7 deletions

View File

@@ -7,6 +7,7 @@ import { NoopAuditLog } from "../noop-audit-log";
import { StdoutJsonAuditLog } from "../stdout-json-audit-log"; import { StdoutJsonAuditLog } from "../stdout-json-audit-log";
import { PayloadAuditLog } from "../payload-audit-log"; import { PayloadAuditLog } from "../payload-audit-log";
import { MultiSinkAuditLog } from "../multi-sink-audit-log"; import { MultiSinkAuditLog } from "../multi-sink-audit-log";
import { TraceIdEnrichingAuditLog } from "../trace-id-enriching-audit-log";
import type { IAuditLog } from "../audit-log.interface"; import type { IAuditLog } from "../audit-log.interface";
describe("bindAudit", () => { describe("bindAudit", () => {
@@ -14,28 +15,32 @@ describe("bindAudit", () => {
const container = new Container(); const container = new Container();
bindAudit(container, { payloadConfig: {} as never }); bindAudit(container, { payloadConfig: {} as never });
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog); const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
expect(auditLog).toBeInstanceOf(MultiSinkAuditLog); expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(MultiSinkAuditLog);
}); });
it("returns StdoutJsonAuditLog alone when payloadConfig omitted + default sinks", () => { it("returns StdoutJsonAuditLog alone when payloadConfig omitted + default sinks", () => {
const container = new Container(); const container = new Container();
bindAudit(container, {}); bindAudit(container, {});
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog); const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
expect(auditLog).toBeInstanceOf(StdoutJsonAuditLog); expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(StdoutJsonAuditLog);
}); });
it("returns NoopAuditLog when sinks=[]", () => { it("returns NoopAuditLog when sinks=[]", () => {
const container = new Container(); const container = new Container();
bindAudit(container, { sinks: [] }); bindAudit(container, { sinks: [] });
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog); const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
expect(auditLog).toBeInstanceOf(NoopAuditLog); expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(NoopAuditLog);
}); });
it("returns PayloadAuditLog when sinks=['payload'] only", () => { it("returns PayloadAuditLog when sinks=['payload'] only", () => {
const container = new Container(); const container = new Container();
bindAudit(container, { payloadConfig: {} as never, sinks: ["payload"] }); bindAudit(container, { payloadConfig: {} as never, sinks: ["payload"] });
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog); const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
expect(auditLog).toBeInstanceOf(PayloadAuditLog); expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(PayloadAuditLog);
}); });
it("validates AUDIT_PSEUDONYM_SALT in production", () => { it("validates AUDIT_PSEUDONYM_SALT in production", () => {

View File

@@ -7,6 +7,7 @@ import { StdoutJsonAuditLog } from "../stdout-json-audit-log";
import { MultiSinkAuditLog } from "../multi-sink-audit-log"; import { MultiSinkAuditLog } from "../multi-sink-audit-log";
import type { IAuditLog } from "../audit-log.interface"; import type { IAuditLog } from "../audit-log.interface";
import { AUDIT_SYMBOLS } from "./symbols"; import { AUDIT_SYMBOLS } from "./symbols";
import { TraceIdEnrichingAuditLog } from "../trace-id-enriching-audit-log";
export type BindAuditOpts = { export type BindAuditOpts = {
/** Payload config; required if "payload" is in sinks. */ /** Payload config; required if "payload" is in sinks. */
@@ -25,8 +26,9 @@ export type BindAuditOpts = {
* if not — better to refuse to start than to ship audit data with a dev-fallback * if not — better to refuse to start than to ship audit data with a dev-fallback
* salt that an attacker could reverse. * salt that an attacker could reverse.
* *
* Note: Phase 4 wraps the returned auditLog in TraceIdEnrichingAuditLog * The returned auditLog is wrapped in TraceIdEnrichingAuditLog (Phase 4)
* for OTel correlation. Phase 2 returns the inner sink/fan-out directly. * 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( export function bindAudit(
container: Container, container: Container,
@@ -49,10 +51,11 @@ export function bindAudit(
sinks.push(new StdoutJsonAuditLog()); sinks.push(new StdoutJsonAuditLog());
} }
const auditLog: IAuditLog = const inner: IAuditLog =
sinks.length > 1 ? new MultiSinkAuditLog(sinks) sinks.length > 1 ? new MultiSinkAuditLog(sinks)
: sinks.length === 1 ? sinks[0]! : sinks.length === 1 ? sinks[0]!
: new NoopAuditLog(); : new NoopAuditLog();
const auditLog: IAuditLog = new TraceIdEnrichingAuditLog(inner);
if (container.isBound(AUDIT_SYMBOLS.IAuditLog)) { if (container.isBound(AUDIT_SYMBOLS.IAuditLog)) {
container.unbind(AUDIT_SYMBOLS.IAuditLog); container.unbind(AUDIT_SYMBOLS.IAuditLog);

View File

@@ -6,6 +6,7 @@ export { PayloadAuditLog } from "./payload-audit-log";
export { MultiSinkAuditLog } from "./multi-sink-audit-log"; export { MultiSinkAuditLog } from "./multi-sink-audit-log";
export { auditLogsCollection } from "./audit-logs-collection"; export { auditLogsCollection } from "./audit-logs-collection";
export { bindAudit, type BindAuditOpts } from "./di/bind-audit"; export { bindAudit, type BindAuditOpts } from "./di/bind-audit";
export { TraceIdEnrichingAuditLog } from "./trace-id-enriching-audit-log";
export { AUDIT_SYMBOLS } from "./di/symbols"; export { AUDIT_SYMBOLS } from "./di/symbols";
// Phase 3 — GDPR erasure // Phase 3 — GDPR erasure
export { pseudonymize } from "./pseudonymize"; export { pseudonymize } from "./pseudonymize";