UNPKG

zyx-env

Version:

Implements routines to work with encrypted environment variable files

143 lines (116 loc) 4.24 kB
// loadEnvFile.js: "use strict"; const fs = require("fs"); const path = require("path"); const system = require("zyx-system"); const TopSecret = require("topsecret"); const isJsonLike = require("./isJsonLike.js"); const { coercePrimitive } = require("zyx-system"); const createReadOnlyObject = require("./createReadOnlyObject"); /** * Load and validate environment configuration from a file. * * @param {string} filename - Path to the env file. * @param {string|null} encryptKey - Optional decryption key if file is encrypted. * @param {Object|null} schema - Optional schema validator with .validate(). * @param {Object} [options] - Optional config. * @param {boolean} [options.verbose=false] - Print loaded keys. * @returns {Object} Read-only configuration object. */ function loadEnvFile(filename, encryptKey = null, schema = {}, options = {}) { system.debug(`loadEnvFile: ${filename}`); system.debug("encryptKey", encryptKey); // destructure verbose flag from options and use false if not specified const { verbose = false } = options; // if the filename is relative then use current working directory as root path const resolvedPath = path.resolve(process.cwd(), filename); // ensure the file exists if (!fs.existsSync(resolvedPath)) { throw new Error(`Configuration file not found: ${resolvedPath}`); } // declare variable which is out of nested scoping let raw; // if no encryption key then read it as plain text if (!encryptKey) { raw = fs.readFileSync(resolvedPath, "utf-8"); } else { // read and decrypt the file into a text buffer const ts = new TopSecret(); ts.key = encryptKey; try { // attempt to decrypt the buffer raw = ts.decryptBufferFromFile(resolvedPath); } catch (err) { const message = `Failed to decrypt file. (${filename})`; system.debug(message); throw new Error(message); } } // split the buffer into lines const lines = raw.split(/\r?\n/); // initialize empty JSON object for raw values const rawValues = {}; // loop thru all lines in buffer lines.forEach(line => { // trim any leading or tailing spaces const trimmed = line.trim(); // skip any lines beginning with comment symbol if (!trimmed || trimmed.startsWith("#")) { return; } // split the line at the equal sign const [name, ...rest] = trimmed.split("="); // if name is blank then move to next line if (!name) { return; } // trim any leading or tailing spaces from name and store in key variable const key = name.trim(); // Strip inline comment and trim again let value = rest .join("=") .replace(/\s+#.*$/, "") .trim(); // see if the value looks likes a JSON object if (isJsonLike(value)) { try { // attempt to parse the value into an object value = JSON.parse(value); } catch { // fallback to plain string } } else { // attempt to coerce string into primative value value = coercePrimitive(value); } // now assign the value to the raw values object rawValues[key] = value; }); // assign raw values to validated object let validated = rawValues; // if schema specified and it has validate function if (schema && typeof schema.validate === "function") { const { validated: result, errors } = schema.validate(rawValues); // check to see if one or more errors if (errors && errors.length > 0) { const message = [ `Fatal configuration error(s) detected in "${filename}":`, ...errors.map(err => `- ${err.field}: ${err.message}`), ].join("\n"); throw new Error(message); } // assign results into validated variable validated = result; } // if verbose logging requested in options if (verbose) { console.log(`Loaded configuration from "${filename}":`); Object.entries(validated).forEach(([key, value]) => { const display = typeof value === "object" ? JSON.stringify(value) : value; console.log(` ${key} = ${display}`); }); } // return the validated object as a read only version return createReadOnlyObject(validated); } module.exports = loadEnvFile;