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

2.7 KiB

schema

(no submodules)

Purpose

Defines the Zod schema and canonical-bytes function that bracket every audit record before it is signed and written. The schema mirrors the RequestAuditEnvelope interface exported by @suaveplan/api-host/request-pipeline; the canonical-bytes function gives every other module a deterministic input for HMAC signing, file-line construction, and signature verification.

The package re-validates at the sink boundary even though the host constructs envelopes from typed data — operators frequently hand-craft audit records in tests, replay tooling, or migration scripts. The boundary check makes corrupt records fail loudly with a typed AUDIT_FILE.INVALID_RECORD instead of silently bypassing the HMAC.

Features

  • AuditRecordSchema — strict Zod object accepting only the envelope's documented fields. Unknown keys reject.
  • AuditRecord — z.infer<typeof AuditRecordSchema>; structurally assignable to RequestAuditEnvelope.
  • canonicalise(record) — UTF-8 bytes of the record JSON-stringified with sorted top-level keys.
  • buildSignedLine(bytes, hmacHex) — emits the on-disk JSON line carrying the record + HMAC, with a trailing \n.

Usage: Basic

import {
    AuditRecordSchema,
    buildSignedLine,
    canonicalise,
} from "@suaveplan/api-audit-file/schema";

const record = AuditRecordSchema.parse(rawEnvelope);
const bytes = canonicalise(record);
const line = buildSignedLine(bytes, "deadbeef...");

Usage: Advanced

Verify an on-disk line:

import { hmac } from "@suaveplan/crypto-hash";
import { AuditRecordSchema, canonicalise } from "@suaveplan/api-audit-file/schema";

const { record, hmac: hmacHex } = JSON.parse(lineFromDisk);
const parsed = AuditRecordSchema.parse(record);
const bytes = canonicalise(parsed);
const expected = Buffer.from(hmac("SHA-256", key, bytes)).toString("hex");
if (expected !== hmacHex) {
    throw new Error("hmac mismatch — record was tampered");
}

API Reference

  • AuditRecordSchema: z.ZodObject (strict)
  • type AuditRecord = z.infer<typeof AuditRecordSchema>
  • canonicalise(record: AuditRecord): Uint8Array
  • buildSignedLine(canonicalBytes: Uint8Array, hmacHex: string): string

Implementation Notes

The canonical bytes drop undefined values for the optional cause slot so the absence of an error never affects the signature. Top-level keys are sorted alphabetically to give every implementation (TypeScript, Go log-reader, Python forensic tooling) the same input bytes for verification regardless of construction order. principal.scopes is preserved in array order — the order is semantically meaningful in the pipeline so we do not collapse it.