@dotenvx/dotenvx 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/cli/actions/run.js +41 -76
- package/src/lib/helpers/changed.js +10 -0
- package/src/lib/helpers/decrypt.js +25 -0
- package/src/lib/helpers/dotenvVault.js +6 -100
- package/src/lib/helpers/encrypt.js +29 -0
- package/src/lib/helpers/guessEnvironment.js +15 -0
- package/src/lib/helpers/hash.js +9 -0
- package/src/lib/helpers/inject.js +27 -0
- package/src/lib/helpers/parseEnvironmentFromDotenvKey.js +19 -0
- package/src/lib/helpers/parseExpand.js +30 -0
- package/src/lib/services/runDefault.js +7 -58
- package/src/lib/services/runVault.js +136 -0
package/README.md
CHANGED
package/package.json
CHANGED
package/src/cli/actions/run.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
1
|
const path = require('path')
|
|
3
2
|
const execa = require('execa')
|
|
4
3
|
const logger = require('./../../shared/logger')
|
|
5
|
-
const helpers = require('./../helpers')
|
|
6
|
-
const main = require('./../../lib/main')
|
|
7
|
-
const parseEncryptionKeyFromDotenvKey = require('./../../lib/helpers/parseEncryptionKeyFromDotenvKey')
|
|
8
4
|
|
|
9
5
|
const RunDefault = require('./../../lib/services/runDefault')
|
|
6
|
+
const RunVault = require('./../../lib/services/runVault')
|
|
10
7
|
|
|
11
|
-
const ENCODING = 'utf8'
|
|
12
8
|
const REPORT_ISSUE_LINK = 'https://github.com/dotenvx/dotenvx/issues/new'
|
|
13
9
|
|
|
14
10
|
const executeCommand = async function (commandArgs, env) {
|
|
@@ -39,7 +35,11 @@ const executeCommand = async function (commandArgs, env) {
|
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
try {
|
|
42
|
-
|
|
38
|
+
const systemCommandPath = execa.sync('which', [commandArgs[0]]).stdout
|
|
39
|
+
logger.debug(`system command path [${systemCommandPath}]`)
|
|
40
|
+
|
|
41
|
+
// commandProcess = execa(commandArgs[0], commandArgs.slice(1), {
|
|
42
|
+
commandProcess = execa(systemCommandPath, commandArgs.slice(1), {
|
|
43
43
|
stdio: 'inherit',
|
|
44
44
|
env: { ...process.env, ...env }
|
|
45
45
|
})
|
|
@@ -76,31 +76,6 @@ const executeCommand = async function (commandArgs, env) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedVault) {
|
|
80
|
-
// Parse DOTENV_KEY. Format is a URI
|
|
81
|
-
let uri
|
|
82
|
-
try {
|
|
83
|
-
uri = new URL(dotenvKey)
|
|
84
|
-
} catch (e) {
|
|
85
|
-
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Get environment
|
|
89
|
-
const environment = uri.searchParams.get('environment')
|
|
90
|
-
if (!environment) {
|
|
91
|
-
throw new Error('INVALID_DOTENV_KEY: Missing environment part')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Get ciphertext payload
|
|
95
|
-
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
96
|
-
const ciphertext = parsedVault[environmentKey] // DOTENV_VAULT_PRODUCTION
|
|
97
|
-
if (!ciphertext) {
|
|
98
|
-
throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return ciphertext
|
|
102
|
-
}
|
|
103
|
-
|
|
104
79
|
async function run () {
|
|
105
80
|
const commandArgs = this.args
|
|
106
81
|
logger.debug(`process command [${commandArgs.join(' ')}]`)
|
|
@@ -110,56 +85,46 @@ async function run () {
|
|
|
110
85
|
|
|
111
86
|
// load from .env.vault file
|
|
112
87
|
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
113
|
-
|
|
114
|
-
|
|
88
|
+
try {
|
|
89
|
+
const {
|
|
90
|
+
envVaultFile,
|
|
91
|
+
parsed,
|
|
92
|
+
injected,
|
|
93
|
+
preExisted,
|
|
94
|
+
uniqueInjectedKeys
|
|
95
|
+
} = new RunVault(options.envVaultFile, options.env, process.env.DOTENV_KEY, options.overload).run()
|
|
96
|
+
|
|
97
|
+
logger.verbose(`loading env from encrypted ${envVaultFile} (${path.resolve(envVaultFile)})`)
|
|
98
|
+
logger.debug(`decrypting encrypted env from ${envVaultFile} (${path.resolve(envVaultFile)})`)
|
|
99
|
+
|
|
100
|
+
// debug parsed
|
|
101
|
+
logger.debug(parsed)
|
|
102
|
+
|
|
103
|
+
// verbose/debug injected key/value
|
|
104
|
+
for (const [key, value] of Object.entries(injected)) {
|
|
105
|
+
logger.verbose(`${key} set`)
|
|
106
|
+
logger.debug(`${key} set to ${value}`)
|
|
107
|
+
}
|
|
115
108
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
123
|
-
const parsedVault = main.parse(src)
|
|
124
|
-
|
|
125
|
-
logger.debug(`decrypting encrypted env from ${filepath}`)
|
|
126
|
-
// handle scenario for comma separated keys - for use with key rotation
|
|
127
|
-
// example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
|
|
128
|
-
const dotenvKeys = process.env.DOTENV_KEY.split(',')
|
|
129
|
-
const length = dotenvKeys.length
|
|
130
|
-
|
|
131
|
-
let decrypted
|
|
132
|
-
for (let i = 0; i < length; i++) {
|
|
133
|
-
try {
|
|
134
|
-
// Get full dotenvKey
|
|
135
|
-
const dotenvKey = dotenvKeys[i].trim()
|
|
136
|
-
|
|
137
|
-
const key = parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
138
|
-
const ciphertext = _parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
|
|
139
|
-
|
|
140
|
-
// Decrypt
|
|
141
|
-
decrypted = main.decrypt(ciphertext, key)
|
|
142
|
-
|
|
143
|
-
break
|
|
144
|
-
} catch (error) {
|
|
145
|
-
// last key
|
|
146
|
-
if (i + 1 >= length) {
|
|
147
|
-
throw error
|
|
148
|
-
}
|
|
149
|
-
// try next key
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
logger.debug(decrypted)
|
|
153
|
-
const parsed = main.parseExpand(decrypted)
|
|
154
|
-
const result = main.inject(process.env, parsed, options.overload)
|
|
109
|
+
// verbose/debug preExisted key/value
|
|
110
|
+
for (const [key, value] of Object.entries(preExisted)) {
|
|
111
|
+
logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
|
|
112
|
+
logger.debug(`${key} pre-exists as ${value} (protip: use --overload to override)`)
|
|
113
|
+
}
|
|
155
114
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
115
|
+
logger.successv(`injecting env (${uniqueInjectedKeys.length}) from encrypted ${envVaultFile}`)
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error(error.message)
|
|
118
|
+
if (error.help) {
|
|
119
|
+
logger.help(error.help)
|
|
159
120
|
}
|
|
160
121
|
}
|
|
161
122
|
} else {
|
|
162
|
-
const {
|
|
123
|
+
const {
|
|
124
|
+
files,
|
|
125
|
+
readableFilepaths,
|
|
126
|
+
uniqueInjectedKeys
|
|
127
|
+
} = new RunDefault(options.envFile, options.env, options.overload).run()
|
|
163
128
|
|
|
164
129
|
for (const file of files) {
|
|
165
130
|
const filepath = file.filepath
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const dotenv = require('dotenv')
|
|
2
|
+
|
|
3
|
+
const parseEncryptionKeyFromDotenvKey = require('./parseEncryptionKeyFromDotenvKey')
|
|
4
|
+
|
|
5
|
+
function decrypt (ciphertext, dotenvKey) {
|
|
6
|
+
const key = parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
return dotenv.decrypt(ciphertext, key)
|
|
10
|
+
} catch (e) {
|
|
11
|
+
const error = new Error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
|
|
12
|
+
error.code = 'DECRYPTION_FAILED'
|
|
13
|
+
error.help = '[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.'
|
|
14
|
+
error.debug = `[DECRYPTION_FAILED] DOTENV_KEY is ${dotenvKey}`
|
|
15
|
+
|
|
16
|
+
switch (e.code) {
|
|
17
|
+
case 'DECRYPTION_FAILED':
|
|
18
|
+
throw error
|
|
19
|
+
default:
|
|
20
|
+
throw e
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = decrypt
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
|
-
const crypto = require('crypto')
|
|
3
|
-
const xxhash = require('xxhashjs')
|
|
4
|
-
const dotenv = require('dotenv')
|
|
5
2
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
3
|
+
const encrypt = require('./encrypt')
|
|
4
|
+
const changed = require('./changed')
|
|
5
|
+
const guessEnvironment = require('./guessEnvironment')
|
|
8
6
|
|
|
9
7
|
class DotenvVault {
|
|
10
8
|
constructor (dotenvFiles = {}, dotenvKeys = {}, dotenvVaults = {}) {
|
|
@@ -19,14 +17,14 @@ class DotenvVault {
|
|
|
19
17
|
const addedDotenvFilenames = new Set()
|
|
20
18
|
|
|
21
19
|
for (const [filepath, raw] of Object.entries(this.dotenvFiles)) {
|
|
22
|
-
const environment =
|
|
20
|
+
const environment = guessEnvironment(filepath)
|
|
23
21
|
const vault = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
24
22
|
|
|
25
23
|
let ciphertext = this.dotenvVaults[vault]
|
|
26
24
|
const dotenvKey = this.dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
|
|
27
25
|
|
|
28
|
-
if (!ciphertext || ciphertext.length === 0 ||
|
|
29
|
-
ciphertext =
|
|
26
|
+
if (!ciphertext || ciphertext.length === 0 || changed(ciphertext, raw, dotenvKey)) {
|
|
27
|
+
ciphertext = encrypt(raw, dotenvKey)
|
|
30
28
|
this.dotenvVaults[vault] = ciphertext
|
|
31
29
|
addedVaults.add(vault) // for info logging to user
|
|
32
30
|
|
|
@@ -54,98 +52,6 @@ class DotenvVault {
|
|
|
54
52
|
addedDotenvFilenames: [...addedDotenvFilenames] // return set as array
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
|
-
|
|
58
|
-
_guessEnvironment (filepath) {
|
|
59
|
-
const filename = path.basename(filepath)
|
|
60
|
-
const parts = filename.split('.')
|
|
61
|
-
const possibleEnvironment = parts[2] // ['', 'env', environment', 'previous']
|
|
62
|
-
|
|
63
|
-
if (!possibleEnvironment || possibleEnvironment.length === 0) {
|
|
64
|
-
return 'development'
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return possibleEnvironment
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
_changed (dotenvKey, ciphertext, raw) {
|
|
71
|
-
const decrypted = this._decrypt(dotenvKey, ciphertext)
|
|
72
|
-
|
|
73
|
-
return this._hash(decrypted) !== this._hash(raw)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
_encrypt (dotenvKey, raw) {
|
|
77
|
-
const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
78
|
-
|
|
79
|
-
// set up nonce
|
|
80
|
-
const nonce = crypto.randomBytes(NONCE_BYTES)
|
|
81
|
-
|
|
82
|
-
// set up cipher
|
|
83
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
|
|
84
|
-
|
|
85
|
-
// generate ciphertext
|
|
86
|
-
let ciphertext = ''
|
|
87
|
-
ciphertext += cipher.update(raw, 'utf8', 'hex')
|
|
88
|
-
ciphertext += cipher.final('hex')
|
|
89
|
-
ciphertext += cipher.getAuthTag().toString('hex')
|
|
90
|
-
|
|
91
|
-
// prepend nonce
|
|
92
|
-
ciphertext = nonce.toString('hex') + ciphertext
|
|
93
|
-
|
|
94
|
-
// base64 encode output
|
|
95
|
-
return Buffer.from(ciphertext, 'hex').toString('base64')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
_decrypt (dotenvKey, ciphertext) {
|
|
99
|
-
const key = this._parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
return dotenv.decrypt(ciphertext, key)
|
|
103
|
-
} catch (e) {
|
|
104
|
-
const decryptionFailedError = new Error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
|
|
105
|
-
decryptionFailedError.code = 'DECRYPTION_FAILED'
|
|
106
|
-
decryptionFailedError.help = '[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.'
|
|
107
|
-
decryptionFailedError.debug = `[DECRYPTION_FAILED] DOTENV_KEY is ${dotenvKey}`
|
|
108
|
-
|
|
109
|
-
switch (e.code) {
|
|
110
|
-
case 'DECRYPTION_FAILED':
|
|
111
|
-
throw decryptionFailedError
|
|
112
|
-
default:
|
|
113
|
-
throw e
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
_parseEncryptionKeyFromDotenvKey (dotenvKey) {
|
|
119
|
-
// Parse DOTENV_KEY. Format is a URI
|
|
120
|
-
let uri
|
|
121
|
-
try {
|
|
122
|
-
uri = new URL(dotenvKey)
|
|
123
|
-
} catch (e) {
|
|
124
|
-
const code = 'INVALID_DOTENV_KEY'
|
|
125
|
-
const message = `INVALID_DOTENV_KEY: ${e.message}`
|
|
126
|
-
const error = new Error(message)
|
|
127
|
-
error.code = code
|
|
128
|
-
|
|
129
|
-
throw error
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Get decrypt key
|
|
133
|
-
const key = uri.password
|
|
134
|
-
if (!key) {
|
|
135
|
-
const code = 'INVALID_DOTENV_KEY'
|
|
136
|
-
const message = 'INVALID_DOTENV_KEY: Missing key part'
|
|
137
|
-
const error = new Error(message)
|
|
138
|
-
error.code = code
|
|
139
|
-
|
|
140
|
-
throw error
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return Buffer.from(key.slice(-64), 'hex')
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
_hash (str) {
|
|
147
|
-
return xxhash.h32(str, XXHASH_SEED).toString(16)
|
|
148
|
-
}
|
|
149
55
|
}
|
|
150
56
|
|
|
151
57
|
module.exports = DotenvVault
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
const parseEncryptionKeyFromDotenvKey = require('./parseEncryptionKeyFromDotenvKey')
|
|
4
|
+
|
|
5
|
+
const NONCE_BYTES = 12
|
|
6
|
+
|
|
7
|
+
function encrypt (raw, dotenvKey) {
|
|
8
|
+
const key = parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
9
|
+
|
|
10
|
+
// set up nonce
|
|
11
|
+
const nonce = crypto.randomBytes(NONCE_BYTES)
|
|
12
|
+
|
|
13
|
+
// set up cipher
|
|
14
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
|
|
15
|
+
|
|
16
|
+
// generate ciphertext
|
|
17
|
+
let ciphertext = ''
|
|
18
|
+
ciphertext += cipher.update(raw, 'utf8', 'hex')
|
|
19
|
+
ciphertext += cipher.final('hex')
|
|
20
|
+
ciphertext += cipher.getAuthTag().toString('hex')
|
|
21
|
+
|
|
22
|
+
// prepend nonce
|
|
23
|
+
ciphertext = nonce.toString('hex') + ciphertext
|
|
24
|
+
|
|
25
|
+
// base64 encode output
|
|
26
|
+
return Buffer.from(ciphertext, 'hex').toString('base64')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = encrypt
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
|
|
3
|
+
function guessEnvironment (filepath) {
|
|
4
|
+
const filename = path.basename(filepath)
|
|
5
|
+
const parts = filename.split('.')
|
|
6
|
+
const possibleEnvironment = parts[2] // ['', 'env', environment', 'previous']
|
|
7
|
+
|
|
8
|
+
if (!possibleEnvironment || possibleEnvironment.length === 0) {
|
|
9
|
+
return 'development'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return possibleEnvironment
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = guessEnvironment
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function inject (processEnv = {}, parsed = {}, overload = false) {
|
|
2
|
+
const injected = {}
|
|
3
|
+
const preExisted = {}
|
|
4
|
+
|
|
5
|
+
// set processEnv
|
|
6
|
+
for (const key of Object.keys(parsed)) {
|
|
7
|
+
if (processEnv[key]) {
|
|
8
|
+
if (overload === true) {
|
|
9
|
+
processEnv[key] = parsed[key]
|
|
10
|
+
|
|
11
|
+
injected[key] = parsed[key] // track injected key/value
|
|
12
|
+
} else {
|
|
13
|
+
preExisted[key] = processEnv[key] // track preExisted key/value
|
|
14
|
+
}
|
|
15
|
+
} else {
|
|
16
|
+
processEnv[key] = parsed[key]
|
|
17
|
+
injected[key] = parsed[key] // track injected key/value
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
injected,
|
|
23
|
+
preExisted
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = inject
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function parseEnvironmentFromDotenvKey (dotenvKey) {
|
|
2
|
+
// Parse DOTENV_KEY. Format is a URI
|
|
3
|
+
let uri
|
|
4
|
+
try {
|
|
5
|
+
uri = new URL(dotenvKey)
|
|
6
|
+
} catch (e) {
|
|
7
|
+
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Get environment
|
|
11
|
+
const environment = uri.searchParams.get('environment')
|
|
12
|
+
if (!environment) {
|
|
13
|
+
throw new Error('INVALID_DOTENV_KEY: Missing environment part')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return environment
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = parseEnvironmentFromDotenvKey
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const dotenv = require('dotenv')
|
|
2
|
+
const dotenvExpand = require('dotenv-expand')
|
|
3
|
+
|
|
4
|
+
function parseExpand (src, overload) {
|
|
5
|
+
const parsed = dotenv.parse(src)
|
|
6
|
+
|
|
7
|
+
// consider moving this logic straight into dotenv-expand
|
|
8
|
+
let inputParsed = {}
|
|
9
|
+
if (overload) {
|
|
10
|
+
inputParsed = { ...process.env, ...parsed }
|
|
11
|
+
} else {
|
|
12
|
+
inputParsed = { ...parsed, ...process.env }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const expandPlease = {
|
|
16
|
+
processEnv: {},
|
|
17
|
+
parsed: inputParsed
|
|
18
|
+
}
|
|
19
|
+
const expanded = dotenvExpand.expand(expandPlease).parsed
|
|
20
|
+
|
|
21
|
+
// but then for logging only log the original keys existing in parsed. this feels unnecessarily complex - like dotenv-expand should support the ability to inject additional `process.env` or objects as it sees fit to the object it wants to expand
|
|
22
|
+
const result = {}
|
|
23
|
+
for (const key in parsed) {
|
|
24
|
+
result[key] = expanded[key]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = parseExpand
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
|
-
const dotenv = require('dotenv')
|
|
4
|
-
const dotenvExpand = require('dotenv-expand')
|
|
5
3
|
|
|
6
4
|
const ENCODING = 'utf8'
|
|
7
5
|
|
|
6
|
+
const inject = require('./../helpers/inject')
|
|
7
|
+
const parseExpand = require('./../helpers/parseExpand')
|
|
8
|
+
|
|
8
9
|
class RunDefault {
|
|
9
10
|
constructor (envFile = '.env', env = [], overload = false) {
|
|
10
11
|
this.envFile = envFile
|
|
@@ -24,10 +25,10 @@ class RunDefault {
|
|
|
24
25
|
row.string = env
|
|
25
26
|
|
|
26
27
|
try {
|
|
27
|
-
const parsed = this.
|
|
28
|
+
const parsed = parseExpand(env, this.overload)
|
|
28
29
|
row.parsed = parsed
|
|
29
30
|
|
|
30
|
-
const { injected, preExisted } =
|
|
31
|
+
const { injected, preExisted } = inject(process.env, parsed, this.overload)
|
|
31
32
|
row.injected = injected
|
|
32
33
|
row.preExisted = preExisted
|
|
33
34
|
|
|
@@ -51,10 +52,10 @@ class RunDefault {
|
|
|
51
52
|
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
52
53
|
readableFilepaths.add(envFilepath)
|
|
53
54
|
|
|
54
|
-
const parsed = this.
|
|
55
|
+
const parsed = parseExpand(src, this.overload)
|
|
55
56
|
row.parsed = parsed
|
|
56
57
|
|
|
57
|
-
const { injected, preExisted } =
|
|
58
|
+
const { injected, preExisted } = inject(process.env, parsed, this.overload)
|
|
58
59
|
row.injected = injected
|
|
59
60
|
row.preExisted = preExisted
|
|
60
61
|
|
|
@@ -98,58 +99,6 @@ class RunDefault {
|
|
|
98
99
|
|
|
99
100
|
return this.env
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
-
_parseExpand (src) {
|
|
103
|
-
const parsed = dotenv.parse(src)
|
|
104
|
-
|
|
105
|
-
// consider moving this logic straight into dotenv-expand
|
|
106
|
-
let inputParsed = {}
|
|
107
|
-
if (this.overload) {
|
|
108
|
-
inputParsed = { ...process.env, ...parsed }
|
|
109
|
-
} else {
|
|
110
|
-
inputParsed = { ...parsed, ...process.env }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const expandPlease = {
|
|
114
|
-
processEnv: {},
|
|
115
|
-
parsed: inputParsed
|
|
116
|
-
}
|
|
117
|
-
const expanded = dotenvExpand.expand(expandPlease).parsed
|
|
118
|
-
|
|
119
|
-
// but then for logging only log the original keys existing in parsed. this feels unnecessarily complex - like dotenv-expand should support the ability to inject additional `process.env` or objects as it sees fit to the object it wants to expand
|
|
120
|
-
const result = {}
|
|
121
|
-
for (const key in parsed) {
|
|
122
|
-
result[key] = expanded[key]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return result
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
_inject (processEnv = {}, parsed = {}) {
|
|
129
|
-
const injected = {}
|
|
130
|
-
const preExisted = {}
|
|
131
|
-
|
|
132
|
-
// set processEnv
|
|
133
|
-
for (const key of Object.keys(parsed)) {
|
|
134
|
-
if (processEnv[key]) {
|
|
135
|
-
if (this.overload === true) {
|
|
136
|
-
processEnv[key] = parsed[key]
|
|
137
|
-
|
|
138
|
-
injected[key] = parsed[key] // track injected key/value
|
|
139
|
-
} else {
|
|
140
|
-
preExisted[key] = processEnv[key] // track preExisted key/value
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
processEnv[key] = parsed[key]
|
|
144
|
-
injected[key] = parsed[key] // track injected key/value
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
injected,
|
|
150
|
-
preExisted
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
102
|
}
|
|
154
103
|
|
|
155
104
|
module.exports = RunDefault
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const dotenv = require('dotenv')
|
|
4
|
+
|
|
5
|
+
const inject = require('./../helpers/inject')
|
|
6
|
+
const decrypt = require('./../helpers/decrypt')
|
|
7
|
+
const parseExpand = require('./../helpers/parseExpand')
|
|
8
|
+
const parseEnvironmentFromDotenvKey = require('./../helpers/parseEnvironmentFromDotenvKey')
|
|
9
|
+
|
|
10
|
+
const ENCODING = 'utf8'
|
|
11
|
+
|
|
12
|
+
class RunVault {
|
|
13
|
+
constructor (envVaultFile = '.env.vault', env = [], DOTENV_KEY = '', overload = false) {
|
|
14
|
+
this.DOTENV_KEY = DOTENV_KEY
|
|
15
|
+
this.envVaultFile = envVaultFile
|
|
16
|
+
this.env = env
|
|
17
|
+
this.overload = overload
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
run () {
|
|
21
|
+
const filepath = path.resolve(this.envVaultFile)
|
|
22
|
+
if (!fs.existsSync(filepath)) {
|
|
23
|
+
const code = 'MISSING_ENV_VAULT_FILE'
|
|
24
|
+
const message = `you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`
|
|
25
|
+
const error = new Error(message)
|
|
26
|
+
error.code = code
|
|
27
|
+
throw error
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (this.DOTENV_KEY.length < 1) {
|
|
31
|
+
const code = 'MISSING_DOTENV_KEY'
|
|
32
|
+
const message = `your DOTENV_KEY appears to be blank: '${this.DOTENV_KEY}'`
|
|
33
|
+
const error = new Error(message)
|
|
34
|
+
error.code = code
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const strings = []
|
|
39
|
+
const uniqueInjectedKeys = new Set()
|
|
40
|
+
|
|
41
|
+
const envs = this._envs()
|
|
42
|
+
for (const env of envs) {
|
|
43
|
+
const row = {}
|
|
44
|
+
row.string = env
|
|
45
|
+
|
|
46
|
+
const parsed = parseExpand(env, this.overload)
|
|
47
|
+
row.parsed = parsed
|
|
48
|
+
|
|
49
|
+
const { injected, preExisted } = inject(process.env, parsed, this.overload)
|
|
50
|
+
row.injected = injected
|
|
51
|
+
row.preExisted = preExisted
|
|
52
|
+
|
|
53
|
+
for (const key of Object.keys(injected)) {
|
|
54
|
+
uniqueInjectedKeys.add(key) // track uniqueInjectedKeys across multiple files
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
strings.push(row)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let decrypted
|
|
61
|
+
const dotenvKeys = this._dotenvKeys()
|
|
62
|
+
const parsedVault = this._parsedVault(filepath)
|
|
63
|
+
for (let i = 0; i < dotenvKeys.length; i++) {
|
|
64
|
+
try {
|
|
65
|
+
const dotenvKey = dotenvKeys[i].trim() // dotenv://key_1234@...?environment=prod
|
|
66
|
+
|
|
67
|
+
decrypted = this._decrypted(dotenvKey, parsedVault)
|
|
68
|
+
|
|
69
|
+
break
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// last key
|
|
72
|
+
if (i + 1 >= dotenvKeys.length) {
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
// try next key
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// parse this. it's the equivalent of the .env file
|
|
80
|
+
const parsed = parseExpand(decrypted, this.overload)
|
|
81
|
+
const { injected, preExisted } = inject(process.env, parsed, this.overload)
|
|
82
|
+
|
|
83
|
+
for (const key of Object.keys(injected)) {
|
|
84
|
+
uniqueInjectedKeys.add(key) // track uniqueInjectedKeys across multiple files
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
envVaultFile: this.envVaultFile, // filepath
|
|
89
|
+
strings,
|
|
90
|
+
dotenvKeys,
|
|
91
|
+
decrypted,
|
|
92
|
+
parsed,
|
|
93
|
+
injected,
|
|
94
|
+
preExisted,
|
|
95
|
+
uniqueInjectedKeys: [...uniqueInjectedKeys]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// handle scenario for comma separated keys - for use with key rotation
|
|
100
|
+
// example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
|
|
101
|
+
_dotenvKeys () {
|
|
102
|
+
return this.DOTENV_KEY.split(',')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_decrypted (dotenvKey, parsedVault) {
|
|
106
|
+
const environment = parseEnvironmentFromDotenvKey(dotenvKey)
|
|
107
|
+
|
|
108
|
+
// DOTENV_KEY_PRODUCTION
|
|
109
|
+
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
110
|
+
const ciphertext = parsedVault[environmentKey]
|
|
111
|
+
if (!ciphertext) {
|
|
112
|
+
const error = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
|
|
113
|
+
error.code = 'NOT_FOUND_DOTENV_ENVIRONMENT'
|
|
114
|
+
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return decrypt(ciphertext, dotenvKey)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// { "DOTENV_VAULT_DEVELOPMENT": "<ciphertext>" }
|
|
122
|
+
_parsedVault (filepath) {
|
|
123
|
+
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
124
|
+
return dotenv.parse(src)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_envs () {
|
|
128
|
+
if (!Array.isArray(this.env)) {
|
|
129
|
+
return [this.env]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return this.env
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = RunVault
|