"use strict";
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.SimplifiedFetchTransport = void 0;
const Utils = __importStar(require("../../primitives/utils.js"));
const defaultFetch = typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'function'
    ? globalThis.fetch.bind(globalThis)
    : fetch;
/**
 * Implements an HTTP-specific transport for handling Peer mutual authentication messages.
 * This class integrates with fetch to send and receive authenticated messages between peers.
 */
class SimplifiedFetchTransport {
    /**
     * Constructs a new instance of SimplifiedFetchTransport.
     * @param baseUrl - The base URL for all HTTP requests made by this transport.
     * @param fetchClient - A fetch implementation to use for HTTP requests (default: global fetch).
     */
    constructor(baseUrl, fetchClient = defaultFetch) {
        if (typeof fetchClient !== 'function') {
            throw new TypeError('SimplifiedFetchTransport requires a fetch implementation. ' +
                'In environments without fetch, provide a polyfill or custom implementation.');
        }
        this.fetchClient = fetchClient;
        this.baseUrl = baseUrl;
    }
    /**
     * Sends a message to an HTTP server using the transport mechanism.
     * Handles both general and authenticated message types. For general messages,
     * the payload is deserialized and sent as an HTTP request. For other message types,
     * the message is sent as a POST request to the `/auth` endpoint.
     *
     * @param message - The AuthMessage to send.
     * @returns A promise that resolves when the message is successfully sent.
     *
     * @throws Will throw an error if no listener has been registered via `onData`.
     */
    async send(message) {
        if (this.onDataCallback == null) {
            throw new Error('Listen before you start speaking. God gave you two ears and one mouth for a reason.');
        }
        if (message.messageType !== 'general') {
            return await new Promise((resolve, reject) => {
                void (async () => {
                    try {
                        const authUrl = `${this.baseUrl}/.well-known/auth`;
                        const responsePromise = (async () => {
                            try {
                                return await this.fetchClient(authUrl, {
                                    method: 'POST',
                                    headers: {
                                        'Content-Type': 'application/json'
                                    },
                                    body: JSON.stringify(message)
                                });
                            }
                            catch (error) {
                                throw this.createNetworkError(authUrl, error);
                            }
                        })();
                        // For initialRequest message, mark connection as established and start pool.
                        if (message.messageType !== 'initialRequest') {
                            resolve();
                        }
                        const response = await responsePromise;
                        if (!response.ok) {
                            const responseBodyArray = Array.from(new Uint8Array(await response.arrayBuffer()));
                            throw this.createUnauthenticatedResponseError(authUrl, response, responseBodyArray);
                        }
                        if (this.onDataCallback != null) {
                            const responseMessage = await response.json();
                            this.onDataCallback(responseMessage);
                        }
                        if (message.messageType === 'initialRequest') {
                            resolve();
                        }
                    }
                    catch (e) {
                        reject(e);
                    }
                })();
            });
        }
        else {
            // Parse message payload
            const httpRequest = this.deserializeRequestPayload(message.payload);
            // Send the byte array as the HTTP payload
            const url = `${this.baseUrl}${httpRequest.urlPostfix}`;
            const httpRequestWithAuthHeaders = httpRequest;
            if (typeof httpRequest.headers !== 'object') {
                httpRequestWithAuthHeaders.headers = {};
            }
            // Append auth headers in request to server
            httpRequestWithAuthHeaders.headers['x-bsv-auth-version'] = message.version;
            httpRequestWithAuthHeaders.headers['x-bsv-auth-identity-key'] = message.identityKey;
            httpRequestWithAuthHeaders.headers['x-bsv-auth-nonce'] = message.nonce;
            httpRequestWithAuthHeaders.headers['x-bsv-auth-your-nonce'] = message.yourNonce;
            httpRequestWithAuthHeaders.headers['x-bsv-auth-signature'] = Utils.toHex(message.signature);
            httpRequestWithAuthHeaders.headers['x-bsv-auth-request-id'] = httpRequest.requestId;
            // Ensure Content-Type is set for requests with a body
            if (httpRequestWithAuthHeaders.body != null) {
                const headers = httpRequestWithAuthHeaders.headers;
                if (headers['content-type'] == null) {
                    throw new Error('Content-Type header is required for requests with a body.');
                }
                const contentType = String(headers['content-type'] ?? '');
                // Transform body based on Content-Type
                if (contentType.includes('application/json')) {
                    // Convert byte array to JSON string
                    httpRequestWithAuthHeaders.body = Utils.toUTF8(httpRequestWithAuthHeaders.body);
                }
                else if (contentType.includes('application/x-www-form-urlencoded')) {
                    // Convert byte array to URL-encoded string
                    httpRequestWithAuthHeaders.body = Utils.toUTF8(httpRequestWithAuthHeaders.body);
                }
                else if (contentType.includes('text/plain')) {
                    // Convert byte array to plain UTF-8 string
                    httpRequestWithAuthHeaders.body = Utils.toUTF8(httpRequestWithAuthHeaders.body);
                }
                else {
                    // For all other content types, treat as binary data
                    httpRequestWithAuthHeaders.body = new Uint8Array(httpRequestWithAuthHeaders.body);
                }
            }
            // Send the actual fetch request to the server
            let response;
            try {
                response = await this.fetchClient(url, {
                    method: httpRequestWithAuthHeaders.method,
                    headers: httpRequestWithAuthHeaders.headers,
                    body: httpRequestWithAuthHeaders.body
                });
            }
            catch (error) {
                throw this.createNetworkError(url, error);
            }
            const responseBodyBuffer = await response.arrayBuffer();
            const responseBodyArray = Array.from(new Uint8Array(responseBodyBuffer));
            const missingAuthHeaders = ['x-bsv-auth-version', 'x-bsv-auth-identity-key', 'x-bsv-auth-signature']
                .filter(headerName => {
                const headerValue = response.headers.get(headerName);
                return headerValue == null || headerValue.trim().length === 0;
            });
            if (missingAuthHeaders.length > 0) {
                throw this.createUnauthenticatedResponseError(url, response, responseBodyArray, missingAuthHeaders);
            }
            const requestedCertificatesHeader = response.headers.get('x-bsv-auth-requested-certificates');
            let requestedCertificates;
            if (requestedCertificatesHeader != null) {
                try {
                    requestedCertificates = JSON.parse(requestedCertificatesHeader);
                }
                catch (error) {
                    throw this.createMalformedHeaderError(url, 'x-bsv-auth-requested-certificates', requestedCertificatesHeader, error);
                }
            }
            const payloadWriter = new Utils.Writer();
            if (response.headers.get('x-bsv-auth-request-id') != null) {
                payloadWriter.write(Utils.toArray(response.headers.get('x-bsv-auth-request-id'), 'base64'));
            }
            payloadWriter.writeVarIntNum(response.status);
            // PARSE RESPONSE HEADERS FROM SERVER --------------------------------
            // Parse response headers from the server and include only the signed headers:
            // - Include custom headers prefixed with x-bsv (excluding those starting with x-bsv-auth)
            // - Include the authorization header
            const includedHeaders = [];
            response.headers.forEach((value, key) => {
                const lowerKey = key.toLowerCase();
                if ((lowerKey.startsWith('x-bsv-') || lowerKey === 'authorization') && !lowerKey.startsWith('x-bsv-auth')) {
                    includedHeaders.push([lowerKey, value]);
                }
            });
            // Sort the headers by key to ensure a consistent order for signing and verification.
            includedHeaders.sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
            // nHeaders
            payloadWriter.writeVarIntNum(includedHeaders.length);
            for (const [headerKey, headerValue] of includedHeaders) {
                // headerKeyLength
                const headerKeyAsArray = Utils.toArray(headerKey, 'utf8');
                payloadWriter.writeVarIntNum(headerKeyAsArray.length);
                // headerKey
                payloadWriter.write(headerKeyAsArray);
                // headerValueLength
                const headerValueAsArray = Utils.toArray(headerValue, 'utf8');
                payloadWriter.writeVarIntNum(headerValueAsArray.length);
                // headerValue
                payloadWriter.write(headerValueAsArray);
            }
            // Handle body
            payloadWriter.writeVarIntNum(responseBodyArray.length);
            if (responseBodyArray.length > 0) {
                payloadWriter.write(responseBodyArray);
            }
            // Build the correct AuthMessage for the response
            const responseMessage = {
                version: response.headers.get('x-bsv-auth-version'),
                messageType: response.headers.get('x-bsv-auth-message-type') === 'certificateRequest' ? 'certificateRequest' : 'general',
                identityKey: response.headers.get('x-bsv-auth-identity-key'),
                nonce: response.headers.get('x-bsv-auth-nonce') ?? undefined,
                yourNonce: response.headers.get('x-bsv-auth-your-nonce') ?? undefined,
                requestedCertificates,
                payload: payloadWriter.toArray(),
                signature: Utils.toArray(response.headers.get('x-bsv-auth-signature'), 'hex')
            };
            // If the server didn't provide the correct authentication headers, throw an error
            if (responseMessage.version == null) {
                throw this.createUnauthenticatedResponseError(url, response, responseBodyArray);
            }
            // Handle the response if data is received and callback is set
            this.onDataCallback(responseMessage);
        }
    }
    /**
     * Registers a callback to handle incoming messages.
     * This must be called before sending any messages to ensure responses can be processed.
     *
     * @param callback - A function to invoke when an incoming AuthMessage is received.
     * @returns A promise that resolves once the callback is set.
     */
    async onData(callback) {
        this.onDataCallback = (m) => {
            void callback(m).catch(() => {
                // Errors from handleIncomingMessage on the client side are not
                // actionable here — prevent unhandled promise rejections.
            });
        };
    }
    createNetworkError(url, originalError) {
        const baseMessage = `Network error while sending authenticated request to ${url}`;
        if (originalError instanceof Error) {
            const error = new Error(`${baseMessage}: ${originalError.message}`);
            error.stack = originalError.stack;
            error.cause = originalError;
            return error;
        }
        return new Error(`${baseMessage}: ${String(originalError)}`);
    }
    createUnauthenticatedResponseError(url, response, bodyBytes, missingHeaders = []) {
        const statusText = (response.statusText ?? '').trim();
        const statusDescription = statusText.length > 0
            ? `${response.status} ${statusText}`
            : `${response.status}`;
        const headerMessage = missingHeaders.length > 0
            ? `missing headers: ${missingHeaders.join(', ')}`
            : 'response lacked required BSV auth headers';
        const bodyPreview = this.getBodyPreview(bodyBytes, response.headers.get('content-type'));
        const parts = [`Received HTTP ${statusDescription} from ${url} without valid BSV authentication (${headerMessage})`];
        if (bodyPreview != null) {
            parts.push(`body preview: ${bodyPreview}`);
        }
        const error = new Error(parts.join(' - '));
        error.details = {
            url,
            status: response.status,
            statusText: response.statusText,
            missingHeaders,
            bodyPreview
        };
        return error;
    }
    createMalformedHeaderError(url, headerName, headerValue, cause) {
        const errorMessage = `Failed to parse ${headerName} returned by ${url}: ${headerValue}`;
        if (cause instanceof Error) {
            const error = new Error(`${errorMessage}. ${cause.message}`);
            error.stack = cause.stack;
            error.cause = cause;
            return error;
        }
        return new Error(`${errorMessage}. ${String(cause)}`);
    }
    getBodyPreview(bodyBytes, contentType) {
        if (bodyBytes.length === 0) {
            return undefined;
        }
        const maxBytesForPreview = 1024;
        const truncated = bodyBytes.length > maxBytesForPreview;
        const slice = truncated ? bodyBytes.slice(0, maxBytesForPreview) : bodyBytes;
        const isText = this.isTextualContent(contentType, slice);
        let preview;
        if (isText) {
            try {
                preview = Utils.toUTF8(slice);
            }
            catch {
                preview = this.formatBinaryPreview(slice, truncated);
            }
        }
        else {
            preview = this.formatBinaryPreview(slice, truncated);
        }
        if (preview.length > 512) {
            preview = `${preview.slice(0, 512)}…`;
        }
        if (truncated) {
            preview = `${preview} (truncated)`;
        }
        return preview;
    }
    isTextualContent(contentType, sample) {
        if (sample.length === 0) {
            return false;
        }
        if (contentType != null) {
            const lowered = contentType.toLowerCase();
            const textualTokens = [
                'application/json',
                'application/problem+json',
                'application/xml',
                'application/xhtml+xml',
                'application/javascript',
                'application/ecmascript',
                'application/x-www-form-urlencoded',
                'text/'
            ];
            if (textualTokens.some(token => lowered.includes(token)) || lowered.includes('charset=')) {
                return true;
            }
        }
        const printableCount = sample.reduce((count, byte) => {
            if (byte === 9 || byte === 10 || byte === 13) {
                return count + 1;
            }
            if (byte >= 32 && byte <= 126) {
                return count + 1;
            }
            return count;
        }, 0);
        return (printableCount / sample.length) > 0.8;
    }
    formatBinaryPreview(bytes, truncated) {
        const hex = bytes.map(byte => byte.toString(16).padStart(2, '0')).join('');
        return `0x${hex}${truncated ? '…' : ''}`;
    }
    /**
     * Deserializes a request payload from a byte array into an HTTP request-like structure.
     *
     * @param payload - The serialized payload to deserialize.
     * @returns An object representing the deserialized request, including the method,
     *          URL postfix (path and query string), headers, body, and request ID.
     */
    deserializeRequestPayload(payload) {
        // Create a reader
        const requestReader = new Utils.Reader(payload);
        // The first 32 bytes is the requestId
        const requestId = Utils.toBase64(requestReader.read(32));
        // Method
        const methodLength = requestReader.readVarIntNum();
        let method = 'GET';
        if (methodLength > 0) {
            method = Utils.toUTF8(requestReader.read(methodLength));
        }
        // Path
        const pathLength = requestReader.readVarIntNum();
        let path = '';
        if (pathLength > 0) {
            path = Utils.toUTF8(requestReader.read(pathLength));
        }
        // Search
        const searchLength = requestReader.readVarIntNum();
        let search = '';
        if (searchLength > 0) {
            search = Utils.toUTF8(requestReader.read(searchLength));
        }
        // Read headers
        const requestHeaders = {};
        const nHeaders = requestReader.readVarIntNum();
        if (nHeaders > 0) {
            for (let i = 0; i < nHeaders; i++) {
                const nHeaderKeyBytes = requestReader.readVarIntNum();
                const headerKeyBytes = requestReader.read(nHeaderKeyBytes);
                const headerKey = Utils.toUTF8(headerKeyBytes);
                const nHeaderValueBytes = requestReader.readVarIntNum();
                const headerValueBytes = requestReader.read(nHeaderValueBytes);
                const headerValue = Utils.toUTF8(headerValueBytes);
                requestHeaders[headerKey] = headerValue;
            }
        }
        // Read body
        let requestBody;
        const requestBodyBytes = requestReader.readVarIntNum();
        if (requestBodyBytes > 0) {
            requestBody = requestReader.read(requestBodyBytes);
        }
        // Return the deserialized RequestInit
        return {
            urlPostfix: path + search,
            method,
            headers: requestHeaders,
            body: requestBody,
            requestId
        };
    }
}
exports.SimplifiedFetchTransport = SimplifiedFetchTransport;
//# sourceMappingURL=SimplifiedFetchTransport.js.map