84 lines
3.4 KiB
Markdown
84 lines
3.4 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# Capability Spec Delta: api-audit-stdout (api-audit-stdout)
|
|
|
|
**Package**: `@suaveplan/api-audit-stdout`
|
|
**Status**: SHIPPED
|
|
|
|
> 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"`
|