"use strict";
/**
 * CLIProxy Local Reverse Proxy
 *
 * Proxies requests from the dashboard to the local CLIProxy service
 * running on 127.0.0.1 inside the same host/container.
 *
 * Mounted at: /api/cliproxy-local/*  ->  http://127.0.0.1:{port}/*
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCliproxyLocalProxyRouter = void 0;
const http_1 = __importDefault(require("http"));
const express_1 = require("express");
const port_manager_1 = require("../../cliproxy/config/port-manager");
const auth_middleware_1 = require("../middleware/auth-middleware");
/** Proxy request timeout in milliseconds (30 seconds) */
const PROXY_TIMEOUT_MS = 30000;
function isJsonContentType(contentType) {
    const values = Array.isArray(contentType) ? contentType : [contentType];
    return values.some((value) => value?.toLowerCase().includes('application/json') === true);
}
function buildProxyBody(req) {
    // If express.json() parsed the body (content-type is JSON and req.body is populated),
    // re-serialize it since the original request stream was consumed by the middleware.
    if (!isJsonContentType(req.headers['content-type']) ||
        req.body === undefined ||
        req.body === null) {
        return undefined;
    }
    // express.json() sets req.body to the parsed value — re-serialize for the proxy target
    return Buffer.from(JSON.stringify(req.body));
}
function buildProxyHeaders(headers, port, bodyBuffer) {
    const proxyHeaders = {
        ...headers,
        host: `127.0.0.1:${port}`,
    };
    delete proxyHeaders.connection;
    if (bodyBuffer) {
        delete proxyHeaders['transfer-encoding'];
        proxyHeaders['content-length'] = String(bodyBuffer.length);
    }
    return proxyHeaders;
}
function createCliproxyLocalProxyRouter(deps = {}) {
    const router = (0, express_1.Router)();
    const enforceAccess = deps.enforceAccess ??
        ((req, res) => (0, auth_middleware_1.requireLocalAccessWhenAuthDisabled)(req, res, 'CLIProxy local proxy requires localhost access when dashboard auth is disabled.'));
    const createRequest = deps.request ?? http_1.default.request;
    const resolveTargetPort = deps.resolveTargetPort ?? port_manager_1.resolveLifecyclePort;
    router.use((req, res, next) => {
        if (enforceAccess(req, res)) {
            next();
        }
    });
    router.all('/*', (req, res) => {
        const targetPort = resolveTargetPort();
        const targetPath = req.url || '/';
        const bodyBuffer = buildProxyBody(req);
        const proxyReq = createRequest({
            hostname: '127.0.0.1',
            port: targetPort,
            path: targetPath,
            method: req.method,
            headers: buildProxyHeaders(req.headers, targetPort, bodyBuffer),
            timeout: PROXY_TIMEOUT_MS,
        }, (proxyRes) => {
            const proxyStatus = proxyRes.statusCode ?? 502;
            const proxyContentLength = proxyRes.headers['content-length'];
            const hasEmptyBody = typeof proxyContentLength === 'string' && Number.parseInt(proxyContentLength, 10) === 0;
            const isSyntheticUnreachableResponse = proxyStatus === 502 &&
                proxyRes.headers['content-type'] === undefined &&
                proxyRes.headers['proxy-connection'] !== undefined &&
                hasEmptyBody;
            if (isSyntheticUnreachableResponse) {
                const payload = JSON.stringify({ error: 'CLIProxy is not reachable' });
                res.writeHead(502, {
                    'Content-Type': 'application/json; charset=utf-8',
                    'Content-Length': String(Buffer.byteLength(payload)),
                });
                res.end(payload);
                return;
            }
            res.writeHead(proxyStatus, proxyRes.headers);
            // Manual streaming instead of pipe() for Bun runtime compatibility
            proxyRes.on('data', (chunk) => res.write(chunk));
            proxyRes.on('end', () => res.end());
        });
        proxyReq.on('timeout', () => proxyReq.destroy());
        proxyReq.on('error', () => {
            if (!res.headersSent) {
                const payload = JSON.stringify({ error: 'CLIProxy is not reachable' });
                res.statusCode = 502;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.setHeader('Content-Length', Buffer.byteLength(payload));
                res.end(payload);
            }
        });
        // Clean up proxy connection only when the client aborts the request.
        // Avoid res.on('close') here because Bun may emit it during local error
        // responses before the JSON body is flushed, which can truncate 502 payloads.
        req.on('aborted', () => {
            if (!res.writableEnded) {
                proxyReq.destroy();
            }
        });
        if (bodyBuffer) {
            proxyReq.end(bodyBuffer);
            return;
        }
        // For methods without a body (GET, HEAD, etc.) or when express.json()
        // has already consumed the stream, end the request immediately.
        const hasBody = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS';
        if (!hasBody) {
            proxyReq.end();
            return;
        }
        req.pipe(proxyReq, { end: true });
    });
    return router;
}
exports.createCliproxyLocalProxyRouter = createCliproxyLocalProxyRouter;
exports.default = createCliproxyLocalProxyRouter();
//# sourceMappingURL=cliproxy-local-proxy.js.map