auth/audit_emitter.ts

Bound audit-emit capability.

AuditEmitter closes over the pool-level Db, the on_audit_event subscriber chain, and the optional AuditLogConfig. Built by the consumer's audit_factory callback on CreateAppBackendOptionscreate_app_backend invokes the factory once with its constructed {db, log} and lands the result on AppDeps.audit. Consumers reach for deps.audit.emit(ctx, input) and never see the pool — handlers cannot accidentally emit an audit event against the request's transactional db (which would be rolled back with the parent on a handler throw).

Four methods cover every fan-out shape the auth domain needs:

- emit(ctx, input) — fire-and-forget pool write. Pushes the in-flight promise onto ctx.pending_effects for post-response flushing. Errors are logged, never thrown. Returns void so callers don't pile up void keywords or accidentally await something whose handle is already in pending_effects. - emit_role_grant_target(ctx, auth, input) — wrapper that lifts the actor_id / account_id / ip boilerplate every role-grant-shape audit site repeated. Delegates to emit. - emit_pool(input) — awaitable pool write for code paths without a pending_effects queue (cleanup sweeps, ad-hoc maintenance scripts). Same write-then-notify semantics as emit, just synchronous-with-await. - notify(event) — fan out an already-written audit row (e.g. rows returned by query_accept_offer that were inserted in-transaction by the query layer). Runs every listener on the chain; per-listener throws are isolated.

The chain is a documented mutable seam — create_app_server appends additional listeners after the backend is built (the factory-managed audit-log SSE, per-endpoint WS auth guards and logout closers, any extra_audit_handlers on a WsEndpointSpec) before the first request runs. Consumers can also append listeners directly on the emitter they return from audit_factory for setups that don't pass through create_app_server.

Declarations
#

7 declarations

view source

AuditEmitFn
#

auth/audit_emitter.ts view source

AuditEmitFn

Signature of AuditEmitter.emit — captured by the inner closure so emit_role_grant_target reaches the decorated function rather than a this.emit lookup. Exposed as a type so EmitDecorator can name the inner / outer slot.

AuditEmitRoleGrantContext
#

auth/audit_emitter.ts view source

AuditEmitRoleGrantContext

Context required by AuditEmitter.emit_role_grant_target — adds client_ip so the helper can lift the ip: ctx.client_ip boilerplate every role-grant-shape emit site repeated.

inheritance

client_ip

Resolved client IP from the trusted-proxy middleware — 'unknown' if not resolved.

type string

AuditEmitter
#

auth/audit_emitter.ts view source

AuditEmitter

Bound audit-emit capability. Built once at backend assembly via create_audit_emitter; lives on AppDeps.audit so factories never see the pool.

on_event_chain

Mutable subscriber chain. create_app_server appends the factory-managed audit-log SSE listener and per-endpoint WS auth guards / logout closers here so SSE + WS fan-out compose on top of the consumer's on_audit_event callback without shallow-copying AppDeps. Consumers can also append listeners directly for setups that don't run through create_app_server.

type Array<(event: AuditLogEvent) => void>
readonly

AuditEmitterContext
#

auth/audit_emitter.ts view source

AuditEmitterContext

Per-request context required by AuditEmitter.emit — just the eager pending_effects queue. The bound emitter carries its own log reference inside the closure, so per-call contexts don't need one.

Audit emits are eager-only by design: the bound emitter fires the pool write immediately and pushes the in-flight Promise<void> here. They never go through emit_after_commit — pool-routed audit writes are already rollback-resilient because they run outside the request transaction, so the post-commit timing the deferred queue provides would only delay forensic visibility without any safety benefit.

Both RouteContext and ActionContext structurally satisfy this shape (they each carry pending_effects), so handlers pass route / ctx directly.

pending_effects

type Array<Promise<void>>

create_audit_emitter
#

CreateAuditEmitterOptions
#

auth/audit_emitter.ts view source

CreateAuditEmitterOptions

db

Pool-level Db. Captured by every emit call.

type Db

log

Logger for write + listener-callback failures.

type Logger

on_audit_event

Initial subscriber appended to on_event_chain. Omit for backends that compose listeners post-assembly (e.g. via audit_log_sse).

type ((event: AuditLogEvent) => void) | null

audit_log_config

Audit-log config. Defaults to builtin_audit_log_config. Consumer- extended configs from create_audit_log_config({extra_events}) get registered here once at backend assembly.

emit_decorator

Test-only hook to wrap emit at construction time. The decorated function is captured by emit_role_grant_target's closure and is the function exposed on the returned AuditEmitter, so both call shapes route through it — see EmitDecorator for the rationale.

Leave unset in production. The intended caller is create_emit_ordering_audit_factory in testing/audit_drift_guard.ts.

EmitDecorator
#

auth/audit_emitter.ts view source

EmitDecorator

Wrap the bound emit before it gets captured by emit_role_grant_target's closure and exposed on the returned AuditEmitter. Test instrumentation uses this to record emit invocation ordering against external markers (e.g. eager ConnectionCloser calls in connection_closer.db.test.ts) without paying the freeze-breaking footgun the pre-decorator patch_audit_emit_capture hot-patcher had.

Because the inner closure captures the decorated function (not the outer slot reference), emit_role_grant_target also routes through the wrap — the close-vs-emit ordering helper sees role-grant-shape emissions, not just bare emit calls. Production never sets this.

Depends on
#

Imported by
#