Files
2026-05-27 23:29:53 +01:00

3.4 KiB

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"