Initial commit
This commit is contained in:
58
packages/core-audit/src/di/bind-audit.test.ts
Normal file
58
packages/core-audit/src/di/bind-audit.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import "reflect-metadata";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Container } from "inversify";
|
||||
import { bindAudit } from "./bind-audit";
|
||||
import { AUDIT_SYMBOLS } from "./symbols";
|
||||
import { NoopAuditLog } from "../noop-audit-log";
|
||||
import { StdoutJsonAuditLog } from "../stdout-json-audit-log";
|
||||
import { PayloadAuditLog } from "../payload-audit-log";
|
||||
import { MultiSinkAuditLog } from "../multi-sink-audit-log";
|
||||
import { TraceIdEnrichingAuditLog } from "../trace-id-enriching-audit-log";
|
||||
import type { IAuditLog } from "../audit-log.interface";
|
||||
|
||||
describe("bindAudit", () => {
|
||||
it("defaults to MultiSinkAuditLog([payload, stdout]) when payloadConfig is provided", () => {
|
||||
const container = new Container();
|
||||
bindAudit(container, { payloadConfig: {} as never });
|
||||
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
|
||||
expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
|
||||
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(MultiSinkAuditLog);
|
||||
});
|
||||
|
||||
it("returns StdoutJsonAuditLog alone when payloadConfig omitted + default sinks", () => {
|
||||
const container = new Container();
|
||||
bindAudit(container, {});
|
||||
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
|
||||
expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
|
||||
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(StdoutJsonAuditLog);
|
||||
});
|
||||
|
||||
it("returns NoopAuditLog when sinks=[]", () => {
|
||||
const container = new Container();
|
||||
bindAudit(container, { sinks: [] });
|
||||
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
|
||||
expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
|
||||
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(NoopAuditLog);
|
||||
});
|
||||
|
||||
it("returns PayloadAuditLog when sinks=['payload'] only", () => {
|
||||
const container = new Container();
|
||||
bindAudit(container, { payloadConfig: {} as never, sinks: ["payload"] });
|
||||
const auditLog = container.get<IAuditLog>(AUDIT_SYMBOLS.IAuditLog);
|
||||
expect(auditLog).toBeInstanceOf(TraceIdEnrichingAuditLog);
|
||||
expect((auditLog as unknown as { inner: unknown }).inner).toBeInstanceOf(PayloadAuditLog);
|
||||
});
|
||||
|
||||
it("validates AUDIT_PSEUDONYM_SALT in production", () => {
|
||||
const env = process.env as Record<string, string | undefined>;
|
||||
const oldEnv = env["NODE_ENV"];
|
||||
const oldSalt = env["AUDIT_PSEUDONYM_SALT"];
|
||||
env["NODE_ENV"] = "production";
|
||||
delete env["AUDIT_PSEUDONYM_SALT"];
|
||||
expect(() => bindAudit(new Container(), { sinks: ["stdout"] })).toThrow(
|
||||
/AUDIT_PSEUDONYM_SALT/,
|
||||
);
|
||||
env["NODE_ENV"] = oldEnv;
|
||||
if (oldSalt) env["AUDIT_PSEUDONYM_SALT"] = oldSalt;
|
||||
});
|
||||
});
|
||||
71
packages/core-audit/src/di/bind-audit.ts
Normal file
71
packages/core-audit/src/di/bind-audit.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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<IAuditLog>(AUDIT_SYMBOLS.IAuditLog).toConstantValue(auditLog);
|
||||
|
||||
return { auditLog };
|
||||
}
|
||||
3
packages/core-audit/src/di/symbols.ts
Normal file
3
packages/core-audit/src/di/symbols.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const AUDIT_SYMBOLS = {
|
||||
IAuditLog: Symbol.for("core-audit:IAuditLog"),
|
||||
} as const;
|
||||
Reference in New Issue
Block a user