Audit Trail
Prerequisites
Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.
- Why Audit Trails Matter
- How It Works
- Configuration
- Audit Event Structure
- SHA-256 Argument Hashing
- Identity Extraction
- Status Detection
- SOC2 Mapping
- GDPR Mapping
- API Reference
The Audit Trail middleware wraps every tool call with compliance-ready logging. It captures who called what, when, with what arguments (hashed), and what happened — without leaking sensitive data.
Why Audit Trails Matter
When an AI agent performs actions on behalf of users, you need answers to six questions:
- Who initiated the action? (identity)
- What action was performed? (tool + action name)
- When did it happen? (timestamp)
- What arguments were passed? (hashed for privacy)
- What was the outcome? (success, error, blocked, rate-limited)
- How long did it take? (performance)
SOC2 auditors ask these questions. GDPR regulators ask these questions. Your incident response team asks these questions at 3 AM. The Audit Trail answers all six automatically.
How It Works
The auditTrail() function returns a middleware that wraps the handler execution:
Tool call ──▶ AuditTrail (start) ──▶ Handler ──▶ AuditTrail (end) ──▶ Response
│ │
└─── Extract identity ───────────────┘
└─── Hash arguments ─────────────────┘
└─── Detect status ──────────────────┘
└─── Emit event ─────────────────────┘import { auditTrail } from '@vurb/core';
const billing = createTool('billing')
.use(auditTrail({
sink: (event) => myAuditStore.append(event),
extractIdentity: (ctx) => ({
userId: ctx.userId,
tenantId: ctx.tenantId,
}),
}))
.action({ name: 'create', handler: async (ctx, args) => { /* ... */ } });Configuration
interface AuditTrailConfig {
/** Event sink — receives every audit event */
readonly sink: (event: SecurityAuditEvent) => void | Promise<void>;
/** Extract identity from context */
readonly extractIdentity?: (ctx: any) => Record<string, string>;
/** Hash function override (default: SHA-256) */
readonly hashFn?: (input: string) => Promise<string>;
}Minimal Configuration
auditTrail({
sink: (event) => console.log(JSON.stringify(event)),
})Production Configuration
auditTrail({
sink: async (event) => {
await prisma.auditLog.create({ data: event });
},
extractIdentity: (ctx) => ({
userId: ctx.user.id,
tenantId: ctx.user.tenantId,
role: ctx.user.role,
ip: ctx.remoteAddress,
}),
})Audit Event Structure
Every tool call produces a SecurityAuditEvent:
interface SecurityAuditEvent {
/** Tool name */
readonly tool: string;
/** Action name */
readonly action: string;
/** ISO 8601 timestamp */
readonly timestamp: string;
/** SHA-256 hash of serialized arguments */
readonly argsHash: string;
/** Resolved identity from extractIdentity() */
readonly identity: Record<string, string>;
/** Outcome: success, error, firewall_blocked, rate_limited */
readonly status: AuditStatus;
/** Execution time in milliseconds */
readonly durationMs: number;
}SHA-256 Argument Hashing
Arguments are serialized to JSON and hashed with SHA-256. The hash is included in the audit event — never the raw arguments:
// Input: { userId: "u_42", amount: 5000 }
// Hash: "a7f5c3d..."This ensures:
- Privacy — Raw arguments are never persisted in audit logs
- Integrity — The hash proves arguments were not tampered with
- Forensics — Given the same arguments, you can verify the hash matches
The default implementation uses the Web Crypto API (crypto.subtle.digest). For environments without Web Crypto, provide a custom hashFn.
Identity Extraction
The extractIdentity function receives the full context and returns a flat record:
extractIdentity: (ctx) => ({
userId: ctx.user.id,
tenantId: ctx.user.tenantId,
role: ctx.user.role,
sessionId: ctx.sessionId,
})When not provided, the identity defaults to {}. The function runs before the handler — errors in identity extraction do not block the tool call.
Status Detection
The middleware automatically classifies the outcome:
| Status | Condition |
|---|---|
success | Handler returned without isError |
error | Handler returned with isError: true |
firewall_blocked | Previous middleware returned security error |
rate_limited | Previous middleware returned rate-limit error |
Detection works by inspecting the response metadata after the handler completes.
SOC2 Mapping
| SOC2 Control | Audit Trail Feature |
|---|---|
| CC6.1 — Logical Access | identity field tracks who accessed what |
| CC6.3 — Access Monitoring | Every tool call is logged with outcome |
| CC7.2 — System Monitoring | durationMs tracks performance anomalies |
| CC7.3 — Change Monitoring | argsHash provides integrity verification |
The Audit Trail generates the evidence your SOC2 auditor needs — automatically, for every tool call, without developer opt-in per action.
GDPR Mapping
| GDPR Article | Audit Trail Feature |
|---|---|
| Art. 5(1)(c) — Data Minimization | Arguments are hashed, not stored raw |
| Art. 25 — Data Protection by Design | PII never leaves the hashing boundary |
| Art. 30 — Records of Processing | Every processing operation is logged |
| Art. 32 — Security of Processing | SHA-256 ensures integrity verification |
API Reference
auditTrail(config)
Returns a MiddlewareFn that can be applied with .use():
const middleware = auditTrail({
sink: (event) => store.append(event),
extractIdentity: (ctx) => ({ userId: ctx.userId }),
});
const tool = createTool('billing').use(middleware);SecurityAuditEvent
interface SecurityAuditEvent {
readonly tool: string;
readonly action: string;
readonly timestamp: string;
readonly argsHash: string;
readonly identity: Record<string, string>;
readonly status: 'success' | 'error' | 'firewall_blocked' | 'rate_limited';
readonly durationMs: number;
}sha256Hex(input: string): Promise<string>
Default hashing function using the Web Crypto API. Returns the hex-encoded SHA-256 digest.