@dotenvx/dotenvx 2.16.1 → 2.17.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,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.1...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.17.1...main)
6
+
7
+ ## [2.17.1](https://github.com/dotenvx/dotenvx/compare/v2.17.0...v2.17.1) (2026-07-21)
8
+
9
+ ### Changed
10
+
11
+ * Handle `ctrl + c` better ([#916](https://github.com/dotenvx/dotenvx/pull/916))
12
+
13
+ ## [2.17.0](https://github.com/dotenvx/dotenvx/compare/v2.16.1...v2.17.0) (2026-07-21)
14
+
15
+ ### Added
16
+
17
+ * Add support for Bitwarden `bw://` secrets in your .env files. ([#915](https://github.com/dotenvx/dotenvx/pull/915))
6
18
 
7
19
  ## [2.16.1](https://github.com/dotenvx/dotenvx/compare/v2.16.0...v2.16.1) (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.1",
2
+ "version": "2.17.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -53,7 +53,7 @@
53
53
  "funding": "https://dotenvx.com",
54
54
  "dependencies": {
55
55
  "@dotenvx/primitives": "^2.1.0",
56
- "@dotenvx/tooling": "^1.0.2",
56
+ "@dotenvx/tooling": "^1.0.3",
57
57
  "yocto-spinner": "^1.2.1"
58
58
  },
59
59
  "devDependencies": {
@@ -120,6 +120,10 @@ async function get (key) {
120
120
  }
121
121
  } catch (error) {
122
122
  if (spinner) spinner.stop()
123
+ if (error.code === 'PROMPT_CANCELLED') {
124
+ process.exit(130)
125
+ return
126
+ }
123
127
  catchAndLog(error)
124
128
  process.exit(1)
125
129
  }
@@ -210,6 +210,10 @@ async function run () {
210
210
  logger.success(`⟐ ${msg}`)
211
211
  } catch (error) {
212
212
  if (spinner) spinner.stop()
213
+ if (error.code === 'PROMPT_CANCELLED') {
214
+ process.exit(130)
215
+ return
216
+ }
213
217
  catchAndLog(error)
214
218
  process.exit(1)
215
219
  }
@@ -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,19 +107,19 @@ 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 }
115
117
  })
116
118
  parsed[key] = secretValue(stdout)
117
119
  } catch (error) {
120
+ if (error && error.code === 'PROMPT_CANCELLED') throw error
118
121
  errors.push(resolutionError(key, error))
119
122
  unresolved.push(key)
120
- delete parsed[key]
121
123
  }
122
124
  }
123
125
  } finally {
@@ -138,13 +140,13 @@ function resolveBitwardenPasswordSync (parsed) {
138
140
  if (!isSecretReference(value)) continue
139
141
 
140
142
  try {
141
- const { itemId, field } = parseSecretReference(value)
143
+ const { item, field } = parseSecretReference(value)
142
144
  if (!process.env.BW_SESSION) {
143
145
  const error = new Error('Bitwarden Password Manager requires an unlocked BW_SESSION')
144
146
  error.code = 'BW_SESSION_MISSING'
145
147
  throw error
146
148
  }
147
- const stdout = execFileSync('bw', ['get', field, itemId], {
149
+ const stdout = execFileSync('bw', ['get', field, item], {
148
150
  encoding: 'utf8',
149
151
  windowsHide: true,
150
152
  stdio: ['ignore', 'pipe', 'pipe']
@@ -153,7 +155,6 @@ function resolveBitwardenPasswordSync (parsed) {
153
155
  } catch (error) {
154
156
  errors.push(resolutionError(key, error))
155
157
  unresolved.push(key)
156
- delete parsed[key]
157
158
  }
158
159
  }
159
160
 
@@ -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,20 +106,19 @@ 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
  }
120
118
 
121
119
  inject(processEnv, row.parsed)
122
120
  } catch (e) {
121
+ if (e.code === 'PROMPT_CANCELLED') throw e
123
122
  row.errors = [e]
124
123
  }
125
124
 
@@ -160,14 +159,12 @@ function injectEnvSync ({ env, overload, processEnv, envKeysFilepath, provider,
160
159
  if (!no1Password) {
161
160
  const result = resolveOnePassword.sync(row.injected)
162
161
  row.errors.push(...result.errors)
163
- for (const key of result.unresolved) delete row.parsed[key]
164
162
  Object.assign(row.parsed, row.injected)
165
163
  }
166
164
 
167
165
  if (!noBitwarden) {
168
166
  const passwordResult = resolveBitwardenPassword.sync(row.injected)
169
167
  row.errors.push(...passwordResult.errors)
170
- for (const key of passwordResult.unresolved) delete row.parsed[key]
171
168
 
172
169
  Object.assign(row.parsed, row.injected)
173
170
  }
@@ -217,20 +214,19 @@ async function injectEnvFile ({ env, overload, processEnv, envKeysFilepath, prov
217
214
  if (!no1Password) {
218
215
  const result = await resolveOnePassword(row.injected, { onStatus })
219
216
  row.errors.push(...result.errors)
220
- for (const key of result.unresolved) delete row.parsed[key]
221
217
  Object.assign(row.parsed, row.injected)
222
218
  }
223
219
 
224
220
  if (!noBitwarden) {
225
221
  const passwordResult = await resolveBitwardenPassword(row.injected, { onStatus })
226
222
  row.errors.push(...passwordResult.errors)
227
- for (const key of passwordResult.unresolved) delete row.parsed[key]
228
223
 
229
224
  Object.assign(row.parsed, row.injected)
230
225
  }
231
226
 
232
227
  inject(processEnv, parsed)
233
228
  } catch (e) {
229
+ if (e.code === 'PROMPT_CANCELLED') throw e
234
230
  if (e.code === 'ENOENT' || e.code === 'EISDIR') {
235
231
  row.errors = [new Errors({ envFilepath: env.value, filepath }).missingEnvFile()]
236
232
  } else {
@@ -278,14 +274,12 @@ function injectEnvFileSync ({ env, overload, processEnv, envKeysFilepath, provid
278
274
  if (!no1Password) {
279
275
  const result = resolveOnePassword.sync(row.injected)
280
276
  row.errors.push(...result.errors)
281
- for (const key of result.unresolved) delete row.parsed[key]
282
277
  Object.assign(row.parsed, row.injected)
283
278
  }
284
279
 
285
280
  if (!noBitwarden) {
286
281
  const passwordResult = resolveBitwardenPassword.sync(row.injected)
287
282
  row.errors.push(...passwordResult.errors)
288
- for (const key of passwordResult.unresolved) delete row.parsed[key]
289
283
 
290
284
  Object.assign(row.parsed, row.injected)
291
285
  }