@dotenvx/dotenvx 0.20.1 → 0.21.0
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 +2 -1
- package/src/cli/actions/decrypt.js +5 -7
- package/src/cli/actions/genexample.js +27 -67
- package/src/cli/actions/run.js +44 -77
- package/src/cli/dotenvx.js +1 -0
- 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/findEnvFiles.js +27 -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/main.js +33 -94
- package/src/lib/services/encrypt.js +3 -13
- package/src/lib/services/genexample.js +94 -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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.21.0",
|
|
3
3
|
"name": "@dotenvx/dotenvx",
|
|
4
4
|
"description": "a better dotenv–from the creator of `dotenv`",
|
|
5
5
|
"author": "@motdotla",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"ora": "^5.4.1",
|
|
43
43
|
"undici": "^5.28.3",
|
|
44
44
|
"update-notifier": "^5.1.0",
|
|
45
|
+
"which": "^4.0.0",
|
|
45
46
|
"winston": "^3.11.0",
|
|
46
47
|
"xxhashjs": "^0.2.2"
|
|
47
48
|
},
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const dotenv = require('dotenv')
|
|
2
3
|
|
|
3
|
-
const main = require('./../../lib/main')
|
|
4
4
|
const logger = require('./../../shared/logger')
|
|
5
5
|
const helpers = require('./../helpers')
|
|
6
6
|
const createSpinner = require('./../../shared/createSpinner')
|
|
7
|
-
const
|
|
7
|
+
const libDecrypt = require('./../../lib/helpers/decrypt')
|
|
8
8
|
|
|
9
9
|
const spinner = createSpinner('decrypting')
|
|
10
10
|
|
|
@@ -37,8 +37,8 @@ async function decrypt () {
|
|
|
37
37
|
process.exit(1)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const dotenvKeys =
|
|
41
|
-
const dotenvVault =
|
|
40
|
+
const dotenvKeys = dotenv.configDotenv({ path: keysFilepath }).parsed
|
|
41
|
+
const dotenvVault = dotenv.configDotenv({ path: vaultFilepath }).parsed
|
|
42
42
|
|
|
43
43
|
Object.entries(dotenvKeys).forEach(([dotenvKey, value]) => {
|
|
44
44
|
// determine environment
|
|
@@ -51,10 +51,8 @@ async function decrypt () {
|
|
|
51
51
|
|
|
52
52
|
// give warning if not found
|
|
53
53
|
if (ciphertext && ciphertext.length >= 1) {
|
|
54
|
-
const key = parseEncryptionKeyFromDotenvKey(value.trim())
|
|
55
|
-
|
|
56
54
|
// Decrypt
|
|
57
|
-
const decrypted =
|
|
55
|
+
const decrypted = libDecrypt(ciphertext, value.trim())
|
|
58
56
|
|
|
59
57
|
// envFilename
|
|
60
58
|
let envFilename = `.env.${environment}`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
-
const helpers = require('./../helpers')
|
|
3
2
|
const main = require('./../../lib/main')
|
|
3
|
+
const helpers = require('./../helpers')
|
|
4
4
|
const logger = require('./../../shared/logger')
|
|
5
5
|
const createSpinner = require('./../../shared/createSpinner')
|
|
6
6
|
|
|
@@ -8,84 +8,44 @@ const spinner = createSpinner('generating')
|
|
|
8
8
|
|
|
9
9
|
const ENCODING = 'utf8'
|
|
10
10
|
|
|
11
|
-
async function genexample () {
|
|
11
|
+
async function genexample (directory) {
|
|
12
12
|
spinner.start()
|
|
13
13
|
await helpers.sleep(500) // better dx
|
|
14
14
|
|
|
15
|
+
logger.debug(`directory: ${directory}`)
|
|
16
|
+
|
|
15
17
|
const options = this.opts()
|
|
16
18
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
spinner.fail('no .env* files found')
|
|
26
|
-
logger.help('? add one with [echo "HELLO=World" > .env] and then run [dotenvx genexample]')
|
|
27
|
-
process.exit(1)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const keys = new Set()
|
|
31
|
-
const addedKeys = new Set()
|
|
20
|
+
try {
|
|
21
|
+
const {
|
|
22
|
+
envExampleFile,
|
|
23
|
+
envFile,
|
|
24
|
+
exampleFilepath,
|
|
25
|
+
addedKeys
|
|
26
|
+
} = main.genexample(directory, options.envFile)
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
const filepath = helpers.resolvePath(envFilepath)
|
|
28
|
+
logger.verbose(`loading env from ${envFile}`)
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
// TODO: display pre-existing
|
|
31
|
+
// TODO: display added/appended/injected
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
40
|
-
const parsed = main.parse(src)
|
|
41
|
-
Object.keys(parsed).forEach(key => { keys.add(key) })
|
|
42
|
-
} catch (e) {
|
|
43
|
-
// calculate development help message depending on state of repo
|
|
44
|
-
const vaultFilepath = helpers.resolvePath('.env.vault')
|
|
45
|
-
let developmentHelp = '? in development: add one with [echo "HELLO=World" > .env] and re-run [dotenvx genexample]'
|
|
46
|
-
if (fs.existsSync(vaultFilepath)) {
|
|
47
|
-
developmentHelp = '? in development: use [dotenvx decrypt] to decrypt .env.vault to .env and then re-run [dotenvx genexample]'
|
|
48
|
-
}
|
|
33
|
+
fs.writeFileSync(exampleFilepath, envExampleFile, ENCODING)
|
|
49
34
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
case 'ENOENT':
|
|
53
|
-
logger.warn(`missing ${envFilepath} file (${filepath})`)
|
|
54
|
-
logger.help(developmentHelp)
|
|
55
|
-
break
|
|
56
|
-
|
|
57
|
-
// unhandled error
|
|
58
|
-
default:
|
|
59
|
-
logger.warn(e)
|
|
60
|
-
break
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const exampleFilename = '.env.example'
|
|
66
|
-
const exampleFilepath = helpers.resolvePath(exampleFilename)
|
|
67
|
-
if (!fs.existsSync(exampleFilepath)) {
|
|
68
|
-
logger.verbose(`creating ${exampleFilename}`)
|
|
69
|
-
fs.writeFileSync(exampleFilename, `# ${exampleFilename}\n`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const currentEnvExample = (main.configDotenv({ path: exampleFilepath }).parsed || {})
|
|
73
|
-
const keysArray = Array.from(keys)
|
|
74
|
-
|
|
75
|
-
keysArray.forEach(key => {
|
|
76
|
-
if (key in currentEnvExample) {
|
|
77
|
-
logger.verbose(`pre-existing ${key} in ${exampleFilename}`)
|
|
35
|
+
if (addedKeys.length > 0) {
|
|
36
|
+
spinner.succeed(`updated .env.example (${addedKeys.length})`)
|
|
78
37
|
} else {
|
|
79
|
-
|
|
80
|
-
logger.verbose(`appending ${key} to ${exampleFilename}`)
|
|
81
|
-
fs.appendFileSync('.env.example', `${key}=""\n`, ENCODING)
|
|
38
|
+
spinner.done('no changes (.env.example)')
|
|
82
39
|
}
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
40
|
+
} catch (error) {
|
|
41
|
+
spinner.fail(error.message)
|
|
42
|
+
if (error.help) {
|
|
43
|
+
logger.help(error.help)
|
|
44
|
+
}
|
|
45
|
+
if (error.code) {
|
|
46
|
+
logger.debug(`ERROR_CODE: ${error.code}`)
|
|
47
|
+
}
|
|
48
|
+
process.exit(1)
|
|
89
49
|
}
|
|
90
50
|
}
|
|
91
51
|
|
package/src/cli/actions/run.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
1
|
const path = require('path')
|
|
3
2
|
const execa = require('execa')
|
|
3
|
+
const which = require('which')
|
|
4
4
|
const logger = require('./../../shared/logger')
|
|
5
|
-
const helpers = require('./../helpers')
|
|
6
|
-
const main = require('./../../lib/main')
|
|
7
|
-
const parseEncryptionKeyFromDotenvKey = require('./../../lib/helpers/parseEncryptionKeyFromDotenvKey')
|
|
8
5
|
|
|
9
6
|
const RunDefault = require('./../../lib/services/runDefault')
|
|
7
|
+
const RunVault = require('./../../lib/services/runVault')
|
|
10
8
|
|
|
11
|
-
const ENCODING = 'utf8'
|
|
12
9
|
const REPORT_ISSUE_LINK = 'https://github.com/dotenvx/dotenvx/issues/new'
|
|
13
10
|
|
|
14
11
|
const executeCommand = async function (commandArgs, env) {
|
|
@@ -39,8 +36,13 @@ const executeCommand = async function (commandArgs, env) {
|
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
let systemCommandPath = commandArgs[0]
|
|
40
|
+
try {
|
|
41
|
+
systemCommandPath = which.sync(`${commandArgs[0]}`)
|
|
42
|
+
logger.debug(`expanding process command to [${systemCommandPath} ${commandArgs.slice(1).join(' ')}]`)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
logger.debug(`could not expand process command. using [${systemCommandPath} ${commandArgs.slice(1).join(' ')}]`)
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
// commandProcess = execa(commandArgs[0], commandArgs.slice(1), {
|
|
46
48
|
commandProcess = execa(systemCommandPath, commandArgs.slice(1), {
|
|
@@ -80,31 +82,6 @@ const executeCommand = async function (commandArgs, env) {
|
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedVault) {
|
|
84
|
-
// Parse DOTENV_KEY. Format is a URI
|
|
85
|
-
let uri
|
|
86
|
-
try {
|
|
87
|
-
uri = new URL(dotenvKey)
|
|
88
|
-
} catch (e) {
|
|
89
|
-
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Get environment
|
|
93
|
-
const environment = uri.searchParams.get('environment')
|
|
94
|
-
if (!environment) {
|
|
95
|
-
throw new Error('INVALID_DOTENV_KEY: Missing environment part')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Get ciphertext payload
|
|
99
|
-
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
100
|
-
const ciphertext = parsedVault[environmentKey] // DOTENV_VAULT_PRODUCTION
|
|
101
|
-
if (!ciphertext) {
|
|
102
|
-
throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return ciphertext
|
|
106
|
-
}
|
|
107
|
-
|
|
108
85
|
async function run () {
|
|
109
86
|
const commandArgs = this.args
|
|
110
87
|
logger.debug(`process command [${commandArgs.join(' ')}]`)
|
|
@@ -114,56 +91,46 @@ async function run () {
|
|
|
114
91
|
|
|
115
92
|
// load from .env.vault file
|
|
116
93
|
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
117
|
-
|
|
118
|
-
|
|
94
|
+
try {
|
|
95
|
+
const {
|
|
96
|
+
envVaultFile,
|
|
97
|
+
parsed,
|
|
98
|
+
injected,
|
|
99
|
+
preExisted,
|
|
100
|
+
uniqueInjectedKeys
|
|
101
|
+
} = new RunVault(options.envVaultFile, options.env, process.env.DOTENV_KEY, options.overload).run()
|
|
102
|
+
|
|
103
|
+
logger.verbose(`loading env from encrypted ${envVaultFile} (${path.resolve(envVaultFile)})`)
|
|
104
|
+
logger.debug(`decrypting encrypted env from ${envVaultFile} (${path.resolve(envVaultFile)})`)
|
|
105
|
+
|
|
106
|
+
// debug parsed
|
|
107
|
+
logger.debug(parsed)
|
|
108
|
+
|
|
109
|
+
// verbose/debug injected key/value
|
|
110
|
+
for (const [key, value] of Object.entries(injected)) {
|
|
111
|
+
logger.verbose(`${key} set`)
|
|
112
|
+
logger.debug(`${key} set to ${value}`)
|
|
113
|
+
}
|
|
119
114
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
127
|
-
const parsedVault = main.parse(src)
|
|
128
|
-
|
|
129
|
-
logger.debug(`decrypting encrypted env from ${filepath}`)
|
|
130
|
-
// handle scenario for comma separated keys - for use with key rotation
|
|
131
|
-
// example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
|
|
132
|
-
const dotenvKeys = process.env.DOTENV_KEY.split(',')
|
|
133
|
-
const length = dotenvKeys.length
|
|
134
|
-
|
|
135
|
-
let decrypted
|
|
136
|
-
for (let i = 0; i < length; i++) {
|
|
137
|
-
try {
|
|
138
|
-
// Get full dotenvKey
|
|
139
|
-
const dotenvKey = dotenvKeys[i].trim()
|
|
140
|
-
|
|
141
|
-
const key = parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
142
|
-
const ciphertext = _parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
|
|
143
|
-
|
|
144
|
-
// Decrypt
|
|
145
|
-
decrypted = main.decrypt(ciphertext, key)
|
|
146
|
-
|
|
147
|
-
break
|
|
148
|
-
} catch (error) {
|
|
149
|
-
// last key
|
|
150
|
-
if (i + 1 >= length) {
|
|
151
|
-
throw error
|
|
152
|
-
}
|
|
153
|
-
// try next key
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
logger.debug(decrypted)
|
|
157
|
-
const parsed = main.parseExpand(decrypted)
|
|
158
|
-
const result = main.inject(process.env, parsed, options.overload)
|
|
115
|
+
// verbose/debug preExisted key/value
|
|
116
|
+
for (const [key, value] of Object.entries(preExisted)) {
|
|
117
|
+
logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
|
|
118
|
+
logger.debug(`${key} pre-exists as ${value} (protip: use --overload to override)`)
|
|
119
|
+
}
|
|
159
120
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
121
|
+
logger.successv(`injecting env (${uniqueInjectedKeys.length}) from encrypted ${envVaultFile}`)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error(error.message)
|
|
124
|
+
if (error.help) {
|
|
125
|
+
logger.help(error.help)
|
|
163
126
|
}
|
|
164
127
|
}
|
|
165
128
|
} else {
|
|
166
|
-
const {
|
|
129
|
+
const {
|
|
130
|
+
files,
|
|
131
|
+
readableFilepaths,
|
|
132
|
+
uniqueInjectedKeys
|
|
133
|
+
} = new RunDefault(options.envFile, options.env, options.overload).run()
|
|
167
134
|
|
|
168
135
|
for (const file of files) {
|
|
169
136
|
const filepath = file.filepath
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -95,6 +95,7 @@ program.command('prebuild')
|
|
|
95
95
|
// dotenvx genexample
|
|
96
96
|
program.command('genexample')
|
|
97
97
|
.description('generate .env.example')
|
|
98
|
+
.argument('[directory]', 'directory to generate from', '.')
|
|
98
99
|
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
|
|
99
100
|
.action(require('./actions/genexample'))
|
|
100
101
|
|
|
@@ -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,27 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
const RESERVED_ENV_FILES = ['.env.vault', '.env.project', '.env.keys', '.env.me', '.env.x', '.env.example']
|
|
4
|
+
|
|
5
|
+
function findEnvFiles (directory) {
|
|
6
|
+
try {
|
|
7
|
+
const files = fs.readdirSync(directory)
|
|
8
|
+
const envFiles = files.filter(file =>
|
|
9
|
+
file.startsWith('.env') &&
|
|
10
|
+
!file.endsWith('.previous') &&
|
|
11
|
+
!RESERVED_ENV_FILES.includes(file)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
return envFiles
|
|
15
|
+
} catch (e) {
|
|
16
|
+
if (e.code === 'ENOENT') {
|
|
17
|
+
const error = new Error(`missing directory (${directory})`)
|
|
18
|
+
error.code = 'MISSING_DIRECTORY'
|
|
19
|
+
|
|
20
|
+
throw error
|
|
21
|
+
} else {
|
|
22
|
+
throw e
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = findEnvFiles
|
|
@@ -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
|
package/src/lib/main.js
CHANGED
|
@@ -1,112 +1,26 @@
|
|
|
1
1
|
const logger = require('./../shared/logger')
|
|
2
2
|
const dotenv = require('dotenv')
|
|
3
|
-
const dotenvExpand = require('dotenv-expand')
|
|
4
3
|
|
|
5
4
|
// services
|
|
6
5
|
const Encrypt = require('./services/encrypt')
|
|
7
6
|
const Ls = require('./services/ls')
|
|
8
7
|
const Get = require('./services/get')
|
|
8
|
+
const Genexample = require('./services/genexample')
|
|
9
9
|
|
|
10
|
+
// proxies to dotenv
|
|
10
11
|
const config = function (options) {
|
|
11
12
|
return dotenv.config(options)
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const decrypt = function (encrypted, keyStr) {
|
|
15
|
-
try {
|
|
16
|
-
return dotenv.decrypt(encrypted, keyStr)
|
|
17
|
-
} catch (e) {
|
|
18
|
-
switch (e.code) {
|
|
19
|
-
case 'DECRYPTION_FAILED':
|
|
20
|
-
// more helpful error when decryption fails
|
|
21
|
-
logger.error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
|
|
22
|
-
logger.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.')
|
|
23
|
-
logger.debug(`[DECRYPTION_FAILED] DOTENV_KEY is ${process.env.DOTENV_KEY}`)
|
|
24
|
-
process.exit(1)
|
|
25
|
-
break
|
|
26
|
-
default:
|
|
27
|
-
throw e
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
15
|
const configDotenv = function (options) {
|
|
33
16
|
return dotenv.configDotenv(options)
|
|
34
17
|
}
|
|
35
18
|
|
|
36
19
|
const parse = function (src) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
logger.debug(result)
|
|
40
|
-
|
|
41
|
-
return result
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const parseExpand = function (src, overload) {
|
|
45
|
-
const parsed = dotenv.parse(src)
|
|
46
|
-
|
|
47
|
-
// consider moving this logic straight into dotenv-expand
|
|
48
|
-
let inputParsed = {}
|
|
49
|
-
if (overload) {
|
|
50
|
-
inputParsed = { ...process.env, ...parsed }
|
|
51
|
-
} else {
|
|
52
|
-
inputParsed = { ...parsed, ...process.env }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const expandPlease = {
|
|
56
|
-
processEnv: {},
|
|
57
|
-
parsed: inputParsed
|
|
58
|
-
}
|
|
59
|
-
const expanded = dotenvExpand.expand(expandPlease).parsed
|
|
60
|
-
|
|
61
|
-
// 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
|
|
62
|
-
const result = {}
|
|
63
|
-
for (const key in parsed) {
|
|
64
|
-
result[key] = expanded[key]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
logger.debug(result)
|
|
68
|
-
|
|
69
|
-
return result
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const inject = function (processEnv = {}, parsed = {}, overload = false) {
|
|
73
|
-
if (typeof parsed !== 'object') {
|
|
74
|
-
throw new Error('OBJECT_REQUIRED: Please check the parsed argument being passed to inject')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const injected = new Set()
|
|
78
|
-
const preExisting = new Set()
|
|
79
|
-
|
|
80
|
-
// set processEnv
|
|
81
|
-
for (const key of Object.keys(parsed)) {
|
|
82
|
-
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|
83
|
-
if (overload === true) {
|
|
84
|
-
processEnv[key] = parsed[key]
|
|
85
|
-
injected.add(key)
|
|
86
|
-
|
|
87
|
-
logger.verbose(`${key} set`)
|
|
88
|
-
logger.debug(`${key} set to ${parsed[key]}`)
|
|
89
|
-
} else {
|
|
90
|
-
preExisting.add(key)
|
|
91
|
-
|
|
92
|
-
logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
|
|
93
|
-
logger.debug(`${key} pre-exists as ${processEnv[key]} (protip: use --overload to override)`)
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
processEnv[key] = parsed[key]
|
|
97
|
-
injected.add(key)
|
|
98
|
-
|
|
99
|
-
logger.verbose(`${key} set`)
|
|
100
|
-
logger.debug(`${key} set to ${parsed[key]}`)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
injected,
|
|
106
|
-
preExisting
|
|
107
|
-
}
|
|
20
|
+
return dotenv.parse(src)
|
|
108
21
|
}
|
|
109
22
|
|
|
23
|
+
// actions related
|
|
110
24
|
const encrypt = function (directory, envFile) {
|
|
111
25
|
return new Encrypt(directory, envFile).run()
|
|
112
26
|
}
|
|
@@ -115,18 +29,43 @@ const ls = function (directory, envFile) {
|
|
|
115
29
|
return new Ls(directory, envFile).run()
|
|
116
30
|
}
|
|
117
31
|
|
|
32
|
+
const genexample = function (directory, envFile) {
|
|
33
|
+
return new Genexample(directory, envFile).run()
|
|
34
|
+
}
|
|
35
|
+
|
|
118
36
|
const get = function (key, envFile, overload, all) {
|
|
119
37
|
return new Get(key, envFile, overload, all).run()
|
|
120
38
|
}
|
|
121
39
|
|
|
40
|
+
// misc/cleanup
|
|
41
|
+
const decrypt = function (encrypted, keyStr) {
|
|
42
|
+
try {
|
|
43
|
+
return dotenv.decrypt(encrypted, keyStr)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
switch (e.code) {
|
|
46
|
+
case 'DECRYPTION_FAILED':
|
|
47
|
+
// more helpful error when decryption fails
|
|
48
|
+
logger.error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
|
|
49
|
+
logger.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.')
|
|
50
|
+
logger.debug(`[DECRYPTION_FAILED] DOTENV_KEY is ${process.env.DOTENV_KEY}`)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
break
|
|
53
|
+
default:
|
|
54
|
+
throw e
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
122
59
|
module.exports = {
|
|
60
|
+
// dotenv proxies
|
|
123
61
|
config,
|
|
124
62
|
configDotenv,
|
|
125
|
-
decrypt,
|
|
126
63
|
parse,
|
|
127
|
-
|
|
128
|
-
inject,
|
|
64
|
+
// actions related
|
|
129
65
|
encrypt,
|
|
130
66
|
ls,
|
|
131
|
-
get
|
|
67
|
+
get,
|
|
68
|
+
genexample,
|
|
69
|
+
// misc/cleanup
|
|
70
|
+
decrypt
|
|
132
71
|
}
|
|
@@ -6,12 +6,13 @@ const DotenvKeys = require('./../helpers/dotenvKeys')
|
|
|
6
6
|
const DotenvVault = require('./../helpers/dotenvVault')
|
|
7
7
|
|
|
8
8
|
const ENCODING = 'utf8'
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
const findEnvFiles = require('../helpers/findEnvFiles')
|
|
10
11
|
|
|
11
12
|
class Encrypt {
|
|
12
13
|
constructor (directory = '.', envFile) {
|
|
13
14
|
this.directory = directory
|
|
14
|
-
this.envFile = envFile ||
|
|
15
|
+
this.envFile = envFile || findEnvFiles(directory)
|
|
15
16
|
// calculated
|
|
16
17
|
this.envKeysFilepath = path.resolve(this.directory, '.env.keys')
|
|
17
18
|
this.envVaultFilepath = path.resolve(this.directory, '.env.vault')
|
|
@@ -112,17 +113,6 @@ class Encrypt {
|
|
|
112
113
|
|
|
113
114
|
return dotenv.configDotenv(options).parsed
|
|
114
115
|
}
|
|
115
|
-
|
|
116
|
-
_findEnvFiles () {
|
|
117
|
-
const files = fs.readdirSync(this.directory)
|
|
118
|
-
const envFiles = files.filter(file =>
|
|
119
|
-
file.startsWith('.env') &&
|
|
120
|
-
!file.endsWith('.previous') &&
|
|
121
|
-
!RESERVED_ENV_FILES.includes(file)
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
return envFiles
|
|
125
|
-
}
|
|
126
116
|
}
|
|
127
117
|
|
|
128
118
|
module.exports = Encrypt
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const dotenv = require('dotenv')
|
|
4
|
+
|
|
5
|
+
const ENCODING = 'utf8'
|
|
6
|
+
|
|
7
|
+
const findEnvFiles = require('../helpers/findEnvFiles')
|
|
8
|
+
|
|
9
|
+
class Genexample {
|
|
10
|
+
constructor (directory = '.', envFile) {
|
|
11
|
+
this.directory = directory
|
|
12
|
+
this.envFile = envFile || findEnvFiles(directory)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
run () {
|
|
16
|
+
if (this.envFile.length < 1) {
|
|
17
|
+
const code = 'MISSING_ENV_FILES'
|
|
18
|
+
const message = 'no .env* files found'
|
|
19
|
+
const help = '? add one with [echo "HELLO=World" > .env] and then run [dotenvx genexample]'
|
|
20
|
+
|
|
21
|
+
const error = new Error(message)
|
|
22
|
+
error.code = code
|
|
23
|
+
error.help = help
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const keys = new Set()
|
|
28
|
+
const addedKeys = new Set()
|
|
29
|
+
const envFilepaths = this._envFilepaths()
|
|
30
|
+
|
|
31
|
+
for (const envFilepath of envFilepaths) {
|
|
32
|
+
const filepath = path.resolve(this.directory, envFilepath)
|
|
33
|
+
if (!fs.existsSync(filepath)) {
|
|
34
|
+
const code = 'MISSING_ENV_FILE'
|
|
35
|
+
const message = `file does not exist at [${filepath}]`
|
|
36
|
+
const help = `? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx genexample]`
|
|
37
|
+
|
|
38
|
+
const error = new Error(message)
|
|
39
|
+
error.code = code
|
|
40
|
+
error.help = help
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const parsed = dotenv.configDotenv({ path: filepath }).parsed
|
|
45
|
+
for (const key of Object.keys(parsed)) {
|
|
46
|
+
keys.add(key)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let envExampleFile = ''
|
|
51
|
+
const exampleFilename = '.env.example'
|
|
52
|
+
const exampleFilepath = path.resolve(this.directory, exampleFilename)
|
|
53
|
+
if (!fs.existsSync(exampleFilepath)) {
|
|
54
|
+
envExampleFile += `# ${exampleFilename}\n`
|
|
55
|
+
} else {
|
|
56
|
+
envExampleFile = fs.readFileSync(exampleFilepath, ENCODING)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const currentEnvExample = dotenv.configDotenv({ path: exampleFilepath }).parsed
|
|
60
|
+
const injected = {}
|
|
61
|
+
const preExisted = {}
|
|
62
|
+
|
|
63
|
+
for (const key of [...keys]) {
|
|
64
|
+
if (key in currentEnvExample) {
|
|
65
|
+
preExisted[key] = currentEnvExample[key]
|
|
66
|
+
} else {
|
|
67
|
+
envExampleFile += `${key}=""\n`
|
|
68
|
+
|
|
69
|
+
addedKeys.add(key)
|
|
70
|
+
|
|
71
|
+
injected[key] = ''
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
envExampleFile,
|
|
77
|
+
envFile: this.envFile,
|
|
78
|
+
exampleFilepath,
|
|
79
|
+
addedKeys: [...addedKeys],
|
|
80
|
+
injected,
|
|
81
|
+
preExisted
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_envFilepaths () {
|
|
86
|
+
if (!Array.isArray(this.envFile)) {
|
|
87
|
+
return [this.envFile]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.envFile
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = Genexample
|
|
@@ -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
|