/**
 * ApprovalQueue -- Manages APPROVAL_REQUIRED workflow for tool access.
 *
 * When a tool call requires human approval, it's queued here. Supports:
 *   - Timeout with configurable default action (deny)
 *   - Escalation to a designated escalation agent
 *   - Delegation (approved by another authorized agent)
 *   - Persistent queue (survives process restart)
 *
 * Required by:
 *   - SOC 2 CC6.1: Logical and physical access
 *   - HIPAA 164.312(a)(1): Access control
 *   - ISO 27001 A.9.2.3: Management of privileged access
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
/**
 * DA-HIGH-5: returns true if any pack in the given list requires the
 * `approval-queue` module, i.e. enforces human-in-the-loop. Used by
 * higher-level init code (governance-server, CLI) to decide whether
 * to pass `requireSecret: true` into the ApprovalQueue constructor.
 *
 * The import of CompliancePack type is deferred via duck-typing so
 * this module stays at the bottom of the dependency graph.
 */
export function packsRequireApprovalQueue(packs) {
    for (const pack of packs) {
        const modules = pack.requiredModules ?? [];
        for (const mod of modules) {
            if (mod.name === 'approval-queue' && mod.required !== false) {
                return true;
            }
        }
    }
    return false;
}
export class ApprovalQueue {
    config;
    queue = new Map();
    queueFile;
    constructor(config) {
        this.config = {
            timeoutMs: 600000, // 10 min (DEV MODE -- production: 300000 / 5min)
            timeoutAction: 'deny',
            escalationTarget: '',
            delegateApprovers: [],
            maxQueueSize: 100,
            approvalSecret: '',
            requireSecret: false,
            // P4-RET-01 defaults: no classifier unless caller provides one (undefined is safe).
            dataClassifier: undefined,
            tenantId: '',
            contractId: '',
            activePackIds: [],
            ...config,
        };
        if (this.config.timeoutAction === 'escalate' && !this.config.escalationTarget) {
            throw new Error('escalationTarget is required when timeoutAction is "escalate"');
        }
        // DA-HIGH-5: when requireSecret is set (by callers who have an
        // active HITL pack), refuse to run without a real HMAC secret.
        if (this.config.requireSecret) {
            if (!this.config.approvalSecret || this.config.approvalSecret.length < 32) {
                throw new Error('SECURITY: ApprovalQueue requireSecret=true but approvalSecret is ' +
                    'missing or under 32 characters. A HITL-required compliance pack ' +
                    'is active; running without a verifiable approval token would ' +
                    'silently degrade the human-in-the-loop control. ' +
                    'Set APPROVAL_SECRET in the environment or disable HITL packs.');
            }
        }
        if (!fs.existsSync(config.queueDir)) {
            fs.mkdirSync(config.queueDir, { recursive: true });
        }
        this.queueFile = path.join(config.queueDir, 'approval-queue.json');
        this.loadFromDisk();
    }
    /**
     * Submit a tool call for approval.
     * Returns the request ID for polling.
     */
    submit(agent, tool, inputSummary, reason) {
        this.pruneExpired();
        if (this.queue.size >= this.config.maxQueueSize) {
            throw new Error(`Approval queue full (${this.config.maxQueueSize} max). ` +
                'Resolve pending requests before submitting new ones.');
        }
        const now = new Date();
        const request = {
            id: `apr_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`,
            agent,
            tool,
            inputSummary,
            reason,
            status: 'pending',
            createdAt: now.toISOString(),
            expiresAt: new Date(now.getTime() + this.config.timeoutMs).toISOString(),
        };
        // P4-RET-01: Stamp DataClassTag at the approval-queue insert write site.
        // Fail-closed: if a classifier is configured and classification throws,
        // the write is refused (UnclassifiedWriteError or UnboundDataError).
        if (this.config.dataClassifier) {
            request.dataClassTag = this.config.dataClassifier.classify('approval-queue:insert', {
                tenantId: this.config.tenantId ?? '',
                contractId: this.config.contractId ?? '',
                activePackIds: this.config.activePackIds ?? [],
            });
        }
        this.queue.set(request.id, request);
        this.saveToDisk();
        return request;
    }
    /**
     * Resolve an approval request (approve or deny).
     * Only humans or authorized delegate agents can resolve.
     */
    resolve(decision) {
        const request = this.queue.get(decision.requestId);
        if (!request) {
            throw new Error(`Approval request '${decision.requestId}' not found`);
        }
        if (request.status !== 'pending') {
            throw new Error(`Request '${decision.requestId}' already resolved with status '${request.status}'`);
        }
        // Verify the resolver is authorized
        const isHumanApproval = decision.resolvedBy.startsWith('human:');
        const isDelegateApproval = this.config.delegateApprovers.includes(decision.resolvedBy);
        if (isHumanApproval) {
            // Human approvals require a valid HMAC token proving the approval
            // came through an authenticated channel (dashboard, CLI, etc.)
            if (!decision.approvalToken || !this.config.approvalSecret) {
                throw new Error('Human approval requires a valid approvalToken and configured approvalSecret. ' +
                    'Approvals must come through an authenticated channel.');
            }
            const expectedToken = crypto.createHmac('sha256', this.config.approvalSecret)
                .update(`${decision.requestId}:${decision.status}`)
                .digest('hex');
            // F-NEW-CRIT-35: length-check before timingSafeEqual. Prior to this
            // fix, a malformed approvalToken (wrong-length hex) crashed the
            // server with uncaught RangeError [ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH]
            // -- DoS via the approvals API.
            const tokenBuf = Buffer.from(decision.approvalToken, 'hex');
            const expBuf = Buffer.from(expectedToken, 'hex');
            if (tokenBuf.length !== expBuf.length || !crypto.timingSafeEqual(tokenBuf, expBuf)) {
                throw new Error('Invalid approval token. Authorization denied.');
            }
        }
        else if (!isDelegateApproval) {
            throw new Error(`Agent '${decision.resolvedBy}' is not authorized to resolve approval requests. ` +
                `Authorized delegates: ${this.config.delegateApprovers.join(', ') || 'none'}. ` +
                'Human approvals must use format "human:<identity>" with a valid approvalToken.');
        }
        request.status = decision.status;
        request.resolvedBy = decision.resolvedBy;
        request.resolvedAt = new Date().toISOString();
        request.note = decision.note;
        this.saveToDisk();
        return request;
    }
    /**
     * Check the status of a request. Handles timeout/escalation.
     */
    check(requestId) {
        const request = this.queue.get(requestId);
        if (!request)
            return null;
        // Check expiry
        if (request.status === 'pending') {
            const now = Date.now();
            const expiresAt = new Date(request.expiresAt).getTime();
            if (now >= expiresAt) {
                if (this.config.timeoutAction === 'escalate' && this.config.escalationTarget) {
                    request.status = 'escalated';
                    request.note = `Escalated to ${this.config.escalationTarget} after ${this.config.timeoutMs / 1000}s timeout`;
                }
                else {
                    request.status = 'expired';
                    request.note = `Auto-denied after ${this.config.timeoutMs / 1000}s timeout`;
                }
                request.resolvedAt = new Date().toISOString();
                request.resolvedBy = 'system:timeout';
                this.saveToDisk();
            }
        }
        return { ...request };
    }
    /**
     * Get all pending requests.
     */
    getPending() {
        this.pruneExpired();
        return Array.from(this.queue.values())
            .filter(r => r.status === 'pending')
            .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    }
    /**
     * Get all requests for a specific agent.
     */
    getByAgent(agent) {
        return Array.from(this.queue.values())
            .filter(r => r.agent === agent)
            .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    }
    /**
     * Get all escalated requests that need attention.
     */
    getEscalated() {
        this.pruneExpired();
        return Array.from(this.queue.values())
            .filter(r => r.status === 'escalated');
    }
    /**
     * Get queue statistics.
     */
    getStats() {
        const statuses = Array.from(this.queue.values()).map(r => r.status);
        return {
            total: statuses.length,
            pending: statuses.filter(s => s === 'pending').length,
            approved: statuses.filter(s => s === 'approved').length,
            denied: statuses.filter(s => s === 'denied').length,
            expired: statuses.filter(s => s === 'expired').length,
            escalated: statuses.filter(s => s === 'escalated').length,
        };
    }
    /**
     * Clear resolved requests older than the given age in ms.
     */
    cleanup(maxAgeMs = 86400000) {
        let removed = 0;
        const cutoff = Date.now() - maxAgeMs;
        for (const [id, request] of this.queue) {
            if (request.status !== 'pending' && request.resolvedAt) {
                if (new Date(request.resolvedAt).getTime() < cutoff) {
                    this.queue.delete(id);
                    removed++;
                }
            }
        }
        if (removed > 0)
            this.saveToDisk();
        return removed;
    }
    // --- Private helpers ---
    pruneExpired() {
        const now = Date.now();
        for (const [, request] of this.queue) {
            if (request.status === 'pending') {
                const expiresAt = new Date(request.expiresAt).getTime();
                if (now >= expiresAt) {
                    if (this.config.timeoutAction === 'escalate' && this.config.escalationTarget) {
                        request.status = 'escalated';
                        request.note = `Escalated to ${this.config.escalationTarget} after timeout`;
                    }
                    else {
                        request.status = 'expired';
                        request.note = `Auto-denied after timeout`;
                    }
                    request.resolvedAt = new Date().toISOString();
                    request.resolvedBy = 'system:timeout';
                }
            }
        }
        this.saveToDisk();
    }
    saveToDisk() {
        try {
            const data = Array.from(this.queue.values());
            fs.writeFileSync(this.queueFile, JSON.stringify(data, null, 2));
        }
        catch (err) {
            process.stderr.write(`[APPROVAL QUEUE CRITICAL] Failed to persist queue: ${err.message}\n`);
        }
    }
    loadFromDisk() {
        if (!fs.existsSync(this.queueFile))
            return;
        try {
            const data = JSON.parse(fs.readFileSync(this.queueFile, 'utf-8'));
            for (const request of data) {
                this.queue.set(request.id, request);
            }
        }
        catch (err) {
            process.stderr.write(`[APPROVAL QUEUE] Failed to load persisted queue: ${err.message}\n`);
        }
    }
}
//# sourceMappingURL=approval-queue.js.map