# Add @suaveplan/api-plugin-audit — write-through audit trail for @suaveplan/api **Package**: `@suaveplan/api-plugin-audit` (NEW, server-tier) **Status**: PROPOSED **Type**: new-package - **Spec delta**: ./specs/api-plugin-audit/spec.md ## Why Genesis ships `@suaveplan/api` (Hono-based orchestrator), `@suaveplan/api-plugin` (`definePlugin` foundation + `ServiceRegistry`), and `@suaveplan/audit-logging` (universal `AuditLogger` with HMAC-SHA256 chain-integrity, pluggable `AuditStorageAdapter`, retention, GDPR export). What is missing is the glue: a `definePlugin` consumer that installs a global Hono middleware to record an audit event for every request — method, path, status, duration, response size, request id, principal id (from `c.get("user")?.id`), tenant id (from `c.get("tenant")?.id`) — and exposes `auditService` in `ServiceRegistry` plus an `audit` slot on the Hono `ContextVariableMap` for handler-level events. This is the compliance/audit half of the api-plugin trio (auth → audit → rate-limit). The upstream `enterprise/api-plugin-audit.md` (126 lines) originally described an in-memory `AuditService`; this genesis package replaces that with the canonical write-through over `@suaveplan/audit-logging`, picking up its chain-integrity, GDPR export, retention, and pluggable sinks for free. ## What changes Net-new package at `0.1.0`. ADDED: ### Plugin factory + descriptor - `createAuditPlugin(options: AuditPluginOptions): PluginDescriptor` — Zod-validated factory. Invalid input → `AuditError(AUDIT_PLUGIN_INVALID_INPUT)` carrying `ZodError` as `cause`. Descriptor declares `name: "audit"`, version, defaults, lifecycle hooks. - `AuditPluginOptions` (Zod-validated): `{ logger?: LoggerLike; sink?: AuditStorageAdapter (default: MemoryAuditAdapter); secretKey?: string (default: generated dev key with warning); routePrefix?: string (default "/"); samplingRate?: number 0..1 (default 1); excludePaths?: readonly string[] (default ["/health","/healthz","/ready","/openapi.json"]); redactQueryParams?: readonly string[]; redactHeaders?: readonly string[]; redactBodyFields?: readonly string[]; recordResponseSize?: boolean (default true); recordRequestBody?: boolean (default false); enableChainIntegrity?: boolean (default false); }`. ### Sink injection Caller supplies a real `AuditStorageAdapter` (file/HTTP/DB/SurrealDB/S3). Default is `MemoryAuditAdapter` from `@suaveplan/audit-logging` for dev. In production, operators wire a durable sink. The plugin owns the `AuditLogger` lifecycle (constructed in `onInit`, retained in plugin state, exposed via `auditService` and the `audit` context slot). ### Global request-audit middleware `onInit` installs a single `app.use("*", ...)` middleware that, for every non-excluded request: 1. Captures `Date.now()` start, request id (from `c.get("requestId")` or `c.req.header("x-request-id")` or generated ULID-like), redacted query, redacted headers, optionally redacted body. 2. `await next()`. 3. Records: `method`, `path`, `status` (from `c.res.status`), `duration_ms`, `response_size` (from `Content-Length` header when present), `request_id`, `principal_id` (`c.get("user")?.id`), `tenant_id` (`c.get("tenant")?.id`), `severity` (info < 400, warning < 500, error otherwise). 4. Honors `samplingRate` (Math.random gate) — when skipped, neither record nor sink call happens. 5. Honors `excludePaths` (exact match — health probes, openapi doc). The middleware never throws into the request path — a sink failure logs at warn via `LoggerLike` and increments `audit.sink_failures_total` but the request response is unaffected. ### Per-handler audit helper `c.get("audit")` returns an `AuditContext` instance exposing `log(event: Partial)` and `getLogger(): AuditLogger`. Handlers fire explicit, semantic events (e.g. user-deleted, vault-unlock) alongside the implicit request audit. The package augments Hono's `ContextVariableMap` with `audit: AuditContext`. ### Service registry slot - `ctx.registerService("auditService", auditService)` where `auditService: AuditService` exposes `getLogger()`, `log(event)`, `query(opts)`, `verifyChainIntegrity(events)`, `exportGDPRData(userId, signal?)`, `applyRetentionPolicy(signal?)`. ServiceRegistry module augmentation declares the slot. ### Redaction - `redactPayload(payload, rules)` — accepts query/header/body field-name lists, returns a shallow-redacted clone replacing matched values with `[REDACTED]`. Header matching is case-insensitive. Defaults cover common secret-bearing names (`authorization`, `cookie`, `set-cookie`, `x-api-key`, plus body `password`, `secret`, `token`, plus query `token`, `code`, `state`). - Uses `@suaveplan/data-masking`'s `maskObject` for body-field traversal when caller supplies custom rules expressed as `MaskRule[]`; otherwise the bundled fast-path covers the default name list without pulling the masking-rule machinery. ### Errors + OTel - `AUDIT_PLUGIN_CODES` (`defineCodes("AUDIT_PLUGIN", [...])`): `INVALID_INPUT`, `SERVICE_NOT_AVAILABLE`, `SINK_INIT_FAILED`, `SINK_WRITE_FAILED`. Every code raised by a production code path. - OTel: counter `audit.requests_recorded_total{outcome,sampled}`, counter `audit.sink_failures_total`, counter `audit.requests_excluded_total`, histogram `audit.middleware_duration_ms`. ### Subpath exports `.`, `./plugin/plugin`, `./middleware/audit-middleware`, `./services/audit-service`, `./context/audit-context`, `./redact/redact`, `./schemas/options`, `./error-codes/error-codes`, `./otel/instruments`, `./types/types`. ## Out of scope - **HTTP routes for querying audit** (e.g. `GET /audit?...`) — the in-process `auditService` + sink-of-choice already cover query; a follow-up `api-plugin-audit-routes` could publish authenticated read endpoints (must depend on `api-plugin-auth`). - **Real durable sinks (SurrealDB / HTTP / SIEM forwarder)** — the package exposes the `AuditStorageAdapter` contract from `@suaveplan/audit-logging`; siblings ship the concrete sinks. - **Hash-chain verification CLI / scheduler-driven retention** — `auditService.verifyChainIntegrity` / `applyRetentionPolicy` are exposed; orchestrating them on a cron belongs in `@suaveplan/job-scheduler` consumers, not this plugin. - **Frontend UI** — backend only. - **PII pseudonymisation via `@suaveplan/data-masking` MaskRules in the request path** — the redactor uses a fast `[REDACTED]` substitution for the request-audit path; full mask-rule machinery is exposed for handler-level `c.get("audit").log({ details: maskObject(payload, rules) })` flows. ## Source mapping This change derives from `../suaveplan/enterprise/api-plugin-audit.md` (canonical, 126 lines). | Requirement | Source lines | Notes | |---|---|---| | Hono plugin wrapper (`definePlugin`) | L44-L57 | Upstream flagged the gap that this package fills | | Dependency on `@suaveplan/api` | L46 | Verbatim — package must `definePlugin` not be a standalone service | | `AuditEntry` records with severity + action | L12, L22 | Replaced upstream's bespoke types with `@suaveplan/audit-logging`'s `AuditEvent` + `AuditSeverity` (L51 explicitly notes the overlap) | | Wrap `@suaveplan/audit-logging` | L51, L56 | Upstream identified the overlap with the universal package; this change resolves it by making `api-plugin-audit` a wrapper | | HTTP routes for query/export | L62 | Scoped out per "Out of scope"; surfaced through `auditService` for now | | Auth-gated read endpoints | L63 | Scoped out — gated behind future `api-plugin-audit-routes` | | Retention enforcement via scheduler | L64 | `auditService.applyRetentionPolicy` exposed; orchestration is consumer responsibility | | PII masking via data-masking | L65 | Implemented via `redactPayload` + handler-level `maskObject` integration | | Append-only / hash-chain integrity | L61, L84 | Inherited from `@suaveplan/audit-logging`'s `enableChainIntegrity` option | | Globally-unique IDs (ULID/UUID) | L60 | Inherited — `AuditLogger` uses `crypto.randomUUID()` by default | | OTel correlation | L92 | Counters + histogram emitted from middleware | | 100% test + docs coverage | L69, L70 | Genesis-wide policy enforced via gates | ### Requirements with no source line | Requirement | Justification | |---|---| | `ServiceRegistry` typed slot for `auditService` | Genesis `@suaveplan/api-plugin` augmentation pattern (mirror of `api-plugin-auth` `authService` slot) | | Hono `ContextVariableMap` augmentation for `audit` slot | Genesis context-augmentation pattern (mirror of api-plugin-auth `user`/`session`) | | Sampling-rate gate | Genesis-wide policy: production audit volumes require head-end sampling | | `excludePaths` for health probes | Avoids audit-noise on liveness/readiness traffic | | Middleware never throws into request path | Standard observability rule — telemetry failures must not 500 | ## Verification approach Every requirement in the spec delta has a `**Verified by:**` line. Test files that will exist after this change ships: - `src/plugin/plugin.test.ts` — factory shape, Zod validation, lifecycle hook presence - `src/plugin/on-init.test.ts` — lifecycle: service resolution, sink wiring, middleware install - `src/middleware/audit-middleware.test.ts` — per-request behavior: capture, redact, sample, exclude, never-throw - `src/services/audit-service.test.ts` — log/query/export/retention proxies - `src/services/audit-service.spec-conformance.test.ts` — ServiceRegistry slot typing - `src/context/audit-context.test.ts` — `c.get("audit").log(...)` - `src/redact/redact.test.ts` — case-insensitive headers, query, body fields - `src/schemas/options.test.ts` — Zod boundaries - `src/otel/instruments.test.ts` — counters/histogram emit - `src/api-plugin-audit.spec-conformance.test.ts` — every `AUDIT_PLUGIN_*` code thrown from prod - `tests/integration/full-boot.test.ts` — real api + plugin + sample route → sink receives event with redacted secrets, principal/tenant/status/duration correct - `tests/integration/sampling.test.ts` — 50% sampling over 1000 requests in `[400, 600]` - `tests/dep-graph.test.ts` — every `.ts` has sibling `.md`; every subpath resolves ## Migration impact None — additive only. The legacy upstream `@suaveplan/api-plugin-audit` shipped a different surface (in-memory `AuditService` with no plugin wrapper). Genesis ships zero existing consumers of the legacy surface, so this is a clean rewrite at `0.1.0`. ## Impact ### Affected dependents No dependents (verified by `bunx turbo ls --filter=...@suaveplan/api-plugin-audit` — package does not yet exist). ### Catalog additions required None — all dependencies (`@suaveplan/api`, `@suaveplan/api-plugin`, `@suaveplan/audit-logging`, `@suaveplan/data-masking`, `@suaveplan/error`, `@suaveplan/types`, `hono`, `zod`, `@opentelemetry/api`, `tslib`) already exist in catalog/workspace.