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:
@@ -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", () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user