Files
suaveplan/openspec/archive/2026-05-26-api-audit-stdout/proposal.md
2026-05-27 23:29:53 +01:00

48 lines
3.1 KiB
Markdown

# `@suaveplan/api-audit-stdout` — stdout audit sink
**Package**: `@suaveplan/api-audit-stdout`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 8 — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Per design doc §12.5, every state-changing admin action AND every non-2xx response on an auth-required route produces an audit record with a fixed JSON shape (`ts`, `type`, `principal`, `tenant`, `request_id`, `trace_id`, `subject`, `outcome`, `cause`). The sink is pluggable via the `audit` capability. The default sink writes JSON-per-line to stdout because container runtimes redirect stdout anyway — operators wire it into their existing log pipeline without touching the host.
## What changes
- ADDED: `createStdoutAuditSink({ writer, hmacKey, clock })` — returns an `AuditSink` implementing the `audit` capability. Each `write(record)` emits a single JSON line to the supplied writer (default `process.stdout`). The line is HMAC-signed (HMAC-SHA-256 over canonical bytes) using `hmacKey` so tampering is detectable.
- ADDED: `AuditRecord` Zod schema — locks the wire shape from §12.5. Records that fail validation raise `AUDIT_STDOUT_INVALID_RECORD` and are NOT written.
- ADDED: Backpressure policy — the writer is async; the sink blocks the caller only up to `maxQueueMs` (default 100ms), then drops with `api.audit.dropped_total{sink="stdout"}` increment and a single warning log per minute. Drops are NEVER silent.
- ADDED: Error codes (`AUDIT_STDOUT_*`): `INVALID_RECORD`, `WRITER_FAILED`, `QUEUE_FULL`.
- ADDED: `api.audit.records_total{sink="stdout", type, outcome}` counter — one increment per successful write.
## Out of scope
- File-rotating sink (that's `api-audit-file`).
- Long-term audit storage (separate package).
- HMAC key management — caller supplies the key. Rotating it without losing replay capability is a host-level concern.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Audit record JSON shape (`ts`, `type`, `principal`, …) | §12.5 |
| Default sink is `api-audit-stdout` | §12.5 |
| HMAC-signed records | §12.5 |
| `audit` capability slot | §12.5 |
## Verification approach
- `src/sink/sink.test.ts` — writes valid records, asserts JSON-per-line output with HMAC suffix; invalid records raise `AUDIT_STDOUT_INVALID_RECORD`.
- `src/sink/sink.spec-conformance.test.ts` — drop policy increments `api.audit.dropped_total{sink="stdout"}` exactly once per dropped record AND emits the warning once per minute (asserted with `FakeClock`).
- `src/schema/schema.test.ts` — every required field present; every optional field accepted; unknown fields rejected (`exactOptionalPropertyTypes` parity).
- `src/hmac/hmac.test.ts` — canonical bytes are deterministic across runs; HMAC verifies with the same key; tampering flips one bit and the verification fails.
- `tests/dep-graph.test.ts` — runtime deps (`api-types`, `crypto-hash`, `error`, `logger`, `zod`).
## Migration impact
None — additive only. `api-host` registers this when `host.audit.sink = "stdout"` (the default).