Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
import { createPluginContext } from "@suaveplan/api-plugin";
import {
type AuditEvent,
AuditSeverity,
MemoryAuditAdapter,
} from "@suaveplan/audit-logging";
import { EventBus } from "@suaveplan/events";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { Hono } from "hono";
import { createAuditPlugin } from "../../src/plugin/plugin.js";
import type { AuditPluginConfig } from "../../src/types/types.js";
const noopLogger = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
describe("full-boot integration", () => {
it("real api + plugin + sample route records redacted audit with principal and tenant", {
timeout: 10000,
}, async () => {
const sink = new MemoryAuditAdapter();
const plugin = createAuditPlugin({
sink,
secretKey: "x".repeat(32),
redactQueryParams: ["token"],
redactHeaders: ["authorization"],
redactBodyFields: ["password"],
recordRequestBody: true,
});
const app = new Hono();
const services = new Map<string, unknown>();
services.set("app", app);
// Drive lifecycle hooks the way @suaveplan/api would.
const ctx = createPluginContext<AuditPluginConfig>({
logger: noopLogger,
eventBus: new EventBus(),
signal: new AbortController().signal,
config: plugin.defaults as AuditPluginConfig,
services,
});
await plugin.onInit?.(ctx);
// Pretend auth + tenant plugins have already populated the context.
app.use("*", async (c, next) => {
c.set("user", { id: "u1" });
c.set("tenant", { id: "t1" });
await next();
});
// Sample route that ALSO fires an explicit semantic event via
// c.get("audit") to prove the handler-level helper is wired.
app.post("/things/:id", async (c) => {
const audit = c.get("audit");
await audit.log({
type: "thing.updated",
severity: AuditSeverity.WARNING,
actor: { userId: "u1" },
target: { type: "thing", id: c.req.param("id") },
});
return c.json({ updated: true });
});
const res = await app.request("/things/42?token=secret&safe=v", {
method: "POST",
headers: {
Authorization: "Bearer top-secret",
"Content-Type": "application/json",
},
body: JSON.stringify({ password: "p", note: "hi" }),
});
expect(res.status).toBe(200);
const events = sink.snapshot();
expect(events.length).toBeGreaterThanOrEqual(2);
const requestEvt = events.find((e) =>
String(e.type).startsWith("http.")
) as AuditEvent | undefined;
expect(requestEvt).toBeDefined();
const det = requestEvt?.details as {
method: string;
path: string;
status: number;
duration_ms: number;
query: Record<string, string>;
headers: Record<string, string>;
body?: Record<string, unknown>;
};
expect(det.method).toBe("POST");
expect(det.path).toBe("/things/42");
expect(det.status).toBe(200);
expect(det.duration_ms).toBeGreaterThanOrEqual(0);
expect(det.query["token"]).toBe("[REDACTED]");
expect(det.query["safe"]).toBe("v");
expect(det.headers["authorization"]).toBe("[REDACTED]");
expect(det.body?.["password"]).toBe("[REDACTED]");
expect(det.body?.["note"]).toBe("hi");
expect(requestEvt?.actor.userId).toBe("u1");
expect(requestEvt?.tenantId).toBe("t1");
const semanticEvt = events.find((e) => e.type === "thing.updated") as
| AuditEvent
| undefined;
expect(semanticEvt).toBeDefined();
expect(semanticEvt?.target?.id).toBe("42");
});
});

View File

@@ -0,0 +1,71 @@
import { AuditLogger, MemoryAuditAdapter } from "@suaveplan/audit-logging";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { Hono } from "hono";
import { createAuditMiddleware } from "../../src/middleware/audit-middleware.js";
import type { AuditPluginConfig } from "../../src/types/types.js";
const noopHostLogger = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
/**
* Deterministic Mulberry32 RNG so the test is reproducible across
* Bun + Node runtime engines.
*/
function mulberry32(seed: number): () => number {
let t = seed >>> 0;
return () => {
t = (t + 0x6d2b79f5) >>> 0;
let r = t;
r = Math.imul(r ^ (r >>> 15), r | 1);
r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
}
describe("sampling integration", () => {
it("50% sample rate over 1000 requests sits in [400, 600] band", {
timeout: 30000,
}, async () => {
const storage = new MemoryAuditAdapter();
const logger = new AuditLogger({
storage,
secretKey: "x".repeat(32),
});
const config: AuditPluginConfig = {
sink: storage,
secretKey: "x".repeat(32),
samplingRate: 0.5,
excludePaths: [],
redactQueryParams: [],
redactHeaders: [],
redactBodyFields: [],
recordResponseSize: false,
recordRequestBody: false,
enableChainIntegrity: false,
rng: mulberry32(1234567),
};
const app = new Hono();
app.use(
"*",
createAuditMiddleware({
logger,
config,
hostLogger: noopHostLogger,
})
);
app.get("/x", (c) => c.text("ok"));
for (let i = 0; i < 1000; i += 1) {
await app.request(`/x?i=${i}`);
}
const count = storage.snapshot().length;
expect(count).toBeGreaterThanOrEqual(400);
expect(count).toBeLessThanOrEqual(600);
});
});