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