@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 +8 -2
- package/package.json +1 -1
- package/src/cli/commands/armor.js +1 -1
- package/src/cli/commands/custody.js +43 -0
- package/src/cli/commands/native.js +1 -1
- package/src/cli/dotenvx.js +9 -4
- package/src/lib/helpers/bitwardenCustody.js +9 -1
- package/src/lib/helpers/onePasswordCustody.js +10 -2
- package/src/lib/helpers/selectKeyStorage.js +1 -1
- package/src/lib/services/custodyTransfer.js +50 -0
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.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.26.1...main)
|
|
6
6
|
|
|
7
|
-
## [2.
|
|
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
|
@@ -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
|
|
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
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -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', '
|
|
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
|
|
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', '
|
|
354
|
-
program.addHelpText('after', ' armor ⛨ move private keys
|
|
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
|
-
|
|
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
|
-
|
|
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
|