// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Break-Glass Emergency Access System
 *
 * Provides emergency privileged access for critical situations when normal
 * authorization cannot be obtained in time. Implements strict controls including:
 * - Multi-factor authentication verification
 * - Real-time notifications to all security admins
 * - SMS/phone alerts to CISO
 * - Comprehensive audit trail
 * - Automatic expiration and post-incident review requirements
 * - Usage limits to prevent abuse
 *
 * @packageDocumentation
 */
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import { createLogger } from '../../common/logger.js';
import { Counter, Histogram, Gauge } from 'prom-client';
import { vorionRegistry } from '../../common/metrics-registry.js';
const logger = createLogger({ component: 'break-glass' });
// =============================================================================
// Constants
// =============================================================================
/** Maximum duration of a break-glass session (4 hours) */
const MAX_SESSION_DURATION_MS = 4 * 60 * 60 * 1000;
/** Maximum number of break-glass uses per user per month */
const MAX_USES_PER_MONTH = 3;
/** Review deadline after session ends (24 hours) */
const REVIEW_DEADLINE_MS = 24 * 60 * 60 * 1000;
/** Renewal extension (4 hours, can only be used once) */
const RENEWAL_DURATION_MS = 4 * 60 * 60 * 1000;
// =============================================================================
// Metrics
// =============================================================================
const breakGlassActivations = new Counter({
    name: 'vorion_break_glass_activations_total',
    help: 'Total break-glass activations',
    labelNames: ['scope', 'outcome'],
    registers: [vorionRegistry],
});
const breakGlassRevocations = new Counter({
    name: 'vorion_break_glass_revocations_total',
    help: 'Total break-glass revocations',
    labelNames: ['reason'],
    registers: [vorionRegistry],
});
const activeBreakGlassSessions = new Gauge({
    name: 'vorion_break_glass_active_sessions',
    help: 'Number of currently active break-glass sessions',
    registers: [vorionRegistry],
});
const breakGlassActionsDuringSession = new Counter({
    name: 'vorion_break_glass_actions_total',
    help: 'Total actions performed during break-glass sessions',
    labelNames: ['action_type'],
    registers: [vorionRegistry],
});
const breakGlassSessionDuration = new Histogram({
    name: 'vorion_break_glass_session_duration_seconds',
    help: 'Duration of break-glass sessions',
    buckets: [300, 600, 1800, 3600, 7200, 14400], // 5m, 10m, 30m, 1h, 2h, 4h
    registers: [vorionRegistry],
});
// =============================================================================
// Types & Schemas
// =============================================================================
/**
 * Scope of break-glass access
 */
export const BreakGlassScope = {
    FULL: 'full',
    LIMITED: 'limited',
};
export const breakGlassScopeSchema = z.nativeEnum(BreakGlassScope);
/**
 * Status of a break-glass session
 */
export const BreakGlassStatus = {
    PENDING_VERIFICATION: 'pending_verification',
    ACTIVE: 'active',
    EXPIRED: 'expired',
    REVOKED: 'revoked',
    REVIEW_PENDING: 'review_pending',
    REVIEWED: 'reviewed',
};
export const breakGlassStatusSchema = z.nativeEnum(BreakGlassStatus);
/**
 * Verification method used for break-glass
 */
export const VerificationMethod = {
    MFA_TOTP: 'mfa_totp',
    MFA_BACKUP_CODE: 'mfa_backup_code',
    PHONE_CALLBACK: 'phone_callback',
    HARDWARE_TOKEN: 'hardware_token',
};
export const verificationMethodSchema = z.nativeEnum(VerificationMethod);
export const breakGlassRequestSchema = z.object({
    userId: z.string().min(1),
    reason: z.string().min(10, 'Reason must be at least 10 characters'),
    incidentId: z.string().optional(),
    scope: breakGlassScopeSchema,
    resources: z.array(z.string()).optional(),
    ipAddress: z.string().optional(),
    deviceFingerprint: z.string().optional(),
    userAgent: z.string().optional(),
}).refine((data) => data.scope !== 'limited' || (data.resources && data.resources.length > 0), { message: 'Resources must be specified for limited scope' });
export const breakGlassActionSchema = z.object({
    id: z.string().uuid(),
    timestamp: z.date(),
    type: z.string(),
    method: z.string().optional(),
    path: z.string().optional(),
    resourceId: z.string().optional(),
    ipAddress: z.string().optional(),
    userAgent: z.string().optional(),
    requestBodyHash: z.string().optional(),
    responseStatus: z.number().int().optional(),
    durationMs: z.number().int().optional(),
    metadata: z.record(z.unknown()).optional(),
});
export const breakGlassSessionSchema = z.object({
    id: z.string().uuid(),
    userId: z.string(),
    reason: z.string(),
    incidentId: z.string().optional(),
    scope: breakGlassScopeSchema,
    resources: z.array(z.string()).optional(),
    status: breakGlassStatusSchema,
    startedAt: z.date(),
    expiresAt: z.date(),
    endedAt: z.date().optional(),
    revokedBy: z.string().optional(),
    revocationReason: z.string().optional(),
    renewed: z.boolean(),
    actions: z.array(breakGlassActionSchema),
    requiresPostReview: z.literal(true),
    verificationMethods: z.array(verificationMethodSchema),
    activationIp: z.string().optional(),
    deviceFingerprint: z.string().optional(),
    notificationsSent: z.array(z.object({
        id: z.string(),
        channel: z.string(),
        target: z.string(),
        sentAt: z.date(),
        success: z.boolean(),
        error: z.string().optional(),
    })),
    review: z.object({
        reviewedAt: z.date(),
        reviewedBy: z.string(),
        approved: z.boolean(),
        findings: z.string(),
        recommendedActions: z.array(z.string()),
    }).optional(),
});
// =============================================================================
// Break-Glass Service
// =============================================================================
/**
 * Break-Glass Emergency Access Service
 *
 * Provides controlled emergency access for critical situations with
 * comprehensive auditing and safety controls.
 */
export class BreakGlassService {
    sessions = new Map();
    userMonthlyUsage = new Map();
    pendingVerifications = new Map();
    expirationTimers = new Map();
    config;
    notificationService;
    mfaService;
    incidentService;
    auditLogger;
    complianceExporter;
    constructor(config = {}, deps = {}) {
        this.config = {
            maxSessionDurationMs: config.maxSessionDurationMs ?? MAX_SESSION_DURATION_MS,
            maxUsesPerMonth: config.maxUsesPerMonth ?? MAX_USES_PER_MONTH,
            reviewDeadlineMs: config.reviewDeadlineMs ?? REVIEW_DEADLINE_MS,
            requirePhoneCallbackForFullScope: config.requirePhoneCallbackForFullScope ?? true,
            securityAdminIds: config.securityAdminIds ?? [],
            cisoContact: config.cisoContact ?? { userId: '', phone: '', email: '' },
            slackWebhookUrl: config.slackWebhookUrl ?? '',
            pagerDutyRoutingKey: config.pagerDutyRoutingKey ?? '',
        };
        this.notificationService = deps.notificationService;
        this.mfaService = deps.mfaService;
        this.incidentService = deps.incidentService;
        this.auditLogger = deps.auditLogger;
        this.complianceExporter = deps.complianceExporter;
        logger.info('BreakGlassService initialized', {
            maxSessionDuration: this.config.maxSessionDurationMs,
            maxUsesPerMonth: this.config.maxUsesPerMonth,
            requirePhoneCallbackForFullScope: this.config.requirePhoneCallbackForFullScope,
        });
    }
    // ===========================================================================
    // Core Methods
    // ===========================================================================
    /**
     * Initiate a break-glass emergency access request
     *
     * This creates a pending session that requires verification before activation.
     * All security admins are immediately notified.
     */
    async initiateBreakGlass(request) {
        // Validate request
        const validated = breakGlassRequestSchema.parse(request);
        // Check monthly usage limit
        const monthlyUsage = this.getMonthlyUsage(validated.userId);
        if (monthlyUsage >= this.config.maxUsesPerMonth) {
            breakGlassActivations.inc({ scope: validated.scope, outcome: 'limit_exceeded' });
            await this.auditLog('BREAK_GLASS_LIMIT_EXCEEDED', validated.userId, {
                monthlyUsage,
                maxAllowed: this.config.maxUsesPerMonth,
            });
            throw new BreakGlassError('MONTHLY_LIMIT_EXCEEDED', `User has exceeded the maximum ${this.config.maxUsesPerMonth} break-glass uses this month`);
        }
        // Create session in pending state
        const now = new Date();
        const sessionId = uuidv4();
        const session = {
            id: sessionId,
            userId: validated.userId,
            reason: validated.reason,
            incidentId: validated.incidentId,
            scope: validated.scope,
            resources: validated.resources,
            status: BreakGlassStatus.PENDING_VERIFICATION,
            startedAt: now,
            expiresAt: new Date(now.getTime() + this.config.maxSessionDurationMs),
            renewed: false,
            actions: [],
            requiresPostReview: true,
            verificationMethods: [],
            activationIp: validated.ipAddress,
            deviceFingerprint: validated.deviceFingerprint,
            notificationsSent: [],
        };
        this.sessions.set(sessionId, session);
        // Generate phone callback code if needed
        if (validated.scope === 'full' && this.config.requirePhoneCallbackForFullScope) {
            const callbackCode = this.generateCallbackCode();
            this.pendingVerifications.set(sessionId, {
                sessionId,
                phoneCallbackCode: callbackCode,
                expiresAt: new Date(now.getTime() + 10 * 60 * 1000), // 10 minutes
            });
        }
        // Immediately notify all security admins
        await this.notifyAllSecurityAdmins(session, 'initiated');
        // Alert CISO via SMS/phone for critical access
        if (validated.scope === 'full') {
            await this.alertCiso(session);
        }
        // Auto-create incident if none linked
        if (!validated.incidentId && this.incidentService) {
            try {
                const incident = await this.incidentService.createIncident({
                    title: `Break-Glass Access: ${validated.userId}`,
                    description: validated.reason,
                    severity: validated.scope === 'full' ? 'P1' : 'P2',
                    type: 'unauthorized_access',
                    metadata: { breakGlassSessionId: sessionId },
                });
                session.incidentId = incident.id;
                logger.info('Auto-created incident for break-glass', { sessionId, incidentId: incident.id });
            }
            catch (error) {
                logger.error('Failed to auto-create incident', { sessionId, error });
            }
        }
        await this.auditLog('BREAK_GLASS_INITIATED', validated.userId, {
            sessionId,
            scope: validated.scope,
            reason: validated.reason,
            incidentId: session.incidentId,
            ipAddress: validated.ipAddress,
        });
        logger.warn('Break-glass access initiated', {
            sessionId,
            userId: validated.userId,
            scope: validated.scope,
            reason: validated.reason,
        });
        return session;
    }
    /**
     * Verify MFA for break-glass activation
     */
    async verifyMfa(sessionId, verification) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new BreakGlassError('SESSION_NOT_FOUND', 'Break-glass session not found');
        }
        if (session.status !== BreakGlassStatus.PENDING_VERIFICATION) {
            throw new BreakGlassError('INVALID_STATE', 'Session is not pending verification');
        }
        if (!this.mfaService) {
            logger.warn('MFA service not configured, skipping verification');
            session.verificationMethods.push(verification.method);
            return true;
        }
        let verified = false;
        switch (verification.method) {
            case VerificationMethod.MFA_TOTP:
                verified = await this.mfaService.verifyTotp(session.userId, verification.code ?? '');
                break;
            case VerificationMethod.MFA_BACKUP_CODE:
                verified = await this.mfaService.verifyBackupCode(session.userId, verification.code ?? '');
                break;
            case VerificationMethod.HARDWARE_TOKEN:
                verified = await this.mfaService.verifyHardwareToken(session.userId, verification.code ?? '');
                break;
            case VerificationMethod.PHONE_CALLBACK:
                const pending = this.pendingVerifications.get(sessionId);
                if (pending && pending.phoneCallbackCode === verification.code) {
                    verified = true;
                    this.pendingVerifications.delete(sessionId);
                }
                break;
        }
        if (verified) {
            session.verificationMethods.push(verification.method);
            await this.auditLog('BREAK_GLASS_MFA_VERIFIED', session.userId, {
                sessionId,
                method: verification.method,
            });
        }
        else {
            await this.auditLog('BREAK_GLASS_MFA_FAILED', session.userId, {
                sessionId,
                method: verification.method,
            });
        }
        return verified;
    }
    /**
     * Activate a break-glass session after verification
     */
    async activateSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new BreakGlassError('SESSION_NOT_FOUND', 'Break-glass session not found');
        }
        if (session.status !== BreakGlassStatus.PENDING_VERIFICATION) {
            throw new BreakGlassError('INVALID_STATE', 'Session is not pending verification');
        }
        // Verify that required verification methods are completed
        if (session.verificationMethods.length === 0) {
            throw new BreakGlassError('MFA_REQUIRED', 'At least one MFA verification is required');
        }
        // For full scope, require phone callback if configured
        if (session.scope === 'full' &&
            this.config.requirePhoneCallbackForFullScope &&
            !session.verificationMethods.includes(VerificationMethod.PHONE_CALLBACK)) {
            throw new BreakGlassError('PHONE_CALLBACK_REQUIRED', 'Phone callback verification required for full scope access');
        }
        // Activate the session
        const now = new Date();
        session.status = BreakGlassStatus.ACTIVE;
        session.startedAt = now;
        session.expiresAt = new Date(now.getTime() + this.config.maxSessionDurationMs);
        // Increment monthly usage
        this.incrementMonthlyUsage(session.userId);
        // Set up expiration timer
        this.setExpirationTimer(session);
        // Update active sessions metric
        activeBreakGlassSessions.inc();
        breakGlassActivations.inc({ scope: session.scope, outcome: 'success' });
        // Notify all security admins of activation
        await this.notifyAllSecurityAdmins(session, 'activated');
        await this.auditLog('BREAK_GLASS_ACTIVATED', session.userId, {
            sessionId,
            scope: session.scope,
            expiresAt: session.expiresAt.toISOString(),
            verificationMethods: session.verificationMethods,
        });
        logger.warn('Break-glass session activated', {
            sessionId,
            userId: session.userId,
            scope: session.scope,
            expiresAt: session.expiresAt,
        });
        return session;
    }
    /**
     * Validate if a break-glass session is currently valid
     */
    async validateBreakGlass(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            return false;
        }
        // Check if active
        if (session.status !== BreakGlassStatus.ACTIVE) {
            return false;
        }
        // Check if expired
        if (new Date() >= session.expiresAt) {
            await this.expireSession(sessionId);
            return false;
        }
        return true;
    }
    /**
     * Revoke a break-glass session
     */
    async revokeBreakGlass(sessionId, revokedBy, reason) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new BreakGlassError('SESSION_NOT_FOUND', 'Break-glass session not found');
        }
        if (session.status !== BreakGlassStatus.ACTIVE && session.status !== BreakGlassStatus.PENDING_VERIFICATION) {
            throw new BreakGlassError('INVALID_STATE', 'Session cannot be revoked in current state');
        }
        const now = new Date();
        const wasActive = session.status === BreakGlassStatus.ACTIVE;
        session.status = BreakGlassStatus.REVOKED;
        session.endedAt = now;
        session.revokedBy = revokedBy;
        session.revocationReason = reason;
        // Clear expiration timer
        this.clearExpirationTimer(sessionId);
        // Update metrics
        if (wasActive) {
            activeBreakGlassSessions.dec();
            const durationSeconds = (now.getTime() - session.startedAt.getTime()) / 1000;
            breakGlassSessionDuration.observe(durationSeconds);
        }
        breakGlassRevocations.inc({ reason: reason ? 'manual' : 'unspecified' });
        // Transition to review pending
        session.status = BreakGlassStatus.REVIEW_PENDING;
        // Notify all security admins
        await this.notifyAllSecurityAdmins(session, 'revoked');
        // Export to compliance
        await this.exportToCompliance(session);
        await this.auditLog('BREAK_GLASS_REVOKED', revokedBy, {
            sessionId,
            userId: session.userId,
            reason,
            duration: wasActive ? now.getTime() - session.startedAt.getTime() : 0,
            actionsCount: session.actions.length,
        });
        logger.warn('Break-glass session revoked', {
            sessionId,
            userId: session.userId,
            revokedBy,
            reason,
        });
    }
    /**
     * Get all currently active break-glass sessions
     */
    async getActiveBreakGlass() {
        const activeSessions = [];
        const sessions = Array.from(this.sessions.values());
        for (const session of sessions) {
            if (session.status === BreakGlassStatus.ACTIVE) {
                // Verify it hasn't expired
                if (new Date() < session.expiresAt) {
                    activeSessions.push(session);
                }
                else {
                    await this.expireSession(session.id);
                }
            }
        }
        return activeSessions;
    }
    /**
     * Get full audit trail for a break-glass session
     */
    async auditBreakGlass(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new BreakGlassError('SESSION_NOT_FOUND', 'Break-glass session not found');
        }
        // Build timeline from session events
        const timeline = [
            {
                timestamp: session.startedAt,
                eventType: 'SESSION_INITIATED',
                actor: session.userId,
                details: {
                    scope: session.scope,
                    reason: session.reason,
                    resources: session.resources,
                },
            },
        ];
        // Add verification events
        for (const method of session.verificationMethods) {
            timeline.push({
                timestamp: session.startedAt,
                eventType: 'MFA_VERIFIED',
                actor: session.userId,
                details: { method },
            });
        }
        // Add notification events
        for (const notification of session.notificationsSent) {
            timeline.push({
                timestamp: notification.sentAt,
                eventType: 'NOTIFICATION_SENT',
                actor: 'system',
                details: {
                    channel: notification.channel,
                    target: notification.target,
                    success: notification.success,
                },
            });
        }
        // Add action events
        for (const action of session.actions) {
            timeline.push({
                timestamp: action.timestamp,
                eventType: 'ACTION_PERFORMED',
                actor: session.userId,
                details: {
                    type: action.type,
                    method: action.method,
                    path: action.path,
                    resourceId: action.resourceId,
                },
            });
        }
        // Add end event if session ended
        if (session.endedAt) {
            timeline.push({
                timestamp: session.endedAt,
                eventType: session.status === BreakGlassStatus.REVOKED ? 'SESSION_REVOKED' : 'SESSION_EXPIRED',
                actor: session.revokedBy ?? 'system',
                details: {
                    reason: session.revocationReason,
                },
            });
        }
        // Sort timeline by timestamp
        timeline.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
        // Build compliance export
        const complianceExport = this.buildComplianceExport(session);
        return {
            session,
            monthlyUsageCount: this.getMonthlyUsage(session.userId),
            timeline,
            complianceExport,
        };
    }
    /**
     * Record an action performed during a break-glass session
     */
    async recordAction(sessionId, action) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new BreakGlassError('SESSION_NOT_FOUND', 'Break-glass session not found');
        }
        if (session.status !== BreakGlassStatus.ACTIVE) {
            throw new BreakGlassError('SESSION_NOT_ACTIVE', 'Break-glass session is not active');
        }
        const fullAction = {
            ...action,
            id: uuidv4(),
            timestamp: new Date(),
        };
        session.actions.push(fullAction);
        breakGlassActionsDuringSession.inc({ action_type: action.type });
        await this.auditLog('BREAK_GLASS_ACTION', session.userId, {
            sessionId,
            actionId: fullAction.id,
            type: action.type,
            method: action.method,
            path: action.path,
            resourceId: action.resourceId,
        });
    }
    /**
     * Renew a break-glass session (can only be done once)
     */
    async renewSession(sessionId, reason) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new BreakGlassError('SESSION_NOT_FOUND', 'Break-glass session not found');
        }
        if (session.status !== BreakGlassStatus.ACTIVE) {
            throw new BreakGlassError('SESSION_NOT_ACTIVE', 'Session is not active');
        }
        if (session.renewed) {
            throw new BreakGlassError('ALREADY_RENEWED', 'Session has already been renewed once');
        }
        // Extend expiration
        session.expiresAt = new Date(session.expiresAt.getTime() + RENEWAL_DURATION_MS);
        session.renewed = true;
        // Reset expiration timer
        this.clearExpirationTimer(sessionId);
        this.setExpirationTimer(session);
        // Notify security admins
        await this.notifyAllSecurityAdmins(session, 'renewed');
        await this.auditLog('BREAK_GLASS_RENEWED', session.userId, {
            sessionId,
            reason,
            newExpiresAt: session.expiresAt.toISOString(),
        });
        logger.warn('Break-glass session renewed', {
            sessionId,
            userId: session.userId,
            reason,
            newExpiresAt: session.expiresAt,
        });
        return session;
    }
    /**
     * Submit post-incident review for a break-glass session
     */
    async submitReview(sessionId, reviewedBy, review) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new BreakGlassError('SESSION_NOT_FOUND', 'Break-glass session not found');
        }
        if (session.status !== BreakGlassStatus.REVIEW_PENDING) {
            throw new BreakGlassError('INVALID_STATE', 'Session is not pending review');
        }
        session.review = {
            ...review,
            reviewedAt: new Date(),
            reviewedBy,
        };
        session.status = BreakGlassStatus.REVIEWED;
        // Export final compliance record
        await this.exportToCompliance(session);
        await this.auditLog('BREAK_GLASS_REVIEWED', reviewedBy, {
            sessionId,
            approved: review.approved,
            findings: review.findings,
            recommendedActions: review.recommendedActions,
        });
        logger.info('Break-glass review submitted', {
            sessionId,
            reviewedBy,
            approved: review.approved,
        });
        return session;
    }
    /**
     * Get session by ID
     */
    getSession(sessionId) {
        return this.sessions.get(sessionId);
    }
    /**
     * Get sessions pending review
     */
    getPendingReviews() {
        const pending = [];
        const sessions = Array.from(this.sessions.values());
        for (const session of sessions) {
            if (session.status === BreakGlassStatus.REVIEW_PENDING) {
                pending.push(session);
            }
        }
        return pending;
    }
    /**
     * Check if a resource is accessible under a break-glass session
     */
    isResourceAccessible(sessionId, resourceId) {
        const session = this.sessions.get(sessionId);
        if (!session || session.status !== BreakGlassStatus.ACTIVE) {
            return false;
        }
        // Full scope grants access to all resources
        if (session.scope === 'full') {
            return true;
        }
        // Limited scope - check if resource is in allowed list
        return session.resources?.includes(resourceId) ?? false;
    }
    // ===========================================================================
    // Private Methods
    // ===========================================================================
    async expireSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session || session.status !== BreakGlassStatus.ACTIVE) {
            return;
        }
        const now = new Date();
        session.status = BreakGlassStatus.EXPIRED;
        session.endedAt = now;
        // Clear expiration timer
        this.clearExpirationTimer(sessionId);
        // Update metrics
        activeBreakGlassSessions.dec();
        const durationSeconds = (now.getTime() - session.startedAt.getTime()) / 1000;
        breakGlassSessionDuration.observe(durationSeconds);
        breakGlassRevocations.inc({ reason: 'expired' });
        // Transition to review pending
        session.status = BreakGlassStatus.REVIEW_PENDING;
        // Notify security admins
        await this.notifyAllSecurityAdmins(session, 'expired');
        // Export to compliance
        await this.exportToCompliance(session);
        await this.auditLog('BREAK_GLASS_EXPIRED', 'system', {
            sessionId,
            userId: session.userId,
            duration: now.getTime() - session.startedAt.getTime(),
            actionsCount: session.actions.length,
        });
        logger.warn('Break-glass session expired', {
            sessionId,
            userId: session.userId,
        });
    }
    setExpirationTimer(session) {
        const timeUntilExpiry = session.expiresAt.getTime() - Date.now();
        if (timeUntilExpiry > 0) {
            const timer = setTimeout(() => {
                this.expireSession(session.id).catch((error) => {
                    logger.error('Error expiring session', { sessionId: session.id, error });
                });
            }, timeUntilExpiry);
            this.expirationTimers.set(session.id, timer);
        }
    }
    clearExpirationTimer(sessionId) {
        const timer = this.expirationTimers.get(sessionId);
        if (timer) {
            clearTimeout(timer);
            this.expirationTimers.delete(sessionId);
        }
    }
    getMonthlyUsage(userId) {
        const currentMonth = new Date().toISOString().substring(0, 7); // YYYY-MM
        const usage = this.userMonthlyUsage.get(userId);
        if (usage && usage.month === currentMonth) {
            return usage.count;
        }
        return 0;
    }
    incrementMonthlyUsage(userId) {
        const currentMonth = new Date().toISOString().substring(0, 7);
        const usage = this.userMonthlyUsage.get(userId);
        if (usage && usage.month === currentMonth) {
            usage.count++;
        }
        else {
            this.userMonthlyUsage.set(userId, { month: currentMonth, count: 1 });
        }
    }
    generateCallbackCode() {
        // Generate a 6-digit numeric code
        return String(Math.floor(100000 + Math.random() * 900000));
    }
    async notifyAllSecurityAdmins(session, event) {
        const eventMessages = {
            initiated: 'BREAK-GLASS ACCESS INITIATED',
            activated: 'BREAK-GLASS ACCESS ACTIVATED',
            revoked: 'BREAK-GLASS ACCESS REVOKED',
            expired: 'BREAK-GLASS ACCESS EXPIRED',
            renewed: 'BREAK-GLASS ACCESS RENEWED',
        };
        const message = {
            event: eventMessages[event],
            sessionId: session.id,
            userId: session.userId,
            scope: session.scope,
            reason: session.reason,
            incidentId: session.incidentId,
            timestamp: new Date().toISOString(),
            resources: session.resources,
            actionsCount: session.actions.length,
        };
        // Send Slack notification
        if (this.notificationService && this.config.slackWebhookUrl) {
            try {
                const result = await this.notificationService.sendSlack(this.config.slackWebhookUrl, {
                    text: `[${event.toUpperCase()}] Break-Glass Access`,
                    blocks: [
                        {
                            type: 'header',
                            text: { type: 'plain_text', text: eventMessages[event] },
                        },
                        {
                            type: 'section',
                            fields: [
                                { type: 'mrkdwn', text: `*User:*\n${session.userId}` },
                                { type: 'mrkdwn', text: `*Scope:*\n${session.scope}` },
                                { type: 'mrkdwn', text: `*Session ID:*\n${session.id}` },
                                { type: 'mrkdwn', text: `*Incident:*\n${session.incidentId ?? 'N/A'}` },
                            ],
                        },
                        {
                            type: 'section',
                            text: { type: 'mrkdwn', text: `*Reason:*\n${session.reason}` },
                        },
                    ],
                });
                session.notificationsSent.push({
                    id: uuidv4(),
                    channel: 'slack',
                    target: this.config.slackWebhookUrl,
                    sentAt: new Date(),
                    success: result.success,
                    error: result.error,
                });
            }
            catch (error) {
                logger.error('Failed to send Slack notification', { sessionId: session.id, error });
            }
        }
        // Send PagerDuty alert
        if (this.notificationService && this.config.pagerDutyRoutingKey && event === 'activated') {
            try {
                const result = await this.notificationService.sendPagerDuty(this.config.pagerDutyRoutingKey, {
                    routing_key: this.config.pagerDutyRoutingKey,
                    event_action: 'trigger',
                    dedup_key: `break-glass-${session.id}`,
                    payload: {
                        summary: `Break-Glass Access Activated: ${session.userId}`,
                        source: 'vorion-break-glass',
                        severity: session.scope === 'full' ? 'critical' : 'warning',
                        custom_details: message,
                    },
                });
                session.notificationsSent.push({
                    id: uuidv4(),
                    channel: 'pagerduty',
                    target: 'pagerduty',
                    sentAt: new Date(),
                    success: result.success,
                    error: result.error,
                });
            }
            catch (error) {
                logger.error('Failed to send PagerDuty alert', { sessionId: session.id, error });
            }
        }
        // Email security admins
        if (this.notificationService) {
            for (const adminId of this.config.securityAdminIds) {
                try {
                    const result = await this.notificationService.sendEmail(adminId, `[ALERT] ${eventMessages[event]} - ${session.userId}`, JSON.stringify(message, null, 2));
                    session.notificationsSent.push({
                        id: uuidv4(),
                        channel: 'email',
                        target: adminId,
                        sentAt: new Date(),
                        success: result.success,
                        error: result.error,
                    });
                }
                catch (error) {
                    logger.error('Failed to email security admin', { sessionId: session.id, adminId, error });
                }
            }
        }
    }
    async alertCiso(session) {
        if (!this.notificationService || !this.config.cisoContact.phone) {
            return;
        }
        // Send SMS
        try {
            const message = `BREAK-GLASS ALERT: ${session.userId} initiated ${session.scope} scope access. Session: ${session.id}. Reason: ${session.reason.substring(0, 100)}`;
            const result = await this.notificationService.sendSms(this.config.cisoContact.phone, message);
            session.notificationsSent.push({
                id: uuidv4(),
                channel: 'sms',
                target: this.config.cisoContact.phone,
                sentAt: new Date(),
                success: result.success,
                error: result.error,
            });
        }
        catch (error) {
            logger.error('Failed to send SMS to CISO', { sessionId: session.id, error });
        }
        // Initiate phone callback if full scope
        if (session.scope === 'full') {
            const pending = this.pendingVerifications.get(session.id);
            if (pending?.phoneCallbackCode) {
                try {
                    const result = await this.notificationService.initiatePhoneCallback(this.config.cisoContact.phone, pending.phoneCallbackCode);
                    session.notificationsSent.push({
                        id: uuidv4(),
                        channel: 'phone',
                        target: this.config.cisoContact.phone,
                        sentAt: new Date(),
                        success: result.success,
                        error: result.error,
                    });
                }
                catch (error) {
                    logger.error('Failed to initiate phone callback', { sessionId: session.id, error });
                }
            }
        }
    }
    async auditLog(eventType, actor, metadata) {
        if (this.auditLogger) {
            try {
                await this.auditLogger.log({
                    eventType,
                    actor: { type: 'user', id: actor },
                    action: eventType.toLowerCase(),
                    resource: { type: 'break_glass', id: metadata.sessionId ?? 'unknown' },
                    outcome: 'success',
                    metadata,
                });
            }
            catch (error) {
                logger.error('Failed to write audit log', { eventType, error });
            }
        }
        // Always log to console for break-glass events
        logger.warn(`Audit: ${eventType}`, { actor, ...metadata });
    }
    buildComplianceExport(session) {
        const resourcesAccessed = new Set();
        for (const action of session.actions) {
            if (action.resourceId) {
                resourcesAccessed.add(action.resourceId);
            }
            if (action.path) {
                resourcesAccessed.add(action.path);
            }
        }
        // Build hash chain from actions
        const hashChain = this.computeHashChain(session);
        return {
            exportedAt: new Date(),
            sessionId: session.id,
            userId: session.userId,
            reason: session.reason,
            scope: session.scope,
            startTime: session.startedAt.toISOString(),
            endTime: (session.endedAt ?? session.expiresAt).toISOString(),
            duration: (session.endedAt ?? new Date()).getTime() - session.startedAt.getTime(),
            actionsCount: session.actions.length,
            resourcesAccessed: Array.from(resourcesAccessed),
            incidentId: session.incidentId,
            reviewStatus: session.review ? (session.review.approved ? 'approved' : 'flagged') : 'pending',
            reviewFindings: session.review?.findings,
            hashChain,
        };
    }
    computeHashChain(session) {
        const crypto = require('crypto');
        let hash = crypto.createHash('sha256')
            .update(`${session.id}:${session.startedAt.toISOString()}:${session.userId}`)
            .digest('hex');
        for (const action of session.actions) {
            hash = crypto.createHash('sha256')
                .update(`${hash}:${action.id}:${action.timestamp.toISOString()}:${action.type}`)
                .digest('hex');
        }
        return hash;
    }
    async exportToCompliance(session) {
        if (this.complianceExporter) {
            const exportData = this.buildComplianceExport(session);
            try {
                await this.complianceExporter.export(exportData);
                logger.info('Exported break-glass session to compliance', { sessionId: session.id });
            }
            catch (error) {
                logger.error('Failed to export to compliance', { sessionId: session.id, error });
            }
        }
    }
    /**
     * Clean up resources
     */
    destroy() {
        // Clear all expiration timers
        const timers = Array.from(this.expirationTimers.entries());
        for (const [_sessionId, timer] of timers) {
            clearTimeout(timer);
        }
        this.expirationTimers.clear();
        logger.info('BreakGlassService destroyed');
    }
}
// =============================================================================
// Error Class
// =============================================================================
/**
 * Break-glass specific error
 */
export class BreakGlassError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.code = code;
        this.name = 'BreakGlassError';
    }
}
/**
 * Create middleware that allows break-glass sessions to override normal authorization
 *
 * This middleware should be added to protected routes that need to support
 * emergency access. It checks for a valid break-glass session and records
 * all actions performed during the session.
 *
 * @example
 * ```typescript
 * fastify.route({
 *   method: 'POST',
 *   url: '/admin/critical-action',
 *   preHandler: [
 *     normalAuthMiddleware,
 *     allowBreakGlass({ service: breakGlassService }),
 *   ],
 *   handler: handleCriticalAction,
 * });
 * ```
 */
export function allowBreakGlass(options) {
    const sessionHeader = options.sessionHeader ?? 'x-break-glass-session';
    return async (request, reply) => {
        const sessionId = request.headers[sessionHeader];
        if (!sessionId) {
            // No break-glass session, proceed with normal authorization
            return;
        }
        const startTime = Date.now();
        // Validate the session
        const isValid = await options.service.validateBreakGlass(sessionId);
        if (!isValid) {
            logger.warn('Invalid break-glass session attempted', {
                sessionId,
                ip: request.ip,
                path: request.url,
            });
            return reply.status(403).send({
                error: {
                    code: 'INVALID_BREAK_GLASS_SESSION',
                    message: 'Break-glass session is invalid or expired',
                },
            });
        }
        const session = options.service.getSession(sessionId);
        if (!session) {
            return reply.status(403).send({
                error: {
                    code: 'SESSION_NOT_FOUND',
                    message: 'Break-glass session not found',
                },
            });
        }
        // Check scope if required
        if (options.requiredScope && session.scope !== 'full' && session.scope !== options.requiredScope) {
            logger.warn('Break-glass session scope insufficient', {
                sessionId,
                required: options.requiredScope,
                actual: session.scope,
            });
            return reply.status(403).send({
                error: {
                    code: 'INSUFFICIENT_SCOPE',
                    message: `Break-glass session requires ${options.requiredScope} scope`,
                },
            });
        }
        // Check resource access for limited scope
        if (options.resourceId && session.scope === 'limited') {
            const resourceId = typeof options.resourceId === 'function'
                ? options.resourceId(request)
                : options.resourceId;
            if (!options.service.isResourceAccessible(sessionId, resourceId)) {
                logger.warn('Break-glass session does not have access to resource', {
                    sessionId,
                    resourceId,
                });
                return reply.status(403).send({
                    error: {
                        code: 'RESOURCE_NOT_ACCESSIBLE',
                        message: 'Break-glass session does not have access to this resource',
                    },
                });
            }
        }
        // Compute request body hash for audit
        let requestBodyHash;
        if (request.body) {
            const crypto = require('crypto');
            requestBodyHash = crypto.createHash('sha256')
                .update(JSON.stringify(request.body))
                .digest('hex')
                .substring(0, 16);
        }
        // Decorate request with break-glass context
        request.breakGlassSession = session;
        // Record the action - we do this synchronously in the preHandler
        // since Fastify reply hooks have different signatures
        const recordActionAsync = async () => {
            const durationMs = Date.now() - startTime;
            await options.service.recordAction(sessionId, {
                type: 'http_request',
                method: request.method,
                path: request.url,
                ipAddress: request.ip,
                userAgent: request.headers['user-agent'],
                requestBodyHash,
                responseStatus: reply.statusCode,
                durationMs,
                metadata: {
                    routeOptions: request.routeOptions?.url,
                    params: request.params,
                    query: request.query,
                },
            });
        };
        // Use reply.then to record after response is sent
        reply.then(recordActionAsync, (error) => {
            logger.error('Error recording break-glass action', { sessionId, error });
        });
        logger.info('Break-glass access granted', {
            sessionId,
            userId: session.userId,
            method: request.method,
            path: request.url,
        });
        // Skip normal authorization - break-glass grants access
        // This is indicated by not returning a 4xx error
    };
}
// =============================================================================
// Factory & Exports
// =============================================================================
/**
 * Create a new break-glass service instance
 */
export function createBreakGlassService(config, deps) {
    return new BreakGlassService(config, deps);
}
//# sourceMappingURL=break-glass.js.map