"use strict";
/**
 * HTTPS Tunnel Proxy
 *
 * Local HTTP server that tunnels requests to a remote HTTPS CLIProxyAPI.
 * Required because Claude Code (via undici/node-fetch) doesn't support
 * HTTPS in ANTHROPIC_BASE_URL directly.
 *
 * Flow:
 *   Claude CLI --HTTP--> Local Tunnel (port X) --HTTPS--> Remote CLIProxyAPI
 *
 * Note: Unlike CodexReasoningProxy, this tunnel does NOT buffer or limit response sizes.
 * Responses are streamed directly (pipe) which is appropriate for a transparent tunnel.
 * Socket-level timeouts handle hung connections; size limits are enforced by the remote server.
 */
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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpsTunnelProxy = void 0;
const http = __importStar(require("http"));
const https = __importStar(require("https"));
class HttpsTunnelProxy {
    constructor(config) {
        this.server = null;
        this.port = null;
        this.startingPromise = null;
        this.activeConnections = new Set();
        // Validate hostname format (basic check for common issues)
        if (!config.remoteHost || !/^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$/.test(config.remoteHost)) {
            if (config.remoteHost &&
                config.remoteHost.length === 1 &&
                /^[a-zA-Z0-9]$/.test(config.remoteHost)) {
                // Single character hostname is valid
            }
            else {
                throw new Error(`Invalid remoteHost format: "${config.remoteHost}". ` +
                    'Expected hostname without protocol (e.g., "api.example.com")');
            }
        }
        this.config = {
            remoteHost: config.remoteHost,
            remotePort: config.remotePort ?? 443,
            timeoutMs: config.timeoutMs ?? 120000,
            verbose: config.verbose ?? false,
            allowSelfSigned: config.allowSelfSigned ?? false,
            authToken: config.authToken,
        };
    }
    log(message) {
        if (this.config.verbose) {
            console.error(`[https-tunnel] ${message}`);
        }
    }
    async start() {
        // Already started
        if (this.server)
            return this.port ?? 0;
        // Prevent race condition: if start() is already in progress, return the same promise
        if (this.startingPromise)
            return this.startingPromise;
        this.startingPromise = new Promise((resolve, reject) => {
            this.server = http.createServer((req, res) => {
                void this.handleRequest(req, res);
            });
            // Track connections for proper cleanup
            this.server.on('connection', (socket) => {
                this.activeConnections.add(socket);
                socket.on('close', () => this.activeConnections.delete(socket));
            });
            this.server.listen(0, '127.0.0.1', () => {
                const address = this.server?.address();
                this.port = typeof address === 'object' && address ? address.port : 0;
                if (this.port === 0) {
                    this.startingPromise = null;
                    reject(new Error('Failed to bind to any port'));
                    return;
                }
                this.log(`Started on port ${this.port}, tunneling to https://${this.config.remoteHost}:${this.config.remotePort}`);
                resolve(this.port);
            });
            this.server.on('error', (err) => {
                this.startingPromise = null;
                reject(err);
            });
        });
        return this.startingPromise;
    }
    stop() {
        if (!this.server)
            return;
        // Forcefully close all active connections
        for (const socket of this.activeConnections) {
            socket.destroy();
        }
        this.activeConnections.clear();
        this.server.close();
        this.server = null;
        this.port = null;
        this.startingPromise = null;
        this.log('Stopped');
    }
    getPort() {
        return this.port;
    }
    buildForwardHeaders(originalHeaders) {
        const headers = {};
        // RFC 7230 hop-by-hop headers that should not be forwarded
        const hopByHop = new Set([
            'host',
            'connection',
            'transfer-encoding',
            'keep-alive',
            'proxy-authenticate',
            'proxy-authorization',
            'te',
            'trailer',
            'upgrade',
        ]);
        for (const [key, value] of Object.entries(originalHeaders)) {
            if (!value)
                continue;
            const lower = key.toLowerCase();
            if (hopByHop.has(lower))
                continue;
            headers[key] = value;
        }
        // Set correct host header for remote (include port when non-default)
        if (this.config.remotePort === 443) {
            headers['Host'] = this.config.remoteHost;
        }
        else {
            headers['Host'] = `${this.config.remoteHost}:${this.config.remotePort}`;
        }
        // Inject Authorization header if not present but authToken is configured
        // This is a fallback - normally the client (CodexReasoningProxy) forwards the header
        if (!headers['authorization'] && !headers['Authorization'] && this.config.authToken) {
            headers['Authorization'] = `Bearer ${this.config.authToken}`;
        }
        return headers;
    }
    async handleRequest(req, res) {
        const method = req.method || 'GET';
        const requestPath = req.url || '/';
        this.log(`${method} ${requestPath} → https://${this.config.remoteHost}:${this.config.remotePort}${requestPath}`);
        try {
            await this.forwardRequest(req, res, requestPath);
        }
        catch (error) {
            const err = error;
            this.log(`Error: ${err.message}`);
            if (!res.headersSent) {
                res.writeHead(502, { 'Content-Type': 'application/json' });
            }
            // Sanitize error message: show details only in verbose mode (localhost-only anyway)
            const errorMessage = this.config.verbose ? err.message : 'Upstream request failed';
            res.end(JSON.stringify({ error: errorMessage }));
        }
    }
    forwardRequest(originalReq, clientRes, requestPath) {
        return new Promise((resolve, reject) => {
            const headers = this.buildForwardHeaders(originalReq.headers);
            const options = {
                hostname: this.config.remoteHost,
                port: this.config.remotePort,
                path: requestPath,
                method: originalReq.method,
                timeout: this.config.timeoutMs,
                headers,
                // Allow self-signed certificates if configured
                rejectUnauthorized: !this.config.allowSelfSigned,
            };
            const upstreamReq = https.request(options, (upstreamRes) => {
                // Forward status and headers
                clientRes.writeHead(upstreamRes.statusCode || 200, upstreamRes.headers);
                // Pipe response body
                upstreamRes.pipe(clientRes);
                upstreamRes.on('end', () => resolve());
                upstreamRes.on('error', reject);
            });
            upstreamReq.on('timeout', () => {
                const timeoutError = new Error('Upstream request timeout');
                this.log(`Timeout: ${timeoutError.message}`);
                upstreamReq.destroy();
                reject(timeoutError);
            });
            upstreamReq.on('error', (err) => {
                this.log(`Upstream error: ${err.message}`);
                reject(err);
            });
            // Handle client disconnect (premature close)
            originalReq.on('error', (err) => {
                this.log(`Client request error: ${err.message}`);
                upstreamReq.destroy();
                reject(err);
            });
            originalReq.on('close', () => {
                if (!originalReq.complete) {
                    this.log('Client disconnected prematurely');
                    upstreamReq.destroy();
                }
            });
            // Pipe request body to upstream
            originalReq.pipe(upstreamReq);
        });
    }
}
exports.HttpsTunnelProxy = HttpsTunnelProxy;
//# sourceMappingURL=https-tunnel-proxy.js.map