| [81bc7da] | 1 | const fs = require('fs')
|
|---|
| 2 | const path = require('path')
|
|---|
| 3 | const os = require('os')
|
|---|
| 4 | const crypto = require('crypto')
|
|---|
| 5 | const packageJson = require('../package.json')
|
|---|
| 6 |
|
|---|
| 7 | const version = packageJson.version
|
|---|
| 8 |
|
|---|
| 9 | const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
|
|---|
| 10 |
|
|---|
| 11 | // Parse src into an Object
|
|---|
| 12 | function parse (src) {
|
|---|
| 13 | const obj = {}
|
|---|
| 14 |
|
|---|
| 15 | // Convert buffer to string
|
|---|
| 16 | let lines = src.toString()
|
|---|
| 17 |
|
|---|
| 18 | // Convert line breaks to same format
|
|---|
| 19 | lines = lines.replace(/\r\n?/mg, '\n')
|
|---|
| 20 |
|
|---|
| 21 | let match
|
|---|
| 22 | while ((match = LINE.exec(lines)) != null) {
|
|---|
| 23 | const key = match[1]
|
|---|
| 24 |
|
|---|
| 25 | // Default undefined or null to empty string
|
|---|
| 26 | let value = (match[2] || '')
|
|---|
| 27 |
|
|---|
| 28 | // Remove whitespace
|
|---|
| 29 | value = value.trim()
|
|---|
| 30 |
|
|---|
| 31 | // Check if double quoted
|
|---|
| 32 | const maybeQuote = value[0]
|
|---|
| 33 |
|
|---|
| 34 | // Remove surrounding quotes
|
|---|
| 35 | value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
|
|---|
| 36 |
|
|---|
| 37 | // Expand newlines if double quoted
|
|---|
| 38 | if (maybeQuote === '"') {
|
|---|
| 39 | value = value.replace(/\\n/g, '\n')
|
|---|
| 40 | value = value.replace(/\\r/g, '\r')
|
|---|
| 41 | }
|
|---|
| 42 |
|
|---|
| 43 | // Add to object
|
|---|
| 44 | obj[key] = value
|
|---|
| 45 | }
|
|---|
| 46 |
|
|---|
| 47 | return obj
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | function _parseVault (options) {
|
|---|
| 51 | options = options || {}
|
|---|
| 52 |
|
|---|
| 53 | const vaultPath = _vaultPath(options)
|
|---|
| 54 | options.path = vaultPath // parse .env.vault
|
|---|
| 55 | const result = DotenvModule.configDotenv(options)
|
|---|
| 56 | if (!result.parsed) {
|
|---|
| 57 | const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`)
|
|---|
| 58 | err.code = 'MISSING_DATA'
|
|---|
| 59 | throw err
|
|---|
| 60 | }
|
|---|
| 61 |
|
|---|
| 62 | // handle scenario for comma separated keys - for use with key rotation
|
|---|
| 63 | // example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
|
|---|
| 64 | const keys = _dotenvKey(options).split(',')
|
|---|
| 65 | const length = keys.length
|
|---|
| 66 |
|
|---|
| 67 | let decrypted
|
|---|
| 68 | for (let i = 0; i < length; i++) {
|
|---|
| 69 | try {
|
|---|
| 70 | // Get full key
|
|---|
| 71 | const key = keys[i].trim()
|
|---|
| 72 |
|
|---|
| 73 | // Get instructions for decrypt
|
|---|
| 74 | const attrs = _instructions(result, key)
|
|---|
| 75 |
|
|---|
| 76 | // Decrypt
|
|---|
| 77 | decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key)
|
|---|
| 78 |
|
|---|
| 79 | break
|
|---|
| 80 | } catch (error) {
|
|---|
| 81 | // last key
|
|---|
| 82 | if (i + 1 >= length) {
|
|---|
| 83 | throw error
|
|---|
| 84 | }
|
|---|
| 85 | // try next key
|
|---|
| 86 | }
|
|---|
| 87 | }
|
|---|
| 88 |
|
|---|
| 89 | // Parse decrypted .env string
|
|---|
| 90 | return DotenvModule.parse(decrypted)
|
|---|
| 91 | }
|
|---|
| 92 |
|
|---|
| 93 | function _warn (message) {
|
|---|
| 94 | console.log(`[dotenv@${version}][WARN] ${message}`)
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | function _debug (message) {
|
|---|
| 98 | console.log(`[dotenv@${version}][DEBUG] ${message}`)
|
|---|
| 99 | }
|
|---|
| 100 |
|
|---|
| 101 | function _log (message) {
|
|---|
| 102 | console.log(`[dotenv@${version}] ${message}`)
|
|---|
| 103 | }
|
|---|
| 104 |
|
|---|
| 105 | function _dotenvKey (options) {
|
|---|
| 106 | // prioritize developer directly setting options.DOTENV_KEY
|
|---|
| 107 | if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
|
|---|
| 108 | return options.DOTENV_KEY
|
|---|
| 109 | }
|
|---|
| 110 |
|
|---|
| 111 | // secondary infra already contains a DOTENV_KEY environment variable
|
|---|
| 112 | if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|---|
| 113 | return process.env.DOTENV_KEY
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | // fallback to empty string
|
|---|
| 117 | return ''
|
|---|
| 118 | }
|
|---|
| 119 |
|
|---|
| 120 | function _instructions (result, dotenvKey) {
|
|---|
| 121 | // Parse DOTENV_KEY. Format is a URI
|
|---|
| 122 | let uri
|
|---|
| 123 | try {
|
|---|
| 124 | uri = new URL(dotenvKey)
|
|---|
| 125 | } catch (error) {
|
|---|
| 126 | if (error.code === 'ERR_INVALID_URL') {
|
|---|
| 127 | 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')
|
|---|
| 128 | err.code = 'INVALID_DOTENV_KEY'
|
|---|
| 129 | throw err
|
|---|
| 130 | }
|
|---|
| 131 |
|
|---|
| 132 | throw error
|
|---|
| 133 | }
|
|---|
| 134 |
|
|---|
| 135 | // Get decrypt key
|
|---|
| 136 | const key = uri.password
|
|---|
| 137 | if (!key) {
|
|---|
| 138 | const err = new Error('INVALID_DOTENV_KEY: Missing key part')
|
|---|
| 139 | err.code = 'INVALID_DOTENV_KEY'
|
|---|
| 140 | throw err
|
|---|
| 141 | }
|
|---|
| 142 |
|
|---|
| 143 | // Get environment
|
|---|
| 144 | const environment = uri.searchParams.get('environment')
|
|---|
| 145 | if (!environment) {
|
|---|
| 146 | const err = new Error('INVALID_DOTENV_KEY: Missing environment part')
|
|---|
| 147 | err.code = 'INVALID_DOTENV_KEY'
|
|---|
| 148 | throw err
|
|---|
| 149 | }
|
|---|
| 150 |
|
|---|
| 151 | // Get ciphertext payload
|
|---|
| 152 | const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|---|
| 153 | const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION
|
|---|
| 154 | if (!ciphertext) {
|
|---|
| 155 | const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`)
|
|---|
| 156 | err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT'
|
|---|
| 157 | throw err
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | return { ciphertext, key }
|
|---|
| 161 | }
|
|---|
| 162 |
|
|---|
| 163 | function _vaultPath (options) {
|
|---|
| 164 | let possibleVaultPath = null
|
|---|
| 165 |
|
|---|
| 166 | if (options && options.path && options.path.length > 0) {
|
|---|
| 167 | if (Array.isArray(options.path)) {
|
|---|
| 168 | for (const filepath of options.path) {
|
|---|
| 169 | if (fs.existsSync(filepath)) {
|
|---|
| 170 | possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`
|
|---|
| 171 | }
|
|---|
| 172 | }
|
|---|
| 173 | } else {
|
|---|
| 174 | possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`
|
|---|
| 175 | }
|
|---|
| 176 | } else {
|
|---|
| 177 | possibleVaultPath = path.resolve(process.cwd(), '.env.vault')
|
|---|
| 178 | }
|
|---|
| 179 |
|
|---|
| 180 | if (fs.existsSync(possibleVaultPath)) {
|
|---|
| 181 | return possibleVaultPath
|
|---|
| 182 | }
|
|---|
| 183 |
|
|---|
| 184 | return null
|
|---|
| 185 | }
|
|---|
| 186 |
|
|---|
| 187 | function _resolveHome (envPath) {
|
|---|
| 188 | return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
|
|---|
| 189 | }
|
|---|
| 190 |
|
|---|
| 191 | function _configVault (options) {
|
|---|
| 192 | const debug = Boolean(options && options.debug)
|
|---|
| 193 | const quiet = options && 'quiet' in options ? options.quiet : true
|
|---|
| 194 |
|
|---|
| 195 | if (debug || !quiet) {
|
|---|
| 196 | _log('Loading env from encrypted .env.vault')
|
|---|
| 197 | }
|
|---|
| 198 |
|
|---|
| 199 | const parsed = DotenvModule._parseVault(options)
|
|---|
| 200 |
|
|---|
| 201 | let processEnv = process.env
|
|---|
| 202 | if (options && options.processEnv != null) {
|
|---|
| 203 | processEnv = options.processEnv
|
|---|
| 204 | }
|
|---|
| 205 |
|
|---|
| 206 | DotenvModule.populate(processEnv, parsed, options)
|
|---|
| 207 |
|
|---|
| 208 | return { parsed }
|
|---|
| 209 | }
|
|---|
| 210 |
|
|---|
| 211 | function configDotenv (options) {
|
|---|
| 212 | const dotenvPath = path.resolve(process.cwd(), '.env')
|
|---|
| 213 | let encoding = 'utf8'
|
|---|
| 214 | const debug = Boolean(options && options.debug)
|
|---|
| 215 | const quiet = options && 'quiet' in options ? options.quiet : true
|
|---|
| 216 |
|
|---|
| 217 | if (options && options.encoding) {
|
|---|
| 218 | encoding = options.encoding
|
|---|
| 219 | } else {
|
|---|
| 220 | if (debug) {
|
|---|
| 221 | _debug('No encoding is specified. UTF-8 is used by default')
|
|---|
| 222 | }
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | let optionPaths = [dotenvPath] // default, look for .env
|
|---|
| 226 | if (options && options.path) {
|
|---|
| 227 | if (!Array.isArray(options.path)) {
|
|---|
| 228 | optionPaths = [_resolveHome(options.path)]
|
|---|
| 229 | } else {
|
|---|
| 230 | optionPaths = [] // reset default
|
|---|
| 231 | for (const filepath of options.path) {
|
|---|
| 232 | optionPaths.push(_resolveHome(filepath))
|
|---|
| 233 | }
|
|---|
| 234 | }
|
|---|
| 235 | }
|
|---|
| 236 |
|
|---|
| 237 | // Build the parsed data in a temporary object (because we need to return it). Once we have the final
|
|---|
| 238 | // parsed data, we will combine it with process.env (or options.processEnv if provided).
|
|---|
| 239 | let lastError
|
|---|
| 240 | const parsedAll = {}
|
|---|
| 241 | for (const path of optionPaths) {
|
|---|
| 242 | try {
|
|---|
| 243 | // Specifying an encoding returns a string instead of a buffer
|
|---|
| 244 | const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))
|
|---|
| 245 |
|
|---|
| 246 | DotenvModule.populate(parsedAll, parsed, options)
|
|---|
| 247 | } catch (e) {
|
|---|
| 248 | if (debug) {
|
|---|
| 249 | _debug(`Failed to load ${path} ${e.message}`)
|
|---|
| 250 | }
|
|---|
| 251 | lastError = e
|
|---|
| 252 | }
|
|---|
| 253 | }
|
|---|
| 254 |
|
|---|
| 255 | let processEnv = process.env
|
|---|
| 256 | if (options && options.processEnv != null) {
|
|---|
| 257 | processEnv = options.processEnv
|
|---|
| 258 | }
|
|---|
| 259 |
|
|---|
| 260 | DotenvModule.populate(processEnv, parsedAll, options)
|
|---|
| 261 |
|
|---|
| 262 | if (debug || !quiet) {
|
|---|
| 263 | const keysCount = Object.keys(parsedAll).length
|
|---|
| 264 | const shortPaths = []
|
|---|
| 265 | for (const filePath of optionPaths) {
|
|---|
| 266 | try {
|
|---|
| 267 | const relative = path.relative(process.cwd(), filePath)
|
|---|
| 268 | shortPaths.push(relative)
|
|---|
| 269 | } catch (e) {
|
|---|
| 270 | if (debug) {
|
|---|
| 271 | _debug(`Failed to load ${filePath} ${e.message}`)
|
|---|
| 272 | }
|
|---|
| 273 | lastError = e
|
|---|
| 274 | }
|
|---|
| 275 | }
|
|---|
| 276 |
|
|---|
| 277 | _log(`injecting env (${keysCount}) from ${shortPaths.join(',')}`)
|
|---|
| 278 | }
|
|---|
| 279 |
|
|---|
| 280 | if (lastError) {
|
|---|
| 281 | return { parsed: parsedAll, error: lastError }
|
|---|
| 282 | } else {
|
|---|
| 283 | return { parsed: parsedAll }
|
|---|
| 284 | }
|
|---|
| 285 | }
|
|---|
| 286 |
|
|---|
| 287 | // Populates process.env from .env file
|
|---|
| 288 | function config (options) {
|
|---|
| 289 | // fallback to original dotenv if DOTENV_KEY is not set
|
|---|
| 290 | if (_dotenvKey(options).length === 0) {
|
|---|
| 291 | return DotenvModule.configDotenv(options)
|
|---|
| 292 | }
|
|---|
| 293 |
|
|---|
| 294 | const vaultPath = _vaultPath(options)
|
|---|
| 295 |
|
|---|
| 296 | // dotenvKey exists but .env.vault file does not exist
|
|---|
| 297 | if (!vaultPath) {
|
|---|
| 298 | _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)
|
|---|
| 299 |
|
|---|
| 300 | return DotenvModule.configDotenv(options)
|
|---|
| 301 | }
|
|---|
| 302 |
|
|---|
| 303 | return DotenvModule._configVault(options)
|
|---|
| 304 | }
|
|---|
| 305 |
|
|---|
| 306 | function decrypt (encrypted, keyStr) {
|
|---|
| 307 | const key = Buffer.from(keyStr.slice(-64), 'hex')
|
|---|
| 308 | let ciphertext = Buffer.from(encrypted, 'base64')
|
|---|
| 309 |
|
|---|
| 310 | const nonce = ciphertext.subarray(0, 12)
|
|---|
| 311 | const authTag = ciphertext.subarray(-16)
|
|---|
| 312 | ciphertext = ciphertext.subarray(12, -16)
|
|---|
| 313 |
|
|---|
| 314 | try {
|
|---|
| 315 | const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)
|
|---|
| 316 | aesgcm.setAuthTag(authTag)
|
|---|
| 317 | return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
|
|---|
| 318 | } catch (error) {
|
|---|
| 319 | const isRange = error instanceof RangeError
|
|---|
| 320 | const invalidKeyLength = error.message === 'Invalid key length'
|
|---|
| 321 | const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data'
|
|---|
| 322 |
|
|---|
| 323 | if (isRange || invalidKeyLength) {
|
|---|
| 324 | const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)')
|
|---|
| 325 | err.code = 'INVALID_DOTENV_KEY'
|
|---|
| 326 | throw err
|
|---|
| 327 | } else if (decryptionFailed) {
|
|---|
| 328 | const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY')
|
|---|
| 329 | err.code = 'DECRYPTION_FAILED'
|
|---|
| 330 | throw err
|
|---|
| 331 | } else {
|
|---|
| 332 | throw error
|
|---|
| 333 | }
|
|---|
| 334 | }
|
|---|
| 335 | }
|
|---|
| 336 |
|
|---|
| 337 | // Populate process.env with parsed values
|
|---|
| 338 | function populate (processEnv, parsed, options = {}) {
|
|---|
| 339 | const debug = Boolean(options && options.debug)
|
|---|
| 340 | const override = Boolean(options && options.override)
|
|---|
| 341 |
|
|---|
| 342 | if (typeof parsed !== 'object') {
|
|---|
| 343 | const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate')
|
|---|
| 344 | err.code = 'OBJECT_REQUIRED'
|
|---|
| 345 | throw err
|
|---|
| 346 | }
|
|---|
| 347 |
|
|---|
| 348 | // Set process.env
|
|---|
| 349 | for (const key of Object.keys(parsed)) {
|
|---|
| 350 | if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|---|
| 351 | if (override === true) {
|
|---|
| 352 | processEnv[key] = parsed[key]
|
|---|
| 353 | }
|
|---|
| 354 |
|
|---|
| 355 | if (debug) {
|
|---|
| 356 | if (override === true) {
|
|---|
| 357 | _debug(`"${key}" is already defined and WAS overwritten`)
|
|---|
| 358 | } else {
|
|---|
| 359 | _debug(`"${key}" is already defined and was NOT overwritten`)
|
|---|
| 360 | }
|
|---|
| 361 | }
|
|---|
| 362 | } else {
|
|---|
| 363 | processEnv[key] = parsed[key]
|
|---|
| 364 | }
|
|---|
| 365 | }
|
|---|
| 366 | }
|
|---|
| 367 |
|
|---|
| 368 | const DotenvModule = {
|
|---|
| 369 | configDotenv,
|
|---|
| 370 | _configVault,
|
|---|
| 371 | _parseVault,
|
|---|
| 372 | config,
|
|---|
| 373 | decrypt,
|
|---|
| 374 | parse,
|
|---|
| 375 | populate
|
|---|
| 376 | }
|
|---|
| 377 |
|
|---|
| 378 | module.exports.configDotenv = DotenvModule.configDotenv
|
|---|
| 379 | module.exports._configVault = DotenvModule._configVault
|
|---|
| 380 | module.exports._parseVault = DotenvModule._parseVault
|
|---|
| 381 | module.exports.config = DotenvModule.config
|
|---|
| 382 | module.exports.decrypt = DotenvModule.decrypt
|
|---|
| 383 | module.exports.parse = DotenvModule.parse
|
|---|
| 384 | module.exports.populate = DotenvModule.populate
|
|---|
| 385 |
|
|---|
| 386 | module.exports = DotenvModule
|
|---|