@dotenvx/dotenvx 2.21.0 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.21.0...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.23.0...main)
6
+
7
+ ## [2.23.0](https://github.com/dotenvx/dotenvx/compare/v2.22.0...v2.23.0) (2026-08-31)
8
+
9
+ ### Added
10
+
11
+ * Support comma-separated env file paths with `-f` and the `config({ path })` option. Example: `dotenvx run -f .env.local,.env -- node index.js`
12
+ * Add `DOTENV_PATH` (or `DOTENV_F`) to support the same. Example: `DOTENV_PATH=.env.local,.env`
13
+
14
+ ### Changed
15
+
16
+ * Rename `DOTENV_CONFIG_*` variables to `DOTENV_CONVENTION`, `DOTENV_IGNORE`, and `DOTENV_QUIET`, while retaining their existing `DOTENV_CONFIG_*` names as fallbacks
17
+
18
+ ## [2.22.0](https://github.com/dotenvx/dotenvx/compare/v2.21.0...v2.22.0) (2026-08-27)
19
+
20
+ ### Added
21
+
22
+ * Install verified binaries directly from GitHub Releases ([#956](https://github.com/dotenvx/dotenvx/pull/956))
23
+
24
+ ### Changed
25
+
26
+ * Fall back to `~/.local/bin` when `/usr/local/bin` is not writable ([#955](https://github.com/dotenvx/dotenvx/pull/955))
6
27
 
7
28
  ## [2.21.0](https://github.com/dotenvx/dotenvx/compare/v2.20.1...v2.21.0) (2026-08-10)
8
29
 
package/README.md CHANGED
@@ -122,7 +122,7 @@ Hello World # with dotenvx
122
122
  > :-D
123
123
  ```
124
124
 
125
- see [quickstart guides](https://dotenvx.com/docs/quickstart)
125
+ see [quickstart guides](https://dotenvx.com/docs/quickstarts)
126
126
 
127
127
  More examples
128
128
 
@@ -186,7 +186,7 @@ $ dotenvx run --redact -- agent -p --force 'Run `dotenvx get HELLO` and echo bac
186
186
  Hello [REDACTED]
187
187
  ```
188
188
 
189
- see [Cursor redaction guide](https://dotenvx.com/docs/secrets-in-cursor)
189
+ see [Cursor redaction guide](https://dotenvx.com/docs/cursor)
190
190
 
191
191
  </details>
192
192
  <details><summary>1Password 🔐</summary><br>
@@ -199,7 +199,7 @@ $ dotenvx run -- sh -c 'echo Hello $HELLO'
199
199
  Hello World
200
200
  ```
201
201
 
202
- see [1Password guide](https://dotenvx.com/docs/secrets-in-1password)
202
+ see [1Password guide](https://dotenvx.com/docs/1password)
203
203
 
204
204
  </details>
205
205
  <details><summary>Bitwarden 🛡️</summary><br>
@@ -272,7 +272,7 @@ export async function GET() {
272
272
  }
273
273
  ```
274
274
 
275
- see [astro guide](https://dotenvx.com/docs/secrets-in-astro)
275
+ see [astro guide](https://dotenvx.com/docs/astro)
276
276
 
277
277
  </details>
278
278
  <details><summary>Expo 🧭</summary><br>
@@ -292,7 +292,7 @@ Preface Expo scripts with `dotenvx run --`.
292
292
  }
293
293
  ```
294
294
 
295
- see [expo guide](https://dotenvx.com/docs/secrets-in-expo)
295
+ see [expo guide](https://dotenvx.com/docs/expo)
296
296
 
297
297
  </details>
298
298
  <details><summary>Next.js ▲</summary><br>
@@ -413,7 +413,7 @@ $ dotenvx run -- python3 index.py
413
413
  Hello World
414
414
  ```
415
415
 
416
- see [extended python guide](https://dotenvx.com/docs/quickstart)
416
+ see [extended python guide](https://dotenvx.com/docs/python)
417
417
 
418
418
  </details>
419
419
  <details><summary>PHP 🐘</summary><br>
@@ -426,7 +426,7 @@ $ dotenvx run -- php index.php
426
426
  Hello World
427
427
  ```
428
428
 
429
- see [extended php guide](https://dotenvx.com/docs/quickstart)
429
+ see [extended php guide](https://dotenvx.com/docs/php)
430
430
 
431
431
  </details>
432
432
  <details><summary>Ruby 💎</summary><br>
@@ -439,7 +439,7 @@ $ dotenvx run -- ruby index.rb
439
439
  Hello World
440
440
  ```
441
441
 
442
- see [extended ruby guide](https://dotenvx.com/docs/quickstart)
442
+ see [extended ruby guide](https://dotenvx.com/docs/ruby)
443
443
 
444
444
  </details>
445
445
  <details><summary>Go 🐹</summary><br>
@@ -452,7 +452,7 @@ $ dotenvx run -- go run main.go
452
452
  Hello World
453
453
  ```
454
454
 
455
- see [extended go guide](https://dotenvx.com/docs/quickstart)
455
+ see [extended go guide](https://dotenvx.com/docs/go)
456
456
 
457
457
  </details>
458
458
  <details><summary>Rust 🦀</summary><br>
@@ -465,7 +465,7 @@ $ dotenvx run -- cargo run
465
465
  Hello World
466
466
  ```
467
467
 
468
- see [extended rust guide](https://dotenvx.com/docs/quickstart)
468
+ see [extended rust guide](https://dotenvx.com/docs/rust)
469
469
 
470
470
  </details>
471
471
  <details><summary>Java ☕️</summary><br>
@@ -552,7 +552,7 @@ $ dotenvx run -- bin/rails s
552
552
  $ dotenvx run -- php artisan serve
553
553
  ```
554
554
 
555
- see [framework guides](https://dotenvx.com/docs#frameworks)
555
+ see [framework guides](https://dotenvx.com/docs/quickstarts)
556
556
 
557
557
  </details>
558
558
  <details><summary>Docker 🐳</summary><br>
@@ -570,7 +570,7 @@ RUN curl -fsS https://dotenvx.sh/install.sh | sh
570
570
  CMD ["/usr/local/bin/dotenvx", "run", "--", "echo", "Hello $HELLO"]
571
571
  ```
572
572
 
573
- see [docker guide](https://dotenvx.com/docs/platforms/docker)
573
+ see [docker guide](https://dotenvx.com/docs/docker)
574
574
 
575
575
  </details>
576
576
  <details><summary>CI/CDs 🐙</summary><br>
@@ -592,7 +592,7 @@ jobs:
592
592
  DOTENV_KEY: ${{ secrets.DOTENV_KEY }}
593
593
  ```
594
594
 
595
- see [github actions guide](https://dotenvx.com/docs/cis/github-actions)
595
+ see [github actions guide](https://dotenvx.com/docs/github-actions)
596
596
 
597
597
  </details>
598
598
  <details><summary>Platforms</summary><br>
@@ -608,7 +608,7 @@ RUN curl -fsS https://dotenvx.sh | sh
608
608
  npm install @dotenvx/dotenvx --save
609
609
  ```
610
610
 
611
- see [platform guides](https://dotenvx.com/docs#platforms)
611
+ see [platform guides](https://dotenvx.com/docs/platforms)
612
612
 
613
613
  </details>
614
614
  <details><summary>Process Managers</summary><br>
@@ -620,7 +620,7 @@ see [platform guides](https://dotenvx.com/docs#platforms)
620
620
  },
621
621
  ```
622
622
 
623
- see [process manager guides](https://dotenvx.com/docs#process-managers)
623
+ see [process manager guides](https://dotenvx.com/docs/process-managers/pm2)
624
624
 
625
625
  </details>
626
626
  <details><summary>npx</summary><br>
@@ -814,10 +814,10 @@ $ dotenvx run -f .env.production --quiet -- node index.js
814
814
  Hello production
815
815
  ```
816
816
 
817
- You can also set `DOTENV_CONFIG_QUIET=true`.
817
+ You can also set `DOTENV_QUIET=true`.
818
818
 
819
819
  ```sh
820
- $ DOTENV_CONFIG_QUIET=true dotenvx run -f .env.production -- node index.js
820
+ $ DOTENV_QUIET=true dotenvx run -f .env.production -- node index.js
821
821
  Hello production
822
822
  ```
823
823
 
@@ -1530,10 +1530,10 @@ $ dotenvx run -f .env.production --quiet -- node index.js
1530
1530
  Hello production
1531
1531
  ```
1532
1532
 
1533
- You can also set `DOTENV_CONFIG_QUIET=true`.
1533
+ You can also set `DOTENV_QUIET=true`.
1534
1534
 
1535
1535
  ```sh
1536
- $ DOTENV_CONFIG_QUIET=true dotenvx run -f .env.production -- node index.js
1536
+ $ DOTENV_QUIET=true dotenvx run -f .env.production -- node index.js
1537
1537
  Hello production
1538
1538
  ```
1539
1539
 
@@ -1603,10 +1603,10 @@ $ dotenvx run -f .env.missing --ignore=MISSING_ENV_FILE -- node index.js
1603
1603
  ...
1604
1604
  ```
1605
1605
 
1606
- You can also set `DOTENV_CONFIG_IGNORE="MISSING_ENV_FILE"`. It is parsed as a comma-separated list.
1606
+ You can also set `DOTENV_IGNORE="MISSING_ENV_FILE"`. It is parsed as a comma-separated list.
1607
1607
 
1608
1608
  ```sh
1609
- $ DOTENV_CONFIG_IGNORE="MISSING_ENV_FILE,OTHER_ERROR_CODE" dotenvx run -f .env.missing -- node index.js
1609
+ $ DOTENV_IGNORE="MISSING_ENV_FILE,OTHER_ERROR_CODE" dotenvx run -f .env.missing -- node index.js
1610
1610
  ```
1611
1611
  </details>
1612
1612
  <details><summary>`run --convention=nextjs`</summary><br>
@@ -1625,10 +1625,10 @@ $ dotenvx run --convention=nextjs -- node index.js
1625
1625
  Hello development local
1626
1626
  ```
1627
1627
 
1628
- You can also set `DOTENV_CONFIG_CONVENTION=nextjs`.
1628
+ You can also set `DOTENV_CONVENTION=nextjs`.
1629
1629
 
1630
1630
  ```sh
1631
- $ DOTENV_CONFIG_CONVENTION=nextjs dotenvx run -- node index.js
1631
+ $ DOTENV_CONVENTION=nextjs dotenvx run -- node index.js
1632
1632
  [dotenvx@1.X.X] injecting env (1) from .env.development.local, .env.local, .env.development, .env
1633
1633
  Hello development local
1634
1634
  ```
@@ -1663,10 +1663,10 @@ $ NODE_ENV=development dotenvx run --convention=flow -- node index.js
1663
1663
  Hello development local
1664
1664
  ```
1665
1665
 
1666
- You can also set `DOTENV_CONFIG_CONVENTION=flow`.
1666
+ You can also set `DOTENV_CONVENTION=flow`.
1667
1667
 
1668
1668
  ```sh
1669
- $ NODE_ENV=development DOTENV_CONFIG_CONVENTION=flow dotenvx run -- node index.js
1669
+ $ NODE_ENV=development DOTENV_CONVENTION=flow dotenvx run -- node index.js
1670
1670
  [dotenvx@1.X.X] injecting env (1) from .env.development.local, .env.development, .env.local, .env
1671
1671
  Hello development local
1672
1672
  ```
@@ -1794,10 +1794,10 @@ Ignore multiple error codes by separating them with spaces.
1794
1794
  $ dotenvx get HELLO --ignore=MISSING_ENV_FILE MISSING_KEY
1795
1795
  ```
1796
1796
 
1797
- You can also set `DOTENV_CONFIG_IGNORE`. Its value is a comma-separated list.
1797
+ You can also set `DOTENV_IGNORE`. Its value is a comma-separated list.
1798
1798
 
1799
1799
  ```sh
1800
- $ DOTENV_CONFIG_IGNORE=MISSING_ENV_FILE,OTHER dotenvx get HELLO
1800
+ $ DOTENV_IGNORE=MISSING_ENV_FILE,OTHER dotenvx get HELLO
1801
1801
  ```
1802
1802
 
1803
1803
  </details>
@@ -1887,10 +1887,10 @@ $ dotenvx get HELLO --convention=nextjs
1887
1887
  development local
1888
1888
  ```
1889
1889
 
1890
- You can also set `DOTENV_CONFIG_CONVENTION=nextjs`.
1890
+ You can also set `DOTENV_CONVENTION=nextjs`.
1891
1891
 
1892
1892
  ```sh
1893
- $ DOTENV_CONFIG_CONVENTION=nextjs dotenvx get HELLO
1893
+ $ DOTENV_CONVENTION=nextjs dotenvx get HELLO
1894
1894
  development local
1895
1895
  ```
1896
1896
 
@@ -1920,10 +1920,10 @@ $ NODE_ENV=development dotenvx get HELLO --convention=flow
1920
1920
  development local
1921
1921
  ```
1922
1922
 
1923
- You can also set `DOTENV_CONFIG_CONVENTION=flow`.
1923
+ You can also set `DOTENV_CONVENTION=flow`.
1924
1924
 
1925
1925
  ```sh
1926
- $ NODE_ENV=development DOTENV_CONFIG_CONVENTION=flow dotenvx get HELLO
1926
+ $ NODE_ENV=development DOTENV_CONVENTION=flow dotenvx get HELLO
1927
1927
  development local
1928
1928
  ```
1929
1929
 
@@ -2395,7 +2395,7 @@ $ echo "HELLO=World" > .env
2395
2395
  $ dotenvx encrypt --stdout
2396
2396
  #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
2397
2397
  #/ public-key encryption for .env files /
2398
- #/ [how it works](https://dotenvx.com/encryption) /
2398
+ #/ [how it works](https://dotenvx.com/docs/quickstart/encryption) /
2399
2399
  #/----------------------------------------------------------/
2400
2400
  DOTENV_PUBLIC_KEY="034af93e93708b994c10f236c96ef88e47291066946cce2e8d98c9e02c741ced45"
2401
2401
  # .env
@@ -2527,7 +2527,7 @@ Decrypt the contents of an encrypted `.env` file and send to stdout.
2527
2527
  $ dotenvx decrypt --stdout
2528
2528
  #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
2529
2529
  #/ public-key encryption for .env files /
2530
- #/ [how it works](https://dotenvx.com/encryption) /
2530
+ #/ [how it works](https://dotenvx.com/docs/quickstart/encryption) /
2531
2531
  #/----------------------------------------------------------/
2532
2532
  DOTENV_PUBLIC_KEY="034af93e93708b994c10f236c96ef88e47291066946cce2e8d98c9e02c741ced45"
2533
2533
  # .env
@@ -2801,10 +2801,10 @@ Ignore multiple error codes by separating them with spaces.
2801
2801
  $ dotenvx validate --ignore=MISSING_ENV_FILE MISSING_ENV_EXAMPLE
2802
2802
  ```
2803
2803
 
2804
- You can also set `DOTENV_CONFIG_IGNORE`. Its value is a comma-separated list.
2804
+ You can also set `DOTENV_IGNORE`. Its value is a comma-separated list.
2805
2805
 
2806
2806
  ```sh
2807
- $ DOTENV_CONFIG_IGNORE=MISSING_ENV_FILE,OTHER dotenvx validate
2807
+ $ DOTENV_IGNORE=MISSING_ENV_FILE,OTHER dotenvx validate
2808
2808
  ```
2809
2809
 
2810
2810
  </details>
@@ -3413,10 +3413,10 @@ Error: [MISSING_ENV_FILE] missing .env.missing file (/path/to/.env.missing)
3413
3413
  Hello World
3414
3414
  ```
3415
3415
 
3416
- You can also set `DOTENV_CONFIG_QUIET=true`.
3416
+ You can also set `DOTENV_QUIET=true`.
3417
3417
 
3418
3418
  ```sh
3419
- $ DOTENV_CONFIG_QUIET=true node index.js
3419
+ $ DOTENV_QUIET=true node index.js
3420
3420
  Error: [MISSING_ENV_FILE] missing .env.missing file (/path/to/.env.missing)
3421
3421
  Hello World
3422
3422
  ```
@@ -3521,10 +3521,10 @@ This is equivalent to using `--convention=nextjs` with the CLI:
3521
3521
  $ dotenvx run --convention=nextjs -- node index.js
3522
3522
  ```
3523
3523
 
3524
- You can also set `DOTENV_CONFIG_CONVENTION=nextjs`.
3524
+ You can also set `DOTENV_CONVENTION=nextjs`.
3525
3525
 
3526
3526
  ```sh
3527
- $ DOTENV_CONFIG_CONVENTION=nextjs node index.js
3527
+ $ DOTENV_CONVENTION=nextjs node index.js
3528
3528
  ```
3529
3529
 
3530
3530
  </details>
@@ -3727,9 +3727,16 @@ There are global settings available that can be configured as environment variab
3727
3727
 
3728
3728
  ```ini
3729
3729
  # config
3730
- DOTENV_CONFIG_CONVENTION= # set to a default convention like 'nextjs' or 'flow'
3731
- DOTENV_CONFIG_IGNORE= # MISSING_ENV_FILE,OTHER
3732
- DOTENV_CONFIG_QUIET= # set to "true" to default to --quiet
3730
+ DOTENV_CONVENTION= # set to a default convention like 'nextjs' or 'flow'
3731
+ DOTENV_PATH= # path to your env file; comma-separate multiple paths
3732
+ DOTENV_F= # synonym for DOTENV_PATH
3733
+ DOTENV_IGNORE= # MISSING_ENV_FILE,OTHER
3734
+ DOTENV_QUIET= # set to "true" to default to --quiet
3735
+
3736
+ # legacy aliases (still supported)
3737
+ DOTENV_CONFIG_CONVENTION=
3738
+ DOTENV_CONFIG_IGNORE=
3739
+ DOTENV_CONFIG_QUIET=
3733
3740
 
3734
3741
  # armor
3735
3742
  DOTENVX_ARMOR_TOKEN= # for api calls
@@ -3804,22 +3811,22 @@ WRONG_PRIVATE_KEY= # the supplied private key cannot decrypt the value
3804
3811
  > Encrypt and run secrets in your language, framework, or tool.
3805
3812
  >
3806
3813
 
3807
- * [Claude](https://dotenvx.com/docs/secrets-in-claude)
3808
- * [Codex](https://dotenvx.com/docs/secrets-in-codex)
3809
- * [1Password](https://dotenvx.com/docs/secrets-in-1password)
3810
- * [Bitwarden](https://dotenvx.com/docs/secrets-in-bitwarden)
3811
- * [Node.js](https://dotenvx.com/docs/secrets-in-nodejs)
3812
- * [Next.js](https://dotenvx.com/docs/secrets-in-nextjs)
3813
- * [Express](https://dotenvx.com/docs/secrets-in-express)
3814
- * [Astro](https://dotenvx.com/docs/secrets-in-astro)
3815
- * [Expo](https://dotenvx.com/docs/secrets-in-expo)
3816
- * [Python](https://dotenvx.com/docs/secrets-in-python)
3817
- * [Ruby](https://dotenvx.com/docs/secrets-in-ruby)
3818
- * [Go](https://dotenvx.com/docs/secrets-in-go)
3819
- * [PHP](https://dotenvx.com/docs/secrets-in-php)
3820
- * [Rust](https://dotenvx.com/docs/secrets-in-rust)
3821
-
3822
- [Browse all quickstarts](https://dotenvx.com/docs)
3814
+ * [Claude](https://dotenvx.com/docs/claude)
3815
+ * [Codex](https://dotenvx.com/docs/codex)
3816
+ * [1Password](https://dotenvx.com/docs/1password)
3817
+ * [Bitwarden](https://dotenvx.com/docs/bitwarden)
3818
+ * [Node.js](https://dotenvx.com/docs/nodejs)
3819
+ * [Next.js](https://dotenvx.com/docs/nextjs)
3820
+ * [Express](https://dotenvx.com/docs/express)
3821
+ * [Astro](https://dotenvx.com/docs/astro)
3822
+ * [Expo](https://dotenvx.com/docs/expo)
3823
+ * [Python](https://dotenvx.com/docs/python)
3824
+ * [Ruby](https://dotenvx.com/docs/ruby)
3825
+ * [Go](https://dotenvx.com/docs/go)
3826
+ * [PHP](https://dotenvx.com/docs/php)
3827
+ * [Rust](https://dotenvx.com/docs/rust)
3828
+
3829
+ [Browse all quickstarts](https://dotenvx.com/docs/quickstarts)
3823
3830
 
3824
3831
  &nbsp;
3825
3832
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.21.0",
2
+ "version": "2.23.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -7,6 +7,7 @@ const Session = require('../../db/session')
7
7
  const getResolver = require('./../../lib/resolvers/get')
8
8
  const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
9
9
  const normalizeDotenvConfigIgnore = require('../../lib/helpers/normalizeDotenvConfigIgnore')
10
+ const normalizeDotenvConfigPath = require('../../lib/helpers/normalizeDotenvConfigPath')
10
11
  const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
11
12
  const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
12
13
  const mask = require('../../lib/helpers/mask')
@@ -26,7 +27,7 @@ async function get (key) {
26
27
  const ignore = options.ignore || []
27
28
  let errorCount = 0
28
29
 
29
- const envs = buildCommandEnvs(this.envs, options.convention)
30
+ const envs = buildCommandEnvs(normalizeDotenvConfigPath(this.envs), options.convention)
30
31
 
31
32
  try {
32
33
  const sesh = new Session()
@@ -9,6 +9,7 @@ const Session = require('../../db/session')
9
9
  const normalizeDotenvConfigQuiet = require('../../lib/helpers/normalizeDotenvConfigQuiet')
10
10
  const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
11
11
  const normalizeDotenvConfigIgnore = require('../../lib/helpers/normalizeDotenvConfigIgnore')
12
+ const normalizeDotenvConfigPath = require('../../lib/helpers/normalizeDotenvConfigPath')
12
13
  const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
13
14
  const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
14
15
  const mask = require('../../lib/helpers/mask')
@@ -106,7 +107,7 @@ async function run () {
106
107
  }
107
108
 
108
109
  try {
109
- let envs = buildCommandEnvs(this.envs, options.convention)
110
+ let envs = buildCommandEnvs(normalizeDotenvConfigPath(this.envs), options.convention)
110
111
  envs = determine(envs, process.env)
111
112
 
112
113
  const {
@@ -16,8 +16,15 @@ const normalizeDotenvConfigQuiet = require('./../lib/helpers/normalizeDotenvConf
16
16
  const envs = []
17
17
  function collectEnvs (type) {
18
18
  return function (value, previous) {
19
- envs.push({ type, value })
20
- return (previous || []).concat([value])
19
+ const values = type === 'envFile'
20
+ ? value.split(',').map(item => item.trim()).filter(Boolean)
21
+ : [value]
22
+
23
+ for (const item of values) {
24
+ envs.push({ type, value: item })
25
+ }
26
+
27
+ return (previous || []).concat(values)
21
28
  }
22
29
  }
23
30
 
@@ -331,11 +338,13 @@ program.command('help [command]')
331
338
  }
332
339
  })
333
340
 
334
- // dotenvx armor
341
+ // security sections (hidden commands advertised here)
335
342
  program.addHelpText('after', ' ')
336
- program.addHelpText('after', 'Professional Security: ')
343
+ program.addHelpText('after', 'Better Security:')
337
344
  program.addHelpText('after', ' lock ⊡ lock private keys with a local passphrase')
338
345
  program.addHelpText('after', ' native ⌥ move private keys into your OS secret store')
346
+ program.addHelpText('after', ' ')
347
+ program.addHelpText('after', 'For Security Teams:')
339
348
  program.addHelpText('after', ' armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
340
349
  program.addHelpText('after', ' curl ⛨ call authenticated api Dotenvx Armor [www.dotenvx.com/armor]')
341
350
 
package/src/cli/help.js CHANGED
@@ -108,30 +108,62 @@ Endpoints:
108
108
  GET /api/account
109
109
  GET /api/armor/keypairs
110
110
  GET /api/armor/keypairs/:public_key
111
+ POST /api/armor/keypairs/:public_key/name
112
+ POST /api/armor/keypairs/:public_key/members/:member_id/grant
113
+ POST /api/armor/keypairs/:public_key/members/:member_id/revoke
111
114
  POST /api/armor/keypairs/:public_key/settings/guard
112
115
  POST /api/armor/keypairs/:public_key/settings/enclave
113
116
  GET /api/teams
114
117
  GET /api/teams/:team
115
118
  GET /api/teams/:team/members
119
+ POST /api/teams/:team/members/:member_id/keypairs/:public_key/grant
120
+ POST /api/teams/:team/members/:member_id/keypairs/:public_key/revoke
116
121
  GET /api/teams/:team/invitations
117
122
  POST /api/teams/:team/invitations
118
- DELETE /api/teams/:team/invitations/:id
123
+ POST /api/teams/:team/invitations/:id/cancel
124
+ GET /api/join_requests
125
+ POST /api/join_requests
126
+ POST /api/join_requests/:id/cancel
127
+ GET /api/teams/:team/join_requests
128
+ POST /api/teams/:team/join_requests/:id/accept
129
+ POST /api/teams/:team/join_requests/:id/decline
130
+ POST /api/armor/:team/settings/join_requests
119
131
  GET /api/logs?team=:team
120
132
 
121
133
  Examples:
122
134
 
123
135
  dotenvx curl "https://armor.dotenvx.com/api/account"
124
136
  dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs"
137
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs?page=1&per=100"
138
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs?sort=name_asc"
139
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs?sort=public_key_desc"
125
140
  dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY"
141
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/name" --data '{"name":"Production"}'
142
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/name" --data '{"name":null}'
143
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/members/123/grant" --request POST
144
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/members/123/revoke" --request POST
126
145
  dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/settings/guard" --data '{"value":true}'
146
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/settings/guard" --data '{"value":false}'
127
147
  dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/settings/enclave" --data '{"value":true}'
148
+ dotenvx curl "https://armor.dotenvx.com/api/armor/keypairs/PUBLIC_KEY/settings/enclave" --data '{"value":false}'
128
149
  dotenvx curl "https://armor.dotenvx.com/api/teams"
129
150
  dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM"
130
151
  dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/members"
131
- dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/members"
152
+ dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/members/123/keypairs/PUBLIC_KEY/grant" --request POST
153
+ dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/members/123/keypairs/PUBLIC_KEY/revoke" --request POST
154
+ dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/invitations"
132
155
  dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/invitations" --data '{"email":"person@example.com","role":"member"}'
133
- dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/invitations/123" --request DELETE
156
+ dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/invitations/123/cancel" --request POST
157
+ dotenvx curl "https://armor.dotenvx.com/api/join_requests"
158
+ dotenvx curl "https://armor.dotenvx.com/api/join_requests" --data '{"team":"TEAM"}'
159
+ dotenvx curl "https://armor.dotenvx.com/api/join_requests/123/cancel" --request POST
160
+ dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/join_requests"
161
+ dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/join_requests/123/accept" --data '{"role":"member"}'
162
+ dotenvx curl "https://armor.dotenvx.com/api/teams/TEAM/join_requests/123/decline" --request POST
163
+ dotenvx curl "https://armor.dotenvx.com/api/armor/TEAM/settings/join_requests" --data '{"value":true}'
134
164
  dotenvx curl "https://armor.dotenvx.com/api/logs?team=TEAM"
165
+ dotenvx curl "https://armor.dotenvx.com/api/logs?team=TEAM&action=keypair/access&actor=USERNAME&result=success&resource=keypair/KEYPAIR_ID&page=1&per=100"
166
+ dotenvx curl "https://armor.dotenvx.com/api/logs?team=TEAM&actor=missing&resource=missing"
135
167
  `
136
168
  }
137
169
 
@@ -5,7 +5,11 @@ function dotenvOptionPaths (options) {
5
5
 
6
6
  if (options && options.path) {
7
7
  if (!Array.isArray(options.path)) {
8
- optionPaths = [resolveHome(options.path)]
8
+ if (typeof options.path === 'string') {
9
+ optionPaths = options.path.split(',').map(filepath => filepath.trim()).filter(Boolean).map(resolveHome)
10
+ } else {
11
+ optionPaths = [resolveHome(options.path)]
12
+ }
9
13
  } else {
10
14
  optionPaths = [] // reset default
11
15
 
@@ -0,0 +1,10 @@
1
+ const TEAM_REQUIRED_CODES = new Set([
2
+ 'TEAM_REQUIRED',
3
+ 'DOTENVX_TEAM_REQUIRED'
4
+ ])
5
+
6
+ function isTeamRequiredError (error) {
7
+ return Boolean(error && TEAM_REQUIRED_CODES.has(error.code))
8
+ }
9
+
10
+ module.exports = isTeamRequiredError
@@ -1,8 +1,10 @@
1
1
  function normalizeDotenvConfigConvention (
2
2
  /** @type {import('../main').DotenvConfigOptions} */ options
3
3
  ) {
4
- if (!options.convention && process.env.DOTENV_CONFIG_CONVENTION) {
5
- options.convention = process.env.DOTENV_CONFIG_CONVENTION
4
+ const convention = process.env.DOTENV_CONVENTION || process.env.DOTENV_CONFIG_CONVENTION
5
+
6
+ if (!options.convention && convention) {
7
+ options.convention = convention
6
8
  }
7
9
 
8
10
  return options
@@ -1,7 +1,8 @@
1
1
  function normalizeDotenvConfigIgnore (
2
2
  /** @type {import('../main').DotenvConfigOptions} */ options
3
3
  ) {
4
- const configIgnore = (process.env.DOTENV_CONFIG_IGNORE || '')
4
+ const configuredIgnore = process.env.DOTENV_IGNORE ?? process.env.DOTENV_CONFIG_IGNORE ?? ''
5
+ const configIgnore = configuredIgnore
5
6
  .split(',')
6
7
  .map(code => code.trim())
7
8
  .filter(Boolean)
@@ -0,0 +1,14 @@
1
+ const TYPE_ENV_FILE = 'envFile'
2
+
3
+ function normalizeDotenvConfigPath (envs = [], processEnv = process.env) {
4
+ if (envs.some(env => env.type === TYPE_ENV_FILE)) return envs
5
+
6
+ const configuredEnvFiles = processEnv.DOTENV_PATH || processEnv.DOTENV_F
7
+ if (!configuredEnvFiles) return envs
8
+
9
+ const envFiles = configuredEnvFiles.split(',').map(value => value.trim()).filter(Boolean)
10
+
11
+ return envFiles.map(value => ({ type: TYPE_ENV_FILE, value })).concat(envs)
12
+ }
13
+
14
+ module.exports = normalizeDotenvConfigPath
@@ -1,7 +1,9 @@
1
1
  function normalizeDotenvConfigQuiet (
2
2
  /** @type {import('../main').DotenvConfigOptions} */ options
3
3
  ) {
4
- if (process.env.DOTENV_CONFIG_QUIET === 'true') {
4
+ const quiet = process.env.DOTENV_QUIET ?? process.env.DOTENV_CONFIG_QUIET
5
+
6
+ if (quiet === 'true') {
5
7
  options.quiet = true
6
8
  }
7
9
 
@@ -1,6 +1,4 @@
1
1
  const { parse, parseSync } = require('@dotenvx/primitives')
2
- const isNetworkError = require('./isNetworkError')
3
-
4
2
  const SERVER_SIDE_DECRYPTION_REQUIRED = 'SERVER_SIDE_DECRYPTION_REQUIRED'
5
3
 
6
4
  function decryptOptions (error) {
@@ -21,23 +19,30 @@ function parseOptionsWithoutProvider (options) {
21
19
  }
22
20
  }
23
21
 
22
+ function failedKeyAccessFallback (result, error) {
23
+ return {
24
+ ...result,
25
+ errors: [error, ...(result.errors || [])]
26
+ }
27
+ }
28
+
24
29
  async function parseWithDecryptor (src, options = {}) {
25
30
  try {
26
31
  return await parse(src, options)
27
32
  } catch (error) {
28
33
  if (error.code !== SERVER_SIDE_DECRYPTION_REQUIRED || typeof options.decryptor !== 'function') {
29
- throw error
34
+ if (typeof options.provider !== 'function') throw error
35
+
36
+ const result = await parse(src, parseOptionsWithoutProvider(options))
37
+ return failedKeyAccessFallback(result, error)
30
38
  }
31
39
 
32
40
  try {
33
41
  const result = await options.decryptor(src, decryptOptions(error))
34
42
  return await parse(result.src, parseOptionsWithoutProvider(options))
35
43
  } catch (decryptorError) {
36
- if (isNetworkError(decryptorError)) {
37
- return await parse(src, parseOptionsWithoutProvider(options))
38
- }
39
-
40
- throw decryptorError
44
+ const result = await parse(src, parseOptionsWithoutProvider(options))
45
+ return failedKeyAccessFallback(result, decryptorError)
41
46
  }
42
47
  }
43
48
  }
@@ -47,18 +52,18 @@ parseWithDecryptor.sync = function parseWithDecryptorSync (src, options = {}) {
47
52
  return parseSync(src, options)
48
53
  } catch (error) {
49
54
  if (error.code !== SERVER_SIDE_DECRYPTION_REQUIRED || typeof options.decryptor !== 'function') {
50
- throw error
55
+ if (typeof options.provider !== 'function') throw error
56
+
57
+ const result = parseSync(src, parseOptionsWithoutProvider(options))
58
+ return failedKeyAccessFallback(result, error)
51
59
  }
52
60
 
53
61
  try {
54
62
  const result = options.decryptor(src, decryptOptions(error))
55
63
  return parseSync(result.src, parseOptionsWithoutProvider(options))
56
64
  } catch (decryptorError) {
57
- if (isNetworkError(decryptorError)) {
58
- return parseSync(src, parseOptionsWithoutProvider(options))
59
- }
60
-
61
- throw decryptorError
65
+ const result = parseSync(src, parseOptionsWithoutProvider(options))
66
+ return failedKeyAccessFallback(result, decryptorError)
62
67
  }
63
68
  }
64
69
  }
@@ -1,10 +1,27 @@
1
- // Remove [options] from help text. example:
1
+ // Remove [options] from usage/command lines and hide the Options: section.
2
+
3
+ function removeOptionsSection (lines) {
4
+ const start = lines.indexOf('Options:')
5
+ if (start === -1) return
6
+
7
+ let end = start + 1
8
+ while (end < lines.length && lines[end] !== '' && !lines[end].endsWith(':')) {
9
+ end++
10
+ }
11
+
12
+ // drop trailing blank line after the Options block when present
13
+ if (lines[end] === '') end++
14
+
15
+ lines.splice(start, end - start)
16
+ }
2
17
 
3
18
  function removeOptionsHelpParts (lines) {
4
19
  for (let i = 0; i < lines.length; i++) {
5
20
  lines[i] = lines[i].replace(' [options]', '')
6
21
  }
7
22
 
23
+ removeOptionsSection(lines)
24
+
8
25
  let commandsStart = -1
9
26
  for (let i = 0; i < lines.length; i++) {
10
27
  if (lines[i] === 'Commands:') {
package/src/lib/main.js CHANGED
@@ -27,6 +27,7 @@ const Errors = require('./helpers/errors')
27
27
  const normalizeDotenvConfigQuiet = require('./helpers/normalizeDotenvConfigQuiet')
28
28
  const normalizeDotenvConfigConvention = require('./helpers/normalizeDotenvConfigConvention')
29
29
  const normalizeDotenvConfigIgnore = require('./helpers/normalizeDotenvConfigIgnore')
30
+ const normalizeDotenvConfigPath = require('./helpers/normalizeDotenvConfigPath')
30
31
  const mask = require('./helpers/mask')
31
32
  const maskProcessedEnvs = require('./helpers/maskProcessedEnvs')
32
33
 
@@ -75,7 +76,7 @@ const config = function (options = {}) {
75
76
  const noKeychain = resolveNoKeychain(options)
76
77
 
77
78
  try {
78
- let envs = buildConfigEnvs(options)
79
+ let envs = normalizeDotenvConfigPath(buildConfigEnvs(options))
79
80
  if (!options.envs) {
80
81
  envs = determine(envs, processEnv)
81
82
  }
@@ -32,6 +32,8 @@ function unresolvedEncryptedErrors (parsed) {
32
32
 
33
33
  function decryptErrors (parsed, errors) {
34
34
  const mappedErrors = (errors || []).map(error => {
35
+ if (error instanceof Error) return error
36
+
35
37
  return new Errors({
36
38
  code: error.code,
37
39
  message: error.message,
@@ -4,6 +4,7 @@ const keynames = require('../conventions/keynames')
4
4
  const upsertEnvKey = require('../helpers/upsertEnvKey')
5
5
  const readEnvKey = require('../helpers/readEnvKey')
6
6
  const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
7
+ const isTeamRequiredError = require('../helpers/isTeamRequiredError')
7
8
 
8
9
  class ArmorDown {
9
10
  constructor (hostname, token, devicePublicKey, envFile = '.env', team = undefined) {
@@ -35,7 +36,7 @@ class ArmorDown {
35
36
  try {
36
37
  json = await new PostArmorDown(hostname, token, devicePublicKey, publicKey, undefined).run()
37
38
  } catch (error) {
38
- if (error.code !== 'DOTENVX_TEAM_REQUIRED') {
39
+ if (!isTeamRequiredError(error)) {
39
40
  throw error
40
41
  }
41
42
 
@@ -4,6 +4,7 @@ const upsertEnvKey = require('../helpers/upsertEnvKey')
4
4
  const keynames = require('../conventions/keynames')
5
5
  const readEnvKey = require('../helpers/readEnvKey')
6
6
  const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
7
+ const isTeamRequiredError = require('../helpers/isTeamRequiredError')
7
8
 
8
9
  class ArmorPull {
9
10
  constructor (hostname, token, devicePublicKey, envFile = '.env', team = undefined) {
@@ -35,7 +36,7 @@ class ArmorPull {
35
36
  try {
36
37
  json = await new PostArmorPull(hostname, token, devicePublicKey, publicKey, undefined).run()
37
38
  } catch (error) {
38
- if (error.code !== 'DOTENVX_TEAM_REQUIRED') {
39
+ if (!isTeamRequiredError(error)) {
39
40
  throw error
40
41
  }
41
42
 
@@ -4,6 +4,7 @@ const PostArmorPush = require('../api/postArmorPush')
4
4
  const keynames = require('../conventions/keynames')
5
5
  const readEnvKey = require('../helpers/readEnvKey')
6
6
  const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
7
+ const isTeamRequiredError = require('../helpers/isTeamRequiredError')
7
8
 
8
9
  function publicKeyFromPrivateKey (privateKey) {
9
10
  try {
@@ -42,7 +43,7 @@ class ArmorPush {
42
43
  try {
43
44
  json = await new PostArmorPush(hostname, token, devicePublicKey, privateKey, undefined).run()
44
45
  } catch (error) {
45
- if (error.code !== 'DOTENVX_TEAM_REQUIRED') {
46
+ if (!isTeamRequiredError(error)) {
46
47
  throw error
47
48
  }
48
49
 
@@ -4,6 +4,7 @@ const keynames = require('../conventions/keynames')
4
4
  const removeEnvKey = require('../helpers/removeEnvKey')
5
5
  const readEnvKey = require('../helpers/readEnvKey')
6
6
  const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
7
+ const isTeamRequiredError = require('../helpers/isTeamRequiredError')
7
8
 
8
9
  class ArmorUp {
9
10
  constructor (hostname, token, devicePublicKey, envFile = '.env', team = undefined) {
@@ -37,7 +38,7 @@ class ArmorUp {
37
38
  try {
38
39
  json = await new PostArmorUp(hostname, token, devicePublicKey, publicKey, privateKey, undefined).run()
39
40
  } catch (error) {
40
- if (error.code !== 'DOTENVX_TEAM_REQUIRED') {
41
+ if (!isTeamRequiredError(error)) {
41
42
  throw error
42
43
  }
43
44
 
@@ -13,6 +13,7 @@ const keynames = require('../conventions/keynames')
13
13
  const PostArmorUp = require('../api/postArmorUp')
14
14
  const prompts = require('../helpers/prompts')
15
15
  const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
16
+ const isTeamRequiredError = require('../helpers/isTeamRequiredError')
16
17
  const Session = require('../../db/session')
17
18
 
18
19
  async function selectKeyStorage () {
@@ -110,7 +111,7 @@ async function encryptTransform (options = {}) {
110
111
  try {
111
112
  await new PostArmorUp(hostname, token, devicePublicKey, publicKey, privateKey, undefined).run()
112
113
  } catch (error) {
113
- if (error.code !== 'DOTENVX_TEAM_REQUIRED') {
114
+ if (!isTeamRequiredError(error)) {
114
115
  throw error
115
116
  }
116
117
 
@@ -13,6 +13,7 @@ const Errors = require('../helpers/errors')
13
13
  const PostArmorUp = require('../api/postArmorUp')
14
14
  const prompts = require('../helpers/prompts')
15
15
  const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
16
+ const isTeamRequiredError = require('../helpers/isTeamRequiredError')
16
17
  const Session = require('../../db/session')
17
18
 
18
19
  async function selectKeyStorage () {
@@ -113,7 +114,7 @@ async function setTransform (options = {}) {
113
114
  try {
114
115
  await new PostArmorUp(hostname, token, devicePublicKey, publicKey, privateKey, undefined).run()
115
116
  } catch (error) {
116
- if (error.code !== 'DOTENVX_TEAM_REQUIRED') {
117
+ if (!isTeamRequiredError(error)) {
117
118
  throw error
118
119
  }
119
120