@dotenvx/dotenvx 2.28.0 → 2.28.2
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 +40 -8
- package/package.json +1 -1
- package/src/cli/actions/define.js +17 -0
- package/src/cli/actions/encrypt.js +1 -1
- package/src/cli/actions/ext/precommit.js +3 -0
- package/src/cli/actions/ext/precommitClean.js +21 -0
- package/src/cli/actions/get.js +2 -1
- package/src/cli/actions/init.js +3 -0
- package/src/cli/actions/lock/down.js +13 -32
- package/src/cli/actions/lock/up.js +13 -30
- package/src/cli/actions/run.js +2 -1
- package/src/cli/actions/set.js +1 -1
- package/src/cli/commands/custody.js +2 -2
- package/src/cli/commands/ext.js +3 -1
- package/src/cli/commands/fileOptions.js +22 -0
- package/src/cli/dotenvx.js +38 -21
- package/src/lib/custodians/index.js +73 -0
- package/src/lib/{helpers/bitwardenCustody.js → custodians/local/bitwarden.js} +12 -7
- package/src/lib/custodians/local/file.js +18 -0
- package/src/lib/custodians/local/native/backend.js +62 -0
- package/src/lib/custodians/local/native/index.js +28 -0
- package/src/lib/{helpers/storeNativePrivateKey.js → custodians/local/native/store.js} +3 -3
- package/src/lib/{helpers/onePasswordCustody.js → custodians/local/onepassword.js} +9 -5
- package/src/lib/custodians/lock.js +80 -0
- package/src/lib/{providers/armor/index.js → custodians/managed/armor/get.js} +6 -6
- package/src/lib/custodians/managed/armor/index.js +24 -0
- package/src/lib/custodians/managed/armor/store.js +37 -0
- package/src/lib/helpers/fsx.js +16 -5
- package/src/lib/helpers/installPrecommitFilter.js +50 -0
- package/src/lib/helpers/installPrecommitHook.js +2 -2
- package/src/lib/helpers/lockedValue.js +24 -0
- package/src/lib/helpers/macosKeychain.js +18 -2
- package/src/lib/helpers/matchesStoredKey.js +16 -0
- package/src/lib/helpers/normalizeDotenvConfigPath.js +1 -1
- package/src/lib/helpers/parseWithDecryptor.js +3 -0
- package/src/lib/helpers/prompts.js +18 -0
- package/src/lib/helpers/removeEnvKey.js +2 -1
- package/src/lib/helpers/resolveLockPassword.js +5 -0
- package/src/lib/helpers/selectKeyStorage.js +9 -21
- package/src/lib/helpers/unlockedValue.js +26 -0
- package/src/lib/helpers/upsertEnvKey.js +2 -1
- package/src/lib/helpers/withLockedKeys.js +41 -0
- package/src/lib/main.d.ts +6 -0
- package/src/lib/main.js +5 -2
- package/src/lib/providers/index.js +9 -66
- package/src/lib/providers/native/index.js +2 -62
- package/src/lib/providers/provider-worker.js +1 -1
- package/src/lib/resolvers/envs.js +14 -5
- package/src/lib/resolvers/get.js +1 -0
- package/src/lib/services/custodyTransfer.js +2 -2
- package/src/lib/services/init.js +49 -0
- package/src/lib/services/precommit.js +12 -1
- package/src/lib/services/validate.js +1 -0
- package/src/lib/transforms/encrypt.js +6 -49
- package/src/lib/transforms/set.js +6 -49
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.28.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.28.2...main)
|
|
6
|
+
|
|
7
|
+
## [2.28.2](https://github.com/dotenvx/dotenvx/compare/v2.28.1...v2.28.2) (2026-09-19)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
* Only owner can write to and read from .env.keys file ([#979](https://github.com/dotenvx/dotenvx/pull/979))
|
|
12
|
+
|
|
13
|
+
## [2.28.1](https://github.com/dotenvx/dotenvx/compare/v2.28.0...v2.28.1) (2026-09-19)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
* Send pk via stdin to keychain. was argv. thank you @kta1kri for the security report. ([#978](https://github.com/dotenvx/dotenvx/pull/978))
|
|
18
|
+
* Move some commands under a hidden menu ([#973](https://github.com/dotenvx/dotenvx/pull/973))
|
|
6
19
|
|
|
7
20
|
## [2.28.0](https://github.com/dotenvx/dotenvx/compare/v2.27.0...v2.28.0) (2026-09-16)
|
|
8
21
|
|
package/README.md
CHANGED
|
@@ -1599,7 +1599,7 @@ end
|
|
|
1599
1599
|
|
|
1600
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
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 `
|
|
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_FILE` use their resolved file paths.
|
|
1603
1603
|
|
|
1604
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
1605
|
|
|
@@ -2790,6 +2790,26 @@ $ dotenvx ls --json
|
|
|
2790
2790
|
$ dotenvx ls --json > dotenv-files.json
|
|
2791
2791
|
```
|
|
2792
2792
|
|
|
2793
|
+
</details>
|
|
2794
|
+
<details><summary>`define` (hidden)</summary><br>
|
|
2795
|
+
|
|
2796
|
+
Create an `Envfile` to validate your project's environment variables:
|
|
2797
|
+
|
|
2798
|
+
```sh
|
|
2799
|
+
$ dotenvx define
|
|
2800
|
+
≡ defined (Envfile)
|
|
2801
|
+
```
|
|
2802
|
+
|
|
2803
|
+
Merges variable names from both `.env.example` and `.env`, skipping missing files and including each name once. Use `dotenvx define -f .env.production` to read only a specific input. If neither default file exists, it creates a starter. An existing `Envfile` is always left unchanged. The hidden `dotenvx init` command currently performs the same step.
|
|
2804
|
+
|
|
2805
|
+
The first line is `encrypted false` by default, or `encrypted true` if any application variable assignment in either input starts with `encrypted:`. Duplicate assignments are all inspected. This sets the encryption requirement for all declarations; use `encrypted: false` on individual variables that should remain plaintext.
|
|
2806
|
+
|
|
2807
|
+
Generated declarations are required by default. Review them and mark optional variables with `optional: true`.
|
|
2808
|
+
|
|
2809
|
+
Only names are copied: values are never included, decrypted, expanded, or fetched from secret providers. Dotenvx public and private key entries are excluded. Your env files are unchanged.
|
|
2810
|
+
|
|
2811
|
+
Run `dotenvx validate` (or `dotenvx validate -f .env.production` for another file) to check your configuration. Once `Envfile` exists, `dotenvx run` validates automatically before starting your command.
|
|
2812
|
+
|
|
2793
2813
|
</details>
|
|
2794
2814
|
<details><summary>`validate`</summary><br>
|
|
2795
2815
|
|
|
@@ -2914,11 +2934,22 @@ $ dotenvx precommit
|
|
|
2914
2934
|
</details>
|
|
2915
2935
|
<details><summary>`precommit --install`</summary><br>
|
|
2916
2936
|
|
|
2917
|
-
Install a
|
|
2937
|
+
Install a pre-commit hook and a required Git clean filter in the current repository. The filter rejects plaintext `.env*` files before staging, including with `git add -A` or `git add -f`. Encrypted files pass through unchanged. `.env.example`, `.env.vault`, and `.env.x` retain their exemptions; `.env.keys*` files are always rejected.
|
|
2938
|
+
|
|
2939
|
+
The filter uses repository-local Git configuration and `info/attributes`; run the installer in each clone. Keep the installed dotenvx executable available, or staging protected files will fail. It does not inspect content already staged before installation, and local Git configuration can be overridden.
|
|
2940
|
+
|
|
2941
|
+
To enable the clean filter for all existing and future repositories for your user, run this once, even outside a Git repository:
|
|
2942
|
+
|
|
2943
|
+
```sh
|
|
2944
|
+
$ dotenvx precommit --install --global
|
|
2945
|
+
```
|
|
2946
|
+
|
|
2947
|
+
This writes the filter configuration to your global Git config and adds the `.env*` rule to your global attributes file. It preserves an existing `core.attributesFile`; otherwise it uses `$XDG_CONFIG_HOME/git/attributes` or `~/.config/git/attributes`. It leaves hooks unchanged. Repository attributes and configuration can override the global protection. Use a persistent dotenvx installation: moving or removing its executable requires reinstalling the filter.
|
|
2918
2948
|
|
|
2919
2949
|
```sh
|
|
2920
2950
|
$ dotenvx precommit --install
|
|
2921
2951
|
▣ dotenvx precommit installed [.git/hooks/pre-commit]
|
|
2952
|
+
▣ dotenvx required clean filter installed (blocks staging plaintext .env files)
|
|
2922
2953
|
```
|
|
2923
2954
|
|
|
2924
2955
|
</details>
|
|
@@ -3249,7 +3280,7 @@ inject env at runtime [dotenvx run -- yourcommand]
|
|
|
3249
3280
|
|
|
3250
3281
|
Options:
|
|
3251
3282
|
-e, --env <strings...> environment variable(s) set as string (example: "HELLO=World") (default: [])
|
|
3252
|
-
-f, --
|
|
3283
|
+
-f, --file <paths...> path(s) to your env file(s) (default: [])
|
|
3253
3284
|
-fv, --env-vault-file <paths...> path(s) to your .env.vault file(s) (default: [])
|
|
3254
3285
|
-o, --overload override existing env variables
|
|
3255
3286
|
--convention <name> load a .env convention (available conventions: ['nextjs'])
|
|
@@ -3747,8 +3778,9 @@ There are global settings available that can be configured as environment variab
|
|
|
3747
3778
|
```ini
|
|
3748
3779
|
# config
|
|
3749
3780
|
DOTENV_CONVENTION= # set to a default convention like 'nextjs' or 'flow'
|
|
3750
|
-
|
|
3751
|
-
|
|
3781
|
+
DOTENV_FILE= # path to your env file; comma-separate multiple paths
|
|
3782
|
+
DOTENV_PATH= # synonym for DOTENV_FILE
|
|
3783
|
+
DOTENV_F= # synonym for DOTENV_FILE
|
|
3752
3784
|
DOTENV_IGNORE= # MISSING_ENV_FILE,OTHER
|
|
3753
3785
|
DOTENV_QUIET= # set to "true" to default to --quiet
|
|
3754
3786
|
|
|
@@ -3872,13 +3904,13 @@ Breaking this encryption would require brute-forcing both AES-256 and elliptic c
|
|
|
3872
3904
|
|
|
3873
3905
|
You are using Node 20 or greater and it adds a differing implementation of `--env-file` flag support. Rather than warn on a missing `.env` file (like dotenv has historically done), it raises an error: `node: .env: not found`.
|
|
3874
3906
|
|
|
3875
|
-
This fix is easy. Replace `--env-file` with `-f
|
|
3907
|
+
This fix is easy. Replace `--env-file` with `--file` (or `-f`).
|
|
3876
3908
|
|
|
3877
3909
|
```bash
|
|
3878
|
-
# from this:
|
|
3910
|
+
# from this (legacy spelling):
|
|
3879
3911
|
./node_modules/.bin/dotenvx run --env-file .env -- yourcommand
|
|
3880
3912
|
# to this:
|
|
3881
|
-
./node_modules/.bin/dotenvx run
|
|
3913
|
+
./node_modules/.bin/dotenvx run --file .env -- yourcommand
|
|
3882
3914
|
```
|
|
3883
3915
|
|
|
3884
3916
|
[more context](https://github.com/dotenvx/dotenvx/issues/131)
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const initEnvfile = require('../../lib/services/init')
|
|
2
|
+
const { logger } = require('../../shared/logger')
|
|
3
|
+
const catchAndLog = require('../../lib/helpers/catchAndLog')
|
|
4
|
+
|
|
5
|
+
module.exports = function define () {
|
|
6
|
+
try {
|
|
7
|
+
const { created } = initEnvfile({ envFile: this.opts().envFile })
|
|
8
|
+
if (created) {
|
|
9
|
+
logger.success('≡ defined (Envfile)')
|
|
10
|
+
} else {
|
|
11
|
+
logger.info('○ Envfile already exists (unchanged)')
|
|
12
|
+
}
|
|
13
|
+
} catch (error) {
|
|
14
|
+
catchAndLog(error)
|
|
15
|
+
process.exitCode = 1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -54,7 +54,7 @@ async function encryptAction () {
|
|
|
54
54
|
const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noNative, no1Password, noBitwarden })
|
|
55
55
|
|
|
56
56
|
if (keysSrc) {
|
|
57
|
-
await fsx.
|
|
57
|
+
await fsx.writeKeyFile(fk, keysSrc)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (spinner) spinner.stop()
|
|
@@ -4,6 +4,9 @@ const Precommit = require('./../../../lib/services/precommit')
|
|
|
4
4
|
const catchAndLog = require('./../../../lib/helpers/catchAndLog')
|
|
5
5
|
|
|
6
6
|
function precommit (directory) {
|
|
7
|
+
if (this.opts().clean !== undefined) {
|
|
8
|
+
return require('./precommitClean')(this.opts().clean)
|
|
9
|
+
}
|
|
7
10
|
// debug args
|
|
8
11
|
logger.debug(`directory: ${directory}`)
|
|
9
12
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { sealed } = require('@dotenvx/primitives')
|
|
4
|
+
|
|
5
|
+
function precommitClean (filepath) {
|
|
6
|
+
try {
|
|
7
|
+
const content = fs.readFileSync(0)
|
|
8
|
+
const filename = path.posix.basename(filepath)
|
|
9
|
+
const exempt = ['.env.example', '.env.vault', '.env.x'].includes(filename)
|
|
10
|
+
if (filename.startsWith('.env.keys') || (!exempt && !sealed(content.toString('utf8')))) {
|
|
11
|
+
throw new Error(`refusing to stage ${JSON.stringify(filepath)}: encrypt this env file or add it to .gitignore`)
|
|
12
|
+
}
|
|
13
|
+
// stdout is Git's blob content: no logging, normalization, or extra newline.
|
|
14
|
+
process.stdout.write(content)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
process.stderr.write(`dotenvx: ${error.message}\n`)
|
|
17
|
+
process.exitCode = 1
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = precommitClean
|
package/src/cli/actions/get.js
CHANGED
|
@@ -18,7 +18,7 @@ async function get (key) {
|
|
|
18
18
|
const spinnerOptions = typeof this.optsWithGlobals === 'function' ? this.optsWithGlobals() : options
|
|
19
19
|
const spinner = await createSpinner({ ...spinnerOptions, ...options, text: 'decrypting' })
|
|
20
20
|
|
|
21
|
-
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
21
|
+
logger.debug(`options: ${JSON.stringify({ ...options, ...(options.lockPassword !== undefined ? { lockPassword: '[REDACTED]' } : {}) })}`)
|
|
22
22
|
if (key) {
|
|
23
23
|
logger.debug(`key: ${key}`)
|
|
24
24
|
}
|
|
@@ -41,6 +41,7 @@ async function get (key) {
|
|
|
41
41
|
envKeysFile: resolveEnvKeysFile(options.envKeysFile),
|
|
42
42
|
noArmor,
|
|
43
43
|
noNative,
|
|
44
|
+
lockPassword: options.lockPassword,
|
|
44
45
|
no1Password: options['1password'] === false || options.no1Password === true,
|
|
45
46
|
noBitwarden: options.bitwarden === false || options.noBitwarden === true,
|
|
46
47
|
onStatus: (text) => {
|
|
@@ -1,32 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const unlockedValue = require('../../../lib/helpers/unlockedValue')
|
|
2
2
|
|
|
3
3
|
const { logger } = require('../../../shared/logger')
|
|
4
4
|
const LockDown = require('./../../../lib/services/lockDown')
|
|
5
5
|
const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
|
|
6
6
|
const prompts = require('../../../lib/helpers/prompts')
|
|
7
|
-
|
|
8
|
-
function unlockedValue (lockedPrivateKey, passphrase) {
|
|
9
|
-
const parts = lockedPrivateKey.split(':')
|
|
10
|
-
const payload = Buffer.from(parts.slice(2).join(':'), 'base64url')
|
|
11
|
-
const version = payload.subarray(0, 1)[0]
|
|
12
|
-
const salt = payload.subarray(1, 17)
|
|
13
|
-
const iv = payload.subarray(17, 29)
|
|
14
|
-
const tag = payload.subarray(29, 45)
|
|
15
|
-
const ciphertext = payload.subarray(45)
|
|
16
|
-
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
17
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
18
|
-
|
|
19
|
-
if (version !== 1) {
|
|
20
|
-
throw new Error('unsupported locked private key version')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
decipher.setAuthTag(tag)
|
|
24
|
-
|
|
25
|
-
return Buffer.concat([
|
|
26
|
-
decipher.update(ciphertext),
|
|
27
|
-
decipher.final()
|
|
28
|
-
]).toString('utf8')
|
|
29
|
-
}
|
|
7
|
+
const resolveLockPassword = require('../../../lib/helpers/resolveLockPassword')
|
|
30
8
|
|
|
31
9
|
async function down () {
|
|
32
10
|
const options = this.opts()
|
|
@@ -38,14 +16,17 @@ async function down () {
|
|
|
38
16
|
let results = plan.alreadyUnlocked
|
|
39
17
|
|
|
40
18
|
if (plan.locked.length > 0) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
19
|
+
const preset = resolveLockPassword(options)
|
|
20
|
+
const passphrase = preset !== undefined
|
|
21
|
+
? preset
|
|
22
|
+
: await prompts.password({
|
|
23
|
+
message: 'passphrase',
|
|
24
|
+
prefix: '⊡',
|
|
25
|
+
separator: '='
|
|
26
|
+
}, {
|
|
27
|
+
input: process.stdin,
|
|
28
|
+
output: process.stderr
|
|
29
|
+
})
|
|
49
30
|
|
|
50
31
|
results = lockDown.run(lockedPrivateKey => unlockedValue(lockedPrivateKey, passphrase)).results
|
|
51
32
|
}
|
|
@@ -1,30 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const lockedValue = require('../../../lib/helpers/lockedValue')
|
|
2
2
|
|
|
3
3
|
const { logger } = require('../../../shared/logger')
|
|
4
4
|
const LockUp = require('./../../../lib/services/lockUp')
|
|
5
5
|
const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
|
|
6
6
|
const prompts = require('../../../lib/helpers/prompts')
|
|
7
|
-
|
|
8
|
-
function lockedValue (privateKey, passphrase, publicKey) {
|
|
9
|
-
const salt = crypto.randomBytes(16)
|
|
10
|
-
const iv = crypto.randomBytes(12)
|
|
11
|
-
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
12
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
|
|
13
|
-
const ciphertext = Buffer.concat([
|
|
14
|
-
cipher.update(privateKey, 'utf8'),
|
|
15
|
-
cipher.final()
|
|
16
|
-
])
|
|
17
|
-
const tag = cipher.getAuthTag()
|
|
18
|
-
const payload = Buffer.concat([
|
|
19
|
-
Buffer.from([1]),
|
|
20
|
-
salt,
|
|
21
|
-
iv,
|
|
22
|
-
tag,
|
|
23
|
-
ciphertext
|
|
24
|
-
]).toString('base64url')
|
|
25
|
-
|
|
26
|
-
return `locked:${publicKey}:${payload}`
|
|
27
|
-
}
|
|
7
|
+
const resolveLockPassword = require('../../../lib/helpers/resolveLockPassword')
|
|
28
8
|
|
|
29
9
|
async function up () {
|
|
30
10
|
const options = this.opts()
|
|
@@ -44,14 +24,17 @@ async function up () {
|
|
|
44
24
|
}))
|
|
45
25
|
|
|
46
26
|
if (plan.matches.length > 0) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
const preset = resolveLockPassword(options)
|
|
28
|
+
const passphrase = preset !== undefined
|
|
29
|
+
? preset
|
|
30
|
+
: await prompts.password({
|
|
31
|
+
message: 'passphrase',
|
|
32
|
+
prefix: '⊡',
|
|
33
|
+
separator: '='
|
|
34
|
+
}, {
|
|
35
|
+
input: process.stdin,
|
|
36
|
+
output: process.stderr
|
|
37
|
+
})
|
|
55
38
|
|
|
56
39
|
results = lockUp.run((privateKey, publicKey) => lockedValue(privateKey, passphrase, publicKey)).results
|
|
57
40
|
}
|
package/src/cli/actions/run.js
CHANGED
|
@@ -25,7 +25,7 @@ function inferCommandArgsFromProcessArgv (argv) {
|
|
|
25
25
|
if (separatorIndex !== -1) return args.slice(separatorIndex + 1)
|
|
26
26
|
|
|
27
27
|
for (let i = 0; i < args.length; i++) {
|
|
28
|
-
if (args[i] === '-f' || args[i] === '--env-file') {
|
|
28
|
+
if (args[i] === '-f' || args[i] === '--file' || args[i] === '--env-file') {
|
|
29
29
|
i++
|
|
30
30
|
continue
|
|
31
31
|
}
|
|
@@ -77,6 +77,7 @@ async function run () {
|
|
|
77
77
|
|
|
78
78
|
debugOptions = { ...options, env: (options.env || []).map(envSrc => maskEnvSrc(envSrc, showChar)), token }
|
|
79
79
|
}
|
|
80
|
+
if (options.lockPassword !== undefined) debugOptions = { ...debugOptions, lockPassword: '[REDACTED]' }
|
|
80
81
|
logger.debug(`options: ${JSON.stringify(debugOptions)}`)
|
|
81
82
|
logger.debug(`process command [${commandArgs.join(' ')}]`)
|
|
82
83
|
|
package/src/cli/actions/set.js
CHANGED
|
@@ -70,7 +70,7 @@ async function set (key, value) {
|
|
|
70
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
|
-
await fsx.
|
|
73
|
+
await fsx.writeKeyFile(fk, keysSrc)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
let withEncryption = ''
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function configureCustodyCommand (command, name,
|
|
1
|
+
function configureCustodyCommand (command, name, custodianId) {
|
|
2
2
|
command.hook('preAction', async () => {
|
|
3
3
|
const Session = require('../../db/session')
|
|
4
4
|
await new Session().notifyUpdate()
|
|
@@ -23,7 +23,7 @@ function configureCustodyCommand (command, name, providerPath) {
|
|
|
23
23
|
const options = this.opts()
|
|
24
24
|
const spinner = await createSpinner({ ...this.optsWithGlobals(), text: `${operation === 'up' || operation === 'push' ? 'storing in' : 'reading from'} ${name}` })
|
|
25
25
|
try {
|
|
26
|
-
const result = await transfer(require(
|
|
26
|
+
const result = await transfer(require('../../lib/custodians').get(custodianId), name, operation, options.envFile, options.envKeysFile)
|
|
27
27
|
if (spinner) spinner.stop()
|
|
28
28
|
const display = armoredKeyDisplay(result.publicKeyValue) || result.privateKeyName
|
|
29
29
|
const messages = { up: `stored in ${name}`, down: `moved to ${options.envKeysFile}`, push: `pushed to ${name}`, pull: `pulled to ${options.envKeysFile}` }
|
package/src/cli/commands/ext.js
CHANGED
|
@@ -61,7 +61,9 @@ ext.command('precommit')
|
|
|
61
61
|
.description('prevent committing .env files to code')
|
|
62
62
|
.addHelpText('after', help.precommit)
|
|
63
63
|
.argument('[directory]', 'directory to prevent committing .env files from', '.')
|
|
64
|
-
.option('-i, --install', 'install
|
|
64
|
+
.option('-i, --install', 'install a pre-commit hook and required Git clean filter')
|
|
65
|
+
.option('--global', 'with --install, install the clean filter for all repositories')
|
|
66
|
+
.addOption(ext.createOption('--clean <path>', 'validate Git filter input from stdin').hideHelp())
|
|
65
67
|
.action(function (...args) {
|
|
66
68
|
return require('./../actions/ext/precommit').apply(this, args)
|
|
67
69
|
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Keep the existing envFile option attribute and parsing behavior for both
|
|
2
|
+
// spellings, including mixed repeated flags. Only --file is advertised.
|
|
3
|
+
function configureFileOptions (command) {
|
|
4
|
+
const legacy = command.options.find(option => option.long === '--env-file')
|
|
5
|
+
if (legacy && !command.options.some(option => option.long === '--file')) {
|
|
6
|
+
const index = command.options.indexOf(legacy)
|
|
7
|
+
const visible = command.createOption(legacy.flags.replace('--env-file', '--file'), legacy.description)
|
|
8
|
+
visible.attributeName = () => legacy.attributeName()
|
|
9
|
+
if (legacy.parseArg) visible.argParser(legacy.parseArg)
|
|
10
|
+
if (legacy.defaultValue !== undefined) visible.default(legacy.defaultValue, legacy.defaultValueDescription)
|
|
11
|
+
legacy.short = undefined
|
|
12
|
+
legacy.flags = legacy.flags.replace(/^-f,?\s*/, '')
|
|
13
|
+
legacy.hideHelp()
|
|
14
|
+
command.addOption(visible)
|
|
15
|
+
command.options.pop()
|
|
16
|
+
command.options.splice(index, 0, visible)
|
|
17
|
+
}
|
|
18
|
+
for (const child of command.commands) configureFileOptions(child)
|
|
19
|
+
return command
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = configureFileOptions
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -78,6 +78,7 @@ program.command('run')
|
|
|
78
78
|
.option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
|
|
79
79
|
.option('--token <token>', 'set Armor ⛨ token')
|
|
80
80
|
.option('--mask [characters]', 'inject masked values, optionally setting visible characters')
|
|
81
|
+
.option('--lock-password <password>', 'password to unlock private keys (defaults to DOTENVX_LOCK_PASSWORD)')
|
|
81
82
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
82
83
|
.option('--no-native', 'disable OS secret store features')
|
|
83
84
|
.option('--no-1password', 'disable 1Password secret reference resolution')
|
|
@@ -106,6 +107,7 @@ program.command('get')
|
|
|
106
107
|
.option('-pp, --pretty-print', 'pretty print output')
|
|
107
108
|
.option('--pp', 'pretty print output (alias)')
|
|
108
109
|
.option('--format <type>', 'format of the output (json, shell, colon, eval, eval-export)', 'json')
|
|
110
|
+
.option('--lock-password <password>', 'password to unlock private keys (defaults to DOTENVX_LOCK_PASSWORD)')
|
|
109
111
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
110
112
|
.option('--no-native', 'disable OS secret store features')
|
|
111
113
|
.option('--no-1password', 'disable 1Password secret reference resolution')
|
|
@@ -211,7 +213,7 @@ program.command('keypair')
|
|
|
211
213
|
})
|
|
212
214
|
|
|
213
215
|
// dotenvx ls
|
|
214
|
-
program.command('ls')
|
|
216
|
+
program.command('ls', { hidden: true })
|
|
215
217
|
.description('print all .env files in a tree structure')
|
|
216
218
|
.argument('[directory]', 'directory to list .env files from', '.')
|
|
217
219
|
.option('-f, --env-file <filenames...>', 'path(s) to your env file(s)', '.env*')
|
|
@@ -221,8 +223,24 @@ program.command('ls')
|
|
|
221
223
|
return require('./actions/ls').apply(this, args)
|
|
222
224
|
})
|
|
223
225
|
|
|
226
|
+
// dotenvx init
|
|
227
|
+
program.command('init', { hidden: true })
|
|
228
|
+
.description('create an Envfile from .env.example and .env')
|
|
229
|
+
.option('-f, --env-file <path>', 'file to read variable names from')
|
|
230
|
+
.action(function () {
|
|
231
|
+
return require('./actions/init').apply(this, arguments)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// dotenvx define
|
|
235
|
+
program.command('define', { hidden: true })
|
|
236
|
+
.description('define your environment in an Envfile')
|
|
237
|
+
.option('-f, --env-file <path>', 'file to read variable names from')
|
|
238
|
+
.action(function () {
|
|
239
|
+
return require('./actions/define').apply(this, arguments)
|
|
240
|
+
})
|
|
241
|
+
|
|
224
242
|
// dotenvx validate
|
|
225
|
-
program.command('validate')
|
|
243
|
+
program.command('validate', { hidden: true })
|
|
226
244
|
.description('validate .env file(s) against Envfile')
|
|
227
245
|
.option('--strict', 'process.exit(1) on any errors, including missing env files', false)
|
|
228
246
|
.option('-e, --env <strings...>', 'environment variable(s) set as string (example: "HELLO=World")', collectEnvs('env'), [])
|
|
@@ -242,7 +260,7 @@ program.command('validate')
|
|
|
242
260
|
})
|
|
243
261
|
|
|
244
262
|
// dotenvx gitignore
|
|
245
|
-
program.command('gitignore')
|
|
263
|
+
program.command('gitignore', { hidden: true })
|
|
246
264
|
.description('append to .gitignore')
|
|
247
265
|
.addHelpText('after', help.gitignore)
|
|
248
266
|
.option('--pattern <patterns...>', 'pattern(s) to gitignore', ['.env*'])
|
|
@@ -251,7 +269,7 @@ program.command('gitignore')
|
|
|
251
269
|
})
|
|
252
270
|
|
|
253
271
|
// dotenvx genexample
|
|
254
|
-
program.command('genexample')
|
|
272
|
+
program.command('genexample', { hidden: true })
|
|
255
273
|
.description('generate .env.example')
|
|
256
274
|
.argument('[directory]', 'directory to generate from', '.')
|
|
257
275
|
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
|
|
@@ -260,17 +278,19 @@ program.command('genexample')
|
|
|
260
278
|
})
|
|
261
279
|
|
|
262
280
|
// dotenvx precommit
|
|
263
|
-
program.command('precommit')
|
|
281
|
+
program.command('precommit', { hidden: true })
|
|
264
282
|
.description('prevent committing .env files to code')
|
|
265
283
|
.addHelpText('after', help.precommit)
|
|
266
284
|
.argument('[directory]', 'directory to prevent committing .env files from', '.')
|
|
267
|
-
.option('-i, --install', 'install
|
|
285
|
+
.option('-i, --install', 'install a pre-commit hook and required Git clean filter')
|
|
286
|
+
.option('--global', 'with --install, install the clean filter for all repositories')
|
|
287
|
+
.addOption(program.createOption('--clean <path>', 'validate Git filter input from stdin').hideHelp())
|
|
268
288
|
.action(function (...args) {
|
|
269
289
|
return require('./actions/ext/precommit').apply(this, args)
|
|
270
290
|
})
|
|
271
291
|
|
|
272
292
|
// dotenvx prebuild
|
|
273
|
-
program.command('prebuild')
|
|
293
|
+
program.command('prebuild', { hidden: true })
|
|
274
294
|
.description('prevent including .env files in docker')
|
|
275
295
|
.addHelpText('after', help.prebuild)
|
|
276
296
|
.argument('[directory]', 'directory to prevent including .env files from', '.')
|
|
@@ -286,6 +306,13 @@ program.command('doctor', { hidden: true })
|
|
|
286
306
|
return require('./actions/doctor').apply(this, args)
|
|
287
307
|
})
|
|
288
308
|
|
|
309
|
+
// dotenvx hidden (a menu of top-level commands)
|
|
310
|
+
program.command('hidden')
|
|
311
|
+
.allowExcessArguments(false)
|
|
312
|
+
.description('hidden features')
|
|
313
|
+
.addHelpText('after', '\nHidden Commands:\n init create an Envfile from .env.example and .env\n define define your environment in an Envfile\n validate validate .env file(s) against Envfile\n genexample [directory] generate .env.example\n gitignore append to .gitignore\n ls [directory] print all .env files in a tree structure\n prebuild [directory] prevent including .env files in docker\n precommit [directory] prevent committing .env files to code\n lock ⊡ lock private keys with a local passphrase\n native ⌥ move private keys in/out of your OS secret store\n 1password □ move private keys in/out of 1Password\n bitwarden □ move private keys in/out of Bitwarden\n armor ⛨ move private keys in/out of Dotenvx Armor [www.dotenvx.com/armor]\n curl ⛨ call authenticated api Dotenvx Armor [www.dotenvx.com/armor]\n\nRun directly: dotenvx <command>')
|
|
314
|
+
.action(function () { this.outputHelp() })
|
|
315
|
+
|
|
289
316
|
// dotenvx update
|
|
290
317
|
program.command('update')
|
|
291
318
|
.description('update dotenvx')
|
|
@@ -344,20 +371,8 @@ program.command('help [command]')
|
|
|
344
371
|
}
|
|
345
372
|
})
|
|
346
373
|
|
|
347
|
-
|
|
348
|
-
program.
|
|
349
|
-
program.addHelpText('after', 'Local Custody:')
|
|
350
|
-
program.addHelpText('after', ' lock ⊡ lock private keys with a local passphrase')
|
|
351
|
-
program.addHelpText('after', ' native ⌥ move private keys in/out of your OS secret store')
|
|
352
|
-
program.addHelpText('after', ' 1password □ move private keys in/out of 1Password')
|
|
353
|
-
program.addHelpText('after', ' bitwarden □ move private keys in/out of Bitwarden')
|
|
354
|
-
program.addHelpText('after', ' ')
|
|
355
|
-
program.addHelpText('after', 'Managed Custody:')
|
|
356
|
-
program.addHelpText('after', ' armor ⛨ move private keys in/out of Dotenvx Armor [www.dotenvx.com/armor]')
|
|
357
|
-
program.addHelpText('after', ' curl ⛨ call authenticated api Dotenvx Armor [www.dotenvx.com/armor]')
|
|
358
|
-
|
|
359
|
-
require('./commands/custody')(program.command('1password', { hidden: true }), '1Password', '../../lib/helpers/onePasswordCustody')
|
|
360
|
-
require('./commands/custody')(program.command('bitwarden', { hidden: true }), 'Bitwarden', '../../lib/helpers/bitwardenCustody')
|
|
374
|
+
require('./commands/custody')(program.command('1password', { hidden: true }), '1Password', 'onepassword')
|
|
375
|
+
require('./commands/custody')(program.command('bitwarden', { hidden: true }), 'Bitwarden', 'bitwarden')
|
|
361
376
|
|
|
362
377
|
// dotenvx native
|
|
363
378
|
require('./commands/native')(program.command('native', { hidden: true }))
|
|
@@ -390,4 +405,6 @@ program.helpInformation = function () {
|
|
|
390
405
|
}
|
|
391
406
|
/* c8 ignore stop */
|
|
392
407
|
|
|
408
|
+
require('./commands/fileOptions')(program)
|
|
409
|
+
|
|
393
410
|
program.parse(process.argv)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const protection = require('./lock')
|
|
2
|
+
|
|
3
|
+
// Explicit registration keeps bundling predictable and avoids executing
|
|
4
|
+
// arbitrary modules discovered in a project's working directory.
|
|
5
|
+
const builtins = [
|
|
6
|
+
require('./local/native'),
|
|
7
|
+
require('./local/onepassword'),
|
|
8
|
+
require('./local/bitwarden'),
|
|
9
|
+
require('./local/file'),
|
|
10
|
+
require('./managed/armor')
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
function createRegistry (custodians) {
|
|
14
|
+
const entries = new Map()
|
|
15
|
+
for (const custodian of custodians) {
|
|
16
|
+
if (!custodian || typeof custodian.id !== 'string' || !custodian.id || entries.has(custodian.id)) {
|
|
17
|
+
throw new Error('custodians must have unique, nonempty ids')
|
|
18
|
+
}
|
|
19
|
+
for (const method of ['enabled', 'available', 'store']) {
|
|
20
|
+
if (typeof custodian[method] !== 'function') throw new Error(`custodian ${custodian.id} requires ${method}()`)
|
|
21
|
+
}
|
|
22
|
+
entries.set(custodian.id, custodian)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function get (id) {
|
|
26
|
+
const custodian = entries.get(id)
|
|
27
|
+
if (!custodian) throw new Error(`unknown custodian: ${id}`)
|
|
28
|
+
return custodian
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
get,
|
|
33
|
+
async choices (options = {}, custody = 'local') {
|
|
34
|
+
const choices = []
|
|
35
|
+
for (const custodian of entries.values()) {
|
|
36
|
+
if ((custodian.custody || 'local') !== custody) continue
|
|
37
|
+
choices.push({ name: custodian.name, value: custodian.id, disabled: !custodian.enabled(options) || !await custodian.available() })
|
|
38
|
+
}
|
|
39
|
+
return choices
|
|
40
|
+
},
|
|
41
|
+
providers (options = {}, sync = false) {
|
|
42
|
+
const providers = []
|
|
43
|
+
for (const custodian of entries.values()) {
|
|
44
|
+
if (custodian.custody === 'managed') continue
|
|
45
|
+
const method = sync ? 'getSync' : 'get'
|
|
46
|
+
if (!custodian.enabled(options) || !custodian.get || (custodian.configured && !custodian.configured())) continue
|
|
47
|
+
if (typeof custodian[method] !== 'function') throw new Error(`custodian ${custodian.id} does not support synchronous reads`)
|
|
48
|
+
if (sync) {
|
|
49
|
+
providers.push(publicKey => protection.unlockSync(publicKey, custodian[method](publicKey), options))
|
|
50
|
+
} else {
|
|
51
|
+
providers.push(async publicKey => protection.unlock(publicKey, await custodian[method](publicKey), options))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return providers
|
|
55
|
+
},
|
|
56
|
+
async store (selection, publicKey, privateKey, context = {}) {
|
|
57
|
+
const { id, lock = false } = typeof selection === 'string' ? { id: selection } : selection
|
|
58
|
+
const custodian = get(id)
|
|
59
|
+
if (lock) {
|
|
60
|
+
if (custodian.custody === 'managed') throw new Error('password locking is only supported for local custody')
|
|
61
|
+
privateKey = await protection.lock(publicKey, privateKey, context)
|
|
62
|
+
}
|
|
63
|
+
const result = await custodian.store(publicKey, privateKey, context) || {}
|
|
64
|
+
// Only the native custodian's unavailable-write path requests fallback.
|
|
65
|
+
// Authentication and verification errors propagate without writing a file.
|
|
66
|
+
if (result.fallback) return get(result.fallback).store(publicKey, privateKey, context)
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = createRegistry(builtins)
|
|
73
|
+
module.exports.createRegistry = createRegistry
|