UNPKG

@bs-core/shell

Version:
4,441 lines 175 kB
import * as fs from 'node:fs';
import * as util from 'node:util';
import { performance } from 'node:perf_hooks';
import * as http from 'node:http';
import * as crypto from 'node:crypto';
import * as zlib from 'node:zlib';
import * as streams from 'node:stream/promises';
import * as stream from 'node:stream';
import { PassThrough } from 'node:stream';
import * as fsPromises from 'node:fs/promises';
import * as path from 'node:path';
import * as https from 'node:https';
import * as os from 'node:os';
import * as readline from 'node:readline';

/**
 * Config manager module. Provides functions to retrieve config values from various sources like
 * CLI, environment variables, and env file. Includes utility functions to handle config value
 * lookup, type conversion, error handling etc.
 */
// imports here
// Config consts here
// The env var that contains the name of the .env file
const CFG_ENV_FILE = "ENV_FILE";
// The env var that contains the name of the cfg file
const CFG_CFG_FILE = "CFG_FILE";
// Private variables here
// Stores the parsed contents of the .env file as key-value pairs
let _envFileStore;
// Stores the parsed contents of the cfg file as an object
let _cfgFileStore;
// Stores the messages generated during configuration.
// NOTE: This is used because the configuration manager is used by Logger
// and it becomes a chicken/egg situation when initialising the Logger
let _messageStore;
/**
 * Enumeration of supported configuration value types.
 * Can be used when retrieving a config value to specify the expected type.
 */
var ConfigType;
(function (ConfigType) {
    ConfigType["String"] = "String";
    ConfigType["Number"] = "Number";
    ConfigType["Boolean"] = "Boolean";
    ConfigType["Object"] = "Object";
    ConfigType["Array"] = "Array";
})(ConfigType || (ConfigType = {}));
/**
 * Represents an error that occurred while retrieving a config value
 */
class ConfigError {
    message;
    constructor(message) {
        this.message = message;
    }
}
// Private methods here
/**
 * Converts a string value to the specified configuration type.
 *
 * NOTE: This is not used for the config files so we dont check
 * for Object or Array types.
 *
 * @param value - The string value to convert.
 * @param type - The expected configuration type.
 * @returns The converted value as a number, string, or boolean.
 */
function convertValue(value, type) {
    //Check the type
    switch (type) {
        case ConfigType.Number:
            return parseInt(value);
        case ConfigType.Boolean:
            // Only accept Y or TRUE (case insensitive) to mean true
            if (value.toUpperCase() === "Y" || value.toUpperCase() === "TRUE") {
                return true;
            }
            // Everything else is false
            return false;
        default:
            // All that is left is String and this is already a string!
            return value;
    }
}
/**
 * Checks the command line arguments for a configuration value matching the
 * specified configuration key.
 *
 * @param config - The configuration key to look for in the command line arguments.
 * @param type - The expected type of the configuration value.
 * @param options - Additional options for configuring the behavior of the function.
 * @returns The configuration value from the command line arguments, converted to the specified type, or `null` if the configuration value is not found.
 */
function checkCli(config, type, options) {
    // Ignore the first 2 params (node bin and executable file)
    let cliParams = process.argv.slice(2);
    // The convention used for config params on the command line is:
    // Convert to lowercase, replace '_' with '-' and prepend "--"
    let cliParam = `--${config.toLowerCase().replaceAll("_", "-")}`;
    // Command line flags are just prepended with a '-'
    let cmdLineFlag = options.cmdLineFlag !== undefined ? `-${options.cmdLineFlag}` : "";
    // If the param is assigned a value on the cli it has the format:
    //   --param=value
    // otherwise the format is and it implies true:
    //   --parm or -flag
    let regExp;
    if (options.cmdLineFlag === undefined) {
        // No flag specified so only look for the param and an assigned value
        regExp = new RegExp(`^${cliParam}=(.+)$`);
    }
    else {
        // Look for param and an assigned value or cmd line flag
        regExp = new RegExp(`^${cliParam}=(.+)$|^${cliParam}$|^${cmdLineFlag}$`);
    }
    let value;
    // Step through each cli params until you find a match
    for (let i = 0; i < cliParams.length; i++) {
        let match = cliParams[i].match(regExp);
        let paramOrFlag;
        if (match === null) {
            // There was no match so look at the next param
            continue;
        }
        // If a value was supplied then match[1] will contain a value
        if (match[1] !== undefined) {
            paramOrFlag = match[0];
            value = match[1];
        }
        else {
            paramOrFlag = match[0];
            // The presence of the flag/param without a value implies a true value
            value = "Y";
        }
        // Check if we can or should log that we found it
        // NOTE: If we log it we want to indicate is was found on the CLI
        if (!options.silent) {
            _messageStore.add(`CLI parameter/flag (${paramOrFlag}) = (${options.redact ? "redacted" : value})`);
        }
        // We are done so break out of the loop
        break;
    }
    // Return null if we have no value
    if (value === undefined) {
        return null;
    }
    return convertValue(value, type);
}
/**
 * Retrieves a configuration value from an environment variable.
 *
 * @param config - The configuration key to retrieve from the environment.
 * @param type - The expected type of the configuration value.
 * @param options - Additional options for configuring the behavior of the function.
 * @returns The configuration value converted to the specified type, or `null` if the environment variable is not set.
 */
function checkEnvVar(config, type, options) {
    // NOTE: Always convert to upper case for env vars
    let evar = config.toUpperCase();
    let value = process.env[evar];
    // Return null if we have no value
    if (value === undefined) {
        return null;
    }
    // If we are here then we found it, now lets check if we can or should
    // log that we found it
    // NOTE: If we log it we want to indicate is was found in an env var
    if (!options.silent) {
        _messageStore.add(`Env var (${evar}) = (${options.redact ? "redacted" : value})`);
    }
    return convertValue(value, type);
}
/**
 * Retrieves a configuration value from an environment file store.
 *
 * @param config - The configuration key to retrieve from the environment file.
 * @param type - The expected type of the configuration value.
 * @param options - Additional options for configuring the behavior of the function.
 * @returns The configuration value converted to the specified type, or `null` if the configuration is not found in the environment file.
 */
function checkEnvFile(config, type, options) {
    // NOTE: Always convert to upper case when checking the env file store
    let evar = config.toUpperCase();
    let value = _envFileStore.get(evar);
    // Return null if we have no value
    if (value === undefined) {
        return null;
    }
    // If we are here then we found it, now lets check if we can or should
    // log that we found it
    // NOTE: If we log it we want to indicate it was found in the env file
    if (!options.silent) {
        _messageStore.add(`Env var from env file (${evar}) = (${options.redact ? "redacted" : value})`);
    }
    return convertValue(value, type);
}
/**
 * Retrieves a configuration value from a configuration file store.
 *
 * @param config - The configuration key to retrieve from the configuration file.
 * @param options - Additional options for configuring the behavior of the function.
 * @returns The configuration value, or `null` if the configuration is not found in the configuration file.
 */
function checkCfgFile(config, options) {
    let value = _cfgFileStore.get(config);
    // Return null if we have no value
    if (value === undefined) {
        return null;
    }
    // If we are here then we found it, now lets check if we can or should
    // log that we found it
    // NOTE: If we log it we want to indicate it was found in the cfg file
    if (!options.silent) {
        _messageStore.add(`Config from cfg file (${config}) = (${options.redact ? "redacted" : JSON.stringify(value)})`);
    }
    // No need for any convertions, just return the value
    return value;
}
/**
 * Retrieves a configuration value from various sources, with the following precedence:
 * 1. Command-line arguments
 * 2. Environment variables
 * 3. Environment file (.env)
 * 4. Configuration file
 *
 * If the configuration value is not found in any of these sources, a default value can be provided.
 *
 * @param config - The configuration key to retrieve.
 * @param type - The expected type of the configuration value.
 * @param defaultVal - The default value to use if the configuration is not found.
 * @param configOptions - Additional options for configuring the behavior of the function.
 * @returns The configuration value, or the default value if the configuration is not found.
 * @throws {ConfigError} If the configuration is required and not found.
 */
function get(config, type, defaultVal, configOptions) {
    // Set up the defaults if not provided
    let options = {
        silent: false,
        redact: false,
        ...configOptions,
    };
    // Check the CLI first, i.e. CLI has higher precedence then env vars
    // of the cfg file
    let value = checkCli(config, type, options);
    if (value !== null) {
        return value;
    }
    // OK it's not in the CLI so lets check the env vars, env var has higher
    // precedence then the .env file
    value = checkEnvVar(config, type, options);
    if (value !== null) {
        return value;
    }
    // OK it's not in the env vars either so check the env file store
    value = checkEnvFile(config, type, options);
    if (value !== null) {
        return value;
    }
    // OK it's not in the env file store either so check the cfg store
    value = checkCfgFile(config, options);
    if (value !== null) {
        return value;
    }
    // If we are here then the value was not found - use default provided
    // NOTE: The default SHOULD have the correct type so do not do a conversion
    if (defaultVal === undefined) {
        // If the default was not provided then the config WAS required. In this
        // scenario we need to throw an error
        throw new ConfigError(`Config parameter (${config}) not found!`);
    }
    // Lets check if we can or should log the default value
    // NOTE: If we log it we want to indicate is the default value
    if (!options.silent) {
        _messageStore.add(`Default value used for (${config}) = (${options.redact ? "redacted" : defaultVal})`);
    }
    return defaultVal;
}
/**
 * Reads the contents of the specified .env file and adds the key-value
 * pairs to the _envFileStore.
 *
 * @param envFile - The path to the .env file to read.
 * @throws {ConfigError} If an error occurs while reading the .env file.
 */
function parseEnvFile(envFile) {
    let lines = [];
    try {
        _messageStore.add(`Reading config info from .env file (${envFile})`);
        // Read env file and split it into lines ...
        let contents = fs.readFileSync(envFile, "utf8");
        // ... makes sure if works for DOS and linux files!
        lines = contents.split(/\r?\n/);
    }
    catch (e) {
        throw new ConfigError(`The following error occured when trying to open the .env file (${envFile}) - (${e})`);
    }
    // Iterate through each line
    for (let line of lines) {
        // If the line is commented out or blank then skip it
        if (line.length === 0 || line.startsWith("#")) {
            continue;
        }
        // Don't use split() here because the value may contain an "="
        let index = line.indexOf("=");
        // Check if there was an equal in the line - if not then skip this line
        if (index === -1) {
            continue;
        }
        // Get the key/value pair - make sure to trim them as well
        let key = line.slice(0, index).trim();
        let value = line.slice(index + 1).trim();
        // Check if the value is delimited with single or double quotes
        if ((value.startsWith('"') && value.endsWith('"')) ||
            (value.startsWith("'") && value.endsWith("'"))) {
            // Strip them away
            value = value.slice(1, value.length - 1);
        }
        // Stick it in the env file store
        // NOTE: Make key upper case to match env vars conventions
        _envFileStore.set(key.toUpperCase(), value);
        _messageStore.add(`Added (${key.toUpperCase()}) to the env file store`);
    }
}
/**
 * Reads the contents of a cfg file and adds the key-value pairs to the
 * configuration store.
 *
 * @param cfgFile - The path to the configuration file to read.
 * @throws {ConfigError} If an error occurs while reading or parsing the configuration file.
 */
function readCfgFile(cfgFile) {
    let contents;
    try {
        _messageStore.add(`Reading config info from cfg file (${cfgFile})`);
        // Read the cfg file
        contents = fs.readFileSync(cfgFile, "utf8");
    }
    catch (e) {
        throw new ConfigError(`The following error occured when trying to open the cfg file (${cfgFile}) - (${e})`);
    }
    try {
        _messageStore.add("Adding the cfg file contents to the cfg store");
        _cfgFileStore = new Map(Object.entries(JSON.parse(contents)));
    }
    catch (e) {
        throw new ConfigError(`The following error occured when trying to add (${contents}) to the cfg store - (${e})`);
    }
}
/**
 * Initializes the configuration manager by setting up the necessary stores and
 * parsing any specified environment and configuration files.
 *
 * The function first initializes the `_envFileStore`, `_cfgFileStore`, and
 * `_messageStore` stores. It then checks if a `.env` file has been specified
 * in the configuration and, if so, calls the `parseEnvFile` function to parse
 * the contents of the file and add the key-value pairs to the `_envFileStore`.
 *
 * Next, the function checks if a configuration file has been specified in the
 * configuration and, if so, calls the `readCfgFile` function to read the
 * contents of the file and add the key-value pairs to the `_cfgFileStore`.
 *
 * This function is typically called during the initialization of the
 * application to ensure that the configuration manager is properly set up and
 * ready to use.
 */
function init$1() {
    // Initialise the stores
    _envFileStore = new Map();
    _cfgFileStore = new Map();
    _messageStore = new Set();
    // Check if the user has specified a .env file
    let envFile = configMan.getStr(CFG_ENV_FILE, "");
    if (envFile.length > 0) {
        parseEnvFile(envFile);
    }
    else {
        _messageStore.add("No .env file specified");
    }
    // Check if the user has specified a cfg file
    // NOTE: The cfg file config CAN be specified in the .env file since it
    // has already been parsed
    let cfgFile = configMan.getStr(CFG_CFG_FILE, "");
    if (cfgFile.length > 0) {
        readCfgFile(cfgFile);
    }
    else {
        _messageStore.add("No cfg file specified");
    }
}
// Public methods here
/**
 * Provides a set of functions for retrieving configuration values from various
 * sources, including environment variables and configuration files.
 *
 * The `configMan` object is a frozen object that contains the following methods:
 *
 * - `getStr(config: string, defaultVal?: string, options?: ConfigOptions): string`
 *   - Retrieves a string configuration value with a default value if not set.
 * - `getBool(config: string, defaultVal?: boolean, options?: ConfigOptions): boolean`
 *   - Retrieves a boolean configuration value with a default value if not set.
 * - `getNum(config: string, defaultVal?: number, options?: ConfigOptions): number`
 *   - Retrieves a number configuration value with a default value if not set.
 * - `getMessages(): IterableIterator<[string, string]>`
 *   - Retrieves an iterator over the key-value pairs of the message store.
 * - `clearMessages(): void`
 *   - Clears all messages stored in the message store.`
 *
 * NOTE: Freezing the object prevents modifications to the exported API.
 */
const configMan = Object.freeze({
    /**
     * Retrieves a string configuration value with a default value if not set.
     *
     * @param config - The name of the config parameter to retrieve.
     * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
     * @param configOptions - The config options.
     * @returns The string configuration value, or the default value if not set.
     */
    getStr: (config, defaultVal, options) => {
        return get(config, ConfigType.String, defaultVal, options);
    },
    /**
     * Retrieves a boolean configuration value with a default value if not set.
     *
     * @param config - The name of the config parameter to retrieve.
     * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
     * @param configOptions - The config options.
     * @returns The boolean configuration value, or the default value if not set.
     */
    getBool: (config, defaultVal, options) => {
        return get(config, ConfigType.Boolean, defaultVal, options);
    },
    /**
     * Retrieves a number configuration value with a default value if not set.
     *
     * @param config - The name of the config parameter to retrieve.
     * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
     * @param configOptions - The config options.
     * @returns The number configuration value, or the default value if not set.
     */
    getNum: (config, defaultVal, options) => {
        return get(config, ConfigType.Number, defaultVal, options);
    },
    /**
     * Retrieves an object configuration value with a default value if not set.
     *
     * @param config - The name of the config parameter to retrieve.
     * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
     * @param configOptions - The config options.
     * @returns The object configuration value, or the default value if not set.
     */
    getObject: (config, defaultVal, options) => {
        return get(config, ConfigType.Object, defaultVal, options);
    },
    /**
     * Retrieves an array configuration value with a default value if not set.
     *
     * @param config - The name of the config parameter to retrieve.
     * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
     * @param configOptions - The config options.
     * @returns The array configuration value, or the default value if not set.
     */
    getArray: (config, defaultVal, options) => {
        return get(config, ConfigType.Array, defaultVal, options);
    },
    /**
     * Retrieves an iterator over the key-value pairs of the message store.
     *
     * @returns An iterator over the key-value pairs of the message store.
     */
    getMessages: () => {
        return _messageStore.entries();
    },
    /**
     * Clears all messages stored in the message store.
     */
    clearMessages: () => {
        _messageStore.clear();
    },
});
// Time to kick this puppy!
init$1();

// imports here
// Config consts here
const CFG_LOG_LEVEL = "LOG_LEVEL";
const CFG_LOG_TIMESTAMP = "LOG_TIMESTAMP";
const CFG_LOG_TIMESTAMP_LOCALE = "LOG_TIMESTAMP_LOCALE";
const CFG_LOG_TIMESTAMP_TZ = "LOG_TIMESTAMP_TZ";
// Types here
var LogLevel;
(function (LogLevel) {
    LogLevel[LogLevel["COMPLETE_SILENCE"] = 0] = "COMPLETE_SILENCE";
    LogLevel[LogLevel["QUIET"] = 100] = "QUIET";
    LogLevel[LogLevel["INFO"] = 200] = "INFO";
    LogLevel[LogLevel["START_UP"] = 250] = "START_UP";
    LogLevel[LogLevel["DEBUG"] = 300] = "DEBUG";
    LogLevel[LogLevel["TRACE"] = 400] = "TRACE";
})(LogLevel || (LogLevel = {}));
// Logger class here
class Logger {
    // Private properties here
    _name;
    _timestamp;
    _timestampLocale;
    _timestampTz;
    _logLevel;
    // Private methods here
    /**
     * Generates a timestamp string to prefix log messages.
     * Returns an empty string if timestamps are disabled. Otherwise returns
     * the formatted timestamp string.
     */
    timestamp() {
        // If we are not supposed to generate timestamps then return nothing
        if (!this._timestamp) {
            return "";
        }
        let now = new Date();
        if (this._timestampLocale === "ISO") {
            // Make sure to add a trailing space!
            return `${now.toISOString()} `;
        }
        // Make sure to add a trailing space!
        return `${now.toLocaleString(this._timestampLocale, {
            timeZone: this._timestampTz,
            year: "numeric",
            month: "2-digit",
            day: "2-digit",
            hour: "2-digit",
            minute: "2-digit",
            second: "2-digit",
            hour12: false,
            fractionalSecondDigits: 3,
        })} `;
    }
    convertLevel(level) {
        let logLevel;
        switch (level.toUpperCase()) {
            case "": // This is in case it is not set
                logLevel = LogLevel.INFO;
                break;
            case "SILENT":
                logLevel = LogLevel.COMPLETE_SILENCE;
                break;
            case "QUIET":
                logLevel = LogLevel.QUIET;
                break;
            case "INFO":
                logLevel = LogLevel.INFO;
                break;
            case "STARTUP":
                logLevel = LogLevel.START_UP;
                break;
            case "DEBUG":
                logLevel = LogLevel.DEBUG;
                break;
            case "TRACE":
                logLevel = LogLevel.TRACE;
                break;
            default:
                throw new Error(`Log Level (${level}) is unknown.`);
        }
        return logLevel;
    }
    // constructor here
    constructor(name) {
        this._name = name;
        this._timestamp = configMan.getBool(CFG_LOG_TIMESTAMP, false);
        this._timestampLocale = configMan.getStr(CFG_LOG_TIMESTAMP_LOCALE, "ISO");
        this._timestampTz = configMan.getStr(CFG_LOG_TIMESTAMP_TZ, "UTC");
        this._logLevel = this.convertLevel(configMan.getStr(CFG_LOG_LEVEL, ""));
        // Now get the messages from the confgiMan for display
        let messages = configMan.getMessages();
        for (const message of messages) {
            this.startupMsg("Logger", message[0]);
        }
        configMan.clearMessages();
    }
    fatal(...args) {
        // fatals are always logged
        let msg = util.format(`${this.timestamp()}FATAL: ${this._name}: ${args[0]}`, ...args.slice(1));
        console.error(msg);
    }
    error(...args) {
        // errors are always logged unless level = LOG_COMPLETE_SILENCE
        if (this._logLevel > LogLevel.COMPLETE_SILENCE) {
            let msg = util.format(`${this.timestamp()}ERROR: ${this._name}: ${args[0]}`, ...args.slice(1));
            console.error(msg);
        }
    }
    warn(...args) {
        // warnings are always logged unless level = LOG_COMPLETE_SILENCE
        if (this._logLevel > LogLevel.COMPLETE_SILENCE) {
            let msg = util.format(`${this.timestamp()}WARN: ${this._name}: ${args[0]}`, ...args.slice(1));
            console.warn(msg);
        }
    }
    info(...args) {
        if (this._logLevel >= LogLevel.INFO) {
            let msg = util.format(`${this.timestamp()}INFO: ${this._name}: ${args[0]}`, ...args.slice(1));
            console.info(msg);
        }
    }
    startupMsg(...args) {
        if (this._logLevel >= LogLevel.START_UP) {
            let msg = util.format(`${this.timestamp()}STARTUP: ${this._name}: ${args[0]}`, ...args.slice(1));
            console.info(msg);
        }
    }
    shutdownMsg(...args) {
        if (this._logLevel >= LogLevel.START_UP) {
            let msg = util.format(`${this.timestamp()}SHUTDOWN: ${this._name}: ${args[0]}`, ...args.slice(1));
            console.info(msg);
        }
    }
    debug(...args) {
        if (this._logLevel >= LogLevel.DEBUG) {
            let msg = util.format(`${this.timestamp()}DEBUG: ${this._name}: ${args[0]}`, ...args.slice(1));
            console.info(msg);
        }
    }
    trace(...args) {
        if (this._logLevel >= LogLevel.TRACE) {
            let msg = util.format(`${this.timestamp()}TRACE: ${this._name}: ${args[0]}`, ...args.slice(1));
            console.info(msg);
        }
    }
    force(...args) {
        // forces are always logged even if level == LOG_COMPLETE_SILENCE
        let msg = util.format(`${this.timestamp()}FORCED: ${this._name}: ${args[0]}`, ...args.slice(1));
        console.error(msg);
    }
    setLevel(level) {
        this._logLevel = level;
    }
}

// NOTE: To use this with endpoints using self signed certs add this env var
// NODE_TLS_REJECT_UNAUTHORIZED=0
// imports here
// Misc consts here
const LOG_TAG = "request";
// Module private variables here
const _logger$1 = new Logger(LOG_TAG);
// Error classes here
class ReqAborted {
    timedOut;
    message;
    constructor(timedOut, message) {
        this.timedOut = timedOut;
        this.message = message;
    }
}
class ReqError {
    status;
    message;
    constructor(status, message) {
        this.status = status;
        this.message = message;
    }
}
// Private methods here
async function callFetch(origin, path, options, body) {
    // Build the url
    let url = `${origin}${path}`;
    // And add the query string if one has been provided
    if (options.searchParams !== undefined) {
        url += `?${new URLSearchParams(options.searchParams)}`;
    }
    let timeoutTimer;
    // Create an AbortController if a timeout has been provided
    if (options.timeout) {
        const controller = new AbortController();
        // NOTE: this will overwrite a signal if one has been provided
        options.signal = controller.signal;
        timeoutTimer = setTimeout(() => {
            controller.abort();
        }, options.timeout * 1000);
    }
    let results = await fetch(url, {
        method: options.method,
        headers: options.headers,
        body,
        keepalive: options.keepalive,
        cache: options.cache,
        credentials: options.credentials,
        mode: options.mode,
        redirect: options.redirect,
        referrer: options.referrer,
        referrerPolicy: options.referrerPolicy,
        signal: options.signal,
    }).catch((e) => {
        // Check if the request was aborted
        if (e.name === "AbortError") {
            // If timeout was set then the req must have timed out
            if (options.timeout) {
                throw new ReqAborted(true, `Request timeout out after ${options.timeout} seconds`);
            }
            throw new ReqAborted(false, "Request aborted");
        }
        // Need to check if we started a timeout
        if (timeoutTimer !== undefined) {
            clearTimeout(timeoutTimer);
        }
        // We don't know what the error is so pass it back
        throw e;
    });
    // Need to check if we started a timeout
    if (timeoutTimer !== undefined) {
        clearTimeout(timeoutTimer);
    }
    // We will throw an error if the response is not 2XX
    if (!results.ok) {
        let message = await results.text();
        throw new ReqError(results.status, message.length === 0 ? results.statusText : message);
    }
    return results;
}
async function handleResponseData(results) {
    // No point worrying if the body is JSON at first, because we know its text
    const body = await results.text();
    // If the body exists then check if it is JSON
    if (body.length > 0) {
        // Check if the content type is JSON
        const contentType = results.headers.get("content-type");
        if (contentType?.startsWith("application/json")) {
            return JSON.parse(body);
        }
    }
    // If we are here, the body wasnt JSON so just return the text
    return body;
}
// Public methods here
let request = async (origin, path, reqOptions) => {
    // We need to remember the start time
    const startTime = performance.now();
    _logger$1.trace("Request for origin (%s) path (%s)", origin, path);
    // Set the default values
    let options = {
        method: "GET",
        timeout: 0,
        keepalive: true,
        handleResponse: true,
        cache: "no-store",
        mode: "cors",
        credentials: "include",
        redirect: "follow",
        referrerPolicy: "no-referrer",
        ...reqOptions,
    };
    // Make sure the headers is set to something for later
    if (options.headers === undefined) {
        options.headers = {};
    }
    // If a bearer token is provided then add a Bearer auth header
    if (options.bearerToken !== undefined) {
        options.headers.Authorization = `Bearer ${options.bearerToken}`;
    }
    // If the basic auth creds are provided add a Basic auth header
    if (options.auth !== undefined) {
        let token = Buffer.from(`${options.auth.username}:${options.auth.password}`).toString("base64");
        options.headers.Authorization = `Basic ${token}`;
    }
    let payloadBody;
    // Automatically stringify and set the header if this is a JSON payload
    // BUT dont do it for GETs and DELETE since they can have no body
    if (options.body !== undefined &&
        options.method !== "GET" &&
        options.method !== "DELETE") {
        // Rem an array is an object to!
        if (typeof options.body === "object") {
            // Add the content-type if it hasn't been provided
            if (options.headers?.["content-type"] === undefined) {
                options.headers["content-type"] = "application/json; charset=utf-8";
            }
            payloadBody = JSON.stringify(options.body);
        }
        else {
            payloadBody = options.body;
        }
    }
    // Call fetch
    let response = await callFetch(origin, path, options, payloadBody);
    // Build the response
    let res = {
        statusCode: response.status,
        headers: response.headers,
        body: undefined, // set to undefined for now
        responseTime: 0,
    };
    // Check if we should handle the response for the user
    if (options.handleResponse) {
        // Yes, so handle and set the body
        res.body = await handleResponseData(response).catch((e) => {
            const msg = `Error handling response data for (${origin}) (${path}) - (${e}))`;
            throw new Error(msg);
        });
    }
    else {
        // No, so set the response
        res.response = response;
    }
    // Don't forget to set the response time
    res.responseTime = Math.round(performance.now() - startTime);
    return res;
};

// Classes here
class HttpError {
    status;
    message;
    constructor(status, message = "Achtung Baby!") {
        this.status = status;
        this.message = message;
    }
}
class HttpRedirect {
    statusCode;
    location;
    message;
    constructor(statusCode = 302, location, message = "") {
        this.statusCode = statusCode;
        this.location = location;
        this.message = message;
    }
}
class ServerRequest extends http.IncomingMessage {
    // Properties here
    urlObj;
    params;
    middlewareProps;
    sseServer;
    json;
    body;
    matchedInfo;
    dontCompressResponse;
    // Constructor here
    constructor(socket) {
        super(socket);
        // When this object is instantiated the body of the req has not yet been
        // received so the details, such as the URL, will not be known until later
        this.urlObj = new URL("http://localhost/");
        this.params = {};
        this.middlewareProps = {};
        this.dontCompressResponse = false;
    }
    getCookie = (cookieName) => {
        // Get the cookie header and spilt it up by cookies -
        // NOTE: cookies are separated by semi colons
        let cookies = this.headers.cookie?.split(";");
        if (cookies === undefined) {
            // Nothing to do so just return
            return null;
        }
        // Loop through the cookies
        for (let cookie of cookies) {
            // Split the cookie up into a key value pair
            // NOTE: key/value is separated by an equals sign and has leading spaces
            let [name, value] = cookie.trim().split("=");
            // Make sure it was a validly formatted cookie
            if (value === undefined) {
                // It is not a valid cookie so skip it
                continue;
            }
            // Check if we found the cookie
            if (name === cookieName) {
                // Return the cookie value
                return value;
            }
        }
        return null;
    };
    setServerTimingHeader = (value) => {
        this.headers["Server-Timing"] = value;
    };
}
class ServerResponse extends http.ServerResponse {
    // Properties here
    _receiveTime;
    _redirected;
    _latencyMetricName;
    _serverTimingsMetrics;
    json;
    body;
    proxied;
    // constructor here
    constructor(req) {
        super(req);
        // NOTE: This will be created at the same time as ServerRequest
        this._receiveTime = performance.now();
        this._redirected = false;
        this._latencyMetricName = "latency";
        this._serverTimingsMetrics = [];
        this.proxied = false;
    }
    // Getter methods here
    get redirected() {
        return this._redirected;
    }
    // Setter methods here
    set latencyMetricName(name) {
        this._latencyMetricName = name;
    }
    // Public functions here
    redirect(location, statusCode = 302, message = "") {
        this._redirected = true;
        let htmlMessage = message.length > 0
            ? message
            : `Redirected to <a href="${location}">here</a>`;
        // Write a little something something for good measure
        this.body = `
    <html>
      <body>
        <p>${htmlMessage}</p>
      </body>
    </html>`;
        this.setHeader("Content-Type", "text/html; charset=utf-8");
        this.setHeader("Location", location);
        this.statusCode = statusCode;
    }
    setCookies = (cookies) => {
        let setCookiesValue = [];
        // Check for exiting cookies and add them to the setCookiesValue array
        let existing = this.getHeader("Set-Cookie");
        if (typeof existing === "string") {
            setCookiesValue.push(existing);
        }
        else if (Array.isArray(existing)) {
            setCookiesValue = existing;
        }
        // Loop through each cookie and build the cookie values
        for (let cookie of cookies) {
            // Set the cookie value first
            let value = `${cookie.name}=${cookie.value}`;
            // if there is a maxAge then set it - NOTE: put ";" first
            if (cookie.maxAge !== undefined) {
                value += `; Max-Age=${cookie.maxAge}`;
            }
            // If there is a path then set it or use default path of "/" - NOTE: put ";" first
            if (cookie.path !== undefined) {
                value += `; Path=${cookie.path}`;
            }
            else {
                value += `; Path=/`;
            }
            // If httpOnly is indicated then add it - NOTE: put ";" first
            if (cookie.httpOnly === true) {
                value += "; HttpOnly";
            }
            // If secure is indicated set then add it - NOTE: put ";" first
            if (cookie.secure === true) {
                value += "; Secure";
            }
            // If sameSite has been provided then add it - NOTE: put ";" first
            if (cookie.sameSite !== undefined) {
                value += `; SameSite=${cookie.sameSite}`;
            }
            // If domain has been provided then add it - NOTE: put ";" first
            if (cookie.domain !== undefined) {
                value += `; Domain=${cookie.domain}`;
            }
            // Save the cookie
            setCookiesValue.push(value);
        }
        // Finally set the cookie/s in the response header
        this.setHeader("Set-Cookie", setCookiesValue);
    };
    clearCookies = (cookies) => {
        let httpCookies = [];
        for (let cookie of cookies) {
            // To clear a cookie - set value to empty string and max age to -1
            httpCookies.push({ name: cookie, value: "", maxAge: -1 });
        }
        this.setCookies(httpCookies);
    };
    setServerTimingHeader = () => {
        let serverTimingHeaders = [];
        // Check if the req has a Server-Timing header. This is not normal but I
        // want to something like a forwardAuth server to be able to add it's
        // metrics to the response header
        if (this?.req?.headers["server-timing"] !== undefined) {
            const reqTimings = this.req.headers["server-timing"];
            // Check if there are multiple headers
            if (Array.isArray(reqTimings)) {
                // If so then since this is the first just use it as the headers array
                serverTimingHeaders = reqTimings;
            }
            else {
                // If not then just add it to the array
                serverTimingHeaders.push(reqTimings);
            }
        }
        let serverTimingValue = "";
        // Add each additional metric added to the res next so they are in
        // the order they were added
        for (let metric of this._serverTimingsMetrics) {
            // Check if we have a string or a metric object
            if (typeof metric === "string") {
                // The string version is already formatted so just add to the array
                serverTimingHeaders.push(metric);
                continue;
            }
            // If we are here then we have a metric object so add the name
            serverTimingValue += metric.name;
            // Check if there is an optional duration
            if (metric.duration !== undefined) {
                serverTimingValue += `;dur=${metric.duration}`;
            }
            // Check if there is an optional description
            if (metric.description !== undefined) {
                serverTimingValue += `;desc="${metric.description}"`;
            }
            serverTimingValue += ", ";
        }
        // Finally add the total latency for the endpoint to the array
        const latency = Math.round(performance.now() - this._receiveTime);
        serverTimingValue += `${this._latencyMetricName};dur=${latency}`;
        serverTimingHeaders.push(serverTimingValue);
        // Of course don't forget to set the header!!
        this.setHeader("Server-Timing", serverTimingHeaders);
    };
    addServerTimingMetric = (name, duration, description) => {
        // This adds a metric to the Server-Timing header for this response
        this._serverTimingsMetrics.push({ name, duration, description });
    };
    addServerTimingHeader = (header) => {
        // This adds a complete Server-Timing header to this response
        this._serverTimingsMetrics.push(header);
    };
}

const contentTypes = {
    //   "123": "application/vnd.lotus-1-2-3",
    //   "1km": "application/vnd.1000minds.decision-model+xml",
    //   "3dml": "text/vnd.in3d.3dml",
    //   "3ds": "image/x-3ds",
    //   "3g2": "video/3gpp2",
    //   "3gp": "video/3gpp",
    //   "3gpp": "video/3gpp",
    //   "3mf": "model/3mf",
    "7z": "application/x-7z-compressed",
    //   "disposition-notification": "message/disposition-notification",
    //   "n-gage": "application/vnd.nokia.n-gage.symbian.install",
    //   "sfd-hdstx": "application/vnd.hydrostatix.sof-data",
    //   "vbox-extpack": "application/x-virtualbox-vbox-extpack",
    //   aab: "application/x-authorware-bin",
    //   aac: "audio/x-aac",
    //   aam: "application/x-authorware-map",
    //   aas: "application/x-authorware-seg",
    //   abw: "application/x-abiword",
    //   ac: "application/vnd.nokia.n-gage.ac+xml",
    //   acc: "application/vnd.americandynamics.acc",
    //   ace: "application/x-ace-compressed",
    //   acu: "application/vnd.acucobol",
    //   acutc: "application/vnd.acucorp",
    //   adp: "audio/adpcm",
    //   adts: "audio/aac",
    //   aep: "application/vnd.audiograph",
    //   afm: "application/x-font-type1",
    //   afp: "application/vnd.ibm.modcap",
    //   age: "application/vnd.age",
    //   ahead: "application/vnd.ahead.space",
    //   ai: "application/postscript",
    //   aif: "audio/x-aiff",
    //   aifc: "audio/x-aiff",
    //   aiff: "audio/x-aiff",
    //   air: "application/vnd.adobe.air-application-installer-package+zip",
    //   ait: "application/vnd.dvb.ait",
    //   ami: "application/vnd.amiga.ami",
    //   aml: "application/automationml-aml+xml",
    //   amlx: "application/automationml-amlx+zip",
    //   amr: "audio/amr",
    //   apk: "application/vnd.android.package-archive",
    //   apng: "image/apng",
    //   appcache: "text/cache-manifest",
    //   appinstaller: "application/appinstaller",
    //   application: "application/x-ms-application",
    //   appx: "application/appx",
    //   appxbundle: "application/appxbundle",
    //   apr: "application/vnd.lotus-approach",
    //   arc: "application/x-freearc",
    //   arj: "application/x-arj",
    //   asc: "application/pgp-signature",
    //   asf: "video/x-ms-asf",
    //   asm: "text/x-asm",
    //   aso: "application/vnd.accpac.simply.aso",
    //   asx: "video/x-ms-asf",
    //   atc: "application/vnd.acucorp",
    //   atom: "application/atom+xml",
    //   atomcat: "application/atomcat+xml",
    //   atomdeleted: "application/atomdeleted+xml",
    //   atomsvc: "application/atomsvc+xml",
    //   atx: "application/vnd.antix.game-component",
    //   au: "audio/basic",
    //   avci: "image/avci",
    //   avcs: "image/avcs",
    //   avi: "video/x-msvideo",
    //   avif: "image/avif",
    //   aw: "application/applixware",
    //   azf: "application/vnd.airzip.filesecure.azf",
    //   azs: "application/vnd.airzip.filesecure.azs",
    //   azv: "image/vnd.airzip.accelerator.azv",
    //   azw: "application/vnd.amazon.ebook",
    //   b16: "image/vnd.pco.b16",
    //   bat: "application/x-msdownload",
    //   bcpio: "application/x-bcpio",
    //   bdf: "application/x-font-bdf",
    //   bdm: "application/vnd.syncml.dm+wbxml",
    //   bdoc: "application/x-bdoc",
    //   bed: "application/vnd.realvnc.bed",
    //   bh2: "application/vnd.fujitsu.oasysprs",
    //   bin: "application/octet-stream",
    //   blb: "application/x-blorb",
    //   blorb: "application/x-blorb",
    //   bmi: "application/vnd.bmi",
    //   bmml: "application/vnd.balsamiq.bmml+xml",
    bmp: "image/x-ms-bmp",
    //   book: "application/vnd.framemaker",
    //   box: "application/vnd.previewsystems.box",
    //   boz: "application/x-bzip2",
    //   bpk: "application/octet-stream",
    //   bsp: "model/vnd.valve.source.compiled-map",
    //   btf: "image/prs.btif",
    //   btif: "image/prs.btif",
    //   buffer: "application/octet-stream",
    //   bz2: "application/x-bzip2",
    //   bz: "application/x-bzip",
    //   c11amc: "application/vnd.cluetrust.cartomobile-config",
    //   c11amz: "application/vnd.cluetrust.cartomobile-config-pkg",
    //   c4d: "application/vnd.clonk.c4group",
    //   c4f: "application/vnd.clonk.c4group",
    //   c4g: "application/vnd.clonk.c4group",
    //   c4p: "application/vnd.clonk.c4group",
    //   c4u: "application/vnd.clonk.c4group",
    //   c: "text/x-c",
    //   cab: "application/vnd.ms-cab-compressed",
    //   caf: "audio/x-caf",
    //   cap: "application/vnd.tcpdump.pcap",
    //   car: "application/vnd.curl.car",
    //   cat: "application/vnd.ms-pki.seccat",
    //   cb7: "application/x-cbr",
    //   cba: "application/x-cbr",
    //   cbr: "application/x-cbr",
    //   cbt: "application/x-cbr",
    //   cbz: "application/x-cbr",
    //   cc: "text/x-c",
    //   cco: "application/x-cocoa",
    //   cct: "application/x-director",
    //   ccxml: "application/ccxml+xml",
    //   cdbcmsg: "application/vnd.contact.cmsg",
    //   cdf: "application/x-netcdf",
    //   cdfx: "application/cdfx+xml",
    //   cdkey: "application/vnd.mediastation.cdkey",
    //   cdmia: "application/cdmi-capability",
    //   cdmic: "application/cdmi-container",
    //   cdmid: "application/cdmi-domain",
    //   cdmio: "application/cdmi-object",
    //   cdmiq: "application/cdmi-queue",
    //   cdx: "chemical/x-cdx",
    //   cdxml: "application/vnd.chemdraw+xml",
    //   cdy: "application/vnd.cinderella",
    //   cer: "application/pkix-cert",
    //   cfs: "application/x-cfs-compressed",
    //   cgm: "image/cgm",
    //   chat: "application/x-chat",
    //   chm: "application/vnd.ms-htmlhelp",
    //   chrt: "application/vnd.kde.kchart",
    //   cif: "chemical/x-cif",
    //   cii: "application/vnd.anser-web-certificate-issue-initiation",
    //   cil: "application/vnd.ms-artgalry",
    //   cjs: "application/node",
    //   cla: "application/vnd.claymore",
    //   class: "application/java-vm",
    //   cld: "model/vnd.cld",
    //   clkk: "application/vnd.crick.clicker.keyboard",
    //   clkp: "application/vnd.crick.clicker.palette",
    //   clkt: "application/vnd.crick.clicker.template",
    //   clkw: "application/vnd.crick.clicker.wordbank",
    //   clkx: "application/vnd.crick.clicker",
    //   clp: "application/x-msclip",
    //   cmc: "application/vnd.cosmocaller",
    //   cmdf: "chemical/x-cmdf",
    //   cml: "chemical/x-cml",
    //   cmp: "application/vnd.yellowriver-custom-menu",
    //   cmx: "image/x-cmx",
    //   cod: "application/vnd.rim.cod",
    //   coffee: "text/coffeescript",
    //   com: "application/x-msdownload",
    //   conf: "text/plain",
    //   cpio: "application/x-cpio",
    //   cpl: "application/cpl+xml",
    //   cpp: "text/x-c",
    //   cpt: "application/mac-compactpro",
    //   crd: "application/x-mscardfile",
    //   crl: "application/pkix-crl",
    //   crt: "application/x-x509-ca-cert",
    //   crx: "application/x-chrome-extension",
    //   cryptonote: "application/vnd.rig.cryptonote",
    //   csh: "application/x-csh",
    //   csl: "application/vnd.citationstyles.style+xml",
    //   csml: "chemical/x-csml",
    //   csp: "application/vnd.commonspace",
    css: "text/css",
    //   cst: "application/x-director",
    csv: "text/csv",
    //   cu: "application/cu-seeme",
    //   curl: "text/vnd.curl",
    //   cwl: "application/cwl",
    //   cww: "application/prs.cww",
    //   cxt: "application/x-director",
    //   cxx: "text/x-c",
    //   dae: "model/vnd.collada+xml",
    //   daf: "application/vnd.mobius.daf",
    //   dart: "application/vnd.dart",
    //   dataless: "application/vnd.fdsn.seed",
    //   davmount: "application/davmount+xml",
    //   dbf: "application/vnd.dbf",
    //   dbk: "application/docbook+xml",
    //   dcr: "application/x-director",
    //   dcurl: "text/vnd.curl.dcurl",
    //   dd2: "application/vnd.oma.dd2+xml",
    //   ddd: "application/vnd.fujixerox.ddd",
    //   ddf: "application/vnd.syncml.dmddf+xml",
    //   dds: "image/vnd.ms-dds",
    //   deb: "application/x-debian-package",
    //   def: "text/plain",
    //   deploy: "application/octet-stream",
    //   der: "application/x-x509-ca-cert",
    //   dfac: "application/vnd.dreamfactory",
    //   dgc: "application/x-dgc-compressed",
    //   dib: "image/bmp",
    //   dic: "text/x-c",
    //   dir: "application/x-director",
    //   dis: "application/vnd.mobius.dis",
    //   dist: "application/octet-stream",
    //   distz: "application/octet-stream",
    //   djv: "image/vnd.djvu",
    //   djvu: "image/vnd.djvu",
    //   dll: "application/x-msdownload",
    //   dmg: "application/x-apple-diskimage",
    //   dmp: "application/vnd.tcpdump.pcap",
    //   dms: "application/octet-stream",
    //   dna: "application/vnd.dna",
    doc: "application/msword",
    docm: "application/vnd.ms-word.document.macroenabled.12",
    docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    dot: "application/msword",
    dotm: "application/vnd.ms-word.template.macroenabled.12",
    dotx: "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
    //   dp: "application/vnd.osgi.dp",
    //   dpg: "application/vnd.dpgraph",
    //   dpx: "image/dpx",
    //   dra: "audio/vnd.dra",
    //   drle: "image/dicom-rle",
    //   dsc: "text/prs.lines.tag",
    //   dssc: "application/dssc+der",
    //   dtb: "application/x-dtbook+xml",
    //   dtd: "application/xml-dtd",
    //   dts: "audio/vnd.dts",
    //   dtshd: "audio/vnd.dts.hd",
    //   dump: "application/octet-stream",
    //   dvb: "video/vnd.dvb.file",
    //   dvi: "application/x-dvi",
    //   dwd: "application/atsc-dwd+xml",
    //   dwf: "model/vnd.dwf",
    //   dwg: "image/vnd.dwg",
    //   dxf: "image/vnd.dxf",
    //   dxp: "application/vnd.spotfire.dxp",
    //   dxr: "application/x-director",
    //   ear: "application/java-archive",
    //   ecelp4800: "audio/vnd.nuera.ecelp4800",
    //   ecelp7470: "audio/vnd.nuera.ecelp7470",
    //   ecelp9600: "audio/vnd.nuera.ecelp9600",
    //   ecma: "application/ecmascript",
    //   edm: "application/vnd.novadigm.edm",
    //   edx: "application/vnd.novadigm.edx",
    //   efif: "application/vnd.picsel",
    //   ei6: "application/vnd.pg.osasli",
    //   elc: "application/octet-stream",
    //   emf: "image/emf",
    //   eml: "message/rfc822",
    //   emma: "application/emma+xml",
    //   emotionml: "application/emotionml+xml",
    //   emz: "application/x-msmetafile",
    //   eol: "audio/vnd.digital-winds",
    eot: "application/vnd.ms-fontobject",
    //   eps: "application/postscript",
    //   epub: "application/epub+zip",
    //   es3: "application/vnd.eszigno3+xml",
    //   esa: "application/vnd.osgi.subsystem",
    //   esf: "application/vnd.epson.esf",
    //   et3: "application/vnd.eszigno3+xml",
    //   etx: "text/x-setext",
    //   eva: "application/x-eva",
    //   evy: "application/x-envoy",
    //   exe: "application/x-msdownload",
    //   exi: "application/exi",
    //   exp: "application/express",
    //   exr: "image/aces",
    //   ext: "application/vnd.novadigm.ext",
    //   ez2: "application/vnd.ezpix-album",
    //   ez3: "application/vnd.ezpix-package",
    //   ez: "application/andrew-inset",
    //   f4v: "video/x-f4v",
    //   f77: "text/x-fortran",
    //   f90: "text/x-fortran",
    //   f: "text/x-fortran",
    //   fbs: "image/vnd.fastbidsheet",
    //   fcdt: "application/vnd.adobe.formscentral.fcdt",
    //   fcs: "application/vnd.isac.fcs",
    //   fdf: "application/vnd.fdf",
    //   fdt: "application/fdt+xml",
    //   fe_launch: "application/vnd.denovo.fcselayout-link",
    //   fg5: "application/vnd.fujitsu.oasysgp",
    //   fgd: "application/x-director",
    //   fh4: "image/x-freehand",
    //   fh5: "image/x-freehand",
    //   fh7: "image/x-freehand",
    //   fh: "image/x-freehand",
    //   fhc: "image/x-freehand",
    //   fig: "application/x-xfig",
    //   fits: "image/fits",
    //   flac: "audio/x-flac",
    //   fli: "video/x-fli",
    //   flo: "application/vnd.micrografx.flo",
    //   flv: "video/x-flv",
    //   flw: "application/vnd.kde.kivio",
    //   flx: "text/vnd.fmi.flexstor",
    //   fly: "text/vnd.fly",
    //   fm: "application/vnd.framemaker",
    //   fnc: "application/vnd.frogans.fnc",
    //   fo: "application/vnd.software602.filler.form+xml",
    //   for: "text/x-fortran",
    //   fpx: "image/vnd.fpx",
    //   frame: "application/vnd.framemaker",
    //   fsc: "application/vnd.fsc.weblaunch",
    //   fst: "image/vnd.fst",
    //   ftc: "application/vnd.fluxtime.clip",
    //   fti: "application/vnd.anser-web-funds-transfer-initiation",
    //   fvt: "video/vnd.fvt",
    //   fxp: "application/vnd.adobe.fxp",
    //   fxpl: "application/vnd.adobe.fxp",
    //   fzs: "application/vnd.fuzzysheet",
    //   g2w: "application/vnd.geoplan",
    //   g3: "image/g3fax",
    //   g3w: "application/vnd.geospace",
    //   gac: "application/vnd.groove-account",
    //   gam: "application/x-tads",
    //   gbr: "application/rpki-ghostbusters",
    //   gca: "application/x-gca-compressed",
    //   gdl: "model/vnd.gdl",
    //   gdoc: "application/vnd.google-apps.document",
    //   ged: "text/vnd.familysearch.gedcom",
    //   geo: "application/vnd.dynageo",
    //   geojson: "application/geo+json",
    //   gex: "application/vnd.geometry-explorer",
    //   ggb: "application/vnd.geogebra.file",
    //   ggt: "application/vnd.geogebra.tool",
    //   ghf: "application/vnd.groove-help",
    gif: "image/gif",
    //   gim: "application/vnd.groove-identity-message",
    //   glb: "model/gltf-binary",
    //   gltf: "model/gltf+json",
    //   gml: "application/gml+xml",
    //   gmx: "application/vnd.gmx",
    //   gnumeric: "application/x-gnumeric",
    //   gph: "application/vnd.flographit",
    //   gpx: "application/gpx+xml",
    //   gqf: "application/vnd.grafeq",
    //   gqs: "application/vnd.grafeq",
    //   gram: "application/srgs",
    //   gramps: "application/x-gramps-xml",
    //   gre: "application/vnd.geometry-explorer",
    //   grv: "application/vnd.groove-injector",
    //   grxml: "application/srgs+xml",
    //   gsf: "application/x-font-ghostscript",
    gsheet: "application/vnd.google-apps.spreadsheet",
    gslides: "application/vnd.google-apps.presentation",
    //   gtar: "application/x-gtar",
    //   gtm: "application/vnd.groove-tool-message",
    //   gtw: "model/vnd.gtw",
    //   gv: "text/vnd.graphviz",
    //   gxf: "application/gxf",
    //   gxt: "application/vnd.geonext",
    gz: "application/gzip",
    //   h261: "video/h261",
    //   h263: "video/h263",
    //   h264: "video/h264",
    //   h: "text/x-c",
    //   hal: "application/vnd.hal+xml",
    //   hbci: "application/vnd.hbci",
    //   hbs: "text/x-handlebars-template",
    //   hdd: "application/x-virtualbox-hdd",
    //   hdf: "application/x-hdf",
    //   heic: "image/heic",
    //   heics: "image/heic-sequence",
    //   heif: "image/heif",
    //   heifs: "image/heif-sequence",
    //   hej2: "image/hej2k",
    //   held: "application/atsc-held+xml",
    //   hh: "text/x-c",
    //   hjson: "application/hjson",
    //   hlp: "application/winhlp",
    //   hpgl: "application/vnd.hp-hpgl",
    //   hpid: "application/vnd.hp-hpid",
    //   hps: "application/vnd.hp-hps",
    //   hqx: "application/mac-binhex40",
    //   hsj2: "image/hsj2",
    //   htc: "text/x-component",
    //   htke: "application/vnd.kenameaapp",
    htm: "text/html",
    html: "text/html",
    //   hvd: "application/vnd.yamaha.hv-dic",
    //   hvp: "application/vnd.yamaha.hv-voice",
    //   hvs: "application/vnd.yamaha.hv-script",
    //   i2g: "application/vnd.intergeo",
    //   icc: "application/vnd.iccprofile",
    //   ice: "x-conference/x-cooltalk",
    //   icm: "application/vnd.iccprofile",
    ico: "image/x-icon",
    //   ics: "text/calendar",
    //   ief: "image/ief",
    //   ifb: "text/calendar",
    //   ifm: "application/vnd.shana.informed.formdata",
    //   iges: "model/iges",
    //   igl: "application/vnd.igloader",
    //   igm: "application/vnd.insors.igm",
    //   igs: "model/iges",
    //   igx: "application/vnd.micrografx.igx",
    //   iif: "application/vnd.shana.informed.interchange",
    img: "application/octet-stream",
    //   imp: "application/vnd.accpac.simply.imp",
    //   ims: "application/vnd.ms-ims",
    //   in: "text/plain",
    //   ini: "text/plain",
    //   ink: "application/inkml+xml",
    //   inkml: "application/inkml+xml",
    //   install: "application/x-install-instructions",
    //   iota: "application/vnd.astraea-software.iota",
    //   ipfix: "application/ipfix",
    //   ipk: "application/vnd.shana.informed.package",
    //   irm: "application/vnd.ibm.rights-management",
    //   irp: "application/vnd.irepository.package+xml",
    //   iso: "application/x-iso9660-image",
    //   itp: "application/vnd.shana.informed.formtemplate",
    //   its: "application/its+xml",
    //   ivp: "application/vnd.immervision-ivp",
    //   ivu: "application/vnd.immervision-ivu",
    //   jad: "text/vnd.sun.j2me.app-descriptor",
    //   jade: "text/jade",
    //   jam: "application/vnd.jam",
    //   jar: "application/java-archive",
    //   jardiff: "application/x-java-archive-diff",
    //   java: "text/x-java-source",
    //   jhc: "image/jphc",
    //   jisp: "application/vnd.jisp",
    //   jls: "image/jls",
    //   jlt: "application/vnd.hp-jlyt",
    //   jng: "image/x-jng",
    //   jnlp: "application/x-java-jnlp-file",
    //   joda: "application/vnd.joost.joda-archive",
    jp2: "image/jp2",
    jpe: "image/jpeg",
    jpeg: "image/jpeg",
    //   jpf: "image/jpx",
    jpg2: "image/jp2",
    jpg: "image/jpeg",
    //   jpgm: "video/jpm",
    //   jpgv: "video/jpeg",
    //   jph: "image/jph",
    //   jpm: "video/jpm",
    jpx: "image/jpx",
    js: "text/javascript",
    json5: "application/json5",
    json: "application/json",
    //   jsonld: "application/ld+json",
    //   jsonml: "application/jsonml+json",
    //   jsx: "text/jsx",
    //   jt: "model/jt",
    //   jxr: "image/jxr",
    //   jxra: "image/jxra",
    //   jxrs: "image/jxrs",
    //   jxs: "image/jxs",
    //   jxsc: "image/jxsc",
    //   jxsi: "image/jxsi",
    //   jxss: "image/jxss",
    //   kar: "audio/midi",
    //   karbon: "application/vnd.kde.karbon",
    //   kdbx: "application/x-keepass2",
    //   key: "application/x-iwork-keynote-sffkey",
    //   kfo: "application/vnd.kde.kformula",
    //   kia: "application/vnd.kidspiration",
    //   kml: "application/vnd.google-earth.kml+xml",
    //   kmz: "application/vnd.google-earth.kmz",
    //   kne: "application/vnd.kinar",
    //   knp: "application/vnd.kinar",
    //   kon: "application/vnd.kde.kontour",
    //   kpr: "application/vnd.kde.kpresenter",
    //   kpt: "application/vnd.kde.kpresenter",
    //   kpxx: "application/vnd.ds-keypoint",
    //   ksp: "application/vnd.kde.kspread",
    //   ktr: "application/vnd.kahootz",
    //   ktx2: "image/ktx2",
    //   ktx: "image/ktx",
    //   ktz: "application/vnd.kahootz",
    //   kwd: "application/vnd.kde.kword",
    //   kwt: "application/vnd.kde.kword",
    //   lasxml: "application/vnd.las.las+xml",
    //   latex: "application/x-latex",
    //   lbd: "application/vnd.llamagraphics.life-balance.desktop",
    //   lbe: "application/vnd.llamagraphics.life-balance.exchange+xml",
    //   les: "application/vnd.hhe.lesson-player",
    //   less: "text/less",
    //   lgr: "application/lgr+xml",
    //   lha: "application/x-lzh-compressed",
    //   link66: "application/vnd.route66.link66+xml",
    //   list3820: "application/vnd.ibm.modcap",
    //   list: "text/plain",
    //   listafp: "application/vnd.ibm.modcap",
    //   litcoffee: "text/coffeescript",
    //   lnk: "application/x-ms-shortcut",
    log: "text/plain",
    //   lostxml: "application/lost+xml",
    //   lrf: "application/octet-stream",
    //   lrm: "application/vnd.ms-lrm",
    //   ltf: "application/vnd.frogans.ltf",
    //   lua: "text/x-lua",
    //   luac: "application/x-lua-bytecode",
    //   lvp: "audio/vnd.lucent.voice",
    //   lwp: "application/vnd.lotus-wordpro",
    //   lzh: "application/x-lzh-compressed",
    //   m13: "application/x-msmediaview",
    //   m14: "application/x-msmediaview",
    //   m1v: "video/mpeg",
    //   m21: "application/mp21",
    //   m2a: "audio/mpeg",
    //   m2v: "video/mpeg",
    //   m3a: "audio/mpeg",
    //   m3u8: "application/vnd.apple.mpegurl",
    //   m3u: "audio/x-mpegurl",
    //   m4a: "audio/x-m4a",
    //   m4p: "application/mp4",
    //   m4s: "video/iso.segment",
    //   m4u: "video/vnd.mpegurl",
    //   m4v: "video/x-m4v",
    //   ma: "application/mathematica",
    //   mads: "application/mads+xml",
    //   maei: "application/mmt-aei+xml",
    //   mag: "application/vnd.ecowin.chart",
    //   maker: "application/vnd.framemaker",
    //   man: "text/troff",
    //   manifest: "text/cache-manifest",
    //   map: "application/json",
    //   mar: "application/octet-stream",
    //   markdown: "text/markdown",
    //   mathml: "application/mathml+xml",
    //   mb: "application/mathematica",
    //   mbk: "application/vnd.mobius.mbk",
    //   mbox: "application/mbox",
    //   mc1: "application/vnd.medcalcdata",
    //   mcd: "application/vnd.mcd",
    //   mcurl: "text/vnd.curl.mcurl",
    md: "text/markdown",
    //   mdb: "application/x-msaccess",
    //   mdi: "image/vnd.ms-modi",
    //   mdx: "text/mdx",
    //   me: "text/troff",
    //   mesh: "model/mesh",
    //   meta4: "application/metalink4+xml",
    //   metalink: "application/metalink+xml",
    //   mets: "application/mets+xml",
    //   mfm: "application/vnd.mfmp",
    //   mft: "application/rpki-manifest",
    //   mgp: "application/vnd.osgeo.mapguide.package",
    //   mgz: "application/vnd.proteus.magazine",
    //   mid: "audio/midi",
    //   midi: "audio/midi",
    //   mie: "application/x-mie",
    //   mif: "application/vnd.mif",
    //   mime: "message/rfc822",
    //   mj2: "video/mj2",
    //   mjp2: "video/mj2",
    //   mjs: "text/javascript",
    //   mk3d: "video/x-matroska",
    //   mka: "audio/x-matroska",
    //   mkd: "text/x-markdown",
    //   mks: "video/x-matroska",
    //   mkv: "video/x-matroska",
    //   mlp: "application/vnd.dolby.mlp",
    //   mmd: "application/vnd.chipnuts.karaoke-mmd",
    //   mmf: "application/vnd.smaf",
    //   mml: "text/mathml",
    //   mmr: "image/vnd.fujixerox.edmics-mmr",
    //   mng: "video/x-mng",
    //   mny: "application/x-msmoney",
    //   mobi: "application/x-mobipocket-ebook",
    //   mods: "application/mods+xml",
    //   mov: "video/quicktime",
    //   movie: "video/x-sgi-movie",
    //   mp21: "application/mp21",
    //   mp2: "audio/mpeg",
    //   mp2a: "audio/mpeg",
    //   mp3: "audio/mpeg",
    //   mp4: "video/mp4",
    //   mp4a: "audio/mp4",
    //   mp4s: "application/mp4",
    //   mp4v: "video/mp4",
    //   mpc: "application/vnd.mophun.certificate",
    //   mpd: "application/dash+xml",
    //   mpe: "video/mpeg",
    //   mpeg: "video/mpeg",
    //   mpf: "application/media-policy-dataset+xml",
    //   mpg4: "video/mp4",
    //   mpg: "video/mpeg",
    //   mpga: "audio/mpeg",
    //   mpkg: "application/vnd.apple.installer+xml",
    //   mpm: "application/vnd.blueice.multipass",
    //   mpn: "application/vnd.mophun.application",
    //   mpp: "application/vnd.ms-project",
    //   mpt: "application/vnd.ms-project",
    //   mpy: "application/vnd.ibm.minipay",
    //   mqy: "application/vnd.mobius.mqy",
    //   mrc: "application/marc",
    //   mrcx: "application/marcxml+xml",
    //   ms: "text/troff",
    //   mscml: "application/mediaservercontrol+xml",
    //   mseed: "application/vnd.fdsn.mseed",
    //   mseq: "application/vnd.mseq",
    //   msf: "application/vnd.epson.msf",
    //   msg: "application/vnd.ms-outlook",
    //   msh: "model/mesh",
    //   msi: "application/x-msdownload",
    //   msix: "application/msix",
    //   msixbundle: "application/msixbundle",
    //   msl: "application/vnd.mobius.msl",
    //   msm: "application/octet-stream",
    //   msp: "application/octet-stream",
    //   msty: "application/vnd.muvee.style",
    //   mtl: "model/mtl",
    //   mts: "model/vnd.mts",
    //   mus: "application/vnd.musician",
    //   musd: "application/mmt-usd+xml",
    //   musicxml: "application/vnd.recordare.musicxml+xml",
    //   mvb: "application/x-msmediaview",
    //   mvt: "application/vnd.mapbox-vector-tile",
    //   mwf: "application/vnd.mfer",
    //   mxf: "application/mxf",
    //   mxl: "application/vnd.recordare.musicxml",
    //   mxmf: "audio/mobile-xmf",
    //   mxml: "application/xv+xml",
    //   mxs: "application/vnd.triscape.mxs",
    //   mxu: "video/vnd.mpegurl",
    //   n3: "text/n3",
    //   nb: "application/mathematica",
    //   nbp: "application/vnd.wolfram.player",
    //   nc: "application/x-netcdf",
    //   ncx: "application/x-dtbncx+xml",
    //   nfo: "text/x-nfo",
    //   ngdat: "application/vnd.nokia.n-gage.data",
    //   nitf: "application/vnd.nitf",
    //   nlu: "application/vnd.neurolanguage.nlu",
    //   nml: "application/vnd.enliven",
    //   nnd: "application/vnd.noblenet-directory",
    //   nns: "application/vnd.noblenet-sealer",
    //   nnw: "application/vnd.noblenet-web",
    //   npx: "image/vnd.net-fpx",
    //   nq: "application/n-quads",
    //   nsc: "application/x-conference",
    //   nsf: "application/vnd.lotus-notes",
    //   nt: "application/n-triples",
    //   ntf: "application/vnd.nitf",
    //   numbers: "application/x-iwork-numbers-sffnumbers",
    //   nzb: "application/x-nzb",
    //   oa2: "application/vnd.fujitsu.oasys2",
    //   oa3: "application/vnd.fujitsu.oasys3",
    //   oas: "application/vnd.fujitsu.oasys",
    //   obd: "application/x-msbinder",
    //   obgx: "application/vnd.openblox.game+xml",
    //   obj: "model/obj",
    //   oda: "application/oda",
    //   odb: "application/vnd.oasis.opendocument.database",
    //   odc: "application/vnd.oasis.opendocument.chart",
    //   odf: "application/vnd.oasis.opendocument.formula",
    //   odft: "application/vnd.oasis.opendocument.formula-template",
    //   odg: "application/vnd.oasis.opendocument.graphics",
    //   odi: "application/vnd.oasis.opendocument.image",
    //   odm: "application/vnd.oasis.opendocument.text-master",
    //   odp: "application/vnd.oasis.opendocument.presentation",
    //   ods: "application/vnd.oasis.opendocument.spreadsheet",
    //   odt: "application/vnd.oasis.opendocument.text",
    //   oga: "audio/ogg",
    //   ogex: "model/vnd.opengex",
    //   ogg: "audio/ogg",
    //   ogv: "video/ogg",
    //   ogx: "application/ogg",
    //   omdoc: "application/omdoc+xml",
    //   onepkg: "application/onenote",
    //   onetmp: "application/onenote",
    //   onetoc2: "application/onenote",
    //   onetoc: "application/onenote",
    //   opf: "application/oebps-package+xml",
    //   opml: "text/x-opml",
    //   oprc: "application/vnd.palm",
    //   opus: "audio/ogg",
    //   org: "text/x-org",
    //   osf: "application/vnd.yamaha.openscoreformat",
    //   osfpvg: "application/vnd.yamaha.openscoreformat.osfpvg+xml",
    //   osm: "application/vnd.openstreetmap.data+xml",
    //   otc: "application/vnd.oasis.opendocument.chart-template",
    otf: "font/otf",
    //   otg: "application/vnd.oasis.opendocument.graphics-template",
    //   oth: "application/vnd.oasis.opendocument.text-web",
    //   oti: "application/vnd.oasis.opendocument.image-template",
    //   otp: "application/vnd.oasis.opendocument.presentation-template",
    //   ots: "application/vnd.oasis.opendocument.spreadsheet-template",
    //   ott: "application/vnd.oasis.opendocument.text-template",
    //   ova: "application/x-virtualbox-ova",
    //   ovf: "application/x-virtualbox-ovf",
    //   owl: "application/rdf+xml",
    //   oxps: "application/oxps",
    //   oxt: "application/vnd.openofficeorg.extension",
    //   p10: "application/pkcs10",
    //   p12: "application/x-pkcs12",
    //   p7b: "application/x-pkcs7-certificates",
    //   p7c: "application/pkcs7-mime",
    //   p7m: "application/pkcs7-mime",
    //   p7r: "application/x-pkcs7-certreqresp",
    //   p7s: "application/pkcs7-signature",
    //   p8: "application/pkcs8",
    //   p: "text/x-pascal",
    //   pac: "application/x-ns-proxy-autoconfig",
    //   pages: "application/x-iwork-pages-sffpages",
    //   pas: "text/x-pascal",
    //   paw: "application/vnd.pawaafile",
    //   pbd: "application/vnd.powerbuilder6",
    //   pbm: "image/x-portable-bitmap",
    //   pcap: "application/vnd.tcpdump.pcap",
    //   pcf: "application/x-font-pcf",
    //   pcl: "application/vnd.hp-pcl",
    //   pclxl: "application/vnd.hp-pclxl",
    //   pct: "image/x-pict",
    //   pcurl: "application/vnd.curl.pcurl",
    //   pcx: "image/x-pcx",
    //   pdb: "application/x-pilot",
    //   pde: "text/x-processing",
    pdf: "application/pdf",
    //   pem: "application/x-x509-ca-cert",
    //   pfa: "application/x-font-type1",
    //   pfb: "application/x-font-type1",
    //   pfm: "application/x-font-type1",
    //   pfr: "application/font-tdpfr",
    //   pfx: "application/x-pkcs12",
    //   pgm: "image/x-portable-graymap",
    //   pgn: "application/x-chess-pgn",
    //   pgp: "application/pgp-encrypted",
    //   php: "application/x-httpd-php",
    //   pic: "image/x-pict",
    //   pkg: "application/octet-stream",
    //   pki: "application/pkixcmp",
    //   pkipath: "application/pkix-pkipath",
    //   pkpass: "application/vnd.apple.pkpass",
    //   pl: "application/x-perl",
    //   plb: "application/vnd.3gpp.pic-bw-large",
    //   plc: "application/vnd.mobius.plc",
    //   plf: "application/vnd.pocketlearn",
    //   pls: "application/pls+xml",
    //   pm: "application/x-perl",
    //   pml: "application/vnd.ctc-posml",
    png: "image/png",
    //   pnm: "image/x-portable-anymap",
    //   portpkg: "application/vnd.macports.portpkg",
    pot: "application/vnd.ms-powerpoint",
    potm: "application/vnd.ms-powerpoint.template.macroenabled.12",
    potx: "application/vnd.openxmlformats-officedocument.presentationml.template",
    ppam: "application/vnd.ms-powerpoint.addin.macroenabled.12",
    //   ppd: "application/vnd.cups-ppd",
    //   ppm: "image/x-portable-pixmap",
    pps: "application/vnd.ms-powerpoint",
    ppsm: "application/vnd.ms-powerpoint.slideshow.macroenabled.12",
    ppsx: "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
    ppt: "application/vnd.ms-powerpoint",
    pptm: "application/vnd.ms-powerpoint.presentation.macroenabled.12",
    pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    //   pqa: "application/vnd.palm",
    //   prc: "model/prc",
    //   pre: "application/vnd.lotus-freelance",
    //   prf: "application/pics-rules",
    //   provx: "application/provenance+xml",
    //   ps: "application/postscript",
    //   psb: "application/vnd.3gpp.pic-bw-small",
    psd: "image/vnd.adobe.photoshop",
    //   psf: "application/x-font-linux-psf",
    //   pskcxml: "application/pskc+xml",
    //   pti: "image/prs.pti",
    //   ptid: "application/vnd.pvi.ptid1",
    //   pub: "application/x-mspublisher",
    //   pvb: "application/vnd.3gpp.pic-bw-var",
    //   pwn: "application/vnd.3m.post-it-notes",
    //   pya: "audio/vnd.ms-playready.media.pya",
    //   pyo: "model/vnd.pytha.pyox",
    //   pyox: "model/vnd.pytha.pyox",
    //   pyv: "video/vnd.ms-playready.media.pyv",
    //   qam: "application/vnd.epson.quickanime",
    //   qbo: "application/vnd.intu.qbo",
    //   qfx: "application/vnd.intu.qfx",
    //   qps: "application/vnd.publishare-delta-tree",
    //   qt: "video/quicktime",
    //   qwd: "application/vnd.quark.quarkxpress",
    //   qwt: "application/vnd.quark.quarkxpress",
    //   qxb: "application/vnd.quark.quarkxpress",
    //   qxd: "application/vnd.quark.quarkxpress",
    //   qxl: "application/vnd.quark.quarkxpress",
    //   qxt: "application/vnd.quark.quarkxpress",
    //   ra: "audio/x-realaudio",
    //   ram: "audio/x-pn-realaudio",
    //   raml: "application/raml+yaml",
    //   rapd: "application/route-apd+xml",
    //   rar: "application/x-rar-compressed",
    //   ras: "image/x-cmu-raster",
    //   rcprofile: "application/vnd.ipunplugged.rcprofile",
    //   rdf: "application/rdf+xml",
    //   rdz: "application/vnd.data-vision.rdz",
    //   relo: "application/p2p-overlay+xml",
    //   rep: "application/vnd.businessobjects",
    //   res: "application/x-dtbresource+xml",
    //   rgb: "image/x-rgb",
    //   rif: "application/reginfo+xml",
    //   rip: "audio/vnd.rip",
    //   ris: "application/x-research-info-systems",
    //   rl: "application/resource-lists+xml",
    //   rlc: "image/vnd.fujixerox.edmics-rlc",
    //   rld: "application/resource-lists-diff+xml",
    //   rm: "application/vnd.rn-realmedia",
    //   rmi: "audio/midi",
    //   rmp: "audio/x-pn-realaudio-plugin",
    //   rms: "application/vnd.jcp.javame.midlet-rms",
    //   rmvb: "application/vnd.rn-realmedia-vbr",
    //   rnc: "application/relax-ng-compact-syntax",
    //   rng: "application/xml",
    //   roa: "application/rpki-roa",
    //   roff: "text/troff",
    //   rp9: "application/vnd.cloanto.rp9",
    //   rpm: "application/x-redhat-package-manager",
    //   rpss: "application/vnd.nokia.radio-presets",
    //   rpst: "application/vnd.nokia.radio-preset",
    //   rq: "application/sparql-query",
    //   rs: "application/rls-services+xml",
    //   rsat: "application/atsc-rsat+xml",
    //   rsd: "application/rsd+xml",
    //   rsheet: "application/urc-ressheet+xml",
    //   rss: "application/rss+xml",
    rtf: "text/rtf",
    //   rtx: "text/richtext",
    //   run: "application/x-makeself",
    //   rusd: "application/route-usd+xml",
    //   s3m: "audio/s3m",
    //   s: "text/x-asm",
    //   saf: "application/vnd.yamaha.smaf-audio",
    //   sass: "text/x-sass",
    //   sbml: "application/sbml+xml",
    //   sc: "application/vnd.ibm.secure-container",
    //   scd: "application/x-msschedule",
    //   scm: "application/vnd.lotus-screencam",
    //   scq: "application/scvp-cv-request",
    //   scs: "application/scvp-cv-response",
    scss: "text/x-scss",
    //   scurl: "text/vnd.curl.scurl",
    //   sda: "application/vnd.stardivision.draw",
    //   sdc: "application/vnd.stardivision.calc",
    //   sdd: "application/vnd.stardivision.impress",
    //   sdkd: "application/vnd.solent.sdkm+xml",
    //   sdkm: "application/vnd.solent.sdkm+xml",
    //   sdp: "application/sdp",
    //   sdw: "application/vnd.stardivision.writer",
    //   sea: "application/x-sea",
    //   see: "application/vnd.seemail",
    //   seed: "application/vnd.fdsn.seed",
    //   sema: "application/vnd.sema",
    //   semd: "application/vnd.semd",
    //   semf: "application/vnd.semf",
    //   senmlx: "application/senml+xml",
    //   sensmlx: "application/sensml+xml",
    //   ser: "application/java-serialized-object",
    //   setpay: "application/set-payment-initiation",
    //   setreg: "application/set-registration-initiation",
    //   sfs: "application/vnd.spotfire.sfs",
    //   sfv: "text/x-sfv",
    //   sgi: "image/sgi",
    //   sgl: "application/vnd.stardivision.writer-global",
    //   sgm: "text/sgml",
    //   sgml: "text/sgml",
    //   sh: "application/x-sh",
    //   shar: "application/x-shar",
    //   shex: "text/shex",
    //   shf: "application/shf+xml",
    //   shtml: "text/html",
    //   sid: "image/x-mrsid-image",
    //   sieve: "application/sieve",
    //   sig: "application/pgp-signature",
    //   sil: "audio/silk",
    //   silo: "model/mesh",
    //   sis: "application/vnd.symbian.install",
    //   sisx: "application/vnd.symbian.install",
    //   sit: "application/x-stuffit",
    //   sitx: "application/x-stuffitx",
    //   siv: "application/sieve",
    //   skd: "application/vnd.koan",
    //   skm: "application/vnd.koan",
    //   skp: "application/vnd.koan",
    //   skt: "application/vnd.koan",
    sldm: "application/vnd.ms-powerpoint.slide.macroenabled.12",
    sldx: "application/vnd.openxmlformats-officedocument.presentationml.slide",
    //   slim: "text/slim",
    //   slm: "text/slim",
    //   sls: "application/route-s-tsid+xml",
    //   slt: "application/vnd.epson.salt",
    //   sm: "application/vnd.stepmania.stepchart",
    //   smf: "application/vnd.stardivision.math",
    //   smi: "application/smil+xml",
    //   smil: "application/smil+xml",
    //   smv: "video/x-smv",
    //   smzip: "application/vnd.stepmania.package",
    //   snd: "audio/basic",
    //   snf: "application/x-font-snf",
    //   so: "application/octet-stream",
    //   spc: "application/x-pkcs7-certificates",
    //   spdx: "text/spdx",
    //   spf: "application/vnd.yamaha.smaf-phrase",
    //   spl: "application/x-futuresplash",
    //   spot: "text/vnd.in3d.spot",
    //   spp: "application/scvp-vp-response",
    //   spq: "application/scvp-vp-request",
    //   spx: "audio/ogg",
    //   sql: "application/x-sql",
    //   src: "application/x-wais-source",
    //   srt: "application/x-subrip",
    //   sru: "application/sru+xml",
    //   srx: "application/sparql-results+xml",
    //   ssdl: "application/ssdl+xml",
    //   sse: "application/vnd.kodak-descriptor",
    //   ssf: "application/vnd.epson.ssf",
    //   ssml: "application/ssml+xml",
    //   st: "application/vnd.sailingtracker.track",
    //   stc: "application/vnd.sun.xml.calc.template",
    //   std: "application/vnd.sun.xml.draw.template",
    //   stf: "application/vnd.wt.stf",
    //   sti: "application/vnd.sun.xml.impress.template",
    //   stk: "application/hyperstudio",
    //   stl: "model/stl",
    //   stpx: "model/step+xml",
    //   stpxz: "model/step-xml+zip",
    //   stpz: "model/step+zip",
    //   str: "application/vnd.pg.format",
    //   stw: "application/vnd.sun.xml.writer.template",
    //   styl: "text/stylus",
    //   stylus: "text/stylus",
    //   sub: "text/vnd.dvb.subtitle",
    //   sus: "application/vnd.sus-calendar",
    //   susp: "application/vnd.sus-calendar",
    //   sv4cpio: "application/x-sv4cpio",
    //   sv4crc: "application/x-sv4crc",
    //   svc: "application/vnd.dvb.service",
    //   svd: "application/vnd.svd",
    svg: "image/svg+xml",
    svgz: "image/svg+xml",
    //   swa: "application/x-director",
    //   swf: "application/x-shockwave-flash",
    //   swi: "application/vnd.aristanetworks.swi",
    //   swidtag: "application/swid+xml",
    //   sxc: "application/vnd.sun.xml.calc",
    //   sxd: "application/vnd.sun.xml.draw",
    //   sxg: "application/vnd.sun.xml.writer.global",
    //   sxi: "application/vnd.sun.xml.impress",
    //   sxm: "application/vnd.sun.xml.math",
    //   sxw: "application/vnd.sun.xml.writer",
    //   t38: "image/t38",
    //   t3: "application/x-t3vm-image",
    //   t: "text/troff",
    //   taglet: "application/vnd.mynfc",
    //   tao: "application/vnd.tao.intent-module-archive",
    //   tap: "image/vnd.tencent.tap",
    tar: "application/x-tar",
    //   tcap: "application/vnd.3gpp2.tcap",
    //   tcl: "application/x-tcl",
    //   td: "application/urc-targetdesc+xml",
    //   teacher: "application/vnd.smart.teacher",
    //   tei: "application/tei+xml",
    //   teicorpus: "application/tei+xml",
    //   tex: "application/x-tex",
    //   texi: "application/x-texinfo",
    //   texinfo: "application/x-texinfo",
    text: "text/plain",
    //   tfi: "application/thraud+xml",
    //   tfm: "application/x-tex-tfm",
    //   tfx: "image/tiff-fx",
    //   tga: "image/x-tga",
    //   thmx: "application/vnd.ms-officetheme",
    tif: "image/tiff",
    tiff: "image/tiff",
    //   tk: "application/x-tcl",
    //   tmo: "application/vnd.tmobile-livetv",
    toml: "application/toml",
    //   torrent: "application/x-bittorrent",
    //   tpl: "application/vnd.groove-tool-template",
    //   tpt: "application/vnd.trid.tpt",
    //   tr: "text/troff",
    //   tra: "application/vnd.trueapp",
    //   trig: "application/trig",
    //   trm: "application/x-msterminal",
    //   ts: "video/mp2t",
    //   tsd: "application/timestamped-data",
    //   tsv: "text/tab-separated-values",
    //   ttc: "font/collection",
    ttf: "font/ttf",
    //   ttl: "text/turtle",
    //   ttml: "application/ttml+xml",
    //   twd: "application/vnd.simtech-mindmapper",
    //   twds: "application/vnd.simtech-mindmapper",
    //   txd: "application/vnd.genomatix.tuxedo",
    //   txf: "application/vnd.mobius.txf",
    txt: "text/plain",
    //   u32: "application/x-authorware-bin",
    //   u3d: "model/u3d",
    //   u8dsn: "message/global-delivery-status",
    //   u8hdr: "message/global-headers",
    //   u8mdn: "message/global-disposition-notification",
    //   u8msg: "message/global",
    //   ubj: "application/ubjson",
    //   udeb: "application/x-debian-package",
    //   ufd: "application/vnd.ufdl",
    //   ufdl: "application/vnd.ufdl",
    //   ulx: "application/x-glulx",
    //   umj: "application/vnd.umajin",
    //   unityweb: "application/vnd.unity",
    //   uo: "application/vnd.uoml+xml",
    //   uoml: "application/vnd.uoml+xml",
    //   uri: "text/uri-list",
    //   uris: "text/uri-list",
    //   urls: "text/uri-list",
    //   usda: "model/vnd.usda",
    //   usdz: "model/vnd.usdz+zip",
    //   ustar: "application/x-ustar",
    //   utz: "application/vnd.uiq.theme",
    //   uu: "text/x-uuencode",
    //   uva: "audio/vnd.dece.audio",
    //   uvd: "application/vnd.dece.data",
    //   uvf: "application/vnd.dece.data",
    //   uvg: "image/vnd.dece.graphic",
    //   uvh: "video/vnd.dece.hd",
    //   uvi: "image/vnd.dece.graphic",
    //   uvm: "video/vnd.dece.mobile",
    //   uvp: "video/vnd.dece.pd",
    //   uvs: "video/vnd.dece.sd",
    //   uvt: "application/vnd.dece.ttml+xml",
    //   uvu: "video/vnd.uvvu.mp4",
    //   uvv: "video/vnd.dece.video",
    //   uvva: "audio/vnd.dece.audio",
    //   uvvd: "application/vnd.dece.data",
    //   uvvf: "application/vnd.dece.data",
    //   uvvg: "image/vnd.dece.graphic",
    //   uvvh: "video/vnd.dece.hd",
    //   uvvi: "image/vnd.dece.graphic",
    //   uvvm: "video/vnd.dece.mobile",
    //   uvvp: "video/vnd.dece.pd",
    //   uvvs: "video/vnd.dece.sd",
    //   uvvt: "application/vnd.dece.ttml+xml",
    //   uvvu: "video/vnd.uvvu.mp4",
    //   uvvv: "video/vnd.dece.video",
    //   uvvx: "application/vnd.dece.unspecified",
    //   uvvz: "application/vnd.dece.zip",
    //   uvx: "application/vnd.dece.unspecified",
    //   uvz: "application/vnd.dece.zip",
    //   vbox: "application/x-virtualbox-vbox",
    //   vcard: "text/vcard",
    //   vcd: "application/x-cdlink",
    //   vcf: "text/x-vcard",
    //   vcg: "application/vnd.groove-vcard",
    //   vcs: "text/x-vcalendar",
    //   vcx: "application/vnd.vcx",
    //   vdi: "application/x-virtualbox-vdi",
    //   vds: "model/vnd.sap.vds",
    //   vhd: "application/x-virtualbox-vhd",
    //   vis: "application/vnd.visionary",
    //   viv: "video/vnd.vivo",
    //   vmdk: "application/x-virtualbox-vmdk",
    //   vob: "video/x-ms-vob",
    //   vor: "application/vnd.stardivision.writer",
    //   vox: "application/x-authorware-bin",
    //   vrml: "model/vrml",
    //   vsd: "application/vnd.visio",
    //   vsf: "application/vnd.vsf",
    //   vss: "application/vnd.visio",
    //   vst: "application/vnd.visio",
    //   vsw: "application/vnd.visio",
    //   vtf: "image/vnd.valve.source.texture",
    //   vtt: "text/vtt",
    //   vtu: "model/vnd.vtu",
    //   vxml: "application/voicexml+xml",
    //   w3d: "application/x-director",
    //   wad: "application/x-doom",
    //   wadl: "application/vnd.sun.wadl+xml",
    //   war: "application/java-archive",
    wasm: "application/wasm",
    //   wav: "audio/x-wav",
    //   wax: "audio/x-ms-wax",
    //   wbmp: "image/vnd.wap.wbmp",
    //   wbs: "application/vnd.criticaltools.wbs+xml",
    //   wbxml: "application/vnd.wap.wbxml",
    //   wcm: "application/vnd.ms-works",
    //   wdb: "application/vnd.ms-works",
    //   wdp: "image/vnd.ms-photo",
    //   weba: "audio/webm",
    //   webapp: "application/x-web-app-manifest+json",
    //   webm: "video/webm",
    //   webmanifest: "application/manifest+json",
    webp: "image/webp",
    //   wg: "application/vnd.pmi.widget",
    //   wgsl: "text/wgsl",
    //   wgt: "application/widget",
    //   wif: "application/watcherinfo+xml",
    //   wks: "application/vnd.ms-works",
    //   wm: "video/x-ms-wm",
    //   wma: "audio/x-ms-wma",
    //   wmd: "application/x-ms-wmd",
    //   wmf: "image/wmf",
    //   wml: "text/vnd.wap.wml",
    //   wmlc: "application/vnd.wap.wmlc",
    //   wmls: "text/vnd.wap.wmlscript",
    //   wmlsc: "application/vnd.wap.wmlscriptc",
    //   wmv: "video/x-ms-wmv",
    //   wmx: "video/x-ms-wmx",
    //   wmz: "application/x-msmetafile",
    woff2: "font/woff2",
    woff: "font/woff",
    //   wpd: "application/vnd.wordperfect",
    //   wpl: "application/vnd.ms-wpl",
    //   wps: "application/vnd.ms-works",
    //   wqd: "application/vnd.wqd",
    //   wri: "application/x-mswrite",
    //   wrl: "model/vrml",
    //   wsc: "message/vnd.wfa.wsc",
    //   wsdl: "application/wsdl+xml",
    //   wspolicy: "application/wspolicy+xml",
    //   wtb: "application/vnd.webturbo",
    //   wvx: "video/x-ms-wvx",
    //   x32: "application/x-authorware-bin",
    //   x3d: "model/x3d+xml",
    //   x3db: "model/x3d+fastinfoset",
    //   x3dbz: "model/x3d+binary",
    //   x3dv: "model/x3d-vrml",
    //   x3dvz: "model/x3d+vrml",
    //   x3dz: "model/x3d+xml",
    //   x_b: "model/vnd.parasolid.transmit.binary",
    //   x_t: "model/vnd.parasolid.transmit.text",
    //   xaml: "application/xaml+xml",
    //   xap: "application/x-silverlight-app",
    //   xar: "application/vnd.xara",
    //   xav: "application/xcap-att+xml",
    //   xbap: "application/x-ms-xbap",
    //   xbd: "application/vnd.fujixerox.docuworks.binder",
    //   xbm: "image/x-xbitmap",
    //   xca: "application/xcap-caps+xml",
    //   xcs: "application/calendar+xml",
    //   xdf: "application/xcap-diff+xml",
    //   xdm: "application/vnd.syncml.dm+xml",
    //   xdp: "application/vnd.adobe.xdp+xml",
    //   xdssc: "application/dssc+xml",
    //   xdw: "application/vnd.fujixerox.docuworks",
    //   xel: "application/xcap-el+xml",
    //   xenc: "application/xenc+xml",
    //   xer: "application/patch-ops-error+xml",
    //   xfdf: "application/xfdf",
    //   xfdl: "application/vnd.xfdl",
    //   xht: "application/xhtml+xml",
    xhtm: "application/vnd.pwg-xhtml-print+xml",
    xhtml: "application/xhtml+xml",
    //   xhvml: "application/xv+xml",
    //   xif: "image/vnd.xiff",
    xla: "application/vnd.ms-excel",
    xlam: "application/vnd.ms-excel.addin.macroenabled.12",
    xlc: "application/vnd.ms-excel",
    //   xlf: "application/xliff+xml",
    xlm: "application/vnd.ms-excel",
    xls: "application/vnd.ms-excel",
    xlsb: "application/vnd.ms-excel.sheet.binary.macroenabled.12",
    xlsm: "application/vnd.ms-excel.sheet.macroenabled.12",
    xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    xlt: "application/vnd.ms-excel",
    xltm: "application/vnd.ms-excel.template.macroenabled.12",
    xltx: "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
    xlw: "application/vnd.ms-excel",
    //   xm: "audio/xm",
    xml: "text/xml",
    //   xns: "application/xcap-ns+xml",
    //   xo: "application/vnd.olpc-sugar",
    //   xop: "application/xop+xml",
    //   xpi: "application/x-xpinstall",
    //   xpl: "application/xproc+xml",
    //   xpm: "image/x-xpixmap",
    //   xpr: "application/vnd.is-xpr",
    //   xps: "application/vnd.ms-xpsdocument",
    //   xpw: "application/vnd.intercon.formnet",
    //   xpx: "application/vnd.intercon.formnet",
    //   xsd: "application/xml",
    //   xsf: "application/prs.xsf+xml",
    //   xsl: "application/xslt+xml",
    //   xslt: "application/xslt+xml",
    //   xsm: "application/vnd.syncml+xml",
    //   xspf: "application/xspf+xml",
    //   xul: "application/vnd.mozilla.xul+xml",
    //   xvm: "application/xv+xml",
    //   xvml: "application/xv+xml",
    //   xwd: "image/x-xwindowdump",
    //   xyz: "chemical/x-xyz",
    //   xz: "application/x-xz",
    yaml: "text/yaml",
    //   yang: "application/yang",
    //   yin: "application/yin+xml",
    yml: "text/yaml",
    //   ymp: "text/x-suse-ymp",
    //   z1: "application/x-zmachine",
    //   z2: "application/x-zmachine",
    //   z3: "application/x-zmachine",
    //   z4: "application/x-zmachine",
    //   z5: "application/x-zmachine",
    //   z6: "application/x-zmachine",
    //   z7: "application/x-zmachine",
    //   z8: "application/x-zmachine",
    //   zaz: "application/vnd.zzazz.deck+xml",
    zip: "application/zip",
    //   zir: "application/vnd.zul",
    //   zirz: "application/vnd.zul",
    //   zmm: "application/vnd.handheld-entertainment+xml",
};

// SseServer class here
class SseServer {
    _res;
    _lastEventId;
    _pingSeqNum;
    constructor(req, res, opts) {
        let retryInterval = opts.retryInterval ?? 0;
        let pingInterval = opts.pingInterval ?? 0;
        let pingEventName = opts.pingEventName ?? "ping";
        this._res = res;
        this._lastEventId = req.headers["last-event-id"];
        this._pingSeqNum = 0;
        // Set up the basics first
        req.socket.setKeepAlive(true);
        req.socket.setNoDelay(true);
        req.socket.setTimeout(0);
        res.setHeader("Content-Type", "text/event-stream");
        res.setHeader("Connection", "keep-alive");
        res.setHeader("Cache-Control", "no-cache");
        res.statusCode = 200;
        // Check if we should set a new delay interval
        if (retryInterval > 0) {
            this.setRetry(retryInterval);
        }
        // Check if we should setup a heartbeat ping
        if (pingInterval > 0) {
            // Setup a timer to send the heartbeat
            let interval = setInterval(() => {
                this.sendData(this._pingSeqNum, { event: pingEventName });
                // Don't forget to increment the ping seq num
                this._pingSeqNum += 1;
            }, pingInterval * 1000);
            // Make sure to stop the timer if the connection closes
            res.addListener("close", () => {
                clearInterval(interval);
            });
        }
    }
    get lastEventId() {
        return this._lastEventId;
    }
    setRetry(delay) {
        this._res.write(`retry: ${delay}\n\n`);
    }
    sendData(data, options) {
        if (options?.event !== undefined) {
            this._res.write(`event: ${options.event}\n`);
        }
        if (options?.id !== undefined) {
            this._res.write(`id: ${options.id}\n`);
        }
        // Rem an array is an object!
        if (typeof data === "object") {
            this._res.write(`data: ${JSON.stringify(data)}\n\n`);
        }
        else {
            this._res.write(`data: ${data}\n\n`);
        }
    }
    close() {
        this._res.end();
    }
}

// Imports here
// Middleware functions here
const jsonMiddleware = () => {
    return async (req, _, next) => {
        // Before we do anything make sure there is a body!
        let body;
        if (Buffer.isBuffer(req.body)) {
            body = req.body;
        }
        if (body === undefined || body.length === 0) {
            // No body to parse so call next middleware and then return
            await next();
            return;
        }
        let jsonBody;
        let parseOk = true;
        let errMessage = "";
        // Now check the content-type header to find out what sort of data we have
        const contentTypeHeader = req.headers["content-type"];
        if (contentTypeHeader !== undefined) {
            let contentType = contentTypeHeader.split(";")[0];
            switch (contentType) {
                case "application/json":
                    try {
                        jsonBody = JSON.parse(body.toString());
                    }
                    catch (_) {
                        // Set the error message you want to return
                        errMessage = "Can not parse JSON body!";
                        parseOk = false;
                    }
                    break;
                case "application/x-www-form-urlencoded":
                    let qry = new URLSearchParams(body.toString());
                    jsonBody = {};
                    for (let [key, value] of qry.entries()) {
                        jsonBody[key] = value;
                    }
                    break;
            }
        }
        // If the parsing failed then return an error
        if (!parseOk) {
            throw new HttpError(400, errMessage);
        }
        req.json = jsonBody;
        await next();
    };
};
const bodyMiddleware = (options = {}) => {
    let opts = {
        maxBodySize: options.maxBodySize ?? 1024 * 1024,
    };
    return async (
    // NOTE: No async here please since this is returning a Promise
    req, _, next) => {
        // Cehck if body has already been set
        if (req.body !== undefined) {
            // If so just continue down the middleware stack
            await next();
            return;
        }
        // Store each data "chunk" we receive this array
        let chunks = [];
        let bodySize = 0;
        // Iterate of the req's AsyncIterator
        for await (let chunk of req) {
            bodySize += chunk.byteLength;
            // Check if the body is larger then the user is allowing
            if (bodySize >= opts.maxBodySize) {
                let msg = `Body length greater than ${opts.maxBodySize} bytes`;
                throw new HttpError(400, msg);
            }
            chunks.push(chunk);
        }
        req.body = Buffer.concat(chunks);
        await next();
    };
};
const corsMiddleware = (options = {}) => {
    let opts = {
        originsAllowed: options.originsAllowed ?? "*",
        methodsAllowed: options.methodsAllowed ?? [],
        headersAllowed: options.headersAllowed ?? [],
        headersExposed: options.headersExposed ?? [],
        credentialsAllowed: options.credentialsAllowed ?? false,
        maxAge: options.maxAge ?? 60 * 60, // 1 hour
    };
    // NOTE: If credentialsAllowed is enabled then other headers cant be a "*"
    if (opts.credentialsAllowed) {
        if (opts.originsAllowed === "*") {
            throw new Error("The originsAllowed MUST be specified when credentialsAllowed is true");
        }
        if (opts.methodsAllowed === "*") {
            throw new Error("The methodsAllowed MUST be specified when credentialsAllowed is true");
        }
        if (opts.headersAllowed === "*") {
            throw new Error("The headersAllowed MUST be specified when credentialsAllowed is true");
        }
        if (opts.headersExposed === "*") {
            throw new Error("The headersExposed MUST be specified when credentialsAllowed is true");
        }
    }
    return async (req, res, next) => {
        let origin = req.headers["origin"];
        // Check if this is a CORS preflight request
        if (req.method === "OPTIONS") {
            // The origin MUST be available or this is not valid
            if (origin === undefined) {
                throw new HttpError(400, "No origin header sent with the CORS request");
            }
            // Set Access-Control-Allow-Origin
            if (opts.originsAllowed === "*" || opts.originsAllowed.includes(origin)) {
                // Best to set this to the origin for this req and NOT allowed origins
                res.setHeader("Access-Control-Allow-Origin", origin);
            }
            else {
                throw new HttpError(400, `The origin ${origin} is not allowed`);
            }
            // Set Access-Control-Allow-Methods
            // We know this header exists otherwise we couldn't have gotten here
            let reqMethod = req.headers["access-control-request-method"];
            if (opts.methodsAllowed === "*") {
                res.setHeader("Access-Control-Allow-Methods", "*");
            }
            else if (opts.methodsAllowed.length === 0) {
                // No methods being specified implies you should use the reqMethod
                res.setHeader("Access-Control-Allow-Methods", reqMethod);
            }
            else if (opts.methodsAllowed.includes(reqMethod)) {
                res.setHeader("Access-Control-Allow-Methods", opts.methodsAllowed.join(","));
            }
            else {
                throw new HttpError(400, `The access-control-request-method ${reqMethod} is not allowed`);
            }
            // Set Access-Control-Allow-Headers
            if (req.headers["access-control-request-headers"] !== undefined) {
                if (opts.headersAllowed === "*") {
                    res.setHeader("Access-Control-Allow-Headers", "*");
                }
                else if (opts.headersAllowed.length) {
                    // Let the browser handle this one
                    res.setHeader("Access-Control-Allow-Headers", opts.headersAllowed.join(","));
                }
            }
            // Set Access-Control-Expose-Headers
            if (opts.headersExposed === "*") {
                res.setHeader("Access-Control-Expose-Headers", "*");
            }
            else if (opts.headersExposed.length) {
                res.setHeader("Access-Control-Expose-Headers", opts.headersExposed.join(","));
            }
            // Access-Control-Max-Age
            res.setHeader("Access-Control-Max-Age", opts.maxAge);
            // Access-Control-Allow-Credentials
            if (opts.credentialsAllowed) {
                res.setHeader("Access-Control-Allow-Credentials", "true");
            }
            // Finish up here and do not continue down the middleware stack
            res.statusCode = 204;
            res.end();
            return;
        }
        // If we are here this was not a preflight request
        // The origin needs to be available or we shouldn't set the CORS headers
        if (origin !== undefined) {
            if (opts.credentialsAllowed === true) {
                res.setHeader("Access-Control-Allow-Credentials", "true");
            }
            if (opts.originsAllowed === "*" || opts.originsAllowed.includes(origin)) {
                // Best to set this to the origin for this req and NOT allowed origins
                res.setHeader("Access-Control-Allow-Origin", origin);
            }
        }
        // If we are here then continue down the middleware stack
        await next();
    };
};
const expressWrapper = (middleware) => {
    // Because we need to pass in the express middleware we will return the
    // middleware, i.e. you need to call this function
    return async (req, res, next) => {
        middleware(req, res, (e) => {
            if (e !== undefined) {
                throw e;
            }
        });
        await next();
    };
};
const csrfChecksMiddleware = (options = {}) => {
    let opts = {
        methods: options.methods ?? ["POST", "PUT", "PATCH", "DELETE"],
        checkType: options.checkType ?? "custom-req-header",
        header: options.header ?? "x-csrf-header",
        cookie: options.cookie ?? "x-csrf-cookie",
        secret: options.secret ?? "",
        hashAlgo: options.hashAlgo ?? "sha256",
        signatureSeparator: options.signatureSeparator ?? ".",
    };
    // Need to make sure the header we check for is always lower case
    opts.header = opts.header.toLowerCase();
    // If this is "naive-double-submit-cookie" check the cookie is supplied
    if (opts.checkType === "signed-double-submit-cookie") {
        if (opts.secret.length === 0) {
            throw new Error("Must set secret to use the 'signed-double-submit-cookie' CSRF check middleware");
        }
    }
    let custReqHeader = (req) => {
        // The custom-req-header check just ensures that the specified
        // header exists - the value is not important
        if (req.headers[opts.header] === undefined) {
            return false;
        }
        return true;
    };
    let naiveDoubleSubmitCookie = (req) => {
        // The naive-double-submit-cookie check ensures the value of the
        // specified cookie matches the value of the specified header
        let cookie = req.getCookie(opts.cookie);
        // Note if cookie doesn't exist value is null and if headers doesn't exist
        // it is undefined
        if (req.headers[opts.header] !== cookie) {
            return false;
        }
        return true;
    };
    let signedDoubleSubmitCookie = (req) => {
        // The signed-double-submit-cookie check ensures the value of the
        // specified cookie matches the value of the specified header
        let cookie = req.getCookie(opts.cookie);
        // Note if cookie doesn't exist value is null and if headers doesn't exist
        // it is undefined
        if (req.headers[opts.header] !== cookie) {
            return false;
        }
        let [token, hash] = cookie.split(opts.signatureSeparator);
        if (hash !==
            crypto.createHmac(opts.hashAlgo, opts.secret).update(token).digest("hex")) {
            return false;
        }
        return true;
    };
    // Because we need to pass in the options we will return the
    // middleware, i.e. you need to call this function
    return async (req, res, next) => {
        // Make sure the method is one of the ones we want to check
        if (opts.methods.includes(req.method)) {
            let passed = false;
            if (opts.checkType === "custom-req-header") {
                passed = custReqHeader(req);
            }
            else if (opts.checkType === "naive-double-submit-cookie") {
                passed = naiveDoubleSubmitCookie(req);
            }
            else {
                passed = signedDoubleSubmitCookie(req);
            }
            // If the CSRF check failed then DO NOT continue down the stack
            if (passed === false) {
                res.statusCode = 401;
                res.write("The request failed the CSRF check");
                return;
            }
        }
        await next();
    };
};
const getSecurityHeaders = (options = {}) => {
    let opts = {
        headers: options.headers ?? [],
        useDefaultHeaders: options.useDefaultHeaders ?? true,
    };
    // These are the default headers to use
    let defaultHeaders = [
        { name: "X-Frame-Options", value: "SAMEORIGIN" },
        { name: "X-XSS-Protection", value: "0" },
        { name: "X-Content-Type-Options", value: "nosniff" },
        { name: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
        {
            name: "Strict-Transport-Security",
            value: "max-age=63072000; includeSubDomains; preload",
        },
        { name: "X-DNS-Prefetch-Control", value: "off" },
        {
            name: "Content-Security-Policy",
            value: "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
        },
    ];
    // These are the headers we will use
    let securityHeaders = [];
    // Set all of the user supplied headers first
    for (let header of opts.headers) {
        securityHeaders.push({ name: header.name, value: header.value });
    }
    // Check if we should use the default headers
    if (opts.useDefaultHeaders) {
        // Looks like it - so add the default headers
        for (let header of defaultHeaders) {
            // Check if the user has already supplied the header (use lower case
            // to be safe)
            let found = opts.headers.find((el) => el.name.toLowerCase() === header.name.toLowerCase());
            if (found !== undefined) {
                continue;
            }
            securityHeaders.push({ name: header.name, value: header.value });
        }
    }
    return securityHeaders;
};
const securityHeadersMiddleware = (options = {}) => {
    // Get the security headers
    let securityHeaders = getSecurityHeaders(options);
    // Because we need to pass in the options we will return the
    // middleware, i.e. you need to call this function
    return async (_, res, next) => {
        // Set all of the sec headers
        for (let header of securityHeaders) {
            res.setHeader(header.name, header.value);
        }
        await next();
    };
};
const dontCompressResponse = () => {
    return async (req, _, next) => {
        // Flag the response should not be compressed
        req.dontCompressResponse = true;
        await next();
    };
};
const setLatencyMetricName = (name) => {
    return async (_, res, next) => {
        // Set the latency metric name
        res.latencyMetricName = name;
        await next();
    };
};

/**
 * Tokenize input string.
 */
function lexer(str) {
    var tokens = [];
    var i = 0;
    while (i < str.length) {
        var char = str[i];
        if (char === "*" || char === "+" || char === "?") {
            tokens.push({ type: "MODIFIER", index: i, value: str[i++] });
            continue;
        }
        if (char === "\\") {
            tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] });
            continue;
        }
        if (char === "{") {
            tokens.push({ type: "OPEN", index: i, value: str[i++] });
            continue;
        }
        if (char === "}") {
            tokens.push({ type: "CLOSE", index: i, value: str[i++] });
            continue;
        }
        if (char === ":") {
            var name = "";
            var j = i + 1;
            while (j < str.length) {
                var code = str.charCodeAt(j);
                if (
                // `0-9`
                (code >= 48 && code <= 57) ||
                    // `A-Z`
                    (code >= 65 && code <= 90) ||
                    // `a-z`
                    (code >= 97 && code <= 122) ||
                    // `_`
                    code === 95) {
                    name += str[j++];
                    continue;
                }
                break;
            }
            if (!name)
                throw new TypeError("Missing parameter name at ".concat(i));
            tokens.push({ type: "NAME", index: i, value: name });
            i = j;
            continue;
        }
        if (char === "(") {
            var count = 1;
            var pattern = "";
            var j = i + 1;
            if (str[j] === "?") {
                throw new TypeError("Pattern cannot start with \"?\" at ".concat(j));
            }
            while (j < str.length) {
                if (str[j] === "\\") {
                    pattern += str[j++] + str[j++];
                    continue;
                }
                if (str[j] === ")") {
                    count--;
                    if (count === 0) {
                        j++;
                        break;
                    }
                }
                else if (str[j] === "(") {
                    count++;
                    if (str[j + 1] !== "?") {
                        throw new TypeError("Capturing groups are not allowed at ".concat(j));
                    }
                }
                pattern += str[j++];
            }
            if (count)
                throw new TypeError("Unbalanced pattern at ".concat(i));
            if (!pattern)
                throw new TypeError("Missing pattern at ".concat(i));
            tokens.push({ type: "PATTERN", index: i, value: pattern });
            i = j;
            continue;
        }
        tokens.push({ type: "CHAR", index: i, value: str[i++] });
    }
    tokens.push({ type: "END", index: i, value: "" });
    return tokens;
}
/**
 * Parse a string for the raw tokens.
 */
function parse(str, options) {
    if (options === undefined) { options = {}; }
    var tokens = lexer(str);
    var _a = options.prefixes, prefixes = _a === undefined ? "./" : _a, _b = options.delimiter, delimiter = _b === undefined ? "/#?" : _b;
    var result = [];
    var key = 0;
    var i = 0;
    var path = "";
    var tryConsume = function (type) {
        if (i < tokens.length && tokens[i].type === type)
            return tokens[i++].value;
    };
    var mustConsume = function (type) {
        var value = tryConsume(type);
        if (value !== undefined)
            return value;
        var _a = tokens[i], nextType = _a.type, index = _a.index;
        throw new TypeError("Unexpected ".concat(nextType, " at ").concat(index, ", expected ").concat(type));
    };
    var consumeText = function () {
        var result = "";
        var value;
        while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) {
            result += value;
        }
        return result;
    };
    var isSafe = function (value) {
        for (var _i = 0, delimiter_1 = delimiter; _i < delimiter_1.length; _i++) {
            var char = delimiter_1[_i];
            if (value.indexOf(char) > -1)
                return true;
        }
        return false;
    };
    var safePattern = function (prefix) {
        var prev = result[result.length - 1];
        var prevText = prefix || (prev && typeof prev === "string" ? prev : "");
        if (prev && !prevText) {
            throw new TypeError("Must have text between two parameters, missing text after \"".concat(prev.name, "\""));
        }
        if (!prevText || isSafe(prevText))
            return "[^".concat(escapeString(delimiter), "]+?");
        return "(?:(?!".concat(escapeString(prevText), ")[^").concat(escapeString(delimiter), "])+?");
    };
    while (i < tokens.length) {
        var char = tryConsume("CHAR");
        var name = tryConsume("NAME");
        var pattern = tryConsume("PATTERN");
        if (name || pattern) {
            var prefix = char || "";
            if (prefixes.indexOf(prefix) === -1) {
                path += prefix;
                prefix = "";
            }
            if (path) {
                result.push(path);
                path = "";
            }
            result.push({
                name: name || key++,
                prefix: prefix,
                suffix: "",
                pattern: pattern || safePattern(prefix),
                modifier: tryConsume("MODIFIER") || "",
            });
            continue;
        }
        var value = char || tryConsume("ESCAPED_CHAR");
        if (value) {
            path += value;
            continue;
        }
        if (path) {
            result.push(path);
            path = "";
        }
        var open = tryConsume("OPEN");
        if (open) {
            var prefix = consumeText();
            var name_1 = tryConsume("NAME") || "";
            var pattern_1 = tryConsume("PATTERN") || "";
            var suffix = consumeText();
            mustConsume("CLOSE");
            result.push({
                name: name_1 || (pattern_1 ? key++ : ""),
                pattern: name_1 && !pattern_1 ? safePattern(prefix) : pattern_1,
                prefix: prefix,
                suffix: suffix,
                modifier: tryConsume("MODIFIER") || "",
            });
            continue;
        }
        mustConsume("END");
    }
    return result;
}
/**
 * Create path match function from `path-to-regexp` spec.
 */
function match(str, options) {
    var keys = [];
    var re = pathToRegexp(str, keys, options);
    return regexpToFunction(re, keys, options);
}
/**
 * Create a path match function from `path-to-regexp` output.
 */
function regexpToFunction(re, keys, options) {
    if (options === undefined) { options = {}; }
    var _a = options.decode, decode = _a === undefined ? function (x) { return x; } : _a;
    return function (pathname) {
        var m = re.exec(pathname);
        if (!m)
            return false;
        var path = m[0], index = m.index;
        var params = Object.create(null);
        var _loop_1 = function (i) {
            if (m[i] === undefined)
                return "continue";
            var key = keys[i - 1];
            if (key.modifier === "*" || key.modifier === "+") {
                params[key.name] = m[i].split(key.prefix + key.suffix).map(function (value) {
                    return decode(value, key);
                });
            }
            else {
                params[key.name] = decode(m[i], key);
            }
        };
        for (var i = 1; i < m.length; i++) {
            _loop_1(i);
        }
        return { path: path, index: index, params: params };
    };
}
/**
 * Escape a regular expression string.
 */
function escapeString(str) {
    return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
}
/**
 * Get the flags for a regexp from the options.
 */
function flags(options) {
    return options && options.sensitive ? "" : "i";
}
/**
 * Pull out keys from a regexp.
 */
function regexpToRegexp(path, keys) {
    if (!keys)
        return path;
    var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g;
    var index = 0;
    var execResult = groupsRegex.exec(path.source);
    while (execResult) {
        keys.push({
            // Use parenthesized substring match if available, index otherwise
            name: execResult[1] || index++,
            prefix: "",
            suffix: "",
            modifier: "",
            pattern: "",
        });
        execResult = groupsRegex.exec(path.source);
    }
    return path;
}
/**
 * Transform an array into a regexp.
 */
function arrayToRegexp(paths, keys, options) {
    var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; });
    return new RegExp("(?:".concat(parts.join("|"), ")"), flags(options));
}
/**
 * Create a path regexp from string input.
 */
function stringToRegexp(path, keys, options) {
    return tokensToRegexp(parse(path, options), keys, options);
}
/**
 * Expose a function for taking tokens and returning a RegExp.
 */
function tokensToRegexp(tokens, keys, options) {
    if (options === undefined) { options = {}; }
    var _a = options.strict, strict = _a === undefined ? false : _a, _b = options.start, start = _b === undefined ? true : _b, _c = options.end, end = _c === undefined ? true : _c, _d = options.encode, encode = _d === undefined ? function (x) { return x; } : _d, _e = options.delimiter, delimiter = _e === undefined ? "/#?" : _e, _f = options.endsWith, endsWith = _f === undefined ? "" : _f;
    var endsWithRe = "[".concat(escapeString(endsWith), "]|$");
    var delimiterRe = "[".concat(escapeString(delimiter), "]");
    var route = start ? "^" : "";
    // Iterate over the tokens and create our regexp string.
    for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
        var token = tokens_1[_i];
        if (typeof token === "string") {
            route += escapeString(encode(token));
        }
        else {
            var prefix = escapeString(encode(token.prefix));
            var suffix = escapeString(encode(token.suffix));
            if (token.pattern) {
                if (keys)
                    keys.push(token);
                if (prefix || suffix) {
                    if (token.modifier === "+" || token.modifier === "*") {
                        var mod = token.modifier === "*" ? "?" : "";
                        route += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod);
                    }
                    else {
                        route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier);
                    }
                }
                else {
                    if (token.modifier === "+" || token.modifier === "*") {
                        throw new TypeError("Can not repeat \"".concat(token.name, "\" without a prefix and suffix"));
                    }
                    route += "(".concat(token.pattern, ")").concat(token.modifier);
                }
            }
            else {
                route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier);
            }
        }
    }
    if (end) {
        if (!strict)
            route += "".concat(delimiterRe, "?");
        route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")");
    }
    else {
        var endToken = tokens[tokens.length - 1];
        var isEndDelimited = typeof endToken === "string"
            ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1
            : endToken === undefined;
        if (!strict) {
            route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?");
        }
        if (!isEndDelimited) {
            route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")");
        }
    }
    return new RegExp(route, flags(options));
}
/**
 * Normalize the given path string, returning a regular expression.
 *
 * An empty array can be passed in for the keys, which will hold the
 * placeholder key descriptions. For example, using `/user/:id`, `keys` will
 * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
 */
function pathToRegexp(path, keys, options) {
    if (path instanceof RegExp)
        return regexpToRegexp(path, keys);
    if (Array.isArray(path))
        return arrayToRegexp(path, keys, options);
    return stringToRegexp(path, keys, options);
}

// imports here
// Misc here
const defaultNotFoundHandler$1 = async (_, res) => {
    res.statusCode = 404;
    res.write("API route not found");
    res.end();
};
// Router class here
class Router {
    _basePathDelimited;
    _basePath;
    _useNotFoundHandler;
    _notFoundHandler;
    _minCompressionSize;
    _logger;
    _methodListMap;
    _defaultMiddlewareList;
    constructor(basePath, config = {}) {
        // Make sure to properly delimit the basePath
        this._basePathDelimited = basePath.replace(/\/*$/, "/");
        // Make sure to strip off the trailing slashes
        this._basePath = basePath.replace(/\/*$/, "");
        this._useNotFoundHandler = config.useNotFoundHandler ?? true;
        this._notFoundHandler = config.notFoundHandler ?? defaultNotFoundHandler$1;
        this._minCompressionSize = config.minCompressionSize ?? 1024;
        this._logger = new Logger(`Router (${this._basePath})`);
        // Initialise the method list manually
        this._methodListMap = {
            ALL: [],
            GET: [],
            DELETE: [],
            PATCH: [],
            POST: [],
            PUT: [],
            OPTIONS: [],
            HEAD: [],
        };
        this._defaultMiddlewareList = [];
    }
    // Getter methods here
    get basePath() {
        return this._basePathDelimited;
    }
    // Private methods here
    searchMethodElements(req, list) {
        let matchedEl = null;
        // Next see if we have a registered callback for the HTTP req path
        for (let el of list) {
            let routerMatch = el.match(req.urlObj);
            // If result is false that means we found nothing
            if (routerMatch === false) {
                continue;
            }
            // If we are here we found the callback
            matchedEl = el;
            // Don't forget to set the matchedInfo and params properties
            req.matchedInfo = routerMatch.matchedInfo;
            req.params = routerMatch.params;
            // Stop looking
            break;
        }
        return matchedEl;
    }
    findEndpoint(req) {
        let method = req.method;
        // Check for a CORS Preflight request - yes there is middleware for this
        // but this has to be checked here because we will not have a registered
        // endpoint under OPTIONS
        if (req.method === "OPTIONS" &&
            req.headers["access-control-request-method"] !== undefined) {
            // Get the method this preflight request is checking for and use that
            // to see there is an endpoint registered for it
            method = req.headers["access-control-request-method"];
        }
        // If the method is HEAD then check the GET method map
        if (req.method === "HEAD") {
            method = "GET";
        }
        // Make sure we don't have some odd method we never heard about
        let list = this._methodListMap[method];
        if (list === undefined) {
            return null;
        }
        // First search for the routes in the req method list
        let matchedEl = this.searchMethodElements(req, list);
        if (matchedEl === null) {
            // If we are here that means we did not find a callback for the req path
            // and we should check for a fallback callback
            matchedEl = this.searchMethodElements(req, this._methodListMap["ALL"]);
        }
        return matchedEl;
    }
    async callMiddleware(req, res, el, middlewareStack) {
        // Check if there handlers still be be called on the stack
        if (middlewareStack.length) {
            // Call the top handler and pass the rest of the handlers after it
            await middlewareStack[0](req, res, async () => {
                await this.callMiddleware(req, res, el, middlewareStack.slice(1));
            });
        }
        else {
            // No more handlers but make sure is NOT an unhandled preflight check.
            // If it is then we DO NOT want to call the endpoint handler
            if (req.method !== "OPTIONS") {
                await this.callEndpoint(req, res, el);
            }
        }
    }
    async callEndpoint(req, res, el) {
        // Check if this should be a server sent event endpoint
        if (el.sseServerOptions !== undefined) {
            req.sseServer = new SseServer(req, res, el.sseServerOptions);
        }
        // The callback can be async or not so check for it
        if (el.callback.constructor.name === "AsyncFunction") {
            // This is async so use await
            await el.callback(req, res);
        }
        else {
            // This is a synchronous call
            el.callback(req, res);
        }
    }
    async addResponse(req, res, etag) {
        let body = null;
        // Check if a json or a body response has been passed back
        if (res.json !== undefined) {
            res.setHeader("Content-Type", "application/json; charset=utf-8");
            body = JSON.stringify(res.json);
        }
        else if (res.body !== undefined) {
            // Check if the content-type has not been set
            if (!res.hasHeader("Content-Type")) {
                // It hasn't so set it to the default type
                res.setHeader("Content-Type", "text/plain; charset=utf-8");
            }
            body = res.body;
        }
        // Check if the user didnt pass any data to send back (body is null)
        if (body === null) {
            // This means there will be an empty body so check if the StatusCode has
            // been change from the default 200 - if it has leave it alone beacuse
            // the user must have set it
            if (res.statusCode === 200) {
                // Otherwise set the status code to indicate an empty body
                res.statusCode = 204;
            }
            // Don't forget to set the server-timing header before we leave
            res.setServerTimingHeader();
            // Nothing else to do including calculating and etag so get out of here
            return;
        }
        // We need to ensure body is a string or a Buffer or we will have problems
        if (Buffer.isBuffer(body) === false && typeof body !== "string") {
            this._logger.error("(%s) response body for (%s) is not of type string or Buffer", req.method, req.urlObj.pathname);
            res.statusCode = 500;
            res.end();
            return;
        }
        // Check if the user wants an etag added to the response
        if (etag) {
            let etag = crypto.createHash("sha1").update(body).digest("hex");
            // All headers need to be set, except content-length, for a 304
            res.setHeader("Cache-Control", "no-cache");
            res.setHeader("Etag", etag);
            // Check if any cache validators exist on the request
            if (req.headers["if-none-match"] === etag) {
                // Don't forget to set the server-timing header after we do everything else
                res.setServerTimingHeader();
                res.statusCode = 304;
                res.end();
                return;
            }
        }
        // Check out if the req will accept a gzip res AND the body is large enough
        // AND compression is not turned off for this request
        let gzipIt = false;
        // Check if the res was proxied. If it was then DO NOT set the
        // transfer-encoding/content-encoding header nor the content-length.
        // Assume that has already been done
        if (res.proxied === false) {
            if (req.headers["accept-encoding"]?.includes("gzip") === true &&
                Buffer.byteLength(body) >= this._minCompressionSize &&
                req.dontCompressResponse === false) {
                // It does ...
                gzipIt = true;
                // Dont set the content-length. Use transfer-encoding instead
                res.setHeader("Transfer-Encoding", "chunked");
                res.setHeader("Content-Encoding", "gzip");
            }
            else {
                // It does not ...
                // Only set the length when we don't do a 304
                res.setHeader("Content-Length", Buffer.byteLength(body));
            }
        }
        // Don't forget to set the server-timing header after we do everything else
        res.setServerTimingHeader();
        // Check if this was a HEAD method - if so we don't want to write the body
        if (req.method !== "HEAD") {
            if (gzipIt) {
                const passThrough = new PassThrough();
                passThrough.end(body);
                // NOTE1: pipeline will close the res when it is finished
                await streams
                    .pipeline(passThrough, zlib.createGzip(), res)
                    .catch((e) => {
                    // We can't do anything else here because either:
                    // - the stream is closed which means we can't send back an error
                    // - we have an internal error, but we have already started streaming
                    //   so we can't do anything
                    this._logger.error("addResponse had this error during streaming: (%s)", e);
                });
            }
            else {
                res.write(body);
            }
        }
        res.end();
    }
    // Public methods here
    inPath(pathname) {
        // Make sure to use the delimited base path to ensure a correct match
        return pathname.startsWith(this._basePathDelimited);
    }
    async handleReq(req, res) {
        // See if this request matches a registered endpoint
        let matchedEl = this.findEndpoint(req);
        if (matchedEl === null) {
            // Check if we should use the supplied Not Found handler or not
            if (this._useNotFoundHandler) {
                await this._notFoundHandler(req, res);
                return true;
            }
            // Couldn't find a match so flag that the req has not been handled
            return false;
        }
        await this.callMiddleware(req, res, matchedEl, matchedEl.middlewareList).catch((e) => {
            let message;
            // If a redirect call res.redirect() and get out of the error handler
            if (e instanceof HttpRedirect) {
                res.redirect(e.location, e.statusCode, e.message);
                return;
            }
            // If it is a HttpError assume the error message has already been logged
            if (e instanceof HttpError) {
                res.statusCode = e.status;
                message = e.message;
            }
            else {
                // We don't know what this is so log it and make sure to return a 500
                this._logger.error("Unknown error happened while handling URL (%s) - (%s)", req.urlObj.pathname, e);
                res.statusCode = 500;
                message = "Unknown error happened";
            }
            // Check if res.write() has NOT been called yet
            if (!res.headersSent) {
                res.setHeader("Content-Type", "text/plain; charset=utf-8");
                res.setHeader("Content-Length", Buffer.byteLength(message));
                res.write(message);
            }
            // Check if the res.end() has NOT been called yet
            if (!res.writableEnded) {
                // End the response now
                res.end();
            }
        });
        // If this is an SSE server dont call addResponse or res.end()
        if (req.sseServer !== undefined) {
            return true;
        }
        // Check if res.write() has NOT been called yet
        if (!res.headersSent) {
            // Check if the callback wants us to add the body, headers etc
            await this.addResponse(req, res, matchedEl.etag);
        }
        // Check if the res.end() has NOT been called yet
        if (!res.writableEnded) {
            // End the response now
            res.end();
        }
        // Flag this req has been handled
        return true;
    }
    pathToRegexMatcher(path) {
        // Create the matching function
        let match$1 = match(path, {
            decode: decodeURIComponent,
            strict: true,
        });
        return (url) => {
            let result = match$1(url.pathname);
            if (result === false) {
                return false;
            }
            return {
                params: result.params,
                matchedInfo: result,
            };
        };
    }
    matchAllMatcher(_) {
        // This will match everything
        return (url) => {
            return {
                matchedInfo: url.pathname,
                params: {}, // We dont know that the params are so just ignore them
            };
        };
    }
    use(middleware) {
        this._defaultMiddlewareList.push(middleware);
        return this;
    }
    endpoint(method, path, callback, endpointOptions = {}) {
        let options = {
            useDefaultMiddlewares: true,
            etag: false,
            generateMatcher: this.pathToRegexMatcher,
            ...endpointOptions,
        };
        // Make sure we have the middlewares requested
        let middlewareList = [];
        // Check if the user wants the default middlewares
        if (options.useDefaultMiddlewares) {
            // ... stick the default middlewares in first
            // NOTE: Any middleware added to the defaults after this endpoint is
            // added will not be used by this endpoint
            middlewareList = [...this._defaultMiddlewareList];
        }
        if (options.middlewareList !== undefined) {
            middlewareList = [...middlewareList, ...options.middlewareList];
        }
        // GEt the full path - check if the path already includes the basePath
        let fullPath = this.inPath(path) ? path : `${this._basePath}${path}`;
        // Finally add it to the list of callbacks
        this._methodListMap[method].push({
            match: options.generateMatcher(fullPath),
            callback,
            middlewareList,
            sseServerOptions: options.sseServerOptions,
            etag: options.etag,
        });
        this._logger.startupMsg("Added %s endpoint for path (%s)", method.toUpperCase(), fullPath);
        return this;
    }
    // endpoint helper methods here
    del(path, callback, endpointOptions = {}) {
        this.endpoint("DELETE", path, callback, endpointOptions);
        return this;
    }
    get(path, callback, endpointOptions = {}) {
        this.endpoint("GET", path, callback, endpointOptions);
        return this;
    }
    patch(path, callback, endpointOptions = {}) {
        this.endpoint("PATCH", path, callback, endpointOptions);
        return this;
    }
    post(path, callback, endpointOptions = {}) {
        this.endpoint("POST", path, callback, endpointOptions);
        return this;
    }
    put(path, callback, endpointOptions = {}) {
        this.endpoint("PUT", path, callback, endpointOptions);
        return this;
    }
    all(path, callback, endpointOptions = {}) {
        this.endpoint("ALL", path, callback, endpointOptions);
        return this;
    }
    route(path) {
        let server = this;
        return {
            get(callback, endpointOptions = {}) {
                server.endpoint("GET", path, callback, endpointOptions);
                return server.route(path);
            },
            patch(callback, endpointOptions = {}) {
                server.endpoint("PATCH", path, callback, endpointOptions);
                return server.route(path);
            },
            post(callback, endpointOptions = {}) {
                server.endpoint("POST", path, callback, endpointOptions);
                return server.route(path);
            },
            put(callback, endpointOptions = {}) {
                server.endpoint("PUT", path, callback, endpointOptions);
                return server.route(path);
            },
            del(callback, endpointOptions = {}) {
                server.endpoint("DELETE", path, callback, endpointOptions);
                return server.route(path);
            },
            all(callback, endpointOptions = {}) {
                server.endpoint("ALL", path, callback, endpointOptions);
                return server.route(path);
            },
        };
    }
    // Middleware methods here
    static body(options = {}) {
        // Rem we have to call bodyMiddleware since it returns the middleware
        return bodyMiddleware(options);
    }
    static json() {
        return jsonMiddleware();
    }
    static cors(options = {}) {
        return corsMiddleware(options);
    }
    static csrf(options = {}) {
        return csrfChecksMiddleware(options);
    }
    static getSecHeaders(options) {
        return getSecurityHeaders(options);
    }
    static secHeaders(options) {
        return securityHeadersMiddleware(options);
    }
    static expressWrapper(options) {
        return expressWrapper(options);
    }
    static dontCompressResponse() {
        return dontCompressResponse();
    }
    static setLatencyMetricName(name) {
        return setLatencyMetricName(name);
    }
}

// imports here
// Misc here
const defaultNotFoundHandler = async (_, res) => {
    res.statusCode = 404;
    res.write("File not found");
    res.end();
};
// StaticFileServer class here
class StaticFileServer {
    _logger;
    _filePath;
    _immutableRegExp;
    _defaultDirFile;
    _defaultCharSet;
    _notFoundHandler;
    _staticFileMap;
    _contentTypes;
    _securityHeaders;
    constructor(config) {
        // Make sure there is no trailing slash at the end of the path
        this._logger = new Logger(config.loggerName);
        this._logger.startupMsg("Creating static file server ...");
        this._filePath = config.filePath.replace(/\/*$/, "");
        // Initialise the immutable regexs array
        this._immutableRegExp = [];
        // Check if user has provided an immutable config value
        if (config.immutableRegExp !== undefined) {
            // Check if user has provided a regexp or string or array
            if (config.immutableRegExp instanceof RegExp) {
                this._immutableRegExp.push(config.immutableRegExp);
            }
            else if (typeof config.immutableRegExp === "string") {
                this._immutableRegExp.push(new RegExp(config.immutableRegExp));
            }
            else if (Array.isArray(config.immutableRegExp)) {
                for (const exp of config.immutableRegExp) {
                    // This is an array so iterate through each element and add it to the list
                    if (exp instanceof RegExp) {
                        this._immutableRegExp.push(exp);
                    }
                    else if (typeof exp === "string") {
                        this._immutableRegExp.push(new RegExp(exp));
                    }
                }
            }
        }
        this._defaultDirFile = config.defaultDirFile ?? "index.html";
        this._defaultCharSet = config.defaultCharSet ?? "charset=utf-8";
        this._notFoundHandler = config.notFoundHandler ?? defaultNotFoundHandler;
        this._staticFileMap = new Map();
        this._contentTypes = new Map();
        // Get the standard sec headers and add the users specified headers as well
        this._securityHeaders = Router.getSecHeaders({
            headers: config.securityHeaders,
        });
        // Populate contentTypes using the predefined types
        for (const type in contentTypes) {
            this._contentTypes.set(type, contentTypes[type]);
        }
        // Then add any extra content types. NOTE: This allows you to overwrite
        // the predefined types
        if (config.extraContentTypes !== undefined) {
            for (const type in config.extraContentTypes) {
                this._contentTypes.set(type, config.extraContentTypes[type]);
            }
        }
        // Get all of the files at start up - but a constructor cant be async so
        // run getFilesRecursively() at the earliest possibile time
        setImmediate(async () => {
            await this.getFilesRecursively();
        });
    }
    // Private methods here
    async getFilesRecursively(urlPath = "/") {
        // Note: urlPath should always start and end in "/"
        const dir = `${this._filePath}${urlPath}`;
        let dirFiles = [];
        // Get a list of files in the dir and check for errors
        try {
            dirFiles = fs.readdirSync(dir);
        }
        catch (e) {
            this._logger.warn("No permissions to read from dir (%s)", dir);
        }
        // Iterate through each file and check if it is a dir or not
        for (const file of dirFiles) {
            const fullPath = `${dir}${file}`;
            const stats = fs.statSync(fullPath);
            const url = `${urlPath}${file}`;
            if (stats.isDirectory()) {
                // Get the files in this dir
                this.getFilesRecursively(`${url}/`);
            }
            else if (stats.isFile()) {
                // Add the file to the list
                await this.addFile(fullPath, `${url}`, stats);
            }
        }
    }
    lookupType(file) {
        // Look up the file extension to get content type - drop the leading '.'
        const ext = path.extname(file).slice(1);
        const type = this._contentTypes.get(ext);
        if (type !== undefined) {
            return `${type}; ${this._defaultCharSet}`;
        }
        // This is the default content type
        return `text/plain; ${this._defaultCharSet}`;
    }
    async calculateEtag(fileBuffer, fileName) {
        // MD5 hash the file contents to calculate the etag
        const contents = stream.Readable.from(fileBuffer);
        const hash = crypto.createHash("sha1");
        // Flag to check if we successfully pipe the file to the hash
        let failed = false;
        await streams.pipeline(contents, hash).catch((e) => {
            this._logger.trace("Error attempting to create etag for file (%s) (%s): ", fileName, e);
            failed = true;
        });
        if (failed) {
            return null;
        }
        return hash.digest("hex");
    }
    async addFile(fullPath, urlPath, stats) {
        // Use a flag to decide if we add the file to the file map or not
        let addFile = true;
        try {
            // Test if we can read the file
            fs.accessSync(fullPath, fs.constants.R_OK);
        }
        catch (e) {
            // There was an error which means we cant read the file so DO NOT add it
            addFile = false;
            this._logger.warn("No permissions to read file : (%s)", fullPath);
        }
        if (addFile === false) {
            // Can't add file so do nothing
            return false;
        }
        // Add the file and it's details to the map
        const modTimeMs = stats.mtime.getTime();
        // Get rid of the ms from the time because we lose it when we convert to a
        // UTC string which means we get a mismatch checking "If-Modified-Since"
        const modTimeNoMs = Math.trunc(modTimeMs / 1000) * 1000;
        const fileBuffer = fs.readFileSync(fullPath);
        const eTag = await this.calculateEtag(fileBuffer, fullPath);
        if (eTag === null) {
            // Couldn't calculate the etag so do nothing
            return false;
        }
        // Default immutable to false until we can prove it is
        let immutable = false;
        // Now check if the path matches one of the RegExps
        for (const regexp of this._immutableRegExp) {
            if (regexp.test(fullPath) === true) {
                immutable = true;
                break;
            }
        }
        const fileDetails = {
            contentType: this.lookupType(fullPath), // In case urlPath is a dir
            size: stats.size,
            lastModifiedNoMs: modTimeNoMs,
            lastModifiedMs: modTimeMs,
            lastModifiedUtcStr: new Date(modTimeNoMs).toUTCString(),
            eTag,
            fullPath,
            immutable,
            fileBuffer,
            compressedBuffer: zlib.gzipSync(fileBuffer),
        };
        this._staticFileMap.set(urlPath, fileDetails);
        this._logger.trace("Added (%s) to file map. Details: contentType (%s), size (%s), lastModifiedMs(%s), eTag (%s), fullPath (%s), immutable (%j)", urlPath, fileDetails.contentType, fileDetails.size, fileDetails.lastModifiedMs, fileDetails.eTag, fileDetails.fullPath, fileDetails.immutable);
        return true;
    }
    async getFileDetails(file) {
        // Check for the details first. If it exists we want to use the stored full
        // path just in case file is s dir. It will save and extra stat!
        let details = this._staticFileMap.get(file);
        let fullPath = details?.fullPath ?? `${this._filePath}${file.replace(/\/*$/, "")}`;
        // If we can't stat the file (doesn't exist) then stat will throw
        let stats = await fsPromises.stat(fullPath).catch((e) => {
            this._logger.trace("Received an error when trying to stat (%s): (%s)", fullPath, e);
        });
        if (stats === undefined) {
            return undefined;
        }
        // Check if the file is a directory (should only happen the 1st time)
        if (stats.isDirectory()) {
            // This is a dir so set the file to be the default file for a dir
            fullPath += `/${this._defaultDirFile}`;
            // Get the stats again for the default file. If we can't stat the file
            // (doesn't exist) then stat will throw
            stats = await fsPromises.stat(fullPath).catch((e) => {
                this._logger.trace("Received an error when trying to stat (%s): (%s)", fullPath, e);
            });
            if (stats === undefined) {
                return undefined;
            }
        }
        // Check if the file wasn't in the file map or it was modified
        if (details === undefined ||
            details.lastModifiedMs !== stats.mtime.getTime() ||
            details.size !== stats.size) {
            // Add the file to the file map and get the new details
            await this.addFile(fullPath, file, stats);
            details = this._staticFileMap.get(file);
        }
        return details;
    }
    // Public methods here
    async handleReq(req, res) {
        // We only handle GET and HEAD for static files. Return a not found
        if (req.method !== "GET" && req.method !== "HEAD") {
            this._notFoundHandler(req, res);
            return;
        }
        // Get the file details and if it doesn't exist return a not found
        const details = await this.getFileDetails(req.urlObj.pathname);
        if (details === undefined) {
            this._notFoundHandler(req, res);
            return;
        }
        const cacheControl = details.immutable
            ? "max-age=31536000, immutable"
            : "no-cache";
        // All headers need to be set, except content-length, for a 304
        res.setHeader("Cache-Control", cacheControl);
        res.setHeader("Etag", details.eTag);
        res.setHeader("Last-Modified", details.lastModifiedUtcStr);
        res.setHeader("Date", new Date().toUTCString());
        res.setHeader("Content-Type", details.contentType);
        // Set all of the sec headers
        for (const header of this._securityHeaders) {
            res.setHeader(header.name, header.value);
        }
        // Don't forget to set the server-timing header
        res.latencyMetricName = "sf-srv";
        res.setServerTimingHeader();
        // Check if any cache validators exist on the request - check etag first
        if (req.headers["if-none-match"] === details.eTag) {
            res.statusCode = 304;
            res.end();
            return;
        }
        if (req.headers["if-modified-since"] !== undefined) {
            const modifiedDate = new Date(req.headers["if-modified-since"]).getTime();
            // NOTE: Check the times are the same, if they are different, even if
            // details.lastModifiedNoMs is LESS than modifiedDate, it will still
            // because that implies there is a potential issue and it is best to
            // be safe
            if (modifiedDate === details.lastModifiedNoMs) {
                res.statusCode = 304;
                res.end();
                return;
            }
        }
        let fileRead;
        // Check out if the req will accept a gzip res
        if (req.headers["accept-encoding"]?.includes("gzip") === true) {
            // It does ...
            fileRead = stream.Readable.from(details.compressedBuffer);
            // Dont set the content-length. Use transfer-encoding instead
            res.setHeader("Transfer-Encoding", "chunked");
            res.setHeader("Content-Encoding", "gzip");
        }
        else {
            // It does not ...
            fileRead = stream.Readable.from(details.fileBuffer);
            // Only set the length when we don't do a 304
            res.setHeader("Content-Length", details.size);
        }
        // If it's a HEAD then do not set the body
        if (req.method === "HEAD") {
            res.end();
            return;
        }
        // NOTE: pipeline will close the res when it is finished
        await streams.pipeline(fileRead, res).catch((e) => {
            // We can't do anything else here because either:
            // - the stream is closed which means we can't send back an error
            // - we have an internal error, but we have already started streaming
            //   so we can't do anything
            this._logger.trace("Error attempting to read (%s): (%s)", fileRead, e);
        });
    }
}

// imports here
class HttpConfigError {
    message;
    constructor(message) {
        this.message = message;
    }
}
// HttpServer class here
class HttpServer {
    _logger;
    _socketMap;
    _socketId;
    _networkInterface;
    _networkPort;
    _networkIp;
    _baseUrl;
    _name;
    _healthcheckCallbacks;
    _httpKeepAliveTimeout;
    _httpHeaderTimeout;
    _healthCheckPath;
    _healthCheckGoodResCode;
    _healthCheckBadResCode;
    _enableHttps;
    _keyFile;
    _certFile;
    _maintenanceModeOn;
    _maintenanceRoute;
    _apiRouterList;
    _defaultApiRouter;
    _ssrRouter;
    _staticFileServer;
    _server;
    constructor(networkInterface, networkPort, config = {}) {
        this._name = `${networkInterface}-${networkPort}`;
        this._logger = new Logger(`HttpServer-${this._name}`);
        this._httpKeepAliveTimeout = config.keepAliveTimeout ?? 65000;
        this._httpHeaderTimeout = config.headerTimeout ?? 66000;
        this._healthCheckPath = config.healthcheckPath ?? "/healthcheck";
        this._healthCheckGoodResCode = config.healthcheckGoodRes ?? 200;
        this._healthCheckBadResCode = config.healthcheckBadRes ?? 503;
        this._enableHttps = config.enableHttps ?? false;
        this._maintenanceRoute = config.maintenanceRoute;
        this._maintenanceModeOn = config.startInMaintenanceMode ?? false;
        this._logger.startupMsg("Maintenance mode is set to (%j)", this._maintenanceModeOn);
        this._socketMap = new Map();
        this._socketId = 0;
        this._networkIp = "";
        this._baseUrl = "";
        this._networkInterface = networkInterface;
        this._networkPort = networkPort;
        this._healthcheckCallbacks = [];
        this._apiRouterList = [];
        // Create the default router AFTER you initialise the _routerList
        this._defaultApiRouter = this.addRouter(config.defaultRouterBasePath ?? "/api");
        if (this._enableHttps) {
            this._keyFile = config.httpsKeyFile;
            this._certFile = config.httpsCertFile;
        }
        // Make sure the SSR Router DOES NOT use the not found handler - we need it
        // to pass control to the static file server and do not add it to the
        // _apiRouterList since it doesnt have a fixed base path
        this._ssrRouter = new Router("/", { useNotFoundHandler: false });
        this._logger.startupMsg("SSR router created");
        if (config.staticFileServer !== undefined) {
            this._staticFileServer = new StaticFileServer({
                loggerName: `HttpServer-${this._name}/StaticFile`,
                filePath: config.staticFileServer.path,
                extraContentTypes: config.staticFileServer.extraContentTypes,
                immutableRegExp: config.staticFileServer.immutableRegExp,
                securityHeaders: config.staticFileServer.securityHeaders,
            });
        }
    }
    // Getter methods here
    get networkIp() {
        return this._networkIp;
    }
    get networkPort() {
        return this._networkPort;
    }
    get baseUrl() {
        return this._baseUrl;
    }
    get httpsEnabled() {
        return this._enableHttps;
    }
    get name() {
        return this._name;
    }
    get ssrRouter() {
        return this._ssrRouter;
    }
    // Setter methods here
    set maintenanceModeOn(on) {
        this._maintenanceModeOn = on;
        this._logger.info("Maintenance mode set to (%j)", this._maintenanceModeOn);
    }
    // Private methods here
    findInterfaceIp(networkInterface) {
        const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})){3}$/;
        if (ipv4Regex.test(networkInterface)) {
            this._logger.startupMsg(`Using provided IP (${networkInterface})`);
            return networkInterface;
        }
        this._logger.startupMsg(`Finding IP for interface (${networkInterface})`);
        let ifaces = os.networkInterfaces();
        this._logger.startupMsg("Interfaces on host: %j", ifaces);
        if (ifaces[networkInterface] === undefined) {
            return null;
        }
        let ip = "";
        // Search for the first I/F with a family of type IPv4
        let found = ifaces[networkInterface]?.find((i) => i.family === "IPv4");
        if (found !== undefined) {
            ip = found.address;
            this._logger.startupMsg(`Found IP (${ip}) for interface ${networkInterface}`);
        }
        if (ip.length === 0) {
            return null;
        }
        return ip;
    }
    async startListening(server) {
        // Start listening
        server.listen(this._networkPort, this._networkIp);
        // Since this is an async event we need to wait for the "listening" event
        // to fire, so lets wrap this in a Promise and resolve the promise when
        // it happens
        return new Promise((resolve, _) => {
            server.on("listening", () => {
                this._logger.startupMsg(`Now listening on (${this._baseUrl}). HTTP manager started!`);
                resolve();
            });
            // We also want to track all of the sockets that are opened
            server.on("connection", (socket) => {
                // We need a local copy of the socket ID for this closure to work
                let socketId = this._socketId++;
                this._socketMap.set(socketId, socket);
                this._logger.trace("'connection' for socketId (%d) on remote connection (%s/%s)", socketId, socket.remoteAddress, socket.remotePort);
                // Check when the socket closes
                socket.on("close", () => {
                    // First check if the socket has not been closed during a stop()
                    if (this._socketMap.has(socketId)) {
                        this._socketMap.delete(socketId);
                        this._logger.trace("'close' for socketId (%d) on remote connection (%s/%s)", socketId, socket.remoteAddress, socket.remotePort);
                    }
                });
            });
        });
    }
    async handleReq(req, res) {
        // Check if we are in maintenance mode
        if (this._maintenanceModeOn && this._maintenanceRoute !== undefined) {
            this._logger.trace("Maintenance mode on. Redirecting (%s) to (%s)", req.url, this._maintenanceRoute);
            // This isn't very sexy and seems a little heavy handed but works a treat
            // we just point the req to the maintenance route and pray the user set
            // it up!
            req.method = "GET";
            req.url = this._maintenanceRoute;
        }
        // We have to do this here because the url will not be set until
        // after this object it created: See req-res.ts
        let protocol = this._enableHttps ? "https" : "http";
        req.urlObj = new URL(req.url, `${protocol}://${req.headers.host}`);
        this._logger.trace("Received (%s) req for (%s)", req.method, req.urlObj.pathname);
        // Look for a router with a basePath that matches the start of the req path
        // NOTE: Make sure to delimit the pathname in case it is a match for
        // the root of the basepath
        let router = this._apiRouterList.find((el) => el.inPath(`${req.urlObj.pathname}/`));
        // Try and handle the request (if router exists)
        if ((await router?.handleReq(req, res)) === true) {
            return;
        }
        // If we're here this wasn't an API req so check if it was SSR req
        if (await this._ssrRouter.handleReq(req, res)) {
            return;
        }
        // If we're here this wasn't a SSR req so check if we're serving
        // static files
        if (this._staticFileServer !== undefined) {
            await this._staticFileServer.handleReq(req, res);
            return;
        }
        // If we are here then we dont know this URL so return a 404
        res.statusCode = 404;
        res.write("Not found");
        res.end();
    }
    async healthcheckCallback(_1, res) {
        let healthy = true;
        for (let cb of this._healthcheckCallbacks) {
            healthy = await cb();
            if (!healthy) {
                break;
            }
        }
        if (healthy) {
            res.statusCode = this._healthCheckGoodResCode;
            res.body = "Healthy";
        }
        else {
            res.statusCode = this._healthCheckBadResCode;
            res.body = "Not Healthy";
        }
    }
    // Public methods here
    async start() {
        this._logger.startupMsg("Initialising HTTP manager ...");
        let ip = this.findInterfaceIp(this._networkInterface);
        if (ip === null) {
            throw new Error(`${this._networkInterface} is not an interface on this server`);
        }
        this._networkIp = ip;
        this._logger.startupMsg(`Will listen on interface ${this._networkInterface} (IP: ${this._networkIp})`);
        // Create either a HTTP or HTTPS server
        if (this._enableHttps) {
            this._baseUrl = `https://${this._networkIp}:${this._networkPort}`;
            if (this._keyFile === undefined) {
                throw new HttpConfigError("HTTPS is enabled but no key file provided!");
            }
            if (this._certFile === undefined) {
                throw new HttpConfigError("HTTPS is enabled but no cert file provided!");
            }
            this._logger.startupMsg(`Attempting to listen on (${this._baseUrl})`);
            const options = {
                IncomingMessage: ServerRequest,
                ServerResponse: ServerResponse, // Something wrong with typedefs
                key: fs.readFileSync(this._keyFile),
                cert: fs.readFileSync(this._certFile),
            };
            this._server = https.createServer(options, (req, res) => {
                this.handleReq(req, res);
            });
        }
        else {
            this._baseUrl = `http://${this._networkIp}:${this._networkPort}`;
            this._logger.startupMsg(`Attempting to listen on (${this._baseUrl})`);
            const options = {
                IncomingMessage: ServerRequest,
                ServerResponse: ServerResponse, // Something wrong with typedefs
            };
            this._server = http.createServer(options, (req, res) => {
                this.handleReq(req, res);
            });
        }
        this._server.keepAliveTimeout = this._httpKeepAliveTimeout;
        this._server.headersTimeout = this._httpHeaderTimeout;
        await this.startListening(this._server);
        // Now we need to add the endpoint for healthchecks
        this._defaultApiRouter.get(this._healthCheckPath, async (req, res) => this.healthcheckCallback(req, res), { useDefaultMiddlewares: false });
    }
    async stop() {
        this._logger.shutdownMsg("Closing all connections now ...");
        // Close all the remote connections
        this._socketMap.forEach((socket, key) => {
            socket.destroy();
            this._logger.trace("Destroying socketId (%d) for remote connection (%s/%s)", key, socket.remoteAddress, socket.remotePort);
        });
        // Just in case someone calls stop() a 2nd time
        this._socketMap.clear();
        if (this._server !== undefined) {
            this._logger.shutdownMsg("Closing HTTP manager port now ...");
            this._server.close();
            this._logger.shutdownMsg("Port closed");
            // Just in case someone calls stop() a 2nd time
            this._server = undefined;
        }
        return;
    }
    addHealthcheck(callback) {
        this._healthcheckCallbacks.push(callback);
    }
    addRouter(basePath, routerConfig = {}) {
        // Make sure the basePath is properly terminated
        basePath = basePath.replace(/\/*$/, "/");
        // Check to make sure this basePath does not overlap with another router's
        // basePath
        let found = this._apiRouterList.find((el) => {
            return el.inPath(basePath) || el.basePath.startsWith(basePath);
        });
        // If there is an overlap with an existing router then "stop the press"!
        if (found !== undefined) {
            throw new Error(`${basePath} clashes with basePath of ${found.basePath}`);
        }
        // If we are here then all is good so create the new router
        let router = new Router(basePath, routerConfig);
        this._apiRouterList.push(router);
        this._logger.startupMsg("(%s) router created", basePath.replace(/\/$/, ""));
        return router;
    }
    router(basePath) {
        if (basePath === undefined) {
            return this._defaultApiRouter;
        }
        // Make sure to remove any trailing slashes and then delimit properly
        let basePathSanitised = basePath.replace(/\/*$/, "/");
        return this._apiRouterList.find((el) => el.basePath === basePathSanitised);
    }
    // Methods for the default router here
    use(middleware) {
        return this._defaultApiRouter.use(middleware);
    }
    del(path, callback, options = {}) {
        return this._defaultApiRouter.del(path, callback, options);
    }
    get(path, callback, options = {}) {
        return this._defaultApiRouter.get(path, callback, options);
    }
    patch(path, callback, options = {}) {
        return this._defaultApiRouter.patch(path, callback, options);
    }
    post(path, callback, options = {}) {
        return this._defaultApiRouter.post(path, callback, options);
    }
    put(path, callback, options = {}) {
        return this._defaultApiRouter.put(path, callback, options);
    }
    endpoint(method, path, callback, options = {}) {
        return this._defaultApiRouter.endpoint(method, path, callback, options);
    }
    route(path) {
        return this._defaultApiRouter.route(path);
    }
}

// imports here
// BSPlugin class here
class BSPlugin {
    _name;
    _version;
    _logger;
    // Constructor here
    constructor(name, version) {
        this._name = name;
        this._version = version;
        this._logger = new Logger(this._name);
        this.startupMsg("Initialising ...");
    }
    // Protected methods (that can be overridden) here
    async stop() {
        // This is a default stop method. Override it if you need to clean up
        this.shutdownMsg("Stopped!");
    }
    // Getters here
    get name() {
        return this._name;
    }
    get version() {
        return this._version;
    }
    get stopHandler() {
        return this.stop;
    }
    // Protected methods here
    // Log convinence methods
    fatal(...args) {
        this._logger.fatal(...args);
    }
    error(...args) {
        this._logger.error(...args);
    }
    warn(...args) {
        this._logger.warn(...args);
    }
    info(...args) {
        this._logger.info(...args);
    }
    startupMsg(...args) {
        this._logger.startupMsg(...args);
    }
    shutdownMsg(...args) {
        this._logger.shutdownMsg(...args);
    }
    debug(...args) {
        this._logger.debug(...args);
    }
    trace(...args) {
        this._logger.trace(...args);
    }
    force(...args) {
        this._logger.force(...args);
    }
}

// imports here
// Misc consts here
const LOGGER_APP_NAME = "App";
// NOTE: 1.20.4 is replaced with package.json#version by a
// rollup plugin at build time
const VERSION = "1.20.4";
// Module private variables here
let _logger;
let _httpServerList;
let _pluginMap;
let _sharedStore;
const _shutdownHandler = async () => {
    await bs.exit(0);
};
const _exceptionHandler = async (e) => {
    bs.error("Caught unhandled error - (%s)", e);
    await bs.exit(1);
};
let _finallyHandler = async () => {
    bs.shutdownMsg("Done!");
};
let _stopHandler = async () => {
    bs.shutdownMsg("Stopped!");
};
let _restartHandler = async () => {
    bs.shutdownMsg("Restarted!");
};
// The shell object here
const bs = Object.freeze({
    // request wrapper
    request: async (origin, path, reqOptions) => {
        return request(origin, path, reqOptions);
    },
    // Config helper methods here
    /**
     * Gets a string config value.
     *
     * @param config - The config key to get.
     * @param defaultVal - The default value if config not found.
     * @param options - Options for getting the config.
     * @returns The string config value.
     */
    getConfigStr: (config, defaultVal, options) => {
        let value = configMan.getStr(config, defaultVal, options);
        logConfigManMsgs();
        return value;
    },
    /**
     * Gets a boolean config value.
     *
     * @param config - The config key to get.
     * @param defaultVal - The default value if config not found.
     * @param options - Options for getting the config.
     * @returns The boolean config value.
     */
    getConfigBool: (config, defaultVal, options) => {
        let value = configMan.getBool(config, defaultVal, options);
        logConfigManMsgs();
        return value;
    },
    /**
     * Gets a number config value.
     *
     * @param config - The config key to get.
     * @param defaultVal - The default value if config not found.
     * @param options - Options for getting the config.
     * @returns The number config value.
     */
    getConfigNum: (config, defaultVal, options) => {
        let value = configMan.getNum(config, defaultVal, options);
        logConfigManMsgs();
        return value;
    },
    /**
     * Gets an object config value.
     *
     * @param config - The config key to get.
     * @param defaultVal - The default value if config not found.
     * @param options - Options for getting the config.
     * @returns The object config value.
     */
    getConfigObj: (config, defaultVal, options) => {
        let value = (configMan.getObject(config, defaultVal, options));
        logConfigManMsgs();
        return value;
    },
    /**
     * Gets an array config value.
     *
     * @param config - The config key to get.
     * @param defaultVal - The default value if config not found.
     * @param options - Options for getting the config.
     * @returns The object config value.
     */
    getConfigArray: (config, defaultVal, options) => {
        let value = configMan.getObject(config, defaultVal, options);
        logConfigManMsgs();
        return value;
    },
    // Log convience methods here
    fatal: (...args) => {
        _logger.fatal(...args);
    },
    error: (...args) => {
        _logger.error(...args);
    },
    warn: (...args) => {
        _logger.warn(...args);
    },
    info: (...args) => {
        _logger.info(...args);
    },
    startupMsg: (...args) => {
        _logger.startupMsg(...args);
    },
    shutdownMsg: (...args) => {
        _logger.shutdownMsg(...args);
    },
    debug: (...args) => {
        _logger.debug(...args);
    },
    trace: (...args) => {
        _logger.trace(...args);
    },
    force: (...args) => {
        _logger.force(...args);
    },
    setLogLevel: (level) => {
        _logger.setLevel(level);
    },
    // General functions here
    shellVersion: () => {
        return VERSION;
    },
    setFinallyHandler: (handler) => {
        _finallyHandler = handler;
    },
    setStopHandler: (handler) => {
        _stopHandler = handler;
    },
    setRestartHandler: (handler) => {
        _restartHandler = handler;
    },
    exit: async (code, hard = true) => {
        bs.shutdownMsg("Exiting ...");
        // Clear the global and const stores
        _sharedStore.clear();
        // Make sure we stop all of the HttpSevers - probably best to do it first
        for (let httpServer of _httpServerList) {
            await httpServer.stop();
        }
        // Clear the HttpServer list
        _httpServerList = [];
        // Stop the application second
        bs.shutdownMsg("Attempting to stop the application ...");
        await _stopHandler().catch((e) => {
            bs.error(e);
        });
        // Stop the plugins in the reverse order you started them
        for (let plugin of [..._pluginMap.values()].reverse()) {
            bs.shutdownMsg(`Attempting to stop plugin ${plugin.name} ...`);
            await plugin.stopHandler().catch((e) => {
                bs.error(e);
            });
        }
        // Clear the plugin list
        _pluginMap.clear();
        // If there was a finally handler provided then call it last
        if (_finallyHandler !== undefined) {
            bs.shutdownMsg("Calling the 'finally handler' ...");
            await _finallyHandler().catch((e) => {
                bs.error(e);
            });
        }
        // Remove the event handlers for catching exit events
        process.removeListener("SIGINT", _shutdownHandler);
        process.removeListener("SIGTERM", _shutdownHandler);
        process.removeListener("beforeExit", _shutdownHandler);
        process.removeListener("uncaughtException", _exceptionHandler);
        process.removeListener("SIGHUP", bs.restart);
        bs.shutdownMsg("So long and thanks for all the fish!");
        // Check if the exit should also exit the process (a hard stop)
        if (hard) {
            process.exit(code);
        }
    },
    restart: async () => {
        bs.info("Restarting now!");
        // Re-init the logger in case config values have changed
        _logger = new Logger(LOGGER_APP_NAME);
        // Do a soft exit
        await bs.exit(0, false);
        // Then re-init this bad boy
        init();
        // Now call the users restart handler
        await _restartHandler();
    },
    shutdownError: async (code = 1, testing = false) => {
        bs.error("Heuston, we have a problem. Shutting down now ...");
        if (testing) {
            // Do a soft stop so we don't force any testing code to exit
            await bs.exit(code, false);
            return;
        }
        await bs.exit(code);
    },
    // Utility functions here
    addHttpServer: async (networkInterface, networkPort, httpConfig = {}, startServer = true) => {
        let server = new HttpServer(networkInterface, networkPort, httpConfig);
        // Automatically start the server if requested
        if (startServer) {
            await server.start();
        }
        _httpServerList.push(server);
        return server;
    },
    httpServer: (index = 0) => {
        // Check if there are any http servers first
        if (_httpServerList.length === 0) {
            throw Error(`There are no http servers!!`);
        }
        // Check if the requested server DOES NOT exist
        if (index >= _httpServerList.length) {
            throw Error(`There is no http servers with the index ${index}`);
        }
        return _httpServerList[index];
    },
    addPlugin: (name, pluginClass, config = {}) => {
        // Make sure we don't have a duplicate name
        if (_pluginMap.has(name)) {
            throw Error(`There is already a plugin with the name ${name}`);
        }
        // Create the plugin
        let plugin = new pluginClass(name, config);
        // And then cache the plugin
        _pluginMap.set(name, plugin);
        return plugin;
    },
    plugin: (name) => {
        // Search for the plugin that has a matching name
        let plugin = _pluginMap.get(name);
        // Check if the plugin DOES NOT exist
        if (plugin === undefined) {
            throw Error(`There is no plugin with the name ${name}`);
        }
        return plugin;
    },
    save: (name, value) => {
        if (_sharedStore.has(name)) {
            throw Error(`There is already a value saved with the name ${name}`);
        }
        _sharedStore.set(name, value);
    },
    update: (name, value) => {
        _sharedStore.set(name, value);
    },
    retrieve: (name) => {
        return _sharedStore.get(name);
    },
    sleep: async (durationInSeconds) => {
        // Convert duration to ms
        let ms = Math.round(durationInSeconds * 1000);
        return new Promise((resolve) => {
            setTimeout(resolve, ms);
        });
    },
    question: async (ask, questionOptions) => {
        let input = process.stdin;
        let output = process.stdout;
        let options = {
            muteAnswer: false,
            muteChar: "*",
            ...questionOptions,
        };
        return new Promise((resolve) => {
            let rl = readline.createInterface({
                input,
                output,
            });
            if (options.muteAnswer) {
                input.on("keypress", () => {
                    // get the number of characters entered so far:
                    var len = rl.line.length;
                    if (options.muteChar.length === 0) {
                        // move cursor back one since we will always be at the start
                        readline.moveCursor(output, -1, 0);
                        // clear everything to the right of the cursor
                        readline.clearLine(output, 1);
                    }
                    else {
                        // move cursor back to the beginning of the input
                        readline.moveCursor(output, -len, 0);
                        // clear everything to the right of the cursor
                        readline.clearLine(output, 1);
                        // If there is a muteChar then replace the original input with it
                        for (var i = 0; i < len; i++) {
                            // In case the user passes a string just use the 1st char
                            output.write(options.muteChar[0]);
                        }
                    }
                });
            }
            // Insert a space after the question for convience
            rl.question(`${ask} `, (answer) => {
                resolve(answer);
                rl.close();
            });
        });
    },
});
// Private functions here
let logConfigManMsgs = () => {
    let messages = configMan.getMessages();
    for (let message of messages) {
        _logger.startupMsg(message[0]);
    }
    configMan.clearMessages();
};
function init() {
    // Initialise the private variables
    _logger = new Logger(LOGGER_APP_NAME);
    _httpServerList = [];
    _pluginMap = new Map();
    _sharedStore = new Map();
    // Now spit out the versions
    bs.startupMsg(`Bamboo Shell version (${VERSION})`);
    bs.startupMsg(`NODE_ENV is (${process.env.NODE_ENV === undefined ? "development" : process.env.NODE_ENV})`);
    // Now set up the event handler
    bs.startupMsg("Setting up shutdown event handlers ...");
    // Call exit() on a Ctrl-C
    process.on("SIGINT", _shutdownHandler);
    // Call exit() when the program is terminated
    process.on("SIGTERM", _shutdownHandler);
    // Call exit() during normal programming termination
    process.on("beforeExit", _shutdownHandler);
    // Catch and log any execptions and then call exit()
    process.on("uncaughtException", _exceptionHandler);
    // Call resatrt() on a HUP signal
    process.on("SIGHUP", bs.restart);
    // And it's party time!
    bs.startupMsg("Ready to Rock and Roll baby!");
}
// OK - lets light this candle!
init();

export { BSPlugin, ConfigError, HttpConfigError, HttpError, HttpRedirect, HttpServer, LogLevel, Logger, ReqAborted, ReqError, Router, ServerRequest, ServerResponse, SseServer, bs };
//# sourceMappingURL=shell.mjs.map