@dotenvx/dotenvx 2.16.0 → 2.17.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 CHANGED
@@ -2,7 +2,19 @@
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.16.0...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.17.0...main)
6
+
7
+ ## [2.17.0](https://github.com/dotenvx/dotenvx/compare/v2.16.1...v2.17.0) (2026-07-21)
8
+
9
+ ### Added
10
+
11
+ * Add support for Bitwarden `bw://` secrets in your .env files. ([#915](https://github.com/dotenvx/dotenvx/pull/915))
12
+
13
+ ## [2.16.1](https://github.com/dotenvx/dotenvx/compare/v2.16.0...v2.16.1) (2026-07-21)
14
+
15
+ ### Changed
16
+
17
+ * Fix: Do not report child error - let child do so. ([#914](https://github.com/dotenvx/dotenvx/pull/914))
6
18
 
7
19
  ## [2.16.0](https://github.com/dotenvx/dotenvx/compare/v2.15.1...v2.16.0) (2026-07-21)
8
20
 
package/README.md CHANGED
@@ -166,6 +166,19 @@ Hello World
166
166
 
167
167
  see [1Password guide](https://dotenvx.com/docs/secrets-in-1password)
168
168
 
169
+ </details>
170
+ <details><summary>Bitwarden 🔑</summary><br>
171
+
172
+ Run with secrets resolved directly from Bitwarden Password Manager.
173
+
174
+ ```sh
175
+ $ echo 'HELLO="bw://My Hello Login/password"' > .env
176
+ $ dotenvx run -- sh -c 'echo Hello $HELLO'
177
+ Hello World
178
+ ```
179
+
180
+ Install the [Bitwarden Password Manager CLI](https://bitwarden.com/help/cli/) before running dotenvx.
181
+
169
182
  </details>
170
183
  <details><summary>TypeScript 📘</summary><br>
171
184
 
@@ -1193,6 +1206,38 @@ $ dotenvx run --no-1password -- node index.js
1193
1206
 
1194
1207
  The same flag is available for `dotenvx get` and `dotenvx validate`.
1195
1208
 
1209
+ </details>
1210
+ <details><summary>`run` - Bitwarden</summary><br>
1211
+
1212
+ Resolve `bw://` references directly from your `.env` file through the [Bitwarden Password Manager CLI](https://bitwarden.com/help/cli/).
1213
+
1214
+ ```ini
1215
+ # .env
1216
+ API_KEY="bw://My GitHub Account/password"
1217
+ ```
1218
+
1219
+ The reference format is `bw://<item>/<field>`. The item can be a human-readable Bitwarden search term or an exact item UUID. Quote the dotenv value when the item name contains spaces.
1220
+
1221
+ Supported fields are:
1222
+
1223
+ - `username`
1224
+ - `password`
1225
+ - `uri`
1226
+
1227
+ When the vault is locked in an interactive terminal, dotenvx prompts for your Bitwarden master password.
1228
+
1229
+ Human-readable names are convenient, but must identify a single item. If multiple items match, Bitwarden returns an error. Use the item UUID when you need an unambiguous reference.
1230
+
1231
+ If Bitwarden cannot resolve a reference, dotenvx reports the error and leaves the original `bw://` value unresolved.
1232
+
1233
+ Use `--no-bitwarden` to skip Bitwarden resolution intentionally.
1234
+
1235
+ ```sh
1236
+ $ dotenvx run --no-bitwarden -- node index.js
1237
+ ```
1238
+
1239
+ The same flag is available for `dotenvx get` and `dotenvx validate`.
1240
+
1196
1241
  </details>
1197
1242
  <details><summary>`run -f <directory>`</summary><br>
1198
1243
 
@@ -3310,6 +3355,30 @@ Set `no1Password` to leave `op://` values unresolved and avoid calling `op`.
3310
3355
  require('@dotenvx/dotenvx').config({no1Password: true})
3311
3356
  ```
3312
3357
 
3358
+ </details>
3359
+ <details><summary>`config(noBitwarden:)` - noBitwarden</summary><br>
3360
+
3361
+ By default, `config()` automatically resolves `bw://` values through the installed [Bitwarden Password Manager CLI](https://bitwarden.com/help/cli/).
3362
+
3363
+ ```ini
3364
+ # .env
3365
+ API_KEY="bw://My GitHub Account/password"
3366
+ ```
3367
+
3368
+ ```js
3369
+ // index.js
3370
+ require('@dotenvx/dotenvx').config()
3371
+
3372
+ console.log(process.env.API_KEY)
3373
+ ```
3374
+
3375
+ Set `noBitwarden` to leave `bw://` values unresolved and avoid calling `bw`.
3376
+
3377
+ ```js
3378
+ // index.js
3379
+ require('@dotenvx/dotenvx').config({noBitwarden: true})
3380
+ ```
3381
+
3313
3382
  </details>
3314
3383
  <details><summary>`parse(src)`</summary><br>
3315
3384
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.16.0",
2
+ "version": "2.17.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -174,14 +174,18 @@ async function executeCommand (commandArgs, env, sensitiveValues = []) {
174
174
 
175
175
  if (exitCode !== 0) {
176
176
  logger.debug(`received exitCode ${exitCode}`)
177
- throw new Errors({ exitCode }).commandExitedWithCode()
177
+ const error = new Errors({ exitCode }).commandExitedWithCode()
178
+ error.exitCode = exitCode
179
+ throw error
178
180
  }
179
181
  } catch (error) {
182
+ const commandExited = Number.isInteger(error.exitCode) && (error.code === 'COMMAND_EXITED_WITH_CODE' || error.command)
183
+
180
184
  // no color on these errors as they can be standard errors for things like jest exiting with exitCode 1 for a single failed test.
181
185
  if (!['SIGINT', 'SIGTERM'].includes(signalSent || error.signal)) {
182
186
  if (error.code === 'ENOENT') {
183
187
  logger.error(`Unknown command: ${error.command}`)
184
- } else {
188
+ } else if (!commandExited) {
185
189
  logger.error(redactOutput(error.message, sensitiveValues))
186
190
  }
187
191
  }
@@ -5,7 +5,6 @@ const createSpinner = require('./createSpinner')
5
5
  const createElapsedStatus = require('./createElapsedStatus')
6
6
 
7
7
  const FIELDS = new Set(['username', 'password', 'uri'])
8
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
9
8
 
10
9
  function execFileAsync (command, args, options) {
11
10
  return new Promise((resolve, reject) => {
@@ -21,9 +20,12 @@ function isSecretReference (value) {
21
20
  }
22
21
 
23
22
  function parseSecretReference (value) {
24
- const [itemId, field, ...extra] = value.slice('bw://'.length).split('/')
23
+ const reference = value.slice('bw://'.length)
24
+ const separatorIndex = reference.lastIndexOf('/')
25
+ const item = reference.slice(0, separatorIndex)
26
+ const field = reference.slice(separatorIndex + 1)
25
27
 
26
- if (!UUID.test(itemId) || !field || extra.length > 0) {
28
+ if (separatorIndex < 1 || !field) {
27
29
  throw new Error('invalid Bitwarden Password Manager reference')
28
30
  }
29
31
 
@@ -31,7 +33,7 @@ function parseSecretReference (value) {
31
33
  throw new Error(`unsupported Bitwarden Password Manager field ${field}`)
32
34
  }
33
35
 
34
- return { itemId, field }
36
+ return { item, field }
35
37
  }
36
38
 
37
39
  async function session (options) {
@@ -105,10 +107,10 @@ async function resolveBitwardenPassword (parsed, options = {}) {
105
107
  if (!isSecretReference(value)) continue
106
108
 
107
109
  try {
108
- const { itemId, field } = parseSecretReference(value)
110
+ const { item, field } = parseSecretReference(value)
109
111
  if (options.session) startStatus()
110
112
  const bwSession = await session(options)
111
- const stdout = await execFileAsync('bw', ['get', field, itemId], {
113
+ const stdout = await execFileAsync('bw', ['get', field, item], {
112
114
  encoding: 'utf8',
113
115
  windowsHide: true,
114
116
  env: { ...process.env, BW_SESSION: bwSession }
@@ -117,7 +119,6 @@ async function resolveBitwardenPassword (parsed, options = {}) {
117
119
  } catch (error) {
118
120
  errors.push(resolutionError(key, error))
119
121
  unresolved.push(key)
120
- delete parsed[key]
121
122
  }
122
123
  }
123
124
  } finally {
@@ -138,13 +139,13 @@ function resolveBitwardenPasswordSync (parsed) {
138
139
  if (!isSecretReference(value)) continue
139
140
 
140
141
  try {
141
- const { itemId, field } = parseSecretReference(value)
142
+ const { item, field } = parseSecretReference(value)
142
143
  if (!process.env.BW_SESSION) {
143
144
  const error = new Error('Bitwarden Password Manager requires an unlocked BW_SESSION')
144
145
  error.code = 'BW_SESSION_MISSING'
145
146
  throw error
146
147
  }
147
- const stdout = execFileSync('bw', ['get', field, itemId], {
148
+ const stdout = execFileSync('bw', ['get', field, item], {
148
149
  encoding: 'utf8',
149
150
  windowsHide: true,
150
151
  stdio: ['ignore', 'pipe', 'pipe']
@@ -153,7 +154,6 @@ function resolveBitwardenPasswordSync (parsed) {
153
154
  } catch (error) {
154
155
  errors.push(resolutionError(key, error))
155
156
  unresolved.push(key)
156
- delete parsed[key]
157
157
  }
158
158
  }
159
159
 
@@ -44,7 +44,6 @@ async function resolveOnePassword (parsed, options = {}) {
44
44
  } catch (error) {
45
45
  errors.push(resolutionError(key, error))
46
46
  unresolved.push(key)
47
- delete parsed[key]
48
47
  }
49
48
  }
50
49
  } finally {
@@ -73,7 +72,6 @@ function resolveOnePasswordSync (parsed) {
73
72
  } catch (error) {
74
73
  errors.push(resolutionError(key, error))
75
74
  unresolved.push(key)
76
- delete parsed[key]
77
75
  }
78
76
  }
79
77
 
@@ -106,14 +106,12 @@ async function injectEnv ({ env, overload, processEnv, envKeysFilepath, provider
106
106
  if (!no1Password) {
107
107
  const result = await resolveOnePassword(row.injected, { onStatus })
108
108
  row.errors.push(...result.errors)
109
- for (const key of result.unresolved) delete row.parsed[key]
110
109
  Object.assign(row.parsed, row.injected)
111
110
  }
112
111
 
113
112
  if (!noBitwarden) {
114
113
  const passwordResult = await resolveBitwardenPassword(row.injected, { onStatus })
115
114
  row.errors.push(...passwordResult.errors)
116
- for (const key of passwordResult.unresolved) delete row.parsed[key]
117
115
 
118
116
  Object.assign(row.parsed, row.injected)
119
117
  }
@@ -160,14 +158,12 @@ function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider,
160
158
  if (!no1Password) {
161
159
  const result = resolveOnePassword.sync(row.injected)
162
160
  row.errors.push(...result.errors)
163
- for (const key of result.unresolved) delete row.parsed[key]
164
161
  Object.assign(row.parsed, row.injected)
165
162
  }
166
163
 
167
164
  if (!noBitwarden) {
168
165
  const passwordResult = resolveBitwardenPassword.sync(row.injected)
169
166
  row.errors.push(...passwordResult.errors)
170
- for (const key of passwordResult.unresolved) delete row.parsed[key]
171
167
 
172
168
  Object.assign(row.parsed, row.injected)
173
169
  }
@@ -217,14 +213,12 @@ async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, prov
217
213
  if (!no1Password) {
218
214
  const result = await resolveOnePassword(row.injected, { onStatus })
219
215
  row.errors.push(...result.errors)
220
- for (const key of result.unresolved) delete row.parsed[key]
221
216
  Object.assign(row.parsed, row.injected)
222
217
  }
223
218
 
224
219
  if (!noBitwarden) {
225
220
  const passwordResult = await resolveBitwardenPassword(row.injected, { onStatus })
226
221
  row.errors.push(...passwordResult.errors)
227
- for (const key of passwordResult.unresolved) delete row.parsed[key]
228
222
 
229
223
  Object.assign(row.parsed, row.injected)
230
224
  }
@@ -278,14 +272,12 @@ function injectEnvFileSync ({ env, overload, processEnv, envKeysFilepath, provid
278
272
  if (!no1Password) {
279
273
  const result = resolveOnePassword.sync(row.injected)
280
274
  row.errors.push(...result.errors)
281
- for (const key of result.unresolved) delete row.parsed[key]
282
275
  Object.assign(row.parsed, row.injected)
283
276
  }
284
277
 
285
278
  if (!noBitwarden) {
286
279
  const passwordResult = resolveBitwardenPassword.sync(row.injected)
287
280
  row.errors.push(...passwordResult.errors)
288
- for (const key of passwordResult.unresolved) delete row.parsed[key]
289
281
 
290
282
  Object.assign(row.parsed, row.injected)
291
283
  }