"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const approval_manager_1 = require("../approval-manager");
const auth_1 = require("../auth");
const config_1 = require("../config");
function canDecideApproval(projectId, username) {
    if ((0, config_1.isAdminUser)(username))
        return true;
    const project = (0, config_1.getProject)(projectId);
    if (!project)
        return false;
    if ((0, config_1.isProjectOwner)(project, username))
        return true;
    const share = project.shares?.find((s) => s.username === username);
    return share?.permission === 'edit';
}
const router = (0, express_1.Router)();
// Hook-facing timeout. User explicitly asked for "no time limit" on approvals.
// 24h is effectively "forever" for practical purposes; prevents memory leak if
// hook client disappears silently without triggering res.on('close').
// Chain (all 24h): settings.json hook timeout > hook script HTTP timeout > backend.
// Practical ceiling: OS TCP keepalive (~2h) may close the socket if idle.
const HOOK_TIMEOUT_MS = 24 * 60 * 60 * 1000;
/** Only accept hook requests from localhost loopback. */
function isLoopback(req) {
    const addr = req.socket.remoteAddress || '';
    return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
}
function badSignature(req, payload) {
    const sig = (req.header('x-ccweb-signature') || '').trim();
    if (!sig)
        return true;
    return !approval_manager_1.approvalManager.verify(payload, sig);
}
/**
 * POST /api/hooks/approval-request
 * Called by ccweb-approval-hook.js. Blocks until the user decides or timeout.
 * Auth: loopback + HMAC signature of the raw JSON body.
 */
router.post('/hooks/approval-request', async (req, res) => {
    if (!isLoopback(req)) {
        res.status(403).json({ error: 'loopback only' });
        return;
    }
    // Require exact raw bytes — never fall back to JSON.stringify (would silently mismatch HMAC).
    const rawBuf = req.rawBody;
    if (!rawBuf) {
        res.status(400).json({ error: 'raw body missing' });
        return;
    }
    const raw = rawBuf.toString('utf-8');
    if (badSignature(req, raw)) {
        res.status(401).json({ error: 'bad signature' });
        return;
    }
    const { projectId, toolUseId, toolName, toolInput, sessionId } = (req.body ?? {});
    if (!projectId || !toolUseId || !toolName) {
        res.status(400).json({ error: 'projectId, toolUseId, toolName required' });
        return;
    }
    // If the hook client drops the TCP connection before we resolve, cancel the
    // pending entry so it doesn't linger in the UI until the 110s timeout.
    // NOTE: use `res.on('close')` NOT `req.on('close')` — the latter fires as soon
    // as the request body is fully received (Node >=16 auto-destroys IncomingMessage
    // after 'end'), which for our tiny POST happens before `register()` even resolves,
    // causing a spurious cancel on every request. `res.on('close')` only fires when
    // the underlying connection closes or after `res.end()`.
    let responded = false;
    res.on('close', () => {
        if (!responded)
            approval_manager_1.approvalManager.cancel(projectId, toolUseId, 'client disconnected');
    });
    const decision = await approval_manager_1.approvalManager.register({ projectId, toolUseId, toolName, toolInput, sessionId: sessionId ?? '', createdAt: Date.now() }, HOOK_TIMEOUT_MS);
    responded = true;
    if (res.writableEnded || res.destroyed)
        return;
    res.json(decision);
});
/**
 * POST /api/approval/:requestId/decide
 * Called by the frontend when user clicks Allow/Deny.
 * Auth: JWT (applied at router mount level).
 * :requestId format is `projectId:toolUseId`.
 */
router.post('/approval/:projectId/:toolUseId/decide', auth_1.authMiddleware, (req, res) => {
    const { projectId, toolUseId } = req.params;
    if (!canDecideApproval(projectId, req.user?.username)) {
        res.status(403).json({ error: 'view-only collaborators cannot decide approvals' });
        return;
    }
    const { behavior, message } = (req.body ?? {});
    if (behavior !== 'allow' && behavior !== 'deny') {
        res.status(400).json({ error: 'behavior must be allow or deny' });
        return;
    }
    const ok = approval_manager_1.approvalManager.decide(projectId, toolUseId, { behavior, message });
    if (!ok) {
        res.status(404).json({ error: 'request not found or already resolved' });
        return;
    }
    res.json({ ok: true });
});
/**
 * GET /api/approval/:projectId/pending
 * Returns pending approvals for a project (for overlay reconnect).
 */
router.get('/approval/:projectId/pending', auth_1.authMiddleware, (req, res) => {
    const { projectId } = req.params;
    if (!canDecideApproval(projectId, req.user?.username)) {
        // view-only users shouldn't see tool inputs either
        res.json({ pending: [] });
        return;
    }
    res.json({ pending: approval_manager_1.approvalManager.listPending(projectId) });
});
exports.default = router;
//# sourceMappingURL=approval.js.map