@dotenvx/dotenvx 2.26.0 → 2.26.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,9 +2,15 @@
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.26.0...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.26.1...main)
6
6
 
7
- ## [2.25.0](https://github.com/dotenvx/dotenvx/compare/v2.25.0...v2.26.0) (2026-09-15)
7
+ ## [2.26.1](https://github.com/dotenvx/dotenvx/compare/v2.26.0...v2.26.1) (2026-09-15)
8
+
9
+ ### Changed
10
+
11
+ * Add `dotenvx 1password up|down|push|pull` and `dotenvx bitwarden up|down|push|pull` ([#969](https://github.com/dotenvx/dotenvx/pull/969))
12
+
13
+ ## [2.26.0](https://github.com/dotenvx/dotenvx/compare/v2.25.0...v2.26.0) (2026-09-15)
8
14
 
9
15
  ### Added
10
16
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.26.0",
2
+ "version": "2.26.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -2,7 +2,7 @@ const executeDynamic = require('./../../lib/helpers/executeDynamic')
2
2
 
3
3
  function configureArmorCommand (armor) {
4
4
  armor
5
- .description('move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
5
+ .description('move private keys in/out of Dotenvx Armor [www.dotenvx.com/armor]')
6
6
  .allowUnknownOption()
7
7
  .argument('[command]', 'dotenvx-armor command')
8
8
  .argument('[args...]', 'dotenvx-armor command arguments')
@@ -0,0 +1,43 @@
1
+ function configureCustodyCommand (command, name, providerPath) {
2
+ command.hook('preAction', async () => {
3
+ const Session = require('../../db/session')
4
+ await new Session().notifyUpdate()
5
+ })
6
+ command.description(`move private keys in/out of ${name}`).action(function () { this.help() })
7
+ const descriptions = {
8
+ up: `move key from .env.keys into ${name}`,
9
+ down: `move key from ${name} to .env.keys`,
10
+ push: `copy key from .env.keys into ${name}`,
11
+ pull: `copy key from ${name} into .env.keys`
12
+ }
13
+ for (const [operation, description] of Object.entries(descriptions)) {
14
+ command.command(operation)
15
+ .description(description)
16
+ .option('-f, --env-file <path>', 'path to your env file')
17
+ .option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
18
+ .action(async function () {
19
+ const { logger } = require('../../shared/logger')
20
+ const createSpinner = require('../../lib/helpers/createSpinner')
21
+ const armoredKeyDisplay = require('../../lib/helpers/armoredKeyDisplay')
22
+ const transfer = require('../../lib/services/custodyTransfer')
23
+ const options = this.opts()
24
+ const spinner = await createSpinner({ ...this.optsWithGlobals(), text: `${operation === 'up' || operation === 'push' ? 'storing in' : 'reading from'} ${name}` })
25
+ try {
26
+ const result = await transfer(require(providerPath), name, operation, options.envFile, options.envKeysFile)
27
+ if (spinner) spinner.stop()
28
+ const display = armoredKeyDisplay(result.publicKeyValue) || result.privateKeyName
29
+ const messages = { up: `stored in ${name}`, down: `moved to ${options.envKeysFile}`, push: `pushed to ${name}`, pull: `pulled to ${options.envKeysFile}` }
30
+ if (result.changed) logger.success(`□ ${messages[operation]} (${display})`)
31
+ else logger.info(`○ no change (${display})`)
32
+ } catch (error) {
33
+ if (spinner) spinner.stop()
34
+ if (error.code === 'PROMPT_CANCELLED') return process.exit(130)
35
+ logger.error(error.message)
36
+ process.exit(1)
37
+ }
38
+ })
39
+ }
40
+ return command
41
+ }
42
+
43
+ module.exports = configureCustodyCommand
@@ -6,7 +6,7 @@ function configureNativeCommand (native) {
6
6
  })
7
7
 
8
8
  native
9
- .description('move private keys into your OS secret store')
9
+ .description('move private keys in/out of your OS secret store')
10
10
  .action(function () {
11
11
  this.help()
12
12
  })
@@ -346,14 +346,19 @@ program.command('help [command]')
346
346
 
347
347
  // security sections (hidden commands advertised here)
348
348
  program.addHelpText('after', ' ')
349
- program.addHelpText('after', 'Better Security:')
349
+ program.addHelpText('after', 'Local Custody:')
350
350
  program.addHelpText('after', ' lock ⊡ lock private keys with a local passphrase')
351
- program.addHelpText('after', ' native ⌥ move private keys into your OS secret store')
351
+ program.addHelpText('after', ' native ⌥ move private keys in/out of your OS secret store')
352
+ program.addHelpText('after', ' 1password □ move private keys in/out of 1Password')
353
+ program.addHelpText('after', ' bitwarden □ move private keys in/out of Bitwarden')
352
354
  program.addHelpText('after', ' ')
353
- program.addHelpText('after', 'For Security Teams:')
354
- program.addHelpText('after', ' armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
355
+ program.addHelpText('after', 'Managed Custody:')
356
+ program.addHelpText('after', ' armor ⛨ move private keys in/out of Dotenvx Armor [www.dotenvx.com/armor]')
355
357
  program.addHelpText('after', ' curl ⛨ call authenticated api Dotenvx Armor [www.dotenvx.com/armor]')
356
358
 
359
+ require('./commands/custody')(program.command('1password', { hidden: true }), '1Password', '../../lib/helpers/onePasswordCustody')
360
+ require('./commands/custody')(program.command('bitwarden', { hidden: true }), 'Bitwarden', '../../lib/helpers/bitwardenCustody')
361
+
357
362
  // dotenvx native
358
363
  require('./commands/native')(program.command('native', { hidden: true }))
359
364
 
@@ -159,4 +159,12 @@ async function set (publicKey, privateKey) {
159
159
  new Session().createStore().set(`${PREFIX}${publicKey}`, Buffer.from(JSON.stringify(loc)).toString('base64'))
160
160
  }
161
161
 
162
- module.exports = { available, configured, get, getSync, set }
162
+ async function remove (publicKey) {
163
+ const loc = location(publicKey)
164
+ if (!loc) return
165
+ await authenticate(loc)
166
+ await run(['delete', 'item', loc.item])
167
+ new Session().openStore().delete(`${PREFIX}${publicKey}`)
168
+ }
169
+
170
+ module.exports = { available, configured, get, getSync, set, delete: remove }
@@ -15,7 +15,7 @@ function failure (message) {
15
15
 
16
16
  function commandError (args, stderr) {
17
17
  const step = args[0] === 'item'
18
- ? 'create the key item'
18
+ ? (args[1] === 'delete' ? 'delete the key item' : 'create the key item')
19
19
  : {
20
20
  whoami: 'check the signed-in account',
21
21
  signin: 'sign in',
@@ -156,4 +156,12 @@ async function set (publicKey, privateKey) {
156
156
  new Session().createStore().set(`${PREFIX}${publicKey}`, `${account}|${reference}`)
157
157
  }
158
158
 
159
- module.exports = { available, configured, get, getSync, set }
159
+ async function remove (publicKey) {
160
+ const loc = location(publicKey)
161
+ if (!loc) return
162
+ const [vault, item] = loc.reference.slice('op://'.length).split('/')
163
+ await run(['item', 'delete', item, `--vault=${vault}`, `--account=${loc.account}`])
164
+ new Session().openStore().delete(`${PREFIX}${publicKey}`)
165
+ }
166
+
167
+ module.exports = { available, configured, get, getSync, set, delete: remove }
@@ -14,7 +14,7 @@ async function selectKeyStorage (options = {}) {
14
14
  if (process.env.CI || options.noCreate || !process.stdin.isTTY || !process.stderr.isTTY) return defaultStorage
15
15
 
16
16
  const choices = [
17
- ...(useNative ? [{ name: `□ Local Custody (${secretStoreNames[process.platform]})`, value: 'native' }] : [])
17
+ ...(useNative ? [{ name: `□ Local Custody (Native ${secretStoreNames[process.platform]})`, value: 'native' }] : [])
18
18
  ]
19
19
  if (!options.no1Password && process.env.DOTENVX_NO_1PASSWORD !== 'true' && await onePasswordCustody.available()) {
20
20
  choices.push({ name: '□ Local Custody (1Password)', value: 'onepassword' })
@@ -0,0 +1,50 @@
1
+ const { derive } = require('@dotenvx/primitives')
2
+ const keynames = require('../conventions/keynames')
3
+ const readEnvKey = require('../helpers/readEnvKey')
4
+ const removeEnvKey = require('../helpers/removeEnvKey')
5
+ const upsertEnvKey = require('../helpers/upsertEnvKey')
6
+
7
+ function verify (publicKey, privateKey) {
8
+ try {
9
+ if (derive(privateKey) === publicKey) return
10
+ } catch {}
11
+ throw new Error('private key does not match the .env public key')
12
+ }
13
+
14
+ async function custodyTransfer (provider, name, operation, envFile = '.env', envKeysFile = '.env.keys') {
15
+ if (!['up', 'down', 'push', 'pull'].includes(operation)) throw new Error('unknown custody operation')
16
+ const { publicKeyName, privateKeyName } = keynames(envFile)
17
+ const publicKey = readEnvKey(publicKeyName, envFile, { strict: true })
18
+ const result = changed => ({ changed, privateKeyName, publicKeyValue: publicKey })
19
+ const local = readEnvKey(privateKeyName, envKeysFile)
20
+ if (operation === 'up' || operation === 'push') {
21
+ if (local) verify(publicKey, local)
22
+ if (operation === 'push' && !local) throw new Error(`missing ${privateKeyName} in ${envKeysFile}`)
23
+ const existing = (await provider.get(publicKey))[publicKey]
24
+ if (existing) verify(publicKey, existing)
25
+ if (!local && !existing) throw new Error(`missing ${privateKeyName} in ${envKeysFile}`)
26
+ if (!existing) await provider.set(publicKey, local)
27
+ const saved = (await provider.get(publicKey))[publicKey]
28
+ if (!saved || saved !== (local || existing)) throw new Error(`could not verify private key in ${name}; ${envKeysFile} unchanged`)
29
+ if (operation === 'up') return result(removeEnvKey(privateKeyName, envKeysFile).changed || !existing)
30
+ return result(!existing)
31
+ }
32
+
33
+ const privateKey = (await provider.get(publicKey))[publicKey]
34
+ if (!privateKey) {
35
+ if (operation === 'down' && local) {
36
+ verify(publicKey, local)
37
+ return result(false)
38
+ }
39
+ throw new Error(`[NOT_FOUND] private key not found in ${name}`)
40
+ }
41
+ verify(publicKey, privateKey)
42
+ const written = upsertEnvKey(privateKeyName, privateKey, envKeysFile)
43
+ if (readEnvKey(privateKeyName, envKeysFile, { strict: true }) !== privateKey) {
44
+ throw new Error(`could not verify private key in ${envKeysFile}; ${name} unchanged`)
45
+ }
46
+ if (operation === 'down') await provider.delete(publicKey)
47
+ return result(written.changed || operation === 'down')
48
+ }
49
+
50
+ module.exports = custodyTransfer