# `@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).