"use strict";
/**
 * ApprovalManager — in-memory Claude Code PermissionRequest approval flow.
 *
 * Hook script (bin/ccweb-approval-hook.js) holds an HTTP POST open until the
 * user decides in the web UI; this manager bridges that POST to a WS event
 * and back to a Promise resolution.
 *
 * Security:
 *  - Shared HMAC secret at ~/.ccweb/approval-secret (mode 0600) verifies hook ↔ backend calls.
 *  - Hook HTTP is 127.0.0.1-only (enforced at route level) so LAN clients can't forge requests.
 *  - Frontend decide() goes through JWT-auth'd route.
 *
 * Claude-only on purpose (not a missed adapter abstraction):
 *   - Codex DOES have an approval system (`approval_policy = on-request`),
 *     but it is purely internal to the TUI — Codex does NOT emit external
 *     webhooks for pre-tool decisions, so ccweb cannot intercept. The only
 *     adapter surfaces are (a) MCP server wrapping or (b) the undocumented
 *     `--remote ws://...` TUI protocol, both of which are an order of
 *     magnitude more engineering than this module. Keeping this Claude-only
 *     is a cost decision, not "Codex lacks approval".
 *   - Gemini / OpenCode / Qwen likewise have no compatible webhook surface.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.approvalManager = void 0;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const logger_1 = require("./logger");
const log = (0, logger_1.modLogger)('approval');
const SECRET_FILE = path.join(os.homedir(), '.ccweb', 'approval-secret');
function loadOrCreateSecret() {
    if (fs.existsSync(SECRET_FILE)) {
        try {
            const existing = fs.readFileSync(SECRET_FILE, 'utf-8').trim();
            if (existing) {
                // Verify file is still 0600. An earlier ccweb version (or a user
                // copy/restore) may have left it world-readable; tighten in place
                // rather than silently trusting it.
                try {
                    const st = fs.statSync(SECRET_FILE);
                    const tooOpen = (st.mode & 0o077) !== 0;
                    if (tooOpen) {
                        try {
                            fs.chmodSync(SECRET_FILE, 0o600);
                            log.warn({ file: SECRET_FILE, oldMode: (st.mode & 0o777).toString(8) }, 'approval-secret was group/other-readable; restored to 0600');
                        }
                        catch (err) {
                            log.error({ err, file: SECRET_FILE, mode: (st.mode & 0o777).toString(8) }, 'approval-secret has unsafe perms and chmod failed — fix manually before relying on this secret');
                        }
                    }
                }
                catch { /* stat fail — leave as is */ }
                return existing;
            }
        }
        catch (err) {
            log.error({ err }, 'approval-secret exists but unreadable — refusing to regenerate (would break active hooks); fix permissions');
            throw err;
        }
    }
    const secret = crypto.randomBytes(32).toString('hex');
    const dir = path.dirname(SECRET_FILE);
    if (!fs.existsSync(dir))
        fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
    fs.writeFileSync(SECRET_FILE, secret, { encoding: 'utf-8', mode: 0o600 });
    return secret;
}
class ApprovalManager {
    constructor() {
        this.pending = new Map();
        this.listeners = new Set();
        this.secret = loadOrCreateSecret();
    }
    getSecretFile() { return SECRET_FILE; }
    sign(data) {
        return crypto.createHmac('sha256', this.secret).update(data).digest('hex');
    }
    verify(data, signature) {
        const expected = this.sign(data);
        const a = Buffer.from(expected, 'hex');
        const b = Buffer.from((signature || '').trim(), 'hex');
        if (a.length === 0 || a.length !== b.length)
            return false;
        return crypto.timingSafeEqual(a, b);
    }
    keyOf(projectId, toolUseId) {
        return `${projectId}:${toolUseId}`;
    }
    /** Register a new pending approval. Resolves with the decision (or timeout deny). */
    register(req, timeoutMs) {
        const key = this.keyOf(req.projectId, req.toolUseId);
        if (this.pending.has(key)) {
            return Promise.resolve({ behavior: 'deny', message: 'Duplicate request' });
        }
        return new Promise((resolve) => {
            const timer = setTimeout(() => {
                const entry = this.pending.get(key);
                if (!entry)
                    return;
                this.pending.delete(key);
                this.emit({ type: 'approval_resolved', projectId: req.projectId, toolUseId: req.toolUseId, behavior: 'deny', reason: 'timeout' });
                resolve({ behavior: 'deny', message: 'Approval timeout' });
            }, timeoutMs);
            this.pending.set(key, { ...req, resolve, timer });
            this.emit({ type: 'approval_request', ...req });
        });
    }
    /** Resolve a pending request with a decision. Returns true if it existed. */
    decide(projectId, toolUseId, decision) {
        const key = this.keyOf(projectId, toolUseId);
        const entry = this.pending.get(key);
        if (!entry)
            return false;
        clearTimeout(entry.timer);
        this.pending.delete(key);
        entry.resolve(decision);
        this.emit({ type: 'approval_resolved', projectId, toolUseId, behavior: decision.behavior });
        return true;
    }
    /** Cancel a pending request (e.g. hook client disconnected). Resolves with a deny internally; no WS resolved event. */
    cancel(projectId, toolUseId, reason) {
        const key = this.keyOf(projectId, toolUseId);
        const entry = this.pending.get(key);
        if (!entry)
            return false;
        clearTimeout(entry.timer);
        this.pending.delete(key);
        entry.resolve({ behavior: 'deny', message: reason });
        this.emit({ type: 'approval_resolved', projectId, toolUseId, behavior: 'deny', reason });
        return true;
    }
    /** List all pending approvals for a project (for UI reconnect). */
    listPending(projectId) {
        const out = [];
        for (const entry of this.pending.values()) {
            if (entry.projectId === projectId) {
                const { resolve: _r, timer: _t, ...info } = entry;
                out.push(info);
            }
        }
        return out;
    }
    subscribe(fn) {
        this.listeners.add(fn);
        return () => this.listeners.delete(fn);
    }
    emit(evt) {
        for (const fn of this.listeners) {
            try {
                fn(evt);
            }
            catch (err) {
                log.warn({ err }, 'approval listener error');
            }
        }
    }
}
exports.approvalManager = new ApprovalManager();
//# sourceMappingURL=approval-manager.js.map