Audit Extension
Add audit logging capabilities to your Bantai contexts
Audit Extension
The @bantai-dev/with-audit package adds comprehensive audit logging capabilities to your Bantai contexts. It automatically tracks policy evaluations, rule decisions, and custom events, providing full observability into your authorization system.
Installation
npm install @bantai-dev/with-audit @bantai-dev/core zodNote: @bantai-dev/core and zod are peer dependencies and must be installed separately.
Quick Start
import { z } from 'zod';
import { defineContext, defineRule, definePolicy, evaluatePolicy, allow } from '@bantai-dev/core';
import { withAudit } from '@bantai-dev/with-audit';
// 1. Define your base context
const appContext = defineContext(
z.object({
userId: z.string(),
})
);
// 2. Create audit sinks (where events will be sent)
const auditSinks = [
(event) => console.log('Audit event:', event),
// Add more sinks: database, logging service, etc.
];
// 3. Extend context with audit capabilities
const contextWithAudit = withAudit(appContext, { sinks: auditSinks });
// 4. Define rules and policies as usual
const userRule = defineRule(
contextWithAudit,
'check-user',
async (input, { tools }) => {
// Your rule logic here
return allow({ reason: 'User authorized' });
}
);
const policy = definePolicy(contextWithAudit, 'user-policy', [userRule]);
// 5. Evaluate policy - audit events are automatically emitted
const result = await evaluatePolicy(policy, {
userId: '123',
audit: {
trace: {
traceId: 'trace-123',
requestId: 'req-456',
},
},
});How It Works
When you use withAudit, the package:
- Extends the context schema with an optional
audit.tracefield for correlation IDs - Adds audit tools to the context that are automatically used by
evaluatePolicy - Automatically emits events during policy evaluation:
policy.start- When evaluation beginsrule.start- When each rule evaluation beginsrule.decision- When each rule makes a decisionrule.end- When each rule evaluation completespolicy.decision- When the final policy decision is madepolicy.end- When evaluation completes
All events are automatically enriched with:
evaluationId- Unique ID for this policy evaluationpolicy- Policy name and versionid- Unique event IDtimestamp- Unix timestamp in milliseconds
Event Types
Policy Events
policy.start - Emitted when policy evaluation begins.
{
type: 'policy.start',
evaluationId: string,
policy: { name: string, version?: string },
trace?: { traceId?: string, requestId?: string },
}policy.decision - Emitted when the final policy decision is made.
{
type: 'policy.decision',
evaluationId: string,
policy: { name: string, version?: string },
decision: {
outcome: 'allow' | 'deny',
reason: string,
},
trace?: { traceId?: string, requestId?: string },
}policy.end - Emitted when policy evaluation completes.
{
type: 'policy.end',
evaluationId: string,
policy: { name: string, version?: string },
trace?: { traceId?: string, requestId?: string },
}Rule Events
rule.start - Emitted when a rule evaluation begins.
{
type: 'rule.start',
evaluationId: string,
policy: { name: string, version?: string },
rule: { name: string },
trace?: { traceId?: string, requestId?: string },
}rule.decision - Emitted when a rule makes a decision.
{
type: 'rule.decision',
evaluationId: string,
policy: { name: string, version?: string },
rule: { name: string },
decision: {
outcome: 'allow' | 'deny' | 'skip',
reason: string | null,
},
trace?: { traceId?: string, requestId?: string },
}rule.end - Emitted when a rule evaluation completes.
{
type: 'rule.end',
evaluationId: string,
policy: { name: string, version?: string },
rule: { name: string },
trace?: { traceId?: string, requestId?: string },
}Custom Events
extension.event - Custom events that you can emit manually.
{
type: 'extension.event',
evaluationId: string,
policy: { name: string, version?: string },
meta?: Record<string, unknown>,
trace?: { traceId?: string, requestId?: string },
}API Reference
withAudit
Extends a Bantai context with audit capabilities. Adds audit tools to the context and extends the schema with optional trace fields.
function withAudit<
TContext extends ContextDefinition<z.ZodRawShape, Record<string, unknown>>
>(
context: TContext,
options: {
sinks: AuditSink[];
}
): WithAuditContext<TContext>Parameters:
context: A Bantai context definitionoptions: Configuration objectsinks: Array ofAuditSinkfunctions that receive audit events
Returns: Extended context with audit tools in context.tools.audit
Example:
const contextWithAudit = withAudit(baseContext, {
sinks: [
(event) => console.log('Audit:', event),
async (event) => {
await db.auditEvents.insert(event);
},
],
});AuditSink
An audit sink is a function that receives validated audit events:
type AuditSink = (event: AuditEvent) => void;Sinks can be synchronous or asynchronous. If a sink throws an error, it will stop the execution of subsequent sinks in the array.
AuditEvent
The complete audit event structure:
type AuditEvent = {
id: string; // Unique event ID
type: 'policy.start' | 'rule.start' | 'rule.decision' | 'rule.end' | 'policy.decision' | 'policy.end' | 'extension.event';
timestamp: number; // Unix timestamp in ms
evaluationId: string; // Unique per policy evaluation
policy: {
name: string;
version?: string;
};
rule?: {
name: string;
};
decision?: {
outcome: 'allow' | 'deny' | 'skip';
reason: string | null;
};
trace?: {
traceId?: string;
requestId?: string;
};
meta?: Record<string, unknown>;
};Context Schema Extension
When you use withAudit, the context schema is automatically extended with:
{
audit?: {
trace?: {
traceId?: string;
requestId?: string;
};
};
}This allows you to pass trace information when evaluating policies:
await evaluatePolicy(policy, {
userId: '123',
audit: {
trace: {
traceId: 'trace-123',
requestId: 'req-456',
},
},
});Examples
Basic Audit Logging
import { z } from 'zod';
import { defineContext, defineRule, definePolicy, evaluatePolicy } from '@bantai-dev/core';
import { withAudit } from '@bantai-dev/with-audit';
const context = withAudit(
defineContext(z.object({ userId: z.string() })),
{
sinks: [
(event) => {
console.log(`[${event.type}] ${event.policy.name}`, event);
},
],
}
);
const rule = defineRule(context, 'check-user', async (input) => {
return input.userId === 'admin' ? allow() : deny({ reason: 'Unauthorized' });
});
const policy = definePolicy(context, 'auth-policy', [rule]);
// Evaluate with trace information
await evaluatePolicy(policy, {
userId: 'admin',
audit: {
trace: {
traceId: 'trace-123',
requestId: 'req-456',
},
},
});Multiple Sinks
You can send audit events to multiple destinations:
const context = withAudit(
defineContext(z.object({ userId: z.string() })),
{
sinks: [
// Console logging
(event) => console.log('Audit:', event),
// Database storage
async (event) => {
await db.auditEvents.insert(event);
},
// External logging service
async (event) => {
await fetch('https://logs.example.com/audit', {
method: 'POST',
body: JSON.stringify(event),
});
},
],
}
);Database Storage
Store audit events in a database:
import { withAudit } from '@bantai-dev/with-audit';
const context = withAudit(
defineContext(z.object({ userId: z.string() })),
{
sinks: [
async (event) => {
await db.query(
'INSERT INTO audit_events (id, type, timestamp, evaluation_id, policy_name, data) VALUES (?, ?, ?, ?, ?, ?)',
[
event.id,
event.type,
new Date(event.timestamp),
event.evaluationId,
event.policy.name,
JSON.stringify(event),
]
);
},
],
}
);Filtering Events
Filter events before sending to sinks:
const context = withAudit(
defineContext(z.object({ userId: z.string() })),
{
sinks: [
(event) => {
// Only log deny decisions
if (event.type === 'rule.decision' && event.decision?.outcome === 'deny') {
console.error('Denial:', event);
}
},
],
}
);Error Handling
Handle errors in sinks gracefully:
const context = withAudit(
defineContext(z.object({ userId: z.string() })),
{
sinks: [
(event) => {
try {
// Your sink logic
sendToService(event);
} catch (error) {
console.error('Failed to send audit event:', error);
// Optionally: send to dead letter queue
}
},
],
}
);Custom Audit Events
You can emit custom audit events using the audit handler:
import { defineContext, definePolicy, evaluatePolicy } from '@bantai-dev/core';
import { withAudit } from '@bantai-dev/with-audit';
const context = withAudit(
defineContext(z.object({ userId: z.string() })),
{
sinks: [(event) => console.log(event)],
}
);
const policy = definePolicy(context, 'my-policy', []);
// Get audit handler for custom events
const auditHandler = context.tools.audit.createAuditPolicy(policy);
// Emit custom events
auditHandler.emit({
type: 'extension.event',
meta: {
customField: 'custom-value',
action: 'user-login',
},
});
// Note: After policy.end is emitted, the handler cannot emit more eventsIntegration with Other Extensions
With Rate Limiting
Audit rate limiting decisions:
import { withRateLimit } from '@bantai-dev/with-rate-limit';
import { withAudit } from '@bantai-dev/with-audit';
const baseContext = defineContext(z.object({ userId: z.string() }));
const context = withAudit(
withRateLimit(baseContext, { storage }),
{
sinks: [(event) => console.log('Rate limit audit:', event)],
}
);Best Practices
-
Use multiple sinks: Separate concerns by using different sinks for different purposes (logging, storage, alerting)
-
Handle errors: Wrap sink logic in try-catch to prevent one failing sink from breaking others
-
Include trace IDs: Always pass trace information when evaluating policies for better observability
-
Filter at sink level: If you need to filter events, do it in the sink function rather than modifying the core package
-
Async sinks: Use async sinks for database operations, but be aware that errors won't stop other sinks
Type Safety
The package provides full TypeScript type safety:
- Context extension: Type-safe context merging with audit tools
- Event validation: All events are validated against the audit event schema
- Schema extension: Type-safe audit trace fields in context input
Requirements
- Node.js >= 18
- TypeScript >= 5.0
- Zod >= 4.3.5
- @bantai-dev/core
Related Documentation
- Core Concepts - Understand contexts, rules, and policies
- Rate Limiting Extension - Combine with rate limiting
- Examples - See audit logging in action