@dotenvx/dotenvx 1.66.0 → 1.67.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 +7 -1
- package/package.json +2 -1
- package/src/cli/actions/encrypt.js +58 -3
- package/src/cli/actions/run.js +7 -5
- package/src/cli/dotenvx.js +1 -0
- package/src/lib/extensions/ops.js +16 -3
- package/src/lib/helpers/cryptography/opsKeypair.js +10 -4
- package/src/lib/helpers/cryptography/provision.js +9 -2
- package/src/lib/helpers/keyResolution/keyValues.js +1 -2
- package/src/lib/helpers/prompts.js +43 -0
- package/src/lib/services/encrypt.js +27 -9
- package/src/lib/services/run.js +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
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/v1.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.67.0...main)
|
|
6
|
+
|
|
7
|
+
## [1.67.0](https://github.com/dotenvx/dotenvx/compare/v1.66.0...v1.67.0) (2026-05-21)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add prompt for local storage vs armored storage ([#819](https://github.com/dotenvx/dotenvx/pull/819))
|
|
6
12
|
|
|
7
13
|
## [1.66.0](https://github.com/dotenvx/dotenvx/compare/v1.65.3...v1.66.0) (2026-05-13)
|
|
8
14
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.67.0",
|
|
3
3
|
"name": "@dotenvx/dotenvx",
|
|
4
4
|
"description": "secrets for agents–from the creator of `dotenv`",
|
|
5
5
|
"author": "@motdotla",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"commander": "^11.1.0",
|
|
47
47
|
"dotenv": "^17.2.1",
|
|
48
48
|
"eciesjs": "^0.4.10",
|
|
49
|
+
"enquirer": "^2.4.1",
|
|
49
50
|
"execa": "^5.1.1",
|
|
50
51
|
"fdir": "^6.2.0",
|
|
51
52
|
"ignore": "^5.3.0",
|
|
@@ -6,8 +6,63 @@ const Encrypt = require('./../../lib/services/encrypt')
|
|
|
6
6
|
const catchAndLog = require('../../lib/helpers/catchAndLog')
|
|
7
7
|
const localDisplayPath = require('../../lib/helpers/localDisplayPath')
|
|
8
8
|
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
9
|
+
const prompts = require('../../lib/helpers/prompts')
|
|
9
10
|
const Session = require('../../db/session')
|
|
10
11
|
|
|
12
|
+
function keypairSpinnerHooks (spinner) {
|
|
13
|
+
let stoppedForOps = false
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
onStderr: () => {
|
|
17
|
+
if (spinner && !stoppedForOps) {
|
|
18
|
+
spinner.stop()
|
|
19
|
+
stoppedForOps = true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
after: () => {
|
|
23
|
+
if (spinner && stoppedForOps) {
|
|
24
|
+
spinner.start('encrypting')
|
|
25
|
+
stoppedForOps = false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function keyStorageSelector (spinner) {
|
|
32
|
+
let selected
|
|
33
|
+
|
|
34
|
+
return async function selectKeyStorage () {
|
|
35
|
+
if (selected) return selected
|
|
36
|
+
|
|
37
|
+
if (spinner) spinner.stop()
|
|
38
|
+
selected = await prompts.select({
|
|
39
|
+
message: 'Select key storage',
|
|
40
|
+
choices: [
|
|
41
|
+
{ name: 'Local (.env.keys)', value: 'local' },
|
|
42
|
+
{ name: 'Armored ⛨', value: 'armored' }
|
|
43
|
+
]
|
|
44
|
+
}, {
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stderr
|
|
47
|
+
})
|
|
48
|
+
if (spinner) spinner.start('encrypting')
|
|
49
|
+
|
|
50
|
+
return selected
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function encryptOptions (spinner, noOps) {
|
|
55
|
+
const options = {
|
|
56
|
+
keypairHooks: keypairSpinnerHooks(spinner)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!noOps) {
|
|
60
|
+
options.selectKeyStorage = keyStorageSelector(spinner)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return options
|
|
64
|
+
}
|
|
65
|
+
|
|
11
66
|
async function encrypt () {
|
|
12
67
|
const options = this.opts()
|
|
13
68
|
const spinner = await createSpinner({ ...options, text: 'encrypting' })
|
|
@@ -16,14 +71,14 @@ async function encrypt () {
|
|
|
16
71
|
|
|
17
72
|
const sesh = new Session()
|
|
18
73
|
const envs = this.envs
|
|
19
|
-
const noOps = options.ops === false || (await sesh.noOps())
|
|
74
|
+
const noOps = options.ops === false || (!options.token && (await sesh.noOps()))
|
|
20
75
|
const noCreate = options.create === false
|
|
21
76
|
|
|
22
77
|
// stdout - should not have a try so that exit codes can surface to stdout
|
|
23
78
|
if (options.stdout) {
|
|
24
79
|
const {
|
|
25
80
|
processedEnvs
|
|
26
|
-
} = await new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, noOps, noCreate).run()
|
|
81
|
+
} = await new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, noOps, noCreate, options.token, encryptOptions(spinner, noOps)).run()
|
|
27
82
|
if (spinner) spinner.stop()
|
|
28
83
|
for (const processedEnv of processedEnvs) {
|
|
29
84
|
console.log(processedEnv.envSrc)
|
|
@@ -35,7 +90,7 @@ async function encrypt () {
|
|
|
35
90
|
processedEnvs,
|
|
36
91
|
changedFilepaths,
|
|
37
92
|
unchangedFilepaths
|
|
38
|
-
} = await new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, noOps, noCreate).run()
|
|
93
|
+
} = await new Encrypt(envs, options.key, options.excludeKey, options.envKeysFile, noOps, noCreate, options.token, encryptOptions(spinner, noOps)).run()
|
|
39
94
|
|
|
40
95
|
for (const processedEnv of processedEnvs) {
|
|
41
96
|
logger.verbose(`encrypting ${processedEnv.envFilepath} (${processedEnv.filepath})`)
|
package/src/cli/actions/run.js
CHANGED
|
@@ -54,11 +54,13 @@ async function run () {
|
|
|
54
54
|
readableFilepaths,
|
|
55
55
|
uniqueInjectedKeys
|
|
56
56
|
} = await new Run(envs, options.overload, process.env, options.envKeysFile, noOps, {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
keypairHooks: {
|
|
58
|
+
before: () => {
|
|
59
|
+
if (spinner) spinner.start('retrieving')
|
|
60
|
+
},
|
|
61
|
+
after: () => {
|
|
62
|
+
if (spinner) spinner.start('injecting')
|
|
63
|
+
}
|
|
62
64
|
}
|
|
63
65
|
}).run()
|
|
64
66
|
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -126,6 +126,7 @@ program.command('encrypt')
|
|
|
126
126
|
.option('-k, --key <keys...>', 'keys(s) to encrypt (default: all keys in file)')
|
|
127
127
|
.option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from encryption (default: none)')
|
|
128
128
|
.option('--stdout', 'send to stdout')
|
|
129
|
+
.option('--token <token>', 'set Ops token')
|
|
129
130
|
.option('--no-create', 'do not create .env file(s) when missing')
|
|
130
131
|
.option('--no-ops', 'disable dotenvx-ops features')
|
|
131
132
|
.action(function (...args) {
|
|
@@ -40,10 +40,13 @@ class Ops {
|
|
|
40
40
|
|
|
41
41
|
const args = ['keypair']
|
|
42
42
|
if (options.noSpinner) args.push('--no-spinner')
|
|
43
|
+
if (options.token) args.push('--token', options.token)
|
|
43
44
|
if (publicKey) args.push(publicKey)
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
|
-
return JSON.parse(await this._execInteractive(binary, args
|
|
47
|
+
return JSON.parse(await this._execInteractive(binary, args, {
|
|
48
|
+
onStderr: options.onStderr
|
|
49
|
+
}))
|
|
47
50
|
} catch (_e) {
|
|
48
51
|
return {}
|
|
49
52
|
}
|
|
@@ -57,6 +60,7 @@ class Ops {
|
|
|
57
60
|
|
|
58
61
|
const args = ['keypair']
|
|
59
62
|
if (options.noSpinner) args.push('--no-spinner')
|
|
63
|
+
if (options.token) args.push('--token', options.token)
|
|
60
64
|
if (publicKey) args.push(publicKey)
|
|
61
65
|
|
|
62
66
|
try {
|
|
@@ -106,17 +110,26 @@ class Ops {
|
|
|
106
110
|
return childProcess.execFileSync(binary, args).toString().trim()
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
_execInteractive (binary, args) {
|
|
113
|
+
_execInteractive (binary, args, options = {}) {
|
|
110
114
|
return new Promise((resolve, reject) => {
|
|
111
115
|
const spawnOptions = {
|
|
112
|
-
stdio: ['inherit', 'pipe', '
|
|
116
|
+
stdio: ['inherit', 'pipe', 'pipe']
|
|
113
117
|
}
|
|
114
118
|
const subprocess = childProcess.spawn(binary, args, spawnOptions)
|
|
115
119
|
let stdout = ''
|
|
120
|
+
let sawStderr = false
|
|
116
121
|
|
|
117
122
|
subprocess.stdout.on('data', (data) => {
|
|
118
123
|
stdout += data.toString()
|
|
119
124
|
})
|
|
125
|
+
subprocess.stderr.on('data', (data) => {
|
|
126
|
+
if (!sawStderr) {
|
|
127
|
+
sawStderr = true
|
|
128
|
+
if (options.onStderr) options.onStderr()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
process.stderr.write(data)
|
|
132
|
+
})
|
|
120
133
|
subprocess.on('error', reject)
|
|
121
134
|
subprocess.on('close', (code) => {
|
|
122
135
|
if (code !== 0) {
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
const Ops = require('../../extensions/ops')
|
|
2
2
|
|
|
3
3
|
async function opsKeypair (existingPublicKey, options = {}) {
|
|
4
|
-
|
|
4
|
+
const hooks = options.hooks || {}
|
|
5
|
+
if (hooks.before) await hooks.before()
|
|
6
|
+
|
|
7
|
+
const keypairOptions = {}
|
|
8
|
+
if (options.token) keypairOptions.token = options.token
|
|
9
|
+
if (hooks.onStderr) keypairOptions.onStderr = hooks.onStderr
|
|
10
|
+
if (hooks.before || hooks.onStderr || hooks.after) keypairOptions.noSpinner = true
|
|
5
11
|
|
|
6
12
|
let kp
|
|
7
13
|
try {
|
|
8
|
-
if (
|
|
9
|
-
kp = await new Ops().keypair(existingPublicKey,
|
|
14
|
+
if (Object.keys(keypairOptions).length > 0) {
|
|
15
|
+
kp = await new Ops().keypair(existingPublicKey, keypairOptions)
|
|
10
16
|
} else {
|
|
11
17
|
kp = await new Ops().keypair(existingPublicKey)
|
|
12
18
|
}
|
|
13
19
|
} finally {
|
|
14
|
-
if (
|
|
20
|
+
if (hooks.after) await hooks.after()
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
const publicKey = kp.public_key
|
|
@@ -4,8 +4,12 @@ const opsKeypair = require('./opsKeypair')
|
|
|
4
4
|
const localKeypair = require('./localKeypair')
|
|
5
5
|
const { keyNames } = require('../keyResolution')
|
|
6
6
|
|
|
7
|
-
async function provision ({ envSrc, envFilepath, keysFilepath, noOps }) {
|
|
7
|
+
async function provision ({ envSrc, envFilepath, keysFilepath, noOps, token, keypairHooks, selectKeyStorage }) {
|
|
8
8
|
noOps = noOps !== false
|
|
9
|
+
if (!noOps && selectKeyStorage) {
|
|
10
|
+
noOps = await selectKeyStorage() !== 'armored'
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
const { publicKeyName, privateKeyName } = keyNames(envFilepath)
|
|
10
14
|
|
|
11
15
|
let publicKey
|
|
@@ -20,7 +24,10 @@ async function provision ({ envSrc, envFilepath, keysFilepath, noOps }) {
|
|
|
20
24
|
publicKey = kp.publicKey
|
|
21
25
|
privateKey = kp.privateKey
|
|
22
26
|
} else {
|
|
23
|
-
const
|
|
27
|
+
const keypairOptions = {}
|
|
28
|
+
if (token) keypairOptions.token = token
|
|
29
|
+
if (keypairHooks) keypairOptions.hooks = keypairHooks
|
|
30
|
+
const kp = await opsKeypair(undefined, keypairOptions)
|
|
24
31
|
publicKey = kp.publicKey
|
|
25
32
|
privateKey = kp.privateKey
|
|
26
33
|
}
|
|
@@ -73,8 +73,7 @@ async function keyValues (filepath, opts = {}) {
|
|
|
73
73
|
// ops
|
|
74
74
|
if (!noOps && !privateKey && publicKey && publicKey.length > 0) {
|
|
75
75
|
const kp = await opsKeypair(publicKey, {
|
|
76
|
-
|
|
77
|
-
afterOpsKeypair: opts.afterOpsKeypair
|
|
76
|
+
hooks: opts.keypairHooks
|
|
78
77
|
})
|
|
79
78
|
privateKey = kp.privateKey
|
|
80
79
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const Enquirer = require('enquirer')
|
|
2
|
+
|
|
3
|
+
const enquirer = new Enquirer()
|
|
4
|
+
|
|
5
|
+
function choicesForSelect (choices) {
|
|
6
|
+
return choices.map(choice => {
|
|
7
|
+
if (typeof choice === 'string') return choice
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
name: choice.value,
|
|
11
|
+
message: choice.name || choice.value
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function enquirerOptions (context = {}) {
|
|
17
|
+
const options = {
|
|
18
|
+
// Enquirer names the render stream stdout; use stderr so stdout stays machine-readable.
|
|
19
|
+
stdout: context.output || process.stderr
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (context.input) {
|
|
23
|
+
options.stdin = context.input
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return options
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function select ({ message, choices }, context) {
|
|
30
|
+
const answer = await enquirer.prompt({
|
|
31
|
+
type: 'select',
|
|
32
|
+
name: 'value',
|
|
33
|
+
message,
|
|
34
|
+
choices: choicesForSelect(choices),
|
|
35
|
+
...enquirerOptions(context)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return answer.value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
select
|
|
43
|
+
}
|
|
@@ -29,13 +29,16 @@ const detectEncoding = require('./../helpers/detectEncoding')
|
|
|
29
29
|
const SAMPLE_ENV_KIT = require('./../helpers/kits/sample')
|
|
30
30
|
|
|
31
31
|
class Encrypt {
|
|
32
|
-
constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null, noOps = false, noCreate = false) {
|
|
32
|
+
constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null, noOps = false, noCreate = false, token = undefined, options = {}) {
|
|
33
33
|
this.envs = determine(envs, process.env)
|
|
34
34
|
this.key = key
|
|
35
35
|
this.excludeKey = excludeKey
|
|
36
36
|
this.envKeysFilepath = envKeysFilepath
|
|
37
37
|
this.noOps = noOps
|
|
38
38
|
this.noCreate = noCreate
|
|
39
|
+
this.token = token
|
|
40
|
+
this.keypairHooks = options.keypairHooks
|
|
41
|
+
this.selectKeyStorage = options.selectKeyStorage
|
|
39
42
|
|
|
40
43
|
this.processedEnvs = []
|
|
41
44
|
this.changedFilepaths = new Set()
|
|
@@ -78,14 +81,17 @@ class Encrypt {
|
|
|
78
81
|
row.envFilepath = envFilepath
|
|
79
82
|
|
|
80
83
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!
|
|
84
|
-
|
|
84
|
+
const fileExists = await fsx.exists(filepath)
|
|
85
|
+
let envSrc
|
|
86
|
+
if (!fileExists && !this.noCreate) {
|
|
87
|
+
envSrc = SAMPLE_ENV_KIT
|
|
85
88
|
fileCreated = true
|
|
89
|
+
row.kitCreated = 'sample'
|
|
90
|
+
row.changed = true
|
|
91
|
+
} else {
|
|
92
|
+
const encoding = await detectEncoding(filepath)
|
|
93
|
+
envSrc = await fsx.readFileX(filepath, { encoding })
|
|
86
94
|
}
|
|
87
|
-
const encoding = await detectEncoding(filepath)
|
|
88
|
-
let envSrc = await fsx.readFileX(filepath, { encoding })
|
|
89
95
|
if (envSrc.trim().length === 0) {
|
|
90
96
|
envSrc = SAMPLE_ENV_KIT
|
|
91
97
|
row.kitCreated = 'sample'
|
|
@@ -97,11 +103,23 @@ class Encrypt {
|
|
|
97
103
|
let privateKey
|
|
98
104
|
|
|
99
105
|
const { publicKeyName, privateKeyName } = keyNames(envFilepath)
|
|
100
|
-
const { publicKeyValue, privateKeyValue } = await keyValues(envFilepath, {
|
|
106
|
+
const { publicKeyValue, privateKeyValue } = await keyValues(envFilepath, {
|
|
107
|
+
keysFilepath: this.envKeysFilepath,
|
|
108
|
+
noOps: this.noOps,
|
|
109
|
+
keypairHooks: this.keypairHooks
|
|
110
|
+
})
|
|
101
111
|
|
|
102
112
|
// first pass - provision
|
|
103
113
|
if (!privateKeyValue && !publicKeyValue) {
|
|
104
|
-
const prov = await provision({
|
|
114
|
+
const prov = await provision({
|
|
115
|
+
envSrc,
|
|
116
|
+
envFilepath,
|
|
117
|
+
keysFilepath: this.envKeysFilepath,
|
|
118
|
+
noOps: this.noOps,
|
|
119
|
+
token: this.token,
|
|
120
|
+
keypairHooks: this.keypairHooks,
|
|
121
|
+
selectKeyStorage: this.selectKeyStorage
|
|
122
|
+
})
|
|
105
123
|
envSrc = prov.envSrc
|
|
106
124
|
publicKey = prov.publicKey
|
|
107
125
|
privateKey = prov.privateKey
|
package/src/lib/services/run.js
CHANGED
|
@@ -24,8 +24,7 @@ class Run {
|
|
|
24
24
|
this.envKeysFilepath = envKeysFilepath
|
|
25
25
|
this.noOps = noOps
|
|
26
26
|
this.noSpinner = options.noSpinner
|
|
27
|
-
this.
|
|
28
|
-
this.afterOpsKeypair = options.afterOpsKeypair
|
|
27
|
+
this.keypairHooks = options.keypairHooks
|
|
29
28
|
|
|
30
29
|
this.processedEnvs = []
|
|
31
30
|
this.readableFilepaths = new Set()
|
|
@@ -189,8 +188,7 @@ class Run {
|
|
|
189
188
|
const { privateKeyValue } = await keyValues(filepath, {
|
|
190
189
|
keysFilepath: this.envKeysFilepath,
|
|
191
190
|
noOps: this.noOps,
|
|
192
|
-
|
|
193
|
-
afterOpsKeypair: this.afterOpsKeypair
|
|
191
|
+
keypairHooks: this.keypairHooks
|
|
194
192
|
})
|
|
195
193
|
|
|
196
194
|
const {
|