100 lines
3.6 KiB
Markdown
100 lines
3.6 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# 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 `<path>.<utc-ISO>.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"`
|