UNPKG

@kya-os/mcp-i

Version:

The TypeScript MCP framework with identity features built-in

431 lines (430 loc) 13.3 kB
/** * Audit Logging System for MCP-I Runtime * * TODO: Update AuditLogger to implement IAuditLogger from @kya-os/mcp-i-core/runtime/audit-logger. * Currently using Node.js-specific implementation. This will allow the interface to be used * across all platform implementations while maintaining platform-specific crypto implementations. * * Handles audit record generation and logging with frozen format * according to requirements 5.4, 5.5, and 6.7 (configurable retention). * * ## Privacy Guarantees * * This system is designed with privacy-first principles: * * **NEVER Logged:** * - Request/response bodies (only SHA-256 hashes) * - Secrets (private keys, API keys, tokens, nonces) * - PII (names, emails, addresses, phone numbers) * - Key material (only key IDs are logged) * * **ALWAYS Logged:** * - Metadata only (DIDs, key IDs, timestamps) * - SHA-256 hashes (not plaintext data) * - Session IDs (for correlation, not sensitive) * - Verification results (yes/no only) * - Scope identifiers (application-level) * * ## Frozen Format * * The audit.v1 format is **frozen** and will not change: * * ``` * audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|-> * ``` * * ## Event-Driven Rotation * * This implementation uses **event-driven rotation** (not timers): * - Rotation checks happen on each `logAuditRecord()` call * - No setInterval/setTimeout (works in Cloudflare Workers) * - No cleanup needed (no timers to clear) * - Predictable behavior (rotation on activity) * * @module audit * @see {@link https://github.com/kya-os/xmcp-i/docs/concepts/audit-logging.md | Audit Logging Documentation} */ import { AuditRecord } from "@kya-os/contracts/proof"; import { SessionContext } from "@kya-os/contracts/handshake"; import { AgentIdentity } from "./identity"; /** * Audit log rotation strategy */ export type AuditRotationStrategy = "size" | "time" | "count" | "custom"; /** * Audit rotation context passed to hooks */ export interface AuditRotationContext { strategy: AuditRotationStrategy; trigger: string; recordsLogged: number; timestamp: number; } /** * Audit log rotation hooks */ export interface AuditRotationHooks { /** * Called when audit log should be rotated * @param context - Rotation context with metadata * @returns Promise that resolves when rotation is complete */ onRotation?: (context: AuditRotationContext) => Promise<void>; /** * Called when audit log reaches size limit * @param sizeBytes - Current size in bytes * @param limit - Size limit that was exceeded */ onSizeLimit?: (sizeBytes: number, limit: number) => Promise<void>; /** * Called on time-based rotation (e.g., daily, hourly) * @param interval - Rotation interval that triggered (e.g., "daily", "hourly") */ onTimeBased?: (interval: string) => Promise<void>; /** * Called when record count reaches threshold * @param count - Number of records logged * @param threshold - Count threshold that was exceeded */ onCountThreshold?: (count: number, threshold: number) => Promise<void>; } /** * Audit logging configuration * * @example Basic configuration * ```typescript * const config: AuditConfig = { * enabled: true, * logFunction: console.log, * includePayloads: false, // ALWAYS false for privacy * }; * ``` * * @example With rotation * ```typescript * const config: AuditConfig = { * enabled: true, * rotation: { * strategy: 'size', * sizeLimit: 10 * 1024 * 1024, // 10 MB * hooks: { * onRotation: async (context) => { * // Archive old log file * }, * }, * }, * }; * ``` */ export interface AuditConfig { /** * Enable audit logging (default: true) */ enabled?: boolean; /** * Custom log function (default: console.log) * * @param record - The formatted audit line (frozen format) * * @example File-based logging * ```typescript * logFunction: (record) => { * fs.appendFileSync('/var/log/audit.log', record + '\n'); * } * ``` */ logFunction?: (record: string) => void; /** * Include payloads in logs (default: false) * * **WARNING:** This should ALWAYS be false. The frozen format ensures * that even if set to true, no request/response bodies will be logged. * This flag exists for compatibility but has no effect on the frozen format. * * @deprecated This flag has no effect due to frozen format privacy guarantees */ includePayloads?: boolean; /** * Log rotation configuration (event-driven, no timers) */ rotation?: { /** * Rotation strategy * - 'size': Rotate when log reaches size limit (checked on each call) * - 'time': Rotate based on elapsed time (checked on each call) * - 'count': Rotate after N records * - 'custom': Custom rotation logic via hooks only */ strategy?: AuditRotationStrategy; /** * Size limit in bytes (for size-based rotation) * * @example 10 MB limit * ```typescript * sizeLimit: 10 * 1024 * 1024 * ``` */ sizeLimit?: number; /** * Time interval in milliseconds (for time-based rotation) * Rotation is checked on each logAuditRecord() call, not via timer. * * @example Daily rotation * ```typescript * timeInterval: 24 * 60 * 60 * 1000 * ``` */ timeInterval?: number; /** * Record count threshold (for count-based rotation) * * @example Rotate after 10k records * ```typescript * countThreshold: 10000 * ``` */ countThreshold?: number; /** * Custom rotation hooks */ hooks?: AuditRotationHooks; }; } /** * Audit context for logging * * Contains all metadata needed to generate an audit record. * * **Privacy Note:** Only metadata is extracted from these objects. * The identity's private key, session's nonce, and other sensitive * fields are NEVER included in the audit log. * * @example * ```typescript * const context: AuditContext = { * identity: { * did: 'did:web:example.com:agents:agent-1', * keyId: 'key-20241014-abc123', * // ... private key NOT logged * }, * session: { * sessionId: 'sess_abc123', * audience: 'example.com', * // ... nonce NOT logged * }, * requestHash: 'sha256:1234567890abcdef...', * responseHash: 'sha256:fedcba0987654321...', * verified: 'yes', * scopeId: 'orders.create', * }; * ``` */ export interface AuditContext { /** * Agent identity * * Only `did` and `keyId` are logged. Private key is NEVER logged. */ identity: AgentIdentity; /** * Session context * * Only `sessionId` and `audience` are logged. Nonce is NEVER logged. */ session: SessionContext; /** * Request hash (SHA-256 with `sha256:` prefix) * * @example 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' */ requestHash: string; /** * Response hash (SHA-256 with `sha256:` prefix) * * @example 'sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321' */ responseHash: string; /** * Verification result * * - 'yes': Proof was verified successfully * - 'no': Proof verification failed */ verified: "yes" | "no"; /** * Optional scope identifier * * Application-level scope (e.g., 'orders.create', 'users.read'). * If not provided, '-' is used in the audit log. * * @example 'orders.create' */ scopeId?: string; } /** * Event context for logging events that bypass session deduplication * * Used for consent events where multiple events occur in the same session. * Unlike AuditContext, this allows multiple events per session. */ export interface AuditEventContext { /** * Event type identifier * * @example "consent:page_viewed", "consent:approved", "runtime:initialized" */ eventType: string; /** * Agent identity * * Only `did` and `keyId` are logged. Private key is NEVER logged. */ identity: AgentIdentity; /** * Session context * * Only `sessionId` and `audience` are logged. Nonce is NEVER logged. */ session: SessionContext; /** * Optional event-specific data * * Used for generating event hash. Not logged directly. */ eventData?: Record<string, any>; } /** * Audit logger class with event-driven rotation support * * Privacy Guarantees: * - NEVER logs request/response bodies (only SHA-256 hashes) * - NEVER logs secrets (private keys, API keys, tokens, nonces) * - NEVER logs PII (personal information) * - Only logs metadata: DIDs, key IDs, timestamps, hashes, session IDs * - Frozen format: audit.v1 with fixed field order * * Rotation Strategy: * - Event-driven (no timers) - rotation checks on each logAuditRecord() call * - Works in all environments (Node.js, Cloudflare Workers, Vercel Edge) * - No cleanup needed (no timers to clear) */ export declare class AuditLogger { private config; private sessionAuditLog; private totalRecordsLogged; private currentLogSize; private lastRotationTime; private destroyed; constructor(config?: AuditConfig); /** * Emit audit record on first call per session * Requirements: 5.4, 5.5 * * This method: * 1. Checks if logger is destroyed * 2. Logs audit record (first call per session) * 3. Checks if rotation is needed (event-driven) * 4. Triggers rotation hooks if threshold met */ logAuditRecord(context: AuditContext): Promise<void>; /** * Log an event using the frozen audit format WITHOUT session deduplication. * * Unlike logAuditRecord(), this method ALWAYS logs the event, regardless * of whether an event has already been logged for this session. This is * necessary for consent events where multiple events occur in the same session. * * The event still uses the frozen audit.v1 format for consistency, but * bypasses the "once per session" constraint. * * @param context - Event context including eventType, identity, session, and eventData */ logEvent(context: AuditEventContext): Promise<void>; /** * Generate deterministic hash for event */ private hashEvent; /** * Format audit record as frozen audit line * Format: audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|-> */ private formatAuditLine; /** * Check if rotation is needed and trigger if necessary (event-driven) * * This method is called on each logAuditRecord() call. * No timers are used - rotation is checked based on current state. */ private checkRotation; /** * Rotate audit log now (manually triggered) * * This method: * 1. Calls onRotation hook (for user to archive/rotate log file) * 2. Resets rotation counters (size, time, count) * * @param trigger - Reason for rotation (e.g., "manual", "size-limit") */ rotateNow(trigger?: string): Promise<void>; /** * Destroy audit logger and cleanup resources * * This method: * 1. Clears session tracking (memory) * 2. Resets statistics * 3. Marks logger as destroyed * * Note: No timers to clear (event-driven rotation) */ destroy(): void; /** * Clear session audit log (useful for testing) */ clearSessionLog(): void; /** * Get audit statistics */ getStats(): { enabled: boolean; sessionsLogged: number; includePayloads: boolean; totalRecordsLogged: number; currentLogSize: number; lastRotationTime: number; }; /** * Update configuration */ updateConfig(config: Partial<AuditConfig>): void; } /** * Key rotation audit logging */ export interface KeyRotationAuditContext { identity: AgentIdentity; oldKeyId: string; newKeyId: string; mode: "dev" | "prod"; delegated: "yes" | "no"; force: "yes" | "no"; } /** * Log key rotation audit record * Format: keys.rotate.v1 ts=<unix> did=<did> oldKid=<kid> newKid=<kid> mode=dev|prod delegated=yes|no force=yes|no */ export declare function logKeyRotationAudit(context: KeyRotationAuditContext, logFunction?: (record: string) => void): void; /** * Default audit logger instance */ export declare const defaultAuditLogger: AuditLogger; /** * Utility functions */ /** * Parse audit line back to record (for testing/analysis) */ export declare function parseAuditLine(line: string): AuditRecord | null; /** * Validate audit record format */ export declare function validateAuditRecord(record: any): record is AuditRecord;