Observability
Audit trail
Structured, append-only audit logging for security-relevant events. The audit trail captures who did what and when — login attempts, permission checks, role changes, session revocations — with secrets automatically redacted before they reach the sink.
Why a dedicated audit system
Application logs mix operational data with security events. An audit trail keeps a separate, append-only channel for events a compliance reviewer or operator needs: "did anyone log in from an unexpected IP?" or "which sessions were revoked before expiry?" Every entry carries a typed event name, a timestamp, the user id (or null), the client IP, and a human-readable reason — all redacted before emission.
Setting up a sink
The default sink is a no-op. Install one at application startup — typically in x.config.ts or a setup file — using setAuditSink. The built-in console sink writes one JSON object per line to stdout:
import { defineConfig, createConsoleAuditSink, setAuditSink } from "@thexjs/core";setAuditSink(createConsoleAuditSink());export default defineConfig({ // ... standard options});{"timestamp":"2026-08-27T10:00:00.000Z","event":"auth.login.success","userId":"user_abc","ip":"203.0.113.42","reason":"login from known device"}{"timestamp":"2026-08-27T10:01:00.000Z","event":"auth.login.failure","ip":"198.51.100.7","reason":"invalid password"}
Logging events
Use the convenience functions for the built-in event types. Each accepts a consistent input shape and handles redaction automatically:
import { auditLoginSuccess, auditLoginFailure, auditLogout, auditPermissionDenied,} from "@thexjs/core";// After validating credentialsauditLoginSuccess({ userId: session.userId, ip: clientIpFromRequest(req), reason: "credentials match", requestId: requestIdFromRequest(req),});// When a password is wrongauditLoginFailure({ ip: clientIpFromRequest(req), reason: "invalid password for user@example.com",});// On logoutauditLogout({ userId: session.userId, ip: clientIpFromRequest(req),});// When a route guard rejects a requestauditPermissionDenied({ userId: activeUser.id, ip: clientIpFromRequest(req), reason: "user lacks admin role", metadata: { route: "/admin/settings" },});Custom events
For application-specific security events, call the low-level audit function directly with your own AuditEntry. The same redaction rules apply:
import { audit } from "@thexjs/core";audit({ timestamp: new Date().toISOString(), event: "auth.login.success", // must match the AuditEvent union userId: "user_abc", ip: "203.0.113.42", reason: "OAuth flow completed", provider: "github", metadata: { org: "acme-corp", plan: "enterprise" },});Writing a custom sink
Implement the AuditSink interface to route events to a SIEM, a database table, or a cloud logging service:
import { type AuditSink, type AuditEntry, setAuditSink } from "@thexjs/core";class SiemWebhookSink implements AuditSink { async write(entry: AuditEntry): Promise<void> { // Fire-and-forget POST to your SIEM fetch("https://siem.example.com/events", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(entry), }).catch(() => {}); // don't block the request }}setAuditSink(new SiemWebhookSink());The sink must treat entries as append-only — never modify or delete past entries. The default console sink achieves this via stdout (append-only at the OS level). A database-backed sink should use INSERT with no UPDATE or DELETE.
Redaction guarantees
Before the entry reaches the sink, the audit function passes the reason string through redactString and every value in metadata through redactValue. This means bearer tokens, password strings, or API keys that accidentally end up in a metadata field are masked as [REDACTED] before they leave the process. See the Secret Redaction page for the full rules.
API reference
interface AuditEntry { timestamp: string; // ISO 8601 event: AuditEvent; // typed union of event names userId: string | null; // authenticated user, or null for anonymous ip: string | null; // client IP from socket or X-Forwarded-For reason?: string; // human-readable explanation (redacted) provider?: string; // e.g. "github", "credentials" requestId?: string; // correlation with request logs sessionHash?: string; // HMAC digest of session token (safe to store) metadata?: Record<string, unknown>; // app-specific data (redacted)}type AuditEvent = | "auth.login.success" | "auth.login.failure" | "auth.logout" | "auth.password_changed" | "auth.role_changed" | "auth.permission_denied" | "auth.session_revoked";