Spaces:
Runtime error
Runtime error
| const fs = require('fs') | |
| const path = require('path') | |
| const os = require('os') | |
| const crypto = require('crypto') | |
| const packageJson = require('../package.json') | |
| const version = packageJson.version | |
| const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg | |
| // Parse src into an Object | |
| function parse (src) { | |
| const obj = {} | |
| // Convert buffer to string | |
| let lines = src.toString() | |
| // Convert line breaks to same format | |
| lines = lines.replace(/\r\n?/mg, '\n') | |
| let match | |
| while ((match = LINE.exec(lines)) != null) { | |
| const key = match[1] | |
| // Default undefined or null to empty string | |
| let value = (match[2] || '') | |
| // Remove whitespace | |
| value = value.trim() | |
| // Check if double quoted | |
| const maybeQuote = value[0] | |
| // Remove surrounding quotes | |
| value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2') | |
| // Expand newlines if double quoted | |
| if (maybeQuote === '"') { | |
| value = value.replace(/\\n/g, '\n') | |
| value = value.replace(/\\r/g, '\r') | |
| } | |
| // Add to object | |
| obj[key] = value | |
| } | |
| return obj | |
| } | |
| function _parseVault (options) { | |
| const vaultPath = _vaultPath(options) | |
| // Parse .env.vault | |
| const result = DotenvModule.configDotenv({ path: vaultPath }) | |
| if (!result.parsed) { | |
| const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`) | |
| err.code = 'MISSING_DATA' | |
| throw err | |
| } | |
| // handle scenario for comma separated keys - for use with key rotation | |
| // example: 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 { | |
| // Get full key | |
| const key = keys[i].trim() | |
| // Get instructions for decrypt | |
| const attrs = _instructions(result, key) | |
| // Decrypt | |
| decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key) | |
| break | |
| } catch (error) { | |
| // last key | |
| if (i + 1 >= length) { | |
| throw error | |
| } | |
| // try next key | |
| } | |
| } | |
| // Parse decrypted .env string | |
| return DotenvModule.parse(decrypted) | |
| } | |
| function _log (message) { | |
| console.log(`[dotenv@${version}][INFO] ${message}`) | |
| } | |
| function _warn (message) { | |
| console.log(`[dotenv@${version}][WARN] ${message}`) | |
| } | |
| function _debug (message) { | |
| console.log(`[dotenv@${version}][DEBUG] ${message}`) | |
| } | |
| function _dotenvKey (options) { | |
| // prioritize developer directly setting options.DOTENV_KEY | |
| if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { | |
| return options.DOTENV_KEY | |
| } | |
| // secondary infra already contains a DOTENV_KEY environment variable | |
| if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { | |
| return process.env.DOTENV_KEY | |
| } | |
| // fallback to empty string | |
| return '' | |
| } | |
| function _instructions (result, dotenvKey) { | |
| // Parse DOTENV_KEY. Format is a URI | |
| let uri | |
| 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 } | |
| } | |
| 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.existsSync(filepath)) { | |
| possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault` | |
| } | |
| } | |
| } else { | |
| possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault` | |
| } | |
| } else { | |
| possibleVaultPath = path.resolve(process.cwd(), '.env.vault') | |
| } | |
| if (fs.existsSync(possibleVaultPath)) { | |
| return possibleVaultPath | |
| } | |
| return null | |
| } | |
| function _resolveHome (envPath) { | |
| return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath | |
| } | |
| function _configVault (options) { | |
| _log('Loading env from encrypted .env.vault') | |
| const parsed = DotenvModule._parseVault(options) | |
| let processEnv = process.env | |
| if (options && options.processEnv != null) { | |
| processEnv = options.processEnv | |
| } | |
| DotenvModule.populate(processEnv, parsed, options) | |
| return { parsed } | |
| } | |
| function configDotenv (options) { | |
| const dotenvPath = path.resolve(process.cwd(), '.env') | |
| let encoding = 'utf8' | |
| const debug = Boolean(options && options.debug) | |
| if (options && options.encoding) { | |
| encoding = options.encoding | |
| } else { | |
| if (debug) { | |
| _debug('No encoding is specified. UTF-8 is used by default') | |
| } | |
| } | |
| let optionPaths = [dotenvPath] // default, look for .env | |
| if (options && options.path) { | |
| if (!Array.isArray(options.path)) { | |
| optionPaths = [_resolveHome(options.path)] | |
| } else { | |
| optionPaths = [] // reset default | |
| for (const filepath of options.path) { | |
| optionPaths.push(_resolveHome(filepath)) | |
| } | |
| } | |
| } | |
| // Build the parsed data in a temporary object (because we need to return it). Once we have the final | |
| // parsed data, we will combine it with process.env (or options.processEnv if provided). | |
| let lastError | |
| const parsedAll = {} | |
| for (const path of optionPaths) { | |
| try { | |
| // Specifying an encoding returns a string instead of a buffer | |
| const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding })) | |
| DotenvModule.populate(parsedAll, parsed, options) | |
| } catch (e) { | |
| if (debug) { | |
| _debug(`Failed to load ${path} ${e.message}`) | |
| } | |
| lastError = e | |
| } | |
| } | |
| let processEnv = process.env | |
| if (options && options.processEnv != null) { | |
| processEnv = options.processEnv | |
| } | |
| DotenvModule.populate(processEnv, parsedAll, options) | |
| if (lastError) { | |
| return { parsed: parsedAll, error: lastError } | |
| } else { | |
| return { parsed: parsedAll } | |
| } | |
| } | |
| // Populates process.env from .env file | |
| function config (options) { | |
| // fallback to original dotenv if DOTENV_KEY is not set | |
| if (_dotenvKey(options).length === 0) { | |
| return DotenvModule.configDotenv(options) | |
| } | |
| const vaultPath = _vaultPath(options) | |
| // dotenvKey exists but .env.vault file does not exist | |
| if (!vaultPath) { | |
| _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`) | |
| return DotenvModule.configDotenv(options) | |
| } | |
| return DotenvModule._configVault(options) | |
| } | |
| 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.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.code = 'INVALID_DOTENV_KEY' | |
| throw err | |
| } else if (decryptionFailed) { | |
| const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY') | |
| err.code = 'DECRYPTION_FAILED' | |
| throw err | |
| } else { | |
| throw error | |
| } | |
| } | |
| } | |
| // Populate process.env with parsed values | |
| function populate (processEnv, 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.code = 'OBJECT_REQUIRED' | |
| throw err | |
| } | |
| // Set process.env | |
| for (const key of Object.keys(parsed)) { | |
| if (Object.prototype.hasOwnProperty.call(processEnv, key)) { | |
| if (override === true) { | |
| processEnv[key] = parsed[key] | |
| } | |
| if (debug) { | |
| if (override === true) { | |
| _debug(`"${key}" is already defined and WAS overwritten`) | |
| } else { | |
| _debug(`"${key}" is already defined and was NOT overwritten`) | |
| } | |
| } | |
| } else { | |
| processEnv[key] = parsed[key] | |
| } | |
| } | |
| } | |
| const DotenvModule = { | |
| configDotenv, | |
| _configVault, | |
| _parseVault, | |
| config, | |
| decrypt, | |
| parse, | |
| populate | |
| } | |
| module.exports.configDotenv = DotenvModule.configDotenv | |
| module.exports._configVault = DotenvModule._configVault | |
| module.exports._parseVault = DotenvModule._parseVault | |
| module.exports.config = DotenvModule.config | |
| module.exports.decrypt = DotenvModule.decrypt | |
| module.exports.parse = DotenvModule.parse | |
| module.exports.populate = DotenvModule.populate | |
| module.exports = DotenvModule | |