Skip to content

pametan/audit-log

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

audit-log

Tamper-evident, append-only audit logging for Node: a SHA-256 hash chain (optionally keyed with HMAC), RBAC-aware events, and PII redaction built in. Built to survive an FCA / OCC / OSFI review.

TypeScript-first, with pluggable storage and one-call verification.

import { createAuditLog, MemorySink } from '@pametan/audit-log';

const log = createAuditLog({ sink: new MemorySink() });
await log.append({ action: 'loan.decision', actor: { id: 'u1', role: 'underwriter' }, outcome: 'success' });

const result = await log.verify();
// { valid: true, count: 1, errors: [] }

Why

Regulated firms must show who did what, when, and what the system decided — and prove the record wasn't altered. audit-log writes an append-only trail where each entry binds the hash of the previous one, so any edit, deletion or reordering breaks the chain and is caught by verify(). Events are structured for a reviewer, and PII is stripped before it's ever written.

Install

npm install @pametan/audit-log

Requires Node 24+. Ships ESM with bundled type declarations. Depends on @pametan/pii-redact for redaction.

Usage

Log an event

await log.append({
  action: 'payment.captured',
  actor: { id: 'u1', role: 'agent' },
  resource: { type: 'payment', id: 'pay_123' },
  outcome: 'success',
  data: { cardNumber: '4111111111111111' }, // redacted before storage -> '[PAN]'
});

append() redacts the event, computes its chain hash, and persists it, resolving to the stored AuditRecord. Appends are serialised internally so the chain stays linear under concurrency.

Verify the chain

const result = await log.verify();
if (!result.valid) console.error('Audit log integrity failure', result.brokenAt, result.errors);

verify() streams the records (constant memory) and detects edits, gaps and reordering, reporting the seq of the first broken record.

Tamper-evidence options

// Keyed HMAC: an attacker who can rewrite the store still can't forge hashes
// without the key. verify() needs the same key.
const log = createAuditLog({ sink, hmacKey: process.env.AUDIT_KEY });

Detecting truncation (important)

A hash chain detects modification, reordering and middle deletion — but not end-truncation: removing the most recent records leaves a still-consistent prefix. To catch that, periodically anchor the head somewhere outside the log, and check it:

const head = await log.head();           // { seq, hash } — store this externally
// ...later...
const result = await log.verify(head);   // fails if the log no longer reaches that head

Storage sinks

import { MemorySink, FileSink } from '@pametan/audit-log';
new MemorySink();                 // ephemeral / tests
new FileSink('/var/log/audit.jsonl'); // append-only JSON Lines, streamed on read

Postgres (separate entry point so the core has no DB dependency — pass your own pg pool):

import { Pool } from 'pg';
import { PostgresSink } from '@pametan/audit-log/postgres';

const sink = new PostgresSink(new Pool());
await sink.ensureSchema();
const log = createAuditLog({ sink });

Implement the Sink interface for anything else (S3, DynamoDB, Kafka). The Postgres sink's primary key on seq also coordinates appends across processes.

Redaction

PII redaction (via @pametan/pii-redact) runs on event.data before storage. actor, action, resource and outcome are left intact so the trail stays queryable.

createAuditLog({ sink });                          // redact data with defaults
createAuditLog({ sink, redact: { strategy: 'last4' } });
createAuditLog({ sink, redact: false });           // opt out entirely

API

Export Description
createAuditLog(options) { append, verify, head }.
append(event) Redact → chain → persist; resolves to the AuditRecord.
verify(expectedHead?) Validate the chain; pass an anchored head to catch truncation.
head() Current { seq, hash } for external anchoring.
MemorySink, FileSink Built-in sinks.
PostgresSink (/postgres) Postgres-backed sink; bring your own pg.
computeHash, canonicalStringify The hashing primitives.

Types AuditEvent, AuditRecord, Sink, Head, VerifyResult are exported.

Security & limitations

  • The chain detects modification, reordering and middle-deletion. It does not by itself detect end-truncation — anchor the head externally and use verify(expectedHead) (above).
  • Appends are serialised in-process; multi-writer setups must coordinate at the sink (the Postgres seq primary key does this).
  • A safety net, not a guarantee of compliance — pair with sound retention and access controls.

Development

npm install
npm run typecheck
npm test          # chain, tamper/gap detection, HMAC, file & postgres sinks, redaction
npm run build     # emit dist/

Note: this package depends on @pametan/pii-redact. Until that is published to npm, link it locally for development (npm link @pametan/pii-redact or a file: reference).

Disclaimer

Provided as an engineering aid, not legal or compliance advice. MIT licensed — see LICENSE.


Need the production version of this?

We're Pametan — a specialist fintech/regtech engineering agency working across UK, US and Canadian rails (FCA · CFPB · FCAC). We build the regulated, audited systems this sits inside: end-to-end audit trails, evidence stores, and the controls a reviewer expects.

Talk to us →

About

Tamper-evident, append-only audit logging for Node: SHA-256 hash chain (optional HMAC), RBAC-aware events, built-in PII redaction, pluggable sinks. Built to survive an audit.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors