@dotenvx/dotenvx 0.37.1 → 0.38.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
@@ -514,224 +514,75 @@ More examples
514
514
 
515
515
  ## Encryption
516
516
 
517
- > Encrypt your secrets to a `.env.vault` file and load from it (recommended for production and ci).
518
- ```sh
519
- $ echo "HELLO=World" > .env
520
- $ echo "HELLO=production" > .env.production
521
- $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
522
-
523
- $ dotenvx encrypt
524
- [dotenvx][info] encrypted to .env.vault (.env,.env.production)
525
- [dotenvx][info] keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
517
+ > Add encryption to your `.env` files with a single command. Pass the `--encrypt` flag.
526
518
 
527
- $ DOTENV_KEY='<dotenv_key_production>' dotenvx run -- node index.js
528
- [dotenvx][info] loading env (1) from encrypted .env.vault
529
- Hello production
530
- ^ :-]
519
+ ```sh
520
+ $ dotenvx set HELLO World --encrypt
521
+ set HELLO with encryption (.env)
531
522
  ```
532
523
 
533
- More examples
534
-
535
- * <details><summary>AWS Lambda</summary><br>
536
-
537
- ```sh
538
- coming soon
539
- ```
540
-
541
- </details>
542
-
543
- * <details><summary>Digital Ocean</summary><br>
544
-
545
- ```sh
546
- coming soon
547
- ```
548
-
549
- </details>
550
-
551
- * <details><summary>Docker 🐳</summary><br>
552
-
553
- > Add the `dotenvx` binary to your Dockerfile
554
-
555
- ```sh
556
- # Install dotenvx
557
- RUN curl -fsS https://dotenvx.sh/ | sh
558
- ```
559
-
560
- > Use it in your Dockerfile CMD
561
-
562
- ```sh
563
- # Prepend dotenvx run
564
- CMD ["dotenvx", "run", "--", "node", "index.js"]
565
- ```
566
-
567
- see [docker guide](https://dotenvx.com/docs/platforms/docker)
568
-
569
- </details>
570
-
571
- * <details><summary>Fly.io 🎈</summary><br>
572
-
573
- > Add the `dotenvx` binary to your Dockerfile
574
-
575
- ```sh
576
- # Install dotenvx
577
- RUN curl -fsS https://dotenvx.sh/ | sh
578
- ```
579
-
580
- > Use it in your Dockerfile CMD
581
-
582
- ```sh
583
- # Prepend dotenvx run
584
- CMD ["dotenvx", "run", "--", "node", "index.js"]
585
- ```
586
-
587
- see [fly guide](https://dotenvx.com/docs/platforms/fly)
588
-
589
- </details>
590
-
591
- * <details><summary>Heroku 🟣</summary><br>
524
+ ![](https://github.com/dotenvx/dotenvx/assets/3848/21f7a529-7a40-44e4-87d4-a72e1637b702)
592
525
 
593
- > Add the buildpack, installing the `dotenvx` binary to your heroku deployment.
594
-
595
- ```sh
596
- heroku buildpacks:add https://github.com/dotenvx/heroku-buildpack-dotenvx
597
- ```
526
+ > A `DOTENV_PUBLIC_KEY` (encryption key) and a `DOTENV_PRIVATE_KEY` (decryption key) is generated using the same public-key cryptography as [Bitcoin](https://en.bitcoin.it/wiki/Secp256k1).
598
527
 
599
- > Use it in your Procfile.
600
-
601
- ```sh
602
- web: dotenvx run -- node index.js
603
- ```
604
-
605
- see [heroku guide](https://dotenvx.com/docs/platforms/heroku)
606
-
607
- </details>
608
-
609
- * <details><summary>Laravel Forge</summary><br>
610
-
611
- ```sh
612
- coming soon
613
- ```
614
-
615
- </details>
616
-
617
- * <details><summary>Netlify 🔷</summary><br>
528
+ More examples
618
529
 
619
- > Add the `dotenvx` npm module
530
+ * <details><summary>`.env`</summary><br>
620
531
 
621
532
  ```sh
622
- npm install @dotenvx/dotenvx --save
623
- ```
624
-
625
- > Use it in your `package.json scripts`
626
-
627
- ```json
628
- "scripts": {
629
- "dotenvx": "dotenvx",
630
- "dev": "dotenvx run -- next dev --turbo",
631
- "build": "dotenvx run -- next build",
632
- "start": "dotenvx run -- next start"
633
- },
634
- ```
635
-
636
- see [netlify guide](https://dotenvx.com/docs/platforms/netlify)
637
-
638
- </details>
639
-
640
- * <details><summary>Railway 🚄</summary><br>
641
-
642
- > Add the `dotenvx` binary to your Dockerfile
533
+ $ dotenvx set HELLO World --encrypt
534
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
643
535
 
644
- ```sh
645
- # Install dotenvx
646
- RUN curl -fsS https://dotenvx.sh/ | sh
536
+ $ dotenvx run -- node index.js
537
+ [dotenvx] injecting env (2) from .env
538
+ Hello World
647
539
  ```
648
540
 
649
- > Use it in your Dockerfile CMD
541
+ * <details><summary>`.env.production`</summary><br>
650
542
 
651
543
  ```sh
652
- # Prepend dotenvx run
653
- CMD ["dotenvx", "run", "--", "node", "index.js"]
654
- ```
655
-
656
- see [railway guide](https://dotenvx.com/docs/platforms/railway)
657
-
658
- </details>
659
-
660
- * <details><summary>Render</summary><br>
544
+ $ dotenvx set HELLO Production --encrypt -f .env.production
545
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
661
546
 
662
- ```sh
663
- coming soon
547
+ $ DOTENV_PRIVATE_KEY_PRODUCTION="<.env.production private key>" dotenvx run -- node index.js
548
+ [dotenvx] injecting env (2) from .env.production
549
+ Hello Production
664
550
  ```
665
551
 
666
- </details>
667
-
668
- * <details><summary>Vercel ▲</summary><br>
552
+ Note the `DOTENV_PRIVATE_KEY_PRODUCTION` ends with `_PRODUCTION`. This instructs `dotenvx run` to load the `.env.production` file.
669
553
 
670
- > Add the `dotenvx` npm module
554
+ * <details><summary>`.env.ci`</summary><br>
671
555
 
672
556
  ```sh
673
- npm install @dotenvx/dotenvx --save
674
- ```
675
-
676
- > Use it in your `package.json scripts`
557
+ $ dotenvx set HELLO Ci --encrypt -f .env.ci
558
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
677
559
 
678
- ```json
679
- "scripts": {
680
- "dotenvx": "dotenvx",
681
- "dev": "dotenvx run -- next dev --turbo",
682
- "build": "dotenvx run -- next build",
683
- "start": "dotenvx run -- next start"
684
- },
560
+ $ DOTENV_PRIVATE_KEY_CI="<.env.ci private key>" dotenvx run -- node index.js
561
+ [dotenvx] injecting env (2) from .env.ci
562
+ Hello Ci
685
563
  ```
686
564
 
687
- see [vercel guide](https://dotenvx.com/docs/platforms/vercel)
688
-
689
- </details>
565
+ Note the `DOTENV_PRIVATE_KEY_CI` ends with `_CI`. This instructs `dotenvx run` to load the `.env.ci` file. See the pattern?
690
566
 
691
- * <details><summary>CircleCI</summary><br>
567
+ * <details><summary>combine multiple encrypted .env files</summary><br>
692
568
 
693
569
  ```sh
694
- coming soon
695
- ```
696
-
697
- </details>
698
-
699
- * <details><summary>GitHub Actions 🐙</summary><br>
700
-
701
- > Add the `dotenvx` binary to GitHub Actions
570
+ $ dotenvx set HELLO World --encrypt -f .env
571
+ $ dotenvx set HELLO Production --encrypt -f .env.production
572
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
702
573
 
703
- ```sh
704
- name: build
705
- on: [push]
706
- jobs:
707
- build:
708
- runs-on: ubuntu-latest
709
- steps:
710
- - uses: actions/checkout@v3
711
- - uses: actions/setup-node@v3
712
- with:
713
- node-version: 16
714
- - run: curl -fsS https://dotenvx.sh/ | sh
715
- - run: dotenvx run -- node build.js
716
- env:
717
- DOTENV_KEY: ${{ secrets.DOTENV_KEY }}
574
+ $ DOTENV_PRIVATE_KEY="<.env private key>" DOTENV_PRIVATE_KEY_PRODUCTION="<.env.production private key>" dotenvx run -- node index.js
575
+ [dotenvx] injecting env (3) from .env, .env.production
576
+ Hello World
718
577
  ```
719
578
 
720
- see [github actions guide](https://dotenvx.com/docs/cis/github-actions)
579
+ Note the `DOTENV_PRIVATE_KEY` instructs `dotenvx run` to load the `.env` file and the `DOTENV_PRIVATE_KEY_PRODUCTION` instructs it to load the `.env.production` file. See the pattern?
721
580
 
722
- </details>
581
+ * <details><summary>other curves</summary><br>
723
582
 
724
- &nbsp;
725
-
726
- ## Hub
727
-
728
- > Integrate tightly with [GitHub](https://github.com) 🐙 and as a team
729
- ```sh
730
- $ dotenvx hub login
731
- $ dotenvx hub push
732
- ```
733
-
734
- **beta**: more details coming soon.
583
+ > `secp256k1` is a well-known and battle tested curve, in use with Bitcoin and other cryptocurrencies, but we are open to adding support for more curves.
584
+ >
585
+ > If your organization's compliance department requires [NIST approved curves](https://csrc.nist.gov/projects/elliptic-curve-cryptography) or other curves like `curve25519`, please reach out at [security@dotenvx.com](mailto:security@dotenvx.com).
735
586
 
736
587
  &nbsp;
737
588
 
@@ -786,6 +637,23 @@ This fix is easy. Replace `--env-file` with `-f`.
786
637
 
787
638
  [more context](https://github.com/dotenvx/dotenvx/issues/131)
788
639
 
640
+ #### What happened to the `.env.vault` file?
641
+
642
+ I've decided we should sunset it as a technological solution to this.
643
+
644
+ The `.env.vault` file got us far, but it had limitations such as:
645
+
646
+ * *Pull Requests* - it was difficult to tell which key had been changed
647
+ * *Security* - there was no mechanism to give a teammate the ability to encrypt without also giving them the ability to decrypt. Sometimes you just want to let a contractor encrypt a new value, but you don't want them to know the rest of the secrets.
648
+ * *Conceptual* - it takes more mental energy to understand the `.env.vault` format. Encrypted values inside a `.env` file is easier to quickly grasp.
649
+ * *Combining Multiple Files* - there was simply no mechanism to do this well with the `.env.vault` file format.
650
+
651
+ That said, the `.env.vault` tooling will still stick around for at least 1 year under `dotenvx vault` parent command. I'm still using it in projects as are many thousands of other people.
652
+
653
+ #### Will you provide a migration tool to quickly switch `.env.vault` files to encrypted `.env` files?
654
+
655
+ Yes. Working on this soon.
656
+
789
657
  &nbsp;
790
658
 
791
659
  ## Contributing
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.37.1",
2
+ "version": "0.38.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -35,6 +35,7 @@
35
35
  "diff": "^5.2.0",
36
36
  "dotenv": "^16.4.5",
37
37
  "dotenv-expand": "^11.0.6",
38
+ "eciesjs": "^0.4.6",
38
39
  "execa": "^5.1.1",
39
40
  "glob": "^10.3.10",
40
41
  "ignore": "^5.3.0",
@@ -13,7 +13,9 @@ function set (key, value) {
13
13
  const {
14
14
  processedEnvFiles,
15
15
  settableFilepaths
16
- } = main.set(key, value, options.envFile)
16
+ } = main.set(key, value, options.envFile, options.encrypt)
17
+
18
+ let atLeastOneSuccess = false
17
19
 
18
20
  for (const processedEnvFile of processedEnvFiles) {
19
21
  logger.verbose(`setting for ${processedEnvFile.filepath}`)
@@ -26,12 +28,15 @@ function set (key, value) {
26
28
  logger.warn(processedEnvFile.error)
27
29
  }
28
30
  } else {
31
+ atLeastOneSuccess = true
29
32
  logger.verbose(`${processedEnvFile.key} set`)
30
33
  logger.debug(`${processedEnvFile.key} set to ${processedEnvFile.value}`)
31
34
  }
32
35
  }
33
36
 
34
- logger.success(`set ${key} (${settableFilepaths.join(', ')})`)
37
+ if (atLeastOneSuccess) {
38
+ logger.success(`set ${key} (${settableFilepaths.join(', ')})`)
39
+ }
35
40
  } catch (error) {
36
41
  logger.error(error.message)
37
42
  if (error.help) {
@@ -1,10 +1,10 @@
1
1
  const fs = require('fs')
2
2
 
3
- const logger = require('./../../shared/logger')
4
- const createSpinner = require('./../../shared/createSpinner')
5
- const sleep = require('./../../lib/helpers/sleep')
3
+ const logger = require('./../../../shared/logger')
4
+ const createSpinner = require('./../../../shared/createSpinner')
5
+ const sleep = require('./../../../lib/helpers/sleep')
6
6
 
7
- const Decrypt = require('./../../lib/services/decrypt')
7
+ const Decrypt = require('./../../../lib/services/decrypt')
8
8
 
9
9
  const spinner = createSpinner('decrypting')
10
10
 
@@ -1,11 +1,11 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
 
4
- const main = require('./../../lib/main')
5
- const logger = require('./../../shared/logger')
6
- const createSpinner = require('./../../shared/createSpinner')
7
- const sleep = require('./../../lib/helpers/sleep')
8
- const pluralize = require('./../../lib/helpers/pluralize')
4
+ const main = require('./../../../lib/main')
5
+ const logger = require('./../../../shared/logger')
6
+ const createSpinner = require('./../../../shared/createSpinner')
7
+ const sleep = require('./../../../lib/helpers/sleep')
8
+ const pluralize = require('./../../../lib/helpers/pluralize')
9
9
 
10
10
  const spinner = createSpinner('encrypting')
11
11
 
@@ -1,6 +1,6 @@
1
- const logger = require('./../../shared/logger')
1
+ const logger = require('./../../../shared/logger')
2
2
 
3
- const main = require('./../../lib/main')
3
+ const main = require('./../../../lib/main')
4
4
 
5
5
  function status (directory) {
6
6
  // debug args
@@ -1,53 +1,89 @@
1
1
  const { Command } = require('commander')
2
2
 
3
3
  const store = require('./../../shared/store')
4
+ const logger = require('./../../shared/logger')
4
5
 
5
6
  const hub = new Command('hub')
6
7
 
7
8
  hub
8
9
  .description('interact with dotenvx hub')
9
10
 
11
+ const loginAction = require('./../actions/hub/login')
10
12
  hub
11
13
  .command('login')
12
14
  .description('authenticate to dotenvx hub')
13
15
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
14
- .action(require('./../actions/hub/login'))
16
+ .action(function (...args) {
17
+ logger.warn('DEPRECATION NOTECE: [dotenvx hub login] will be removed in 1.0.0 release soon')
15
18
 
19
+ loginAction.apply(this, args)
20
+ })
21
+
22
+ const pushAction = require('./../actions/hub/push')
16
23
  hub
17
24
  .command('push')
18
25
  .description('push .env.keys to dotenvx hub')
19
26
  .argument('[directory]', 'directory to push', '.')
20
27
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
21
- .action(require('./../actions/hub/push'))
28
+ .action(function (...args) {
29
+ logger.warn('DEPRECATION NOTECE: [dotenvx hub push] will be removed in 1.0.0 release soon')
30
+
31
+ pushAction.apply(this, args)
32
+ })
22
33
 
34
+ const pullAction = require('./../actions/hub/pull')
23
35
  hub
24
36
  .command('pull')
25
37
  .description('pull .env.keys from dotenvx hub')
26
38
  .argument('[directory]', 'directory to pull', '.')
27
39
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
28
- .action(require('./../actions/hub/pull'))
40
+ .action(function (...args) {
41
+ logger.warn('DEPRECATION NOTECE: [dotenvx hub pull] will be removed in 1.0.0 release soon')
29
42
 
43
+ pullAction.apply(this, args)
44
+ })
45
+
46
+ const openAction = require('./../actions/hub/open')
30
47
  hub
31
48
  .command('open')
32
49
  .description('view repository on dotenvx hub')
33
50
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
34
- .action(require('./../actions/hub/open'))
51
+ .action(function (...args) {
52
+ logger.warn('DEPRECATION NOTECE: [dotenvx hub open] will be removed in 1.0.0 release soon')
53
+
54
+ openAction.apply(this, args)
55
+ })
35
56
 
57
+ const tokenAction = require('./../actions/hub/token')
36
58
  hub
37
59
  .command('token')
38
60
  .description('print the auth token dotenvx hub is configured to use')
39
61
  .option('-h, --hostname <url>', 'set hostname', 'https://hub.dotenvx.com')
40
- .action(require('./../actions/hub/token'))
62
+ .action(function (...args) {
63
+ logger.warn('DEPRECATION NOTECE: [dotenvx hub token] will be removed in 1.0.0 release soon')
41
64
 
65
+ tokenAction.apply(this, args)
66
+ })
67
+
68
+ const statusAction = require('./../actions/hub/status')
42
69
  hub
43
70
  .command('status')
44
71
  .description('display logged in user')
45
- .action(require('./../actions/hub/status'))
72
+ .action(function (...args) {
73
+ logger.warn('DEPRECATION NOTECE: [dotenvx hub status] will be removed in 1.0.0 release soon')
74
+
75
+ statusAction.apply(this, args)
76
+ })
46
77
 
78
+ const logoutAction = require('./../actions/hub/logout')
47
79
  hub
48
80
  .command('logout')
49
81
  .description('log out this machine from dotenvx hub')
50
82
  .option('-h, --hostname <url>', 'set hostname', store.getHostname())
51
- .action(require('./../actions/hub/logout'))
83
+ .action(function (...args) {
84
+ logger.warn('DEPRECATION NOTECE: [dotenvx hub logout] will be removed in 1.0.0 release soon')
85
+
86
+ logoutAction.apply(this, args)
87
+ })
52
88
 
53
89
  module.exports = hub
@@ -0,0 +1,31 @@
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 vault encrypt
11
+ vault.command('encrypt')
12
+ .description('encrypt .env.* to .env.vault')
13
+ .addHelpText('after', examples.encrypt)
14
+ .argument('[directory]', 'directory to encrypt', '.')
15
+ .option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
16
+ .action(require('./../actions/vault/encrypt'))
17
+
18
+ // dotenvx vault decrypt
19
+ vault.command('decrypt')
20
+ .description('decrypt .env.vault to .env*')
21
+ .argument('[directory]', 'directory to decrypt', '.')
22
+ .option('-e, --environment <environments...>', 'environment(s) to decrypt')
23
+ .action(require('./../actions/vault/decrypt'))
24
+
25
+ // dotenvx vault status
26
+ vault.command('status')
27
+ .description('compare your .env* content(s) to your .env.vault decrypted content(s)')
28
+ .argument('[directory]', 'directory to check status against', '.')
29
+ .action(require('./../actions/vault/status'))
30
+
31
+ module.exports = vault
@@ -12,7 +12,7 @@ const packageJson = require('./../lib/helpers/packageJson')
12
12
  const notice = new UpdateNotice()
13
13
  notice.check()
14
14
  if (notice.update) {
15
- logger.warn(`Update available ${notice.packageVersion} → ${notice.latestVersion} changelog: https://dotenvx.com/changelog`)
15
+ logger.warn(`Update available ${notice.packageVersion} → ${notice.latestVersion} 0.38.0 and higher have SIGNIFICANT changes. please read the changelog: https://dotenvx.com/changelog`)
16
16
  }
17
17
 
18
18
  // for use with run
@@ -99,29 +99,9 @@ program.command('set')
99
99
  .argument('KEY', 'KEY')
100
100
  .argument('value', 'value')
101
101
  .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
102
+ .option('-c, --encrypt', 'encrypt value')
102
103
  .action(require('./actions/set'))
103
104
 
104
- // dotenvx encrypt
105
- program.command('encrypt')
106
- .description('encrypt .env.* to .env.vault')
107
- .addHelpText('after', examples.encrypt)
108
- .argument('[directory]', 'directory to encrypt', '.')
109
- .option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
110
- .action(require('./actions/encrypt'))
111
-
112
- // dotenvx decrypt
113
- program.command('decrypt')
114
- .description('decrypt .env.vault to .env*')
115
- .argument('[directory]', 'directory to decrypt', '.')
116
- .option('-e, --environment <environments...>', 'environment(s) to decrypt')
117
- .action(require('./actions/decrypt'))
118
-
119
- // dotenvx status
120
- program.command('status')
121
- .description('compare your .env* content(s) to your .env.vault decrypted content(s)')
122
- .argument('[directory]', 'directory to check status against', '.')
123
- .action(require('./actions/status'))
124
-
125
105
  // dotenvx genexample
126
106
  program.command('genexample')
127
107
  .description('generate .env.example')
@@ -167,6 +147,45 @@ program.command('settings')
167
147
  .option('-pp, --pretty-print', 'pretty print output')
168
148
  .action(require('./actions/settings'))
169
149
 
150
+ // dotenvx encrypt
151
+ const encryptAction = require('./actions/vault/encrypt')
152
+ program.command('encrypt')
153
+ .description('DEPRECATED: moved to [dotenvx vault encrypt]')
154
+ .addHelpText('after', examples.encrypt)
155
+ .argument('[directory]', 'directory to encrypt', '.')
156
+ .option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
157
+ .action(function (...args) {
158
+ logger.warn('DEPRECATION NOTICE: [dotenvx encrypt] has moved. change your command to [dotenvx vault encrypt]')
159
+
160
+ encryptAction.apply(this, args)
161
+ })
162
+
163
+ // dotenvx decrypt
164
+ const decryptAction = require('./actions/vault/decrypt')
165
+ program.command('decrypt')
166
+ .description('DEPRECATED: moved to [dotenvx vault decrypt]')
167
+ .argument('[directory]', 'directory to decrypt', '.')
168
+ .option('-e, --environment <environments...>', 'environment(s) to decrypt')
169
+ .action(function (...args) {
170
+ logger.warn('DEPRECATION NOTECE: [dotenvx decrypt] has moved. change your command to [dotenvx vault decrypt]')
171
+
172
+ decryptAction.apply(this, args)
173
+ })
174
+
175
+ // dotenvx status
176
+ const statusAction = require('./actions/vault/status')
177
+ program.command('status')
178
+ .description('DEPRECATED: moved to [dotenvx vault status]')
179
+ .argument('[directory]', 'directory to check status against', '.')
180
+ .action(function (...args) {
181
+ logger.warn('DEPRECATION NOTICE: [dotenvx status] has moved. change your command to [dotenvx vault status]')
182
+
183
+ statusAction.apply(this, args)
184
+ })
185
+
186
+ // dotenvx vault
187
+ program.addCommand(require('./commands/vault'))
188
+
170
189
  // dotenvx hub
171
190
  program.addCommand(require('./commands/hub'))
172
191
 
@@ -0,0 +1,29 @@
1
+ const { decrypt } = require('eciesjs')
2
+
3
+ const PREFIX = 'encrypted:'
4
+
5
+ function decryptValue (value, privateKey) {
6
+ if (!value.startsWith(PREFIX)) {
7
+ return value
8
+ }
9
+
10
+ const privateKeys = privateKey.split(',')
11
+
12
+ let decryptedValue
13
+ for (const key of privateKeys) {
14
+ const secret = Buffer.from(key, 'hex')
15
+ const encoded = value.substring(PREFIX.length)
16
+ const ciphertext = Buffer.from(encoded, 'base64')
17
+
18
+ try {
19
+ decryptedValue = decrypt(secret, ciphertext).toString()
20
+ break
21
+ } catch (_error) {
22
+ // TODO: somehow surface these errors to the user's logs
23
+ }
24
+ }
25
+
26
+ return decryptedValue || value
27
+ }
28
+
29
+ module.exports = decryptValue
@@ -0,0 +1,12 @@
1
+ const { encrypt } = require('eciesjs')
2
+
3
+ const PREFIX = 'encrypted:'
4
+
5
+ function encryptValue (value, publicKey) {
6
+ const ciphertext = encrypt(publicKey, Buffer.from(value))
7
+ const encoded = Buffer.from(ciphertext, 'hex').toString('base64') // base64 encode ciphertext
8
+
9
+ return `${PREFIX}${encoded}`
10
+ }
11
+
12
+ module.exports = encryptValue
@@ -0,0 +1,80 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const dotenv = require('dotenv')
4
+
5
+ const keyPair = require('./keyPair')
6
+ const guessPublicKeyName = require('./guessPublicKeyName')
7
+ const guessPrivateKeyName = require('./guessPrivateKeyName')
8
+
9
+ const ENCODING = 'utf8'
10
+
11
+ function findOrCreatePublicKey (envFilepath, envKeysFilepath) {
12
+ // filename
13
+ const filename = path.basename(envFilepath)
14
+ const publicKeyName = guessPublicKeyName(envFilepath)
15
+ const privateKeyName = guessPrivateKeyName(envFilepath)
16
+
17
+ // src
18
+ let envSrc = fs.readFileSync(envFilepath, { encoding: ENCODING })
19
+ let keysSrc = ''
20
+ if (fs.existsSync(envKeysFilepath)) {
21
+ keysSrc = fs.readFileSync(envKeysFilepath, { encoding: ENCODING })
22
+ }
23
+
24
+ // parsed
25
+ const envParsed = dotenv.parse(envSrc)
26
+ const keysParsed = dotenv.parse(keysSrc)
27
+
28
+ // if DOTENV_PUBLIC_KEY_${environment} already present then go no further
29
+ if (envParsed[publicKeyName] && envParsed[publicKeyName].length > 0) {
30
+ return {
31
+ envSrc,
32
+ keysSrc,
33
+ publicKey: envParsed[publicKeyName],
34
+ privateKey: keysParsed[privateKeyName]
35
+ }
36
+ }
37
+
38
+ // generate key pair
39
+ const { publicKey, privateKey } = keyPair()
40
+
41
+ // publicKey
42
+ const prependPublicKey = [
43
+ '#/-------------------[DOTENV_PUBLIC_KEY]--------------------/',
44
+ '#/ public-key encryption for .env files /',
45
+ '#/ [how it works](https://dotenvx.com/encryption) /',
46
+ '#/----------------------------------------------------------/',
47
+ `${publicKeyName}="${publicKey}"`,
48
+ '',
49
+ `# ${filename}`
50
+ ].join('\n')
51
+
52
+ // privateKey
53
+ const firstTimeKeysSrc = [
54
+ '#/------------------!DOTENV_PRIVATE_KEYS!-------------------/',
55
+ '#/ private decryption keys. DO NOT commit to source control /',
56
+ '#/ [how it works](https://dotenvx.com/encryption) /',
57
+ '#/----------------------------------------------------------/'
58
+ ].join('\n')
59
+ const appendPrivateKey = [
60
+ `# ${filename}`,
61
+ `${privateKeyName}="${privateKey}"`,
62
+ ''
63
+ ].join('\n')
64
+
65
+ envSrc = `${prependPublicKey}\n${envSrc}`
66
+ keysSrc = keysSrc.length > 1 ? keysSrc : `${firstTimeKeysSrc}\n`
67
+ keysSrc = `${keysSrc}\n${appendPrivateKey}`
68
+
69
+ fs.writeFileSync(envFilepath, envSrc)
70
+ fs.writeFileSync(envKeysFilepath, keysSrc)
71
+
72
+ return {
73
+ envSrc,
74
+ keysSrc,
75
+ publicKey,
76
+ privateKey
77
+ }
78
+ }
79
+
80
+ module.exports = findOrCreatePublicKey
@@ -2,6 +2,7 @@ const path = require('path')
2
2
 
3
3
  function guessEnvironment (filepath) {
4
4
  const filename = path.basename(filepath)
5
+
5
6
  const parts = filename.split('.')
6
7
  const possibleEnvironmentList = [...parts.slice(2)]
7
8
 
@@ -0,0 +1,15 @@
1
+ const PREFIX = 'DOTENV_PRIVATE_KEY'
2
+
3
+ function guessPrivateKeyFilename (privateKeyName) {
4
+ // .env
5
+ if (privateKeyName === PREFIX) {
6
+ return '.env'
7
+ }
8
+
9
+ const filenameSuffix = privateKeyName.substring(`${PREFIX}_`.length).split('_').join('.').toLowerCase()
10
+ // .env.ENVIRONMENT
11
+
12
+ return `.env.${filenameSuffix}`
13
+ }
14
+
15
+ module.exports = guessPrivateKeyFilename
@@ -0,0 +1,18 @@
1
+ const path = require('path')
2
+ const guessEnvironment = require('./guessEnvironment')
3
+
4
+ function guessPrivateKeyName (filepath) {
5
+ const filename = path.basename(filepath).toLowerCase()
6
+
7
+ // .env
8
+ if (filename === '.env') {
9
+ return 'DOTENV_PRIVATE_KEY'
10
+ }
11
+
12
+ // .env.ENVIRONMENT
13
+ const environment = guessEnvironment(filename)
14
+
15
+ return `DOTENV_PRIVATE_KEY_${environment.toUpperCase()}`
16
+ }
17
+
18
+ module.exports = guessPrivateKeyName
@@ -0,0 +1,18 @@
1
+ const path = require('path')
2
+ const guessEnvironment = require('./guessEnvironment')
3
+
4
+ function guessPublicKeyName (filepath) {
5
+ const filename = path.basename(filepath).toLowerCase()
6
+
7
+ // .env
8
+ if (filename === '.env') {
9
+ return 'DOTENV_PUBLIC_KEY'
10
+ }
11
+
12
+ // .env.ENVIRONMENT
13
+ const environment = guessEnvironment(filename)
14
+
15
+ return `DOTENV_PUBLIC_KEY_${environment.toUpperCase()}`
16
+ }
17
+
18
+ module.exports = guessPublicKeyName
@@ -0,0 +1,15 @@
1
+ const { PrivateKey } = require('eciesjs')
2
+
3
+ function keyPair () {
4
+ const kp = new PrivateKey()
5
+
6
+ const publicKey = kp.publicKey.toHex()
7
+ const privateKey = kp.secret.toString('hex')
8
+
9
+ return {
10
+ publicKey,
11
+ privateKey
12
+ }
13
+ }
14
+
15
+ module.exports = keyPair
@@ -1,11 +1,23 @@
1
1
  const dotenv = require('dotenv')
2
2
  const dotenvExpand = require('dotenv-expand')
3
3
  const dotenvEval = require('./dotenvEval')
4
+ const decryptValue = require('./decryptValue')
4
5
 
5
- function parseExpandAndEval (src) {
6
+ function parseDecryptEvalExpand (src, privateKey = null) {
6
7
  // parse
7
8
  const parsed = dotenv.parse(src)
8
9
 
10
+ // inline decrypt
11
+ for (const key in parsed) {
12
+ const value = parsed[key]
13
+
14
+ // handle inline encrypted values
15
+ if (privateKey && privateKey.length > 0) {
16
+ // privateKey
17
+ parsed[key] = decryptValue(value, privateKey)
18
+ }
19
+ }
20
+
9
21
  // eval parsed only. do NOT eval process.env ever. too risky/dangerous.
10
22
  const inputParsed = {
11
23
  processEnv: {},
@@ -28,4 +40,4 @@ function parseExpandAndEval (src) {
28
40
  return result
29
41
  }
30
42
 
31
- module.exports = parseExpandAndEval
43
+ module.exports = parseDecryptEvalExpand
@@ -0,0 +1,26 @@
1
+ const dotenv = require('dotenv')
2
+
3
+ function replace (src, key, value) {
4
+ let output
5
+ let formatted = `${key}="${value}"`
6
+
7
+ const parsed = dotenv.parse(src)
8
+ if (Object.prototype.hasOwnProperty.call(parsed, key)) {
9
+ // replace
10
+ const regex = new RegExp(`^${key}=.*$`, 'm') // Regular expression to find the key and replace its value
11
+ output = src.replace(regex, formatted)
12
+ } else {
13
+ // append
14
+ if (src.endsWith('\n')) {
15
+ formatted = formatted + '\n'
16
+ } else {
17
+ formatted = '\n' + formatted
18
+ }
19
+
20
+ output = src + formatted
21
+ }
22
+
23
+ return output
24
+ }
25
+
26
+ module.exports = replace
@@ -0,0 +1,30 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const dotenv = require('dotenv')
4
+
5
+ const guessPrivateKeyName = require('./guessPrivateKeyName')
6
+
7
+ function smartDotenvPrivateKey (envFilepath) {
8
+ const privateKeyName = guessPrivateKeyName(envFilepath) // DOTENV_PRIVATE_KEY_${ENVIRONMENT}
9
+
10
+ // process.env wins
11
+ if (process.env[privateKeyName] && process.env[privateKeyName].length > 0) {
12
+ return process.env[privateKeyName]
13
+ }
14
+
15
+ // fallback to presence of .env.keys - path/to/.env.keys
16
+ const directory = path.dirname(envFilepath)
17
+ const envKeysFilepath = path.resolve(directory, '.env.keys')
18
+ if (fs.existsSync(envKeysFilepath)) {
19
+ const keysSrc = fs.readFileSync(envKeysFilepath)
20
+ const keysParsed = dotenv.parse(keysSrc)
21
+
22
+ if (keysParsed[privateKeyName] && keysParsed[privateKeyName].length > 0) {
23
+ return keysParsed[privateKeyName]
24
+ }
25
+ }
26
+
27
+ return null
28
+ }
29
+
30
+ module.exports = smartDotenvPrivateKey
package/src/lib/main.js CHANGED
@@ -58,8 +58,8 @@ const get = function (key, envs = [], overload = false, DOTENV_KEY = '', all = f
58
58
  return new Get(key, envs, overload, DOTENV_KEY, all).run()
59
59
  }
60
60
 
61
- const set = function (key, value, envFile) {
62
- return new Sets(key, value, envFile).run()
61
+ const set = function (key, value, envFile, encrypt) {
62
+ return new Sets(key, value, envFile, encrypt).run()
63
63
  }
64
64
 
65
65
  const status = function (directory) {
@@ -11,11 +11,14 @@ const DEFAULT_ENV_VAULTS = [{ type: TYPE_ENV_VAULT_FILE, value: '.env.vault' }]
11
11
 
12
12
  const inject = require('./../helpers/inject')
13
13
  const decrypt = require('./../helpers/decrypt')
14
- const parseExpandAndEval = require('./../helpers/parseExpandAndEval')
14
+ const parseDecryptEvalExpand = require('./../helpers/parseDecryptEvalExpand')
15
15
  const parseEnvironmentFromDotenvKey = require('./../helpers/parseEnvironmentFromDotenvKey')
16
+ const smartDotenvPrivateKey = require('./../helpers/smartDotenvPrivateKey')
17
+ const guessPrivateKeyFilename = require('./../helpers/guessPrivateKeyFilename')
16
18
 
17
19
  class Run {
18
20
  constructor (envs = [], overload = false, DOTENV_KEY = '', processEnv = process.env) {
21
+ this.dotenvPrivateKeyNames = Object.keys(processEnv).filter(key => key.startsWith('DOTENV_PRIVATE_KEY')) // important, must be first. used by determineEnvs
19
22
  this.envs = this._determineEnvs(envs, DOTENV_KEY)
20
23
  this.overload = overload
21
24
  this.DOTENV_KEY = DOTENV_KEY
@@ -35,6 +38,7 @@ class Run {
35
38
  // { type: 'envFile', value: '.env' },
36
39
  // { type: 'env', value: 'HELLO=three' }
37
40
  // ]
41
+
38
42
  for (const env of this.envs) {
39
43
  if (env.type === TYPE_ENV_VAULT_FILE) {
40
44
  this._injectEnvVaultFile(env.value)
@@ -59,7 +63,7 @@ class Run {
59
63
  row.string = env
60
64
 
61
65
  try {
62
- const parsed = parseExpandAndEval(env)
66
+ const parsed = parseDecryptEvalExpand(env)
63
67
  row.parsed = parsed
64
68
  this.readableStrings.add(env)
65
69
 
@@ -87,7 +91,9 @@ class Run {
87
91
  const src = fs.readFileSync(filepath, { encoding: ENCODING })
88
92
  this.readableFilepaths.add(envFilepath)
89
93
 
90
- const parsed = parseExpandAndEval(src)
94
+ // if DOTENV_PRIVATE_KEY_* already set in process.env then use it
95
+ const privateKey = smartDotenvPrivateKey(envFilepath)
96
+ const parsed = parseDecryptEvalExpand(src, privateKey)
91
97
  row.parsed = parsed
92
98
 
93
99
  const { injected, preExisted } = this._inject(this.processEnv, parsed, this.overload)
@@ -156,7 +162,7 @@ class Run {
156
162
 
157
163
  try {
158
164
  // parse this. it's the equivalent of the .env file
159
- const parsed = parseExpandAndEval(decrypted)
165
+ const parsed = parseDecryptEvalExpand(decrypted)
160
166
  row.parsed = parsed
161
167
 
162
168
  const { injected, preExisted } = this._inject(this.processEnv, parsed, this.overload)
@@ -177,8 +183,24 @@ class Run {
177
183
  return inject(processEnv, parsed, overload)
178
184
  }
179
185
 
186
+ _determineEnvsFromDotenvPrivateKey () {
187
+ const envs = []
188
+
189
+ for (const privateKeyName of this.dotenvPrivateKeyNames) {
190
+ const filename = guessPrivateKeyFilename(privateKeyName)
191
+ envs.push({ type: TYPE_ENV_FILE, value: filename })
192
+ }
193
+
194
+ return envs
195
+ }
196
+
180
197
  _determineEnvs (envs = [], DOTENV_KEY = '') {
181
198
  if (!envs || envs.length <= 0) {
199
+ // if process.env.DOTENV_PRIVATE_KEY or process.env.DOTENV_PRIVATE_KEY_${environment} is set, assume inline encryption methodology
200
+ if (this.dotenvPrivateKeyNames.length > 0) {
201
+ return this._determineEnvsFromDotenvPrivateKey()
202
+ }
203
+
182
204
  if (DOTENV_KEY.length > 0) {
183
205
  // if DOTENV_KEY is set then default to look for .env.vault file
184
206
  return DEFAULT_ENV_VAULTS
@@ -1,15 +1,20 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const dotenv = require('dotenv')
3
+
4
+ const findOrCreatePublicKey = require('./../helpers/findOrCreatePublicKey')
5
+ const encryptValue = require('./../helpers/encryptValue')
6
+ const replace = require('./../helpers/replace')
4
7
 
5
8
  const ENCODING = 'utf8'
6
9
 
7
10
  class Sets {
8
- constructor (key, value, envFile = '.env') {
11
+ constructor (key, value, envFile = '.env', encrypt = false) {
9
12
  this.key = key
10
13
  this.value = value
11
14
  this.envFile = envFile
15
+ this.encrypt = encrypt
12
16
 
17
+ this.publicKey = null
13
18
  this.processedEnvFiles = []
14
19
  this.settableFilepaths = new Set()
15
20
  }
@@ -19,21 +24,26 @@ class Sets {
19
24
  for (const envFilepath of envFilepaths) {
20
25
  const row = {}
21
26
  row.key = this.key
22
- row.value = this.value
23
27
  row.filepath = envFilepath
28
+ row.value = this.value
24
29
 
25
30
  const filepath = path.resolve(envFilepath)
26
31
  try {
27
- const src = fs.readFileSync(filepath, { encoding: ENCODING })
28
- const parsed = dotenv.parse(src)
29
-
30
- let newSrc
31
- if (Object.prototype.hasOwnProperty.call(parsed, this.key)) {
32
- newSrc = this._srcReplaced(src)
33
- } else {
34
- newSrc = this._srcAppended(src)
32
+ let value = this.value
33
+ let src = fs.readFileSync(filepath, { encoding: ENCODING })
34
+ if (this.encrypt) {
35
+ const envKeysFilepath = path.join(path.dirname(filepath), '.env.keys')
36
+ const {
37
+ publicKey,
38
+ envSrc
39
+ } = findOrCreatePublicKey(filepath, envKeysFilepath)
40
+ src = envSrc // overwrite the original read (because findOrCreatePublicKey) rewrite to it
41
+ value = encryptValue(value, publicKey)
42
+ row.encryptedValue = value // useful
43
+ row.publicKey = publicKey
35
44
  }
36
45
 
46
+ const newSrc = replace(src, this.key, value)
37
47
  fs.writeFileSync(filepath, newSrc)
38
48
 
39
49
  this.settableFilepaths.add(envFilepath)
@@ -64,24 +74,6 @@ class Sets {
64
74
 
65
75
  return this.envFile
66
76
  }
67
-
68
- _srcReplaced (src) {
69
- // Regular expression to find the key and replace its value
70
- const regex = new RegExp(`^${this.key}=.*$`, 'm')
71
-
72
- return src.replace(regex, `${this.key}="${this.value}"`)
73
- }
74
-
75
- _srcAppended (src) {
76
- let formatted = `${this.key}="${this.value}"`
77
- if (src.endsWith('\n')) {
78
- formatted = formatted + '\n'
79
- } else {
80
- formatted = '\n' + formatted
81
- }
82
-
83
- return src + formatted
84
- }
85
77
  }
86
78
 
87
79
  module.exports = Sets