@dotenvx/dotenvx 0.37.0 → 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
@@ -490,230 +490,99 @@ More examples
490
490
  Available log levels are `error, warn, info, verbose, debug, silly`
491
491
 
492
492
  </details>
493
+ * <details><summary>`--convention` flag</summary><br>
493
494
 
494
-
495
- &nbsp;
496
-
497
- ## Encryption
498
-
499
- > Encrypt your secrets to a `.env.vault` file and load from it (recommended for production and ci).
500
- ```sh
501
- $ echo "HELLO=World" > .env
502
- $ echo "HELLO=production" > .env.production
503
- $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
504
-
505
- $ dotenvx encrypt
506
- [dotenvx][info] encrypted to .env.vault (.env,.env.production)
507
- [dotenvx][info] keys added to .env.keys (DOTENV_KEY_PRODUCTION,DOTENV_KEY_PRODUCTION)
508
-
509
- $ DOTENV_KEY='<dotenv_key_production>' dotenvx run -- node index.js
510
- [dotenvx][info] loading env (1) from encrypted .env.vault
511
- Hello production
512
- ^ :-]
513
- ```
514
-
515
- More examples
516
-
517
- * <details><summary>AWS Lambda</summary><br>
518
-
519
- ```sh
520
- coming soon
521
- ```
522
-
523
- </details>
524
-
525
- * <details><summary>Digital Ocean</summary><br>
526
-
527
- ```sh
528
- coming soon
529
- ```
530
-
531
- </details>
532
-
533
- * <details><summary>Docker 🐳</summary><br>
534
-
535
- > Add the `dotenvx` binary to your Dockerfile
536
-
537
- ```sh
538
- # Install dotenvx
539
- RUN curl -fsS https://dotenvx.sh/ | sh
540
- ```
541
-
542
- > Use it in your Dockerfile CMD
495
+ Want to load envs conveniently usng the same convention as Next.js? Set `--convention` to `nextjs`:
543
496
 
544
497
  ```sh
545
- # Prepend dotenvx run
546
- CMD ["dotenvx", "run", "--", "node", "index.js"]
547
- ```
548
-
549
- see [docker guide](https://dotenvx.com/docs/platforms/docker)
550
-
551
- </details>
552
-
553
- * <details><summary>Fly.io 🎈</summary><br>
554
-
555
- > Add the `dotenvx` binary to your Dockerfile
498
+ $ echo "HELLO=development local" > .env.development.local
499
+ $ echo "HELLO=local" > .env.local
500
+ $ echo "HELLO=development" > .env.development
501
+ $ echo "HELLO=env" > .env
556
502
 
557
- ```sh
558
- # Install dotenvx
559
- RUN curl -fsS https://dotenvx.sh/ | sh
503
+ $ dotenvx run --convention=nextjs -- node index.js
504
+ Hello development local
560
505
  ```
561
506
 
562
- > Use it in your Dockerfile CMD
563
-
564
- ```sh
565
- # Prepend dotenvx run
566
- CMD ["dotenvx", "run", "--", "node", "index.js"]
567
- ```
507
+ See [next.js environment variable load order](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#environment-variable-load-order)
568
508
 
569
- see [fly guide](https://dotenvx.com/docs/platforms/fly)
509
+ (more conventions available upon request)
570
510
 
571
511
  </details>
572
512
 
573
- * <details><summary>Heroku 🟣</summary><br>
574
-
575
- > Add the buildpack, installing the `dotenvx` binary to your heroku deployment.
576
-
577
- ```sh
578
- heroku buildpacks:add https://github.com/dotenvx/heroku-buildpack-dotenvx
579
- ```
580
-
581
- > Use it in your Procfile.
582
-
583
- ```sh
584
- web: dotenvx run -- node index.js
585
- ```
513
+ &nbsp;
586
514
 
587
- see [heroku guide](https://dotenvx.com/docs/platforms/heroku)
515
+ ## Encryption
588
516
 
589
- </details>
517
+ > Add encryption to your `.env` files with a single command. Pass the `--encrypt` flag.
590
518
 
591
- * <details><summary>Laravel Forge</summary><br>
519
+ ```sh
520
+ $ dotenvx set HELLO World --encrypt
521
+ set HELLO with encryption (.env)
522
+ ```
592
523
 
593
- ```sh
594
- coming soon
595
- ```
524
+ ![](https://github.com/dotenvx/dotenvx/assets/3848/21f7a529-7a40-44e4-87d4-a72e1637b702)
596
525
 
597
- </details>
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
- * <details><summary>Netlify 🔷</summary><br>
528
+ More examples
600
529
 
601
- > Add the `dotenvx` npm module
530
+ * <details><summary>`.env`</summary><br>
602
531
 
603
532
  ```sh
604
- npm install @dotenvx/dotenvx --save
605
- ```
606
-
607
- > Use it in your `package.json scripts`
608
-
609
- ```json
610
- "scripts": {
611
- "dotenvx": "dotenvx",
612
- "dev": "dotenvx run -- next dev --turbo",
613
- "build": "dotenvx run -- next build",
614
- "start": "dotenvx run -- next start"
615
- },
616
- ```
617
-
618
- see [netlify guide](https://dotenvx.com/docs/platforms/netlify)
619
-
620
- </details>
621
-
622
- * <details><summary>Railway 🚄</summary><br>
623
-
624
- > Add the `dotenvx` binary to your Dockerfile
533
+ $ dotenvx set HELLO World --encrypt
534
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
625
535
 
626
- ```sh
627
- # Install dotenvx
628
- RUN curl -fsS https://dotenvx.sh/ | sh
536
+ $ dotenvx run -- node index.js
537
+ [dotenvx] injecting env (2) from .env
538
+ Hello World
629
539
  ```
630
540
 
631
- > Use it in your Dockerfile CMD
541
+ * <details><summary>`.env.production`</summary><br>
632
542
 
633
543
  ```sh
634
- # Prepend dotenvx run
635
- CMD ["dotenvx", "run", "--", "node", "index.js"]
636
- ```
637
-
638
- see [railway guide](https://dotenvx.com/docs/platforms/railway)
639
-
640
- </details>
641
-
642
- * <details><summary>Render</summary><br>
544
+ $ dotenvx set HELLO Production --encrypt -f .env.production
545
+ $ echo "console.log('Hello ' + process.env.HELLO)" > index.js
643
546
 
644
- ```sh
645
- 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
646
550
  ```
647
551
 
648
- </details>
649
-
650
- * <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.
651
553
 
652
- > Add the `dotenvx` npm module
554
+ * <details><summary>`.env.ci`</summary><br>
653
555
 
654
556
  ```sh
655
- npm install @dotenvx/dotenvx --save
656
- ```
657
-
658
- > 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
659
559
 
660
- ```json
661
- "scripts": {
662
- "dotenvx": "dotenvx",
663
- "dev": "dotenvx run -- next dev --turbo",
664
- "build": "dotenvx run -- next build",
665
- "start": "dotenvx run -- next start"
666
- },
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
667
563
  ```
668
564
 
669
- see [vercel guide](https://dotenvx.com/docs/platforms/vercel)
670
-
671
- </details>
565
+ Note the `DOTENV_PRIVATE_KEY_CI` ends with `_CI`. This instructs `dotenvx run` to load the `.env.ci` file. See the pattern?
672
566
 
673
- * <details><summary>CircleCI</summary><br>
567
+ * <details><summary>combine multiple encrypted .env files</summary><br>
674
568
 
675
569
  ```sh
676
- coming soon
677
- ```
678
-
679
- </details>
680
-
681
- * <details><summary>GitHub Actions 🐙</summary><br>
682
-
683
- > 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
684
573
 
685
- ```sh
686
- name: build
687
- on: [push]
688
- jobs:
689
- build:
690
- runs-on: ubuntu-latest
691
- steps:
692
- - uses: actions/checkout@v3
693
- - uses: actions/setup-node@v3
694
- with:
695
- node-version: 16
696
- - run: curl -fsS https://dotenvx.sh/ | sh
697
- - run: dotenvx run -- node build.js
698
- env:
699
- 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
700
577
  ```
701
578
 
702
- 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?
703
580
 
704
- </details>
581
+ * <details><summary>other curves</summary><br>
705
582
 
706
- &nbsp;
707
-
708
- ## Hub
709
-
710
- > Integrate tightly with [GitHub](https://github.com) 🐙 and as a team
711
- ```sh
712
- $ dotenvx hub login
713
- $ dotenvx hub push
714
- ```
715
-
716
- **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).
717
586
 
718
587
  &nbsp;
719
588
 
@@ -768,6 +637,23 @@ This fix is easy. Replace `--env-file` with `-f`.
768
637
 
769
638
  [more context](https://github.com/dotenvx/dotenvx/issues/131)
770
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
+
771
657
  &nbsp;
772
658
 
773
659
  ## Contributing
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.37.0",
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
@@ -10,10 +10,11 @@ function status (directory) {
10
10
  logger.debug(`options: ${JSON.stringify(options)}`)
11
11
 
12
12
  try {
13
- const { changes, nochanges } = main.status(directory)
13
+ const { changes, nochanges, untracked } = main.status(directory)
14
14
 
15
15
  const changeFilenames = []
16
16
  const nochangeFilenames = []
17
+ const untrackedFilenames = []
17
18
 
18
19
  for (const row of nochanges) {
19
20
  nochangeFilenames.push(row.filename)
@@ -50,6 +51,15 @@ function status (directory) {
50
51
  logger.warn('no .env* files.')
51
52
  logger.help('? add one with [echo "HELLO=World" > .env] and then run [dotenvx status]')
52
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 encrypt ${directory}]`)
62
+ }
53
63
  } catch (error) {
54
64
  logger.error(error.message)
55
65
  if (error.help) {
@@ -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
@@ -16,6 +16,7 @@ class Status {
16
16
  this.directory = directory
17
17
  this.changes = []
18
18
  this.nochanges = []
19
+ this.untracked = [] // not tracked in .env.vault
19
20
  }
20
21
 
21
22
  run () {
@@ -60,7 +61,14 @@ class Status {
60
61
 
61
62
  // grab decrypted
62
63
  const { processedEnvs } = new Decrypt(this.directory, row.environment).run()
63
- row.decrypted = processedEnvs[0].decrypted
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
+ }
64
72
 
65
73
  // differences
66
74
  row.differences = diff.diffWords(row.decrypted, row.raw)
@@ -78,7 +86,8 @@ class Status {
78
86
 
79
87
  return {
80
88
  changes: this.changes,
81
- nochanges: this.nochanges
89
+ nochanges: this.nochanges,
90
+ untracked: this.untracked
82
91
  }
83
92
  }
84
93