@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 CHANGED
@@ -162,8 +162,8 @@ More examples
162
162
  ```sh
163
163
  $ echo "HELLO=World" > .env
164
164
 
165
- $ dotenvx run --quiet -- sh -c 'echo $HELLO'
166
- World
165
+ $ dotenvx run --quiet -- sh -c 'echo Hello $HELLO'
166
+ Hello World
167
167
  ```
168
168
 
169
169
  </details>
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.20.1",
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 parseEncryptionKeyFromDotenvKey = require('./../../lib/helpers/parseEncryptionKeyFromDotenvKey')
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 = (main.configDotenv({ path: keysFilepath }).parsed || {})
41
- const dotenvVault = (main.configDotenv({ path: vaultFilepath }).parsed || {})
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 = main.decrypt(ciphertext, key)
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
- let optionEnvFile = options.envFile
19
- if (!Array.isArray(optionEnvFile)) {
20
- optionEnvFile = [optionEnvFile]
21
- }
22
-
23
- // must be at least one .env* file
24
- if (optionEnvFile.length < 1) {
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
- for (const envFilepath of optionEnvFile) {
34
- const filepath = helpers.resolvePath(envFilepath)
28
+ logger.verbose(`loading env from ${envFile}`)
35
29
 
36
- logger.verbose(`loading env from ${filepath}`)
30
+ // TODO: display pre-existing
31
+ // TODO: display added/appended/injected
37
32
 
38
- try {
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
- switch (e.code) {
51
- // missing .env
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
- addedKeys.add(key)
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
- if (addedKeys.size > 0) {
86
- spinner.succeed(`updated ${exampleFilename} (${addedKeys.size})`)
87
- } else {
88
- spinner.done(`no changes (${exampleFilename})`)
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
 
@@ -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
- const systemCommandPath = execa.sync('which', [commandArgs[0]]).stdout
43
- logger.debug(`system command path [${systemCommandPath}]`)
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
- const envVaultFilepath = options.envVaultFile // .env.vault
118
- const filepath = helpers.resolvePath(envVaultFilepath)
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
- if (!fs.existsSync(filepath)) {
121
- logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
122
- } else {
123
- logger.verbose(`loading env from encrypted ${filepath}`)
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
- logger.successv(`injecting env (${result.injected.size}) from encrypted ${envVaultFilepath}`)
161
- } catch (e) {
162
- logger.error(e)
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 { files, readableFilepaths, uniqueInjectedKeys } = new RunDefault(options.envFile, options.env, options.overload).run()
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
@@ -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,10 @@
1
+ const decrypt = require('./decrypt')
2
+ const hash = require('./hash')
3
+
4
+ function changed (ciphertext, raw, dotenvKey) {
5
+ const decrypted = decrypt(ciphertext, dotenvKey)
6
+
7
+ return hash(decrypted) !== hash(raw)
8
+ }
9
+
10
+ module.exports = changed
@@ -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 XXHASH_SEED = 0xABCD
7
- const NONCE_BYTES = 12
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 = this._guessEnvironment(filepath)
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 || this._changed(dotenvKey, ciphertext, raw)) {
29
- ciphertext = this._encrypt(dotenvKey, raw)
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,9 @@
1
+ const xxhash = require('xxhashjs')
2
+
3
+ const XXHASH_SEED = 0xABCD // DO NOT CHANGE
4
+
5
+ function hash (str) {
6
+ return xxhash.h32(str, XXHASH_SEED).toString(16)
7
+ }
8
+
9
+ module.exports = hash
@@ -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
- const result = dotenv.parse(src)
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
- parseExpand,
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
- const RESERVED_ENV_FILES = ['.env.vault', '.env.project', '.env.keys', '.env.me', '.env.x']
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 || this._findEnvFiles()
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._parseExpand(env)
28
+ const parsed = parseExpand(env, this.overload)
28
29
  row.parsed = parsed
29
30
 
30
- const { injected, preExisted } = this._inject(process.env, parsed)
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._parseExpand(src)
55
+ const parsed = parseExpand(src, this.overload)
55
56
  row.parsed = parsed
56
57
 
57
- const { injected, preExisted } = this._inject(process.env, parsed)
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