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,129 @@
# 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<AuditPluginConfig>` — 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<AuditEvent>)` 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.