/**
* Audit logging for tool invocations
*
* Provides structured audit trail for security and debugging.
* Automatically sanitizes PII from logged data.
*/
import { createLogger } from './logger.js';
import { sanitizeObject } from './pii-sanitizer.js';
import { getTraceContext } from './tracing.js';
const logger = createLogger('audit');
/**
* Audit log entry
*/
export interface AuditEntry {
/** Event type */
event: 'tool.invoke' | 'tool.complete' | 'tool.error' | 'auth.success' | 'auth.failure' | 'session.create' | 'session.destroy';
/** Tool or operation name */
operation: string;
/** Duration in milliseconds (for completed events) */
durationMs?: number;
/** Whether operation succeeded */
success?: boolean;
/** Error code if failed */
errorCode?: string;
/** Sanitized input summary */
input?: Record<string, unknown>;
/** Sanitized output summary */
output?: Record<string, unknown>;
/** Session ID if applicable */
sessionId?: string;
/** Client identifier */
clientId?: string;
/** Timestamp */
timestamp: string;
/** Trace context */
traceId?: string;
spanId?: string;
}
/**
* Log a tool invocation start
*/
export function auditToolInvoke(
toolName: string,
input: unknown,
options?: { sessionId?: string; clientId?: string }
): void {
const { traceId, spanId } = getTraceContext();
const entry: AuditEntry = {
event: 'tool.invoke',
operation: toolName,
input: sanitizeObject(summarizeInput(input)),
sessionId: options?.sessionId,
clientId: options?.clientId,
timestamp: new Date().toISOString(),
traceId,
spanId,
};
logger.info('Tool invoked', entry as unknown as Record<string, unknown>);
}
/**
* Log a tool completion
*/
export function auditToolComplete(
toolName: string,
durationMs: number,
success: boolean,
options?: { errorCode?: string; sessionId?: string }
): void {
const { traceId, spanId } = getTraceContext();
const entry: AuditEntry = {
event: success ? 'tool.complete' : 'tool.error',
operation: toolName,
durationMs,
success,
errorCode: options?.errorCode,
sessionId: options?.sessionId,
timestamp: new Date().toISOString(),
traceId,
spanId,
};
if (success) {
logger.info('Tool completed', entry as unknown as Record<string, unknown>);
} else {
logger.warning('Tool failed', entry as unknown as Record<string, unknown>);
}
}
/**
* Log an authentication event
*/
export function auditAuth(
success: boolean,
options?: { clientId?: string; reason?: string }
): void {
const { traceId, spanId } = getTraceContext();
const entry: AuditEntry = {
event: success ? 'auth.success' : 'auth.failure',
operation: 'authentication',
success,
clientId: options?.clientId,
errorCode: success ? undefined : options?.reason,
timestamp: new Date().toISOString(),
traceId,
spanId,
};
if (success) {
logger.info('Authentication succeeded', entry as unknown as Record<string, unknown>);
} else {
logger.warning('Authentication failed', entry as unknown as Record<string, unknown>);
}
}
/**
* Log a session lifecycle event
*/
export function auditSession(
event: 'create' | 'destroy',
sessionId: string,
options?: { clientId?: string; reason?: string }
): void {
const { traceId, spanId } = getTraceContext();
const entry: AuditEntry = {
event: event === 'create' ? 'session.create' : 'session.destroy',
operation: 'session',
sessionId,
clientId: options?.clientId,
timestamp: new Date().toISOString(),
traceId,
spanId,
};
logger.info(`Session ${event}d`, entry as unknown as Record<string, unknown>);
}
/**
* Summarize input for audit logging (avoid logging large payloads)
*/
function summarizeInput(input: unknown): Record<string, unknown> {
if (input === null || input === undefined) {
return {};
}
if (typeof input !== 'object') {
return { value: String(input).slice(0, 100) };
}
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (typeof value === 'string') {
// Truncate long strings
result[key] = value.length > 100 ? `${value.slice(0, 100)}...` : value;
} else if (Array.isArray(value)) {
// Show array length instead of contents
result[key] = `[Array(${value.length})]`;
} else if (typeof value === 'object' && value !== null) {
// Show object keys instead of full object
result[key] = `{${Object.keys(value).join(', ')}}`;
} else {
result[key] = value;
}
}
return result;
}