@usebetterdev/audit-hono
v0.8.1
Published
Hono 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-hono
Hono 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-hono @usebetterdev/audit-coreQuick start
import { Hono } from "hono";
import { betterAudit } from "@usebetterdev/audit-core";
import { betterAuditHono } from "@usebetterdev/audit-hono";
const audit = betterAudit({
database: { writeLog: async (log) => console.log(log) },
auditTables: ["users", "orders"],
});
const app = new Hono();
// Use the middleware — by default it reads the `sub` claim
// from the Authorization: Bearer <jwt> header.
app.use("*", betterAuditHono(audit));
app.post("/users", async (c) => {
// actorId is automatically attached from the JWT
await audit.captureLog({
tableName: "users",
operation: "INSERT",
recordId: "user-42",
after: { name: "Alice" },
});
return c.json({ id: "user-42" }, 201);
});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("*", betterAuditHono(audit));
// 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(
"*",
betterAuditHono(audit, {
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(
"*",
betterAuditHono(audit, {
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(
"*",
betterAuditHono(audit, {
extractor: { actor: fromCookie("session_id") },
}),
);Custom extractor function
Write your own ValueExtractor for full control. It receives the raw Request and returns a string or undefined:
app.use(
"*",
betterAuditHono(audit, {
extractor: {
actor: async (request) => {
const apiKey = request.headers.get("x-api-key");
if (!apiKey) {
return undefined;
}
// Look up the API key owner
const owner = await resolveApiKeyOwner(apiKey);
return owner?.id;
},
},
}),
);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(
"*",
betterAuditHono(audit, {
onError: (error) => console.error("Audit extraction failed:", error),
}),
);API
betterAuditHono(audit, options?)
Convenience wrapper that returns a Hono middleware. Equivalent to createHonoMiddleware.
createHonoMiddleware(audit, options?)
Creates a Hono-compatible middleware function.
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. |
