AbsoluteJS

Tamper-evident Audit & SIEM

Hash-chain the audit log so any modified, removed, or reordered entry is detectable — something WorkOS's audit product doesn't offer — and stream every event to your SIEM.

#Tamper-evident chain

createTamperEvidentSink wraps any audit sink and hash-chains each event to the one before it (HMAC-SHA256 with a secret, so an attacker who can write rows still can't forge the chain). The link is stored under metadata.__integrity — no schema change.

TS
import { auth, createNeonAuditSink, createTamperEvidentSink } from '@absolutejs/auth';

// Wrap your durable sink: every event is hash-chained to the one before it
// (HMAC-SHA256 with a secret), stored under metadata.__integrity — no schema
// change. WorkOS's audit logs offer no integrity guarantee; this does.
const auditStore = createTamperEvidentSink({
  secret: process.env.AUDIT_HMAC_KEY,
  sink: createNeonAuditSink(process.env.DATABASE_URL)
});

await auth<User>({ providersConfiguration: {}, audit: { auditStore } });

#Per-writer sharding

A single in-process chain can't span concurrent instances or survive a redeploy. So the chain is keyed per writerId — by default a fresh random id per process, so every instance and deploy is a self-contained chain that never forks another writer's. verifyAuditChain groups by writerId and verifies each independently. A single-writer deployment can pass a stable writerId (with loadWriterHead) to keep one continuous chain across restarts.

TS
import { createNeonAuditSink, createTamperEvidentSink } from '@absolutejs/auth';

// One hash-chain per WRITER. A single in-process chain can't span concurrent
// instances or survive a redeploy (each restart is a new process), so by default
// every process gets its own random writerId — each instance / deploy is a
// self-contained, independently-verifiable chain that never forks another.
const auditStore = createTamperEvidentSink({
  secret: process.env.AUDIT_HMAC_KEY,
  sink: createNeonAuditSink(process.env.DATABASE_URL)
  // writerId defaults to a fresh random id per process
});

// Single-writer deployment? Pass a stable writerId to keep ONE continuous chain
// across restarts. Supply loadWriterHead to resume without a scan (return that
// writer's most recent integrity hash, or undefined to start at genesis).
const single = createTamperEvidentSink({
  loadWriterHead: (writerId) => readLatestHashFor(writerId),
  secret: process.env.AUDIT_HMAC_KEY,
  sink: createNeonAuditSink(process.env.DATABASE_URL),
  writerId: 'audit-writer-1'
});

#Verify the chain

verifyAuditChain:

1
Walk the events
The events are walked oldest-first.
2
Recompute each link
Each link is recomputed — deterministic even after a jsonb round-trip reorders keys.
3
Report the first break
Returns the index of the first break.
TS
import { verifyAuditChain } from '@absolutejs/auth';

// Pass events oldest-first. Each writer's sub-chain is verified independently
// (grouped by writerId), detecting any modified, removed, or reordered entry and
// returning the input index of the first broken link.
const recent = (await auditStore.list?.({ limit: 1000 })) ?? [];
const result = await verifyAuditChain(
  recent.reverse(),
  process.env.AUDIT_HMAC_KEY
);

if (!result.ok) {
  console.error('audit log tampered — first broken link at', result.brokenAt);
}

#Retention & CSV export

exportAuditCsv renders events to RFC-4180 CSV (the parity piece to WorkOS's CSV export), and auditStore.prune(before) enforces a retention window by deleting older events.

Pruning necessarily drops the tamper-evidence of the removed rows.
TS
import { exportAuditCsv } from '@absolutejs/auth';

// CSV export (RFC-4180 quoted) — the parity piece to WorkOS CSV export.
const events = (await auditStore.list?.({ limit: 10000 })) ?? [];
const csv = exportAuditCsv(events.reverse()); // oldest-first

// Retention: delete events older than the window; returns the count removed.
// Note: pruning necessarily drops the tamper-evidence of the removed rows.
const ninetyDaysMs = 90 * 24 * 60 * 60 * 1000;
await auditStore.prune?.(Date.now() - ninetyDaysMs);

#SIEM streaming

createSiemLogStream forwards every event to Datadog, Splunk HEC, or any HTTP collector — the parity piece to WorkOS Log Streams. Best-effort and isolated per endpoint.

TS
import { auth, createSiemLogStream } from '@absolutejs/auth';

// Stream every audit event to your SIEM — Datadog, Splunk HEC, or any HTTP
// collector. Best-effort and isolated per endpoint (one slow sink can't block
// the auth flow). Matches WorkOS "Log Streams".
const siem = createSiemLogStream({
  endpoints: [
    {
      format: 'datadog',
      token: process.env.DD_API_KEY,
      url: 'https://http-intake.logs.datadoghq.com/api/v2/logs'
    },
    {
      format: 'splunk',
      token: process.env.SPLUNK_HEC_TOKEN,
      url: 'https://splunk.example/services/collector'
    }
  ]
});

await auth<User>({
  providersConfiguration: {},
  audit: { auditStore, onAuditEvent: (event) => siem.append(event) }
});