@dotenvx/dotenvx 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,19 +2,31 @@
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.6.0...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.7.1...main)
6
+
7
+ ## [2.7.1](https://github.com/dotenvx/dotenvx/compare/v2.7.0...v2.7.1) (2026-07-13)
8
+
9
+ ### Changed
10
+
11
+ * Security Improvement: `dotenvx armor login` attempts to set the `DOTENVX_ARMOR_TOKEN` in your native OS secret store going forward. For current Armor users run `dotenvx armor logout` and then `dotenvx armor login` to make use of this immediately. ([#886](https://github.com/dotenvx/dotenvx/pull/886))
12
+
13
+ ## [2.7.0](https://github.com/dotenvx/dotenvx/compare/v2.6.0...v2.7.0) (2026-07-13)
14
+
15
+ ### Added
16
+
17
+ * Added support for `--mask` on `get, run, decrypt` ([#885](https://github.com/dotenvx/dotenvx/pull/885))
6
18
 
7
19
  ## [2.6.0](https://github.com/dotenvx/dotenvx/compare/v2.5.0...v2.6.0) (2026-07-11)
8
20
 
9
21
  ### Added
10
22
 
11
- * Support `native up|down|pull|push` for windows ([#834](https://github.com/dotenvx/dotenvx/pull/884))
23
+ * Support `native up|down|pull|push` for windows ([#884](https://github.com/dotenvx/dotenvx/pull/884))
12
24
 
13
25
  ## [2.5.0](https://github.com/dotenvx/dotenvx/compare/v2.4.2...v2.5.0) (2026-07-11)
14
26
 
15
27
  ### Added
16
28
 
17
- * Support `native up|down|pull|push` for windows ([#833](https://github.com/dotenvx/dotenvx/pull/883))
29
+ * Support `native up|down|pull|push` for windows ([#883](https://github.com/dotenvx/dotenvx/pull/883))
18
30
 
19
31
  ## [2.4.1](https://github.com/dotenvx/dotenvx/compare/v2.4.0...v2.4.1) (2026-07-10)
20
32
 
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
 
@@ -2725,6 +2771,33 @@ Hello Dotenvx
2725
2771
 
2726
2772
  It defaults to looking for a `.env` file.
2727
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
+
2728
2801
  </details>
2729
2802
  <details><summary>`config(path: ['.env.local', '.env'])` - multiple files</summary><br>
2730
2803
 
@@ -3051,6 +3124,25 @@ console.log(decryptedValue)
3051
3124
 
3052
3125
  This is known as *Decryption at Access* and is written about in [the whitepaper](https://dotenvx.com/dotenvx.pdf).
3053
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
+
3054
3146
  </details>
3055
3147
 
3056
3148
  &nbsp;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.6.0",
2
+ "version": "2.7.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -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
- console.log(processedEnv.envSrc)
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
 
@@ -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) {
@@ -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
- logger.debug(`options: ${JSON.stringify(options)}`)
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
- logger.verbose(`loading env from string (${processedEnv.string})`)
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, process.env)
191
+ await executeCommand(commandArgs, commandEnv)
165
192
  }
166
193
 
167
194
  module.exports = run
@@ -0,0 +1,23 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const Session = require('../../../db/session')
3
+ const mask = require('../../../lib/helpers/mask')
4
+
5
+ function device () {
6
+ const options = this.opts()
7
+
8
+ try {
9
+ const value = new Session().devicePublicKey()
10
+ if (value && value.length > 1) {
11
+ console.log(options.unmask ? value : mask(value, 6))
12
+ return
13
+ }
14
+
15
+ logger.error('missing device. Try generating one with [dotenvx armor login].')
16
+ process.exit(1)
17
+ } catch (error) {
18
+ logger.error(error.message)
19
+ process.exit(1)
20
+ }
21
+ }
22
+
23
+ module.exports = device
@@ -0,0 +1,20 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const Session = require('../../../db/session')
3
+
4
+ function hostname () {
5
+ try {
6
+ const value = new Session().hostname()
7
+ if (value && value.length > 1) {
8
+ console.log(value)
9
+ return
10
+ }
11
+
12
+ logger.error('missing hostname. Try running [dotenvx armor login].')
13
+ process.exit(1)
14
+ } catch (error) {
15
+ logger.error(error.message)
16
+ process.exit(1)
17
+ }
18
+ }
19
+
20
+ module.exports = hostname
@@ -0,0 +1,14 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const Session = require('../../../db/session')
3
+
4
+ function off () {
5
+ try {
6
+ new Session().turnOff()
7
+ logger.success('✔ armor: off')
8
+ } catch (error) {
9
+ logger.error(error.message)
10
+ process.exit(1)
11
+ }
12
+ }
13
+
14
+ module.exports = off
@@ -0,0 +1,14 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const Session = require('../../../db/session')
3
+
4
+ function on () {
5
+ try {
6
+ new Session().turnOn()
7
+ logger.success('✔ armor: on')
8
+ } catch (error) {
9
+ logger.error(error.message)
10
+ process.exit(1)
11
+ }
12
+ }
13
+
14
+ module.exports = on
@@ -0,0 +1,20 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const Session = require('../../../db/session')
3
+
4
+ function path () {
5
+ try {
6
+ const value = new Session().path()
7
+ if (value && value.length > 1) {
8
+ console.log(value)
9
+ return
10
+ }
11
+
12
+ logger.error('missing path. Try running [dotenvx armor login].')
13
+ process.exit(1)
14
+ } catch (error) {
15
+ logger.error(error.message)
16
+ process.exit(1)
17
+ }
18
+ }
19
+
20
+ module.exports = path
@@ -0,0 +1,23 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const Session = require('../../../db/session')
3
+ const mask = require('../../../lib/helpers/mask')
4
+
5
+ function token () {
6
+ const options = this.opts()
7
+
8
+ try {
9
+ const value = new Session().token()
10
+ if (value && value.length > 1) {
11
+ console.log(options.unmask ? value : mask(value))
12
+ return
13
+ }
14
+
15
+ logger.error('missing token. Try generating one with [dotenvx armor login].')
16
+ process.exit(1)
17
+ } catch (error) {
18
+ logger.error(error.message)
19
+ process.exit(1)
20
+ }
21
+ }
22
+
23
+ module.exports = token
@@ -0,0 +1,20 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const Session = require('../../../db/session')
3
+
4
+ function username () {
5
+ try {
6
+ const value = new Session().username()
7
+ if (value) {
8
+ console.log(value)
9
+ return
10
+ }
11
+
12
+ logger.error('login required. Try running [dotenvx armor login].')
13
+ process.exit(1)
14
+ } catch (error) {
15
+ logger.error(error.message)
16
+ process.exit(1)
17
+ }
18
+ }
19
+
20
+ module.exports = username
@@ -91,6 +91,9 @@ function configureArmorCommand (armor) {
91
91
  return require('./../actions/logout').apply(this, args)
92
92
  })
93
93
 
94
+ // dotenvx armor settings
95
+ require('./settings')(armor.command('settings'))
96
+
94
97
  return armor
95
98
  }
96
99
 
@@ -1,4 +1,10 @@
1
1
  function configureLockCommand (lock) {
2
+ lock.hook('preAction', async () => {
3
+ const Session = require('./../../db/session')
4
+ const sesh = new Session()
5
+ await sesh.notifyUpdate()
6
+ })
7
+
2
8
  lock
3
9
  .description('lock private keys with a local passphrase')
4
10
  .action(function () {
@@ -1,4 +1,10 @@
1
1
  function configureNativeCommand (native) {
2
+ native.hook('preAction', async () => {
3
+ const Session = require('./../../db/session')
4
+ const sesh = new Session()
5
+ await sesh.notifyUpdate()
6
+ })
7
+
2
8
  native
3
9
  .description('move private keys into your OS secret store')
4
10
  .action(function () {
@@ -0,0 +1,62 @@
1
+ function configureSettingsCommand (settings) {
2
+ settings
3
+ .description('settings')
4
+ .action(function () {
5
+ this.help()
6
+ })
7
+
8
+ settings
9
+ .command('username')
10
+ .description('print your username')
11
+ .action(function (...args) {
12
+ return require('./../actions/settings/username').apply(this, args)
13
+ })
14
+
15
+ settings
16
+ .command('token')
17
+ .description('print your access token (--unmask)')
18
+ .option('--unmask', 'unmask access token')
19
+ .action(function (...args) {
20
+ return require('./../actions/settings/token').apply(this, args)
21
+ })
22
+
23
+ settings
24
+ .command('device')
25
+ .description('print your device pubkey (--unmask)')
26
+ .option('--unmask', 'unmask device pubkey')
27
+ .action(function (...args) {
28
+ return require('./../actions/settings/device').apply(this, args)
29
+ })
30
+
31
+ settings
32
+ .command('hostname')
33
+ .description('print hostname')
34
+ .action(function (...args) {
35
+ return require('./../actions/settings/hostname').apply(this, args)
36
+ })
37
+
38
+ settings
39
+ .command('path')
40
+ .description('print path to settings file')
41
+ .action(function (...args) {
42
+ return require('./../actions/settings/path').apply(this, args)
43
+ })
44
+
45
+ settings
46
+ .command('on')
47
+ .description('turn armor on')
48
+ .action(function (...args) {
49
+ return require('./../actions/settings/on').apply(this, args)
50
+ })
51
+
52
+ settings
53
+ .command('off')
54
+ .description('turn armor off')
55
+ .action(function (...args) {
56
+ return require('./../actions/settings/off').apply(this, args)
57
+ })
58
+
59
+ return settings
60
+ }
61
+
62
+ module.exports = configureSettingsCommand
@@ -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)
package/src/db/session.js CHANGED
@@ -5,6 +5,7 @@ const { Conf, dotenv, envPaths } = require('@dotenvx/tooling')
5
5
  const jsonToEnv = require('./../lib/helpers/jsonToEnv')
6
6
  const packageJson = require('./../lib/helpers/packageJson')
7
7
  const { http } = require('./../lib/helpers/http')
8
+ const nativeProvider = require('./../lib/providers/native')
8
9
  const { logger } = require('./../shared/logger')
9
10
 
10
11
  const HOURS_24 = 60 * 60 * 24 * 1000
@@ -14,7 +15,8 @@ const ARMOR = {
14
15
  HOSTNAME: 'DOTENVX_ARMOR_HOSTNAME',
15
16
  USER: 'DOTENVX_ARMOR_USER',
16
17
  USERNAME: 'DOTENVX_ARMOR_USERNAME',
17
- TOKEN: 'DOTENVX_ARMOR_TOKEN'
18
+ TOKEN: 'DOTENVX_ARMOR_TOKEN',
19
+ ON: 'DOTENVX_ARMOR_ON'
18
20
  }
19
21
 
20
22
  const DOTENVX = {
@@ -100,7 +102,7 @@ class Session {
100
102
 
101
103
  status () {
102
104
  // if logged in
103
- if (this.username() && this.token()) {
105
+ if (this.username() && this.token() && this.on()) {
104
106
  return 'on'
105
107
  }
106
108
 
@@ -133,7 +135,15 @@ class Session {
133
135
  }
134
136
 
135
137
  token () {
136
- return this.readSetting('TOKEN') || undefined
138
+ return this.getFromSecretStore(ARMOR.TOKEN) || undefined
139
+ }
140
+
141
+ on () {
142
+ return (this.readSetting('ON') || 'true') === 'true'
143
+ }
144
+
145
+ off () {
146
+ return (this.readSetting('ON') || 'true') === 'false'
137
147
  }
138
148
 
139
149
  devicePublicKey () {
@@ -187,7 +197,7 @@ class Session {
187
197
  store.set(DOTENVX.VERSION_LAST_CHECK, now)
188
198
 
189
199
  if (versionGreaterThan(remote, packageJson.version)) {
190
- console.error('⛆ update available [npm install @dotenvx/dotenvx@latest]')
200
+ console.error('⛆ update available [curl -sfS https://dotenvx.sh | sh]')
191
201
  }
192
202
  } catch (error) {
193
203
  logger.debug(error.message)
@@ -222,6 +232,47 @@ class Session {
222
232
  //
223
233
  // Set/Delete
224
234
  //
235
+ getFromSecretStore (key) {
236
+ try {
237
+ const value = nativeProvider.get(key)
238
+ if (value) return value
239
+ } catch {}
240
+
241
+ const store = this.openStore()
242
+ if (!store) return undefined
243
+
244
+ return store.get(key)
245
+ }
246
+
247
+ setInSecretStore (key, value) {
248
+ try {
249
+ nativeProvider.set(key, value)
250
+ return value
251
+ } catch {
252
+ this.createStore().set(key, value)
253
+ return value
254
+ }
255
+ }
256
+
257
+ deleteFromSecretStore (key) {
258
+ try {
259
+ nativeProvider.delete(key)
260
+ } catch {}
261
+
262
+ const store = this.openStore()
263
+ if (store) store.delete(key)
264
+ }
265
+
266
+ turnOn () {
267
+ this.createStore().set(ARMOR.ON, 'true')
268
+ return 'true'
269
+ }
270
+
271
+ turnOff () {
272
+ this.createStore().set(ARMOR.ON, 'false')
273
+ return 'false'
274
+ }
275
+
225
276
  login (hostname, id, username, accessToken) {
226
277
  if (!hostname) {
227
278
  throw new Error('DOTENVX_ARMOR_HOSTNAME not set. Run [dotenvx armor login]')
@@ -242,8 +293,9 @@ class Session {
242
293
  const store = this.createStore()
243
294
  store.set(ARMOR.USER, id)
244
295
  store.set(ARMOR.USERNAME, username)
245
- store.set(ARMOR.TOKEN, accessToken)
296
+ this.setInSecretStore(ARMOR.TOKEN, accessToken)
246
297
  store.set(ARMOR.HOSTNAME, hostname)
298
+ store.set(ARMOR.ON, 'true')
247
299
 
248
300
  return accessToken
249
301
  }
@@ -261,13 +313,14 @@ class Session {
261
313
  throw new Error('DOTENVX_ARMOR_TOKEN not set. Run [dotenvx armor login]')
262
314
  }
263
315
 
316
+ this.deleteFromSecretStore(ARMOR.TOKEN)
317
+
264
318
  const store = this.openStore()
265
319
  if (!store) return true
266
-
267
320
  store.delete(ARMOR.USER)
268
321
  store.delete(ARMOR.USERNAME)
269
- store.delete(ARMOR.TOKEN)
270
322
  store.delete(ARMOR.HOSTNAME)
323
+ store.delete(ARMOR.ON)
271
324
  store.delete(DOTENVX.VERSION)
272
325
  store.delete(DOTENVX.VERSION_LAST_CHECK)
273
326
  return true
@@ -7,7 +7,7 @@ function attributes (publicKey) {
7
7
  return ['service', SERVICE, 'public-key', publicKey]
8
8
  }
9
9
 
10
- function findGenericPassword (publicKey) {
10
+ function get (publicKey) {
11
11
  try {
12
12
  return execFileSync(SECRET_TOOL_BIN, ['lookup', ...attributes(publicKey)], {
13
13
  encoding: 'utf8',
@@ -18,7 +18,7 @@ function findGenericPassword (publicKey) {
18
18
  }
19
19
  }
20
20
 
21
- function addGenericPassword (publicKey, label, privateKey) {
21
+ function set (publicKey, privateKey, label) {
22
22
  try {
23
23
  execFileSync(SECRET_TOOL_BIN, ['store', `--label=${label}`, ...attributes(publicKey)], {
24
24
  input: privateKey,
@@ -30,14 +30,16 @@ function addGenericPassword (publicKey, label, privateKey) {
30
30
  }
31
31
  }
32
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')
33
+ module.exports = {
34
+ get,
35
+ set,
36
+ delete (publicKey) {
37
+ try {
38
+ execFileSync(SECRET_TOOL_BIN, ['clear', ...attributes(publicKey)], {
39
+ stdio: ['ignore', 'ignore', 'pipe']
40
+ })
41
+ } catch {
42
+ throw new Error('failed to delete private key from Linux Secret Service')
43
+ }
40
44
  }
41
45
  }
42
-
43
- module.exports = { findGenericPassword, addGenericPassword, deleteGenericPassword }
@@ -0,0 +1,26 @@
1
+ const { execFileSync } = require('child_process')
2
+
3
+ const SECURITY_BIN = '/usr/bin/security'
4
+ const SERVICE = 'dotenvx'
5
+
6
+ module.exports = {
7
+ get (key) {
8
+ return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', key, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
9
+ },
10
+
11
+ set (key, value, label) {
12
+ try {
13
+ execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', key, '-l', label, '-w', value], { stdio: 'ignore' })
14
+ } catch {
15
+ throw new Error('failed to save private key to macOS Keychain')
16
+ }
17
+ },
18
+
19
+ delete (key) {
20
+ try {
21
+ execFileSync(SECURITY_BIN, ['delete-generic-password', '-s', SERVICE, '-a', key], { stdio: 'ignore' })
22
+ } catch {
23
+ throw new Error('failed to delete private key from macOS Keychain')
24
+ }
25
+ }
26
+ }
@@ -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
@@ -134,7 +134,7 @@ function run (payload) {
134
134
  })
135
135
  }
136
136
 
137
- function findGenericPassword (publicKey) {
137
+ function get (publicKey) {
138
138
  try {
139
139
  return run({ action: 'read', target: target(publicKey) }).trim() || null
140
140
  } catch {
@@ -142,7 +142,7 @@ function findGenericPassword (publicKey) {
142
142
  }
143
143
  }
144
144
 
145
- function addGenericPassword (publicKey, privateKey) {
145
+ function set (publicKey, privateKey) {
146
146
  try {
147
147
  run({ action: 'write', target: target(publicKey), username: publicKey, secret: privateKey })
148
148
  } catch {
@@ -150,12 +150,14 @@ function addGenericPassword (publicKey, privateKey) {
150
150
  }
151
151
  }
152
152
 
153
- function deleteGenericPassword (publicKey) {
154
- try {
155
- run({ action: 'delete', target: target(publicKey) })
156
- } catch {
157
- throw new Error('failed to delete private key from Windows Credential Manager')
153
+ module.exports = {
154
+ get,
155
+ set,
156
+ delete (publicKey) {
157
+ try {
158
+ run({ action: 'delete', target: target(publicKey) })
159
+ } catch {
160
+ throw new Error('failed to delete private key from Windows Credential Manager')
161
+ }
158
162
  }
159
163
  }
160
-
161
- module.exports = { findGenericPassword, addGenericPassword, deleteGenericPassword }
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
@@ -1,27 +1,52 @@
1
- const { execFileSync } = require('child_process')
2
-
1
+ const macosKeychain = require('../../helpers/macosKeychain')
3
2
  const windowsCredentialManager = require('../../helpers/windowsCredentialManager')
4
3
  const linuxSecretService = require('../../helpers/linuxSecretService')
5
4
 
6
- const SECURITY_BIN = '/usr/bin/security'
7
- const SERVICE = 'dotenvx'
5
+ function get (key) {
6
+ if (process.platform === 'win32') {
7
+ return windowsCredentialManager.get(key)
8
+ }
8
9
 
9
- function findMacosPrivateKey (publicKeyHex) {
10
- return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKeyHex, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
10
+ if (process.platform === 'linux') {
11
+ return linuxSecretService.get(key)
12
+ }
13
+
14
+ return macosKeychain.get(key)
15
+ }
16
+
17
+ function set (key, value, label = key) {
18
+ if (process.platform === 'win32') {
19
+ windowsCredentialManager.set(key, value, label)
20
+ return
21
+ }
22
+
23
+ if (process.platform === 'linux') {
24
+ linuxSecretService.set(key, value, label)
25
+ return
26
+ }
27
+
28
+ macosKeychain.set(key, value, label)
29
+ }
30
+
31
+ index.delete = function (key) {
32
+ if (process.platform === 'win32') {
33
+ windowsCredentialManager.delete(key)
34
+ return
35
+ }
36
+
37
+ if (process.platform === 'linux') {
38
+ linuxSecretService.delete(key)
39
+ return
40
+ }
41
+
42
+ macosKeychain.delete(key)
11
43
  }
12
44
 
13
45
  function index (publicKeyHex) {
14
46
  if (!['darwin', 'linux', 'win32'].includes(process.platform)) return {}
15
47
 
16
48
  try {
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
- }
49
+ const privateKeyHex = get(publicKeyHex)
25
50
 
26
51
  if (!privateKeyHex) return {}
27
52
 
@@ -31,4 +56,7 @@ function index (publicKeyHex) {
31
56
  }
32
57
  }
33
58
 
59
+ index.set = set
60
+ index.get = get
61
+
34
62
  module.exports = index
@@ -1,40 +1,8 @@
1
- const { execFileSync } = require('child_process')
2
-
3
1
  const keynames = require('../conventions/keynames')
4
2
  const readEnvKey = require('../helpers/readEnvKey')
5
3
  const upsertEnvKey = require('../helpers/upsertEnvKey')
6
4
  const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
7
- const windowsCredentialManager = require('../helpers/windowsCredentialManager')
8
- const linuxSecretService = require('../helpers/linuxSecretService')
9
-
10
- const SECURITY_BIN = '/usr/bin/security'
11
- const SERVICE = 'dotenvx'
12
-
13
- function findGenericPassword (publicKey) {
14
- if (process.platform === 'win32') {
15
- return windowsCredentialManager.findGenericPassword(publicKey)
16
- }
17
-
18
- if (process.platform === 'linux') {
19
- return linuxSecretService.findGenericPassword(publicKey)
20
- }
21
-
22
- return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
23
- }
24
-
25
- function deleteGenericPassword (publicKey) {
26
- if (process.platform === 'win32') {
27
- windowsCredentialManager.deleteGenericPassword(publicKey)
28
- return
29
- }
30
-
31
- if (process.platform === 'linux') {
32
- linuxSecretService.deleteGenericPassword(publicKey)
33
- return
34
- }
35
-
36
- execFileSync(SECURITY_BIN, ['delete-generic-password', '-s', SERVICE, '-a', publicKey], { stdio: 'ignore' })
37
- }
5
+ const nativeProvider = require('../providers/native')
38
6
 
39
7
  class KeychainDown {
40
8
  constructor (envFile = '.env', envKeysFile = '.env.keys') {
@@ -55,7 +23,7 @@ class KeychainDown {
55
23
  let privateKey
56
24
 
57
25
  try {
58
- privateKey = findGenericPassword(publicKey)
26
+ privateKey = nativeProvider.get(publicKey)
59
27
  if (!privateKey) {
60
28
  const secretStore = process.platform === 'win32'
61
29
  ? 'Windows Credential Manager'
@@ -78,7 +46,7 @@ class KeychainDown {
78
46
  }
79
47
 
80
48
  upsertEnvKey(privateKeyName, privateKey, envKeysFile)
81
- deleteGenericPassword(publicKey)
49
+ nativeProvider.delete(publicKey)
82
50
 
83
51
  return {
84
52
  changed: true,
@@ -1,26 +1,8 @@
1
- const { execFileSync } = require('child_process')
2
-
3
1
  const keynames = require('../conventions/keynames')
4
2
  const readEnvKey = require('../helpers/readEnvKey')
5
3
  const upsertEnvKey = require('../helpers/upsertEnvKey')
6
4
  const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
7
- const windowsCredentialManager = require('../helpers/windowsCredentialManager')
8
- const linuxSecretService = require('../helpers/linuxSecretService')
9
-
10
- const SECURITY_BIN = '/usr/bin/security'
11
- const SERVICE = 'dotenvx'
12
-
13
- function findGenericPassword (publicKey) {
14
- if (process.platform === 'win32') {
15
- return windowsCredentialManager.findGenericPassword(publicKey)
16
- }
17
-
18
- if (process.platform === 'linux') {
19
- return linuxSecretService.findGenericPassword(publicKey)
20
- }
21
-
22
- return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
23
- }
5
+ const nativeProvider = require('../providers/native')
24
6
 
25
7
  class KeychainPull {
26
8
  constructor (envFile = '.env', envKeysFile = '.env.keys') {
@@ -41,7 +23,7 @@ class KeychainPull {
41
23
  let privateKey
42
24
 
43
25
  try {
44
- privateKey = findGenericPassword(publicKey)
26
+ privateKey = nativeProvider.get(publicKey)
45
27
  if (!privateKey) throw new Error('not found')
46
28
  } catch {
47
29
  const secretStore = process.platform === 'win32'
@@ -1,43 +1,7 @@
1
- const { execFileSync } = require('child_process')
2
-
3
1
  const keynames = require('../conventions/keynames')
4
2
  const readEnvKey = require('../helpers/readEnvKey')
5
3
  const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
6
- const windowsCredentialManager = require('../helpers/windowsCredentialManager')
7
- const linuxSecretService = require('../helpers/linuxSecretService')
8
-
9
- const SECURITY_BIN = '/usr/bin/security'
10
- const SERVICE = 'dotenvx'
11
-
12
- function addGenericPassword (publicKey, label, privateKey) {
13
- if (process.platform === 'win32') {
14
- windowsCredentialManager.addGenericPassword(publicKey, privateKey)
15
- return
16
- }
17
-
18
- if (process.platform === 'linux') {
19
- linuxSecretService.addGenericPassword(publicKey, label, privateKey)
20
- return
21
- }
22
-
23
- try {
24
- execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', publicKey, '-l', label, '-w', privateKey], { stdio: 'ignore' })
25
- } catch {
26
- throw new Error('failed to save private key to macOS Keychain')
27
- }
28
- }
29
-
30
- function findGenericPassword (publicKey) {
31
- if (process.platform === 'win32') {
32
- return windowsCredentialManager.findGenericPassword(publicKey)
33
- }
34
-
35
- if (process.platform === 'linux') {
36
- return linuxSecretService.findGenericPassword(publicKey)
37
- }
38
-
39
- return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
40
- }
4
+ const nativeProvider = require('../providers/native')
41
5
 
42
6
  class KeychainPush {
43
7
  constructor (envFile = '.env', envKeysFile = '.env.keys') {
@@ -59,7 +23,7 @@ class KeychainPush {
59
23
  const label = `dotenvx (${armoredKeyDisplay(publicKey)})`
60
24
 
61
25
  try {
62
- const existingPrivateKey = findGenericPassword(publicKey)
26
+ const existingPrivateKey = nativeProvider.get(publicKey)
63
27
 
64
28
  if (existingPrivateKey === privateKey) {
65
29
  return {
@@ -72,7 +36,7 @@ class KeychainPush {
72
36
  }
73
37
  } catch {}
74
38
 
75
- addGenericPassword(publicKey, label, privateKey)
39
+ nativeProvider.set(publicKey, privateKey, label)
76
40
 
77
41
  return {
78
42
  changed: true,
@@ -1,44 +1,8 @@
1
- const { execFileSync } = require('child_process')
2
-
3
1
  const keynames = require('../conventions/keynames')
4
2
  const readEnvKey = require('../helpers/readEnvKey')
5
3
  const removeEnvKey = require('../helpers/removeEnvKey')
6
4
  const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
7
- const windowsCredentialManager = require('../helpers/windowsCredentialManager')
8
- const linuxSecretService = require('../helpers/linuxSecretService')
9
-
10
- const SECURITY_BIN = '/usr/bin/security'
11
- const SERVICE = 'dotenvx'
12
-
13
- function addGenericPassword (publicKey, label, privateKey) {
14
- if (process.platform === 'win32') {
15
- windowsCredentialManager.addGenericPassword(publicKey, privateKey)
16
- return
17
- }
18
-
19
- if (process.platform === 'linux') {
20
- linuxSecretService.addGenericPassword(publicKey, label, privateKey)
21
- return
22
- }
23
-
24
- try {
25
- execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', publicKey, '-l', label, '-w', privateKey], { stdio: 'ignore' })
26
- } catch {
27
- throw new Error('failed to save private key to macOS Keychain')
28
- }
29
- }
30
-
31
- function findGenericPassword (publicKey) {
32
- if (process.platform === 'win32') {
33
- return windowsCredentialManager.findGenericPassword(publicKey)
34
- }
35
-
36
- if (process.platform === 'linux') {
37
- return linuxSecretService.findGenericPassword(publicKey)
38
- }
39
-
40
- return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
41
- }
5
+ const nativeProvider = require('../providers/native')
42
6
 
43
7
  class KeychainUp {
44
8
  constructor (envFile = '.env', envKeysFile = '.env.keys') {
@@ -60,7 +24,7 @@ class KeychainUp {
60
24
  let existingPrivateKey
61
25
 
62
26
  try {
63
- existingPrivateKey = findGenericPassword(publicKey)
27
+ existingPrivateKey = nativeProvider.get(publicKey)
64
28
  } catch {}
65
29
 
66
30
  const privateKey = readEnvKey(privateKeyName, envKeysFile, { strict: !existingPrivateKey })
@@ -89,7 +53,7 @@ class KeychainUp {
89
53
  }
90
54
  }
91
55
 
92
- addGenericPassword(publicKey, label, privateKey)
56
+ nativeProvider.set(publicKey, privateKey, label)
93
57
  removeEnvKey(privateKeyName, envKeysFile)
94
58
 
95
59
  return {