"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BunRequest = exports.Request = exports.isBun = void 0;
const node_events_1 = require("node:events");
const node_stream_1 = require("node:stream");
const Headers_ts_1 = require("./Headers.js");
exports.isBun = typeof globalThis.Bun !== 'undefined';
/**
 * We define our own request class, to ensure that it has integrity against leaks in a secure environment
 * and for better conformance to WHATWG standards.
 */
class Request {
    #body;
    #peerCertificate;
    #abortController = new AbortController();
    _nodeRequest;
    _nodeResponse;
    method;
    url;
    headers;
    requestId;
    isOperationsServer;
    handlerPath;
    __harperdbRequestUpgraded;
    __harperRequestUpgraded;
    createdResource;
    newLocation;
    isWebSocket;
    user; // User object can be attached during authentication
    response;
    responseHeaders;
    expiresAt;
    onlyIfCached;
    noCache;
    noCacheStore;
    staleIfError;
    mustRevalidate;
    replicatedConfirmation;
    replicateTo;
    replicateFrom;
    data;
    authorize;
    lastModified;
    lastRefreshed;
    constructor(nodeRequest, nodeResponse) {
        this.method = nodeRequest.method;
        const url = nodeRequest.url;
        this._nodeRequest = nodeRequest;
        this._nodeResponse = nodeResponse;
        this.url = url;
        this.headers = new RequestHeaders(nodeRequest.headers);
        this.__harperRequestUpgraded = false;
        // Abort the request's signal on premature client disconnect. nodeResponse 'close'
        // also fires on clean completion; the writableFinished guard restricts to disconnect.
        if (typeof nodeResponse?.on === 'function') {
            nodeResponse.on('close', () => {
                if (!nodeResponse.writableFinished)
                    this.#abortController.abort();
            });
        }
        else if (typeof nodeRequest.socket?.once === 'function') {
            // No response on this Request — typically the WebSocket-upgrade path
            // (http.ts creates the Request before the ws library takes over). The TCP
            // socket close is the fallback abort trigger; REST.ts's ws.on('close') hook
            // also calls _abort() and is the primary signal in the WS case. The two
            // are redundant by design (idempotent abort) so any future single-arg
            // caller still gets disconnect semantics without relying on the WS layer.
            nodeRequest.socket.once('close', () => this.#abortController.abort());
        }
    }
    get signal() {
        return this.#abortController.signal;
    }
    /**
     * Abort this request's signal. Used by transports (e.g. WebSocket) that need to
     * signal client-side cancellation independently of the Node response lifecycle.
     */
    _abort() {
        this.#abortController.abort();
    }
    get absoluteURL() {
        return this.protocol + '://' + this.host + this.url;
    }
    get pathname() {
        const queryStart = this.url.indexOf('?');
        if (queryStart > -1)
            return this.url.slice(0, queryStart);
        return this.url;
    }
    set pathname(pathname) {
        const queryStart = this.url.indexOf('?');
        if (queryStart > -1)
            this.url = pathname + this.url.slice(queryStart);
        else
            this.url = pathname;
    }
    get protocol() {
        return this._nodeRequest.socket.encrypted ? 'https' : 'http';
    }
    get ip() {
        return this._nodeRequest.socket.remoteAddress;
    }
    get authorized() {
        return this._nodeRequest.socket.authorized;
    }
    get peerCertificate() {
        // Cache the certificate to avoid repeated parsing overhead
        // getPeerCertificate() calls translatePeerCertificate which parses
        // the raw certificate data each time (via handle.getPeerCertificate() -> SSL_get_peer_certificate)
        // This issue persists in Node.js v24 - https://github.com/nodejs/node/blob/v24.x/lib/_tls_wrap.js#L1117
        if (this.#peerCertificate === undefined) {
            // Pass true to include the full certificate chain with issuerCertificate properties
            // This is required for OCSP verification which needs both the peer cert and its issuer
            this.#peerCertificate = this._nodeRequest.socket.getPeerCertificate?.(true) || null;
        }
        return this.#peerCertificate;
    }
    get mtlsConfig() {
        return this._nodeRequest.socket.server.mtlsConfig;
    }
    get body() {
        return this.#body || (this.#body = new RequestBody(this._nodeRequest));
    }
    get host() {
        return this._nodeRequest.authority || this._nodeRequest.headers.host;
    }
    get hostname() {
        return this._nodeRequest.headers.host;
    }
    get httpVersion() {
        return this._nodeRequest.httpVersion;
    }
    get isAborted() {
        return this.#abortController.signal.aborted;
    }
    // Expose node request for cases that need direct access (e.g., replication)
    get nodeRequest() {
        return this._nodeRequest;
    }
    /**
     * Invokes `handler` with a Node.js IncomingMessage/ServerResponse pair that adapts this Request
     * into the Node HTTP API, and returns a Promise that resolves to a Harper Response once the handler
     * writes its status and headers. Useful for integrating third-party Node middleware that expects
     * the native node http request/response objects.
     *
     * The IncomingMessage mirrors the current Request state (method, url, headers may have been modified
     * by middleware) while delegating body reading to the underlying stream. The ServerResponse captures
     * status, headers, and body, resolving the returned promise as soon as headers are available with a
     * streaming body that can be piped back through the Harper middleware chain.
     *
     * **Important:** The resolved `body` PassThrough must have an `error` listener attached (or be piped
     * to a destination that handles errors) before it is consumed. If the underlying connection is reset
     * after headers are sent, the body stream is destroyed with an error — without a listener, Node.js
     * will throw an uncaught exception.
     *
     * Example:
     *   server.http((request, next) =>
     *     request.withNodeAdapter((req, res) => someNodeMiddleware(req, res))
     *   );
     */
    withNodeAdapter(handler) {
        // Flat headers object matching IncomingMessage.headers format (lowercase keys)
        const reqHeaders = Object.create(null);
        for (const [key, value] of this.headers) {
            reqHeaders[key.toLowerCase()] = value;
        }
        // Proxy the underlying IncomingMessage so body streaming works, but expose
        // the current Request's (possibly middleware-mutated) method/url/headers.
        const self = this;
        const nodeReq = new Proxy(this._nodeRequest, {
            get(target, prop, receiver) {
                if (prop === 'method')
                    return self.method;
                if (prop === 'url')
                    return self.url;
                if (prop === 'headers')
                    return reqHeaders;
                return Reflect.get(target, prop, receiver);
            },
        });
        let resolveResponse;
        let rejectResponse;
        const response = new Promise((resolve, reject) => {
            resolveResponse = resolve;
            rejectResponse = reject;
        });
        const responseBody = new node_stream_1.PassThrough();
        const capturedHeaders = new Headers_ts_1.Headers();
        let headersFlushed = false;
        let nodeRes;
        const flushHeaders = () => {
            if (!headersFlushed) {
                headersFlushed = true;
                nodeRes.headersSent = true;
                resolveResponse({ status: nodeRes.statusCode, headers: capturedHeaders, body: responseBody });
            }
        };
        const applyHeaders = (hdrs) => {
            if (Array.isArray(hdrs)) {
                if (hdrs.length > 0 && Array.isArray(hdrs[0])) {
                    for (const [name, value] of hdrs)
                        capturedHeaders.set(name, value);
                }
                else {
                    for (let i = 0; i < hdrs.length; i += 2)
                        capturedHeaders.set(hdrs[i], hdrs[i + 1]);
                }
            }
            else {
                for (const [k, v] of Object.entries(hdrs))
                    capturedHeaders.set(k, v);
            }
        };
        nodeRes = Object.assign(new node_events_1.EventEmitter(), {
            statusCode: 200,
            statusMessage: '',
            headersSent: false,
            writable: true,
            writableEnded: false,
            writableFinished: false,
            socket: this._nodeRequest.socket,
            setHeader(name, value) {
                if (Array.isArray(value)) {
                    // Use set() for first value (overwrites any existing entry) then append() for
                    // the rest so multiple values — critical for Set-Cookie — are preserved as an
                    // array rather than collapsed into a single comma-joined string.
                    if (value.length > 0) {
                        capturedHeaders.set(name, String(value[0]));
                        for (let i = 1; i < value.length; i++)
                            capturedHeaders.append(name, String(value[i]));
                    }
                }
                else {
                    capturedHeaders.set(name, String(value));
                }
                return nodeRes;
            },
            getHeader(name) {
                return capturedHeaders.get(name);
            },
            getHeaders() {
                return Object.fromEntries(capturedHeaders);
            },
            hasHeader(name) {
                return capturedHeaders.has(name);
            },
            removeHeader(name) {
                capturedHeaders.delete(name);
            },
            flushHeaders() {
                flushHeaders();
            },
            writeHead(statusCode, statusMessageOrHeaders, maybeHeaders) {
                if (headersFlushed)
                    return nodeRes;
                nodeRes.statusCode = statusCode;
                const hdrs = typeof statusMessageOrHeaders === 'string' ? maybeHeaders : statusMessageOrHeaders;
                if (hdrs)
                    applyHeaders(hdrs);
                flushHeaders();
                return nodeRes;
            },
            write(chunk, encoding, callback) {
                flushHeaders();
                if (typeof encoding === 'function')
                    return responseBody.write(chunk, encoding);
                return responseBody.write(chunk, encoding, callback);
            },
            end(chunk, encoding, callback) {
                flushHeaders();
                nodeRes.writableEnded = true;
                if (typeof chunk === 'function')
                    responseBody.end(chunk);
                else if (typeof encoding === 'function')
                    responseBody.end(chunk, encoding);
                else
                    responseBody.end(chunk, encoding, callback);
                return nodeRes;
            },
            destroy(error) {
                if (!headersFlushed) {
                    if (error)
                        rejectResponse(error);
                    else
                        rejectResponse(new Error('Response destroyed before headers were sent'));
                    // The error has been forwarded to the response promise; suppress the
                    // PassThrough 'error' event for this one destroy call so Node doesn't
                    // throw due to having no other listeners.
                    responseBody.once('error', () => { });
                }
                responseBody.destroy(error);
                return nodeRes;
            },
        });
        responseBody.on('finish', () => {
            nodeRes.writableFinished = true;
            nodeRes.emit('finish');
        });
        responseBody.on('drain', () => {
            nodeRes.emit('drain');
        });
        responseBody.on('close', () => {
            nodeRes.emit('close');
        });
        const handlerResult = handler(nodeReq, nodeRes);
        if (handlerResult != null && typeof handlerResult.then === 'function') {
            handlerResult.catch((err) => {
                if (!headersFlushed)
                    rejectResponse(err);
            });
        }
        return response;
    }
    sendEarlyHints(link, headers = {}) {
        headers.link = link;
        this._nodeResponse.writeEarlyHints(headers);
    }
}
exports.Request = Request;
/**
 * Bun-compatible Request adapter. Wraps a Web Fetch API Request (from Bun.serve's fetch handler)
 * to present the same interface as the Node.js-based Request class.
 */
class BunRequest {
    #body;
    #ip;
    #isSecure;
    _webRequest;
    _bunServer;
    // Provide _nodeRequest as null for code that checks it; _nodeResponse is not applicable
    _nodeRequest = null;
    _nodeResponse = null;
    method;
    url;
    headers;
    isWebSocket;
    user;
    response;
    __harperRequestUpgraded;
    constructor(webRequest, bunServer, isSecure) {
        this._webRequest = webRequest;
        this._bunServer = bunServer;
        this.#isSecure = isSecure;
        this.method = webRequest.method;
        // Web Request.url is a full URL; extract the path + query to match Node's IncomingMessage.url
        const fullUrl = webRequest.url;
        const protocolEnd = fullUrl.indexOf('//');
        const pathStart = fullUrl.indexOf('/', protocolEnd + 2);
        this.url = pathStart >= 0 ? fullUrl.slice(pathStart) : '/';
        // Convert Web Headers to plain object for our RequestHeaders wrapper
        const headersObj = {};
        webRequest.headers.forEach((value, key) => {
            headersObj[key] = value;
        });
        this.headers = new RequestHeaders(headersObj);
        this.__harperRequestUpgraded = false;
    }
    get absoluteURL() {
        return this._webRequest.url;
    }
    get pathname() {
        const queryStart = this.url.indexOf('?');
        if (queryStart > -1)
            return this.url.slice(0, queryStart);
        return this.url;
    }
    set pathname(pathname) {
        const queryStart = this.url.indexOf('?');
        if (queryStart > -1)
            this.url = pathname + this.url.slice(queryStart);
        else
            this.url = pathname;
    }
    get protocol() {
        return this.#isSecure ? 'https' : 'http';
    }
    get ip() {
        if (this.#ip === undefined) {
            const addr = this._bunServer?.requestIP?.(this._webRequest);
            this.#ip = addr?.address || '';
        }
        return this.#ip;
    }
    get authorized() {
        // TLS client authorization is not directly accessible via Bun.serve()
        return undefined;
    }
    get peerCertificate() {
        // Peer certificates are not accessible via Bun.serve()
        return null;
    }
    get mtlsConfig() {
        return undefined;
    }
    get body() {
        return this.#body || (this.#body = new BunRequestBody(this._webRequest));
    }
    get host() {
        return this.headers.get('host');
    }
    get hostname() {
        return this.headers.get('host');
    }
    get httpVersion() {
        return '1.1';
    }
    get isAborted() {
        return this._webRequest.signal?.aborted ?? false;
    }
    get signal() {
        // Bun.serve() aborts this signal on HTTP client disconnect. Behavior on
        // WebSocket-upgrade is implementation-defined; if the WS path needs
        // guaranteed abort-on-close under Bun, wire it through Bun's ws close
        // handler with a Bun-side AbortController, similar to REST.ts on Node.
        return this._webRequest.signal;
    }
    _abort() {
        // On Bun, abort is driven by the underlying Web Request's signal; no-op for parity with Node path.
    }
    get nodeRequest() {
        return null;
    }
    sendEarlyHints(_link, _headers = {}) {
        // Early hints not supported on Bun
    }
}
exports.BunRequest = BunRequest;
class RequestBody {
    #nodeRequest;
    constructor(nodeRequest) {
        this.#nodeRequest = nodeRequest;
    }
    on(event, listener) {
        this.#nodeRequest.on(event, listener);
        return this;
    }
    pipe(destination, options) {
        return this.#nodeRequest.pipe(destination, options);
    }
}
class BunRequestBody {
    #webRequest;
    #readable; // lazily created Readable stream
    constructor(webRequest) {
        this.#webRequest = webRequest;
    }
    #getReadable() {
        if (!this.#readable) {
            const body = this.#webRequest.body;
            if (body) {
                this.#readable = node_stream_1.Readable.fromWeb(body);
            }
            else {
                // No body — create an empty readable that immediately ends
                this.#readable = new node_stream_1.Readable({
                    read() {
                        this.push(null);
                    },
                });
            }
        }
        return this.#readable;
    }
    on(event, listener) {
        this.#getReadable().on(event, listener);
        return this;
    }
    pipe(destination, options) {
        return this.#getReadable().pipe(destination, options);
    }
}
class RequestHeaders {
    asObject;
    constructor(asObject) {
        this.asObject = asObject;
    }
    set(name, value) {
        this.asObject[name.toLowerCase()] = value;
    }
    get(name) {
        return this.asObject[name.toLowerCase()];
    }
    has(name) {
        return Object.prototype.hasOwnProperty.call(this.asObject, name.toLowerCase());
    }
    [Symbol.iterator]() {
        return Object.entries(this.asObject)[Symbol.iterator]();
    }
    keys() {
        return Object.keys(this.asObject);
    }
    values() {
        return Object.values(this.asObject);
    }
    delete(name) {
        delete this.asObject[name.toLowerCase()];
    }
    forEach(callback) {
        for (const [key, value] of this) {
            callback(value, key, this);
        }
    }
}
//# sourceMappingURL=Request.js.map