4.6 KiB
middleware/audit-middleware
(no submodules)
Purpose
Builds the global Hono middleware that records one audit event per non-excluded request through the underlying AuditLogger. The middleware also installs the c.set("audit", ...) slot before invoking next() so handlers can fire explicit, semantic events through c.get("audit").log(...) regardless of whether the request itself was recorded. This file is the per-request hot path; everything else in the plugin exists to support it.
The middleware never throws into the request response — sink failures are caught, logged at warn, and surfaced via the audit.sink_failures_total counter. Telemetry must not 500 the caller.
Features
- Per-request event capture: method, path, status, duration (ms), request id, principal id, tenant id, response size, severity (info < 400, warning < 500, error otherwise).
- Configurable redaction of query, headers, and (opt-in) JSON body via
redactPayload. - Head-end sampling gate via
samplingRateand caller-supplied (or defaultMath.random) RNG. - Exclude paths (exact match) skip recording while still installing the
auditcontext slot. - Never throws — sink failures logged at warn, counted, propagated through
audit.requests_recorded_total{outcome="error",sampled="true"}. - Re-throws any error the downstream handler raised AFTER recording the audit (so the host error-handler still runs).
- Records four OTel observations: recorded counter (with
outcome+sampledlabels), excluded counter, sink-failures counter, duration histogram.
Usage: Basic
import { AuditLogger, MemoryAuditAdapter } from "@suaveplan/audit-logging";
import { createAuditMiddleware } from "@suaveplan/api-plugin-audit/middleware/audit-middleware";
import { Hono } from "hono";
const app = new Hono();
const logger = new AuditLogger({
storage: new MemoryAuditAdapter(),
secretKey: process.env.AUDIT_SECRET_KEY!,
});
app.use(
"*",
createAuditMiddleware({
logger,
config: {
sink: logger.getLogger ? logger : (undefined as never),
secretKey: "",
samplingRate: 1,
excludePaths: ["/health"],
redactQueryParams: ["token"],
redactHeaders: ["authorization"],
redactBodyFields: ["password"],
recordResponseSize: true,
recordRequestBody: false,
enableChainIntegrity: false,
rng: Math.random,
},
hostLogger: console as never,
})
);
Usage: Advanced
import { AuditLogger, MemoryAuditAdapter } from "@suaveplan/audit-logging";
import { createAuditMiddleware } from "@suaveplan/api-plugin-audit/middleware/audit-middleware";
// Deterministic sampling for tests:
let i = 0;
const seq = [0.1, 0.9, 0.4, 0.8];
const rng = () => seq[i++ % seq.length] as number;
const middleware = createAuditMiddleware({
logger: new AuditLogger({
storage: new MemoryAuditAdapter(),
secretKey: "x".repeat(32),
}),
config: {
sink: new MemoryAuditAdapter(),
secretKey: "x".repeat(32),
samplingRate: 0.5,
excludePaths: [],
redactQueryParams: ["secret"],
redactHeaders: ["authorization"],
redactBodyFields: ["password"],
recordResponseSize: true,
recordRequestBody: true, // includes JSON bodies in the audit detail
enableChainIntegrity: true,
rng,
},
hostLogger: console as never,
});
API Reference
createAuditMiddleware(deps: { logger: AuditLogger; config: AuditPluginConfig; hostLogger: LoggerLike }): MiddlewareHandlerlogger— the underlyingAuditLoggerconstructed inonInit.config— the resolvedAuditPluginConfig.hostLogger— the logger used to surface sink-write failures at warn.
The returned middleware is a standard Hono MiddlewareHandler you can register via app.use("*", ...).
Implementation Notes
c.set("audit", ...) is set BEFORE the excluded-path / sampling gates so handlers on excluded paths can still emit explicit events. The middleware reads the response status from c.res?.status after next(); when the downstream handler throws, the middleware captures the throw, sets status to 500 in the audit record, then re-throws so the host error handler still produces the actual response. JSON body capture clones c.req.raw to avoid consuming the body the handler needs to read; non-JSON bodies are not captured (binary uploads would balloon the audit volume). Sink-write failures never propagate to the request — they update three OTel counters and a warn-level log instead.