"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Object.defineProperty(exports, "__esModule", { value: true });
exports.TunnelManagementHttpClient = exports.ManagementApiVersions = void 0;
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const dev_tunnels_ssh_1 = require("@microsoft/dev-tunnels-ssh");
const dev_tunnels_contracts_1 = require("@microsoft/dev-tunnels-contracts");
const tunnelManagementClient_1 = require("./tunnelManagementClient");
const tunnelAccessTokenProperties_1 = require("./tunnelAccessTokenProperties");
const version_1 = require("./version");
const axios_1 = require("axios");
const tunnelPlanTokenProperties_1 = require("./tunnelPlanTokenProperties");
const idGeneration_1 = require("./idGeneration");
const tunnelsApiPath = '/tunnels';
const limitsApiPath = '/userlimits';
const endpointsApiSubPath = '/endpoints';
const portsApiSubPath = '/ports';
const eventsApiSubPath = '/events';
const clustersApiPath = '/clusters';
const tunnelAuthentication = 'Authorization';
const checkAvailablePath = ':checkNameAvailability';
const createNameRetries = 3;
var ManagementApiVersions;
(function (ManagementApiVersions) {
    ManagementApiVersions["Version20230927preview"] = "2023-09-27-preview";
})(ManagementApiVersions = exports.ManagementApiVersions || (exports.ManagementApiVersions = {}));
function comparePorts(a, b) {
    var _a, _b;
    return ((_a = a.portNumber) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER) - ((_b = b.portNumber) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER);
}
function parseDate(value) {
    return typeof value === 'string' ? new Date(Date.parse(value)) : value;
}
/**
 * Fixes Tunnel properties of type Date that were deserialized as strings.
 */
function parseTunnelDates(tunnel) {
    if (!tunnel)
        return;
    tunnel.created = parseDate(tunnel.created);
    if (tunnel.status) {
        tunnel.status.lastHostConnectionTime = parseDate(tunnel.status.lastHostConnectionTime);
        tunnel.status.lastClientConnectionTime = parseDate(tunnel.status.lastClientConnectionTime);
    }
}
/**
 * Fixes TunnelPort properties of type Date that were deserialized as strings.
 */
function parseTunnelPortDates(port) {
    if (!port)
        return;
    if (port.status) {
        port.status.lastClientConnectionTime = parseDate(port.status.lastClientConnectionTime);
    }
}
/**
 * Copy access tokens from the request object to the result object, except for any
 * tokens that were refreshed by the request.
 */
function preserveAccessTokens(requestObject, resultObject) {
    var _a;
    // This intentionally does not check whether any existing tokens are expired. So
    // expired tokens may be preserved also, if not refreshed. This allows for better
    // diagnostics in that case.
    if (requestObject.accessTokens && resultObject) {
        (_a = resultObject.accessTokens) !== null && _a !== void 0 ? _a : (resultObject.accessTokens = {});
        for (const scopeAndToken of Object.entries(requestObject.accessTokens)) {
            if (!resultObject.accessTokens[scopeAndToken[0]]) {
                resultObject.accessTokens[scopeAndToken[0]] = scopeAndToken[1];
            }
        }
    }
}
const manageAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Manage];
const hostAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Host];
const managePortsAccessTokenScopes = [
    dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
    dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
    dev_tunnels_contracts_1.TunnelAccessScopes.Host,
];
const readAccessTokenScopes = [
    dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
    dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
    dev_tunnels_contracts_1.TunnelAccessScopes.Host,
    dev_tunnels_contracts_1.TunnelAccessScopes.Connect,
];
const apiVersions = ["2023-09-27-preview"];
const defaultRequestTimeoutMS = 20000;
class TunnelManagementHttpClient {
    /**
     * Initializes a new instance of the `TunnelManagementHttpClient` class
     * with a client authentication callback, service URI, and HTTP handler.
     *
     * @param userAgent { name, version } object or a comment string to use as the User-Agent header.
     * @param apiVersion ApiVersion to be used for requests, value should be one of ManagementApiVersions enum.
     * @param userTokenCallback Optional async callback for retrieving a client authentication
     * header value with access token, for AAD or GitHub user authentication. This may be omitted
     * for anonymous tunnel clients, or if tunnel access tokens will be specified via
     * `TunnelRequestOptions.accessToken`.
     * @param tunnelServiceUri Optional tunnel service URI (not including any path). Defaults to
     * the global tunnel service URI.
     * @param httpsAgent Optional agent that will be invoked for HTTPS requests to the tunnel
     * service.
     * @param adapter Optional axios adapter to use for HTTP requests.
     */
    constructor(userAgents, apiVersion, userTokenCallback, tunnelServiceUri, httpsAgent, adapter) {
        var _a;
        this.httpsAgent = httpsAgent;
        this.adapter = adapter;
        this.reportProgressEmitter = new vscode_jsonrpc_1.Emitter();
        /**
         * Event that is raised to report tunnel management progress.
         *
         * See `Progress` for a description of the different progress events that can be reported.
         */
        this.onReportProgress = this.reportProgressEmitter.event;
        this.trace = (msg) => { };
        /**
         * Gets or sets a value indicating whether events reporting is enabled.
         *
         * When not enabled, any events reported via {@link reportEvent}
         * (either by the tunnel SDK or the application) will be ignored.
         */
        this.enableEventsReporting = false;
        this.eventsQueue = [];
        this.eventsPromise = null;
        this.isDisposed = false;
        this.eventsAvailableCompletion = new dev_tunnels_ssh_1.PromiseCompletionSource();
        if (apiVersions.indexOf(apiVersion) === -1) {
            throw new TypeError(`Invalid API version: ${apiVersion}, must be one of ${apiVersions}`);
        }
        this.apiVersion = apiVersion;
        if (!userAgents) {
            throw new TypeError('User agent must be provided.');
        }
        if (Array.isArray(userAgents)) {
            if (userAgents.length === 0) {
                throw new TypeError('User agents cannot be empty.');
            }
            let combinedUserAgents = '';
            userAgents.forEach((userAgent) => {
                var _a;
                if (typeof userAgent !== 'string') {
                    if (!userAgent.name) {
                        throw new TypeError('Invalid user agent. The name must be provided.');
                    }
                    if (typeof userAgent.name !== 'string') {
                        throw new TypeError('Invalid user agent. The name must be a string.');
                    }
                    if (userAgent.version && typeof userAgent.version !== 'string') {
                        throw new TypeError('Invalid user agent. The version must be a string.');
                    }
                    combinedUserAgents = `${combinedUserAgents}${userAgent.name}/${(_a = userAgent.version) !== null && _a !== void 0 ? _a : 'unknown'} `;
                }
                else {
                    combinedUserAgents = `${combinedUserAgents}${userAgent} `;
                }
            });
            this.userAgents = combinedUserAgents.trim();
        }
        else if (typeof userAgents !== 'string') {
            if (!userAgents.name) {
                throw new TypeError('Invalid user agent. The name must be provided.');
            }
            if (typeof userAgents.name !== 'string') {
                throw new TypeError('Invalid user agent. The name must be a string.');
            }
            if (userAgents.version && typeof userAgents.version !== 'string') {
                throw new TypeError('Invalid user agent. The version must be a string.');
            }
            this.userAgents = `${userAgents.name}/${(_a = userAgents.version) !== null && _a !== void 0 ? _a : 'unknown'}`;
        }
        else {
            this.userAgents = userAgents;
        }
        this.userTokenCallback = userTokenCallback !== null && userTokenCallback !== void 0 ? userTokenCallback : (() => Promise.resolve(null));
        if (!tunnelServiceUri) {
            tunnelServiceUri = dev_tunnels_contracts_1.TunnelServiceProperties.production.serviceUri;
        }
        const parsedUri = new URL(tunnelServiceUri);
        if (!parsedUri || parsedUri.pathname !== '/') {
            throw new TypeError(`Invalid tunnel service URI: ${tunnelServiceUri}`);
        }
        this.baseAddress = tunnelServiceUri;
        this.isCustomDomain = new URL(tunnelServiceUri).hostname.startsWith('cp.');
    }
    /**
     * Creates a `TunnelManagementHttpClient` configured for a custom domain.
     *
     * When a custom domain is configured (e.g., "app.github.dev"), control plane calls
     * are routed to "cp.{domain}" and cluster ID hostname manipulation is skipped
     * because routing is handled at the infrastructure level.
     *
     * @param customDomain The custom domain (e.g., "app.github.dev").
     * @param userAgents User agent(s).
     * @param apiVersion API version.
     * @param userTokenCallback Optional authentication callback.
     * @param httpsAgent Optional HTTPS agent.
     * @param adapter Optional axios adapter.
     */
    static forCustomDomain(customDomain, userAgents, apiVersion, userTokenCallback, httpsAgent, adapter) {
        if (!customDomain) {
            throw new TypeError('Custom domain must be a non-empty string.');
        }
        const serviceUri = `https://cp.${customDomain}/`;
        return new TunnelManagementHttpClient(userAgents, apiVersion, userTokenCallback, serviceUri, httpsAgent, adapter);
    }
    async listTunnels(clusterId, domain, options, cancellation) {
        const queryParams = [clusterId ? null : 'global=true', domain ? `domain=${domain}` : null];
        const query = queryParams.filter((p) => !!p).join('&');
        const results = (await this.sendRequest('GET', clusterId, tunnelsApiPath, query, options, undefined, undefined, cancellation));
        let tunnels = new Array();
        if (results.value) {
            for (const region of results.value) {
                if (region.value) {
                    tunnels = tunnels.concat(region.value);
                }
            }
        }
        tunnels.forEach(parseTunnelDates);
        return tunnels;
    }
    async getTunnel(tunnel, options, cancellation) {
        const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, undefined, undefined, options, undefined, undefined, cancellation);
        preserveAccessTokens(tunnel, result);
        parseTunnelDates(result);
        return result;
    }
    async createTunnel(tunnel, options, cancellation) {
        var _a;
        const tunnelId = tunnel.tunnelId;
        const idGenerated = tunnelId === undefined || tunnelId === null || tunnelId === '';
        options = options || {};
        options.additionalHeaders = options.additionalHeaders || {};
        options.additionalHeaders['If-Not-Match'] = "*";
        if (idGenerated) {
            tunnel.tunnelId = idGeneration_1.IdGeneration.generateTunnelId();
        }
        for (let retries = 0;; retries++) {
            try {
                const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel), undefined, cancellation, true));
                preserveAccessTokens(tunnel, result);
                parseTunnelDates(result);
                return result;
            }
            catch (error) {
                if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 409 &&
                    idGenerated && retries < createNameRetries) {
                    // The tunnel ID was generated and there was a conflict.
                    // Try again with a new ID.
                    tunnel.tunnelId = idGeneration_1.IdGeneration.generateTunnelId();
                }
                else {
                    throw error;
                }
            }
        }
    }
    async createOrUpdateTunnel(tunnel, options, cancellation) {
        const tunnelId = tunnel.tunnelId;
        if (!tunnelId) {
            // When no tunnel ID is specified, a randomly-generated ID should not unintionally
            // update an existing tunnel.
            return this.createTunnel(tunnel, options, cancellation);
        }
        const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel), undefined, cancellation, true));
        preserveAccessTokens(tunnel, result);
        parseTunnelDates(result);
        return result;
    }
    async updateTunnel(tunnel, options, cancellation) {
        options = options || {};
        options.additionalHeaders = options.additionalHeaders || {};
        options.additionalHeaders['If-Match'] = "*";
        const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel), undefined, cancellation));
        preserveAccessTokens(tunnel, result);
        parseTunnelDates(result);
        return result;
    }
    async deleteTunnel(tunnel, options, cancellation) {
        return await this.sendTunnelRequest('DELETE', tunnel, manageAccessTokenScope, undefined, undefined, options, undefined, true, cancellation);
    }
    async updateTunnelEndpoint(tunnel, endpoint, options, cancellation) {
        if (endpoint.id == null) {
            throw new Error('Endpoint ID must be specified when updating an endpoint.');
        }
        const path = `${endpointsApiSubPath}/${endpoint.id}`;
        const result = (await this.sendTunnelRequest('PUT', tunnel, hostAccessTokenScope, path, "connectionMode=" + endpoint.connectionMode, options, endpoint, undefined, cancellation));
        if (tunnel.endpoints) {
            // Also update the endpoint in the local tunnel object.
            tunnel.endpoints = tunnel.endpoints
                .filter((e) => e.hostId !== endpoint.hostId ||
                e.connectionMode !== endpoint.connectionMode)
                .concat(result);
        }
        return result;
    }
    async deleteTunnelEndpoints(tunnel, id, options, cancellation) {
        const path = `${endpointsApiSubPath}/${id}`;
        const result = await this.sendTunnelRequest('DELETE', tunnel, hostAccessTokenScope, path, undefined, options, undefined, true, cancellation);
        if (result && tunnel.endpoints) {
            // Also delete the endpoint in the local tunnel object.
            tunnel.endpoints = tunnel.endpoints.filter((e) => e.id !== id);
        }
        return result;
    }
    async listUserLimits(cancellation) {
        const results = await this.sendRequest('GET', undefined, limitsApiPath, undefined, undefined, undefined, undefined, cancellation);
        return results || [];
    }
    async listTunnelPorts(tunnel, options, cancellation) {
        const results = (await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, portsApiSubPath, undefined, options, undefined, undefined, cancellation));
        if (results.value) {
            results.value.forEach(parseTunnelPortDates);
        }
        return results.value;
    }
    async getTunnelPort(tunnel, portNumber, options, cancellation) {
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingGetTunnelPort);
        const path = `${portsApiSubPath}/${portNumber}`;
        const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, path, undefined, options, undefined, undefined, cancellation);
        parseTunnelPortDates(result);
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedGetTunnelPort);
        return result;
    }
    async createTunnelPort(tunnel, tunnelPort, options, cancellation) {
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingCreateTunnelPort);
        tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
        const path = `${portsApiSubPath}/${tunnelPort.portNumber}`;
        options = options || {};
        options.additionalHeaders = options.additionalHeaders || {};
        options.additionalHeaders['If-Not-Match'] = "*";
        const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort, undefined, cancellation));
        tunnel.ports = tunnel.ports || [];
        // Also add the port to the local tunnel object.
        tunnel.ports = tunnel.ports
            .filter((p) => p.portNumber !== tunnelPort.portNumber)
            .concat(result)
            .sort(comparePorts);
        parseTunnelPortDates(result);
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedCreateTunnelPort);
        return result;
    }
    async updateTunnelPort(tunnel, tunnelPort, options, cancellation) {
        if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
            throw new Error('Tunnel port cluster ID is not consistent.');
        }
        options = options || {};
        options.additionalHeaders = options.additionalHeaders || {};
        options.additionalHeaders['If-Match'] = "*";
        const portNumber = tunnelPort.portNumber;
        const path = `${portsApiSubPath}/${portNumber}`;
        tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
        const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort, undefined, cancellation));
        preserveAccessTokens(tunnelPort, result);
        parseTunnelPortDates(result);
        tunnel.ports = tunnel.ports || [];
        // Also add the port to the local tunnel object.
        tunnel.ports = tunnel.ports
            .filter((p) => p.portNumber !== tunnelPort.portNumber)
            .concat(result)
            .sort(comparePorts);
        return result;
    }
    async createOrUpdateTunnelPort(tunnel, tunnelPort, options, cancellation) {
        tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
        const path = `${portsApiSubPath}/${tunnelPort.portNumber}`;
        const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort, undefined, cancellation));
        tunnel.ports = tunnel.ports || [];
        // Also add the port to the local tunnel object.
        tunnel.ports = tunnel.ports
            .filter((p) => p.portNumber !== tunnelPort.portNumber)
            .concat(result)
            .sort(comparePorts);
        parseTunnelPortDates(result);
        return result;
    }
    async deleteTunnelPort(tunnel, portNumber, options, cancellation) {
        const path = `${portsApiSubPath}/${portNumber}`;
        const result = await this.sendTunnelRequest('DELETE', tunnel, managePortsAccessTokenScopes, path, undefined, options, undefined, true, cancellation);
        if (result && tunnel.ports) {
            // Also delete the port in the local tunnel object.
            tunnel.ports = tunnel.ports
                .filter((p) => p.portNumber !== portNumber)
                .sort(comparePorts);
        }
        return result;
    }
    async listClusters(cancellation) {
        return (await this.sendRequest('GET', undefined, clustersApiPath, undefined, undefined, undefined, false, cancellation));
    }
    /**
     * Sends an HTTP request to the tunnel management API, targeting a specific tunnel.
     * This protected method enables subclasses to support additional tunnel management APIs.
     * @param method HTTP request method.
     * @param tunnel Tunnel that the request is targeting.
     * @param accessTokenScopes Required array of access scopes for tokens in `tunnel.accessTokens`
     * that could be used to authorize the request.
     * @param path Optional request sub-path relative to the tunnel.
     * @param query Optional query string to append to the request.
     * @param options Request options.
     * @param body Optional request body object.
     * @param allowNotFound If true, a 404 response is returned as a null or false result
     * instead of an error.
     * @param cancellationToken Optional cancellation token for the request.
     * @param isCreate Set to true if this is a tunnel create request, default is false.
     * @returns Result of the request.
     */
    async sendTunnelRequest(method, tunnel, accessTokenScopes, path, query, options, body, allowNotFound, cancellation, isCreate = false) {
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingRequestUri);
        const uri = await this.buildUriForTunnel(tunnel, path, query, options, isCreate);
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingRequestConfig);
        const config = await this.getAxiosRequestConfig(tunnel, options, accessTokenScopes);
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingSendTunnelRequest);
        try {
            const result = await this.request(method, uri, body, config, allowNotFound, cancellation);
            this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedSendTunnelRequest);
            return result;
        }
        catch (error) {
            if (/certificate/i.test(error.message)) {
                const originalErrorMessage = error.message;
                throw new Error("Tunnel service HTTPS certificate is invalid. This may be caused by the use of a " +
                    "self-signed certificate or a firewall intercepting the connection. " + originalErrorMessage + ". ");
            }
            throw error;
        }
    }
    /**
     * Sends an HTTP request to the tunnel management API.
     * This protected method enables subclasses to support additional tunnel management APIs.
     * @param method HTTP request method.
     * @param clusterId Optional tunnel service cluster ID to direct the request to. If unspecified,
     * the request will use the global traffic-manager to find the nearest cluster.
     * @param path Required request path.
     * @param query Optional query string to append to the request.
     * @param options Request options.
     * @param body Optional request body object.
     * @param allowNotFound If true, a 404 response is returned as a null or false result
     * instead of an error.
     * @param cancellationToken Optional cancellation token for the request.
     * @returns Result of the request.
     */
    async sendRequest(method, clusterId, path, query, options, body, allowNotFound, cancellation) {
        this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingSendTunnelRequest);
        const uri = await this.buildUri(clusterId, path, query, options);
        const config = await this.getAxiosRequestConfig(undefined, options);
        try {
            const result = await this.request(method, uri, body, config, allowNotFound, cancellation);
            this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedSendTunnelRequest);
            return result;
        }
        catch (error) {
            if (/certificate/i.test(error.message)) {
                throw new Error("Tunnel service HTTPS certificate is invalid. This may be caused by the use of a " +
                    "self signed certificate or a firewall intercepting the connection.");
            }
            throw error;
        }
    }
    async checkNameAvailablility(tunnelName, cancellation) {
        tunnelName = encodeURI(tunnelName);
        const uri = await this.buildUri(undefined, `${tunnelsApiPath}/${tunnelName}${checkAvailablePath}`);
        const config = {
            httpsAgent: this.httpsAgent,
            adapter: this.adapter,
        };
        return await this.request('GET', uri, undefined, config, undefined, cancellation);
    }
    reportEvent(tunnel, tunnelEvent, options) {
        var _a;
        if (!tunnel) {
            throw new TypeError('A tunnel is required.');
        }
        if (!tunnelEvent) {
            throw new TypeError('A tunnelEvent is required.');
        }
        if (!this.apiVersion) {
            // Events are not supported by the V1 API.
            return;
        }
        if (!this.enableEventsReporting) {
            return;
        }
        if (this.isDisposed) {
            // Do not queue any more events after the client is disposed.
            return;
        }
        // Set the client timestamp if it wasn't already initialized.
        (_a = tunnelEvent.timestamp) !== null && _a !== void 0 ? _a : (tunnelEvent.timestamp = new Date());
        const wasEmpty = this.eventsQueue.length === 0;
        this.eventsQueue.push({
            tunnel: tunnel,
            event: tunnelEvent,
            requestOptions: options
        });
        // Signal that events are available
        if (wasEmpty) {
            this.eventsAvailableCompletion.resolve();
        }
        if (this.eventsPromise === null) {
            this.eventsPromise = this.processPendingEventsAsync();
        }
    }
    async processPendingEventsAsync() {
        const eventsToSend = [];
        while (!this.isDisposed) {
            await this.eventsAvailableCompletion.promise;
            this.eventsAvailableCompletion = new dev_tunnels_ssh_1.PromiseCompletionSource();
            // Get the first event
            const nextEventInfo = this.eventsQueue.shift();
            if (!nextEventInfo) {
                // The completion was resolved, but no events were queued.
                // This indicates the client is being disposed.
                break;
            }
            const tunnel = nextEventInfo.tunnel;
            const requestOptions = nextEventInfo.requestOptions;
            eventsToSend.length = 0;
            eventsToSend.push(nextEventInfo.event);
            // Batch events for the same tunnel with the same request options
            while (this.eventsQueue.length > 0) {
                const peekEventInfo = this.eventsQueue[0];
                // Check if next event is for the same tunnel and has same request options
                if (peekEventInfo.tunnel !== tunnel || peekEventInfo.requestOptions !== requestOptions) {
                    // Different tunnel or options, process as separate batch
                    break;
                }
                eventsToSend.push(this.eventsQueue.shift().event);
            }
            // Upload a batch of events for the same tunnel
            try {
                // Do not use sendTunnelRequest() here, to avoid reporting progress
                // for these requests.
                const uri = await this.buildUriForTunnel(tunnel, eventsApiSubPath, this.tunnelRequestOptionsToQueryString(requestOptions), requestOptions);
                const config = await this.getAxiosRequestConfig(tunnel, requestOptions, readAccessTokenScopes);
                await this.request('POST', uri, [...eventsToSend], // Create a copy to avoid mutation issues
                config, undefined, undefined);
            }
            catch (error) {
                // Errors uploading events are ignored.
                this.trace(`Error uploading events: ${error}`);
            }
        }
    }
    raiseReportProgress(progress) {
        const args = {
            progress: progress
        };
        this.reportProgressEmitter.fire(args);
    }
    getResponseErrorMessage(error, signal) {
        var _a, _b, _c, _d, _e, _f;
        let errorMessage = '';
        if (signal.aborted) {
            // connection timeout
            error.code = 'ECONNABORTED';
            errorMessage = `ECONNABORTED: (signal aborted) ${error.message}`;
        }
        else if (error.code === 'ECONNABORTED') {
            // server timeout
            errorMessage = `ECONNABORTED: (timeout) ${error.message}`;
        }
        if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
            const problemDetails = error.response.data;
            if (problemDetails.title || problemDetails.detail) {
                errorMessage = `Tunnel service error: ${problemDetails.title}`;
                if (problemDetails.detail) {
                    errorMessage += ' ' + problemDetails.detail;
                }
                if (problemDetails.errors) {
                    errorMessage += JSON.stringify(problemDetails.errors);
                }
            }
        }
        if (!errorMessage && error.response && error.response.status &&
            error.response.status >= 400 && error.response.status < 500 &&
            error.response.headers) {
            const headers = error.response.headers;
            const servedBy = headers['X-Served-By'] || headers['x-served-by'];
            if (!/tunnels-/.test(servedBy)) {
                // The response did not include either a ProblemDetails body object or a header
                // confirming it was served by the tunnel service. This check excludes 5xx status
                // responses which may include non-firwall network infrastructure issues.
                const requestDomain = new URL((_c = (_b = error.config) === null || _b === void 0 ? void 0 : _b.url) !== null && _c !== void 0 ? _c : dev_tunnels_contracts_1.TunnelServiceProperties.production.serviceUri).host;
                errorMessage = 'The tunnel request resulted in ' +
                    `${error.response.status} status, but the request ` +
                    'did not reach the tunnel service. This may indicate the domain ' +
                    `'${requestDomain}' is blocked by a firewall.`;
            }
        }
        if (!errorMessage) {
            if (error.response) {
                errorMessage =
                    'Tunnel service returned status code: ' +
                        `${error.response.status} ${error.response.statusText}`;
            }
            else {
                errorMessage = (_e = (_d = error === null || error === void 0 ? void 0 : error.message) !== null && _d !== void 0 ? _d : error) !== null && _e !== void 0 ? _e : 'Unknown tunnel service request error.';
            }
        }
        const requestIdHeaderName = 'VsSaaS-Request-Id';
        if (((_f = error.response) === null || _f === void 0 ? void 0 : _f.headers) && error.response.headers[requestIdHeaderName]) {
            errorMessage += `\nRequest ID: ${error.response.headers[requestIdHeaderName]}`;
        }
        return errorMessage;
    }
    // Helper functions
    async buildUri(clusterId, path, query, options) {
        if (clusterId === undefined && this.userTokenCallback) {
            let token = await this.userTokenCallback();
            if (token && token.startsWith("tunnelplan")) {
                token = token.replace("tunnelplan ", "");
                const parsedToken = tunnelPlanTokenProperties_1.TunnelPlanTokenProperties.tryParse(token);
                if (parsedToken !== null && parsedToken.clusterId) {
                    clusterId = parsedToken.clusterId;
                }
            }
        }
        let baseAddress = this.baseAddress;
        if (clusterId && !this.isCustomDomain) {
            const url = new URL(baseAddress);
            const portNumber = parseInt(url.port, 10);
            url.hostname = TunnelManagementHttpClient.replaceTunnelServiceHostnameClusterId(url.hostname, clusterId);
            if (url.protocol === 'https:' &&
                clusterId.startsWith('localhost') &&
                portNumber % 10 > 0) {
                // Local testing simulates clusters by running the service on multiple ports.
                // Change the port number to match the cluster ID suffix.
                const clusterNumber = parseInt(clusterId.substring('localhost'.length), 10);
                if (clusterNumber > 0 && clusterNumber < 10) {
                    url.port = (portNumber - (portNumber % 10) + clusterNumber).toString();
                }
            }
            baseAddress = url.toString();
        }
        baseAddress = `${baseAddress.replace(/\/$/, '')}${path}`;
        const optionsQuery = this.tunnelRequestOptionsToQueryString(options, query);
        if (optionsQuery) {
            baseAddress += `?${optionsQuery}`;
        }
        return baseAddress;
    }
    static replaceTunnelServiceHostnameClusterId(hostname, clusterId) {
        // tunnels.local.api.visualstudio.com resolves to localhost (for local development).
        if (!clusterId ||
            hostname === 'localhost' ||
            hostname === 'tunnels.local.api.visualstudio.com') {
            return hostname;
        }
        if (hostname.startsWith('global.') ||
            dev_tunnels_contracts_1.TunnelConstraints.clusterIdPrefixRegex.test(hostname)) {
            // Hostname is in the form "global.rel.tunnels..." or "<clusterId>.rel.tunnels..."
            // Replace the first part of the hostname with the specified cluster ID.
            return clusterId + hostname.substring(hostname.indexOf('.'));
        }
        else {
            // Hostname does not have a recognized cluster prefix. Prepend the cluster ID.
            return `${clusterId}.${hostname}`;
        }
    }
    buildUriForTunnel(tunnel, path, query, options, isCreate = false) {
        let tunnelPath = '';
        if ((tunnel.clusterId || isCreate) && tunnel.tunnelId) {
            tunnelPath = `${tunnelsApiPath}/${tunnel.tunnelId}`;
        }
        else {
            throw new Error('Tunnel object must include a tunnel ID always and cluster ID for non creates.');
        }
        if (options === null || options === void 0 ? void 0 : options.additionalQueryParameters) {
            for (const [paramName, paramValue] of Object.entries(options.additionalQueryParameters)) {
                if (query) {
                    query += `&${paramName}=${paramValue}`;
                }
                else {
                    query = `${paramName}=${paramValue}`;
                }
            }
        }
        return this.buildUri(tunnel.clusterId, tunnelPath + (path ? path : ''), query, options);
    }
    async getAxiosRequestConfig(tunnel, options, accessTokenScopes) {
        // Get access token header
        const headers = {};
        if (options && options.accessToken) {
            headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${options.accessToken}`;
        }
        if (!(tunnelAuthentication in headers) && this.userTokenCallback) {
            const token = await this.userTokenCallback();
            if (token) {
                headers[tunnelAuthentication] = token;
            }
        }
        if (!(tunnelAuthentication in headers)) {
            const accessToken = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.getTunnelAccessToken(tunnel, accessTokenScopes);
            if (accessToken) {
                headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${accessToken}`;
            }
        }
        const copyAdditionalHeaders = (additionalHeaders) => {
            if (additionalHeaders) {
                for (const [headerName, headerValue] of Object.entries(additionalHeaders)) {
                    headers[headerName] = headerValue;
                }
            }
        };
        copyAdditionalHeaders(this.additionalRequestHeaders);
        copyAdditionalHeaders(options === null || options === void 0 ? void 0 : options.additionalHeaders);
        const userAgentPrefix = headers['User-Agent'] ? headers['User-Agent'] + ' ' : '';
        headers['User-Agent'] = `${userAgentPrefix}${this.userAgents} ${version_1.tunnelSdkUserAgent}`;
        // Get axios config
        const config = Object.assign(Object.assign({ headers }, (this.httpsAgent && { httpsAgent: this.httpsAgent })), (this.adapter && { adapter: this.adapter }));
        if ((options === null || options === void 0 ? void 0 : options.followRedirects) === false) {
            config.maxRedirects = 0;
        }
        return config;
    }
    convertTunnelForRequest(tunnel) {
        var _a;
        const convertedTunnel = {
            tunnelId: tunnel.tunnelId,
            name: tunnel.name,
            domain: tunnel.domain,
            description: tunnel.description,
            labels: tunnel.labels,
            options: tunnel.options,
            customExpiration: tunnel.customExpiration,
            accessControl: !tunnel.accessControl
                ? undefined
                : { entries: tunnel.accessControl.entries.filter((ace) => !ace.isInherited) },
            endpoints: tunnel.endpoints,
            ports: (_a = tunnel.ports) === null || _a === void 0 ? void 0 : _a.map((p) => this.convertTunnelPortForRequest(tunnel, p)),
        };
        return convertedTunnel;
    }
    convertTunnelPortForRequest(tunnel, tunnelPort) {
        if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
            throw new Error('Tunnel port cluster ID does not match tunnel.');
        }
        if (tunnelPort.tunnelId && tunnel.tunnelId && tunnelPort.tunnelId !== tunnel.tunnelId) {
            throw new Error('Tunnel port tunnel ID does not match tunnel.');
        }
        return {
            portNumber: tunnelPort.portNumber,
            protocol: tunnelPort.protocol,
            isDefault: tunnelPort.isDefault,
            description: tunnelPort.description,
            labels: tunnelPort.labels,
            sshUser: tunnelPort.sshUser,
            options: tunnelPort.options,
            accessControl: !tunnelPort.accessControl
                ? undefined
                : { entries: tunnelPort.accessControl.entries.filter((ace) => !ace.isInherited) },
        };
    }
    tunnelRequestOptionsToQueryString(options, additionalQuery) {
        const queryOptions = {};
        const queryItems = [];
        if (options) {
            if (options.includePorts) {
                queryOptions.includePorts = ['true'];
            }
            if (options.includeAccessControl) {
                queryOptions.includeAccessControl = ['true'];
            }
            if (options.tokenScopes) {
                dev_tunnels_contracts_1.TunnelAccessControl.validateScopes(options.tokenScopes, undefined, true);
                queryOptions.tokenScopes = options.tokenScopes;
            }
            if (options.forceRename) {
                queryOptions.forceRename = ['true'];
            }
            if (options.labels) {
                queryOptions.labels = options.labels;
                if (options.requireAllLabels) {
                    queryOptions.allLabels = ['true'];
                }
            }
            if (options.limit) {
                queryOptions.limit = [options.limit.toString()];
            }
            queryItems.push(...Object.keys(queryOptions).map((key) => {
                const value = queryOptions[key];
                return `${key}=${value.map(encodeURIComponent).join(',')}`;
            }));
        }
        if (additionalQuery) {
            queryItems.push(additionalQuery);
        }
        queryItems.push(`api-version=${this.apiVersion}`);
        const queryString = queryItems.join('&');
        return queryString;
    }
    /**
     * Axios request that can be overridden for unit tests purposes.
     * @param config axios request config
     * @param _cancellation the cancellation token for the request (used by unit tests to simulate timeouts).
     */
    async axiosRequest(config, _cancellation) {
        return await axios_1.default.request(config);
    }
    /**
     * Makes an HTTP request using Axios, while tracing request and response details.
     */
    async request(method, uri, data, config, allowNotFound, cancellation) {
        var _a, _b;
        this.trace(`${method} ${uri}`);
        if (config.headers) {
            this.traceHeaders(config.headers);
        }
        this.traceContent(data);
        const traceResponse = (response) => {
            this.trace(`${response.status} ${response.statusText}`);
            this.traceHeaders(response.headers);
            this.traceContent(response.data);
        };
        let disposable;
        const abortController = new AbortController();
        let timeout = undefined;
        const newAbortSignal = () => {
            if (cancellation === null || cancellation === void 0 ? void 0 : cancellation.isCancellationRequested) {
                abortController.abort('Cancelled: CancellationToken cancel requested.');
            }
            else if (cancellation) {
                disposable = cancellation.onCancellationRequested(() => abortController.abort('Cancelled: CancellationToken cancel requested.'));
            }
            else {
                timeout = setTimeout(() => abortController.abort('Cancelled: default request timeout reached.'), defaultRequestTimeoutMS);
            }
            return abortController.signal;
        };
        try {
            config.url = uri;
            config.method = method;
            config.data = data;
            config.signal = newAbortSignal();
            config.timeout = defaultRequestTimeoutMS;
            const response = await this.axiosRequest(config, cancellation);
            traceResponse(response);
            // This assumes that TResult is always boolean for DELETE requests.
            return (method === 'DELETE' ? true : response.data);
        }
        catch (e) {
            if (!(e instanceof Error) || !e.isAxiosError)
                throw e;
            const requestError = e;
            if (requestError.response) {
                traceResponse(requestError.response);
                if (allowNotFound && requestError.response.status === 404) {
                    return (method === 'DELETE' ? false : null);
                }
            }
            requestError.message = this.getResponseErrorMessage(requestError, abortController.signal);
            // Axios errors have too much redundant detail! Delete some of it.
            delete requestError.request;
            if (requestError.response) {
                (_a = requestError.config) === null || _a === void 0 ? true : delete _a.httpAgent;
                (_b = requestError.config) === null || _b === void 0 ? true : delete _b.httpsAgent;
                delete requestError.response.request;
            }
            throw requestError;
        }
        finally {
            if (timeout) {
                clearTimeout(timeout);
            }
            disposable === null || disposable === void 0 ? void 0 : disposable.dispose();
        }
    }
    traceHeaders(headers) {
        for (const [headerName, headerValue] of Object.entries(headers)) {
            if (headerName === 'Authorization') {
                this.traceAuthorizationHeader(headerName, headerValue);
                return;
            }
            this.trace(`${headerName}: ${headerValue !== null && headerValue !== void 0 ? headerValue : ''}`);
        }
    }
    traceAuthorizationHeader(key, value) {
        if (typeof value !== 'string')
            return;
        const spaceIndex = value.indexOf(' ');
        if (spaceIndex < 0) {
            this.trace(`${key}: [${value.length}]`);
            return;
        }
        const scheme = value.substring(0, spaceIndex);
        const token = value.substring(spaceIndex + 1);
        if (scheme.toLowerCase() === tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel.toLowerCase()) {
            const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
            if (tokenProperties) {
                this.trace(`${key}: ${scheme} <${tokenProperties}>`);
                return;
            }
        }
        this.trace(`${key}: ${scheme} <token>`);
    }
    traceContent(data) {
        if (typeof data === 'object') {
            data = JSON.stringify(data, undefined, '  ');
        }
        if (typeof data === 'string') {
            this.trace(TunnelManagementHttpClient.replaceTokensInContent(data));
        }
    }
    static replaceTokensInContent(content) {
        var _a;
        const tokenRegex = /"(eyJ[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+)"/;
        let match = tokenRegex.exec(content);
        while (match) {
            let token = match[1];
            const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
            token = (_a = tokenProperties === null || tokenProperties === void 0 ? void 0 : tokenProperties.toString()) !== null && _a !== void 0 ? _a : 'token';
            content =
                content.substring(0, match.index + 1) +
                    '<' +
                    token +
                    '>' +
                    content.substring(match.index + match[0].length - 1);
            match = tokenRegex.exec(content);
        }
        return content;
    }
    /**
     * Disposes the client and any background tasks.
     */
    async dispose() {
        this.isDisposed = true;
        // Resolving the events-available completion will cause the events processing task
        // to exit after processing any remaining already-queued events.
        this.eventsAvailableCompletion.resolve();
        if (this.eventsPromise) {
            await this.eventsPromise;
            this.eventsPromise = null;
        }
    }
}
exports.TunnelManagementHttpClient = TunnelManagementHttpClient;
//# sourceMappingURL=https://github.com/VSCodium/sourcemaps/releases/download/stable-4c0b0c6cc561d2d3636d1ec250935431876ce4dc/node_modules-@microsoft-dev-tunnels-management-tunnelManagementHttpClient.js.map