@dotenvx/dotenvx 2.13.0 → 2.15.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 +14 -1
- package/README.md +111 -2
- package/package.json +12 -3
- package/src/cli/actions/get.js +2 -0
- package/src/cli/actions/run.js +17 -0
- package/src/cli/actions/validate.js +86 -0
- package/src/cli/dotenvx.js +30 -6
- package/src/lib/helpers/createSpinner.js +17 -1
- package/src/lib/helpers/errors.js +52 -0
- package/src/lib/helpers/resolveBitwardenPassword.js +149 -0
- package/src/lib/helpers/resolveOnePassword.js +72 -0
- package/src/lib/helpers/validate.js +40 -0
- package/src/lib/helpers/validateEnvExample.js +24 -0
- package/src/lib/main.d.ts +32 -0
- package/src/lib/main.js +5 -1
- package/src/lib/resolvers/envs.js +78 -9
- package/src/lib/resolvers/get.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,20 @@
|
|
|
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.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.15.0...main)
|
|
6
|
+
|
|
7
|
+
## [2.15.0](https://github.com/dotenvx/dotenvx/compare/v2.14.0...v2.15.0) (2026-07-20)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add support for 1Password `op://` secrets in your .env files. ([#908](https://github.com/dotenvx/dotenvx/pull/908))
|
|
12
|
+
|
|
13
|
+
## [2.14.0](https://github.com/dotenvx/dotenvx/compare/v2.13.0...v2.14.0) (2026-07-16)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
* Add `dotenvx run --validate` for simple env validation against your `.env.example` ([#906](https://github.com/dotenvx/dotenvx/pull/906))
|
|
18
|
+
* Add `dotenvx validate` for a pre-check of validation against your `.env.example` file ([#906](https://github.com/dotenvx/dotenvx/pull/906))
|
|
6
19
|
|
|
7
20
|
## [2.13.0](https://github.com/dotenvx/dotenvx/compare/v2.12.0...v2.13.0) (2026-07-16)
|
|
8
21
|
|
package/README.md
CHANGED
|
@@ -153,6 +153,19 @@ Hello [REDACTED]
|
|
|
153
153
|
|
|
154
154
|
see [Codex redaction guide](https://dotenvx.com/docs/cli/run-redact-codex-exec)
|
|
155
155
|
|
|
156
|
+
</details>
|
|
157
|
+
<details><summary>1Password 🔐</summary><br>
|
|
158
|
+
|
|
159
|
+
Run with secrets resolved directly from 1Password.
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
$ echo "HELLO=op://Personal/hello/password" > .env
|
|
163
|
+
$ dotenvx run -- sh -c 'echo Hello $HELLO'
|
|
164
|
+
Hello World
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
see [1Password guide](https://dotenvx.com/docs/secrets-in-1password)
|
|
168
|
+
|
|
156
169
|
</details>
|
|
157
170
|
<details><summary>TypeScript 📘</summary><br>
|
|
158
171
|
|
|
@@ -1156,6 +1169,30 @@ $ dotenvx get GOODBYE
|
|
|
1156
1169
|
[MISSING_KEY] missing key (GOODBYE)
|
|
1157
1170
|
```
|
|
1158
1171
|
|
|
1172
|
+
</details>
|
|
1173
|
+
<details><summary>`run` - 1Password</summary><br>
|
|
1174
|
+
|
|
1175
|
+
Resolve [1Password op://](https://developer.1password.com/docs/cli/secrets-reference-syntax/) directly from your `.env` file.
|
|
1176
|
+
|
|
1177
|
+
```ini
|
|
1178
|
+
# .env
|
|
1179
|
+
API_KEY=op://Personal/my_api_key/password
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
Install the [1Password CLI](https://developer.1password.com/docs/cli/get-started/) and authenticate with `op`. Dotenvx automatically reads `op://` values through `op read` before injecting them.
|
|
1183
|
+
|
|
1184
|
+
```sh
|
|
1185
|
+
$ dotenvx run -- node index.js
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
Use `--no-1password` to leave `op://` values unresolved.
|
|
1189
|
+
|
|
1190
|
+
```sh
|
|
1191
|
+
$ dotenvx run --no-1password -- node index.js
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
The same flag is available for `dotenvx get` and `dotenvx validate`.
|
|
1195
|
+
|
|
1159
1196
|
</details>
|
|
1160
1197
|
<details><summary>`run -f <directory>`</summary><br>
|
|
1161
1198
|
|
|
@@ -1453,6 +1490,31 @@ Hello production
|
|
|
1453
1490
|
|
|
1454
1491
|
Available log levels are `error, warn, info, verbose, debug, silly` ([source](https://docs.npmjs.com/cli/v8/using-npm/logging#setting-log-levels))
|
|
1455
1492
|
|
|
1493
|
+
</details>
|
|
1494
|
+
<details><summary>`run --validate`</summary><br>
|
|
1495
|
+
|
|
1496
|
+
Validate your environment against `.env.example`.
|
|
1497
|
+
|
|
1498
|
+
```ini
|
|
1499
|
+
# .env.example
|
|
1500
|
+
DATABASE_URL=
|
|
1501
|
+
API_KEY=
|
|
1502
|
+
SENTRY_DSN= # optional
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
```sh
|
|
1506
|
+
$ dotenvx run --validate -- node index.js
|
|
1507
|
+
[VALIDATION_FAILED] missing required (DATABASE_URL, API_KEY). fix: [https://github.com/dotenvx/dotenvx/issues/907]
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
Validation errors are reported without stopping your command. Combine `--validate` with `--strict` to exit with code `1` before the command runs.
|
|
1511
|
+
|
|
1512
|
+
```sh
|
|
1513
|
+
$ dotenvx run --validate --strict -- node index.js
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
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.
|
|
1517
|
+
|
|
1456
1518
|
</details>
|
|
1457
1519
|
<details><summary>`run --strict`</summary><br>
|
|
1458
1520
|
|
|
@@ -2442,6 +2504,29 @@ $ dotenvx ls --json
|
|
|
2442
2504
|
$ dotenvx ls --json > dotenv-files.json
|
|
2443
2505
|
```
|
|
2444
2506
|
|
|
2507
|
+
</details>
|
|
2508
|
+
<details><summary>`validate`</summary><br>
|
|
2509
|
+
|
|
2510
|
+
Validate `.env` file(s) against `.env.example` without running a command.
|
|
2511
|
+
|
|
2512
|
+
```ini
|
|
2513
|
+
# .env.example
|
|
2514
|
+
DATABASE_URL=
|
|
2515
|
+
API_KEY=
|
|
2516
|
+
SENTRY_DSN= # optional
|
|
2517
|
+
```
|
|
2518
|
+
|
|
2519
|
+
```sh
|
|
2520
|
+
$ dotenvx validate
|
|
2521
|
+
[VALIDATION_FAILED] missing required (DATABASE_URL, API_KEY). fix: [https://github.com/dotenvx/dotenvx/issues/907]
|
|
2522
|
+
```
|
|
2523
|
+
|
|
2524
|
+
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`.
|
|
2525
|
+
|
|
2526
|
+
```sh
|
|
2527
|
+
$ dotenvx validate -f .env.production -fk .env.keys
|
|
2528
|
+
```
|
|
2529
|
+
|
|
2445
2530
|
</details>
|
|
2446
2531
|
<details><summary>`genexample`</summary><br>
|
|
2447
2532
|
|
|
@@ -2825,9 +2910,10 @@ Commands:
|
|
|
2825
2910
|
decrypt decrypt .env file(s)
|
|
2826
2911
|
keypair [KEY] print public/private keys for .env file(s)
|
|
2827
2912
|
ls [directory] print all .env files in a tree structure
|
|
2913
|
+
gitignore append to .gitignore
|
|
2828
2914
|
genexample [directory]
|
|
2829
2915
|
generate .env.example
|
|
2830
|
-
|
|
2916
|
+
validate validate .env file(s) against .env.example
|
|
2831
2917
|
precommit [directory]
|
|
2832
2918
|
prevent committing .env files to code
|
|
2833
2919
|
prebuild [directory]
|
|
@@ -3176,6 +3262,30 @@ Turn off [Dotenvx Armor ⛨](https://dotenvx.com/armor) features.
|
|
|
3176
3262
|
require('@dotenvx/dotenvx').config({noArmor: true})
|
|
3177
3263
|
```
|
|
3178
3264
|
|
|
3265
|
+
</details>
|
|
3266
|
+
<details><summary>`config(no1Password:)` - no1Password</summary><br>
|
|
3267
|
+
|
|
3268
|
+
By default, `config()` automatically resolves `op://` values through the installed [1Password CLI](https://developer.1password.com/docs/cli/get-started/).
|
|
3269
|
+
|
|
3270
|
+
```ini
|
|
3271
|
+
# .env
|
|
3272
|
+
API_KEY=op://Personal/my_api_key/password
|
|
3273
|
+
```
|
|
3274
|
+
|
|
3275
|
+
```js
|
|
3276
|
+
// index.js
|
|
3277
|
+
require('@dotenvx/dotenvx').config()
|
|
3278
|
+
|
|
3279
|
+
console.log(process.env.API_KEY)
|
|
3280
|
+
```
|
|
3281
|
+
|
|
3282
|
+
Set `no1Password` to leave `op://` values unresolved and avoid calling `op`.
|
|
3283
|
+
|
|
3284
|
+
```js
|
|
3285
|
+
// index.js
|
|
3286
|
+
require('@dotenvx/dotenvx').config({no1Password: true})
|
|
3287
|
+
```
|
|
3288
|
+
|
|
3179
3289
|
</details>
|
|
3180
3290
|
<details><summary>`parse(src)`</summary><br>
|
|
3181
3291
|
|
|
@@ -3336,7 +3446,6 @@ Set `mask: 0` to fully mask values.
|
|
|
3336
3446
|
* [GitHub Actions](https://dotenvx.com/docs/cis/github-actions)
|
|
3337
3447
|
* [Password Managers](https://dotenvx.com/docs#password-managers)
|
|
3338
3448
|
* [1Password](https://dotenvx.com/docs/guides/1password)
|
|
3339
|
-
* [Bitwarden](https://dotenvx.com/docs/guides/bitwarden)
|
|
3340
3449
|
* [Background Jobs](https://dotenvx.com/docs#background-jobs)
|
|
3341
3450
|
* [Trigger.dev](https://dotenvx.com/docs/background-jobs/triggerdotdev)
|
|
3342
3451
|
* [Package Managers](https://dotenvx.com/docs#package-managers)
|
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.15.0",
|
|
3
3
|
"name": "@dotenvx/dotenvx",
|
|
4
4
|
"description": "a secure dotenv–from the creator of `dotenv`",
|
|
5
5
|
"author": "@motdotla",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dotenv",
|
|
8
|
-
"env"
|
|
8
|
+
"env",
|
|
9
|
+
".env",
|
|
10
|
+
"environment",
|
|
11
|
+
"variables",
|
|
12
|
+
"config",
|
|
13
|
+
"settings",
|
|
14
|
+
"env vars",
|
|
15
|
+
"environment variables",
|
|
16
|
+
"secret-management",
|
|
17
|
+
"secrets"
|
|
9
18
|
],
|
|
10
19
|
"homepage": "https://github.com/dotenvx/dotenvx",
|
|
11
20
|
"repository": {
|
|
@@ -43,7 +52,7 @@
|
|
|
43
52
|
},
|
|
44
53
|
"funding": "https://dotenvx.com",
|
|
45
54
|
"dependencies": {
|
|
46
|
-
"@dotenvx/primitives": "^2.
|
|
55
|
+
"@dotenvx/primitives": "^2.1.0",
|
|
47
56
|
"@dotenvx/tooling": "^1.0.2",
|
|
48
57
|
"yocto-spinner": "^1.2.1"
|
|
49
58
|
},
|
package/src/cli/actions/get.js
CHANGED
|
@@ -38,6 +38,8 @@ async function get (key) {
|
|
|
38
38
|
envKeysFile: resolveEnvKeysFile(options.envKeysFile),
|
|
39
39
|
noArmor,
|
|
40
40
|
noKeychain,
|
|
41
|
+
no1Password: options['1password'] === false || options.no1Password === true,
|
|
42
|
+
noBitwarden: options.bitwarden === false || options.noBitwarden === true,
|
|
41
43
|
onStatus: (text) => {
|
|
42
44
|
if (spinner && text) {
|
|
43
45
|
spinner.text = text
|
package/src/cli/actions/run.js
CHANGED
|
@@ -15,6 +15,7 @@ const maskEnvSrc = require('../../lib/helpers/maskEnvSrc')
|
|
|
15
15
|
const maskProcessedEnvs = require('../../lib/helpers/maskProcessedEnvs')
|
|
16
16
|
const redactedValues = require('../../lib/helpers/redactedValues')
|
|
17
17
|
const { redactOutput } = require('../../lib/helpers/redactOutput')
|
|
18
|
+
const validateEnvExample = require('../../lib/helpers/validateEnvExample')
|
|
18
19
|
|
|
19
20
|
const { determine } = require('./../../lib/helpers/envResolution')
|
|
20
21
|
|
|
@@ -117,6 +118,8 @@ async function run () {
|
|
|
117
118
|
envKeysFile: resolveEnvKeysFile(options.envKeysFile),
|
|
118
119
|
noArmor,
|
|
119
120
|
noKeychain,
|
|
121
|
+
no1Password: options['1password'] === false || options.no1Password === true,
|
|
122
|
+
noBitwarden: options.bitwarden === false || options.noBitwarden === true,
|
|
120
123
|
token: options.token,
|
|
121
124
|
command: commandArgs,
|
|
122
125
|
onStatus: (text) => {
|
|
@@ -135,6 +138,20 @@ async function run () {
|
|
|
135
138
|
maskProcessedEnvs(processedEnvs, commandEnv, showChar)
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
if (options.validate) {
|
|
142
|
+
const error = validateEnvExample(process.env)
|
|
143
|
+
|
|
144
|
+
if (error) {
|
|
145
|
+
if (ignore.includes(error.code)) {
|
|
146
|
+
logger.verbose(`ignored: ${error.message}`)
|
|
147
|
+
} else if (options.strict) {
|
|
148
|
+
throw error
|
|
149
|
+
} else {
|
|
150
|
+
logger.error(error.messageWithHelp || error.message)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
for (const processedEnv of processedEnvs) {
|
|
139
156
|
if (processedEnv.type === 'envFile') {
|
|
140
157
|
logger.verbose(`loading env from ${processedEnv.filepath} (${path.resolve(processedEnv.filepath)})`)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { logger } = require('./../../shared/logger')
|
|
2
|
+
|
|
3
|
+
const envsResolver = require('./../../lib/resolvers/envs')
|
|
4
|
+
const catchAndLog = require('./../../lib/helpers/catchAndLog')
|
|
5
|
+
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
6
|
+
const Session = require('../../db/session')
|
|
7
|
+
const normalizeDotenvConfigQuiet = require('../../lib/helpers/normalizeDotenvConfigQuiet')
|
|
8
|
+
const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
|
|
9
|
+
const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
|
|
10
|
+
const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
|
|
11
|
+
const validateEnvExample = require('../../lib/helpers/validateEnvExample')
|
|
12
|
+
|
|
13
|
+
const { determine } = require('./../../lib/helpers/envResolution')
|
|
14
|
+
|
|
15
|
+
async function validate () {
|
|
16
|
+
const options = normalizeDotenvConfigConvention(normalizeDotenvConfigQuiet(this.opts()))
|
|
17
|
+
const spinnerOptions = typeof this.optsWithGlobals === 'function' ? this.optsWithGlobals() : options
|
|
18
|
+
const spinner = await createSpinner({ ...spinnerOptions, ...options, text: 'validating' })
|
|
19
|
+
const ignore = options.ignore || []
|
|
20
|
+
const validateEnv = { ...process.env }
|
|
21
|
+
let errorCount = 0
|
|
22
|
+
|
|
23
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
let envs = buildCommandEnvs(this.envs, options.convention)
|
|
27
|
+
envs = determine(envs, process.env)
|
|
28
|
+
|
|
29
|
+
const sesh = new Session()
|
|
30
|
+
const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
|
|
31
|
+
const noKeychain = options.native === false || options.noNative === true
|
|
32
|
+
|
|
33
|
+
const { processedEnvs } = await envsResolver({
|
|
34
|
+
envs,
|
|
35
|
+
overload: options.overload,
|
|
36
|
+
processEnv: validateEnv,
|
|
37
|
+
envKeysFile: resolveEnvKeysFile(options.envKeysFile),
|
|
38
|
+
noArmor,
|
|
39
|
+
noKeychain,
|
|
40
|
+
no1Password: options['1password'] === false || options.no1Password === true,
|
|
41
|
+
noBitwarden: options.bitwarden === false || options.noBitwarden === true,
|
|
42
|
+
token: options.token,
|
|
43
|
+
onStatus: (text) => {
|
|
44
|
+
if (spinner && text) {
|
|
45
|
+
spinner.text = text
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
for (const processedEnv of processedEnvs) {
|
|
51
|
+
for (const error of processedEnv.errors || []) {
|
|
52
|
+
if (ignore.includes(error.code)) {
|
|
53
|
+
logger.verbose(`ignored: ${error.message}`)
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
errorCount += 1
|
|
58
|
+
logger.error(error.messageWithHelp || error.message)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const validationError = validateEnvExample(validateEnv)
|
|
63
|
+
if (validationError) {
|
|
64
|
+
if (ignore.includes(validationError.code)) {
|
|
65
|
+
logger.verbose(`ignored: ${validationError.message}`)
|
|
66
|
+
} else {
|
|
67
|
+
errorCount += 1
|
|
68
|
+
logger.error(validationError.messageWithHelp || validationError.message)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (spinner) spinner.stop()
|
|
73
|
+
|
|
74
|
+
if (errorCount > 0) {
|
|
75
|
+
process.exit(1)
|
|
76
|
+
} else {
|
|
77
|
+
logger.success('▣ validated')
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (spinner) spinner.stop()
|
|
81
|
+
catchAndLog(error)
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = validate
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -66,6 +66,7 @@ program.command('run')
|
|
|
66
66
|
.option('-fk, --env-keys-file <path>', 'path(s) to your .env.keys file(s) (default: same path as your env file)', collectEnvKeys)
|
|
67
67
|
.option('--redact', 'redact injected values except keys ending in _PLAIN', false)
|
|
68
68
|
.option('-o, --overload', 'override existing env variables (by default, existing env vars take precedence over .env files)')
|
|
69
|
+
.option('--validate', 'validate against .env.example', false)
|
|
69
70
|
.option('--strict', 'process.exit(1) on any errors', false)
|
|
70
71
|
.option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
|
|
71
72
|
.option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
|
|
@@ -73,6 +74,8 @@ program.command('run')
|
|
|
73
74
|
.option('--mask [characters]', 'inject masked values, optionally setting visible characters')
|
|
74
75
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
75
76
|
.option('--no-native', 'disable OS secret store features')
|
|
77
|
+
.option('--no-1password', 'disable 1Password secret reference resolution')
|
|
78
|
+
.option('--no-bitwarden', 'disable Bitwarden secret reference resolution')
|
|
76
79
|
.action(function (...args) {
|
|
77
80
|
this.envs = envs
|
|
78
81
|
return require('./actions/run').apply(this, args)
|
|
@@ -97,6 +100,8 @@ program.command('get')
|
|
|
97
100
|
.option('--format <type>', 'format of the output (json, shell, colon, eval, eval-export)', 'json')
|
|
98
101
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
99
102
|
.option('--no-native', 'disable OS secret store features')
|
|
103
|
+
.option('--no-1password', 'disable 1Password secret reference resolution')
|
|
104
|
+
.option('--no-bitwarden', 'disable Bitwarden secret reference resolution')
|
|
100
105
|
.action(function (...args) {
|
|
101
106
|
this.envs = envs
|
|
102
107
|
return require('./actions/get').apply(this, args)
|
|
@@ -182,13 +187,23 @@ program.command('ls')
|
|
|
182
187
|
return require('./actions/ls').apply(this, args)
|
|
183
188
|
})
|
|
184
189
|
|
|
185
|
-
// dotenvx
|
|
186
|
-
program.command('
|
|
187
|
-
.description('
|
|
188
|
-
.
|
|
189
|
-
.option('-f, --env-file <
|
|
190
|
+
// dotenvx validate
|
|
191
|
+
program.command('validate')
|
|
192
|
+
.description('validate .env file(s) against .env.example')
|
|
193
|
+
.option('-e, --env <strings...>', 'environment variable(s) set as string (example: "HELLO=World")', collectEnvs('env'), [])
|
|
194
|
+
.option('-f, --env-file <path>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
|
|
195
|
+
.option('-fk, --env-keys-file <path>', 'path(s) to your .env.keys file(s) (default: same path as your env file)', collectEnvKeys)
|
|
196
|
+
.option('-o, --overload', 'override existing env variables (by default, existing env vars take precedence over .env files)')
|
|
197
|
+
.option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
|
|
198
|
+
.option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
|
|
199
|
+
.option('--token <token>', 'set Armor ⛨ token')
|
|
200
|
+
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
201
|
+
.option('--no-native', 'disable OS secret store features')
|
|
202
|
+
.option('--no-1password', 'disable 1Password secret reference resolution')
|
|
203
|
+
.option('--no-bitwarden', 'disable Bitwarden secret reference resolution')
|
|
190
204
|
.action(function (...args) {
|
|
191
|
-
|
|
205
|
+
this.envs = envs
|
|
206
|
+
return require('./actions/validate').apply(this, args)
|
|
192
207
|
})
|
|
193
208
|
|
|
194
209
|
// dotenvx gitignore
|
|
@@ -200,6 +215,15 @@ program.command('gitignore')
|
|
|
200
215
|
return require('./actions/ext/gitignore').apply(this, args)
|
|
201
216
|
})
|
|
202
217
|
|
|
218
|
+
// dotenvx genexample
|
|
219
|
+
program.command('genexample')
|
|
220
|
+
.description('generate .env.example')
|
|
221
|
+
.argument('[directory]', 'directory to generate from', '.')
|
|
222
|
+
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
|
|
223
|
+
.action(function (...args) {
|
|
224
|
+
return require('./actions/ext/genexample').apply(this, args)
|
|
225
|
+
})
|
|
226
|
+
|
|
203
227
|
// dotenvx precommit
|
|
204
228
|
program.command('precommit')
|
|
205
229
|
.description('prevent committing .env files to code')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const FRAMES = ['◇', '⬖', '◆', '⬗']
|
|
2
2
|
const FRAME_INTERVAL_MS = 80
|
|
3
|
+
let activeSpinner
|
|
3
4
|
|
|
4
5
|
async function createSpinner (options = {}) {
|
|
5
6
|
const stream = process.stderr
|
|
@@ -11,7 +12,7 @@ async function createSpinner (options = {}) {
|
|
|
11
12
|
const frames = options.frames || FRAMES
|
|
12
13
|
|
|
13
14
|
const { default: yoctoSpinner } = await import('yocto-spinner')
|
|
14
|
-
|
|
15
|
+
activeSpinner = yoctoSpinner({
|
|
15
16
|
text,
|
|
16
17
|
spinner: {
|
|
17
18
|
frames,
|
|
@@ -19,6 +20,21 @@ async function createSpinner (options = {}) {
|
|
|
19
20
|
},
|
|
20
21
|
stream
|
|
21
22
|
}).start()
|
|
23
|
+
|
|
24
|
+
return activeSpinner
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
createSpinner.stop = function () {
|
|
28
|
+
if (activeSpinner) activeSpinner.stop()
|
|
29
|
+
activeSpinner = null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
createSpinner.pause = function () {
|
|
33
|
+
if (activeSpinner) activeSpinner.stop()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
createSpinner.resume = function () {
|
|
37
|
+
if (activeSpinner) activeSpinner.start()
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
module.exports = createSpinner
|
|
@@ -9,9 +9,12 @@ const ISSUE_BY_CODE = {
|
|
|
9
9
|
INVALID_PASSPHRASE: 'try again with the correct passphrase',
|
|
10
10
|
INVALID_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/465',
|
|
11
11
|
INVALID_PUBLIC_KEY: 'https://github.com/dotenvx/dotenvx/issues/756',
|
|
12
|
+
'1PASSWORD_FAILED': 'https://www.1password.dev/cli/get-started',
|
|
13
|
+
BITWARDEN_FAILED: 'https://bitwarden.com/help/cli/',
|
|
12
14
|
MALFORMED_ENCRYPTED_DATA: 'https://github.com/dotenvx/dotenvx/issues/467',
|
|
13
15
|
MISPAIRED_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/752',
|
|
14
16
|
MISSING_DIRECTORY: 'https://github.com/dotenvx/dotenvx/issues/758',
|
|
17
|
+
MISSING_ENV_EXAMPLE: 'https://github.com/dotenvx/dotenvx/issues/905',
|
|
15
18
|
MISSING_ENV_FILE: 'https://github.com/dotenvx/dotenvx/issues/484',
|
|
16
19
|
MISSING_ENV_KEYS_FILE: 'https://github.com/dotenvx/dotenvx/issues/775',
|
|
17
20
|
MISSING_ENV_FILES: 'https://github.com/dotenvx/dotenvx/issues/760',
|
|
@@ -22,6 +25,7 @@ const ISSUE_BY_CODE = {
|
|
|
22
25
|
MISSING_VALUE: 'https://github.com/dotenvx/dotenvx/issues/864',
|
|
23
26
|
FILE_NOT_WRITABLE: 'https://github.com/dotenvx/dotenvx/issues/890',
|
|
24
27
|
PRECOMMIT_HOOK_MODIFY_FAILED: 'try again or report error',
|
|
28
|
+
VALIDATION_FAILED: 'https://github.com/dotenvx/dotenvx/issues/907',
|
|
25
29
|
WRONG_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/466'
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -98,6 +102,30 @@ class Errors {
|
|
|
98
102
|
return e
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
onePasswordFailed () {
|
|
106
|
+
const code = '1PASSWORD_FAILED'
|
|
107
|
+
const message = `[${code}] ${this.message}`
|
|
108
|
+
const help = `fix: [${ISSUE_BY_CODE[code]}]`
|
|
109
|
+
|
|
110
|
+
const e = new Error(message)
|
|
111
|
+
e.code = code
|
|
112
|
+
e.help = help
|
|
113
|
+
e.messageWithHelp = `${message}. ${help}`
|
|
114
|
+
return e
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
bitwardenFailed () {
|
|
118
|
+
const code = 'BITWARDEN_FAILED'
|
|
119
|
+
const message = `[${code}] ${this.message}`
|
|
120
|
+
const help = this.help || `fix: [${ISSUE_BY_CODE[code]}]`
|
|
121
|
+
|
|
122
|
+
const e = new Error(message)
|
|
123
|
+
e.code = code
|
|
124
|
+
e.help = help
|
|
125
|
+
e.messageWithHelp = `${message}. ${help}`
|
|
126
|
+
return e
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
invalidColor () {
|
|
102
130
|
const code = 'INVALID_COLOR'
|
|
103
131
|
const message = `[${code}] Invalid color ${this.color}`
|
|
@@ -207,6 +235,18 @@ class Errors {
|
|
|
207
235
|
return e
|
|
208
236
|
}
|
|
209
237
|
|
|
238
|
+
missingEnvExample () {
|
|
239
|
+
const code = 'MISSING_ENV_EXAMPLE'
|
|
240
|
+
const message = `[${code}] missing .env.example file`
|
|
241
|
+
const help = `fix: [${ISSUE_BY_CODE[code]}]`
|
|
242
|
+
|
|
243
|
+
const e = new Error(message)
|
|
244
|
+
e.code = code
|
|
245
|
+
e.help = help
|
|
246
|
+
e.messageWithHelp = `${message}. ${help}`
|
|
247
|
+
return e
|
|
248
|
+
}
|
|
249
|
+
|
|
210
250
|
missingEnvKeysFile () {
|
|
211
251
|
const code = 'MISSING_ENV_KEYS_FILE'
|
|
212
252
|
const envKeysFilepath = this.envKeysFilepath || '.env.keys'
|
|
@@ -305,6 +345,18 @@ class Errors {
|
|
|
305
345
|
return e
|
|
306
346
|
}
|
|
307
347
|
|
|
348
|
+
validationFailed () {
|
|
349
|
+
const code = 'VALIDATION_FAILED'
|
|
350
|
+
const message = `[${code}] ${this.message}`
|
|
351
|
+
const help = `fix: [${ISSUE_BY_CODE[code]}]`
|
|
352
|
+
|
|
353
|
+
const e = new Error(message)
|
|
354
|
+
e.code = code
|
|
355
|
+
e.help = help
|
|
356
|
+
e.messageWithHelp = `${message}. ${help}`
|
|
357
|
+
return e
|
|
358
|
+
}
|
|
359
|
+
|
|
308
360
|
precommitHookModifyFailed () {
|
|
309
361
|
const code = 'PRECOMMIT_HOOK_MODIFY_FAILED'
|
|
310
362
|
const message = `[${code}] failed to modify pre-commit hook: ${this.error.message}`
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const { execFile, execFileSync } = require('child_process')
|
|
2
|
+
const Errors = require('./errors')
|
|
3
|
+
const prompts = require('./prompts')
|
|
4
|
+
const createSpinner = require('./createSpinner')
|
|
5
|
+
|
|
6
|
+
const FIELDS = new Set(['username', 'password', 'uri'])
|
|
7
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
8
|
+
|
|
9
|
+
function execFileAsync (command, args, options) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
execFile(command, args, options, (error, stdout) => {
|
|
12
|
+
if (error) return reject(error)
|
|
13
|
+
resolve(stdout)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isSecretReference (value) {
|
|
19
|
+
return typeof value === 'string' && value.startsWith('bw://')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseSecretReference (value) {
|
|
23
|
+
const [itemId, field, ...extra] = value.slice('bw://'.length).split('/')
|
|
24
|
+
|
|
25
|
+
if (!UUID.test(itemId) || !field || extra.length > 0) {
|
|
26
|
+
throw new Error('invalid Bitwarden Password Manager reference')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!FIELDS.has(field)) {
|
|
30
|
+
throw new Error(`unsupported Bitwarden Password Manager field ${field}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { itemId, field }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function session (options) {
|
|
37
|
+
if (options.session) return options.session
|
|
38
|
+
|
|
39
|
+
if (process.stdin.isTTY && process.stderr.isTTY) {
|
|
40
|
+
createSpinner.pause()
|
|
41
|
+
const password = await prompts.password({
|
|
42
|
+
message: 'Bitwarden master password',
|
|
43
|
+
prefix: '◇',
|
|
44
|
+
separator: '='
|
|
45
|
+
}, {
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
output: process.stderr
|
|
48
|
+
})
|
|
49
|
+
createSpinner.resume()
|
|
50
|
+
const passwordEnv = 'DOTENVX_BITWARDEN_PASSWORD'
|
|
51
|
+
options.session = secretValue(await execFileAsync('bw', ['unlock', '--passwordenv', passwordEnv, '--raw'], {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
env: { ...process.env, [passwordEnv]: password }
|
|
56
|
+
}))
|
|
57
|
+
return options.session
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!options.session) {
|
|
61
|
+
const error = new Error('Bitwarden Password Manager requires an unlocked BW_SESSION')
|
|
62
|
+
error.code = 'BW_SESSION_MISSING'
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolutionError (key, error) {
|
|
68
|
+
let message
|
|
69
|
+
if (error && error.code === 'ENOENT') {
|
|
70
|
+
message = `Bitwarden Password Manager CLI is not installed and could not resolve ${key}`
|
|
71
|
+
} else if (error && error.code === 'BW_SESSION_MISSING') {
|
|
72
|
+
message = `Bitwarden Password Manager is locked and could not resolve ${key}; run 'export BW_SESSION="$(bw unlock --raw)"'`
|
|
73
|
+
} else if (error && error.message && error.message.startsWith('unsupported Bitwarden Password Manager field')) {
|
|
74
|
+
message = `${error.message} for ${key}`
|
|
75
|
+
} else if (error && error.message === 'invalid Bitwarden Password Manager reference') {
|
|
76
|
+
message = `invalid Bitwarden Password Manager reference for ${key}`
|
|
77
|
+
} else {
|
|
78
|
+
message = `Bitwarden Password Manager CLI failed to resolve ${key}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Errors({
|
|
82
|
+
message,
|
|
83
|
+
help: 'fix: [https://bitwarden.com/help/cli/]'
|
|
84
|
+
}).bitwardenFailed()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function secretValue (stdout) {
|
|
88
|
+
return stdout.replace(/\r?\n$/, '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function resolveBitwardenPassword (parsed, options = {}) {
|
|
92
|
+
const errors = []
|
|
93
|
+
const unresolved = []
|
|
94
|
+
options.session = options.session || process.env.BW_SESSION
|
|
95
|
+
|
|
96
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
97
|
+
if (!isSecretReference(value)) continue
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const { itemId, field } = parseSecretReference(value)
|
|
101
|
+
const bwSession = await session(options)
|
|
102
|
+
const stdout = await execFileAsync('bw', ['get', field, itemId], {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
env: { ...process.env, BW_SESSION: bwSession }
|
|
106
|
+
})
|
|
107
|
+
parsed[key] = secretValue(stdout)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
errors.push(resolutionError(key, error))
|
|
110
|
+
unresolved.push(key)
|
|
111
|
+
delete parsed[key]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { errors, unresolved }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveBitwardenPasswordSync (parsed) {
|
|
119
|
+
const errors = []
|
|
120
|
+
const unresolved = []
|
|
121
|
+
|
|
122
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
123
|
+
if (!isSecretReference(value)) continue
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const { itemId, field } = parseSecretReference(value)
|
|
127
|
+
if (!process.env.BW_SESSION) {
|
|
128
|
+
const error = new Error('Bitwarden Password Manager requires an unlocked BW_SESSION')
|
|
129
|
+
error.code = 'BW_SESSION_MISSING'
|
|
130
|
+
throw error
|
|
131
|
+
}
|
|
132
|
+
const stdout = execFileSync('bw', ['get', field, itemId], {
|
|
133
|
+
encoding: 'utf8',
|
|
134
|
+
windowsHide: true,
|
|
135
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
136
|
+
})
|
|
137
|
+
parsed[key] = secretValue(stdout)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
errors.push(resolutionError(key, error))
|
|
140
|
+
unresolved.push(key)
|
|
141
|
+
delete parsed[key]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { errors, unresolved }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = resolveBitwardenPassword
|
|
149
|
+
module.exports.sync = resolveBitwardenPasswordSync
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { execFile, execFileSync } = require('child_process')
|
|
2
|
+
const Errors = require('./errors')
|
|
3
|
+
|
|
4
|
+
function execFileAsync (command, args, options) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
execFile(command, args, options, (error, stdout) => {
|
|
7
|
+
if (error) return reject(error)
|
|
8
|
+
resolve(stdout)
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isSecretReference (value) {
|
|
14
|
+
return typeof value === 'string' && value.startsWith('op://')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolutionError (key, error) {
|
|
18
|
+
const message = error && error.code === 'ENOENT'
|
|
19
|
+
? `1Password CLI is not installed and could not resolve ${key}`
|
|
20
|
+
: `1Password CLI failed to resolve ${key}`
|
|
21
|
+
|
|
22
|
+
return new Errors({ message }).onePasswordFailed()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function resolveOnePassword (parsed) {
|
|
26
|
+
const errors = []
|
|
27
|
+
const unresolved = []
|
|
28
|
+
|
|
29
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
30
|
+
if (!isSecretReference(value)) continue
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const stdout = await execFileAsync('op', ['read', value, '--no-newline'], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
windowsHide: true
|
|
36
|
+
})
|
|
37
|
+
parsed[key] = stdout
|
|
38
|
+
} catch (error) {
|
|
39
|
+
errors.push(resolutionError(key, error))
|
|
40
|
+
unresolved.push(key)
|
|
41
|
+
delete parsed[key]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { errors, unresolved }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveOnePasswordSync (parsed) {
|
|
49
|
+
const errors = []
|
|
50
|
+
const unresolved = []
|
|
51
|
+
|
|
52
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
53
|
+
if (!isSecretReference(value)) continue
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
parsed[key] = execFileSync('op', ['read', value, '--no-newline'], {
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
60
|
+
})
|
|
61
|
+
} catch (error) {
|
|
62
|
+
errors.push(resolutionError(key, error))
|
|
63
|
+
unresolved.push(key)
|
|
64
|
+
delete parsed[key]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { errors, unresolved }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = resolveOnePassword
|
|
72
|
+
module.exports.sync = resolveOnePasswordSync
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function optionalKeys (comments) {
|
|
2
|
+
const keys = new Set()
|
|
3
|
+
|
|
4
|
+
for (const [key, values] of Object.entries(comments)) {
|
|
5
|
+
if (values.some(comment => comment && /\boptional\b/i.test(comment))) {
|
|
6
|
+
keys.add(key)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return keys
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validate (example = {}, env = {}, options = {}) {
|
|
14
|
+
const errors = []
|
|
15
|
+
const missingRequired = []
|
|
16
|
+
const optional = optionalKeys(options.comments || {})
|
|
17
|
+
|
|
18
|
+
for (const key of Object.keys(example)) {
|
|
19
|
+
const value = env[key]
|
|
20
|
+
const missing = !Object.prototype.hasOwnProperty.call(env, key) || value.trim() === ''
|
|
21
|
+
if (!optional.has(key) && missing) {
|
|
22
|
+
missingRequired.push(key)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (missingRequired.length > 0) {
|
|
27
|
+
errors.push({
|
|
28
|
+
code: 'MISSING_REQUIRED',
|
|
29
|
+
keys: missingRequired,
|
|
30
|
+
message: `missing required (${missingRequired.join(', ')})`
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
valid: errors.length === 0,
|
|
36
|
+
errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = validate
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const { scan } = require('@dotenvx/primitives')
|
|
3
|
+
|
|
4
|
+
const Errors = require('./errors')
|
|
5
|
+
const validate = require('./validate')
|
|
6
|
+
|
|
7
|
+
function validateEnvExample (env = process.env, options = {}) {
|
|
8
|
+
const filepath = options.filepath || '.env.example'
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(filepath)) {
|
|
11
|
+
return new Errors().missingEnvExample()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const exampleSrc = fs.readFileSync(filepath, 'utf8')
|
|
15
|
+
const { parsed: example, comments } = scan(exampleSrc)
|
|
16
|
+
const validation = validate(example, env, { comments })
|
|
17
|
+
|
|
18
|
+
if (!validation.valid) {
|
|
19
|
+
const message = validation.errors.map(error => error.message).join('; ')
|
|
20
|
+
return new Errors({ message }).validationFailed()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = validateEnvExample
|
package/src/lib/main.d.ts
CHANGED
|
@@ -194,6 +194,22 @@ export interface DotenvConfigOptions {
|
|
|
194
194
|
*/
|
|
195
195
|
noNative?: boolean;
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Turn off 1Password secret reference resolution.
|
|
199
|
+
*
|
|
200
|
+
* @default false
|
|
201
|
+
* @example require('@dotenvx/dotenvx').config({ no1Password: true })
|
|
202
|
+
*/
|
|
203
|
+
no1Password?: boolean;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Turn off Bitwarden secret reference resolution.
|
|
207
|
+
*
|
|
208
|
+
* @default false
|
|
209
|
+
* @example require('@dotenvx/dotenvx').config({ noBitwarden: true })
|
|
210
|
+
*/
|
|
211
|
+
noBitwarden?: boolean;
|
|
212
|
+
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
export type DotenvConfigEnv =
|
|
@@ -370,6 +386,22 @@ export interface GetOptions {
|
|
|
370
386
|
*/
|
|
371
387
|
noNative?: boolean;
|
|
372
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Turn off 1Password secret reference resolution.
|
|
391
|
+
*
|
|
392
|
+
* @default false
|
|
393
|
+
* @example require('@dotenvx/dotenvx').get('KEY', { no1Password: true })
|
|
394
|
+
*/
|
|
395
|
+
no1Password?: boolean;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Turn off Bitwarden secret reference resolution.
|
|
399
|
+
*
|
|
400
|
+
* @default false
|
|
401
|
+
* @example require('@dotenvx/dotenvx').get('KEY', { noBitwarden: true })
|
|
402
|
+
*/
|
|
403
|
+
noBitwarden?: boolean;
|
|
404
|
+
|
|
373
405
|
}
|
|
374
406
|
|
|
375
407
|
/**
|
package/src/lib/main.js
CHANGED
|
@@ -87,6 +87,8 @@ const config = function (options = {}) {
|
|
|
87
87
|
envKeysFile,
|
|
88
88
|
noArmor,
|
|
89
89
|
noKeychain,
|
|
90
|
+
no1Password: options.no1Password,
|
|
91
|
+
noBitwarden: options.noBitwarden,
|
|
90
92
|
noSpinner: options.noSpinner,
|
|
91
93
|
token: options.token
|
|
92
94
|
})
|
|
@@ -331,7 +333,9 @@ const get = async function (key, options = {}) {
|
|
|
331
333
|
all: options.all,
|
|
332
334
|
envKeysFile: options.envKeysFile,
|
|
333
335
|
noArmor,
|
|
334
|
-
noKeychain
|
|
336
|
+
noKeychain,
|
|
337
|
+
no1Password: options.no1Password,
|
|
338
|
+
noBitwarden: options.noBitwarden
|
|
335
339
|
})
|
|
336
340
|
|
|
337
341
|
if (options.mask !== undefined) {
|
|
@@ -12,6 +12,8 @@ const keynames = require('./../conventions/keynames')
|
|
|
12
12
|
const providers = require('./../providers')
|
|
13
13
|
const decryptors = require('./../decryptors')
|
|
14
14
|
const parseWithDecryptor = require('./../helpers/parseWithDecryptor')
|
|
15
|
+
const resolveOnePassword = require('./../helpers/resolveOnePassword')
|
|
16
|
+
const resolveBitwardenPassword = require('./../helpers/resolveBitwardenPassword')
|
|
15
17
|
|
|
16
18
|
function unresolvedEncryptedErrors (parsed) {
|
|
17
19
|
const keys = []
|
|
@@ -70,7 +72,7 @@ function buildParseOptions ({ processEnv, overload, envKeysFilepath, provider, d
|
|
|
70
72
|
return options
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider, decryptor }) {
|
|
75
|
+
async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden }) {
|
|
74
76
|
const row = {}
|
|
75
77
|
row.type = TYPE_ENV
|
|
76
78
|
row.string = env.value
|
|
@@ -101,6 +103,21 @@ async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider
|
|
|
101
103
|
row.injected = injected || {}
|
|
102
104
|
row.existed = existed || {}
|
|
103
105
|
|
|
106
|
+
if (!no1Password) {
|
|
107
|
+
const result = await resolveOnePassword(row.injected)
|
|
108
|
+
row.errors.push(...result.errors)
|
|
109
|
+
for (const key of result.unresolved) delete row.parsed[key]
|
|
110
|
+
Object.assign(row.parsed, row.injected)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!noBitwarden) {
|
|
114
|
+
const passwordResult = await resolveBitwardenPassword(row.injected)
|
|
115
|
+
row.errors.push(...passwordResult.errors)
|
|
116
|
+
for (const key of passwordResult.unresolved) delete row.parsed[key]
|
|
117
|
+
|
|
118
|
+
Object.assign(row.parsed, row.injected)
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
inject(processEnv, row.parsed)
|
|
105
122
|
} catch (e) {
|
|
106
123
|
row.errors = [e]
|
|
@@ -109,7 +126,7 @@ async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider
|
|
|
109
126
|
return row
|
|
110
127
|
}
|
|
111
128
|
|
|
112
|
-
function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider, decryptor }) {
|
|
129
|
+
function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, no1Password, noBitwarden }) {
|
|
113
130
|
const row = {}
|
|
114
131
|
row.type = TYPE_ENV
|
|
115
132
|
row.string = env.value
|
|
@@ -140,6 +157,21 @@ function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider,
|
|
|
140
157
|
row.injected = injected || {}
|
|
141
158
|
row.existed = existed || {}
|
|
142
159
|
|
|
160
|
+
if (!no1Password) {
|
|
161
|
+
const result = resolveOnePassword.sync(row.injected)
|
|
162
|
+
row.errors.push(...result.errors)
|
|
163
|
+
for (const key of result.unresolved) delete row.parsed[key]
|
|
164
|
+
Object.assign(row.parsed, row.injected)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!noBitwarden) {
|
|
168
|
+
const passwordResult = resolveBitwardenPassword.sync(row.injected)
|
|
169
|
+
row.errors.push(...passwordResult.errors)
|
|
170
|
+
for (const key of passwordResult.unresolved) delete row.parsed[key]
|
|
171
|
+
|
|
172
|
+
Object.assign(row.parsed, row.injected)
|
|
173
|
+
}
|
|
174
|
+
|
|
143
175
|
inject(processEnv, row.parsed)
|
|
144
176
|
} catch (e) {
|
|
145
177
|
row.errors = [e]
|
|
@@ -148,7 +180,7 @@ function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider,
|
|
|
148
180
|
return row
|
|
149
181
|
}
|
|
150
182
|
|
|
151
|
-
async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths }) {
|
|
183
|
+
async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden }) {
|
|
152
184
|
const row = {}
|
|
153
185
|
row.type = TYPE_ENV_FILE
|
|
154
186
|
row.filepath = env.value
|
|
@@ -182,6 +214,21 @@ async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, prov
|
|
|
182
214
|
row.errors = decryptErrors(parsed, errors)
|
|
183
215
|
row.existed = existed || {}
|
|
184
216
|
|
|
217
|
+
if (!no1Password) {
|
|
218
|
+
const result = await resolveOnePassword(row.injected)
|
|
219
|
+
row.errors.push(...result.errors)
|
|
220
|
+
for (const key of result.unresolved) delete row.parsed[key]
|
|
221
|
+
Object.assign(row.parsed, row.injected)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!noBitwarden) {
|
|
225
|
+
const passwordResult = await resolveBitwardenPassword(row.injected)
|
|
226
|
+
row.errors.push(...passwordResult.errors)
|
|
227
|
+
for (const key of passwordResult.unresolved) delete row.parsed[key]
|
|
228
|
+
|
|
229
|
+
Object.assign(row.parsed, row.injected)
|
|
230
|
+
}
|
|
231
|
+
|
|
185
232
|
inject(processEnv, parsed)
|
|
186
233
|
} catch (e) {
|
|
187
234
|
if (e.code === 'ENOENT' || e.code === 'EISDIR') {
|
|
@@ -194,7 +241,7 @@ async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, prov
|
|
|
194
241
|
return row
|
|
195
242
|
}
|
|
196
243
|
|
|
197
|
-
function injectEnvFileSync ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths }) {
|
|
244
|
+
function injectEnvFileSync ({ env, overload, processEnv, envKeysFilepath, provider, decryptor, readableFilepaths, no1Password, noBitwarden }) {
|
|
198
245
|
const row = {}
|
|
199
246
|
row.type = TYPE_ENV_FILE
|
|
200
247
|
row.filepath = env.value
|
|
@@ -228,6 +275,21 @@ function injectEnvFileSync ({ env, overload, processEnv, envKeysFilepath, provid
|
|
|
228
275
|
row.errors = decryptErrors(parsed, errors)
|
|
229
276
|
row.existed = existed || {}
|
|
230
277
|
|
|
278
|
+
if (!no1Password) {
|
|
279
|
+
const result = resolveOnePassword.sync(row.injected)
|
|
280
|
+
row.errors.push(...result.errors)
|
|
281
|
+
for (const key of result.unresolved) delete row.parsed[key]
|
|
282
|
+
Object.assign(row.parsed, row.injected)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!noBitwarden) {
|
|
286
|
+
const passwordResult = resolveBitwardenPassword.sync(row.injected)
|
|
287
|
+
row.errors.push(...passwordResult.errors)
|
|
288
|
+
for (const key of passwordResult.unresolved) delete row.parsed[key]
|
|
289
|
+
|
|
290
|
+
Object.assign(row.parsed, row.injected)
|
|
291
|
+
}
|
|
292
|
+
|
|
231
293
|
inject(processEnv, parsed)
|
|
232
294
|
} catch (e) {
|
|
233
295
|
if (e.code === 'ENOENT' || e.code === 'EISDIR') {
|
|
@@ -247,7 +309,6 @@ async function envs (options = {}) {
|
|
|
247
309
|
const envKeysFilepath = options.envKeysFilepath || options.envKeysFile || null
|
|
248
310
|
const provider = await providers(options)
|
|
249
311
|
const decryptor = await decryptors(options)
|
|
250
|
-
|
|
251
312
|
for (const env of options.envs || []) {
|
|
252
313
|
if (env.type === TYPE_ENV_FILE) {
|
|
253
314
|
processedEnvs.push(await injectEnvFile({
|
|
@@ -257,7 +318,9 @@ async function envs (options = {}) {
|
|
|
257
318
|
envKeysFilepath,
|
|
258
319
|
provider,
|
|
259
320
|
decryptor,
|
|
260
|
-
readableFilepaths
|
|
321
|
+
readableFilepaths,
|
|
322
|
+
no1Password: options.no1Password,
|
|
323
|
+
noBitwarden: options.noBitwarden
|
|
261
324
|
}))
|
|
262
325
|
} else if (env.type === TYPE_ENV) {
|
|
263
326
|
processedEnvs.push(await injectEnv({
|
|
@@ -266,7 +329,9 @@ async function envs (options = {}) {
|
|
|
266
329
|
processEnv,
|
|
267
330
|
envKeysFilepath,
|
|
268
331
|
provider,
|
|
269
|
-
decryptor
|
|
332
|
+
decryptor,
|
|
333
|
+
no1Password: options.no1Password,
|
|
334
|
+
noBitwarden: options.noBitwarden
|
|
270
335
|
}))
|
|
271
336
|
}
|
|
272
337
|
}
|
|
@@ -294,7 +359,9 @@ function envsSync (options = {}) {
|
|
|
294
359
|
envKeysFilepath,
|
|
295
360
|
provider,
|
|
296
361
|
decryptor,
|
|
297
|
-
readableFilepaths
|
|
362
|
+
readableFilepaths,
|
|
363
|
+
no1Password: options.no1Password,
|
|
364
|
+
noBitwarden: options.noBitwarden
|
|
298
365
|
}))
|
|
299
366
|
} else if (env.type === TYPE_ENV) {
|
|
300
367
|
processedEnvs.push(injectEnvSync({
|
|
@@ -303,7 +370,9 @@ function envsSync (options = {}) {
|
|
|
303
370
|
processEnv,
|
|
304
371
|
envKeysFilepath,
|
|
305
372
|
provider,
|
|
306
|
-
decryptor
|
|
373
|
+
decryptor,
|
|
374
|
+
no1Password: options.no1Password,
|
|
375
|
+
noBitwarden: options.noBitwarden
|
|
307
376
|
}))
|
|
308
377
|
}
|
|
309
378
|
}
|
package/src/lib/resolvers/get.js
CHANGED
|
@@ -51,6 +51,8 @@ function buildOptions (options, processEnv) {
|
|
|
51
51
|
envKeysFilepath: options.envKeysFilepath || options.envKeysFile || null,
|
|
52
52
|
noArmor: options.noArmor,
|
|
53
53
|
noKeychain: options.noKeychain,
|
|
54
|
+
no1Password: options.no1Password,
|
|
55
|
+
noBitwarden: options.noBitwarden,
|
|
54
56
|
onStatus: options.onStatus
|
|
55
57
|
}
|
|
56
58
|
}
|