@dotenvx/dotenvx 0.39.0 → 0.41.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
@@ -624,6 +624,16 @@ More examples
624
624
  DATABASE_URL postgres://yourusername@localhost/my_database
625
625
  ```
626
626
 
627
+ </details>
628
+ * <details><summary>`run` - Shell Expansion</summary><br>
629
+
630
+ Prevent your shell from expanding inline `$VARIABLES` before dotenvx has a chance to inject it. Use a subshell.
631
+
632
+ ```sh
633
+ $ dotenvx run --env="HELLO=World" -- sh -c 'echo Hello $HELLO'
634
+ Hello World
635
+ ```
636
+
627
637
  </details>
628
638
  * <details><summary>`run` - multiple `-f` flags</summary><br>
629
639
 
@@ -668,6 +678,82 @@ More examples
668
678
  Hello World
669
679
  ```
670
680
 
681
+ </details>
682
+ * <details><summary>`DOTENV_PRIVATE_KEY=key run`</summary><br>
683
+
684
+ Decrypt your encrypted `.env` by setting `DOTENV_PRIVATE_KEY` before `dotenvx run`.
685
+
686
+ ```sh
687
+ $ touch .env
688
+ $ dotenvx set HELLO encrypted --encrypt
689
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
690
+
691
+ # check your .env.keys files for your privateKey
692
+ $ DOTENV_PRIVATE_KEY="122...0b8" dotenvx run -- node index.js
693
+ [dotenvx] injecting env (2) from .env
694
+ Hello encrypted
695
+ ```
696
+
697
+ </details>
698
+ * <details><summary>`DOTENV_PRIVATE_KEY_PRODUCTION=key run`</summary><br>
699
+
700
+ Decrypt your encrypted `.env.production` by setting `DOTENV_PRIVATE_KEY_PRODUCTION` before `dotenvx run`. Alternatively, this can be already set on your server or cloud provider.
701
+
702
+ ```sh
703
+ $ touch .env.production
704
+ $ dotenvx set HELLO "production encrypted" -f .env.production --encrypt
705
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
706
+
707
+ # check .env.keys for your privateKey
708
+ $ DOTENV_PRIVATE_KEY_PRODUCTION="122...0b8" dotenvx run -- node index.js
709
+ [dotenvx] injecting env (2) from .env.production
710
+ Hello production encrypted
711
+ ```
712
+
713
+ Note the `DOTENV_PRIVATE_KEY_PRODUCTION` ends with `_PRODUCTION`. This instructs dotenvx run to load the `.env.production` file.
714
+
715
+ </details>
716
+ * <details><summary>`DOTENV_PRIVATE_KEY_CI=key dotenvx run`</summary><br>
717
+
718
+ Decrypt your encrypted `.env.ci` by setting `DOTENV_PRIVATE_KEY_CI` before `dotenvx run`. Alternatively, this can be already set on your server or cloud provider.
719
+
720
+ ```sh
721
+ $ touch .env.ci
722
+ $ dotenvx set HELLO "ci encrypted" -f .env.production --encrypt
723
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
724
+
725
+ # check .env.keys for your privateKey
726
+ $ DOTENV_PRIVATE_KEY_CI="122...0b8" dotenvx run -- node index.js
727
+ [dotenvx] injecting env (2) from .env.ci
728
+ Hello ci encrypted
729
+ ```
730
+
731
+ Note the `DOTENV_PRIVATE_KEY_CI` ends with `_CI`. This instructs dotenvx run to load the `.env.ci` file. See the pattern?
732
+
733
+ </details>
734
+ * <details><summary>`DOTENV_PRIVATE_KEY=key DOTENV_PRIVATE_KEY_PRODUCTION=key run` - Combine Multiple</summary><br>
735
+
736
+ Decrypt your encrypted `.env` and `.env.production` files by setting `DOTENV_PRIVATE_KEY` and `DOTENV_PRIVATE_KEY_PRODUCTION` before `dotenvx run`.
737
+
738
+ ```sh
739
+ $ touch .env
740
+ $ touch .env.production
741
+ $ dotenvx set HELLO encrypted --encrypt
742
+ $ dotenvx set HELLO "production encrypted" -f .env.production --encrypt
743
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
744
+
745
+ # check .env.keys for your privateKeys
746
+ $ DOTENV_PRIVATE_KEY="122...0b8" DOTENV_PRIVATE_KEY_PRODUCTION="122...0b8" dotenvx run -- node index.js
747
+ [dotenvx] injecting env (3) from .env, .env.production
748
+ Hello encrypted
749
+
750
+ $ DOTENV_PRIVATE_KEY_PRODUCTION="122...0b8" DOTENV_PRIVATE_KEY="122...0b8" dotenvx run -- node index.js
751
+ [dotenvx] injecting env (3) from .env.production, .env
752
+ Hello production encrypted
753
+ ```
754
+
755
+ Compose any encrypted files you want this way. As long as a `DOTENV_PRIVATE_KEY_${environment}` is set, the values from `.env.${environment}` will be decrypted at runtime.
756
+
671
757
  </details>
672
758
  * <details><summary>`run --verbose`</summary><br>
673
759
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.39.0",
2
+ "version": "0.41.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -0,0 +1,73 @@
1
+ const fs = require('fs')
2
+ const logger = require('./../../shared/logger')
3
+
4
+ const main = require('./../../lib/main')
5
+
6
+ const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys')
7
+
8
+ const ENCODING = 'utf8'
9
+
10
+ async function convert () {
11
+ const options = this.opts()
12
+ logger.debug(`options: ${JSON.stringify(options)}`)
13
+
14
+ try {
15
+ const {
16
+ processedEnvFiles,
17
+ changedFilepaths,
18
+ unchangedFilepaths
19
+ } = main.convert(options.envFile)
20
+
21
+ for (const processedEnvFile of processedEnvFiles) {
22
+ logger.verbose(`encrypting ${processedEnvFile.envFilepath} (${processedEnvFile.filepath})`)
23
+ if (processedEnvFile.error) {
24
+ if (processedEnvFile.error.code === 'MISSING_ENV_FILE') {
25
+ logger.warn(processedEnvFile.error)
26
+ logger.help(`? add one with [echo "HELLO=World" > ${processedEnvFile.envFilepath}] and re-run [dotenvx convert]`)
27
+ } else {
28
+ logger.warn(processedEnvFile.error)
29
+ }
30
+ } else if (processedEnvFile.changed) {
31
+ fs.writeFileSync(processedEnvFile.filepath, processedEnvFile.envSrc, ENCODING)
32
+
33
+ logger.verbose(`encrypted ${processedEnvFile.envFilepath} (${processedEnvFile.filepath})`)
34
+ } else {
35
+ logger.verbose(`no changes ${processedEnvFile.envFilepath} (${processedEnvFile.filepath})`)
36
+ }
37
+ }
38
+
39
+ if (changedFilepaths.length > 0) {
40
+ logger.success(`✔ encrypted (${changedFilepaths.join(',')})`)
41
+ } else if (unchangedFilepaths.length > 0) {
42
+ logger.info(`no changes (${unchangedFilepaths})`)
43
+ } else {
44
+ // do nothing - scenario when no .env files found
45
+ }
46
+
47
+ for (const processedEnvFile of processedEnvFiles) {
48
+ if (processedEnvFile.privateKeyAdded) {
49
+ logger.success(`✔ key added to .env.keys (${processedEnvFile.privateKeyName})`)
50
+
51
+ if (!isIgnoringDotenvKeys()) {
52
+ logger.help2('ℹ add .env.keys to .gitignore: [echo ".env.keys" >> .gitignore]')
53
+ }
54
+
55
+ logger.help2(`ℹ run [${processedEnvFile.privateKeyName}='${processedEnvFile.privateKey}' dotenvx run -- yourcommand] to test decryption locally`)
56
+ }
57
+ }
58
+ } catch (error) {
59
+ logger.error(error.message)
60
+ if (error.help) {
61
+ logger.help(error.help)
62
+ }
63
+ if (error.debug) {
64
+ logger.debug(error.debug)
65
+ }
66
+ if (error.code) {
67
+ logger.debug(`ERROR_CODE: ${error.code}`)
68
+ }
69
+ process.exit(1)
70
+ }
71
+ }
72
+
73
+ module.exports = convert
@@ -136,8 +136,11 @@ async function run () {
136
136
 
137
137
  if (processedEnv.error) {
138
138
  if (processedEnv.error.code === 'MISSING_ENV_FILE') {
139
- logger.warnv(processedEnv.error)
140
- logger.help(`? in development: add one with [echo "HELLO=World" > ${processedEnv.filepath}] and re-run [dotenvx run -- ${commandArgs.join(' ')}]`)
139
+ // do not warn for conventions (too noisy)
140
+ if (!options.convention) {
141
+ logger.warnv(processedEnv.error)
142
+ logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.filepath}] and re-run [dotenvx run -- ${commandArgs.join(' ')}]`)
143
+ }
141
144
  } else {
142
145
  logger.warnv(processedEnv.error)
143
146
  }
@@ -1,7 +1,12 @@
1
+ const fs = require('fs')
1
2
  const logger = require('./../../shared/logger')
2
3
 
3
4
  const main = require('./../../lib/main')
4
5
 
6
+ const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys')
7
+
8
+ const ENCODING = 'utf8'
9
+
5
10
  function set (key, value) {
6
11
  logger.debug(`key: ${key}`)
7
12
  logger.debug(`value: ${value}`)
@@ -12,36 +17,65 @@ function set (key, value) {
12
17
  try {
13
18
  const {
14
19
  processedEnvFiles,
15
- settableFilepaths
20
+ changedFilepaths,
21
+ unchangedFilepaths
16
22
  } = main.set(key, value, options.envFile, options.encrypt)
17
23
 
18
- let atLeastOneSuccess = false
24
+ let withEncryption = ''
25
+
26
+ if (options.encrypt) {
27
+ withEncryption = ' with encryption'
28
+ }
19
29
 
20
30
  for (const processedEnvFile of processedEnvFiles) {
21
- logger.verbose(`setting for ${processedEnvFile.filepath}`)
31
+ logger.verbose(`setting for ${processedEnvFile.envFilepath}`)
22
32
 
23
33
  if (processedEnvFile.error) {
24
34
  if (processedEnvFile.error.code === 'MISSING_ENV_FILE') {
25
35
  logger.warn(processedEnvFile.error)
26
- logger.help(`? add one with [echo "HELLO=World" > ${processedEnvFile.filepath}] and re-run [dotenvx set]`)
36
+ logger.help(`? add one with [echo "HELLO=World" > ${processedEnvFile.envFilepath}] and re-run [dotenvx set]`)
27
37
  } else {
28
38
  logger.warn(processedEnvFile.error)
29
39
  }
30
40
  } else {
31
- atLeastOneSuccess = true
32
- logger.verbose(`${processedEnvFile.key} set`)
33
- logger.debug(`${processedEnvFile.key} set to ${processedEnvFile.value}`)
41
+ fs.writeFileSync(processedEnvFile.filepath, processedEnvFile.envSrc, ENCODING)
42
+
43
+ logger.verbose(`${processedEnvFile.key} set${withEncryption} (${processedEnvFile.envFilepath})`)
44
+ logger.debug(`${processedEnvFile.key} set${withEncryption} to ${processedEnvFile.value} (${processedEnvFile.envFilepath})`)
34
45
  }
35
46
  }
36
47
 
37
- if (atLeastOneSuccess) {
38
- logger.success(`set ${key} (${settableFilepaths.join(', ')})`)
48
+ if (changedFilepaths.length > 0) {
49
+ logger.success(`✔ set ${key}${withEncryption} (${changedFilepaths.join(',')})`)
50
+ } else if (unchangedFilepaths.length > 0) {
51
+ logger.info(`no changes (${unchangedFilepaths})`)
52
+ } else {
53
+ // do nothing
54
+ }
55
+
56
+ for (const processedEnvFile of processedEnvFiles) {
57
+ if (processedEnvFile.privateKeyAdded) {
58
+ logger.success(`✔ key added to .env.keys (${processedEnvFile.privateKeyName})`)
59
+
60
+ if (!isIgnoringDotenvKeys()) {
61
+ logger.help2('ℹ add .env.keys to .gitignore: [echo ".env.keys" >> .gitignore]')
62
+ }
63
+
64
+ logger.help2(`ℹ run [${processedEnvFile.privateKeyName}='${processedEnvFile.privateKey}' dotenvx get ${key}] to test decryption locally`)
65
+ }
39
66
  }
40
67
  } catch (error) {
41
68
  logger.error(error.message)
42
69
  if (error.help) {
43
70
  logger.help(error.help)
44
71
  }
72
+ if (error.debug) {
73
+ logger.debug(error.debug)
74
+ }
75
+ if (error.code) {
76
+ logger.debug(`ERROR_CODE: ${error.code}`)
77
+ }
78
+ process.exit(1)
45
79
  }
46
80
  }
47
81
 
@@ -0,0 +1,47 @@
1
+ const logger = require('./../../../shared/logger')
2
+
3
+ function convert (directory) {
4
+ // debug args
5
+ logger.debug(`directory: ${directory}`)
6
+
7
+ const options = this.opts()
8
+ logger.debug(`options: ${JSON.stringify(options)}`)
9
+
10
+ try {
11
+ logger.help2('To convert your .env.vault file to encrypted .env file(s):')
12
+ logger.help('')
13
+ logger.help(' 1. Run [dotenvx vault decrypt]')
14
+ logger.help(' 2. Run [ls -a .env*]')
15
+ logger.help('')
16
+ logger.help2('Lastly, convert each .env(.environment) file:')
17
+ logger.help('')
18
+ logger.help(' 3. Run [dotenvx convert -f .env.production]')
19
+ logger.help2('')
20
+ logger.help2('For example:')
21
+ logger.help2('')
22
+ logger.help2(' $ dotenvx convert -f .env')
23
+ logger.help2(' $ dotenvx convert -f .env.ci')
24
+ logger.help2(' $ dotenvx convert -f .env.production')
25
+ logger.help2('')
26
+ logger.help2('Afterward:')
27
+ logger.help2('')
28
+ logger.help2('Update production with your new DOTENV_PRIVATE_KEY_PRODUCTION located in .env.keys')
29
+ logger.help2('')
30
+ logger.success('Learn more at [https://dotenvx.com/docs/quickstart#add-encryption]')
31
+ logger.help2('')
32
+ } catch (error) {
33
+ logger.error(error.message)
34
+ if (error.help) {
35
+ logger.help(error.help)
36
+ }
37
+ if (error.debug) {
38
+ logger.debug(error.debug)
39
+ }
40
+ if (error.code) {
41
+ logger.debug(`ERROR_CODE: ${error.code}`)
42
+ }
43
+ process.exit(1)
44
+ }
45
+ }
46
+
47
+ module.exports = convert
@@ -58,7 +58,6 @@ async function encrypt (directory) {
58
58
 
59
59
  if (addedKeys.length > 0) {
60
60
  spinner.succeed(`${pluralize('key', addedKeys.length)} added to .env.keys (${addedKeys})`)
61
- logger.help2('ℹ push .env.keys up to hub: [dotenvx hub push]')
62
61
  }
63
62
 
64
63
  if (addedVaults.length > 0) {
@@ -14,7 +14,7 @@ hub
14
14
  .description('authenticate to dotenvx hub')
15
15
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
16
16
  .action(function (...args) {
17
- logger.warn('DEPRECATION NOTECE: [dotenvx hub login] will be removed in 1.0.0 release soon')
17
+ logger.warn('DEPRECATION NOTICE: [dotenvx hub login] will be removed in 1.0.0 release soon')
18
18
 
19
19
  loginAction.apply(this, args)
20
20
  })
@@ -26,7 +26,7 @@ hub
26
26
  .argument('[directory]', 'directory to push', '.')
27
27
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
28
28
  .action(function (...args) {
29
- logger.warn('DEPRECATION NOTECE: [dotenvx hub push] will be removed in 1.0.0 release soon')
29
+ logger.warn('DEPRECATION NOTICE: [dotenvx hub push] will be removed in 1.0.0 release soon')
30
30
 
31
31
  pushAction.apply(this, args)
32
32
  })
@@ -38,7 +38,7 @@ hub
38
38
  .argument('[directory]', 'directory to pull', '.')
39
39
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
40
40
  .action(function (...args) {
41
- logger.warn('DEPRECATION NOTECE: [dotenvx hub pull] will be removed in 1.0.0 release soon')
41
+ logger.warn('DEPRECATION NOTICE: [dotenvx hub pull] will be removed in 1.0.0 release soon')
42
42
 
43
43
  pullAction.apply(this, args)
44
44
  })
@@ -49,7 +49,7 @@ hub
49
49
  .description('view repository on dotenvx hub')
50
50
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
51
51
  .action(function (...args) {
52
- logger.warn('DEPRECATION NOTECE: [dotenvx hub open] will be removed in 1.0.0 release soon')
52
+ logger.warn('DEPRECATION NOTICE: [dotenvx hub open] will be removed in 1.0.0 release soon')
53
53
 
54
54
  openAction.apply(this, args)
55
55
  })
@@ -60,7 +60,7 @@ hub
60
60
  .description('print the auth token dotenvx hub is configured to use')
61
61
  .option('-h, --hostname <url>', 'set hostname', 'https://hub.dotenvx.com')
62
62
  .action(function (...args) {
63
- logger.warn('DEPRECATION NOTECE: [dotenvx hub token] will be removed in 1.0.0 release soon')
63
+ logger.warn('DEPRECATION NOTICE: [dotenvx hub token] will be removed in 1.0.0 release soon')
64
64
 
65
65
  tokenAction.apply(this, args)
66
66
  })
@@ -70,7 +70,7 @@ hub
70
70
  .command('status')
71
71
  .description('display logged in user')
72
72
  .action(function (...args) {
73
- logger.warn('DEPRECATION NOTECE: [dotenvx hub status] will be removed in 1.0.0 release soon')
73
+ logger.warn('DEPRECATION NOTICE: [dotenvx hub status] will be removed in 1.0.0 release soon')
74
74
 
75
75
  statusAction.apply(this, args)
76
76
  })
@@ -81,7 +81,7 @@ hub
81
81
  .description('log out this machine from dotenvx hub')
82
82
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
83
83
  .action(function (...args) {
84
- logger.warn('DEPRECATION NOTECE: [dotenvx hub logout] will be removed in 1.0.0 release soon')
84
+ logger.warn('DEPRECATION NOTICE: [dotenvx hub logout] will be removed in 1.0.0 release soon')
85
85
 
86
86
  logoutAction.apply(this, args)
87
87
  })
@@ -7,6 +7,11 @@ const vault = new Command('vault')
7
7
  vault
8
8
  .description('manage .env.vault files')
9
9
 
10
+ // dotenvx vault convert
11
+ vault.command('convert')
12
+ .description('instructions for converting .env.vault to encrypted env file(s)')
13
+ .action(require('./../actions/vault/convert'))
14
+
10
15
  // dotenvx vault encrypt
11
16
  vault.command('encrypt')
12
17
  .description('encrypt .env.* to .env.vault')
@@ -103,6 +103,12 @@ program.command('set')
103
103
  .option('-c, --encrypt', 'encrypt value')
104
104
  .action(require('./actions/set'))
105
105
 
106
+ // dotenvx convert
107
+ program.command('convert')
108
+ .description('convert env file(s) to encrypted env file(s)')
109
+ .option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
110
+ .action(require('./actions/convert'))
111
+
106
112
  // dotenvx ls
107
113
  program.command('ls')
108
114
  .description('print all .env files in a tree structure')
@@ -160,6 +166,7 @@ program.command('encrypt')
160
166
  .option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
161
167
  .action(function (...args) {
162
168
  logger.warn('DEPRECATION NOTICE: [dotenvx encrypt] has moved. change your command to [dotenvx vault encrypt]')
169
+ logger.warn('DEPRECATION NOTICE: [dotenvx encryptme] will become [dotenvx encrypt] in a 1.0.0 release scheduled for middle of June 2024.')
163
170
 
164
171
  encryptAction.apply(this, args)
165
172
  })
@@ -171,7 +178,7 @@ program.command('decrypt')
171
178
  .argument('[directory]', 'directory to decrypt', '.')
172
179
  .option('-e, --environment <environments...>', 'environment(s) to decrypt')
173
180
  .action(function (...args) {
174
- logger.warn('DEPRECATION NOTECE: [dotenvx decrypt] has moved. change your command to [dotenvx vault decrypt]')
181
+ logger.warn('DEPRECATION NOTICE: [dotenvx decrypt] has moved. change your command to [dotenvx vault decrypt]')
175
182
 
176
183
  decryptAction.apply(this, args)
177
184
  })
@@ -31,7 +31,8 @@ function findOrCreatePublicKey (envFilepath, envKeysFilepath) {
31
31
  envSrc,
32
32
  keysSrc,
33
33
  publicKey: envParsed[publicKeyName],
34
- privateKey: keysParsed[privateKeyName]
34
+ privateKey: keysParsed[privateKeyName],
35
+ privateKeyAdded: false
35
36
  }
36
37
  }
37
38
 
@@ -73,7 +74,8 @@ function findOrCreatePublicKey (envFilepath, envKeysFilepath) {
73
74
  envSrc,
74
75
  keysSrc,
75
76
  publicKey,
76
- privateKey
77
+ privateKey,
78
+ privateKeyAdded: true
77
79
  }
78
80
  }
79
81
 
@@ -0,0 +1,8 @@
1
+ const ENCRYPTION_PATTERN = /^encrypted:.+/
2
+ const PUBLIC_KEY_PATTERN = /^DOTENV_PUBLIC_KEY/
3
+
4
+ function isEncrypted (key, value) {
5
+ return PUBLIC_KEY_PATTERN.test(key) || ENCRYPTION_PATTERN.test(value)
6
+ }
7
+
8
+ module.exports = isEncrypted
@@ -0,0 +1,18 @@
1
+ const dotenv = require('dotenv')
2
+
3
+ const isEncrypted = require('./isEncrypted')
4
+
5
+ function isFullyEncrypted (src) {
6
+ const parsed = dotenv.parse(src)
7
+
8
+ for (const [key, value] of Object.entries(parsed)) {
9
+ const result = isEncrypted(key, value)
10
+ if (!result) {
11
+ return false
12
+ }
13
+ }
14
+
15
+ return true
16
+ }
17
+
18
+ module.exports = isFullyEncrypted
@@ -0,0 +1,19 @@
1
+ const fs = require('fs')
2
+ const ignore = require('ignore')
3
+
4
+ function isIgnoringDotenvKeys () {
5
+ if (!fs.existsSync('.gitignore')) {
6
+ return false
7
+ }
8
+
9
+ const gitignore = fs.readFileSync('.gitignore').toString()
10
+ const ig = ignore(gitignore).add(gitignore)
11
+
12
+ if (!ig.ignores('.env.keys')) {
13
+ return false
14
+ }
15
+
16
+ return false
17
+ }
18
+
19
+ module.exports = isIgnoringDotenvKeys
@@ -7,7 +7,7 @@ function replace (src, key, value) {
7
7
  const parsed = dotenv.parse(src)
8
8
  if (Object.prototype.hasOwnProperty.call(parsed, key)) {
9
9
  // replace
10
- const regex = new RegExp(`^${key}=.*$`, 'm') // Regular expression to find the key and replace its value
10
+ const regex = new RegExp(`^${key}=(?:(["'\`])[^\\1]*\\1|[^\\n]*)(\\n[^A-Z0-9_].*)*`, 'm')
11
11
  output = src.replace(regex, formatted)
12
12
  } else {
13
13
  // append
package/src/lib/main.js CHANGED
@@ -3,13 +3,14 @@ const dotenv = require('dotenv')
3
3
  const dotenvExpand = require('dotenv-expand')
4
4
 
5
5
  // services
6
- const Encrypt = require('./services/encrypt')
7
6
  const Ls = require('./services/ls')
8
7
  const Get = require('./services/get')
9
8
  const Sets = require('./services/sets')
10
9
  const Status = require('./services/status')
10
+ const Encrypt = require('./services/encrypt')
11
11
  const Genexample = require('./services/genexample')
12
12
  const Settings = require('./services/settings')
13
+ const VaultEncrypt = require('./services/vaultEncrypt')
13
14
 
14
15
  // helpers
15
16
  const dotenvEval = require('./helpers/dotenvEval')
@@ -41,9 +42,13 @@ const parse = function (src) {
41
42
  return dotenv.parse(src)
42
43
  }
43
44
 
44
- // actions related
45
+ // DEPRECATED: will became the same function as convert
45
46
  const encrypt = function (directory, envFile) {
46
- return new Encrypt(directory, envFile).run()
47
+ return new VaultEncrypt(directory, envFile).run()
48
+ }
49
+
50
+ const vaultEncrypt = function (directory, envFile) {
51
+ return new VaultEncrypt(directory, envFile).run()
47
52
  }
48
53
 
49
54
  const ls = function (directory, envFile) {
@@ -62,6 +67,10 @@ const set = function (key, value, envFile, encrypt) {
62
67
  return new Sets(key, value, envFile, encrypt).run()
63
68
  }
64
69
 
70
+ const convert = function (envFile) {
71
+ return new Encrypt(envFile).run()
72
+ }
73
+
65
74
  const status = function (directory) {
66
75
  return new Status(directory).run()
67
76
  }
@@ -96,9 +105,11 @@ module.exports = {
96
105
  parse,
97
106
  // actions related
98
107
  encrypt,
108
+ vaultEncrypt,
99
109
  ls,
100
110
  get,
101
111
  set,
112
+ convert,
102
113
  status,
103
114
  genexample,
104
115
  // settings
@@ -2,91 +2,93 @@ const fs = require('fs')
2
2
  const path = require('path')
3
3
  const dotenv = require('dotenv')
4
4
 
5
- const DotenvKeys = require('./../helpers/dotenvKeys')
6
- const DotenvVault = require('./../helpers/dotenvVault')
5
+ const findOrCreatePublicKey = require('./../helpers/findOrCreatePublicKey')
6
+ const guessPrivateKeyName = require('./../helpers/guessPrivateKeyName')
7
+ const encryptValue = require('./../helpers/encryptValue')
8
+ const isEncrypted = require('./../helpers/isEncrypted')
9
+ const replace = require('./../helpers/replace')
7
10
 
8
11
  const ENCODING = 'utf8'
9
12
 
10
- const findEnvFiles = require('../helpers/findEnvFiles')
11
-
12
13
  class Encrypt {
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')
14
+ constructor (envFile = '.env') {
15
+ this.envFile = envFile
16
+ this.processedEnvFiles = []
17
+ this.changedFilepaths = new Set()
18
+ this.unchangedFilepaths = new Set()
19
19
  }
20
20
 
21
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 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
22
  const envFilepaths = this._envFilepaths()
36
-
37
- // build filepaths to be passed to DotenvKeys
38
- const uniqueEnvFilepaths = new Set()
39
23
  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 encrypt]`
45
-
46
- const error = new Error(message)
47
- error.code = code
48
- error.help = help
49
- throw error
24
+ const filepath = path.resolve(envFilepath)
25
+
26
+ const row = {}
27
+ row.keys = []
28
+ row.filepath = filepath
29
+ row.envFilepath = envFilepath
30
+
31
+ try {
32
+ // get the original src
33
+ let src = fs.readFileSync(filepath, { encoding: ENCODING })
34
+ // get/generate the public key
35
+ const envKeysFilepath = path.join(path.dirname(filepath), '.env.keys')
36
+ const {
37
+ envSrc,
38
+ publicKey,
39
+ privateKey,
40
+ privateKeyAdded
41
+ } = findOrCreatePublicKey(filepath, envKeysFilepath)
42
+ row.publicKey = publicKey
43
+ row.privateKey = privateKey
44
+ row.privateKeyName = guessPrivateKeyName(filepath)
45
+ row.privateKeyAdded = privateKeyAdded
46
+
47
+ src = envSrc // src was potentially changed by findOrCreatePublicKey so we set it again here
48
+
49
+ // track possible changes
50
+ row.changed = false
51
+
52
+ // iterate over all non-encrypted values and encrypt them
53
+ const parsed = dotenv.parse(src)
54
+ for (const [key, value] of Object.entries(parsed)) {
55
+ const encrypted = isEncrypted(key, value)
56
+ if (!encrypted) {
57
+ row.keys.push(key) // track key(s)
58
+
59
+ const encryptedValue = encryptValue(value, publicKey)
60
+ // once newSrc is built write it out
61
+ src = replace(src, key, encryptedValue)
62
+
63
+ row.changed = true // track change
64
+ }
65
+ }
66
+
67
+ if (row.changed) {
68
+ row.envSrc = src
69
+ this.changedFilepaths.add(envFilepath)
70
+ } else {
71
+ row.envSrc = src
72
+ this.unchangedFilepaths.add(envFilepath)
73
+ }
74
+ } catch (e) {
75
+ if (e.code === 'ENOENT') {
76
+ const error = new Error(`missing ${envFilepath} file (${filepath})`)
77
+ error.code = 'MISSING_ENV_FILE'
78
+
79
+ row.error = error
80
+ } else {
81
+ row.error = e
82
+ }
50
83
  }
51
84
 
52
- uniqueEnvFilepaths.add(filepath)
85
+ this.processedEnvFiles.push(row)
53
86
  }
54
87
 
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
88
  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
89
+ processedEnvFiles: this.processedEnvFiles,
90
+ changedFilepaths: [...this.changedFilepaths],
91
+ unchangedFilepaths: [...this.unchangedFilepaths]
90
92
  }
91
93
  }
92
94
 
@@ -97,22 +99,6 @@ class Encrypt {
97
99
 
98
100
  return this.envFile
99
101
  }
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
102
  }
117
103
 
118
104
  module.exports = Encrypt
@@ -3,7 +3,9 @@ const fs = require('fs')
3
3
  const ignore = require('ignore')
4
4
 
5
5
  const pluralize = require('./../helpers/pluralize')
6
+ const isFullyEncrypted = require('./../helpers/isFullyEncrypted')
6
7
  const InstallPrecommitHook = require('./../helpers/installPrecommitHook')
8
+ const MISSING_GITIGNORE = '.env.keys' // by default only ignore .env.keys. all other .env* files COULD be included - as long as they are encrypted
7
9
 
8
10
  class Precommit {
9
11
  constructor (options = {}) {
@@ -22,16 +24,20 @@ class Precommit {
22
24
  }
23
25
  } else {
24
26
  const warnings = []
27
+ let successMessage = 'success'
28
+ let gitignore = MISSING_GITIGNORE
25
29
 
26
30
  // 1. check for .gitignore file
27
31
  if (!fs.existsSync('.gitignore')) {
28
- const error = new Error('.gitignore missing')
29
- error.help = '? add it with [touch .gitignore]'
30
- throw error
32
+ const warning = new Error('.gitignore missing')
33
+ warning.help = '? add it with [touch .gitignore]'
34
+ warnings.push(warning)
35
+ } else {
36
+ gitignore = fs.readFileSync('.gitignore').toString()
31
37
  }
32
38
 
33
39
  // 2. check .env* files against .gitignore file
34
- const ig = ignore().add(fs.readFileSync('.gitignore').toString())
40
+ const ig = ignore().add(gitignore)
35
41
  const files = fs.readdirSync(process.cwd())
36
42
  const dotenvFiles = files.filter(file => file.match(/^\.env(\..+)?$/))
37
43
  dotenvFiles.forEach(file => {
@@ -44,14 +50,19 @@ class Precommit {
44
50
  }
45
51
  } else {
46
52
  if (file !== '.env.example' && file !== '.env.vault') {
47
- const error = new Error(`${file} not properly gitignored`)
48
- error.help = `? add ${file} to .gitignore with [echo ".env*" >> .gitignore]`
49
- throw error
53
+ const src = fs.readFileSync(file).toString()
54
+ const encrypted = isFullyEncrypted(src)
55
+
56
+ // if contents are encrypted don't raise an error
57
+ if (!encrypted) {
58
+ const error = new Error(`${file} not encrypted (or not gitignored)`)
59
+ error.help = `? encrypt it with [dotenvx protect -f ${file}] or add ${file} to .gitignore with [echo ".env*" >> .gitignore]`
60
+ throw error
61
+ }
50
62
  }
51
63
  }
52
64
  })
53
65
 
54
- let successMessage = 'success'
55
66
  if (warnings.length > 0) {
56
67
  successMessage = `success (with ${pluralize('warning', warnings.length)})`
57
68
  }
@@ -1,7 +1,10 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
 
4
+ const dotenv = require('dotenv')
5
+
4
6
  const findOrCreatePublicKey = require('./../helpers/findOrCreatePublicKey')
7
+ const guessPrivateKeyName = require('./../helpers/guessPrivateKeyName')
5
8
  const encryptValue = require('./../helpers/encryptValue')
6
9
  const replace = require('./../helpers/replace')
7
10
 
@@ -14,39 +17,57 @@ class Sets {
14
17
  this.envFile = envFile
15
18
  this.encrypt = encrypt
16
19
 
17
- this.publicKey = null
18
20
  this.processedEnvFiles = []
19
- this.settableFilepaths = new Set()
21
+ this.changedFilepaths = new Set()
22
+ this.unchangedFilepaths = new Set()
20
23
  }
21
24
 
22
25
  run () {
23
26
  const envFilepaths = this._envFilepaths()
24
27
  for (const envFilepath of envFilepaths) {
28
+ const filepath = path.resolve(envFilepath)
29
+
25
30
  const row = {}
26
31
  row.key = this.key
27
- row.filepath = envFilepath
28
32
  row.value = this.value
33
+ row.filepath = filepath
34
+ row.envFilepath = envFilepath
35
+ row.changed = false
29
36
 
30
- const filepath = path.resolve(envFilepath)
31
37
  try {
32
38
  let value = this.value
33
39
  let src = fs.readFileSync(filepath, { encoding: ENCODING })
40
+ row.originalValue = dotenv.parse(src)[row.key] || null
41
+
34
42
  if (this.encrypt) {
35
43
  const envKeysFilepath = path.join(path.dirname(filepath), '.env.keys')
36
44
  const {
45
+ envSrc,
37
46
  publicKey,
38
- envSrc
47
+ privateKey,
48
+ privateKeyAdded
39
49
  } = findOrCreatePublicKey(filepath, envKeysFilepath)
40
50
  src = envSrc // overwrite the original read (because findOrCreatePublicKey) rewrite to it
41
51
  value = encryptValue(value, publicKey)
42
- row.encryptedValue = value // useful
52
+
53
+ row.changed = true // track change
54
+ row.encryptedValue = value
43
55
  row.publicKey = publicKey
56
+ row.privateKey = privateKey
57
+ row.privateKeyAdded = privateKeyAdded
58
+ row.privateKeyName = guessPrivateKeyName(filepath)
44
59
  }
45
60
 
46
- const newSrc = replace(src, this.key, value)
47
- fs.writeFileSync(filepath, newSrc)
61
+ if (value !== row.originalValue) {
62
+ row.envSrc = replace(src, this.key, value)
63
+
64
+ this.changedFilepaths.add(envFilepath)
65
+ row.changed = true
66
+ } else {
67
+ row.envSrc = src
48
68
 
49
- this.settableFilepaths.add(envFilepath)
69
+ this.unchangedFilepaths.add(envFilepath)
70
+ }
50
71
  } catch (e) {
51
72
  if (e.code === 'ENOENT') {
52
73
  const error = new Error(`missing ${envFilepath} file (${filepath})`)
@@ -63,7 +84,8 @@ class Sets {
63
84
 
64
85
  return {
65
86
  processedEnvFiles: this.processedEnvFiles,
66
- settableFilepaths: [...this.settableFilepaths]
87
+ changedFilepaths: [...this.changedFilepaths],
88
+ unchangedFilepaths: [...this.unchangedFilepaths]
67
89
  }
68
90
  }
69
91
 
@@ -0,0 +1,118 @@
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 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 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