@usebetterdev/audit-express
v0.8.1
Published
Express middleware for [`@usebetterdev/audit-core`](../core). Automatically extracts actor identity from incoming requests and makes it available to audit logging via `AsyncLocalStorage`.
Readme
@usebetterdev/audit-express
Express middleware for @usebetterdev/audit-core. Automatically extracts actor identity from incoming requests and makes it available to audit logging via AsyncLocalStorage.
Installation
pnpm add @usebetterdev/audit-express @usebetterdev/audit-coreQuick start
import express from "express";
import { betterAudit } from "@usebetterdev/audit-core";
import { betterAuditExpress } from "@usebetterdev/audit-express";
const audit = betterAudit({
database: { writeLog: async (log) => console.log(log) },
auditTables: ["users", "orders"],
});
const app = express();
app.use(express.json());
// Use the middleware — by default it reads the `sub` claim
// from the Authorization: Bearer <jwt> header.
app.use(betterAuditExpress());
app.post("/users", async (req, res, next) => {
try {
// actorId is automatically attached from the JWT
await audit.captureLog({
tableName: "users",
operation: "INSERT",
recordId: "user-42",
after: { name: "Alice" },
});
res.status(201).json({ id: "user-42" });
} catch (error) {
next(error);
}
});Actor extraction
The middleware resolves the current actor (the user or service making the request) and stores it in AuditContext.actorId. Every log captured during that request automatically includes the actor.
Default: JWT Bearer token
With no options, the middleware decodes the sub claim from the Authorization: Bearer <jwt> header. The token is decoded without signature verification — that is the auth layer's responsibility.
app.use(betterAuditExpress());
// Authorization: Bearer eyJ... → actorId = jwt.subCustom JWT claim
Extract a different claim by providing a custom extractor:
import { fromBearerToken } from "@usebetterdev/audit-core";
app.use(betterAuditExpress({ extractor: { actor: fromBearerToken("user_id") } }));Header-based extraction
Use fromHeader when the actor identity is passed as a plain request header (common behind API gateways):
import { fromHeader } from "@usebetterdev/audit-core";
app.use(betterAuditExpress({ extractor: { actor: fromHeader("x-user-id") } }));Cookie-based extraction
Use fromCookie for session-based auth where the actor ID lives in a cookie:
import { fromCookie } from "@usebetterdev/audit-core";
app.use(betterAuditExpress({ extractor: { actor: fromCookie("session_id") } }));Custom extractor function
Write your own ValueExtractor for full control. It receives a Web-standard Request and returns a string or undefined:
app.use(
betterAuditExpress({
extractor: {
actor: async (request) => {
const apiKey = request.headers.get("x-api-key");
if (!apiKey) return undefined;
const owner = await resolveApiKeyOwner(apiKey);
return owner?.id;
},
},
}),
);ALS scope and async handlers
The middleware keeps the AsyncLocalStorage scope open until the response finishes (via response.on('finish'/'close')). This means audit context is available even after await inside async route handlers — the context does not reset at the first await boundary.
Error handling
Extraction errors never break the request. By default the middleware fails open — if an extractor throws, the request proceeds without audit context.
Use onError to log or report extraction failures:
app.use(
betterAuditExpress({
onError: (error) => console.error("Audit extraction failed:", error),
}),
);API
betterAuditExpress(options?)
Convenience wrapper that returns an Express middleware. Equivalent to createExpressMiddleware.
createExpressMiddleware(options?)
Creates an Express-compatible middleware function (req, res, next) => Promise<void>.
Options:
| Option | Type | Description |
| ----------- | -------------------------- | ---------------------------------------------------- |
| extractor | ContextExtractor | Actor extractor config. Defaults to JWT sub claim. |
| onError | (error: unknown) => void | Called when an extractor throws. Defaults to no-op. |
