UNPKG

@expressots/shared

Version:

Shared library for ExpressoTS modules 🐎

317 lines (316 loc) 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports._configVault = _configVault; exports.config = config; exports.configDotenv = configDotenv; exports.parse = parse; exports.decrypt = decrypt; exports.populate = populate; exports._parseVault = _parseVault; exports._vaultPath = _vaultPath; exports._dotenvKey = _dotenvKey; exports._instructions = _instructions; exports._resolveHome = _resolveHome; /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const crypto_1 = __importDefault(require("crypto")); const constants_1 = require("./constants"); const logger_1 = require("../utils/logger"); /** * Module to parse the .env.vault file * @param options - The configuration options * @returns The parsed object */ function _parseVault(options) { const vaultPath = _vaultPath(options); const result = configDotenv({ path: vaultPath }); if (!result.parsed) { const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); err.name = "MISSING_DATA"; throw err; } //DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod, // dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod" const keys = _dotenvKey(options).split(","); const length = keys.length; let decrypted = ""; for (let i = 0; i < length; i++) { try { const key = keys[i].trim(); const attrs = _instructions(result, key); decrypted = decrypt(attrs.ciphertext, attrs.key); break; } catch (error) { if (i + 1 >= length) { throw error; } } } return parse(decrypted); } /** * Module to verify and return the .env.vault file path * @param options - The configuration options * @returns The .env.vault file path */ function _vaultPath(options) { let possibleVaultPath = null; if (options && options.path && options.path.length > 0) { if (Array.isArray(options.path)) { for (const filepath of options.path) { if (fs_1.default.existsSync(filepath)) { possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; } } } else { possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; } } else { possibleVaultPath = path_1.default.resolve(process.cwd(), ".env.vault"); } if (fs_1.default.existsSync(possibleVaultPath)) { return possibleVaultPath; } return null; } /** * Module to verify and return the DOTENV_KEY vault key * @param options - The configuration options * @returns The DOTENV_KEY as a string */ function _dotenvKey(options) { if (options && options.vaultEnvKey && options.vaultEnvKey.length > 0) { return options.vaultEnvKey; } if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { return process.env.DOTENV_KEY; } return ""; } /** * Module to get instructions for decrypting the .env.vault file * @param result - * @param dotenvKey * @returns */ function _instructions(result, dotenvKey) { let uri = null; try { uri = new URL(dotenvKey); } catch (error) { if (error.code === "ERR_INVALID_URL") { const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); err.code = "INVALID_DOTENV_KEY"; throw err; } throw error; } // Get decrypt key const key = uri.password; if (!key) { const err = new Error("INVALID_DOTENV_KEY: Missing key part"); err.code = "INVALID_DOTENV_KEY"; throw err; } // Get environment const environment = uri.searchParams.get("environment"); if (!environment) { const err = new Error("INVALID_DOTENV_KEY: Missing environment part"); err.code = "INVALID_DOTENV_KEY"; throw err; } // Get ciphertext payload const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; const ciphertext = result.parsed[environmentKey]; // DOTENV_VAULT_PRODUCTION if (!ciphertext) { const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; throw err; } return { ciphertext, key }; } /** * Module responsible to resolve home path * @param envPath - The path to resolve * @returns The resolved path */ function _resolveHome(envPath) { return envPath[0] === "~" ? path_1.default.join(os_1.default.homedir(), envPath.slice(1)) : envPath; } /** * Module to load environment variables from .env.vault file * @param options - The configuration options * @returns The parsed object */ function _configVault(options) { (0, logger_1.log)("Loading env from encrypted .env.vault"); const parsed = _parseVault(options); let processEnv = process.env; if (options && options.envObject != null) { processEnv = options.envObject; } populate(processEnv, parsed, options); return { parsed }; } /** * Module to load environment variables from .env file * @param options - The configuration options * @returns The parsed object * @public API */ function config(options) { if (_dotenvKey(options).length === 0) { return configDotenv(options); } const vaultPath = _vaultPath(options); console.log(vaultPath); if (!vaultPath) { (0, logger_1.log)(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`, logger_1.LogLevel.Warn); return configDotenv(options); } return _configVault(options); } /** * Module to load environment variables from .env file * @param options - The configuration options * @returns The parsed object * @public API */ function configDotenv(options) { const dotenvPath = path_1.default.resolve(process.cwd(), String(options?.path ?? ".env")); const encoding = (options.encoding ?? "utf8"); const debug = !!options.debug; const paths = Array.isArray(options.path) ? options.path.map(_resolveHome) : [_resolveHome(dotenvPath)]; const parsed = {}; let lastError; for (const envPath of paths) { try { const fileContent = fs_1.default.readFileSync(envPath, { encoding }); const parsedContent = parse(fileContent); populate(process.env, parsedContent, options); } catch (error) { lastError = error; if (debug) { (0, logger_1.log)(`Failed to load ${envPath} file with error: ${error.message}`, logger_1.LogLevel.Debug); } } } return { parsed, error: lastError }; } /** * Module to load environment variables from .env file * @param envFile - The source of the .env file * @returns The parsed object * @public API */ function parse(envFile) { const obj = {}; const lines = envFile.toString().replace(/\r\n?/gm, "\n"); let match; while ((match = constants_1.LINE_REGEX.exec(lines)) != null) { const key = match[1].trim(); let value = match[2]?.trim() ?? ""; if (["'", '"', "`"].includes(value[0])) { value = value.slice(1, -1); } value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); if (match[2]?.[0] === '"') { value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r"); } obj[key] = value; } return obj; } /** * Decrypts a base64 encoded string * @param encrypted - The base64 encoded string to decrypt * @param keyStr - The key to use for decryption * @returns The decrypted string * @public API */ function decrypt(encrypted, keyStr) { const key = Buffer.from(keyStr.slice(-64), "hex"); let ciphertext = Buffer.from(encrypted, "base64"); const nonce = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(-16); ciphertext = ciphertext.subarray(12, -16); try { const aesgcm = crypto_1.default.createDecipheriv("aes-256-gcm", key, nonce); aesgcm.setAuthTag(authTag); return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; } catch (error) { const isRange = error instanceof RangeError; const invalidKeyLength = error.message === "Invalid key length"; const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; if (isRange || invalidKeyLength) { const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); err.name = "INVALID_DOTENV_KEY"; throw err; } else if (decryptionFailed) { const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); err.name = "DECRYPTION_FAILED"; throw err; } else { throw error; } } } /** * Populates the environment with the given parsed object * @param envObject - The object to populate the environment with (e.g. process.env) * @param parsed - The parsed object * @param options - The configuration options * @public API */ function populate(envObject, // Usually process.env parsed, options = {}) { const debug = Boolean(options && options.debug); const override = Boolean(options && options.override); if (typeof parsed !== "object") { const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); err.name = "OBJECT_REQUIRED"; throw err; } // Set process.env (envObject) for (const key of Object.keys(parsed)) { const parsedValue = parsed[key]; // Decide whether to override existing env var or not if (Object.prototype.hasOwnProperty.call(envObject, key)) { if (override) { envObject[key] = parsedValue; if (debug) { (0, logger_1.log)(`"${key}" was overwritten to "${parsedValue}"`, logger_1.LogLevel.Debug); } } else if (debug) { (0, logger_1.log)(`"${key}" was NOT overwritten (already exists)`, logger_1.LogLevel.Debug); } } else { // Variable doesn't exist in process.env, so set it envObject[key] = parsedValue; if (debug) { (0, logger_1.log)(`"${key}" was set to "${parsedValue}"`, logger_1.LogLevel.Debug); } } } // Final debug log to ensure variables are correctly populated if (debug) { console.log("Final process.env object:", envObject); } }