← all parts

Part · audit.log

What's actually behind audit.log

The part exactly as partkit add audit.log vendors it into your repo — verified, locked, every byte readable. Nothing here is mocked.

audit.logv1.1.0

✓ attested🔒 read-onlyaudit.log@1

Lives at parts/audit.log/ in your repo — open, owned, readable. Not buried in node_modules. 317 lines of source you can audit.

9 conformance tests passedverified 2026-06-15↗ CI run
content hash 7b1ebe7d9c…d9eb5bpinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0

Public API — what your seam calls

  • auditLog(db: SqlExecutor): AuditLog
  • AuditLog { append(event: AuditEventInput): Promise<AuditEvent>; query(filter?: AuditQuery): Promise<AuditEvent[]> }
  • class AuditError extends Error { code: AuditErrorCode }
  • types: SqlExecutor, AuditEvent, AuditEventInput, AuditQuery, AuditLog, AuditErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; inputs are validated at call time, and every failure — invalid input or a storage error from the executor — surfaces as a typed AuditError (raw driver errors never escape)
  2. append inserts exactly one row with a server-assigned monotonic id and occurred_at timestamp, returns the stored event, and the event is immediately readable via query
  3. The log is append-only: no mutation is exported, and the database itself rejects UPDATE and DELETE on the events table (trigger) — the trail cannot be rewritten even with direct table access
  4. query returns events newest-first by id, honors actor/action/target/since/until filters, and paginates deterministically via the `before` cursor under a bounded limit
  5. action, target, and metadata round-trip faithfully; every value is parameterized, so SQL metacharacters in inputs are stored literally and never executed
  6. An invalid event (blank action, over-long field, oversized metadata) or invalid query (non-positive or over-max limit) fails with a typed AuditError and issues zero SQL
  7. The part operates solely through the provided SqlExecutor seam — it imports no database driver — and every statement it issues targets only its own audit_events table

Owns in your Postgres

audit_events

Dependencies

zero-dep — runs on your Postgres
SOURCEparts/audit.log/13 files · click to read
parts/audit.log/src/index.tstypescript · 2,460 bytes
/**
 * audit.log — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 */
import { AuditError } from "./internal/errors";
import { INSERT_SQL, rowToEvent, SELECT_SQL } from "./internal/sql";
import type {
  AuditEvent,
  AuditEventInput,
  AuditLog,
  AuditQuery,
  SqlExecutor,
} from "./internal/types";
import { validateEvent, validateQuery } from "./internal/validate";

export { AuditError } from "./internal/errors";
export type { AuditErrorCode } from "./internal/errors";
export type {
  AuditEvent,
  AuditEventInput,
  AuditLog,
  AuditQuery,
  SqlExecutor,
} from "./internal/types";

/**
 * Bind an append-only audit log to a database connection (the SqlExecutor
 * seam). Constructing it performs no I/O and never throws — configuration is
 * validated, and the database touched, only when `append`/`query` run
 * (contract invariant 1, serverless-safe). Pass a per-request executor from
 * your pool; the part runs on the connection/transaction you hand it.
 */
export function auditLog(db: SqlExecutor): AuditLog {
  return {
    append: (event: AuditEventInput): Promise<AuditEvent> => appendEvent(db, event),
    query: (filter?: AuditQuery): Promise<AuditEvent[]> => queryEvents(db, filter ?? {}),
  };
}

async function appendEvent(db: SqlExecutor, event: AuditEventInput): Promise<AuditEvent> {
  const v = validateEvent(event); // throws AuditError("invalid_event") before any SQL
  let result: { rows: Record<string, unknown>[] };
  try {
    result = await db.query(INSERT_SQL, [v.actor, v.action, v.target, v.metadataJson]);
  } catch (e) {
    throw new AuditError("storage", "failed to append audit event", { cause: e });
  }
  const row = result.rows[0];
  if (row === undefined) {
    throw new AuditError("storage", "append returned no row — is the audit_events migration applied?");
  }
  return rowToEvent(row);
}

async function queryEvents(db: SqlExecutor, filter: AuditQuery): Promise<AuditEvent[]> {
  const v = validateQuery(filter); // throws AuditError("invalid_query") before any SQL
  let result: { rows: Record<string, unknown>[] };
  try {
    result = await db.query(SELECT_SQL, [
      v.actor,
      v.action,
      v.target,
      v.since,
      v.until,
      v.before,
      v.limit,
    ]);
  } catch (e) {
    throw new AuditError("storage", "failed to query audit events", { cause: e });
  }
  return result.rows.map(rowToEvent);
}