# Capability Spec Delta: api-audit-stdout (api-audit-stdout) **Package**: `@suaveplan/api-audit-stdout` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. ## ADDED Requirements ### Requirement: JSON-per-line writes with HMAC signature The package SHALL write each audit record as a single JSON line to the supplied writer (default `process.stdout`). Each line MUST include an HMAC-SHA-256 signature computed over the canonical bytes of the record using the configured `hmacKey`. #### Scenario: written line is JSON with signature - WHEN `sink.write({ ts: "...", type: "request.error", ... })` is called - THEN exactly one line is appended to the writer AND that line parses as JSON AND it contains a `_hmac` field whose value verifies against the canonical bytes **Verified by:** `src/sink/sink.test.ts::"writes JSON-per-line with HMAC signature"` --- ### Requirement: Record shape validated by Zod The package SHALL validate each record against `AuditRecord` Zod schema BEFORE writing. Invalid records raise `AUDIT_STDOUT_INVALID_RECORD` and MUST NOT be written. #### Scenario: missing required field rejected - WHEN a record is missing `request_id` - THEN `write()` throws `AUDIT_STDOUT_INVALID_RECORD` AND no line is written #### Scenario: unknown field rejected - WHEN a record carries an unknown top-level field - THEN `write()` throws `AUDIT_STDOUT_INVALID_RECORD` **Verified by:** `src/schema/schema.test.ts::"missing request_id is rejected"` AND `src/schema/schema.test.ts::"unknown field is rejected"` --- ### Requirement: Backpressure drops are observable The package SHALL block the caller up to `maxQueueMs` (default 100ms) and then drop the record with `api.audit.dropped_total{sink="stdout"}` increment AND a single warning log per minute. Drops MUST NEVER be silent. #### Scenario: drop increments dropped_total - WHEN the writer is paused such that the queue exceeds `maxQueueMs` - THEN the record is dropped AND `api.audit.dropped_total{sink="stdout"}` increments by 1 #### Scenario: warning log throttled to once per minute - WHEN 100 records are dropped within 30 seconds (with `FakeClock`) - THEN at most one warning is logged in that minute window **Verified by:** `src/sink/sink.spec-conformance.test.ts::"drop increments dropped_total"` AND `src/sink/sink.spec-conformance.test.ts::"warning log throttled to once per minute"` --- ### Requirement: Successful writes counted with labels The package SHALL increment `api.audit.records_total{sink="stdout", type, outcome}` once per successful write. #### Scenario: successful write increments counter - WHEN a valid record is written - THEN `api.audit.records_total{sink="stdout", type, outcome}` increments by exactly 1 with the record's `type` and `outcome` as labels **Verified by:** `src/sink/sink.spec-conformance.test.ts::"successful write increments records_total with type and outcome labels"` --- ### Requirement: HMAC verification detects tampering The package SHALL produce HMAC signatures such that any modification to the record's canonical bytes invalidates the signature on verification with the same key. #### Scenario: tampering invalidates HMAC - WHEN a written line's `outcome` is flipped from `success` to `failure` after signing - THEN HMAC verification with the original key returns `false` **Verified by:** `src/hmac/hmac.test.ts::"tampering invalidates HMAC"`