@dotenvx/dotenvx 1.9.0 → 1.10.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/CHANGELOG.md CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- ## [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.9.0...main)
5
+ ## [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.10.0...main)
6
+
7
+ ## 1.10.0
8
+
9
+ ### Removed
10
+
11
+ * remove `dotenvx ext vault`, replace with [dotenvx-ext-vault](https://github.com/dotenvx/dotenvx-ext-vaut) (install there to continue using `ext vault`) ([#351](https://github.com/dotenvx/dotenvx/pull/351))
12
+
13
+ ## 1.9.1
14
+
15
+ ### Added
16
+
17
+ * warn if private key is missing or blank ([#349](https://github.com/dotenvx/dotenvx/pull/349))
6
18
 
7
19
  ## 1.9.0
8
20
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.9.0",
2
+ "version": "1.10.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -45,8 +45,7 @@
45
45
  "object-treeify": "1.1.33",
46
46
  "picomatch": "^4.0.2",
47
47
  "tinyexec": "^0.2.0",
48
- "which": "^4.0.0",
49
- "xxhashjs": "^0.2.2"
48
+ "which": "^4.0.0"
50
49
  },
51
50
  "devDependencies": {
52
51
  "capture-console": "^1.0.2",
@@ -9,6 +9,9 @@ ext
9
9
  .description('🔌 extensions')
10
10
  .allowUnknownOption()
11
11
 
12
+ // list known extensions here you want to display
13
+ ext.addHelpText('after', ' vault 🔐 manage .env.vault files')
14
+
12
15
  ext
13
16
  .argument('[command]', 'dynamic ext command')
14
17
  .argument('[args...]', 'dynamic ext command arguments')
@@ -55,6 +58,4 @@ ext.command('scan')
55
58
  .description('scan for leaked secrets')
56
59
  .action(require('./../actions/ext/scan'))
57
60
 
58
- ext.addCommand(require('./../commands/ext/vault'))
59
-
60
61
  module.exports = ext
@@ -22,32 +22,6 @@ Try it:
22
22
  `
23
23
  }
24
24
 
25
- const vaultEncrypt = function () {
26
- return `
27
- Examples:
28
-
29
- \`\`\`
30
- $ dotenvx ext vault encrypt
31
- \`\`\`
32
-
33
- Try it:
34
-
35
- \`\`\`
36
- $ echo "HELLO=World" > .env
37
- $ echo "HELLO=production" > .env.production
38
- $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
39
-
40
- $ dotenvx ext vault encrypt
41
- encrypted to .env.vault (.env,.env.production)
42
- keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
43
-
44
- $ DOTENV_KEY='<dotenv_key_production>' dotenvx run -- node index.js
45
- [dotenvx] injecting env (1) from encrypted .env.vault
46
- Hello production
47
- \`\`\`
48
- `
49
- }
50
-
51
25
  const precommit = function () {
52
26
  return `
53
27
  Examples:
@@ -117,7 +91,6 @@ Examples:
117
91
 
118
92
  module.exports = {
119
93
  run,
120
- vaultEncrypt,
121
94
  precommit,
122
95
  prebuild,
123
96
  gitignore,
@@ -3,35 +3,41 @@ const { decrypt } = require('eciesjs')
3
3
  const PREFIX = 'encrypted:'
4
4
 
5
5
  function decryptValue (value, privateKey) {
6
+ let decryptedValue
7
+ let decryptionError
8
+
6
9
  if (!value.startsWith(PREFIX)) {
7
10
  return value
8
11
  }
9
12
 
10
- const privateKeys = privateKey.split(',')
11
-
12
- let decryptedValue
13
- let decryptionError
14
- for (const key of privateKeys) {
15
- const secret = Buffer.from(key, 'hex')
16
- const encoded = value.substring(PREFIX.length)
17
- const ciphertext = Buffer.from(encoded, 'base64')
18
-
19
- try {
20
- decryptedValue = decrypt(secret, ciphertext).toString()
21
- decryptionError = null // reset to null error (scenario for multiple private keys)
22
- break
23
- } catch (e) {
24
- if (e.message === 'Invalid private key') {
25
- decryptionError = new Error('private key looks invalid')
26
- } else if (e.message === 'Unsupported state or unable to authenticate data') {
27
- decryptionError = new Error('private key looks wrong')
28
- } else if (e.message === 'Point of length 65 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes') {
29
- decryptionError = new Error('encrypted data looks malformed')
30
- } else {
31
- decryptionError = new Error(`${e.message}`)
13
+ privateKey = privateKey || ''
14
+ if (privateKey.length <= 0) {
15
+ decryptionError = new Error('private key missing or blank')
16
+ decryptionError.code = 'DECRYPTION_FAILED'
17
+ } else {
18
+ const privateKeys = privateKey.split(',')
19
+ for (const key of privateKeys) {
20
+ const secret = Buffer.from(key, 'hex')
21
+ const encoded = value.substring(PREFIX.length)
22
+ const ciphertext = Buffer.from(encoded, 'base64')
23
+
24
+ try {
25
+ decryptedValue = decrypt(secret, ciphertext).toString()
26
+ decryptionError = null // reset to null error (scenario for multiple private keys)
27
+ break
28
+ } catch (e) {
29
+ if (e.message === 'Invalid private key') {
30
+ decryptionError = new Error('private key looks invalid')
31
+ } else if (e.message === 'Unsupported state or unable to authenticate data') {
32
+ decryptionError = new Error('private key looks wrong')
33
+ } else if (e.message === 'Point of length 65 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes') {
34
+ decryptionError = new Error('encrypted data looks malformed')
35
+ } else {
36
+ decryptionError = new Error(`${e.message}`)
37
+ }
38
+
39
+ decryptionError.code = 'DECRYPTION_FAILED'
32
40
  }
33
-
34
- decryptionError.code = 'DECRYPTION_FAILED'
35
41
  }
36
42
  }
37
43
 
@@ -22,10 +22,10 @@ function executeExtension (ext, command, rawArgs) {
22
22
 
23
23
  const result = childProcess.spawnSync(`dotenvx-ext-${command}`, forwardedArgs, { stdio: 'inherit', env })
24
24
  if (result.error) {
25
- if (command === 'vault') {
26
- // when ready, uncomment to deprecate ext vault
27
- // logger.warn(`[INSTALLATION_NEEDED] install dotenvx-ext-${command} to use [dotenvx ext ${command}] commands`)
28
- // logger.help('? see installation instructions [https://github.com/dotenvx/dotenvx-ext-vault]')
25
+ // list known extension here for convenience to the user
26
+ if (['vault', 'hub'].includes(command)) {
27
+ logger.warn(`[INSTALLATION_NEEDED] install dotenvx-ext-${command} to use [dotenvx ext ${command}] commands`)
28
+ logger.help('? see installation instructions [https://github.com/dotenvx/dotenvx-ext-vault]')
29
29
  } else {
30
30
  logger.info(`error: unknown command '${command}'`)
31
31
  }
@@ -4,8 +4,8 @@ const dotenvExpand = require('./dotenvExpand')
4
4
  const decryptValue = require('./decryptValue')
5
5
  const truncate = require('./truncate')
6
6
 
7
- function warning (e, key, privateKey) {
8
- const warning = new Error(`[${e.code}] could not decrypt ${key} using private key ${truncate(privateKey)}`)
7
+ function warning (e, key, privateKey = null) {
8
+ const warning = new Error(`[${e.code}] could not decrypt ${key} using private key '${truncate(privateKey)}'`)
9
9
  warning.code = e.code
10
10
  warning.help = `[${e.code}] ? ${e.message}`
11
11
 
@@ -17,14 +17,12 @@ function parseDecryptEvalExpand (src, privateKey = null, processEnv = process.en
17
17
 
18
18
  // parse
19
19
  const parsed = dotenv.parse(src)
20
- if (privateKey && privateKey.length > 0) {
21
- for (const key in parsed) {
22
- try {
23
- const decryptedValue = decryptValue(parsed[key], privateKey)
24
- parsed[key] = decryptedValue
25
- } catch (_e) {
26
- // do nothing. warnings tracked further below.
27
- }
20
+ for (const key in parsed) {
21
+ try {
22
+ const decryptedValue = decryptValue(parsed[key], privateKey)
23
+ parsed[key] = decryptedValue
24
+ } catch (_e) {
25
+ // do nothing. warnings tracked further below.
28
26
  }
29
27
  }
30
28
 
@@ -41,22 +39,20 @@ function parseDecryptEvalExpand (src, privateKey = null, processEnv = process.en
41
39
  parsed: evaled
42
40
  }
43
41
  const expanded = dotenvExpand.expand(inputEvaled)
44
- if (privateKey && privateKey.length > 0) {
45
- for (const key in expanded.parsed) {
46
- try {
47
- const decryptedValue = decryptValue(expanded.parsed[key], privateKey)
48
- expanded.parsed[key] = decryptedValue
49
- } catch (e) {
50
- warnings.push(warning(e, key, privateKey))
51
- }
42
+ for (const key in expanded.parsed) {
43
+ try {
44
+ const decryptedValue = decryptValue(expanded.parsed[key], privateKey)
45
+ expanded.parsed[key] = decryptedValue
46
+ } catch (e) {
47
+ warnings.push(warning(e, key, privateKey))
52
48
  }
53
- for (const key in processEnv) {
54
- try {
55
- const decryptedValue = decryptValue(processEnv[key], privateKey)
56
- processEnv[key] = decryptedValue
57
- } catch (e) {
58
- warnings.push(warning(e, key, privateKey))
59
- }
49
+ }
50
+ for (const key in processEnv) {
51
+ try {
52
+ const decryptedValue = decryptValue(processEnv[key], privateKey)
53
+ processEnv[key] = decryptedValue
54
+ } catch (e) {
55
+ warnings.push(warning(e, key, privateKey))
60
56
  }
61
57
  }
62
58
 
@@ -1,6 +1,10 @@
1
1
  function truncate (str, showChar = 7) {
2
- const visiblePart = str.slice(0, showChar)
3
- return visiblePart + '…'
2
+ if (str && str.length > 0) {
3
+ const visiblePart = str.slice(0, showChar)
4
+ return visiblePart + '…'
5
+ } else {
6
+ return ''
7
+ }
4
8
  }
5
9
 
6
10
  module.exports = truncate
package/src/lib/main.d.ts CHANGED
@@ -180,18 +180,6 @@ export function encrypt(
180
180
  key?: string | string[]
181
181
  ): EncryptOutput;
182
182
 
183
- export type VaultEncryptOutput = {
184
- dotenvKeys: Record<string, string>;
185
- dotenvKeysFile: string;
186
- addedKeys: string[];
187
- existingKeys: string[];
188
- dotenvVaultFile: string;
189
- addedVaults: string[];
190
- existingVaults: string[];
191
- addedDotenvFilenames: string[];
192
- envFile: string | string[];
193
- };
194
-
195
183
  /**
196
184
  * List all env files in the current working directory
197
185
  *
@@ -288,22 +276,3 @@ export function genexample(
288
276
  directory: string,
289
277
  envFile: string
290
278
  ): GenExampleOutput;
291
-
292
- /**
293
- * Decrypt ciphertext
294
- * @param encrypted - the encrypted ciphertext string
295
- * @param keyStr - the decryption key string
296
- */
297
- export function vaultDecrypt(encrypted: string, keyStr: string): string;
298
-
299
- /**
300
- * Encrypt plaintext
301
- *
302
- * @see https://dotenvx.com/docs
303
- * @param directory - current working directory
304
- * @param envFile - path to the .env file(s)
305
- */
306
- export function vaultEncrypt(
307
- directory?: string,
308
- envFile?: string | string[]
309
- ): VaultEncryptOutput;
package/src/lib/main.js CHANGED
@@ -8,11 +8,9 @@ const Ls = require('./services/ls')
8
8
  const Get = require('./services/get')
9
9
  const Run = require('./services/run')
10
10
  const Sets = require('./services/sets')
11
- const Status = require('./services/status')
12
11
  const Encrypt = require('./services/encrypt')
13
12
  const Decrypt = require('./services/decrypt')
14
13
  const Genexample = require('./services/genexample')
15
- const VaultEncrypt = require('./services/vaultEncrypt')
16
14
 
17
15
  // helpers
18
16
  const dotenvOptionPaths = require('./helpers/dotenvOptionPaths')
@@ -198,43 +196,6 @@ const decrypt = function (envFile, key, excludeKey) {
198
196
  return new Decrypt(envFile, key, excludeKey).run()
199
197
  }
200
198
 
201
- /** @type {import('./main').status} */
202
- const status = function (directory) {
203
- return new Status(directory).run()
204
- }
205
-
206
- // misc/cleanup
207
-
208
- /** @type {import('./main').vaultDecrypt} */
209
- const vaultDecrypt = function (encrypted, keyStr) {
210
- try {
211
- return dotenv.decrypt(encrypted, keyStr)
212
- } catch (e) {
213
- switch (e.code) {
214
- case 'DECRYPTION_FAILED':
215
- // more helpful error when decryption fails
216
- logger.error(
217
- '[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.'
218
- )
219
- logger.help(
220
- '[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.'
221
- )
222
- logger.debug(
223
- `[DECRYPTION_FAILED] DOTENV_KEY is ${process.env.DOTENV_KEY}`
224
- )
225
- process.exit(1)
226
- break
227
- default:
228
- throw e
229
- }
230
- }
231
- }
232
-
233
- /** @type {import('./main').vaultEncrypt} */
234
- const vaultEncrypt = function (directory, envFile) {
235
- return new VaultEncrypt(directory, envFile).run()
236
- }
237
-
238
199
  module.exports = {
239
200
  // dotenv proxies
240
201
  config,
@@ -246,9 +207,5 @@ module.exports = {
246
207
  ls,
247
208
  get,
248
209
  set,
249
- status,
250
- genexample,
251
- // misc/cleanup
252
- vaultEncrypt,
253
- vaultDecrypt
210
+ genexample
254
211
  }
@@ -1,64 +0,0 @@
1
- const fs = require('fs')
2
-
3
- const { logger } = require('./../../../../shared/logger')
4
-
5
- const VaultDecrypt = require('./../../../../lib/services/vaultDecrypt')
6
-
7
- function decrypt (directory) {
8
- logger.debug(`directory: ${directory}`)
9
-
10
- const options = this.opts()
11
- logger.debug(`options: ${JSON.stringify(options)}`)
12
-
13
- try {
14
- const {
15
- processedEnvs,
16
- changedFilenames,
17
- unchangedFilenames
18
- } = new VaultDecrypt(directory, options.environment).run()
19
-
20
- for (const env of processedEnvs) {
21
- if (env.warning) {
22
- const warning = env.warning
23
- logger.warn(warning.message)
24
- } else {
25
- if (env.shouldWrite) {
26
- logger.debug(`writing ${env.filepath}`)
27
- fs.writeFileSync(env.filepath, env.decrypted)
28
- } else {
29
- logger.debug(`no changes for ${env.filename}`)
30
- }
31
- }
32
- }
33
-
34
- let changedMsg = ''
35
- if (changedFilenames.length > 0) {
36
- changedMsg = `decrypted (${changedFilenames.join(', ')})`
37
- }
38
-
39
- let unchangedMsg = ''
40
- if (unchangedFilenames.length > 0) {
41
- unchangedMsg = `no changes (${unchangedFilenames.join(', ')})`
42
- }
43
-
44
- if (changedMsg.length > 0) {
45
- logger.success(`${changedMsg} ${unchangedMsg}`)
46
- } else {
47
- logger.blank(`${unchangedMsg}`)
48
- }
49
- } catch (error) {
50
- logger.error(error.message)
51
- if (error.help) {
52
- logger.help(error.help)
53
- }
54
- if (error.debug) {
55
- logger.debug(error.debug)
56
- }
57
- if (error.code) {
58
- logger.debug(`ERROR_CODE: ${error.code}`)
59
- }
60
- process.exit(1)
61
- }
62
- }
63
-
64
- module.exports = decrypt
@@ -1,78 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
-
4
- const main = require('./../../../../lib/main')
5
- const { logger } = require('./../../../../shared/logger')
6
- const pluralize = require('./../../../../lib/helpers/pluralize')
7
-
8
- function encrypt (directory) {
9
- logger.debug(`directory: ${directory}`)
10
-
11
- const options = this.opts()
12
- logger.debug(`options: ${JSON.stringify(options)}`)
13
-
14
- try {
15
- const {
16
- dotenvKeys,
17
- dotenvKeysFile,
18
- addedKeys,
19
- existingKeys,
20
- dotenvVaultFile,
21
- addedVaults,
22
- existingVaults,
23
- addedDotenvFilenames,
24
- envFile
25
- } = main.vaultEncrypt(directory, options.envFile)
26
-
27
- logger.verbose(`generating .env.keys from ${envFile}`)
28
- if (addedKeys.length > 0) {
29
- logger.verbose(`generated ${addedKeys}`)
30
- }
31
- if (existingKeys.length > 0) {
32
- logger.verbose(`existing ${existingKeys}`)
33
- }
34
- fs.writeFileSync(path.resolve(directory, '.env.keys'), dotenvKeysFile)
35
-
36
- logger.verbose(`generating .env.vault from ${envFile}`)
37
- if (addedVaults.length > 0) {
38
- logger.verbose(`encrypting ${addedVaults}`)
39
- }
40
- if (existingVaults.length > 0) {
41
- logger.verbose(`existing ${existingVaults}`)
42
- }
43
- fs.writeFileSync(path.resolve(directory, '.env.vault'), dotenvVaultFile)
44
-
45
- if (addedDotenvFilenames.length > 0) {
46
- logger.success(`encrypted to .env.vault (${addedDotenvFilenames})`)
47
- logger.help2('ℹ commit .env.vault to code: [git commit -am ".env.vault"]')
48
- } else {
49
- logger.blank(`no changes (${envFile})`)
50
- }
51
-
52
- if (addedKeys.length > 0) {
53
- logger.success(`${pluralize('key', addedKeys.length)} added to .env.keys (${addedKeys})`)
54
- }
55
-
56
- if (addedVaults.length > 0) {
57
- const DOTENV_VAULT_X = addedVaults[addedVaults.length - 1]
58
- const DOTENV_KEY_X = DOTENV_VAULT_X.replace('_VAULT_', '_KEY_')
59
- const tryKey = dotenvKeys[DOTENV_KEY_X]
60
-
61
- logger.help2(`ℹ run [DOTENV_KEY='${tryKey}' dotenvx run -- yourcommand] to test decryption locally`)
62
- }
63
- } catch (error) {
64
- logger.error(error.message)
65
- if (error.help) {
66
- logger.help(error.help)
67
- }
68
- if (error.debug) {
69
- logger.debug(error.debug)
70
- }
71
- if (error.code) {
72
- logger.debug(`ERROR_CODE: ${error.code}`)
73
- }
74
- process.exit(1)
75
- }
76
- }
77
-
78
- module.exports = encrypt
@@ -1,30 +0,0 @@
1
- const { logger } = require('./../../../../shared/logger')
2
-
3
- function migrate () {
4
- const options = this.opts()
5
- logger.debug(`options: ${JSON.stringify(options)}`)
6
-
7
- logger.help2('To migrate your .env.vault file to encrypted .env file(s):')
8
- logger.help('')
9
- logger.help(' 1. Run [dotenvx ext vault decrypt]')
10
- logger.help(' 2. Run [ls -a .env*]')
11
- logger.help('')
12
- logger.help2('Lastly, encrypt each .env(.environment) file:')
13
- logger.help('')
14
- logger.help(' 3. Run [dotenvx encrypt -f .env.production]')
15
- logger.help2('')
16
- logger.help2('For example:')
17
- logger.help2('')
18
- logger.help2(' $ dotenvx encrypt -f .env')
19
- logger.help2(' $ dotenvx encrypt -f .env.ci')
20
- logger.help2(' $ dotenvx encrypt -f .env.production')
21
- logger.help2('')
22
- logger.help2('Afterward:')
23
- logger.help2('')
24
- logger.help2('Update production with your new DOTENV_PRIVATE_KEY_PRODUCTION located in .env.keys')
25
- logger.help2('')
26
- logger.success('Learn more at [https://dotenvx.com/docs/quickstart#add-encryption]')
27
- logger.help2('')
28
- }
29
-
30
- module.exports = migrate
@@ -1,78 +0,0 @@
1
- const { logger } = require('./../../../../shared/logger')
2
-
3
- const main = require('./../../../../lib/main')
4
-
5
- function status (directory) {
6
- // debug args
7
- logger.debug(`directory: ${directory}`)
8
-
9
- const options = this.opts()
10
- logger.debug(`options: ${JSON.stringify(options)}`)
11
-
12
- try {
13
- const { changes, nochanges, untracked } = main.status(directory)
14
-
15
- const changeFilenames = []
16
- const nochangeFilenames = []
17
- const untrackedFilenames = []
18
-
19
- for (const row of nochanges) {
20
- nochangeFilenames.push(row.filename)
21
- }
22
-
23
- if (nochangeFilenames.length > 0) {
24
- logger.blank(`no changes (${nochangeFilenames.join(', ')})`)
25
- }
26
-
27
- for (const row of changes) {
28
- changeFilenames.push(row.filename)
29
- }
30
-
31
- if (changeFilenames.length > 0) {
32
- logger.warn(`changes (${changeFilenames.join(', ')})`)
33
- }
34
-
35
- for (const row of changes) {
36
- logger.blank('')
37
- const padding = ' '
38
- logger.blank(`${padding}\`\`\`${row.filename}`)
39
- const paddedResult = row.coloredDiff.split('\n').map(line => padding + line).join('\n')
40
- console.log(paddedResult)
41
- logger.blank(`${padding}\`\`\``)
42
- }
43
-
44
- if (changeFilenames.length > 0) {
45
- logger.blank('')
46
- const optionalDirectory = directory === '.' ? '' : ` ${directory}`
47
- logger.blank(`run [dotenvx encrypt${optionalDirectory}] to apply changes to .env.vault`)
48
- }
49
-
50
- if (nochangeFilenames.length < 1 && changeFilenames.length < 1) {
51
- logger.warn('no .env* files.')
52
- logger.help('? add one with [echo "HELLO=World" > .env] and then run [dotenvx status]')
53
- }
54
-
55
- for (const row of untracked) {
56
- untrackedFilenames.push(row.filename)
57
- }
58
-
59
- if (untrackedFilenames.length > 0) {
60
- logger.warn(`untracked (${untrackedFilenames.join(', ')})`)
61
- logger.help(`? track them with [dotenvx ext vault encrypt ${directory}]`)
62
- }
63
- } catch (error) {
64
- logger.error(error.message)
65
- if (error.help) {
66
- logger.help(error.help)
67
- }
68
- if (error.debug) {
69
- logger.debug(error.debug)
70
- }
71
- if (error.code) {
72
- logger.debug(`ERROR_CODE: ${error.code}`)
73
- }
74
- process.exit(1)
75
- }
76
- }
77
-
78
- module.exports = status
@@ -1,36 +0,0 @@
1
- const { Command } = require('commander')
2
-
3
- const examples = require('./../../examples')
4
-
5
- const vault = new Command('vault')
6
-
7
- vault
8
- .description('🔐 manage .env.vault files')
9
-
10
- // dotenvx ext vault migrate
11
- vault.command('migrate')
12
- .description('instructions for migrating .env.vault to encrypted env file(s)')
13
- .action(require('./../../actions/ext/vault/migrate'))
14
-
15
- // dotenvx ext vault encrypt
16
- vault.command('encrypt')
17
- .description('encrypt .env.* to .env.vault')
18
- .addHelpText('after', examples.vaultEncrypt)
19
- .argument('[directory]', 'directory to encrypt', '.')
20
- .option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
21
- .action(require('./../../actions/ext/vault/encrypt'))
22
-
23
- // dotenvx ext vault decrypt
24
- vault.command('decrypt')
25
- .description('decrypt .env.vault to .env*')
26
- .argument('[directory]', 'directory to decrypt', '.')
27
- .option('-e, --environment <environments...>', 'environment(s) to decrypt')
28
- .action(require('./../../actions/ext/vault/decrypt'))
29
-
30
- // dotenvx ext vault status
31
- vault.command('status')
32
- .description('compare your .env* content(s) to your .env.vault decrypted content(s)')
33
- .argument('[directory]', 'directory to check status against', '.')
34
- .action(require('./../../actions/ext/vault/status'))
35
-
36
- module.exports = vault
@@ -1,10 +0,0 @@
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
@@ -1,7 +0,0 @@
1
- const path = require('path')
2
-
3
- function containsDirectory (filepath) {
4
- return filepath.includes(path.sep)
5
- }
6
-
7
- module.exports = containsDirectory
@@ -1,57 +0,0 @@
1
- const crypto = require('crypto')
2
-
3
- const guessEnvironment = require('./guessEnvironment')
4
-
5
- class DotenvKeys {
6
- constructor (envFilepaths = [], dotenvKeys = {}) {
7
- this.envFilepaths = envFilepaths // pass .env* filepaths to be encrypted
8
- this.dotenvKeys = dotenvKeys // pass current parsed dotenv keys from .env.keys file
9
- }
10
-
11
- run () {
12
- const addedKeys = new Set()
13
- const existingKeys = new Set()
14
-
15
- for (const filepath of this.envFilepaths) {
16
- const environment = guessEnvironment(filepath)
17
- const key = `DOTENV_KEY_${environment.toUpperCase()}`
18
-
19
- let value = this.dotenvKeys[key]
20
-
21
- // first time seeing new DOTENV_KEY_${environment}
22
- if (!value || value.length === 0) {
23
- value = this._generateDotenvKey(environment)
24
-
25
- this.dotenvKeys[key] = value
26
-
27
- addedKeys.add(key) // for info logging to user
28
- } else {
29
- existingKeys.add(key) // for info logging to user
30
- }
31
- }
32
-
33
- let keysData = `#/!!!!!!!!!!!!!!!!!!!.env.keys!!!!!!!!!!!!!!!!!!!!!!/
34
- #/ DOTENV_KEYs. DO NOT commit to source control /
35
- #/ [how it works](https://dotenvx.com/env-keys) /
36
- #/--------------------------------------------------/\n`
37
-
38
- for (const [key, value] of Object.entries(this.dotenvKeys)) {
39
- keysData += `${key}="${value}"\n`
40
- }
41
-
42
- return {
43
- dotenvKeys: this.dotenvKeys,
44
- dotenvKeysFile: keysData,
45
- addedKeys: [...addedKeys], // return set as array
46
- existingKeys: [...existingKeys] // return set as array
47
- }
48
- }
49
-
50
- _generateDotenvKey (environment) {
51
- const rand = crypto.randomBytes(32).toString('hex')
52
-
53
- return `dotenv://:key_${rand}@dotenvx.com/vault/.env.vault?environment=${environment.toLowerCase()}`
54
- }
55
- }
56
-
57
- module.exports = DotenvKeys
@@ -1,60 +0,0 @@
1
- const path = require('path')
2
-
3
- const encrypt = require('./encrypt')
4
- const changed = require('./changed')
5
- const guessEnvironment = require('./guessEnvironment')
6
- const removePersonal = require('./removePersonal')
7
-
8
- class DotenvVault {
9
- constructor (dotenvFiles = {}, dotenvKeys = {}, dotenvVaults = {}) {
10
- this.dotenvFiles = dotenvFiles // key: filepath and value: filecontent
11
- this.dotenvKeys = dotenvKeys // pass current parsed dotenv keys from .env.keys
12
- this.dotenvVaults = dotenvVaults // pass current parsed dotenv vaults from .env.vault
13
- }
14
-
15
- run () {
16
- const addedVaults = new Set()
17
- const existingVaults = new Set()
18
- const addedDotenvFilenames = new Set()
19
-
20
- for (const [filepath, raw] of Object.entries(this.dotenvFiles)) {
21
- const environment = guessEnvironment(filepath)
22
- const vault = `DOTENV_VAULT_${environment.toUpperCase()}`
23
-
24
- let ciphertext = this.dotenvVaults[vault]
25
- const dotenvKey = this.dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
26
-
27
- const cleanRaw = removePersonal(raw)
28
-
29
- if (!ciphertext || ciphertext.length === 0 || changed(ciphertext, cleanRaw, dotenvKey)) {
30
- ciphertext = encrypt(cleanRaw, dotenvKey)
31
- this.dotenvVaults[vault] = ciphertext
32
- addedVaults.add(vault) // for info logging to user
33
-
34
- addedDotenvFilenames.add(path.basename(filepath)) // for info logging to user
35
- } else {
36
- existingVaults.add(vault) // for info logging to user
37
- }
38
- }
39
-
40
- let vaultData = `#/-------------------.env.vault---------------------/
41
- #/ cloud-agnostic vaulting standard /
42
- #/ [how it works](https://dotenvx.com/env-vault) /
43
- #/--------------------------------------------------/\n\n`
44
-
45
- for (const [vault, value] of Object.entries(this.dotenvVaults)) {
46
- const environment = vault.replace('DOTENV_VAULT_', '').toLowerCase()
47
- vaultData += `# ${environment}\n`
48
- vaultData += `${vault}="${value}"\n\n`
49
- }
50
-
51
- return {
52
- dotenvVaultFile: vaultData,
53
- addedVaults: [...addedVaults], // return set as array
54
- existingVaults: [...existingVaults], // return set as array
55
- addedDotenvFilenames: [...addedDotenvFilenames] // return set as array
56
- }
57
- }
58
- }
59
-
60
- module.exports = DotenvVault
@@ -1,9 +0,0 @@
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
@@ -1,9 +0,0 @@
1
- const SPLIT_REGEX = /#\s*personal\.dotenv\s*/i
2
-
3
- function removePersonal (raw) {
4
- const parts = raw.split(SPLIT_REGEX)
5
-
6
- return parts[0]
7
- }
8
-
9
- module.exports = removePersonal
@@ -1,114 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
- const diff = require('diff')
4
-
5
- const Ls = require('./ls')
6
- const VaultDecrypt = require('./vaultDecrypt')
7
-
8
- const containsDirectory = require('./../helpers/containsDirectory')
9
- const guessEnvironment = require('./../helpers/guessEnvironment')
10
- const { getColor } = require('./../../shared/colors')
11
-
12
- const ENCODING = 'utf8'
13
-
14
- class Status {
15
- constructor (directory = '.') {
16
- this.directory = directory
17
- this.changes = []
18
- this.nochanges = []
19
- this.untracked = [] // not tracked in .env.vault
20
- }
21
-
22
- run () {
23
- // get list of .env files
24
- const files = new Ls(this.directory).run()
25
- // iterate over each one
26
- for (const filename of files) {
27
- // skip file if directory
28
- if (containsDirectory(filename)) {
29
- continue
30
- }
31
-
32
- // skip file if .env.keys
33
- if (filename.endsWith('.env.keys')) {
34
- continue
35
- }
36
-
37
- // skip file if .env.vault
38
- if (filename.endsWith('.env.vault')) {
39
- continue
40
- }
41
-
42
- // skip file if .env.example
43
- if (filename.endsWith('.env.example')) {
44
- continue
45
- }
46
-
47
- // skip file if *.previous
48
- if (filename.endsWith('.previous')) {
49
- continue
50
- }
51
-
52
- const filepath = path.resolve(this.directory, filename)
53
-
54
- const row = {}
55
- row.filename = filename
56
- row.filepath = filepath
57
- row.environment = guessEnvironment(filepath)
58
-
59
- // grab raw
60
- row.raw = fs.readFileSync(filepath, { encoding: ENCODING })
61
-
62
- // grab decrypted
63
- const { processedEnvs } = new VaultDecrypt(this.directory, row.environment).run()
64
- const result = processedEnvs[0]
65
-
66
- // handle warnings
67
- row.decrypted = result.decrypted
68
- if (result.warning) {
69
- this.untracked.push(row)
70
- continue
71
- }
72
-
73
- // differences
74
- row.differences = diff.diffWords(row.decrypted, row.raw)
75
-
76
- // any changes?
77
- const hasChanges = this._hasChanges(row.differences)
78
-
79
- if (hasChanges) {
80
- row.coloredDiff = row.differences.map(this._colorizeDiff).join('')
81
- this.changes.push(row)
82
- } else {
83
- this.nochanges.push(row)
84
- }
85
- }
86
-
87
- return {
88
- changes: this.changes,
89
- nochanges: this.nochanges,
90
- untracked: this.untracked
91
- }
92
- }
93
-
94
- _colorizeDiff (part) {
95
- // If the part was added, color it green
96
- if (part.added) {
97
- return getColor('green')(part.value)
98
- }
99
-
100
- // If the part was removed, color it red
101
- if (part.removed) {
102
- return getColor('red')(part.value)
103
- }
104
-
105
- // No color for unchanged parts
106
- return part.value
107
- }
108
-
109
- _hasChanges (differences) {
110
- return differences.some(part => part.added || part.removed)
111
- }
112
- }
113
-
114
- module.exports = Status
@@ -1,126 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
- const dotenv = require('dotenv')
4
-
5
- const ENCODING = 'utf8'
6
-
7
- const libDecrypt = require('./../../lib/helpers/decrypt')
8
-
9
- class VaultDecrypt {
10
- constructor (directory = '.', environment) {
11
- this.directory = directory
12
- this.environment = environment
13
-
14
- this.envKeysFilepath = path.resolve(this.directory, '.env.keys')
15
- this.envVaultFilepath = path.resolve(this.directory, '.env.vault')
16
-
17
- this.processedEnvs = []
18
- this.changedFilenames = new Set()
19
- this.unchangedFilenames = new Set()
20
- }
21
-
22
- run () {
23
- if (!fs.existsSync(this.envVaultFilepath)) {
24
- const code = 'MISSING_ENV_VAULT_FILE'
25
- const message = `missing .env.vault (${this.envVaultFilepath})`
26
- const help = `? generate one with [dotenvx ext vault encrypt ${this.directory}]`
27
-
28
- const error = new Error(message)
29
- error.code = code
30
- error.help = help
31
- throw error
32
- }
33
-
34
- if (!fs.existsSync(this.envKeysFilepath)) {
35
- const code = 'MISSING_ENV_KEYS_FILE'
36
- const message = `missing .env.keys (${this.envKeysFilepath})`
37
- const help = '? a .env.keys file must be present in order to decrypt your .env.vault contents to .env file(s)'
38
-
39
- const error = new Error(message)
40
- error.code = code
41
- error.help = help
42
- throw error
43
- }
44
-
45
- const dotenvKeys = dotenv.configDotenv({ path: this.envKeysFilepath }).parsed
46
- const dotenvVault = dotenv.configDotenv({ path: this.envVaultFilepath }).parsed
47
- const environments = this._environments()
48
-
49
- if (environments.length > 0) {
50
- // iterate over the environments
51
- for (const environment of environments) {
52
- const value = dotenvKeys[`DOTENV_KEY_${environment.toUpperCase()}`]
53
- const row = this._processRow(value, dotenvVault, environment)
54
-
55
- this.processedEnvs.push(row)
56
- }
57
- } else {
58
- for (const [dotenvKey, value] of Object.entries(dotenvKeys)) {
59
- const environment = dotenvKey.replace('DOTENV_KEY_', '').toLowerCase()
60
- const row = this._processRow(value, dotenvVault, environment)
61
-
62
- this.processedEnvs.push(row)
63
- }
64
- }
65
-
66
- return {
67
- processedEnvs: this.processedEnvs,
68
- changedFilenames: [...this.changedFilenames],
69
- unchangedFilenames: [...this.unchangedFilenames]
70
- }
71
- }
72
-
73
- _processRow (value, dotenvVault, environment) {
74
- environment = environment.toLowerCase() // important so we don't later write .env.DEVELOPMENT for example
75
- const vaultKey = `DOTENV_VAULT_${environment.toUpperCase()}`
76
- const ciphertext = dotenvVault[vaultKey] // attempt to find ciphertext
77
-
78
- const row = {}
79
- row.environment = environment
80
- row.dotenvKey = value ? value.trim() : value
81
- row.ciphertext = ciphertext
82
-
83
- if (ciphertext && ciphertext.length >= 1) {
84
- // Decrypt
85
- const decrypted = libDecrypt(ciphertext, value.trim())
86
- row.decrypted = decrypted
87
-
88
- // envFilename
89
- // replace _ with . to support filenames like .env.development.local
90
- let envFilename = `.env.${environment.replace('_', '.')}`
91
- if (environment === 'development') {
92
- envFilename = '.env'
93
- }
94
- row.filename = envFilename
95
- row.filepath = path.resolve(this.directory, envFilename)
96
-
97
- // check if exists and is changing
98
- if (fs.existsSync(row.filepath) && (fs.readFileSync(row.filepath, { encoding: ENCODING }).toString() === decrypted)) {
99
- this.unchangedFilenames.add(envFilename)
100
- } else {
101
- row.shouldWrite = true
102
- this.changedFilenames.add(envFilename)
103
- }
104
- } else {
105
- const message = `${vaultKey} missing in .env.vault: ${this.envVaultFilepath}`
106
- const warning = new Error(message)
107
- row.warning = warning
108
- }
109
-
110
- return row
111
- }
112
-
113
- _environments () {
114
- if (this.environment === undefined) {
115
- return []
116
- }
117
-
118
- if (!Array.isArray(this.environment)) {
119
- return [this.environment]
120
- }
121
-
122
- return this.environment
123
- }
124
- }
125
-
126
- module.exports = VaultDecrypt
@@ -1,118 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
- const dotenv = require('dotenv')
4
-
5
- const DotenvKeys = require('./../helpers/dotenvKeys')
6
- const DotenvVault = require('./../helpers/dotenvVault')
7
-
8
- const ENCODING = 'utf8'
9
-
10
- const findEnvFiles = require('../helpers/findEnvFiles')
11
-
12
- class VaultEncrypt {
13
- constructor (directory = '.', envFile) {
14
- this.directory = directory
15
- this.envFile = envFile || findEnvFiles(directory)
16
- // calculated
17
- this.envKeysFilepath = path.resolve(this.directory, '.env.keys')
18
- this.envVaultFilepath = path.resolve(this.directory, '.env.vault')
19
- }
20
-
21
- run () {
22
- if (this.envFile.length < 1) {
23
- const code = 'MISSING_ENV_FILES'
24
- const message = 'no .env* files found'
25
- const help = '? add one with [echo "HELLO=World" > .env] and then run [dotenvx ext vault encrypt]'
26
-
27
- const error = new Error(message)
28
- error.code = code
29
- error.help = help
30
- throw error
31
- }
32
-
33
- const parsedDotenvKeys = this._parsedDotenvKeys()
34
- const parsedDotenvVaults = this._parsedDotenvVault()
35
- const envFilepaths = this._envFilepaths()
36
-
37
- // build filepaths to be passed to DotenvKeys
38
- const uniqueEnvFilepaths = new Set()
39
- for (const envFilepath of envFilepaths) {
40
- const filepath = path.resolve(this.directory, envFilepath)
41
- if (!fs.existsSync(filepath)) {
42
- const code = 'MISSING_ENV_FILE'
43
- const message = `file does not exist at [${filepath}]`
44
- const help = `? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx ext vault encrypt]`
45
-
46
- const error = new Error(message)
47
- error.code = code
48
- error.help = help
49
- throw error
50
- }
51
-
52
- uniqueEnvFilepaths.add(filepath)
53
- }
54
-
55
- // generate .env.keys string
56
- const {
57
- dotenvKeys,
58
- dotenvKeysFile,
59
- addedKeys,
60
- existingKeys
61
- } = new DotenvKeys([...uniqueEnvFilepaths], parsedDotenvKeys).run()
62
-
63
- // build look up of .env filepaths and their raw content
64
- const dotenvFiles = {}
65
- for (const filepath of [...uniqueEnvFilepaths]) {
66
- const raw = fs.readFileSync(filepath, ENCODING)
67
- dotenvFiles[filepath] = raw
68
- }
69
-
70
- // generate .env.vault string
71
- const {
72
- dotenvVaultFile,
73
- addedVaults,
74
- existingVaults,
75
- addedDotenvFilenames
76
- } = new DotenvVault(dotenvFiles, dotenvKeys, parsedDotenvVaults).run()
77
-
78
- return {
79
- // from DotenvKeys
80
- dotenvKeys,
81
- dotenvKeysFile,
82
- addedKeys,
83
- existingKeys,
84
- // from DotenvVault
85
- dotenvVaultFile,
86
- addedVaults,
87
- existingVaults,
88
- addedDotenvFilenames,
89
- envFile: this.envFile
90
- }
91
- }
92
-
93
- _envFilepaths () {
94
- if (!Array.isArray(this.envFile)) {
95
- return [this.envFile]
96
- }
97
-
98
- return this.envFile
99
- }
100
-
101
- _parsedDotenvKeys () {
102
- const options = {
103
- path: this.envKeysFilepath
104
- }
105
-
106
- return dotenv.configDotenv(options).parsed
107
- }
108
-
109
- _parsedDotenvVault () {
110
- const options = {
111
- path: this.envVaultFilepath
112
- }
113
-
114
- return dotenv.configDotenv(options).parsed
115
- }
116
- }
117
-
118
- module.exports = VaultEncrypt