newX 1.3: islands to disk, server-mode islands, and an image proxy

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:

x.config.ts
import { defineConfig, createConsoleAuditSink, setAuditSink } from "@thexjs/core";setAuditSink(createConsoleAuditSink());export default defineConfig({  // ... standard options});
stdout
{"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:

src/api/auth.ts
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:

custom audit event
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:

custom sink — SIEM webhook
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

AuditEntry
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)}
Event types
type AuditEvent =  | "auth.login.success"  | "auth.login.failure"  | "auth.logout"  | "auth.password_changed"  | "auth.role_changed"  | "auth.permission_denied"  | "auth.session_revoked";