// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Session Recording System for Privileged Access Audit Trails
 *
 * Provides comprehensive recording of privileged access sessions with:
 * - Tamper-evident hash chain for integrity
 * - Privacy controls (masking, PII hashing)
 * - Buffered storage with Redis and database persistence
 * - Compliance-ready export capabilities
 *
 * @packageDocumentation
 */
import { createHash, randomUUID } from 'crypto';
import { z } from 'zod';
import { createLogger } from '../../common/logger.js';
import { VorionError, NotFoundError, ForbiddenError } from '../../common/errors.js';
import { Counter, Histogram, Gauge } from 'prom-client';
import { vorionRegistry } from '../../common/metrics-registry.js';
import { redact } from '../../common/redaction.js';
const logger = createLogger({ component: 'session-recording' });
// =============================================================================
// Constants
// =============================================================================
const DEFAULT_RETENTION_DAYS = 90;
const DEFAULT_FLUSH_INTERVAL_MS = 5000;
const DEFAULT_MAX_BUFFER_SIZE = 100;
const HASH_ALGORITHM = 'sha256';
// =============================================================================
// Metrics
// =============================================================================
const recordingsTotal = new Counter({
    name: 'vorion_pam_recordings_total',
    help: 'Total session recordings',
    labelNames: ['status'],
    registers: [vorionRegistry],
});
const eventsRecorded = new Counter({
    name: 'vorion_pam_events_recorded_total',
    help: 'Total events recorded in sessions',
    labelNames: ['event_type'],
    registers: [vorionRegistry],
});
const recordingDuration = new Histogram({
    name: 'vorion_pam_recording_duration_seconds',
    help: 'Duration of session recordings',
    buckets: [60, 300, 600, 1800, 3600, 7200, 14400],
    registers: [vorionRegistry],
});
const activeRecordings = new Gauge({
    name: 'vorion_pam_active_recordings',
    help: 'Number of currently active recordings',
    registers: [vorionRegistry],
});
const eventBufferSize = new Gauge({
    name: 'vorion_pam_event_buffer_size',
    help: 'Current size of event buffer',
    registers: [vorionRegistry],
});
// =============================================================================
// Types
// =============================================================================
/**
 * Session event types that can be recorded
 */
export const SessionEventType = {
    API_CALL: 'api_call',
    DATA_ACCESS: 'data_access',
    PERMISSION_USAGE: 'permission_usage',
    CONFIG_CHANGE: 'config_change',
    AUTH_EVENT: 'auth_event',
    ERROR: 'error',
    CUSTOM: 'custom',
};
export const sessionEventTypeSchema = z.nativeEnum(SessionEventType);
export const sessionEventSchema = z.object({
    id: z.string().uuid(),
    recordingId: z.string().uuid(),
    timestamp: z.string().datetime(),
    sequenceNumber: z.number().int().nonnegative(),
    eventType: sessionEventTypeSchema,
    details: z.record(z.unknown()),
    previousHash: z.string().nullable(),
    eventHash: z.string(),
});
export const recordingMetadataSchema = z.object({
    reason: z.string().min(1),
    ticketId: z.string().optional(),
    approvers: z.array(z.string()).optional(),
    justification: z.string().optional(),
    targetResources: z.array(z.string()).optional(),
    expectedDurationMinutes: z.number().int().positive().optional(),
    riskLevel: z.enum(['low', 'medium', 'high', 'critical']).optional(),
    custom: z.record(z.unknown()).optional(),
});
export const recordingSummarySchema = z.object({
    totalEvents: z.number().int().nonnegative(),
    eventsByType: z.record(z.number()),
    dataAccessed: z.object({
        resourceTypes: z.array(z.string()),
        totalRecords: z.number().int().nonnegative(),
        operations: z.record(z.number()),
    }),
    permissionsUsed: z.object({
        permissions: z.array(z.string()),
        denied: z.number().int().nonnegative(),
        granted: z.number().int().nonnegative(),
    }),
    errors: z.object({
        count: z.number().int().nonnegative(),
        bySeverity: z.record(z.number()),
    }),
    apiCalls: z.object({
        count: z.number().int().nonnegative(),
        avgDurationMs: z.number().nonnegative(),
        statusCodes: z.record(z.number()),
    }),
    durationSeconds: z.number().nonnegative(),
    integrityValid: z.boolean(),
});
/**
 * Recording status
 */
export const RecordingStatus = {
    ACTIVE: 'active',
    COMPLETED: 'completed',
    FAILED: 'failed',
    ARCHIVED: 'archived',
};
export const recordingStatusSchema = z.nativeEnum(RecordingStatus);
export const recordingSchema = z.object({
    id: z.string().uuid(),
    sessionId: z.string(),
    userId: z.string(),
    tenantId: z.string().optional(),
    status: recordingStatusSchema,
    startedAt: z.string().datetime(),
    endedAt: z.string().datetime().optional(),
    events: z.array(sessionEventSchema),
    metadata: recordingMetadataSchema,
    summary: recordingSummarySchema.optional(),
    retentionUntil: z.string().datetime(),
    recordingHash: z.string(),
    createdBy: z.string(),
    deletionApprovedBy: z.string().optional(),
    deletionApprovedAt: z.string().datetime().optional(),
});
export const recordingFilterSchema = z.object({
    userId: z.string().optional(),
    sessionId: z.string().optional(),
    tenantId: z.string().optional(),
    status: recordingStatusSchema.optional(),
    startedAfter: z.string().datetime().optional(),
    startedBefore: z.string().datetime().optional(),
    ticketId: z.string().optional(),
    riskLevel: z.string().optional(),
    hasErrors: z.boolean().optional(),
    permissionUsed: z.string().optional(),
    resourceAccessed: z.string().optional(),
    limit: z.number().int().positive().max(1000).optional(),
    offset: z.number().int().nonnegative().optional(),
});
// =============================================================================
// Errors
// =============================================================================
/**
 * Session recording error
 */
export class SessionRecordingError extends VorionError {
    code = 'SESSION_RECORDING_ERROR';
    statusCode = 500;
    constructor(message, details) {
        super(message, details);
        this.name = 'SessionRecordingError';
    }
}
/**
 * Recording not found error
 */
export class RecordingNotFoundError extends NotFoundError {
    code = 'RECORDING_NOT_FOUND';
    constructor(recordingId) {
        super(`Recording not found: ${recordingId}`, { recordingId });
        this.name = 'RecordingNotFoundError';
    }
}
/**
 * Recording deletion denied error
 */
export class RecordingDeletionDeniedError extends ForbiddenError {
    code = 'RECORDING_DELETION_DENIED';
    constructor(recordingId, reason) {
        super(`Recording deletion denied: ${reason}`, { recordingId, reason });
        this.name = 'RecordingDeletionDeniedError';
    }
}
const DEFAULT_PRIVACY_CONFIG = {
    maskFields: [
        'password',
        'secret',
        'token',
        'apiKey',
        'api_key',
        'authorization',
        'cookie',
        'sessionId',
        'refreshToken',
        'accessToken',
        'privateKey',
        'private_key',
    ],
    excludeFields: [
        'x-api-key',
        'x-auth-token',
        'set-cookie',
    ],
    piiFields: [
        'email',
        'phone',
        'ssn',
        'socialSecurityNumber',
        'dateOfBirth',
        'address',
        'creditCard',
        'bankAccount',
    ],
    maskRequestBodies: false,
    maskResponseBodies: true,
    maxBodySize: 10000,
};
/**
 * Apply privacy controls to event data
 */
function applyPrivacyControls(data, config) {
    if (data === null || data === undefined) {
        return data;
    }
    // Use redaction utility with custom config
    const redactionConfig = {
        sensitivePatterns: config.maskFields.map((field) => new RegExp(field, 'i')),
        replacement: '[MASKED]',
        allowedFields: [],
        ...config.redactionConfig,
    };
    let result = redact(data, redactionConfig);
    // Hash PII fields
    if (typeof result === 'object' && result !== null) {
        result = hashPiiFields(result, config.piiFields);
    }
    // Remove excluded fields
    if (typeof result === 'object' && result !== null) {
        result = removeExcludedFields(result, config.excludeFields);
    }
    return result;
}
/**
 * Hash PII fields for privacy
 */
function hashPiiFields(obj, piiFields) {
    const result = {};
    for (const [key, value] of Object.entries(obj)) {
        if (piiFields.some((field) => key.toLowerCase().includes(field.toLowerCase()))) {
            if (typeof value === 'string') {
                result[key] = `[PII:${createHash('sha256').update(value).digest('hex').substring(0, 8)}]`;
            }
            else {
                result[key] = '[PII:HASHED]';
            }
        }
        else if (typeof value === 'object' && value !== null) {
            if (Array.isArray(value)) {
                result[key] = value.map((item) => typeof item === 'object' && item !== null
                    ? hashPiiFields(item, piiFields)
                    : item);
            }
            else {
                result[key] = hashPiiFields(value, piiFields);
            }
        }
        else {
            result[key] = value;
        }
    }
    return result;
}
/**
 * Remove excluded fields from object
 */
function removeExcludedFields(obj, excludeFields) {
    const result = {};
    for (const [key, value] of Object.entries(obj)) {
        if (excludeFields.some((field) => key.toLowerCase() === field.toLowerCase())) {
            continue;
        }
        if (typeof value === 'object' && value !== null) {
            if (Array.isArray(value)) {
                result[key] = value.map((item) => typeof item === 'object' && item !== null
                    ? removeExcludedFields(item, excludeFields)
                    : item);
            }
            else {
                result[key] = removeExcludedFields(value, excludeFields);
            }
        }
        else {
            result[key] = value;
        }
    }
    return result;
}
/**
 * In-memory storage implementation (for development/testing)
 */
export class InMemoryRecordingStorage {
    recordings = new Map();
    events = new Map();
    async save(recording) {
        this.recordings.set(recording.id, { ...recording });
        this.events.set(recording.id, [...recording.events]);
    }
    async get(recordingId) {
        const recording = this.recordings.get(recordingId);
        if (!recording)
            return null;
        const events = this.events.get(recordingId) ?? [];
        return { ...recording, events: [...events] };
    }
    async update(recording) {
        if (!this.recordings.has(recording.id)) {
            throw new RecordingNotFoundError(recording.id);
        }
        this.recordings.set(recording.id, { ...recording });
    }
    async search(filter) {
        let results = Array.from(this.recordings.values());
        // Apply filters
        if (filter.userId) {
            results = results.filter((r) => r.userId === filter.userId);
        }
        if (filter.sessionId) {
            results = results.filter((r) => r.sessionId === filter.sessionId);
        }
        if (filter.tenantId) {
            results = results.filter((r) => r.tenantId === filter.tenantId);
        }
        if (filter.status) {
            results = results.filter((r) => r.status === filter.status);
        }
        if (filter.startedAfter) {
            results = results.filter((r) => r.startedAt >= filter.startedAfter);
        }
        if (filter.startedBefore) {
            results = results.filter((r) => r.startedAt <= filter.startedBefore);
        }
        if (filter.ticketId) {
            results = results.filter((r) => r.metadata.ticketId === filter.ticketId);
        }
        if (filter.riskLevel) {
            results = results.filter((r) => r.metadata.riskLevel === filter.riskLevel);
        }
        if (filter.hasErrors) {
            results = results.filter((r) => {
                const errorEvents = this.events.get(r.id) ?? [];
                return errorEvents.some((e) => e.eventType === SessionEventType.ERROR);
            });
        }
        const total = results.length;
        const offset = filter.offset ?? 0;
        const limit = filter.limit ?? 50;
        results = results
            .sort((a, b) => b.startedAt.localeCompare(a.startedAt))
            .slice(offset, offset + limit);
        // Attach events to recordings
        return {
            recordings: results.map((r) => ({
                ...r,
                events: this.events.get(r.id) ?? [],
            })),
            total,
        };
    }
    async delete(recordingId, approvedBy) {
        const recording = this.recordings.get(recordingId);
        if (!recording) {
            throw new RecordingNotFoundError(recordingId);
        }
        // Mark as deleted with approval
        recording.deletionApprovedBy = approvedBy;
        recording.deletionApprovedAt = new Date().toISOString();
        this.recordings.delete(recordingId);
        this.events.delete(recordingId);
    }
    async flushEvents(recordingId, events) {
        const existing = this.events.get(recordingId) ?? [];
        this.events.set(recordingId, [...existing, ...events]);
    }
    async getEvents(recordingId) {
        return this.events.get(recordingId) ?? [];
    }
}
/**
 * Session Recorder Class
 *
 * Records privileged access sessions with tamper-evident logging,
 * privacy controls, and compliance-ready exports.
 *
 * @example
 * ```typescript
 * const recorder = new SessionRecorder({
 *   storage: new InMemoryRecordingStorage(),
 *   retentionDays: 90,
 * });
 *
 * // Start recording
 * const recording = await recorder.startRecording('session-123', 'user-456', {
 *   reason: 'Production database maintenance',
 *   ticketId: 'JIRA-789',
 *   approvers: ['admin-1', 'admin-2'],
 * });
 *
 * // Record events
 * await recorder.recordEvent(recording.id, {
 *   type: 'api_call',
 *   method: 'GET',
 *   path: '/api/users',
 *   statusCode: 200,
 *   durationMs: 45,
 * });
 *
 * // Stop recording
 * const summary = await recorder.stopRecording(recording.id);
 * ```
 */
export class SessionRecorder {
    storage;
    privacyConfig;
    retentionDays;
    flushIntervalMs;
    maxBufferSize;
    tenantId;
    // Active recording state
    activeRecordings = new Map();
    // Flush timer
    flushTimer = null;
    constructor(config) {
        this.storage = config.storage;
        this.privacyConfig = { ...DEFAULT_PRIVACY_CONFIG, ...config.privacy };
        this.retentionDays = config.retentionDays ?? DEFAULT_RETENTION_DAYS;
        this.flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
        this.maxBufferSize = config.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
        this.tenantId = config.tenantId;
        // Start flush timer
        this.startFlushTimer();
        logger.info({
            retentionDays: this.retentionDays,
            flushIntervalMs: this.flushIntervalMs,
            maxBufferSize: this.maxBufferSize,
        }, 'Session recorder initialized');
    }
    /**
     * Start recording a session
     */
    async startRecording(sessionId, userId, metadata) {
        // Validate metadata
        recordingMetadataSchema.parse(metadata);
        const now = new Date();
        const retentionUntil = new Date(now);
        retentionUntil.setDate(retentionUntil.getDate() + this.retentionDays);
        const recording = {
            id: randomUUID(),
            sessionId,
            userId,
            tenantId: this.tenantId,
            status: RecordingStatus.ACTIVE,
            startedAt: now.toISOString(),
            events: [],
            metadata,
            retentionUntil: retentionUntil.toISOString(),
            recordingHash: '',
            createdBy: userId,
        };
        // Compute initial recording hash
        recording.recordingHash = this.computeRecordingHash(recording);
        // Initialize active recording state
        this.activeRecordings.set(recording.id, {
            recording,
            buffer: [],
            sequenceNumber: 0,
            lastHash: null,
        });
        // Save to storage
        await this.storage.save(recording);
        activeRecordings.inc();
        recordingsTotal.inc({ status: 'started' });
        logger.info({
            recordingId: recording.id,
            sessionId,
            userId,
            reason: metadata.reason,
            ticketId: metadata.ticketId,
        }, 'Session recording started');
        return recording;
    }
    /**
     * Record an event in a session
     */
    async recordEvent(recordingId, eventDetails) {
        const state = this.activeRecordings.get(recordingId);
        if (!state) {
            // Try to load from storage if not active
            const recording = await this.storage.get(recordingId);
            if (!recording) {
                throw new RecordingNotFoundError(recordingId);
            }
            if (recording.status !== RecordingStatus.ACTIVE) {
                throw new SessionRecordingError(`Cannot record event: recording is ${recording.status}`, { recordingId, status: recording.status });
            }
            throw new SessionRecordingError('Recording not active in this instance', { recordingId });
        }
        // Apply privacy controls
        const sanitizedDetails = applyPrivacyControls(eventDetails, this.privacyConfig);
        // Create event
        const event = {
            id: randomUUID(),
            recordingId,
            timestamp: new Date().toISOString(),
            sequenceNumber: state.sequenceNumber++,
            eventType: eventDetails.type,
            details: sanitizedDetails,
            previousHash: state.lastHash,
            eventHash: '',
        };
        // Compute event hash (hash chain)
        event.eventHash = this.computeEventHash(event);
        state.lastHash = event.eventHash;
        // Add to buffer
        state.buffer.push(event);
        eventsRecorded.inc({ event_type: event.eventType });
        eventBufferSize.set(state.buffer.length);
        // Force flush if buffer is full
        if (state.buffer.length >= this.maxBufferSize) {
            await this.flushBuffer(recordingId);
        }
    }
    /**
     * Stop recording and finalize
     */
    async stopRecording(recordingId) {
        const state = this.activeRecordings.get(recordingId);
        if (!state) {
            throw new RecordingNotFoundError(recordingId);
        }
        // Flush any remaining events
        if (state.buffer.length > 0) {
            await this.flushBuffer(recordingId);
        }
        // Get all events from storage
        const events = await this.storage.getEvents(recordingId);
        // Compute summary
        const summary = this.computeSummary(events, state.recording.startedAt);
        // Update recording
        state.recording.status = RecordingStatus.COMPLETED;
        state.recording.endedAt = new Date().toISOString();
        state.recording.summary = summary;
        state.recording.events = events;
        state.recording.recordingHash = this.computeRecordingHash(state.recording);
        // Save final state
        await this.storage.update(state.recording);
        // Remove from active recordings
        this.activeRecordings.delete(recordingId);
        activeRecordings.dec();
        recordingsTotal.inc({ status: 'completed' });
        const durationSeconds = summary.durationSeconds;
        recordingDuration.observe(durationSeconds);
        logger.info({
            recordingId,
            sessionId: state.recording.sessionId,
            userId: state.recording.userId,
            totalEvents: summary.totalEvents,
            durationSeconds,
            hasErrors: summary.errors.count > 0,
        }, 'Session recording stopped');
        return summary;
    }
    /**
     * Get a recording by ID
     */
    async getRecording(recordingId) {
        // Check active recordings first
        const state = this.activeRecordings.get(recordingId);
        if (state) {
            const events = await this.storage.getEvents(recordingId);
            return {
                ...state.recording,
                events: [...events, ...state.buffer],
            };
        }
        // Load from storage
        const recording = await this.storage.get(recordingId);
        if (!recording) {
            throw new RecordingNotFoundError(recordingId);
        }
        return recording;
    }
    /**
     * Search recordings
     */
    async searchRecordings(filter) {
        recordingFilterSchema.parse(filter);
        const { recordings } = await this.storage.search(filter);
        return recordings;
    }
    /**
     * Export recording for audit
     */
    async exportRecording(recordingId, format) {
        const recording = await this.getRecording(recordingId);
        if (format === 'json') {
            return JSON.stringify(recording, null, 2);
        }
        // CSV format
        const lines = [];
        // Header
        lines.push('Recording ID,Session ID,User ID,Started At,Ended At,Status,Reason,Ticket ID');
        lines.push([
            recording.id,
            recording.sessionId,
            recording.userId,
            recording.startedAt,
            recording.endedAt ?? '',
            recording.status,
            `"${recording.metadata.reason.replace(/"/g, '""')}"`,
            recording.metadata.ticketId ?? '',
        ].join(','));
        lines.push('');
        lines.push('Events:');
        lines.push('Event ID,Timestamp,Sequence,Type,Details Hash');
        for (const event of recording.events) {
            lines.push([
                event.id,
                event.timestamp,
                event.sequenceNumber.toString(),
                event.eventType,
                event.eventHash.substring(0, 16),
            ].join(','));
        }
        if (recording.summary) {
            lines.push('');
            lines.push('Summary:');
            lines.push(`Total Events,${recording.summary.totalEvents}`);
            lines.push(`Duration (seconds),${recording.summary.durationSeconds}`);
            lines.push(`API Calls,${recording.summary.apiCalls.count}`);
            lines.push(`Errors,${recording.summary.errors.count}`);
            lines.push(`Integrity Valid,${recording.summary.integrityValid}`);
        }
        return lines.join('\n');
    }
    /**
     * Verify recording integrity
     */
    async verifyIntegrity(recordingId) {
        const recording = await this.getRecording(recordingId);
        let previousHash = null;
        for (const event of recording.events) {
            // Verify hash chain
            if (event.previousHash !== previousHash) {
                return {
                    valid: false,
                    brokenAt: event.sequenceNumber,
                    error: `Hash chain broken at sequence ${event.sequenceNumber}`,
                };
            }
            // Verify event hash
            const expectedHash = this.computeEventHash({
                ...event,
                eventHash: '',
            });
            if (event.eventHash !== expectedHash) {
                return {
                    valid: false,
                    brokenAt: event.sequenceNumber,
                    error: `Event hash mismatch at sequence ${event.sequenceNumber}`,
                };
            }
            previousHash = event.eventHash;
        }
        return { valid: true };
    }
    /**
     * Request deletion of a recording (requires admin approval)
     */
    async requestDeletion(recordingId, requestedBy, reason) {
        const recording = await this.getRecording(recordingId);
        // Check if within retention period
        const retentionUntil = new Date(recording.retentionUntil);
        const now = new Date();
        if (now < retentionUntil) {
            throw new RecordingDeletionDeniedError(recordingId, `Recording is within retention period (until ${recording.retentionUntil})`);
        }
        logger.warn({
            recordingId,
            requestedBy,
            reason,
            retentionUntil: recording.retentionUntil,
        }, 'Recording deletion requested');
        // Note: Actual deletion should go through approval workflow
        // This just validates the request can be made
    }
    /**
     * Approve and execute deletion (admin only)
     */
    async approveDeletion(recordingId, approvedBy) {
        const recording = await this.getRecording(recordingId);
        // Remove from active recordings if present
        this.activeRecordings.delete(recordingId);
        // Delete from storage
        await this.storage.delete(recordingId, approvedBy);
        logger.warn({
            recordingId,
            approvedBy,
            sessionId: recording.sessionId,
            userId: recording.userId,
        }, 'Recording deleted with admin approval');
    }
    /**
     * Check if a session has an active recording
     */
    isRecording(sessionId) {
        const states = Array.from(this.activeRecordings.values());
        for (const state of states) {
            if (state.recording.sessionId === sessionId) {
                return true;
            }
        }
        return false;
    }
    /**
     * Get active recording for a session
     */
    getActiveRecordingForSession(sessionId) {
        const states = Array.from(this.activeRecordings.values());
        for (const state of states) {
            if (state.recording.sessionId === sessionId) {
                return state.recording;
            }
        }
        return null;
    }
    /**
     * Destroy the recorder and cleanup resources
     */
    destroy() {
        if (this.flushTimer) {
            clearInterval(this.flushTimer);
            this.flushTimer = null;
        }
        // Attempt to flush remaining buffers
        const entries = Array.from(this.activeRecordings.entries());
        for (const [recordingId] of entries) {
            this.flushBuffer(recordingId).catch((error) => {
                logger.error({ error, recordingId }, 'Failed to flush buffer on destroy');
            });
        }
        logger.info('Session recorder destroyed');
    }
    // ==========================================================================
    // Private Methods
    // ==========================================================================
    startFlushTimer() {
        this.flushTimer = setInterval(() => {
            this.flushAllBuffers().catch((error) => {
                logger.error({ error }, 'Failed to flush buffers');
            });
        }, this.flushIntervalMs);
    }
    async flushAllBuffers() {
        const entries = Array.from(this.activeRecordings.entries());
        for (const [recordingId, state] of entries) {
            if (state.buffer.length > 0) {
                await this.flushBuffer(recordingId);
            }
        }
    }
    async flushBuffer(recordingId) {
        const state = this.activeRecordings.get(recordingId);
        if (!state || state.buffer.length === 0) {
            return;
        }
        const events = [...state.buffer];
        state.buffer = [];
        await this.storage.flushEvents(recordingId, events);
        eventBufferSize.set(state.buffer.length);
        logger.debug({ recordingId, eventCount: events.length }, 'Flushed event buffer');
    }
    computeEventHash(event) {
        const payload = {
            id: event.id,
            recordingId: event.recordingId,
            timestamp: event.timestamp,
            sequenceNumber: event.sequenceNumber,
            eventType: event.eventType,
            details: event.details,
            previousHash: event.previousHash,
        };
        return createHash(HASH_ALGORITHM)
            .update(JSON.stringify(payload, Object.keys(payload).sort()))
            .digest('hex');
    }
    computeRecordingHash(recording) {
        const payload = {
            id: recording.id,
            sessionId: recording.sessionId,
            userId: recording.userId,
            startedAt: recording.startedAt,
            endedAt: recording.endedAt,
            metadata: recording.metadata,
            eventCount: recording.events.length,
            lastEventHash: recording.events[recording.events.length - 1]?.eventHash,
        };
        return createHash(HASH_ALGORITHM)
            .update(JSON.stringify(payload, Object.keys(payload).sort()))
            .digest('hex');
    }
    computeSummary(events, startedAt) {
        const summary = {
            totalEvents: events.length,
            eventsByType: {},
            dataAccessed: {
                resourceTypes: [],
                totalRecords: 0,
                operations: {},
            },
            permissionsUsed: {
                permissions: [],
                denied: 0,
                granted: 0,
            },
            errors: {
                count: 0,
                bySeverity: {},
            },
            apiCalls: {
                count: 0,
                avgDurationMs: 0,
                statusCodes: {},
            },
            durationSeconds: 0,
            integrityValid: true,
        };
        // Initialize event type counts
        for (const type of Object.values(SessionEventType)) {
            summary.eventsByType[type] = 0;
        }
        const resourceTypes = new Set();
        const permissions = new Set();
        let totalApiDuration = 0;
        // Verify hash chain and compute stats
        let previousHash = null;
        for (const event of events) {
            // Check hash chain
            if (event.previousHash !== previousHash) {
                summary.integrityValid = false;
            }
            previousHash = event.eventHash;
            // Count by type
            summary.eventsByType[event.eventType]++;
            // Process by type
            const details = event.details;
            switch (details.type) {
                case 'api_call': {
                    summary.apiCalls.count++;
                    totalApiDuration += details.durationMs;
                    const statusKey = String(details.statusCode);
                    summary.apiCalls.statusCodes[statusKey] =
                        (summary.apiCalls.statusCodes[statusKey] ?? 0) + 1;
                    break;
                }
                case 'data_access': {
                    resourceTypes.add(details.resourceType);
                    summary.dataAccessed.totalRecords += details.recordCount ?? 1;
                    summary.dataAccessed.operations[details.operation] =
                        (summary.dataAccessed.operations[details.operation] ?? 0) + 1;
                    break;
                }
                case 'permission_usage': {
                    permissions.add(details.permission);
                    if (details.granted) {
                        summary.permissionsUsed.granted++;
                    }
                    else {
                        summary.permissionsUsed.denied++;
                    }
                    break;
                }
                case 'error': {
                    summary.errors.count++;
                    summary.errors.bySeverity[details.severity] =
                        (summary.errors.bySeverity[details.severity] ?? 0) + 1;
                    break;
                }
            }
        }
        summary.dataAccessed.resourceTypes = Array.from(resourceTypes);
        summary.permissionsUsed.permissions = Array.from(permissions);
        if (summary.apiCalls.count > 0) {
            summary.apiCalls.avgDurationMs = totalApiDuration / summary.apiCalls.count;
        }
        // Calculate duration
        const lastEvent = events[events.length - 1];
        if (lastEvent) {
            const start = new Date(startedAt).getTime();
            const end = new Date(lastEvent.timestamp).getTime();
            summary.durationSeconds = (end - start) / 1000;
        }
        return summary;
    }
}
/**
 * Create a Fastify plugin for auto-recording requests during elevated sessions
 */
export function createSessionRecordingPlugin(options) {
    const { recorder, shouldRecord = () => true, getSessionId = (req) => req.session?.id ?? null, getUserId = (req) => req.user?.id ?? null, excludePaths = ['/health', '/ready', '/metrics'], recordRequestBodies = false, recordResponseBodies = false, } = options;
    return async function sessionRecordingPlugin(fastify) {
        // Add recording context to request
        fastify.decorateRequest('pamRecording', null);
        // Pre-handler to check for active recording
        fastify.addHook('preHandler', async (request) => {
            // Skip excluded paths
            if (excludePaths.some((p) => request.url.startsWith(p))) {
                return;
            }
            // Check if we should record this request
            if (!shouldRecord(request)) {
                return;
            }
            const sessionId = getSessionId(request);
            if (!sessionId) {
                return;
            }
            // Check for active recording
            const recording = recorder.getActiveRecordingForSession(sessionId);
            if (!recording) {
                return;
            }
            // Store recording ID on request for response hook
            request.pamRecording = {
                id: recording.id,
                startTime: Date.now(),
            };
        });
        // Response hook to record the API call
        fastify.addHook('onResponse', async (request, reply) => {
            const pamContext = request.pamRecording;
            if (!pamContext) {
                return;
            }
            const durationMs = Date.now() - pamContext.startTime;
            const eventDetails = {
                type: 'api_call',
                method: request.method,
                path: request.url,
                statusCode: reply.statusCode,
                durationMs,
                requestId: request.id,
            };
            // Optionally include query params
            if (Object.keys(request.query).length > 0) {
                eventDetails.query = request.query;
            }
            // Optionally include bodies
            if (recordRequestBodies && request.body) {
                eventDetails.requestBody = request.body;
            }
            try {
                await recorder.recordEvent(pamContext.id, eventDetails);
            }
            catch (error) {
                logger.error({ error, recordingId: pamContext.id, path: request.url }, 'Failed to record API call event');
            }
        });
        logger.info('Session recording plugin registered');
    };
}
/**
 * Create PAM session hooks
 */
export function createPAMSessionHooks(recorder) {
    return {
        async onSessionStart(sessionId, userId, metadata) {
            return recorder.startRecording(sessionId, userId, metadata);
        },
        async onSessionEnd(sessionId) {
            const recording = recorder.getActiveRecordingForSession(sessionId);
            if (!recording) {
                return null;
            }
            return recorder.stopRecording(recording.id);
        },
        async recordPermissionUsage(sessionId, permission, action, granted, details) {
            const recording = recorder.getActiveRecordingForSession(sessionId);
            if (!recording) {
                return;
            }
            const event = {
                type: 'permission_usage',
                permission,
                action,
                granted,
                ...details,
            };
            await recorder.recordEvent(recording.id, event);
        },
        async recordDataAccess(sessionId, operation, resourceType, details) {
            const recording = recorder.getActiveRecordingForSession(sessionId);
            if (!recording) {
                return;
            }
            const event = {
                type: 'data_access',
                operation,
                resourceType,
                ...details,
            };
            await recorder.recordEvent(recording.id, event);
        },
    };
}
// =============================================================================
// Factory Function
// =============================================================================
/**
 * Create a session recorder with default configuration
 */
export function createSessionRecorder(options) {
    const storage = options?.storage ?? new InMemoryRecordingStorage();
    return new SessionRecorder({
        storage,
        privacy: options?.privacy,
        retentionDays: options?.retentionDays,
        flushIntervalMs: options?.flushIntervalMs,
        maxBufferSize: options?.maxBufferSize,
        tenantId: options?.tenantId,
    });
}
// =============================================================================
// Exports
// =============================================================================
export { DEFAULT_PRIVACY_CONFIG, DEFAULT_RETENTION_DAYS, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BUFFER_SIZE, };
//# sourceMappingURL=session-recording.js.map