@dotenvx/dotenvx 2.26.1 → 2.28.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +48 -40
  3. package/package.json +12 -2
  4. package/src/cli/actions/decrypt.js +3 -3
  5. package/src/cli/actions/encrypt.js +3 -3
  6. package/src/cli/actions/get.js +2 -2
  7. package/src/cli/actions/keypair.js +1 -1
  8. package/src/cli/actions/run.js +46 -42
  9. package/src/cli/actions/set.js +2 -2
  10. package/src/cli/actions/validate.js +21 -33
  11. package/src/cli/dotenvx.js +2 -2
  12. package/src/lib/grammars/envfile.peggy +98 -0
  13. package/src/lib/helpers/encryptedSources.js +15 -0
  14. package/src/lib/helpers/envfileParser.js +2731 -0
  15. package/src/lib/helpers/errors.js +16 -16
  16. package/src/lib/helpers/executeCommand.js +6 -2
  17. package/src/lib/helpers/formatEnvfileSyntaxError.js +24 -0
  18. package/src/lib/helpers/isValidEmail.js +11 -0
  19. package/src/lib/helpers/isValidUrl.js +9 -0
  20. package/src/lib/helpers/parseWithDecryptor.js +9 -1
  21. package/src/lib/helpers/prompts.js +2 -1
  22. package/src/lib/helpers/readEnvfile.js +128 -0
  23. package/src/lib/helpers/selectKeyStorage.js +19 -13
  24. package/src/lib/helpers/validate.js +83 -14
  25. package/src/lib/helpers/validateEnvfile.js +24 -0
  26. package/src/lib/main.js +7 -7
  27. package/src/lib/providers/index.js +1 -1
  28. package/src/lib/proxy/configureProxy.js +55 -0
  29. package/src/lib/proxy/eligibleHeaders.js +16 -0
  30. package/src/lib/proxy/prepareProxy.js +24 -0
  31. package/src/lib/proxy/proxyCertificates.js +44 -0
  32. package/src/lib/proxy/proxyForward.js +65 -0
  33. package/src/lib/proxy/proxyPreload.js +21 -0
  34. package/src/lib/proxy/proxyPreloadSource.js +3 -0
  35. package/src/lib/proxy/proxyServer.js +175 -0
  36. package/src/lib/resolvers/envs.js +15 -5
  37. package/src/lib/resolvers/get.js +1 -1
  38. package/src/lib/services/validate.js +58 -0
  39. package/src/lib/transforms/set.js +2 -2
  40. package/src/lib/helpers/validateEnvExample.js +0 -24
package/CHANGELOG.md CHANGED
@@ -2,7 +2,30 @@
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.26.1...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.28.0...main)
6
+
7
+ ## [2.28.0](https://github.com/dotenvx/dotenvx/compare/v2.27.0...v2.28.0) (2026-09-16)
8
+
9
+ ### Added
10
+
11
+ * Choose where you custody your keys ([#972](https://github.com/dotenvx/dotenvx/pull/972))
12
+
13
+ ```text
14
+ Your private key
15
+ ├── ⛉ Local Custody
16
+ │ ├── OS (macOS Keychain / Windows Credential Manager / Linux Secret Service)
17
+ │ ├── 1Password
18
+ │ ├── Bitwarden
19
+ │ └── File (.env.keys)
20
+ └── ⛊ Managed Custody
21
+ └── ⛨ Armor
22
+ ```
23
+
24
+ ## [2.27.0](https://github.com/dotenvx/dotenvx/compare/v2.26.1...v2.27.0) (2026-09-16)
25
+
26
+ ### Removed
27
+
28
+ * BREAKING: Removed `run --validate` flag and `validate` command against .env.example file. To be replaced soon with a modern validation scheme.
6
29
 
7
30
  ## [2.26.1](https://github.com/dotenvx/dotenvx/compare/v2.26.0...v2.26.1) (2026-09-15)
8
31
 
package/README.md CHANGED
@@ -1565,29 +1565,44 @@ Hello production
1565
1565
  Available log levels are `error, warn, info, verbose, debug, silly` ([source](https://docs.npmjs.com/cli/v8/using-npm/logging#setting-log-levels))
1566
1566
 
1567
1567
  </details>
1568
- <details><summary>`run --validate`</summary><br>
1568
+ <details><summary>`run` with an Envfile</summary><br>
1569
1569
 
1570
- Validate your environment against `.env.example`.
1570
+ When an `Envfile` is present, `run` automatically validates the resolved environment before starting your command. No validation flag is needed.
1571
1571
 
1572
- ```ini
1573
- # .env.example
1574
- DATABASE_URL=
1575
- API_KEY=
1576
- SENTRY_DSN= # optional
1572
+ ```ruby
1573
+ # Envfile
1574
+ env "DATABASE_URL", type: "url"
1575
+ env "API_KEY"
1576
+ env "SENTRY_DSN", optional: true
1577
1577
  ```
1578
1578
 
1579
1579
  ```sh
1580
- $ dotenvx run --validate -- node index.js
1581
- [VALIDATION_FAILED] missing required (DATABASE_URL, API_KEY). fix: [https://github.com/dotenvx/dotenvx/issues/907]
1580
+ $ dotenvx run -- node index.js
1581
+ [INVALID_ENV] DATABASE_URL is required; API_KEY is required
1582
1582
  ```
1583
1583
 
1584
- Validation errors are reported without stopping your command. Combine `--validate` with `--strict` to exit with code `1` before the command runs.
1584
+ Envfile validation failures stop the command. Other loading errors require `--strict` to stop execution.
1585
1585
 
1586
- ```sh
1587
- $ dotenvx run --validate --strict -- node index.js
1586
+ Use exact file blocks to override rules for selected files:
1587
+
1588
+ ```ruby
1589
+ env "HELLO"
1590
+ env "PORT", type: "port"
1591
+ env "STRIPE_SECRET_KEY", optional: true
1592
+
1593
+ file ".env.production" do
1594
+ encrypted true
1595
+ env "STRIPE_SECRET_KEY", required: true
1596
+ env "PORT", min: 1024
1597
+ end
1588
1598
  ```
1589
1599
 
1590
- Any inline comment containing the word `optional` marks that key as optional. If `.env.example` is missing, dotenvx reports `MISSING_ENV_EXAMPLE`. An empty `.env.example` is valid and declares no required variables.
1600
+ Both `dotenvx run -f .env.production -- node index.js` and `dotenvx validate -f .env.production` use the production rules. Block declarations inherit top-level options and override only the options they specify. A block's `encrypted` directive applies to all inherited and newly declared variables; a per-variable `encrypted:` option inside that block overrides it.
1601
+
1602
+ Paths in file blocks are relative to the Envfile. They match the selected paths exactly after path normalization (`./.env.production` matches `.env.production`); they are not basename matches or globs. Directory inputs and `DOTENV_PATH` use their resolved file paths.
1603
+
1604
+ When no file block matches, top-level rules apply. When one or more blocks match, each matching block's inherited rules must hold for the final resolved environment. Shell values, fallback files, and `--overload` cannot bypass them. A selected missing file still activates its block. Multiple matching blocks cannot cancel each other's restrictions; conflicting proxy domains are rejected. Blocks cannot be nested, and duplicate declarations within one scope or duplicate file blocks are errors.
1605
+
1591
1606
 
1592
1607
  </details>
1593
1608
  <details><summary>`run --strict`</summary><br>
@@ -2778,46 +2793,38 @@ $ dotenvx ls --json > dotenv-files.json
2778
2793
  </details>
2779
2794
  <details><summary>`validate`</summary><br>
2780
2795
 
2781
- Validate `.env` file(s) against `.env.example` without running a command.
2796
+ Validate resolved `.env` values against an `Envfile` without running a command. An `Envfile` in the current directory is required; `.env.example` is not used for validation.
2782
2797
 
2783
- ```ini
2784
- # .env.example
2785
- DATABASE_URL=
2786
- API_KEY=
2787
- SENTRY_DSN= # optional
2798
+ ```ruby
2799
+ # Envfile
2800
+ env "DATABASE_URL", type: "url"
2801
+ env "PORT", type: "port"
2802
+ env "SENTRY_DSN", optional: true
2788
2803
  ```
2789
2804
 
2790
2805
  ```sh
2791
2806
  $ dotenvx validate
2792
- [VALIDATION_FAILED] missing required (DATABASE_URL, API_KEY). fix: [https://github.com/dotenvx/dotenvx/issues/907]
2793
- ```
2794
-
2795
- Use `-f` and `-fk` to validate a specific env file and keys file. The command exits with code `1` when validation fails and prints errors to stderr. On success, it exits with code `0`.
2807
+ [INVALID_ENV] DATABASE_URL is required; PORT is required
2796
2808
 
2797
- ```sh
2798
2809
  $ dotenvx validate -f .env.production -fk .env.keys
2799
2810
  ```
2800
2811
 
2801
- </details>
2802
- <details><summary>`validate --ignore`</summary><br>
2812
+ The command enforces required values, types, enums, bounds, and encryption requirements. It exits with code `1` on validation or other loading errors. Missing env files are reported but do not fail validation when the resolved values satisfy Envfile; use `--strict` to make missing files fatal too. On success, it prints `▣ valid (.env)` (listing the loaded input files) and exits with code `0` on success. It does not change your shell's environment.
2803
2813
 
2804
- Ignore specific validation error codes.
2814
+ A missing Envfile reports `ENVFILE_REQUIRED`; invalid syntax reports `MALFORMED_ENVFILE`.
2805
2815
 
2806
- ```sh
2807
- $ dotenvx validate --ignore=MISSING_ENV_EXAMPLE
2808
- ```
2816
+ </details>
2817
+ <details><summary>`validate --ignore`</summary><br>
2809
2818
 
2810
- Ignore multiple error codes by separating them with spaces.
2819
+ Ignore specific loading or value-validation errors:
2811
2820
 
2812
2821
  ```sh
2813
- $ dotenvx validate --ignore=MISSING_ENV_FILE MISSING_ENV_EXAMPLE
2822
+ $ dotenvx validate --ignore=MISSING_ENV_FILE
2823
+ $ dotenvx validate --ignore=MISSING_ENV_FILE INVALID_ENV
2824
+ $ DOTENV_IGNORE=MISSING_ENV_FILE dotenvx validate
2814
2825
  ```
2815
2826
 
2816
- You can also set `DOTENV_IGNORE`. Its value is a comma-separated list.
2817
-
2818
- ```sh
2819
- $ DOTENV_IGNORE=MISSING_ENV_FILE,OTHER dotenvx validate
2820
- ```
2827
+ An Envfile is still required, even when errors are ignored.
2821
2828
 
2822
2829
  </details>
2823
2830
  <details><summary>`genexample`</summary><br>
@@ -3784,7 +3791,7 @@ INVALID_PUBLIC_KEY= # a public key is malformed or otherwise invalid
3784
3791
  MALFORMED_ENCRYPTED_DATA= # the encrypted value is malformed
3785
3792
  MISPAIRED_PRIVATE_KEY= # a private key does not match the existing public key
3786
3793
  MISSING_DIRECTORY= # the requested directory does not exist
3787
- MISSING_ENV_EXAMPLE= # the required .env.example file does not exist
3794
+ ENVFILE_REQUIRED= # the required Envfile does not exist
3788
3795
  MISSING_ENV_FILE= # a requested environment file does not exist
3789
3796
  MISSING_ENV_FILES= # no .env* files were found
3790
3797
  MISSING_ENV_KEYS_FILE= # the requested .env.keys file does not exist
@@ -3792,11 +3799,12 @@ MISSING_KEY= # a requested environment key does not exist
3792
3799
  MISSING_LOG_LEVEL= # the requested log level is not supported
3793
3800
  MISSING_PRIVATE_KEY= # the private key required to decrypt a value is missing
3794
3801
  MISSING_PUBLIC_KEY= # the public key required to encrypt a value is missing
3795
- MISSING_REQUIRED= # validation detail for which required variables are missing; surfaced by VALIDATION_FAILED
3802
+ MISSING_REQUIRED= # validation detail for which required variables are missing; surfaced by INVALID_ENV
3796
3803
  MISSING_VALUE= # no value was supplied for a key
3797
3804
  NOT_FOUND= # a private key was not found in the native secret store
3798
3805
  PRECOMMIT_HOOK_MODIFY_FAILED= # dotenvx could not update the pre-commit hook
3799
- VALIDATION_FAILED= # one or more required variables declared by .env.example are missing
3806
+ INVALID_ENV= # resolved values do not satisfy Envfile rules
3807
+ MALFORMED_ENVFILE= # Envfile syntax or configuration is invalid
3800
3808
  WRONG_PRIVATE_KEY= # the supplied private key cannot decrypt the value
3801
3809
  ```
3802
3810
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.26.1",
2
+ "version": "2.28.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -49,18 +49,23 @@
49
49
  "testshell": "shellspec --shell /bin/bash",
50
50
  "release:check": "npm test && npm run testshell",
51
51
  "prerelease": "npm run release:check",
52
- "release": "npm run release:check && standard-version"
52
+ "release": "npm run release:check && standard-version",
53
+ "build:envfile": "peggy --format commonjs -o src/lib/helpers/envfileParser.js src/lib/grammars/envfile.peggy"
53
54
  },
54
55
  "funding": "https://dotenvx.com",
55
56
  "dependencies": {
56
57
  "@dotenvx/primitives": "^3.0.2",
57
58
  "@dotenvx/tooling": "^1.0.3",
59
+ "global-agent": "^3.0.0",
60
+ "node-forge": "^1.4.0",
61
+ "undici": "^6.28.1",
58
62
  "yocto-spinner": "^1.2.1"
59
63
  },
60
64
  "devDependencies": {
61
65
  "@yao-pkg/pkg": "^6.12.0",
62
66
  "capture-console": "^1.0.2",
63
67
  "esbuild": "^0.28.1",
68
+ "peggy": "^5.1.0",
64
69
  "proxyquire": "^2.1.3",
65
70
  "sinon": "^14.0.1",
66
71
  "standard": "^17.1.2",
@@ -74,5 +79,10 @@
74
79
  "publishConfig": {
75
80
  "access": "public",
76
81
  "provenance": true
82
+ },
83
+ "standard": {
84
+ "ignore": [
85
+ "src/lib/helpers/envfileParser.js"
86
+ ]
77
87
  }
78
88
  }
@@ -18,7 +18,7 @@ async function decrypt () {
18
18
  const envs = this.envs
19
19
  const sesh = new Session()
20
20
  const noArmor = options.armor === false || (await sesh.noArmor())
21
- const noKeychain = options.native === false || options.noNative === true
21
+ const noNative = options.native === false || options.noNative === true
22
22
 
23
23
  let errorCount = 0
24
24
 
@@ -32,7 +32,7 @@ async function decrypt () {
32
32
  ek: options.excludeKey,
33
33
  fk: options.envKeysFile,
34
34
  noArmor,
35
- noKeychain,
35
+ noNative,
36
36
  onStatus: (text) => {
37
37
  if (spinner && text) {
38
38
  spinner.text = text
@@ -76,7 +76,7 @@ async function decrypt () {
76
76
  ek: options.excludeKey,
77
77
  fk: options.envKeysFile,
78
78
  noArmor,
79
- noKeychain,
79
+ noNative,
80
80
  onStatus: (text) => {
81
81
  if (spinner && text) {
82
82
  spinner.text = text
@@ -21,7 +21,7 @@ async function encryptAction () {
21
21
  const fk = options.envKeysFile || '.env.keys'
22
22
  const noCreate = options.create === false
23
23
  const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
24
- const noKeychain = options.native === false || options.noNative === true
24
+ const noNative = options.native === false || options.noNative === true
25
25
 
26
26
  const noBitwarden = options.bitwarden === false || options.noBitwarden === true
27
27
  const no1Password = options['1password'] === false || options.no1Password === true
@@ -30,7 +30,7 @@ async function encryptAction () {
30
30
 
31
31
  // stdout - should not have a try so that exit codes can surface to stdout
32
32
  if (options.stdout) {
33
- const { processedEnvs } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain, no1Password, noBitwarden })
33
+ const { processedEnvs } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noNative, no1Password, noBitwarden })
34
34
 
35
35
  if (spinner) spinner.stop()
36
36
  for (const processedEnv of processedEnvs) {
@@ -51,7 +51,7 @@ async function encryptAction () {
51
51
  }
52
52
 
53
53
  try {
54
- const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain, no1Password, noBitwarden })
54
+ const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noNative, no1Password, noBitwarden })
55
55
 
56
56
  if (keysSrc) {
57
57
  await fsx.writeFileX(fk, keysSrc)
@@ -32,7 +32,7 @@ async function get (key) {
32
32
  try {
33
33
  const sesh = new Session()
34
34
  const noArmor = options.armor === false || (await sesh.noArmor())
35
- const noKeychain = options.native === false || options.noNative === true
35
+ const noNative = options.native === false || options.noNative === true
36
36
  const { parsed: resolved, errors } = await getResolver({
37
37
  key,
38
38
  envs,
@@ -40,7 +40,7 @@ async function get (key) {
40
40
  all: options.all,
41
41
  envKeysFile: resolveEnvKeysFile(options.envKeysFile),
42
42
  noArmor,
43
- noKeychain,
43
+ noNative,
44
44
  no1Password: options['1password'] === false || options.no1Password === true,
45
45
  noBitwarden: options.bitwarden === false || options.noBitwarden === true,
46
46
  onStatus: (text) => {
@@ -21,7 +21,7 @@ async function keypair (key) {
21
21
  envFile: options.envFile,
22
22
  envKeysFile: options.envKeysFile,
23
23
  armor: options.armor,
24
- noKeychain: options.native === false || options.noNative === true,
24
+ noNative: options.native === false || options.noNative === true,
25
25
  noBitwarden: options.bitwarden === false || options.noBitwarden === true,
26
26
  no1Password: options['1password'] === false || options.no1Password === true,
27
27
  token: options.token,
@@ -2,24 +2,18 @@ const path = require('path')
2
2
  const { logger } = require('./../../shared/logger')
3
3
 
4
4
  const executeCommand = require('./../../lib/helpers/executeCommand')
5
- const envsResolver = require('./../../lib/resolvers/envs')
6
5
  const catchAndLog = require('./../../lib/helpers/catchAndLog')
7
6
  const createSpinner = require('../../lib/helpers/createSpinner')
8
- const Session = require('../../db/session')
7
+ const prepareValidatedEnv = require('../../lib/services/validate')
9
8
  const normalizeDotenvConfigQuiet = require('../../lib/helpers/normalizeDotenvConfigQuiet')
10
9
  const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
11
10
  const normalizeDotenvConfigIgnore = require('../../lib/helpers/normalizeDotenvConfigIgnore')
12
- const normalizeDotenvConfigPath = require('../../lib/helpers/normalizeDotenvConfigPath')
13
- const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
14
- const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
15
11
  const mask = require('../../lib/helpers/mask')
16
12
  const maskEnvSrc = require('../../lib/helpers/maskEnvSrc')
17
13
  const maskProcessedEnvs = require('../../lib/helpers/maskProcessedEnvs')
18
14
  const redactedValues = require('../../lib/helpers/redactedValues')
19
15
  const { redactOutput } = require('../../lib/helpers/redactOutput')
20
- const validateEnvExample = require('../../lib/helpers/validateEnvExample')
21
-
22
- const { determine } = require('./../../lib/helpers/envResolution')
16
+ const configureProxy = require('../../lib/proxy/configureProxy')
23
17
 
24
18
  function inferCommandArgsFromProcessArgv (argv) {
25
19
  const runIndex = argv.indexOf('run')
@@ -65,6 +59,7 @@ async function run () {
65
59
  }
66
60
  let commandEnv = process.env
67
61
  let sensitiveValues = []
62
+ let closeProxy
68
63
 
69
64
  let commandArgs = this.args
70
65
  if (commandArgs.length < 1) {
@@ -87,10 +82,6 @@ async function run () {
87
82
 
88
83
  const ignore = options.ignore || []
89
84
 
90
- const sesh = new Session()
91
- const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
92
- const noKeychain = options.native === false || options.noNative === true
93
-
94
85
  if (commandArgs.length < 1) {
95
86
  if (spinner) spinner.stop()
96
87
 
@@ -107,29 +98,26 @@ async function run () {
107
98
  }
108
99
 
109
100
  try {
110
- let envs = buildCommandEnvs(normalizeDotenvConfigPath(this.envs), options.convention)
111
- envs = determine(envs, process.env)
112
-
113
101
  const {
114
102
  processedEnvs,
115
- readableFilepaths
116
- } = await envsResolver({
117
- envs,
118
- overload: options.overload,
103
+ readableFilepaths,
104
+ hasEnvfile,
105
+ proxyCredentials,
106
+ proxyToken,
107
+ session: sesh,
108
+ proxyError,
109
+ validationError: error
110
+ } = await prepareValidatedEnv({
111
+ envs: this.envs,
112
+ options,
119
113
  processEnv: process.env,
120
- envKeysFile: resolveEnvKeysFile(options.envKeysFile),
121
- noArmor,
122
- noKeychain,
123
- no1Password: options['1password'] === false || options.no1Password === true,
124
- noBitwarden: options.bitwarden === false || options.noBitwarden === true,
125
- token: options.token,
114
+ requireEnvfile: false,
126
115
  command: commandArgs,
127
116
  onStatus: (text) => {
128
- if (spinner && text) {
129
- spinner.text = text
130
- }
117
+ if (spinner && text) spinner.text = text
131
118
  }
132
119
  })
120
+ if (proxyError) throw proxyError
133
121
 
134
122
  if (redactEnabled) {
135
123
  sensitiveValues = redactedValues(processedEnvs)
@@ -140,17 +128,13 @@ async function run () {
140
128
  maskProcessedEnvs(processedEnvs, commandEnv, showChar)
141
129
  }
142
130
 
143
- if (options.validate) {
144
- const error = validateEnvExample(process.env)
145
-
146
- if (error) {
147
- if (ignore.includes(error.code)) {
148
- logger.verbose(`ignored: ${error.message}`)
149
- } else if (options.strict) {
150
- throw error
151
- } else {
152
- logger.error(error.messageWithHelp || error.message)
153
- }
131
+ if (error) {
132
+ if (ignore.includes(error.code)) {
133
+ logger.verbose(`ignored: ${error.message}`)
134
+ } else if (options.strict || hasEnvfile) {
135
+ throw error
136
+ } else {
137
+ logger.error(error.messageWithHelp || error.message)
154
138
  }
155
139
  }
156
140
 
@@ -198,7 +182,21 @@ async function run () {
198
182
  }
199
183
  }
200
184
 
201
- let msg = `injected env (${uniqueInjectedKeys(processedEnvs).size})`
185
+ const proxy = await configureProxy(commandArgs, commandEnv, proxyCredentials, sesh, proxyToken)
186
+ closeProxy = proxy.close
187
+ commandArgs = proxy.commandArgs
188
+ commandEnv = proxy.env
189
+
190
+ const gatedKeys = new Set((proxyCredentials || [])
191
+ .filter(credential => commandEnv[credential.name] === credential.placeholder)
192
+ .map(credential => credential.name))
193
+ const injectedKeys = uniqueInjectedKeys(processedEnvs)
194
+ for (const key of gatedKeys) {
195
+ injectedKeys.delete(key)
196
+ logger.verbose(`${key} proxied via Armor proxy`)
197
+ }
198
+
199
+ let msg = ''
202
200
  const envStringCount = processedEnvs.filter((processedEnv) => processedEnv.type === 'env' && processedEnv.parsed).length
203
201
  if (readableFilepaths.length > 0 && envStringCount > 0) {
204
202
  msg += ` from ${readableFilepaths.join(', ')}, and --env flag${envStringCount > 1 ? 's' : ''}`
@@ -209,8 +207,10 @@ async function run () {
209
207
  }
210
208
 
211
209
  if (spinner) spinner.stop()
212
- logger.success(`⟐ ${msg}`)
210
+ if (gatedKeys.size > 0) logger.success(`⧈ proxied (${gatedKeys.size})${msg}`)
211
+ logger.success(`⟐ injected env (${injectedKeys.size})${msg}`)
213
212
  } catch (error) {
213
+ if (closeProxy) await closeProxy()
214
214
  if (spinner) spinner.stop()
215
215
  if (error.code === 'PROMPT_CANCELLED') {
216
216
  process.exit(130)
@@ -220,7 +220,11 @@ async function run () {
220
220
  process.exit(1)
221
221
  }
222
222
 
223
- await executeCommand(commandArgs, commandEnv, sensitiveValues)
223
+ try {
224
+ await executeCommand(commandArgs, commandEnv, sensitiveValues, closeProxy)
225
+ } finally {
226
+ if (closeProxy) await closeProxy()
227
+ }
224
228
  }
225
229
 
226
230
  module.exports = run
@@ -59,7 +59,7 @@ async function set (key, value) {
59
59
  const fk = options.envKeysFile || '.env.keys'
60
60
  const noCreate = options.create === false
61
61
  const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
62
- const noKeychain = options.native === false || options.noNative === true
62
+ const noNative = options.native === false || options.noNative === true
63
63
 
64
64
  const noBitwarden = options.bitwarden === false || options.noBitwarden === true
65
65
  const no1Password = options['1password'] === false || options.no1Password === true
@@ -67,7 +67,7 @@ async function set (key, value) {
67
67
  let errorCount = 0
68
68
 
69
69
  try {
70
- const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await setTransform({ envs, key, value, fk, noArmor, noCreate, encrypt, noKeychain, no1Password, noBitwarden })
70
+ const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await setTransform({ envs, key, value, fk, noArmor, noCreate, encrypt, noNative, no1Password, noBitwarden })
71
71
 
72
72
  if (keysSrc) {
73
73
  await fsx.writeFileX(fk, keysSrc)
@@ -1,53 +1,35 @@
1
1
  const { logger } = require('./../../shared/logger')
2
2
 
3
- const envsResolver = require('./../../lib/resolvers/envs')
4
3
  const catchAndLog = require('./../../lib/helpers/catchAndLog')
5
4
  const createSpinner = require('../../lib/helpers/createSpinner')
6
- const Session = require('../../db/session')
5
+ const prepareValidatedEnv = require('../../lib/services/validate')
7
6
  const normalizeDotenvConfigQuiet = require('../../lib/helpers/normalizeDotenvConfigQuiet')
8
7
  const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
9
8
  const normalizeDotenvConfigIgnore = require('../../lib/helpers/normalizeDotenvConfigIgnore')
10
- const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
11
- const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
12
- const validateEnvExample = require('../../lib/helpers/validateEnvExample')
13
-
14
- const { determine } = require('./../../lib/helpers/envResolution')
15
9
 
16
10
  async function validate () {
17
11
  const options = normalizeDotenvConfigIgnore(normalizeDotenvConfigConvention(normalizeDotenvConfigQuiet(this.opts())))
18
12
  const spinnerOptions = typeof this.optsWithGlobals === 'function' ? this.optsWithGlobals() : options
19
13
  const spinner = await createSpinner({ ...spinnerOptions, ...options, text: 'validating' })
20
14
  const ignore = options.ignore || []
21
- const validateEnv = { ...process.env }
22
15
  let errorCount = 0
23
16
 
24
17
  logger.debug(`options: ${JSON.stringify(options)}`)
25
18
 
26
19
  try {
27
- let envs = buildCommandEnvs(this.envs, options.convention)
28
- envs = determine(envs, process.env)
29
-
30
- const sesh = new Session()
31
- const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
32
- const noKeychain = options.native === false || options.noNative === true
33
-
34
- const { processedEnvs } = await envsResolver({
35
- envs,
36
- overload: options.overload,
37
- processEnv: validateEnv,
38
- envKeysFile: resolveEnvKeysFile(options.envKeysFile),
39
- noArmor,
40
- noKeychain,
41
- no1Password: options['1password'] === false || options.no1Password === true,
42
- noBitwarden: options.bitwarden === false || options.noBitwarden === true,
43
- token: options.token,
20
+ const { processedEnvs, readableFilepaths, proxyError, validationError } = await prepareValidatedEnv({
21
+ envs: this.envs,
22
+ options,
44
23
  onStatus: (text) => {
45
- if (spinner && text) {
46
- spinner.text = text
47
- }
24
+ if (spinner && text) spinner.text = text
48
25
  }
49
26
  })
50
27
 
28
+ const sources = [...readableFilepaths]
29
+ if (processedEnvs.some(env => env.type === 'env' && env.parsed)) sources.push('--env')
30
+ if (sources.length === 0) sources.push('shell environment')
31
+ const sourceSummary = `(${sources.join(', ')})`
32
+
51
33
  for (const processedEnv of processedEnvs) {
52
34
  for (const error of processedEnv.errors || []) {
53
35
  if (ignore.includes(error.code)) {
@@ -55,18 +37,24 @@ async function validate () {
55
37
  continue
56
38
  }
57
39
 
58
- errorCount += 1
40
+ if (error.code !== 'MISSING_ENV_FILE' || options.strict) errorCount += 1
41
+ if (error.code === 'MISSING_ENV_FILE' && options.convention && !options.strict) continue
59
42
  logger.error(error.messageWithHelp || error.message)
60
43
  }
61
44
  }
62
45
 
63
- const validationError = validateEnvExample(validateEnv)
46
+ if (proxyError) {
47
+ proxyError.message += ` ${sourceSummary}`
48
+ throw proxyError
49
+ }
50
+
64
51
  if (validationError) {
52
+ const message = `${validationError.message} ${sourceSummary}`
65
53
  if (ignore.includes(validationError.code)) {
66
- logger.verbose(`ignored: ${validationError.message}`)
54
+ logger.verbose(`ignored: ${message}`)
67
55
  } else {
68
56
  errorCount += 1
69
- logger.error(validationError.messageWithHelp || validationError.message)
57
+ logger.error(message)
70
58
  }
71
59
  }
72
60
 
@@ -75,7 +63,7 @@ async function validate () {
75
63
  if (errorCount > 0) {
76
64
  process.exit(1)
77
65
  } else {
78
- logger.success('▣ validated')
66
+ logger.success(`▣ valid ${sourceSummary}`)
79
67
  }
80
68
  } catch (error) {
81
69
  if (spinner) spinner.stop()
@@ -73,7 +73,6 @@ program.command('run')
73
73
  .option('-fk, --env-keys-file <path>', 'path(s) to your .env.keys file(s) (default: same path as your env file)', collectEnvKeys)
74
74
  .option('--redact', 'redact injected values except keys ending in _PLAIN', false)
75
75
  .option('-o, --overload', 'override existing env variables (by default, existing env vars take precedence over .env files)')
76
- .option('--validate', 'validate against .env.example', false)
77
76
  .option('--strict', 'process.exit(1) on any errors', false)
78
77
  .option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
79
78
  .option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
@@ -224,7 +223,8 @@ program.command('ls')
224
223
 
225
224
  // dotenvx validate
226
225
  program.command('validate')
227
- .description('validate .env file(s) against .env.example')
226
+ .description('validate .env file(s) against Envfile')
227
+ .option('--strict', 'process.exit(1) on any errors, including missing env files', false)
228
228
  .option('-e, --env <strings...>', 'environment variable(s) set as string (example: "HELLO=World")', collectEnvs('env'), [])
229
229
  .option('-f, --env-file <path>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
230
230
  .option('-fk, --env-keys-file <path>', 'path(s) to your .env.keys file(s) (default: same path as your env file)', collectEnvKeys)