@dotenvx/dotenvx 2.5.0 → 2.7.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 -2
- package/README.md +96 -2
- package/package.json +1 -1
- package/src/cli/actions/decrypt.js +11 -1
- package/src/cli/actions/get.js +12 -0
- package/src/cli/actions/run.js +30 -3
- package/src/cli/commands/native.js +1 -1
- package/src/cli/dotenvx.js +4 -1
- package/src/lib/helpers/linuxSecretService.js +43 -0
- package/src/lib/helpers/mask.js +22 -0
- package/src/lib/helpers/maskEnvSrc.js +15 -0
- package/src/lib/helpers/maskProcessedEnvs.js +29 -0
- package/src/lib/main.d.ts +16 -0
- package/src/lib/main.js +14 -0
- package/src/lib/providers/index.js +1 -1
- package/src/lib/providers/native/index.js +10 -4
- package/src/lib/services/keychainDown.js +13 -1
- package/src/lib/services/keychainPull.js +8 -1
- package/src/lib/services/keychainPush.js +10 -0
- package/src/lib/services/keychainUp.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,25 @@
|
|
|
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.7.0...main)
|
|
6
|
+
|
|
7
|
+
## [2.7.0](https://github.com/dotenvx/dotenvx/compare/v2.6.0...v2.7.0) (2026-07-13)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Added support for `--mask` on `get, run, decrypt` ([#885](https://github.com/dotenvx/dotenvx/pull/885))
|
|
12
|
+
|
|
13
|
+
## [2.6.0](https://github.com/dotenvx/dotenvx/compare/v2.5.0...v2.6.0) (2026-07-11)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
* Support `native up|down|pull|push` for windows ([#884](https://github.com/dotenvx/dotenvx/pull/884))
|
|
6
18
|
|
|
7
19
|
## [2.5.0](https://github.com/dotenvx/dotenvx/compare/v2.4.2...v2.5.0) (2026-07-11)
|
|
8
20
|
|
|
9
21
|
### Added
|
|
10
22
|
|
|
11
|
-
* Support `native up|down|pull|push` for windows ([#
|
|
23
|
+
* Support `native up|down|pull|push` for windows ([#883](https://github.com/dotenvx/dotenvx/pull/883))
|
|
12
24
|
|
|
13
25
|
## [2.4.1](https://github.com/dotenvx/dotenvx/compare/v2.4.0...v2.4.1) (2026-07-10)
|
|
14
26
|
|
package/README.md
CHANGED
|
@@ -1471,6 +1471,21 @@ $ dotenvx set HELLO Dotenvx -fk .env.keys -f apps/app1/.env
|
|
|
1471
1471
|
$ dotenvx run -fk .env.keys -f apps/app1/.env -- yourcommand
|
|
1472
1472
|
```
|
|
1473
1473
|
|
|
1474
|
+
</details>
|
|
1475
|
+
<details><summary>`run --mask`</summary><br>
|
|
1476
|
+
|
|
1477
|
+
Inject masked values into the command. By default, up to the first six characters are visible.
|
|
1478
|
+
|
|
1479
|
+
```sh
|
|
1480
|
+
$ echo "SECRET=abcdefghijkl" > .env
|
|
1481
|
+
$ echo "console.log(process.env.SECRET)" > index.js
|
|
1482
|
+
|
|
1483
|
+
$ dotenvx run --mask --quiet -- node index.js
|
|
1484
|
+
abcdef******
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
Pass a number to control how many characters are visible, such as `--mask 0` to fully mask values.
|
|
1488
|
+
|
|
1474
1489
|
</details>
|
|
1475
1490
|
<details><summary>`run --token`</summary><br>
|
|
1476
1491
|
|
|
@@ -1510,6 +1525,20 @@ $ dotenvx get HELLO
|
|
|
1510
1525
|
World
|
|
1511
1526
|
```
|
|
1512
1527
|
|
|
1528
|
+
</details>
|
|
1529
|
+
<details><summary>`get KEY --mask`</summary><br>
|
|
1530
|
+
|
|
1531
|
+
Return a masked environment variable value. By default, up to the first six characters are visible.
|
|
1532
|
+
|
|
1533
|
+
```sh
|
|
1534
|
+
$ echo "SECRET=abcdefghijkl" > .env
|
|
1535
|
+
|
|
1536
|
+
$ dotenvx get SECRET --mask
|
|
1537
|
+
abcdef******
|
|
1538
|
+
```
|
|
1539
|
+
|
|
1540
|
+
Pass a number to control how many characters are visible, such as `--mask 0` to fully mask values.
|
|
1541
|
+
|
|
1513
1542
|
</details>
|
|
1514
1543
|
<details><summary>`get KEY --no-native`</summary><br>
|
|
1515
1544
|
|
|
@@ -2141,6 +2170,23 @@ or send to a file:
|
|
|
2141
2170
|
$ dotenvx decrypt --stdout > somefile.txt
|
|
2142
2171
|
```
|
|
2143
2172
|
|
|
2173
|
+
</details>
|
|
2174
|
+
<details><summary>`decrypt --stdout --mask`</summary><br>
|
|
2175
|
+
|
|
2176
|
+
Decrypt the contents of an encrypted `.env` file to stdout with its values masked. The encrypted `.env` file remains unchanged.
|
|
2177
|
+
|
|
2178
|
+
```sh
|
|
2179
|
+
$ echo "SECRET=abcdefghijkl" > .env
|
|
2180
|
+
$ dotenvx encrypt
|
|
2181
|
+
◈ encrypted (.env)
|
|
2182
|
+
|
|
2183
|
+
$ dotenvx decrypt --stdout --mask
|
|
2184
|
+
...
|
|
2185
|
+
SECRET="abcdef******"
|
|
2186
|
+
```
|
|
2187
|
+
|
|
2188
|
+
Pass a number to control how many characters are visible, such as `--mask 0` to fully mask values.
|
|
2189
|
+
|
|
2144
2190
|
</details>
|
|
2145
2191
|
<details><summary>`keypair`</summary><br>
|
|
2146
2192
|
|
|
@@ -2484,7 +2530,9 @@ $ dotenvx lock down
|
|
|
2484
2530
|
|
|
2485
2531
|
Move private keys into your OS secret store.
|
|
2486
2532
|
|
|
2487
|
-
Native commands support macOS Keychain and Windows Credential Manager.
|
|
2533
|
+
Native commands support macOS Keychain, Linux Secret Service, and Windows Credential Manager.
|
|
2534
|
+
|
|
2535
|
+
On Linux, `secret-tool` and an available Secret Service are required.
|
|
2488
2536
|
|
|
2489
2537
|
</details>
|
|
2490
2538
|
<details><summary>`native up`</summary><br>
|
|
@@ -2646,7 +2694,7 @@ Commands:
|
|
|
2646
2694
|
|
|
2647
2695
|
Professional Security:
|
|
2648
2696
|
lock ⊡ lock private keys with a local passphrase
|
|
2649
|
-
native ⌥ move private keys into your OS secret store
|
|
2697
|
+
native ⌥ move private keys into your OS secret store
|
|
2650
2698
|
armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]
|
|
2651
2699
|
```
|
|
2652
2700
|
|
|
@@ -2723,6 +2771,33 @@ Hello Dotenvx
|
|
|
2723
2771
|
|
|
2724
2772
|
It defaults to looking for a `.env` file.
|
|
2725
2773
|
|
|
2774
|
+
</details>
|
|
2775
|
+
<details><summary>`config(mask: true)` - mask</summary><br>
|
|
2776
|
+
|
|
2777
|
+
Inject and return masked values. By default, up to the first six characters are visible.
|
|
2778
|
+
|
|
2779
|
+
```ini
|
|
2780
|
+
# .env
|
|
2781
|
+
SECRET="abcdefghijkl"
|
|
2782
|
+
```
|
|
2783
|
+
|
|
2784
|
+
```js
|
|
2785
|
+
// index.js
|
|
2786
|
+
const dotenvx = require('@dotenvx/dotenvx')
|
|
2787
|
+
const result = dotenvx.config({ mask: true, quiet: true })
|
|
2788
|
+
|
|
2789
|
+
console.log(process.env.SECRET)
|
|
2790
|
+
console.log(result.parsed.SECRET)
|
|
2791
|
+
```
|
|
2792
|
+
|
|
2793
|
+
```sh
|
|
2794
|
+
$ node index.js
|
|
2795
|
+
abcdef******
|
|
2796
|
+
abcdef******
|
|
2797
|
+
```
|
|
2798
|
+
|
|
2799
|
+
Set `mask: 0` to fully mask values.
|
|
2800
|
+
|
|
2726
2801
|
</details>
|
|
2727
2802
|
<details><summary>`config(path: ['.env.local', '.env'])` - multiple files</summary><br>
|
|
2728
2803
|
|
|
@@ -3049,6 +3124,25 @@ console.log(decryptedValue)
|
|
|
3049
3124
|
|
|
3050
3125
|
This is known as *Decryption at Access* and is written about in [the whitepaper](https://dotenvx.com/dotenvx.pdf).
|
|
3051
3126
|
|
|
3127
|
+
</details>
|
|
3128
|
+
<details><summary>`get(KEY, {mask:})`</summary><br>
|
|
3129
|
+
|
|
3130
|
+
Programmatically return a masked environment variable value.
|
|
3131
|
+
|
|
3132
|
+
```js
|
|
3133
|
+
// index.js
|
|
3134
|
+
const dotenvx = require('@dotenvx/dotenvx')
|
|
3135
|
+
const maskedValue = await dotenvx.get('SECRET', { mask: true })
|
|
3136
|
+
console.log(maskedValue)
|
|
3137
|
+
```
|
|
3138
|
+
|
|
3139
|
+
```sh
|
|
3140
|
+
$ node index.js
|
|
3141
|
+
abcdef******
|
|
3142
|
+
```
|
|
3143
|
+
|
|
3144
|
+
Set `mask: 0` to fully mask values.
|
|
3145
|
+
|
|
3052
3146
|
</details>
|
|
3053
3147
|
|
|
3054
3148
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ const createSpinner = require('../../lib/helpers/createSpinner')
|
|
|
6
6
|
const Session = require('../../db/session')
|
|
7
7
|
|
|
8
8
|
const decryptTransform = require('./../../lib/transforms/decrypt')
|
|
9
|
+
const maskEnvSrc = require('../../lib/helpers/maskEnvSrc')
|
|
9
10
|
|
|
10
11
|
async function decrypt () {
|
|
11
12
|
const options = this.opts()
|
|
@@ -44,7 +45,16 @@ async function decrypt () {
|
|
|
44
45
|
errorCount += 1
|
|
45
46
|
logger.error(processedEnv.error.messageWithHelp || processedEnv.error.message)
|
|
46
47
|
} else {
|
|
47
|
-
|
|
48
|
+
let showChar = options.mask
|
|
49
|
+
if (options.mask === true) {
|
|
50
|
+
showChar = 6
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let envSrc = processedEnv.envSrc
|
|
54
|
+
if (options.mask !== undefined) {
|
|
55
|
+
envSrc = maskEnvSrc(processedEnv.envSrc, showChar)
|
|
56
|
+
}
|
|
57
|
+
console.log(envSrc)
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
package/src/cli/actions/get.js
CHANGED
|
@@ -8,6 +8,7 @@ const getResolver = require('./../../lib/resolvers/get')
|
|
|
8
8
|
const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
|
|
9
9
|
const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
|
|
10
10
|
const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
|
|
11
|
+
const mask = require('../../lib/helpers/mask')
|
|
11
12
|
|
|
12
13
|
async function get (key) {
|
|
13
14
|
const options = normalizeDotenvConfigConvention(this.opts())
|
|
@@ -55,6 +56,17 @@ async function get (key) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
if (spinner) spinner.stop()
|
|
59
|
+
if (options.mask !== undefined) {
|
|
60
|
+
let showChar = options.mask
|
|
61
|
+
if (options.mask === true) {
|
|
62
|
+
showChar = 6
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const key of Object.keys(parsed)) {
|
|
66
|
+
parsed[key] = mask(parsed[key], showChar)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
if (key) {
|
|
59
71
|
const single = parsed[key]
|
|
60
72
|
if (single === undefined) {
|
package/src/cli/actions/run.js
CHANGED
|
@@ -10,6 +10,9 @@ const normalizeDotenvConfigQuiet = require('../../lib/helpers/normalizeDotenvCon
|
|
|
10
10
|
const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
|
|
11
11
|
const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
|
|
12
12
|
const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
|
|
13
|
+
const mask = require('../../lib/helpers/mask')
|
|
14
|
+
const maskEnvSrc = require('../../lib/helpers/maskEnvSrc')
|
|
15
|
+
const maskProcessedEnvs = require('../../lib/helpers/maskProcessedEnvs')
|
|
13
16
|
|
|
14
17
|
const { determine } = require('./../../lib/helpers/envResolution')
|
|
15
18
|
|
|
@@ -48,6 +51,12 @@ function uniqueInjectedKeys (processedEnvs) {
|
|
|
48
51
|
|
|
49
52
|
async function run () {
|
|
50
53
|
const options = normalizeDotenvConfigConvention(normalizeDotenvConfigQuiet(this.opts()))
|
|
54
|
+
const maskEnabled = options.mask !== undefined
|
|
55
|
+
let showChar = options.mask
|
|
56
|
+
if (options.mask === true) {
|
|
57
|
+
showChar = 6
|
|
58
|
+
}
|
|
59
|
+
let commandEnv = process.env
|
|
51
60
|
|
|
52
61
|
let commandArgs = this.args
|
|
53
62
|
if (commandArgs.length < 1) {
|
|
@@ -56,7 +65,16 @@ async function run () {
|
|
|
56
65
|
|
|
57
66
|
const spinner = await createSpinner({ ...options, text: 'injecting' })
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
let debugOptions = options
|
|
69
|
+
if (maskEnabled) {
|
|
70
|
+
let token = options.token
|
|
71
|
+
if (options.token) {
|
|
72
|
+
token = mask(options.token, showChar)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
debugOptions = { ...options, env: (options.env || []).map(envSrc => maskEnvSrc(envSrc, showChar)), token }
|
|
76
|
+
}
|
|
77
|
+
logger.debug(`options: ${JSON.stringify(debugOptions)}`)
|
|
60
78
|
logger.debug(`process command [${commandArgs.join(' ')}]`)
|
|
61
79
|
|
|
62
80
|
const ignore = options.ignore || []
|
|
@@ -103,13 +121,22 @@ async function run () {
|
|
|
103
121
|
}
|
|
104
122
|
})
|
|
105
123
|
|
|
124
|
+
if (maskEnabled) {
|
|
125
|
+
commandEnv = { ...process.env }
|
|
126
|
+
maskProcessedEnvs(processedEnvs, commandEnv, showChar)
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
for (const processedEnv of processedEnvs) {
|
|
107
130
|
if (processedEnv.type === 'envFile') {
|
|
108
131
|
logger.verbose(`loading env from ${processedEnv.filepath} (${path.resolve(processedEnv.filepath)})`)
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
if (processedEnv.type === 'env') {
|
|
112
|
-
|
|
135
|
+
let envString = processedEnv.string
|
|
136
|
+
if (maskEnabled) {
|
|
137
|
+
envString = maskEnvSrc(processedEnv.string, showChar)
|
|
138
|
+
}
|
|
139
|
+
logger.verbose(`loading env from string (${envString})`)
|
|
113
140
|
}
|
|
114
141
|
|
|
115
142
|
for (const error of processedEnv.errors || []) {
|
|
@@ -161,7 +188,7 @@ async function run () {
|
|
|
161
188
|
process.exit(1)
|
|
162
189
|
}
|
|
163
190
|
|
|
164
|
-
await executeCommand(commandArgs,
|
|
191
|
+
await executeCommand(commandArgs, commandEnv)
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
module.exports = run
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -69,6 +69,7 @@ program.command('run')
|
|
|
69
69
|
.option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
|
|
70
70
|
.option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
|
|
71
71
|
.option('--token <token>', 'set Armor ⛨ token')
|
|
72
|
+
.option('--mask [characters]', 'inject masked values, optionally setting visible characters')
|
|
72
73
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
73
74
|
.option('--no-native', 'disable OS secret store features')
|
|
74
75
|
.action(function (...args) {
|
|
@@ -89,6 +90,7 @@ program.command('get')
|
|
|
89
90
|
.option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
|
|
90
91
|
.option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
|
|
91
92
|
.option('-a, --all', 'include all machine envs as well')
|
|
93
|
+
.option('--mask [characters]', 'mask values, optionally setting visible characters')
|
|
92
94
|
.option('-pp, --pretty-print', 'pretty print output')
|
|
93
95
|
.option('--pp', 'pretty print output (alias)')
|
|
94
96
|
.option('--format <type>', 'format of the output (json, shell, colon, eval)', 'json')
|
|
@@ -146,6 +148,7 @@ program.command('decrypt')
|
|
|
146
148
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
147
149
|
.option('--no-native', 'disable OS secret store features')
|
|
148
150
|
.option('--stdout', 'send to stdout')
|
|
151
|
+
.option('--mask [characters]', 'mask stdout values, optionally setting visible characters')
|
|
149
152
|
.action(function (...args) {
|
|
150
153
|
this.envs = envs
|
|
151
154
|
return require('./actions/decrypt').apply(this, args)
|
|
@@ -260,7 +263,7 @@ program.command('help [command]')
|
|
|
260
263
|
program.addHelpText('after', ' ')
|
|
261
264
|
program.addHelpText('after', 'Professional Security: ')
|
|
262
265
|
program.addHelpText('after', ' lock ⊡ lock private keys with a local passphrase')
|
|
263
|
-
program.addHelpText('after', ' native ⌥ move private keys into your OS secret store
|
|
266
|
+
program.addHelpText('after', ' native ⌥ move private keys into your OS secret store')
|
|
264
267
|
program.addHelpText('after', ' armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
|
|
265
268
|
|
|
266
269
|
// dotenvx native
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process')
|
|
2
|
+
|
|
3
|
+
const SECRET_TOOL_BIN = 'secret-tool'
|
|
4
|
+
const SERVICE = 'dotenvx'
|
|
5
|
+
|
|
6
|
+
function attributes (publicKey) {
|
|
7
|
+
return ['service', SERVICE, 'public-key', publicKey]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findGenericPassword (publicKey) {
|
|
11
|
+
try {
|
|
12
|
+
return execFileSync(SECRET_TOOL_BIN, ['lookup', ...attributes(publicKey)], {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
15
|
+
}).trim() || null
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error('failed to read private key from Linux Secret Service')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addGenericPassword (publicKey, label, privateKey) {
|
|
22
|
+
try {
|
|
23
|
+
execFileSync(SECRET_TOOL_BIN, ['store', `--label=${label}`, ...attributes(publicKey)], {
|
|
24
|
+
input: privateKey,
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['pipe', 'ignore', 'pipe']
|
|
27
|
+
})
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error('failed to save private key to Linux Secret Service')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deleteGenericPassword (publicKey) {
|
|
34
|
+
try {
|
|
35
|
+
execFileSync(SECRET_TOOL_BIN, ['clear', ...attributes(publicKey)], {
|
|
36
|
+
stdio: ['ignore', 'ignore', 'pipe']
|
|
37
|
+
})
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error('failed to delete private key from Linux Secret Service')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { findGenericPassword, addGenericPassword, deleteGenericPassword }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function mask (str, showChar = 6) {
|
|
2
|
+
if (!str || str.length < 1) {
|
|
3
|
+
return ''
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const number = Number(showChar)
|
|
7
|
+
showChar = 0
|
|
8
|
+
if (Number.isFinite(number)) {
|
|
9
|
+
showChar = Math.abs(Math.trunc(number))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let visibleChars = Math.min(showChar, Math.ceil(str.length / 2))
|
|
13
|
+
if (str.length === 1) {
|
|
14
|
+
visibleChars = 0
|
|
15
|
+
}
|
|
16
|
+
const visiblePart = str.slice(0, visibleChars)
|
|
17
|
+
const maskedPart = '*'.repeat(str.length - visibleChars)
|
|
18
|
+
|
|
19
|
+
return visiblePart + maskedPart
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = mask
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { scan, upsert } = require('@dotenvx/primitives')
|
|
2
|
+
|
|
3
|
+
const mask = require('./mask')
|
|
4
|
+
|
|
5
|
+
function maskEnvSrc (envSrc, showChar) {
|
|
6
|
+
const { parsed } = scan(envSrc)
|
|
7
|
+
|
|
8
|
+
for (const [key, values] of Object.entries(parsed)) {
|
|
9
|
+
envSrc = upsert(envSrc, key, values.map(value => mask(value, showChar)))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return envSrc
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = maskEnvSrc
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const mask = require('./mask')
|
|
2
|
+
|
|
3
|
+
function maskProcessedEnvs (processedEnvs, processEnv, showChar) {
|
|
4
|
+
// Track every key resolved from the requested env sources so the same keys
|
|
5
|
+
// can also be masked in the environment receiving the resolved values.
|
|
6
|
+
const resolvedKeys = new Set()
|
|
7
|
+
|
|
8
|
+
for (const processedEnv of processedEnvs) {
|
|
9
|
+
for (const key of Object.keys(processedEnv.parsed || {})) {
|
|
10
|
+
resolvedKeys.add(key)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// These values may be logged, so mask each resolver reporting bucket.
|
|
14
|
+
for (const values of [processedEnv.parsed, processedEnv.injected, processedEnv.existed]) {
|
|
15
|
+
for (const key of Object.keys(values || {})) {
|
|
16
|
+
values[key] = mask(values[key], showChar)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Leave unrelated inherited environment variables unchanged.
|
|
22
|
+
for (const key of resolvedKeys) {
|
|
23
|
+
if (processEnv[key] !== undefined) {
|
|
24
|
+
processEnv[key] = mask(processEnv[key], showChar)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = maskProcessedEnvs
|
package/src/lib/main.d.ts
CHANGED
|
@@ -114,6 +114,14 @@ export interface DotenvConfigOptions {
|
|
|
114
114
|
*/
|
|
115
115
|
processEnv?: DotenvPopulateInput;
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Mask resolved values, optionally setting the number of visible characters.
|
|
119
|
+
* @default false
|
|
120
|
+
* @example require('@dotenvx/dotenvx').config({ mask: true })
|
|
121
|
+
* @example require('@dotenvx/dotenvx').config({ mask: 0 })
|
|
122
|
+
*/
|
|
123
|
+
mask?: boolean | number;
|
|
124
|
+
|
|
117
125
|
/**
|
|
118
126
|
* Customize the path to your .env.keys file. This is useful with monorepos.
|
|
119
127
|
* @default []
|
|
@@ -309,6 +317,14 @@ export function set(
|
|
|
309
317
|
): Promise<SetOutput>;
|
|
310
318
|
|
|
311
319
|
export interface GetOptions {
|
|
320
|
+
/**
|
|
321
|
+
* Mask returned values, optionally setting the number of visible characters.
|
|
322
|
+
* @default false
|
|
323
|
+
* @example require('@dotenvx/dotenvx').get('KEY', { mask: true })
|
|
324
|
+
* @example require('@dotenvx/dotenvx').get('KEY', { mask: 0 })
|
|
325
|
+
*/
|
|
326
|
+
mask?: boolean | number;
|
|
327
|
+
|
|
312
328
|
/**
|
|
313
329
|
* Suppress specific errors like MISSING_ENV_FILE. The error keys can be found
|
|
314
330
|
* in src/lib/helpers/errors.js
|
package/src/lib/main.js
CHANGED
|
@@ -25,6 +25,8 @@ const decryptKeyValue = require('./helpers/cryptography/decryptKeyValue')
|
|
|
25
25
|
const Errors = require('./helpers/errors')
|
|
26
26
|
const normalizeDotenvConfigQuiet = require('./helpers/normalizeDotenvConfigQuiet')
|
|
27
27
|
const normalizeDotenvConfigConvention = require('./helpers/normalizeDotenvConfigConvention')
|
|
28
|
+
const mask = require('./helpers/mask')
|
|
29
|
+
const maskProcessedEnvs = require('./helpers/maskProcessedEnvs')
|
|
28
30
|
|
|
29
31
|
function uniqueInjectedKeys (processedEnvs) {
|
|
30
32
|
const result = new Set()
|
|
@@ -88,6 +90,11 @@ const config = function (options = {}) {
|
|
|
88
90
|
token: options.token
|
|
89
91
|
})
|
|
90
92
|
|
|
93
|
+
if (options.mask !== undefined) {
|
|
94
|
+
const showChar = options.mask === true ? 6 : options.mask
|
|
95
|
+
maskProcessedEnvs(processedEnvs, processEnv, showChar)
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
let lastError
|
|
92
99
|
/** @type {Record<string, string>} */
|
|
93
100
|
const parsedAll = {}
|
|
@@ -326,6 +333,13 @@ const get = async function (key, options = {}) {
|
|
|
326
333
|
noKeychain
|
|
327
334
|
})
|
|
328
335
|
|
|
336
|
+
if (options.mask !== undefined) {
|
|
337
|
+
const showChar = options.mask === true ? 6 : options.mask
|
|
338
|
+
for (const key of Object.keys(parsed)) {
|
|
339
|
+
parsed[key] = mask(parsed[key], showChar)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
329
343
|
for (const error of errors || []) {
|
|
330
344
|
if (ignore.includes(error.code)) {
|
|
331
345
|
continue // ignore error
|
|
@@ -44,7 +44,7 @@ function armorProviderForOptions (options) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function useNative (options) {
|
|
47
|
-
if (
|
|
47
|
+
if (!['darwin', 'linux', 'win32'].includes(process.platform)) return false
|
|
48
48
|
if (process.env.CI) return false
|
|
49
49
|
return options.noNative !== true && options.native !== false && options.noKeychain !== true
|
|
50
50
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { execFileSync } = require('child_process')
|
|
2
2
|
|
|
3
3
|
const windowsCredentialManager = require('../../helpers/windowsCredentialManager')
|
|
4
|
+
const linuxSecretService = require('../../helpers/linuxSecretService')
|
|
4
5
|
|
|
5
6
|
const SECURITY_BIN = '/usr/bin/security'
|
|
6
7
|
const SERVICE = 'dotenvx'
|
|
@@ -10,12 +11,17 @@ function findMacosPrivateKey (publicKeyHex) {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function index (publicKeyHex) {
|
|
13
|
-
if (
|
|
14
|
+
if (!['darwin', 'linux', 'win32'].includes(process.platform)) return {}
|
|
14
15
|
|
|
15
16
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
let privateKeyHex
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
privateKeyHex = windowsCredentialManager.findGenericPassword(publicKeyHex)
|
|
20
|
+
} else if (process.platform === 'linux') {
|
|
21
|
+
privateKeyHex = linuxSecretService.findGenericPassword(publicKeyHex)
|
|
22
|
+
} else {
|
|
23
|
+
privateKeyHex = findMacosPrivateKey(publicKeyHex)
|
|
24
|
+
}
|
|
19
25
|
|
|
20
26
|
if (!privateKeyHex) return {}
|
|
21
27
|
|
|
@@ -5,6 +5,7 @@ const readEnvKey = require('../helpers/readEnvKey')
|
|
|
5
5
|
const upsertEnvKey = require('../helpers/upsertEnvKey')
|
|
6
6
|
const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
|
|
7
7
|
const windowsCredentialManager = require('../helpers/windowsCredentialManager')
|
|
8
|
+
const linuxSecretService = require('../helpers/linuxSecretService')
|
|
8
9
|
|
|
9
10
|
const SECURITY_BIN = '/usr/bin/security'
|
|
10
11
|
const SERVICE = 'dotenvx'
|
|
@@ -14,6 +15,10 @@ function findGenericPassword (publicKey) {
|
|
|
14
15
|
return windowsCredentialManager.findGenericPassword(publicKey)
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
if (process.platform === 'linux') {
|
|
19
|
+
return linuxSecretService.findGenericPassword(publicKey)
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
18
23
|
}
|
|
19
24
|
|
|
@@ -23,6 +28,11 @@ function deleteGenericPassword (publicKey) {
|
|
|
23
28
|
return
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
if (process.platform === 'linux') {
|
|
32
|
+
linuxSecretService.deleteGenericPassword(publicKey)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
execFileSync(SECURITY_BIN, ['delete-generic-password', '-s', SERVICE, '-a', publicKey], { stdio: 'ignore' })
|
|
27
37
|
}
|
|
28
38
|
|
|
@@ -47,7 +57,9 @@ class KeychainDown {
|
|
|
47
57
|
try {
|
|
48
58
|
privateKey = findGenericPassword(publicKey)
|
|
49
59
|
if (!privateKey) {
|
|
50
|
-
const secretStore = process.platform === 'win32'
|
|
60
|
+
const secretStore = process.platform === 'win32'
|
|
61
|
+
? 'Windows Credential Manager'
|
|
62
|
+
: process.platform === 'linux' ? 'Linux Secret Service' : 'macOS Keychain'
|
|
51
63
|
throw new Error(`[NOT_FOUND] private key not found in ${secretStore} (${armoredKeyDisplay(publicKey)}). fix: [dotenvx native up]`)
|
|
52
64
|
}
|
|
53
65
|
} catch (error) {
|
|
@@ -5,6 +5,7 @@ const readEnvKey = require('../helpers/readEnvKey')
|
|
|
5
5
|
const upsertEnvKey = require('../helpers/upsertEnvKey')
|
|
6
6
|
const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
|
|
7
7
|
const windowsCredentialManager = require('../helpers/windowsCredentialManager')
|
|
8
|
+
const linuxSecretService = require('../helpers/linuxSecretService')
|
|
8
9
|
|
|
9
10
|
const SECURITY_BIN = '/usr/bin/security'
|
|
10
11
|
const SERVICE = 'dotenvx'
|
|
@@ -14,6 +15,10 @@ function findGenericPassword (publicKey) {
|
|
|
14
15
|
return windowsCredentialManager.findGenericPassword(publicKey)
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
if (process.platform === 'linux') {
|
|
19
|
+
return linuxSecretService.findGenericPassword(publicKey)
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
18
23
|
}
|
|
19
24
|
|
|
@@ -39,7 +44,9 @@ class KeychainPull {
|
|
|
39
44
|
privateKey = findGenericPassword(publicKey)
|
|
40
45
|
if (!privateKey) throw new Error('not found')
|
|
41
46
|
} catch {
|
|
42
|
-
const secretStore = process.platform === 'win32'
|
|
47
|
+
const secretStore = process.platform === 'win32'
|
|
48
|
+
? 'Windows Credential Manager'
|
|
49
|
+
: process.platform === 'linux' ? 'Linux Secret Service' : 'macOS Keychain'
|
|
43
50
|
throw new Error(`[NOT_FOUND] private key not found in ${secretStore} (${armoredKeyDisplay(publicKey)}). fix: [dotenvx native up]`)
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -4,6 +4,7 @@ const keynames = require('../conventions/keynames')
|
|
|
4
4
|
const readEnvKey = require('../helpers/readEnvKey')
|
|
5
5
|
const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
|
|
6
6
|
const windowsCredentialManager = require('../helpers/windowsCredentialManager')
|
|
7
|
+
const linuxSecretService = require('../helpers/linuxSecretService')
|
|
7
8
|
|
|
8
9
|
const SECURITY_BIN = '/usr/bin/security'
|
|
9
10
|
const SERVICE = 'dotenvx'
|
|
@@ -14,6 +15,11 @@ function addGenericPassword (publicKey, label, privateKey) {
|
|
|
14
15
|
return
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
if (process.platform === 'linux') {
|
|
19
|
+
linuxSecretService.addGenericPassword(publicKey, label, privateKey)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
try {
|
|
18
24
|
execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', publicKey, '-l', label, '-w', privateKey], { stdio: 'ignore' })
|
|
19
25
|
} catch {
|
|
@@ -26,6 +32,10 @@ function findGenericPassword (publicKey) {
|
|
|
26
32
|
return windowsCredentialManager.findGenericPassword(publicKey)
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
if (process.platform === 'linux') {
|
|
36
|
+
return linuxSecretService.findGenericPassword(publicKey)
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
30
40
|
}
|
|
31
41
|
|
|
@@ -5,6 +5,7 @@ const readEnvKey = require('../helpers/readEnvKey')
|
|
|
5
5
|
const removeEnvKey = require('../helpers/removeEnvKey')
|
|
6
6
|
const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
|
|
7
7
|
const windowsCredentialManager = require('../helpers/windowsCredentialManager')
|
|
8
|
+
const linuxSecretService = require('../helpers/linuxSecretService')
|
|
8
9
|
|
|
9
10
|
const SECURITY_BIN = '/usr/bin/security'
|
|
10
11
|
const SERVICE = 'dotenvx'
|
|
@@ -15,6 +16,11 @@ function addGenericPassword (publicKey, label, privateKey) {
|
|
|
15
16
|
return
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
if (process.platform === 'linux') {
|
|
20
|
+
linuxSecretService.addGenericPassword(publicKey, label, privateKey)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
try {
|
|
19
25
|
execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', publicKey, '-l', label, '-w', privateKey], { stdio: 'ignore' })
|
|
20
26
|
} catch {
|
|
@@ -27,6 +33,10 @@ function findGenericPassword (publicKey) {
|
|
|
27
33
|
return windowsCredentialManager.findGenericPassword(publicKey)
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
if (process.platform === 'linux') {
|
|
37
|
+
return linuxSecretService.findGenericPassword(publicKey)
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
31
41
|
}
|
|
32
42
|
|