GNL
Docs menu
Enterprise@gnldev/auth-ee

Audit log

Records every authorization decision and admin action, via a console or journal-backed durable sink (createJournalAuditSink) — the actor is written from the verified identity and can't be spoofed.

What it's for / when to use it#

When you need compliance (SOC2/ISO-like) guarantees, or just a trustworthy answer to "who did what, when", two separate audit layers kick in. The first is @gnldev/auth-ee's AuditSink: it records the allow/deny decision of every authorize call (who, which path/method, which organization) — it's built into createEnterpriseAuth, so you don't need to call it by hand. The second is Studio's own admin-action log: it keeps WRITE actions like organization/user create-delete, policy updates, run purge, and agent promote readable back via GET /audit.

Critical guarantee: the actor field isn't something the client claims — it's derived from the verified identity (Principal.id). If multiple people work behind a shared token, the x-gnl-actor header lets you leave a per-person trail, but no client can impersonate someone else's identity and later claim "it wasn't me" in the record.

Setup / import#

package
import { createEnterpriseAuth, createJournalAuditSink, consoleAuditSink } from '@gnldev/auth-ee';
import type { AuditSink, AuditEvent } from '@gnldev/auth-ee';
import { toJournal } from '@gnldev/durable';

@gnldev/auth-ee is a paid/licensed package — without a valid license key, createEnterpriseAuth falls back to fallback and the given audit sink never kicks in. For a journal-backed durable sink, the storage's run journal is wrapped with toJournal (@gnldev/durable).

Step-by-step usage#

1. If you want durable audit, create a sink with createJournalAuditSink and pass it to createEnterpriseAuth as the audit option — if omitted, the default ephemeral consoleAuditSink is used (writes to console only, not durable):

src/index.ts
const auth = createEnterpriseAuth({
  licenseKey,
  userStore,
  audit: createJournalAuditSink(toJournal(storage.runs)), // durable: decisions are written to the journal
  fallback,
});

Every authorize(principal, ctx) call now automatically writes an AuditEvent to the sink via recordDecision — you don't need to write any extra code.

2. To read the written decisions back, use readJournalAudit (returns newest first):

read
import { readJournalAudit } from '@gnldev/auth-ee';

const events = await readJournalAudit(toJournal(storage.runs));
// [{ ts, principalId, orgId, path, method, action, allowed, reason }, ...]

3. This is SEPARATE from Studio's own admin-action log. Studio drops every successful WRITE action (e.g. org.create, policy.update, run.purge, agent.promote) into its own __audit__ log and serves it from an organization-scoped, filterable endpoint:

read — Studio
GET /audit?limit=200&action=policy.update&q=acme
// -> { items: [{ id, at, actor, action, target, org?, detail? }, ...] } (newest first)

An identity bound to an organization only sees its own organization's records (?org= is ignored); an unbound (operator) identity can optionally filter with ?org=.

API reference#

fncreateJournalAuditSink

(journal: JournalLike) => AuditSink. A durable sink that writes every decision to the journal under the key __eeaudit__:<ts>-<rnd>.

fnconsoleAuditSink

() => AuditSink. The ephemeral default sink that writes to console (used when audit isn't provided).

fnreadJournalAudit

(journal: JournalLike) => Promise<AuditEvent[]>. Returns every event written by createJournalAuditSink, newest first (requires listKeys).

fnrecordDecision

(sink, principal, ctx, decision) => internal helper that calls sink.write(...); called automatically inside createEnterpriseAuth.authorize.

typeAuditSink

The write(event: AuditEvent): void | Promise<void> contract — implement it to write to a custom sink (DB/SIEM).

typeAuditEvent

{ ts, principalId?, orgId?, path, method, action: "read"|"write", allowed, reason? }.

constAUDIT_PRE

The journal key prefix: '__eeaudit__:' — shared by createJournalAuditSink/readJournalAudit.

Warning
This feature lives in @gnldev/auth-ee and requires a valid license (licenseKey) — with an invalid license, createEnterpriseAuth falls back to fallback and the given audit sink never kicks in at all.
Note
consoleAuditSink is ephemeral (stdout only) — always use createJournalAuditSink for a durable, readable-back record. readJournalAudit returns an empty array if the journal doesn't support listKeys (e.g. some custom adapters).
Related
See rbac for the role/permission layer that produces authorization decisions, signed-license for license verification, agent-versioning for the audit trail of its own version/promote events, and guard-policy for the audit trail of data-driven rule changes.