# Capability Spec: api-audit-file **Package**: `@suaveplan/api-audit-file` **Status**: ACCEPTED > Every requirement below carries a `**Verified by:**` line. ## Requirements ### Requirement: JSON-per-line file writes with HMAC signature The package SHALL write each audit record as a single JSON line to the configured file `path` with an HMAC-SHA-256 signature computed over the canonical record bytes. #### Scenario: 1000 records write as 1000 JSON lines - WHEN 1000 valid records are written - THEN the file contains 1000 lines, each parses as JSON AND each line's HMAC verifies **Verified by:** `src/sink/sink.test.ts::"writes 1000 records as 1000 verified JSON lines"` --- ### Requirement: Size-based rotation The package SHALL rotate the file when its size reaches `rotation.maxSizeBytes` (default 100 MB). The rotated file is renamed to `..log` and a new empty file is opened. Subsequent writes go to the new file. #### Scenario: write past maxSizeBytes triggers rotation - WHEN `maxSizeBytes` is 1024 AND 2048 bytes of records are written - THEN a rotated file with an ISO-timestamp suffix exists AND subsequent writes go to a fresh file **Verified by:** `src/rotation/rotation.test.ts::"size rotation produces timestamp-suffixed file"` --- ### Requirement: Time-based rotation The package SHALL rotate when the current file's age (against the supplied `Clock`) reaches `rotation.maxAgeMs` (default 24h). Time rotation uses `FakeClock` in tests for determinism. #### Scenario: clock advance past maxAgeMs triggers rotation - WHEN `maxAgeMs` is 60_000ms AND the clock advances by 61_000ms - THEN the next write rotates first AND lands in the new file **Verified by:** `src/rotation/rotation.test.ts::"time rotation triggers on clock advance"` --- ### Requirement: Retention enforces maxFiles The package SHALL retain at most `rotation.maxFiles` (default 30) rotated files. Older files are deleted with `api.audit.rotation_evicted_total{sink="file"}` increment. #### Scenario: N+1 rotated files evicts the oldest - WHEN `maxFiles` is 3 AND a 4th rotation occurs - THEN the oldest rotated file is deleted AND `api.audit.rotation_evicted_total` increments by 1 **Verified by:** `src/retention/retention.test.ts::"evicts oldest beyond maxFiles and increments counter"` --- ### Requirement: Path-not-writeable surfaces at boot The package SHALL fail fast at sink construction if `path`'s directory is not writeable, raising `AUDIT_FILE_PATH_NOT_WRITEABLE`. #### Scenario: unwriteable path raises at boot - WHEN `path` resolves to a directory the process cannot write - THEN `createFileAuditSink` throws `AUDIT_FILE_PATH_NOT_WRITEABLE` **Verified by:** `src/sink/sink.test.ts::"unwriteable path raises AUDIT_FILE_PATH_NOT_WRITEABLE"` --- ### Requirement: Drops are observable The package SHALL drop records that exceed `maxQueueMs` and increment `api.audit.dropped_total{sink="file"}`. Drops MUST NEVER be silent. #### Scenario: drop increments dropped_total - WHEN the file writer is stalled long enough for the queue to overflow - THEN the record is dropped AND `api.audit.dropped_total{sink="file"}` increments by 1 **Verified by:** `src/sink/sink.spec-conformance.test.ts::"drop increments dropped_total with sink label file"` --- ### Requirement: HMAC verification detects tampering The package SHALL produce HMACs such that any byte change in the record invalidates verification with the same key. #### Scenario: tampering invalidates HMAC - WHEN one byte in a written line is flipped - THEN HMAC verification fails **Verified by:** `src/hmac/hmac.test.ts::"tampering invalidates HMAC"`