# `hmac` — canonical bytes + HMAC-SHA-256 signing _(no submodules)_ ## Purpose This module produces the HMAC-SHA-256 tag attached to every audit line. Signing operates on a *canonical* byte representation of the record (keys serialised in a fixed order, `cause` omitted when absent) so the same logical record always produces the same MAC under the same key, regardless of how the source object was constructed. Verification recomputes the MAC and constant-time-compares to the hex-decoded signature. The point isn't to encrypt the record — the line is plaintext JSON written to stdout. The HMAC catches tampering downstream: a log collector or storage tier that re-writes a field to mask an audited action invalidates the MAC, and the auditor's verifier flags the discrepancy. The key never leaves the host; rotation is the caller's concern. ## Features - `canonicalBytes(record)` — UTF-8 bytes of JSON with top-level keys in design-doc §12.5 order and `cause` omitted when absent. - `signRecord(record, key)` — lowercase hex HMAC-SHA-256, computed via `@suaveplan/crypto-hash`'s `hmac("SHA-256", …)` primitive. - `verifyRecord(record, key, signatureHex)` — constant-time match; rejects bad-length, non-hex, and tampered inputs. - `HMAC_FIELD` — the `"_hmac"` field name the sink appends to the JSON line. ## Usage: Basic ```ts import { signRecord, verifyRecord } from "@suaveplan/api-audit-stdout/hmac"; const key = new Uint8Array(32); const tag = signRecord(record, key); const ok = verifyRecord(record, key, tag); ``` ## Usage: Advanced ```ts import { canonicalBytes, HMAC_FIELD, signRecord, } from "@suaveplan/api-audit-stdout/hmac"; function emit(record: AuditRecord, key: Uint8Array): string { // Equivalent to what the sink does internally, but caller-managed. const line = JSON.parse( new TextDecoder().decode(canonicalBytes(record)) ) as Record; line[HMAC_FIELD] = signRecord(record, key); return JSON.stringify(line); } ``` ## API Reference - `canonicalBytes(record: AuditRecord): Uint8Array` - `signRecord(record: AuditRecord, key: Uint8Array): string` — hex. - `verifyRecord(record: AuditRecord, key: Uint8Array, sig: string): boolean` - `HMAC_FIELD: "_hmac"` ## Implementation Notes The canonical encoder lists top-level keys explicitly rather than sorting alphabetically — the design doc fixes the order so callers reading raw lines see fields in the documented sequence. Nested objects use natural insertion order; their shape is locked by the Zod schema so the JSON.stringify output is deterministic. `cause` is omitted (not serialised as `null`) when absent so a `success` record without `cause` and a `success` record with `cause: undefined` produce the same MAC. Hex was chosen over base64 because hex is case-insensitive trivially and stays ASCII-safe through every common log pipeline.