@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
431 lines (430 loc) • 13.3 kB
TypeScript
/**
* 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;