npm.io
0.1.1 • Published yesterday

@gomagentic/verdict-audit

Licence
Apache-2.0
Version
0.1.1
Deps
1
Size
29 kB
Vulns
0
Weekly
0

@gomagentic/verdict-audit

Async audit pipeline: batched, bounded, retrying. Never blocks a decision.

Part of Verdict — a serverless-first authorization engine. Policies (RBAC / ABAC / ReBAC) compile once and decide in microseconds, embedded in your app, behind a central PDP, or synced to the edge.

Authorization decisions are the hot path; writing the audit trail is not. This sink buffers decision records in memory and flushes them in batches to your store or queue. The buffer is bounded: past its limit it drops the oldest records — and counts the drops — rather than growing without limit or making a request wait on audit I/O. An audit outage degrades to dropped records, never to failed or slowed decisions.

Install

npm install @gomagentic/verdict-audit

Usage

Wrap any downstream sink — anything with writeAudit(records), such as a @gomagentic/verdict-store audit sink — in a BufferedAuditSink. Calls to writeAudit append to the buffer and return immediately; flushes happen on batch size, on interval, or explicitly.

import { BufferedAuditSink } from "@gomagentic/verdict-audit";
import type { AuditRecord } from "@gomagentic/verdict-core";

// `store` is any AuditSinkLike: async writeAudit(records: readonly AuditRecord[])
const sink = new BufferedAuditSink(store, {
  maxBatch: 200,          // records per downstream write (default 200)
  flushIntervalMs: 2_000, // time-based flush (default 2000 ms)
  maxBuffered: 10_000,    // bounded buffer; oldest dropped beyond this (default 10 000)
  onError: (err) => console.error("audit write failed", err),
  onDrop: (count) => console.warn(`audit dropped ${count} records`),
});

// On the request path: fire-and-forget, never awaited on the hot path.
await sink.writeAudit(records); // returns as soon as the records are buffered

// On Workers, drain in the background instead of blocking the response:
ctx.waitUntil(sink.flush());

// On shutdown (or between requests elsewhere): final drain.
await sink.stop(); // flushes, then rejects further records

flush() drains the buffer in maxBatch chunks and coalesces concurrent callers; stop() performs a final flush and stops accepting records.

Backpressure & drops

The buffer is capped at maxBuffered. When it would overflow, the oldest records are evicted (recent history wins) and the drop count is incremented — onDrop fires with the number evicted. A downstream write failure increments failures, invokes onError, and re-queues the batch at the front (subject to the same bound) to retry on the next write, interval, or explicit flush().

The stats getter exposes live counters:

const { buffered, written, dropped, failures } = sink.stats;
// buffered — records currently in memory awaiting flush
// written  — records successfully handed to the downstream sink
// dropped  — records evicted because the buffer was full
// failures — downstream write failures (each re-queues its batch)

Queue adapter

QueueAuditSink adapts a batch queue — anything exposing sendBatch(messages: { body: AuditRecord }[]), such as Cloudflare Queues, SQS, or a Kafka producer — into an AuditSinkLike. Each record becomes one queue message for durable, out-of-band delivery. Compose it under a BufferedAuditSink to get batching and bounded buffering in front of the queue:

import { BufferedAuditSink, QueueAuditSink } from "@gomagentic/verdict-audit";

// `env.AUDIT_QUEUE` is a Cloudflare Queue binding (has sendBatch)
const sink = new BufferedAuditSink(new QueueAuditSink(env.AUDIT_QUEUE));

Documentation

License

Apache-2.0

Keywords