@bs-core/shell
Version:
The Bamboo Shell
1 lines • 344 kB
Source Map (JSON)
{"version":3,"file":"shell.mjs","sources":["../out/config-man.js","../out/logger.js","../out/utils.js","../out/http-req.js","../out/http-server/req-res.js","../out/http-server/content-types.js","../out/http-server/middleware.js","../out/http-server/static-file-server.js","../out/http-server/sse-server.js","../../node_modules/.pnpm/path-to-regexp@8.4.2/node_modules/path-to-regexp/dist/index.js","../out/http-server/router.js","../out/http-server/main.js","../out/bs-plugin.js","../out/main.js"],"sourcesContent":["/**\n * Config manager module. Provides functions to retrieve config values from various sources like\n * CLI, environment variables, and env file. Includes utility functions to handle config value\n * lookup, type conversion, error handling etc.\n */\n// imports here\nimport * as fs from \"node:fs\";\n// Config consts here\n// The env var that contains the name of the .env file\nconst CFG_ENV_FILE = \"ENV_FILE\";\n// The env var that contains the name of the cfg file\nconst CFG_CFG_FILE = \"CFG_FILE\";\n// Private variables here\n// Stores the parsed contents of the .env file as key-value pairs\nlet _envFileStore;\n// Stores the parsed contents of the cfg file as an object\nlet _cfgFileStore;\n// Stores the messages generated during configuration.\n// NOTE: This is used because the configuration manager is used by Logger\n// and it becomes a chicken/egg situation when initialising the Logger\nlet _messageStore;\n/**\n * Enumeration of supported configuration value types.\n * Can be used when retrieving a config value to specify the expected type.\n */\nexport var ConfigType;\n(function (ConfigType) {\n ConfigType[\"String\"] = \"String\";\n ConfigType[\"Number\"] = \"Number\";\n ConfigType[\"Boolean\"] = \"Boolean\";\n ConfigType[\"Object\"] = \"Object\";\n ConfigType[\"Array\"] = \"Array\";\n})(ConfigType || (ConfigType = {}));\n/**\n * Represents an error that occurred while retrieving a config value\n */\nexport class ConfigError {\n message;\n constructor(message) {\n this.message = message;\n }\n}\n// Private methods here\n/**\n * Converts a string value to the specified configuration type.\n *\n * NOTE: This is not used for the config files so we dont check\n * for Object or Array types.\n *\n * @param value - The string value to convert.\n * @param type - The expected configuration type.\n * @returns The converted value as a number, string, or boolean.\n */\nfunction convertValue(value, type) {\n //Check the type\n switch (type) {\n case ConfigType.Number:\n return parseInt(value);\n case ConfigType.Boolean:\n // Only accept Y or TRUE (case insensitive) to mean true\n if (value.toUpperCase() === \"Y\" || value.toUpperCase() === \"TRUE\") {\n return true;\n }\n // Everything else is false\n return false;\n default:\n // All that is left is String and this is already a string!\n return value;\n }\n}\n/**\n * Checks the command line arguments for a configuration value matching the\n * specified configuration key.\n *\n * @param config - The configuration key to look for in the command line arguments.\n * @param type - The expected type of the configuration value.\n * @param options - Additional options for configuring the behavior of the function.\n * @returns The configuration value from the command line arguments, converted to the specified type, or `null` if the configuration value is not found.\n */\nfunction checkCli(config, type, options) {\n // Ignore the first 2 params (node bin and executable file)\n let cliParams = process.argv.slice(2);\n // The convention used for config params on the command line is:\n // Convert to lowercase, replace '_' with '-' and prepend \"--\"\n let cliParam = `--${config.toLowerCase().replaceAll(\"_\", \"-\")}`;\n // Command line flags are just prepended with a '-'\n let cmdLineFlag = options.cmdLineFlag !== undefined ? `-${options.cmdLineFlag}` : \"\";\n // If the param is assigned a value on the cli it has the format:\n // --param=value\n // otherwise the format is and it implies true:\n // --parm or -flag\n let regExp;\n if (options.cmdLineFlag === undefined) {\n // No flag specified so only look for the param and an assigned value\n regExp = new RegExp(`^${cliParam}=(.+)$`);\n }\n else {\n // Look for param and an assigned value or cmd line flag\n regExp = new RegExp(`^${cliParam}=(.+)$|^${cliParam}$|^${cmdLineFlag}$`);\n }\n let value;\n // Step through each cli params until you find a match\n for (let i = 0; i < cliParams.length; i++) {\n let match = cliParams[i].match(regExp);\n let paramOrFlag;\n if (match === null) {\n // There was no match so look at the next param\n continue;\n }\n // If a value was supplied then match[1] will contain a value\n if (match[1] !== undefined) {\n paramOrFlag = match[0];\n value = match[1];\n }\n else {\n paramOrFlag = match[0];\n // The presence of the flag/param without a value implies a true value\n value = \"Y\";\n }\n // Check if we can or should log that we found it\n // NOTE: If we log it we want to indicate is was found on the CLI\n if (!options.silent) {\n _messageStore.add(`CLI parameter/flag (${paramOrFlag}) = (${options.redact ? \"redacted\" : value})`);\n }\n // We are done so break out of the loop\n break;\n }\n // Return null if we have no value\n if (value === undefined) {\n return null;\n }\n return convertValue(value, type);\n}\n/**\n * Retrieves a configuration value from an environment variable.\n *\n * @param config - The configuration key to retrieve from the environment.\n * @param type - The expected type of the configuration value.\n * @param options - Additional options for configuring the behavior of the function.\n * @returns The configuration value converted to the specified type, or `null` if the environment variable is not set.\n */\nfunction checkEnvVar(config, type, options) {\n // NOTE: Always convert to upper case for env vars\n let evar = config.toUpperCase();\n let value = process.env[evar];\n // Return null if we have no value\n if (value === undefined) {\n return null;\n }\n // If we are here then we found it, now lets check if we can or should\n // log that we found it\n // NOTE: If we log it we want to indicate is was found in an env var\n if (!options.silent) {\n _messageStore.add(`Env var (${evar}) = (${options.redact ? \"redacted\" : value})`);\n }\n return convertValue(value, type);\n}\n/**\n * Retrieves a configuration value from an environment file store.\n *\n * @param config - The configuration key to retrieve from the environment file.\n * @param type - The expected type of the configuration value.\n * @param options - Additional options for configuring the behavior of the function.\n * @returns The configuration value converted to the specified type, or `null` if the configuration is not found in the environment file.\n */\nfunction checkEnvFile(config, type, options) {\n // NOTE: Always convert to upper case when checking the env file store\n let evar = config.toUpperCase();\n let value = _envFileStore.get(evar);\n // Return null if we have no value\n if (value === undefined) {\n return null;\n }\n // If we are here then we found it, now lets check if we can or should\n // log that we found it\n // NOTE: If we log it we want to indicate it was found in the env file\n if (!options.silent) {\n _messageStore.add(`Env var from env file (${evar}) = (${options.redact ? \"redacted\" : value})`);\n }\n return convertValue(value, type);\n}\n/**\n * Retrieves a configuration value from a configuration file store.\n *\n * @param config - The configuration key to retrieve from the configuration file.\n * @param options - Additional options for configuring the behavior of the function.\n * @returns The configuration value, or `null` if the configuration is not found in the configuration file.\n */\nfunction checkCfgFile(config, options) {\n let value = _cfgFileStore.get(config);\n // Return null if we have no value\n if (value === undefined) {\n return null;\n }\n // If we are here then we found it, now lets check if we can or should\n // log that we found it\n // NOTE: If we log it we want to indicate it was found in the cfg file\n if (!options.silent) {\n _messageStore.add(`Config from cfg file (${config}) = (${options.redact ? \"redacted\" : JSON.stringify(value)})`);\n }\n // No need for any convertions, just return the value\n return value;\n}\n/**\n * Retrieves a configuration value from various sources, with the following precedence:\n * 1. Command-line arguments\n * 2. Environment variables\n * 3. Environment file (.env)\n * 4. Configuration file\n *\n * If the configuration value is not found in any of these sources, a default value can be provided.\n *\n * @param config - The configuration key to retrieve.\n * @param type - The expected type of the configuration value.\n * @param defaultVal - The default value to use if the configuration is not found.\n * @param configOptions - Additional options for configuring the behavior of the function.\n * @returns The configuration value, or the default value if the configuration is not found.\n * @throws {ConfigError} If the configuration is required and not found.\n */\nfunction get(config, type, defaultVal, configOptions) {\n // Set up the defaults if not provided\n let options = {\n silent: false,\n redact: false,\n ...configOptions,\n };\n // Check the CLI first, i.e. CLI has higher precedence then env vars\n // of the cfg file\n let value = checkCli(config, type, options);\n if (value !== null) {\n return value;\n }\n // OK it's not in the CLI so lets check the env vars, env var has higher\n // precedence then the .env file\n value = checkEnvVar(config, type, options);\n if (value !== null) {\n return value;\n }\n // OK it's not in the env vars either so check the env file store\n value = checkEnvFile(config, type, options);\n if (value !== null) {\n return value;\n }\n // OK it's not in the env file store either so check the cfg store\n value = checkCfgFile(config, options);\n if (value !== null) {\n return value;\n }\n // If we are here then the value was not found - use default provided\n // NOTE: The default SHOULD have the correct type so do not do a conversion\n if (defaultVal === undefined) {\n // If the default was not provided then the config WAS required. In this\n // scenario we need to throw an error\n throw new ConfigError(`Config parameter (${config}) not found!`);\n }\n // Lets check if we can or should log the default value\n // NOTE: If we log it we want to indicate is the default value\n if (!options.silent) {\n _messageStore.add(`Default value used for (${config}) = (${options.redact ? \"redacted\" : defaultVal})`);\n }\n return defaultVal;\n}\n/**\n * Reads the contents of the specified .env file and adds the key-value\n * pairs to the _envFileStore.\n *\n * @param envFile - The path to the .env file to read.\n * @throws {ConfigError} If an error occurs while reading the .env file.\n */\nfunction parseEnvFile(envFile) {\n let lines = [];\n try {\n _messageStore.add(`Reading config info from .env file (${envFile})`);\n // Read env file and split it into lines ...\n let contents = fs.readFileSync(envFile, \"utf8\");\n // ... makes sure if works for DOS and linux files!\n lines = contents.split(/\\r?\\n/);\n }\n catch (e) {\n throw new ConfigError(`The following error occured when trying to open the .env file (${envFile}) - (${e})`);\n }\n // Iterate through each line\n for (let line of lines) {\n // If the line is commented out or blank then skip it\n if (line.length === 0 || line.startsWith(\"#\")) {\n continue;\n }\n // Don't use split() here because the value may contain an \"=\"\n let index = line.indexOf(\"=\");\n // Check if there was an equal in the line - if not then skip this line\n if (index === -1) {\n continue;\n }\n // Get the key/value pair - make sure to trim them as well\n let key = line.slice(0, index).trim();\n let value = line.slice(index + 1).trim();\n // Check if the value is delimited with single or double quotes\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n // Strip them away\n value = value.slice(1, value.length - 1);\n }\n // Stick it in the env file store\n // NOTE: Make key upper case to match env vars conventions\n _envFileStore.set(key.toUpperCase(), value);\n _messageStore.add(`Added (${key.toUpperCase()}) to the env file store`);\n }\n}\n/**\n * Reads the contents of a cfg file and adds the key-value pairs to the\n * configuration store.\n *\n * @param cfgFile - The path to the configuration file to read.\n * @throws {ConfigError} If an error occurs while reading or parsing the configuration file.\n */\nfunction readCfgFile(cfgFile) {\n let contents;\n try {\n _messageStore.add(`Reading config info from cfg file (${cfgFile})`);\n // Read the cfg file\n contents = fs.readFileSync(cfgFile, \"utf8\");\n }\n catch (e) {\n throw new ConfigError(`The following error occured when trying to open the cfg file (${cfgFile}) - (${e})`);\n }\n try {\n _messageStore.add(\"Adding the cfg file contents to the cfg store\");\n _cfgFileStore = new Map(Object.entries(JSON.parse(contents)));\n }\n catch (e) {\n throw new ConfigError(`The following error occured when trying to add (${contents}) to the cfg store - (${e})`);\n }\n}\n/**\n * Initializes the configuration manager by setting up the necessary stores and\n * parsing any specified environment and configuration files.\n *\n * The function first initializes the `_envFileStore`, `_cfgFileStore`, and\n * `_messageStore` stores. It then checks if a `.env` file has been specified\n * in the configuration and, if so, calls the `parseEnvFile` function to parse\n * the contents of the file and add the key-value pairs to the `_envFileStore`.\n *\n * Next, the function checks if a configuration file has been specified in the\n * configuration and, if so, calls the `readCfgFile` function to read the\n * contents of the file and add the key-value pairs to the `_cfgFileStore`.\n *\n * This function is typically called during the initialization of the\n * application to ensure that the configuration manager is properly set up and\n * ready to use.\n */\nfunction init() {\n // Initialise the stores\n _envFileStore = new Map();\n _cfgFileStore = new Map();\n _messageStore = new Set();\n // Check if the user has specified a .env file\n let envFile = configMan.getStr(CFG_ENV_FILE, \"\");\n if (envFile.length > 0) {\n parseEnvFile(envFile);\n }\n else if (fs.existsSync(\".env\")) {\n envFile = \".env\";\n _messageStore.add(`Defaulting the env-file to ${envFile}`);\n parseEnvFile(envFile);\n }\n else {\n _messageStore.add(\"No .env file specified or found\");\n }\n // Check if the user has specified a cfg file\n // NOTE: The cfg file config CAN be specified in the .env file since it\n // has already been parsed\n let cfgFile = configMan.getStr(CFG_CFG_FILE, \"\");\n if (cfgFile.length > 0) {\n readCfgFile(cfgFile);\n }\n else {\n _messageStore.add(\"No cfg file specified\");\n }\n}\n// Public methods here\n/**\n * Provides a set of functions for retrieving configuration values from various\n * sources, including environment variables and configuration files.\n *\n * The `configMan` object is a frozen object that contains the following methods:\n *\n * - `getStr(config: string, defaultVal?: string, options?: ConfigOptions): string`\n * - Retrieves a string configuration value with a default value if not set.\n * - `getBool(config: string, defaultVal?: boolean, options?: ConfigOptions): boolean`\n * - Retrieves a boolean configuration value with a default value if not set.\n * - `getNum(config: string, defaultVal?: number, options?: ConfigOptions): number`\n * - Retrieves a number configuration value with a default value if not set.\n * - `getMessages(): IterableIterator<[string, string]>`\n * - Retrieves an iterator over the key-value pairs of the message store.\n * - `clearMessages(): void`\n * - Clears all messages stored in the message store.`\n *\n * NOTE: Freezing the object prevents modifications to the exported API.\n */\nexport const configMan = Object.freeze({\n /**\n * Retrieves a string configuration value with a default value if not set.\n *\n * @param config - The name of the config parameter to retrieve.\n * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.\n * @param configOptions - The config options.\n * @returns The string configuration value, or the default value if not set.\n */\n getStr: (config, defaultVal, options) => {\n return get(config, ConfigType.String, defaultVal, options);\n },\n /**\n * Retrieves a boolean configuration value with a default value if not set.\n *\n * @param config - The name of the config parameter to retrieve.\n * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.\n * @param configOptions - The config options.\n * @returns The boolean configuration value, or the default value if not set.\n */\n getBool: (config, defaultVal, options) => {\n return get(config, ConfigType.Boolean, defaultVal, options);\n },\n /**\n * Retrieves a number configuration value with a default value if not set.\n *\n * @param config - The name of the config parameter to retrieve.\n * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.\n * @param configOptions - The config options.\n * @returns The number configuration value, or the default value if not set.\n */\n getNum: (config, defaultVal, options) => {\n return get(config, ConfigType.Number, defaultVal, options);\n },\n /**\n * Retrieves an object configuration value with a default value if not set.\n *\n * @param config - The name of the config parameter to retrieve.\n * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.\n * @param configOptions - The config options.\n * @returns The object configuration value, or the default value if not set.\n */\n getObject: (config, defaultVal, options) => {\n return get(config, ConfigType.Object, defaultVal, options);\n },\n /**\n * Retrieves an array configuration value with a default value if not set.\n *\n * @param config - The name of the config parameter to retrieve.\n * @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.\n * @param configOptions - The config options.\n * @returns The array configuration value, or the default value if not set.\n */\n getArray: (config, defaultVal, options) => {\n return get(config, ConfigType.Array, defaultVal, options);\n },\n /**\n * Retrieves an iterator over the key-value pairs of the message store.\n *\n * @returns An iterator over the key-value pairs of the message store.\n */\n getMessages: () => {\n return _messageStore.entries();\n },\n /**\n * Clears all messages stored in the message store.\n */\n clearMessages: () => {\n _messageStore.clear();\n },\n});\n// Time to kick this puppy!\ninit();\n//# sourceMappingURL=config-man.js.map","// imports here\nimport { configMan } from \"./config-man.js\";\nimport * as util from \"node:util\";\n// Config consts here\nconst CFG_LOG_LEVEL = \"LOG_LEVEL\";\nconst CFG_LOG_TIMESTAMP = \"LOG_TIMESTAMP\";\nconst CFG_LOG_TIMESTAMP_LOCALE = \"LOG_TIMESTAMP_LOCALE\";\nconst CFG_LOG_TIMESTAMP_TZ = \"LOG_TIMESTAMP_TZ\";\n// Types here\nexport var LogLevel;\n(function (LogLevel) {\n LogLevel[LogLevel[\"COMPLETE_SILENCE\"] = 0] = \"COMPLETE_SILENCE\";\n LogLevel[LogLevel[\"QUIET\"] = 100] = \"QUIET\";\n LogLevel[LogLevel[\"INFO\"] = 200] = \"INFO\";\n LogLevel[LogLevel[\"START_UP\"] = 250] = \"START_UP\";\n LogLevel[LogLevel[\"DEBUG\"] = 300] = \"DEBUG\";\n LogLevel[LogLevel[\"TRACE\"] = 400] = \"TRACE\";\n})(LogLevel || (LogLevel = {}));\n// Logger class here\nexport class Logger {\n // Private properties here\n _name;\n _timestamp;\n _timestampLocale;\n _timestampTz;\n _logLevel;\n _formatter;\n // Private methods here\n /**\n * Generates a timestamp string to prefix log messages.\n * Returns an empty string if timestamps are disabled. Otherwise returns\n * the formatted timestamp string.\n */\n timestamp() {\n // If we are not supposed to generate timestamps then return nothing\n if (!this._timestamp) {\n return \"\";\n }\n let now = new Date();\n // Check if we should formet the time in ISO format\n if (this._timestampLocale === \"ISO\") {\n // Make sure to add a trailing space!\n return `${now.toISOString()} `;\n }\n // Make sure to add a trailing space!\n return `${this._formatter.format(now)} `;\n }\n convertLevel(level) {\n let logLevel;\n switch (level.toUpperCase()) {\n case \"\": // This is in case it is not set\n logLevel = LogLevel.INFO;\n break;\n case \"SILENT\":\n logLevel = LogLevel.COMPLETE_SILENCE;\n break;\n case \"QUIET\":\n logLevel = LogLevel.QUIET;\n break;\n case \"INFO\":\n logLevel = LogLevel.INFO;\n break;\n case \"STARTUP\":\n logLevel = LogLevel.START_UP;\n break;\n case \"DEBUG\":\n logLevel = LogLevel.DEBUG;\n break;\n case \"TRACE\":\n logLevel = LogLevel.TRACE;\n break;\n default:\n throw new Error(`Log Level (${level}) is unknown.`);\n }\n return logLevel;\n }\n // constructor here\n constructor(name) {\n this._name = name;\n this._timestamp = configMan.getBool(CFG_LOG_TIMESTAMP, false);\n this._timestampLocale = configMan.getStr(CFG_LOG_TIMESTAMP_LOCALE, \"ISO\");\n this._timestampTz = configMan.getStr(CFG_LOG_TIMESTAMP_TZ, \"UTC\");\n this._logLevel = this.convertLevel(configMan.getStr(CFG_LOG_LEVEL, \"\"));\n this._formatter = new Intl.DateTimeFormat(this._timestampLocale, {\n timeZone: this._timestampTz,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n fractionalSecondDigits: 3,\n });\n // Now get the messages from the confgiMan for display\n let messages = configMan.getMessages();\n for (const message of messages) {\n this.startupMsg(\"Logger\", message[0]);\n }\n configMan.clearMessages();\n }\n fatal(...args) {\n // fatals are always logged\n let msg = util.format(`${this.timestamp()}FATAL: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.error(msg);\n }\n error(...args) {\n // errors are always logged unless level = LOG_COMPLETE_SILENCE\n if (this._logLevel > LogLevel.COMPLETE_SILENCE) {\n let msg = util.format(`${this.timestamp()}ERROR: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.error(msg);\n }\n }\n warn(...args) {\n // warnings are always logged unless level = LOG_COMPLETE_SILENCE\n if (this._logLevel > LogLevel.COMPLETE_SILENCE) {\n let msg = util.format(`${this.timestamp()}WARN: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.warn(msg);\n }\n }\n info(...args) {\n if (this._logLevel >= LogLevel.INFO) {\n let msg = util.format(`${this.timestamp()}INFO: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.info(msg);\n }\n }\n startupMsg(...args) {\n if (this._logLevel >= LogLevel.START_UP) {\n let msg = util.format(`${this.timestamp()}STARTUP: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.info(msg);\n }\n }\n shutdownMsg(...args) {\n if (this._logLevel >= LogLevel.START_UP) {\n let msg = util.format(`${this.timestamp()}SHUTDOWN: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.info(msg);\n }\n }\n debug(...args) {\n if (this._logLevel >= LogLevel.DEBUG) {\n let msg = util.format(`${this.timestamp()}DEBUG: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.info(msg);\n }\n }\n trace(...args) {\n if (this._logLevel >= LogLevel.TRACE) {\n let msg = util.format(`${this.timestamp()}TRACE: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.info(msg);\n }\n }\n force(...args) {\n // forces are always logged even if level == LOG_COMPLETE_SILENCE\n let msg = util.format(`${this.timestamp()}FORCED: ${this._name}: ${args[0]}`, ...args.slice(1));\n console.error(msg);\n }\n setLevel(level) {\n this._logLevel = level;\n }\n}\n//# sourceMappingURL=logger.js.map","// imports here\nimport * as readline from \"node:readline\";\nimport * as crypto from \"node:crypto\";\n// Consts here\nconst SECRET_IV_SIZE = 12;\nconst SECRET_KEY_SIZE = 32;\nconst SECRET_ALGO = \"aes-256-gcm\";\nexport const sleep = async (durationInSeconds) => {\n // Convert duration to ms\n let ms = Math.round(durationInSeconds * 1000);\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n};\nexport const question = async (ask, questionOptions) => {\n let input = process.stdin;\n let output = process.stdout;\n let options = {\n muteAnswer: false,\n muteChar: \"*\",\n ...questionOptions,\n };\n return new Promise((resolve) => {\n let rl = readline.createInterface({\n input,\n output,\n });\n if (options.muteAnswer) {\n input.on(\"keypress\", () => {\n // get the number of characters entered so far:\n var len = rl.line.length;\n if (options.muteChar.length === 0) {\n // move cursor back one since we will always be at the start\n readline.moveCursor(output, -1, 0);\n // clear everything to the right of the cursor\n readline.clearLine(output, 1);\n }\n else {\n // move cursor back to the beginning of the input\n readline.moveCursor(output, -len, 0);\n // clear everything to the right of the cursor\n readline.clearLine(output, 1);\n // If there is a muteChar then replace the original input with it\n for (var i = 0; i < len; i++) {\n // In case the user passes a string just use the 1st char\n output.write(options.muteChar[0]);\n }\n }\n });\n }\n // Insert a space after the question for convience\n rl.question(`${ask} `, (answer) => {\n resolve(answer);\n rl.close();\n });\n });\n};\nexport const encryptSecret = (secret, key, outputEncoding = \"base64\", // used for strings and Buffers\ninputEncoding = \"utf-8\") => {\n // Make sure the key len is 32 - this is a requirement\n if (key.length !== SECRET_KEY_SIZE) {\n return null;\n }\n // Check if the secret is a buffer or string\n const isBuffer = Buffer.isBuffer(secret);\n // Create a random IV\n const iv = crypto.randomBytes(SECRET_IV_SIZE);\n // Create a new cipher object\n const cipher = crypto.createCipheriv(SECRET_ALGO, key, iv);\n // Encrypt the secret - check if it is a string or Buffer\n let cipherText = isBuffer\n ? cipher.update(secret, undefined, outputEncoding) // No inputEncoding for Buffers\n : cipher.update(secret, inputEncoding, outputEncoding);\n cipherText += cipher.final(outputEncoding);\n // Generate the authentication tag\n const tag = cipher.getAuthTag();\n const encrypted = {\n isBuffer,\n iv: iv.toString(outputEncoding), // convert the Buffer to string\n authTag: tag.toString(outputEncoding), // convert the Buffer to string\n cipherText,\n };\n return encrypted;\n};\nexport const decryptSecret = (secret, key, inputEncoding = \"base64\", // outputEncoding from encryptSecret\noutputEncoding = \"utf-8\") => {\n // Make sure the key len is 32 - this is a requirement\n if (key.length !== SECRET_KEY_SIZE) {\n return null;\n }\n // Create a decipher object\n const iv = Buffer.from(secret.iv, inputEncoding);\n const decipher = crypto.createDecipheriv(SECRET_ALGO, key, iv);\n // Set the authentication tag\n const tag = Buffer.from(secret.authTag, inputEncoding);\n decipher.setAuthTag(tag);\n let decrypted = null;\n // Decrypt the secret\n try {\n // Check if the secret is a Buffer or a string\n if (secret.isBuffer) {\n decrypted = Buffer.concat([\n decipher.update(secret.cipherText, inputEncoding), // no outputEncoding for Buffers\n decipher.final(), // no outputEncoding for Buffers\n ]);\n }\n else {\n decrypted =\n decipher.update(secret.cipherText, inputEncoding, outputEncoding) +\n decipher.final(outputEncoding);\n }\n }\n catch (_) { }\n return decrypted;\n};\n//# sourceMappingURL=utils.js.map","// NOTE: To use this with endpoints using self signed certs add this env var\n// NODE_TLS_REJECT_UNAUTHORIZED=0\n// imports here\nimport { sleep } from \"./utils.js\";\nimport { Logger } from \"./logger.js\";\nimport { performance } from \"node:perf_hooks\";\n// Misc consts here\nconst LOG_TAG = \"request\";\n// Module private variables here\nconst _logger = new Logger(LOG_TAG);\n// Error classes here\nexport class ReqAborted {\n timedOut;\n message;\n constructor(timedOut, message) {\n this.timedOut = timedOut;\n this.message = message;\n }\n}\nexport class ReqError {\n status;\n message;\n constructor(status, message) {\n this.status = status;\n this.message = message;\n }\n}\n// Private methods here\nasync function callFetch(origin, path, options, body) {\n // Build the url\n let url = `${origin}${path}`;\n // And add the query string if one has been provided\n if (options.searchParams !== undefined) {\n url += `?${new URLSearchParams(options.searchParams)}`;\n }\n let results;\n let timeoutTimer;\n let attempt = 0;\n do {\n // Increment attempt\n attempt += 1;\n // Create an AbortController and use a timer if no signal has been provided\n if (options.signal === undefined) {\n const controller = new AbortController();\n options.signal = controller.signal;\n // Start your engines!\n timeoutTimer = setTimeout(() => {\n controller.abort();\n }, options.timeoutS * 1000);\n }\n results = await fetch(url, {\n method: options.method,\n headers: options.headers,\n body,\n keepalive: options.keepalive,\n cache: options.cache,\n credentials: options.credentials,\n mode: options.mode,\n redirect: options.redirect,\n referrer: options.referrer,\n referrerPolicy: options.referrerPolicy,\n signal: options.signal,\n }).catch((e) => {\n // OK, there was an error - check if the request was aborted\n if (e.name === \"AbortError\") {\n // If there was a timeoutTimer then the req must have timed out\n if (timeoutTimer !== undefined) {\n // NOTE: We DO NOT need to clear the timer since it already fired\n throw new ReqAborted(true, `Request timeout out after ${options.timeoutS} seconds`);\n }\n throw new ReqAborted(false, \"Request aborted\");\n }\n // Check if we started a timer and if so stop it - it hasnt expired yet\n if (timeoutTimer !== undefined) {\n clearTimeout(timeoutTimer);\n }\n // We don't know what the error is so pass it up the chain\n throw e;\n });\n // Check if we started a timer and if so stop it - it hasnt expired yet\n if (timeoutTimer !== undefined) {\n clearTimeout(timeoutTimer);\n }\n // Check if the fetch failed\n if (!results.ok) {\n // Figure out if we wait and try again or throw an error\n const status = results.status;\n if (attempt < options.retries + 1 && // Still attempts left (note mac attempts are numRetries + 1)\n (status >= 500 || // Server error\n status === 408 || // Req timeout\n status === 429) // Too many requests\n ) {\n // We can go again, now we need to introduce a delay\n // Use baseDelay as a multiple of the attempts and some random jitter\n const delay = options.baseDelayMs * 2 ** attempt;\n const jitter = Math.random() * delay * 0.5;\n const totalDelay = Math.round(delay + jitter);\n _logger.trace(\"Call to (%s) failed. Retrying in (%d) ms.\", url, totalDelay);\n // Now just sleep for the delay\n await sleep(totalDelay / 1000);\n }\n else {\n // If we are here we are not going to retry\n // So, get the last error and throw it\n let message = await results.text();\n throw new ReqError(status, message.length === 0 ? results.statusText : message);\n }\n }\n // Loop if the results are NOT OK\n } while (!results.ok);\n // We can only be here if everything was OK\n return results;\n}\nasync function handleResponseData(results) {\n // No point worrying if the body is JSON at first, because we know its text\n const body = await results.text();\n // If the body exists then check if it is JSON\n if (body.length > 0) {\n // Check if the content type is JSON\n const contentType = results.headers.get(\"content-type\");\n if (contentType?.startsWith(\"application/json\")) {\n return JSON.parse(body);\n }\n }\n // If we are here, the body wasnt JSON so just return the text\n return body;\n}\n// Public methods here\nexport let request = async (origin, path, reqOptions) => {\n // We need to remember the start time\n const startTime = performance.now();\n _logger.trace(\"Request for origin (%s) path (%s)\", origin, path);\n // Set the default values\n let options = {\n method: \"GET\",\n timeoutS: 60,\n retries: 3,\n baseDelayMs: 500,\n keepalive: true,\n handleResponse: true,\n cache: \"no-store\",\n mode: \"cors\",\n credentials: \"include\",\n redirect: \"follow\",\n referrerPolicy: \"no-referrer\",\n ...reqOptions,\n };\n // Make sure the headers is set to something for later\n if (options.headers === undefined) {\n options.headers = {};\n }\n // If a bearer token is provided then add a Bearer auth header\n if (options.bearerToken !== undefined) {\n options.headers.Authorization = `Bearer ${options.bearerToken}`;\n }\n // If the basic auth creds are provided add a Basic auth header\n if (options.auth !== undefined) {\n let token = Buffer.from(`${options.auth.username}:${options.auth.password}`).toString(\"base64\");\n options.headers.Authorization = `Basic ${token}`;\n }\n let payloadBody;\n // Automatically stringify and set the header if this is a JSON payload\n // BUT dont do it for GETs and DELETE since they can have no body\n if (options.body !== undefined &&\n options.method !== \"GET\" &&\n options.method !== \"DELETE\") {\n // Rem an array is an object to!\n if (typeof options.body === \"object\") {\n // Add the content-type if it hasn't been provided\n if (options.headers?.[\"content-type\"] === undefined) {\n options.headers[\"content-type\"] = \"application/json; charset=utf-8\";\n }\n payloadBody = JSON.stringify(options.body);\n }\n else {\n payloadBody = options.body;\n }\n }\n // Call fetch\n let response = await callFetch(origin, path, options, payloadBody);\n // Build the response\n let res = {\n statusCode: response.status,\n headers: response.headers,\n body: undefined, // set to undefined for now\n responseTime: 0,\n };\n // Check if we should handle the response for the user\n if (options.handleResponse) {\n // Yes, so handle and set the body\n res.body = await handleResponseData(response).catch((e) => {\n const msg = `Error handling response data for (${origin}) (${path}) - (${e}))`;\n throw new Error(msg);\n });\n }\n else {\n // No, so set the response\n res.response = response;\n }\n // Don't forget to set the response time\n res.responseTime = Math.round(performance.now() - startTime);\n _logger.trace(\"Request for origin (%s) path (%s) took (%i) ms\", origin, path, res.responseTime);\n return res;\n};\n//# sourceMappingURL=http-req.js.map","import { performance } from \"node:perf_hooks\";\nimport { Readable } from \"node:stream\";\n// Classes here\nexport class HttpError {\n status;\n message;\n constructor(status, message = \"Achtung Baby!\") {\n this.status = status;\n this.message = message;\n }\n}\nexport class HttpRedirect {\n statusCode;\n location;\n message;\n constructor(statusCode = 302, location, message = \"\") {\n this.statusCode = statusCode;\n this.location = location;\n this.message = message;\n }\n}\n// This method enhances an existing IncomingMessage to be a ServerRequest\nexport const enhanceIncomingMessage = (req) => {\n // Treat req like a ServerRequest (which it is minus some props and methods)\n let enhancedReq = req;\n // Now set all of the props that make it a ServerRequest\n // Check for a valid url or \"new URL()\" to throw\n // NOTE: I can not figure out a good way to tell if the req is HTTP or HTTPS\n // but I dont think it matters here unless someone needs the protocol prop\n enhancedReq.validUrl = URL.canParse(req.url, `https://${req.headers.host}`);\n // If URL is valid check to see if the it has any encodings that arent valid\n if (enhancedReq.validUrl) {\n try {\n // This will not throw if everything is good\n decodeURIComponent(req.url);\n }\n catch {\n // if we are here then the URL encoding is not valid so flag it\n enhancedReq.validUrl = false;\n }\n }\n // If the URL is not valid then we should return now\n if (!enhancedReq.validUrl) {\n return enhancedReq;\n }\n // If we are here we know the URL is valid so it is safe to use it\n enhancedReq.urlObj = new URL(req.url, `https://${req.headers.host}`);\n enhancedReq.params = {};\n enhancedReq.handled = false;\n // By default we will compress the response\n enhancedReq.compressResponse = true;\n // By default we shld check all the different types of routes\n enhancedReq.checkApiRoutes = true;\n enhancedReq.checkSsrRoutes = true;\n enhancedReq.checkStaticFiles = true;\n // By default the req handler is expected to handle the 404\n enhancedReq.handle404 = true;\n // Now set all of the public methods that make it a ServerRequest\n enhancedReq.getCookie = (cookieName) => {\n // Get the cookie header and spilt it up by cookies -\n // NOTE: cookies are separated by semi colons\n let cookies = enhancedReq.headers.cookie?.split(\";\");\n if (cookies === undefined) {\n // Nothing to do so just return\n return null;\n }\n // Loop through the cookies\n for (let cookie of cookies) {\n // Split the cookie up into a key value pair\n // NOTE: key/value is separated by an equals sign and has leading spaces\n let [name, value] = cookie.trim().split(\"=\");\n // Make sure it was a validly formatted cookie\n if (value === undefined) {\n // It is not a valid cookie so skip it\n continue;\n }\n // Check if we found the cookie\n if (name === cookieName) {\n // Return the cookie value\n return value;\n }\n }\n return null;\n };\n enhancedReq.setServerTimingHeader = (value) => {\n enhancedReq.headers[\"Server-Timing\"] = value;\n };\n enhancedReq.getBearerToken = () => {\n const authzHeader = req.headers[\"authorization\"];\n // The authz header MUST be a string\n if (typeof authzHeader !== \"string\") {\n return null;\n }\n // This is the length of \"Bearer \" in the authZ header\n // NOTE: Must include the space at the end!\n const LEN_OF_BEARER = 7;\n if (authzHeader.length > LEN_OF_BEARER &&\n authzHeader.slice(0, LEN_OF_BEARER).toLowerCase().startsWith(\"bearer \")) {\n return authzHeader.slice(LEN_OF_BEARER);\n }\n return null;\n };\n return enhancedReq;\n};\n// This method enhances an existing ServerResponse to be a ServerResponse\nexport const enhanceServerResponse = (res) => {\n // Treat res like a ServerResponse (which it is minus the props and methods)\n let enhancedRes = res;\n // Now set all of the props that make it a ServerRequest\n // NOTE: enhancedRes will be created at the same time as ServerRequest\n enhancedRes.receiveTime = performance.now();\n enhancedRes.redirected = false;\n enhancedRes.latencyMetricName = \"latency\";\n enhancedRes.serverTimingsMetrics = [];\n enhancedRes.enhancedReq = enhancedRes.req;\n // Now set all of the public methods that make it a ServerResponse\n enhancedRes.setCookies = (cookies) => {\n let setCookiesValue = [];\n // Check for exiting cookies and add them to the setCookiesValue array\n let existing = enhancedRes.getHeader(\"Set-Cookie\");\n if (typeof existing === \"string\") {\n setCookiesValue.push(existing);\n }\n else if (Array.isArray(existing)) {\n setCookiesValue = existing;\n }\n // Loop through each cookie and build the cookie values\n for (let cookie of cookies) {\n // Set the cookie value first\n let value = `${cookie.name}=${cookie.value}`;\n // if there is a maxAge then set it - NOTE: put \";\" first\n if (cookie.maxAge !== undefined) {\n value += `; Max-Age=${cookie.maxAge}`;\n }\n // If there is a path then set it or use default path of \"/\" - NOTE: put \";\" first\n if (cookie.path !== undefined) {\n value += `; Path=${cookie.path}`;\n }\n else {\n value += `; Path=/`;\n }\n // If httpOnly is indicated then add it - NOTE: put \";\" first\n if (cookie.httpOnly === true) {\n value += \"; HttpOnly\";\n }\n // If secure is indicated set then add it - NOTE: put \";\" first\n if (cookie.secure === true) {\n value += \"; Secure\";\n }\n // If sameSite has been provided then add it - NOTE: put \";\" first\n if (cookie.sameSite !== undefined) {\n value += `; SameSite=${cookie.sameSite}`;\n }\n // If domain has been provided then add it - NOTE: put \";\" first\n if (cookie.domain !== undefined) {\n value += `; Domain=${cookie.domain}`;\n }\n // Save the cookie\n setCookiesValue.push(value);\n }\n // Finally set the cookie/s in the response header\n enhancedRes.setHeader(\"Set-Cookie\", setCookiesValue);\n };\n enhancedRes.clearCookies = (cookies, domain) => {\n let httpCookies = [];\n for (let cookie of cookies) {\n // To clear a cookie - set value to empty string and max age to -1\n httpCookies.push({ name: cookie, value: \"\", maxAge: -1, domain });\n }\n enhancedRes.setCookies(httpCookies);\n };\n enhancedRes.setServerTimingHeader = () => {\n let serverTimingHeaders = [];\n // Check if the req has a Server-Timing header. This is not normal but I\n // want to something like a forwardAuth server to be able to add it's\n // metrics to the response header\n if (enhancedRes?.req?.headers[\"server-timing\"] !== undefined) {\n const reqTimings = enhancedRes.req.headers[\"server-timing\"];\n // Check if there are multiple headers\n if (Array.isArray(reqTimings)) {\n // If so then since this is the first just use it as the headers array\n serverTimingHeaders = reqTimings;\n }\n else {\n // If not then just add it to the array\n serverTimingHeaders.push(reqTimings);\n }\n }\n let serverTimingValue = \"\";\n // Add each additional metric added to the res next so they are in\n // the order they were added\n for (let metric of enhancedRes.serverTimingsMetrics) {\n // Check if we have a string or a metric object\n if (typeof metric === \"string\") {\n // The string version is already formatted so just add to the array\n serverTimingHeaders.push(metric);\n continue;\n }\n // If we are here then we have a metric object so add the name\n serverTimingValue += metric.name;\n // Check if there is an optional duration\n if (metric.duration !== undefined) {\n serverTimingValue += `;dur=${metric.duration}`;\n }\n // Check if there is an optional description\n if (metric.description !== undefined) {\n serverTimingValue += `;desc=\"${metric.description}\"`;\n }\n serverTimingValue += \", \";\n }\n // Finally add the total latency for the endpoint to the array\n const latency = Math.round(performance.now() - enhancedRes.receiveTime);\n serverTimingValue += `${enhancedRes.latencyMetricName};dur=${latency}`;\n serverTimingHeaders.push(serverTimingValue);\n // Of course don't forget to set the header!!\n enhancedRes.setHeader(\"Server-Timing\", serverTimingHeaders);\n };\n enhancedRes.redirect = (location, statusCode = 302, message = \"\") => {\n enhancedRes.redirected = true;\n let htmlMessage = message.length > 0\n ? message\n : `Redirected to <a href=\"${location}\">here</a>`;\n // Write a little something something for good measure\n enhancedRes.body = `\n <html>\n <body>\n <p>${htmlMessage}</p>\n </body>\n </html>`;\n enhancedRes.setHeader(\"Content-Type\", \"text/html; charset=utf-8\");\n enhancedRes.setHeader(\"Location\", location);\n enhancedRes.statusCode = statusCode;\n };\n enhancedRes.addServerTimingMetric = (name, duration, description) => {\n // This adds a metric to the Server-Timing header for this response\n enhancedRes.serverTimingsMetrics.push({ name, duration, description });\n };\n enhancedRes.addServerTimingHeader = (header) => {\n // This adds a complete Server-Timing header to this response\n enhancedRes.serverTimingsMetrics.push(header);\n };\n return enhancedRes;\n};