@dotenvx/dotenvx 2.1.4 → 2.2.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.1.4...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.2.0...main)
6
+
7
+ ## [2.2.0](https://github.com/dotenvx/dotenvx/compare/v2.1.5...v2.2.0) (2026-07-07)
8
+
9
+ ### Added
10
+
11
+ * Use `dotenvx keychain up|down` to move your private keys to your macOS Keychain ([#872](https://github.com/dotenvx/dotenvx/pull/872))
12
+
13
+ ## [2.1.5](https://github.com/dotenvx/dotenvx/compare/v2.1.4...v2.1.5) (2026-07-06)
14
+
15
+ ### Changed
16
+
17
+ * `precommit` and `prebuild` respect `_PLAIN` ([#871](https://github.com/dotenvx/dotenvx/pull/871))
6
18
 
7
19
  ## [2.1.4](https://github.com/dotenvx/dotenvx/compare/v2.1.3...v2.1.4) (2026-07-02)
8
20
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.1.4",
2
+ "version": "2.2.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "funding": "https://dotenvx.com",
45
45
  "dependencies": {
46
- "@dotenvx/primitives": "^1.7.0",
46
+ "@dotenvx/primitives": "^1.7.1",
47
47
  "commander": "^11.1.0",
48
48
  "conf": "^10.2.0",
49
49
  "dotenv": "^17.4.2",
@@ -17,6 +17,7 @@ async function decrypt () {
17
17
  const envs = this.envs
18
18
  const sesh = new Session()
19
19
  const noArmor = options.armor === false || (await sesh.noArmor())
20
+ const noKeychain = options.keychain === false || options.noKeychain === true
20
21
 
21
22
  let errorCount = 0
22
23
 
@@ -30,6 +31,7 @@ async function decrypt () {
30
31
  ek: options.excludeKey,
31
32
  fk: options.envKeysFile,
32
33
  noArmor,
34
+ noKeychain,
33
35
  onStatus: (text) => {
34
36
  if (spinner && text) {
35
37
  spinner.text = text
@@ -64,6 +66,7 @@ async function decrypt () {
64
66
  ek: options.excludeKey,
65
67
  fk: options.envKeysFile,
66
68
  noArmor,
69
+ noKeychain,
67
70
  onStatus: (text) => {
68
71
  if (spinner && text) {
69
72
  spinner.text = text
@@ -21,12 +21,13 @@ async function encryptAction () {
21
21
  const fk = options.envKeysFile || '.env.keys'
22
22
  const noCreate = options.create === false
23
23
  const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
24
+ const noKeychain = options.keychain === false || options.noKeychain === true
24
25
 
25
26
  let errorCount = 0
26
27
 
27
28
  // stdout - should not have a try so that exit codes can surface to stdout
28
29
  if (options.stdout) {
29
- const { processedEnvs } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate })
30
+ const { processedEnvs } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain })
30
31
 
31
32
  if (spinner) spinner.stop()
32
33
  for (const processedEnv of processedEnvs) {
@@ -47,7 +48,7 @@ async function encryptAction () {
47
48
  }
48
49
 
49
50
  try {
50
- const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate })
51
+ const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain })
51
52
 
52
53
  if (keysSrc) {
53
54
  await fsx.writeFileX(fk, keysSrc)
@@ -33,6 +33,7 @@ async function get (key) {
33
33
  try {
34
34
  const sesh = new Session()
35
35
  const noArmor = options.armor === false || (await sesh.noArmor())
36
+ const noKeychain = options.keychain === false || options.noKeychain === true
36
37
  const { parsed, errors } = await getResolver({
37
38
  key,
38
39
  envs,
@@ -40,6 +41,7 @@ async function get (key) {
40
41
  all: options.all,
41
42
  envKeysFile: options.envKeysFile,
42
43
  noArmor,
44
+ noKeychain,
43
45
  onStatus: (text) => {
44
46
  if (spinner && text) {
45
47
  spinner.text = text
@@ -0,0 +1,29 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const KeychainDown = require('./../../../lib/services/keychainDown')
3
+ const createSpinner = require('../../../lib/helpers/createSpinner')
4
+ const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
5
+
6
+ async function down () {
7
+ const options = this.opts()
8
+ const spinner = await createSpinner({ ...options, text: 'unkeychaining' })
9
+
10
+ logger.debug(`options: ${JSON.stringify(options)}`)
11
+
12
+ try {
13
+ const { changed, privateKeyName, publicKeyValue } = new KeychainDown(options.envFile, options.envKeysFile).run()
14
+ const keyDisplay = armoredKeyDisplay(publicKeyValue) || privateKeyName
15
+
16
+ if (spinner) spinner.stop()
17
+ if (changed) {
18
+ logger.success(`◇ moved to .env.keys (${keyDisplay})`)
19
+ } else {
20
+ logger.info(`○ no change (${keyDisplay})`)
21
+ }
22
+ } catch (error) {
23
+ if (spinner) spinner.stop()
24
+ logger.error(error.message)
25
+ process.exit(1)
26
+ }
27
+ }
28
+
29
+ module.exports = down
@@ -0,0 +1,29 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const KeychainPull = require('./../../../lib/services/keychainPull')
3
+ const createSpinner = require('../../../lib/helpers/createSpinner')
4
+ const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
5
+
6
+ async function pull () {
7
+ const options = this.opts()
8
+ const spinner = await createSpinner({ ...options, text: 'pulling' })
9
+
10
+ logger.debug(`options: ${JSON.stringify(options)}`)
11
+
12
+ try {
13
+ const { changed, privateKeyName, publicKeyValue } = new KeychainPull(options.envFile, options.envKeysFile).run()
14
+ const keyDisplay = armoredKeyDisplay(publicKeyValue) || privateKeyName
15
+
16
+ if (spinner) spinner.stop()
17
+ if (changed) {
18
+ logger.success(`◇ pulled to .env.keys (${keyDisplay})`)
19
+ } else {
20
+ logger.info(`○ no change (${keyDisplay})`)
21
+ }
22
+ } catch (error) {
23
+ if (spinner) spinner.stop()
24
+ logger.error(error.message)
25
+ process.exit(1)
26
+ }
27
+ }
28
+
29
+ module.exports = pull
@@ -0,0 +1,29 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const KeychainPush = require('./../../../lib/services/keychainPush')
3
+ const createSpinner = require('../../../lib/helpers/createSpinner')
4
+ const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
5
+
6
+ async function push () {
7
+ const options = this.opts()
8
+ const spinner = await createSpinner({ ...options, text: 'pushing' })
9
+
10
+ logger.debug(`options: ${JSON.stringify(options)}`)
11
+
12
+ try {
13
+ const { changed, privateKeyName, publicKeyValue } = new KeychainPush(options.envFile, options.envKeysFile).run()
14
+ const keyDisplay = armoredKeyDisplay(publicKeyValue) || privateKeyName
15
+
16
+ if (spinner) spinner.stop()
17
+ if (changed) {
18
+ logger.success(`⌥ pushed (${keyDisplay})`)
19
+ } else {
20
+ logger.info(`○ no change (${keyDisplay})`)
21
+ }
22
+ } catch (error) {
23
+ if (spinner) spinner.stop()
24
+ logger.error(error.message)
25
+ process.exit(1)
26
+ }
27
+ }
28
+
29
+ module.exports = push
@@ -0,0 +1,29 @@
1
+ const { logger } = require('../../../shared/logger')
2
+ const KeychainUp = require('./../../../lib/services/keychainUp')
3
+ const createSpinner = require('../../../lib/helpers/createSpinner')
4
+ const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
5
+
6
+ async function up () {
7
+ const options = this.opts()
8
+ const spinner = await createSpinner({ ...options, text: 'keychaining' })
9
+
10
+ logger.debug(`options: ${JSON.stringify(options)}`)
11
+
12
+ try {
13
+ const { changed, privateKeyName, publicKeyValue } = new KeychainUp(options.envFile, options.envKeysFile).run()
14
+ const keyDisplay = armoredKeyDisplay(publicKeyValue) || privateKeyName
15
+
16
+ if (spinner) spinner.stop()
17
+ if (changed) {
18
+ logger.success(`⌥ keychained (${keyDisplay})`)
19
+ } else {
20
+ logger.info(`○ no change (${keyDisplay})`)
21
+ }
22
+ } catch (error) {
23
+ if (spinner) spinner.stop()
24
+ logger.error(error.message)
25
+ process.exit(1)
26
+ }
27
+ }
28
+
29
+ module.exports = up
@@ -21,6 +21,7 @@ async function keypair (key) {
21
21
  envFile: options.envFile,
22
22
  envKeysFile: options.envKeysFile,
23
23
  armor: options.armor,
24
+ noKeychain: options.keychain === false || options.noKeychain === true,
24
25
  token: options.token,
25
26
  command: process.argv.slice(2),
26
27
  onStatus: (text) => {
@@ -63,6 +63,7 @@ async function run () {
63
63
 
64
64
  const sesh = new Session()
65
65
  const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
66
+ const noKeychain = options.keychain === false || options.noKeychain === true
66
67
 
67
68
  if (commandArgs.length < 1) {
68
69
  if (spinner) spinner.stop()
@@ -98,6 +99,7 @@ async function run () {
98
99
  processEnv: process.env,
99
100
  envKeysFile: options.envKeysFile,
100
101
  noArmor,
102
+ noKeychain,
101
103
  token: options.token,
102
104
  command: commandArgs,
103
105
  onStatus: (text) => {
@@ -65,11 +65,12 @@ async function set (key, value) {
65
65
  const fk = options.envKeysFile || '.env.keys'
66
66
  const noCreate = options.create === false
67
67
  const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
68
+ const noKeychain = options.keychain === false || options.noKeychain === true
68
69
 
69
70
  let errorCount = 0
70
71
 
71
72
  try {
72
- const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await setTransform({ envs, key, value, fk, noArmor, noCreate, encrypt })
73
+ const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await setTransform({ envs, key, value, fk, noArmor, noCreate, encrypt, noKeychain })
73
74
 
74
75
  if (keysSrc) {
75
76
  await fsx.writeFileX(fk, keysSrc)
@@ -0,0 +1,43 @@
1
+ function configureKeychainCommand (keychain) {
2
+ keychain
3
+ .description('store private keys in macOS Keychain')
4
+ .action(function () {
5
+ this.help()
6
+ })
7
+
8
+ const upAction = require('./../actions/keychain/up')
9
+ keychain
10
+ .command('up')
11
+ .description('store key in macOS Keychain')
12
+ .option('-f, --env-file <path>', 'path to your env file')
13
+ .option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
14
+ .action(upAction)
15
+
16
+ const downAction = require('./../actions/keychain/down')
17
+ keychain
18
+ .command('down')
19
+ .description('move key from macOS Keychain to .env.keys')
20
+ .option('-f, --env-file <path>', 'path to your env file')
21
+ .option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
22
+ .action(downAction)
23
+
24
+ const pushAction = require('./../actions/keychain/push')
25
+ keychain
26
+ .command('push')
27
+ .description('push key to macOS Keychain from .env.keys')
28
+ .option('-f, --env-file <path>', 'path to your env file')
29
+ .option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
30
+ .action(pushAction)
31
+
32
+ const pullAction = require('./../actions/keychain/pull')
33
+ keychain
34
+ .command('pull')
35
+ .description('pull key from macOS Keychain into .env.keys')
36
+ .option('-f, --env-file <path>', 'path to your env file')
37
+ .option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
38
+ .action(pullAction)
39
+
40
+ return keychain
41
+ }
42
+
43
+ module.exports = configureKeychainCommand
@@ -72,7 +72,8 @@ program.command('run')
72
72
  .option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
73
73
  .option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
74
74
  .option('--token <token>', 'set Armor ⛨ token')
75
- .option('--no-armor', 'disable dotenvx-armor features')
75
+ .option('--no-armor', 'disable Dotenvx Armor features')
76
+ .option('--no-keychain', 'disable macOS Keychain features')
76
77
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
77
78
  .action(function (...args) {
78
79
  this.envs = envs
@@ -95,7 +96,8 @@ program.command('get')
95
96
  .option('-pp, --pretty-print', 'pretty print output')
96
97
  .option('--pp', 'pretty print output (alias)')
97
98
  .option('--format <type>', 'format of the output (json, shell, colon, eval)', 'json')
98
- .option('--no-armor', 'disable dotenvx-armor features')
99
+ .option('--no-armor', 'disable Dotenvx Armor features')
100
+ .option('--no-keychain', 'disable macOS Keychain features')
99
101
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
100
102
  .action(function (...args) {
101
103
  this.envs = envs
@@ -115,7 +117,8 @@ program.command('set')
115
117
  .option('-c, --encrypt', 'encrypt value', true)
116
118
  .option('-p, --plain', 'store value as plain text', false)
117
119
  .option('--no-create', 'do not create .env file(s) when missing')
118
- .option('--no-armor', 'disable dotenvx-armor features')
120
+ .option('--no-armor', 'disable Dotenvx Armor features')
121
+ .option('--no-keychain', 'disable macOS Keychain features')
119
122
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
120
123
  .action(function (...args) {
121
124
  this.envs = envs
@@ -132,7 +135,8 @@ program.command('encrypt')
132
135
  .option('--stdout', 'send to stdout')
133
136
  .option('--token <token>', 'set Armor ⛨ token')
134
137
  .option('--no-create', 'do not create .env file(s) when missing')
135
- .option('--no-armor', 'disable dotenvx-armor features')
138
+ .option('--no-armor', 'disable Dotenvx Armor features')
139
+ .option('--no-keychain', 'disable macOS Keychain features')
136
140
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
137
141
  .action(function (...args) {
138
142
  this.envs = envs
@@ -146,7 +150,8 @@ program.command('decrypt')
146
150
  .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
147
151
  .option('-k, --key <keys...>', 'keys(s) to decrypt (default: all keys in file)')
148
152
  .option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from decryption (default: none)')
149
- .option('--no-armor', 'disable dotenvx-armor features')
153
+ .option('--no-armor', 'disable Dotenvx Armor features')
154
+ .option('--no-keychain', 'disable macOS Keychain features')
150
155
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
151
156
  .option('--stdout', 'send to stdout')
152
157
  .action(function (...args) {
@@ -161,7 +166,8 @@ program.command('keypair')
161
166
  .argument('[KEY]', 'environment variable key name')
162
167
  .option('-f, --env-file <path>', 'path(s) to your env file(s)')
163
168
  .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
164
- .option('--no-armor', 'disable dotenvx-armor features')
169
+ .option('--no-armor', 'disable Dotenvx Armor features')
170
+ .option('--no-keychain', 'disable macOS Keychain features')
165
171
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
166
172
  .option('-pp, --pretty-print', 'pretty print output')
167
173
  .option('--pp', 'pretty print output (alias)')
@@ -255,7 +261,11 @@ program.addHelpText('after', ' ')
255
261
  program.addHelpText('after', 'Professional Security: ')
256
262
  program.addHelpText('after', ' login log in to move keys off-device, share with your team, and audit access')
257
263
  program.addHelpText('after', ' logout log out of connected security features')
258
- program.addHelpText('after', ' armor ⛨ move private keys off-device [www.dotenvx.com/armor]')
264
+ program.addHelpText('after', ' keychain ⌥ move private keys into macOS Keychain')
265
+ program.addHelpText('after', ' armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
266
+
267
+ // dotenvx keychain
268
+ require('./commands/keychain')(program.command('keychain', { hidden: true }))
259
269
 
260
270
  // dotenvx armor
261
271
  require('./commands/armor')(program.command('armor', { hidden: true }))
package/src/lib/main.d.ts CHANGED
@@ -179,6 +179,14 @@ export interface DotenvConfigOptions {
179
179
  */
180
180
  noArmor?: boolean;
181
181
 
182
+ /**
183
+ * Turn off macOS Keychain features.
184
+ *
185
+ * @default false
186
+ * @example require('@dotenvx/dotenvx').config({ noKeychain: true })
187
+ */
188
+ noKeychain?: boolean;
189
+
182
190
  /**
183
191
  * Turn off Dotenvx Armor features. Alias for `noArmor`.
184
192
  *
@@ -262,6 +270,14 @@ export interface SetOptions {
262
270
  */
263
271
  noArmor?: boolean;
264
272
 
273
+ /**
274
+ * Turn off macOS Keychain features.
275
+ *
276
+ * @default false
277
+ * @example require('@dotenvx/dotenvx').set(key, value, { noKeychain: true })
278
+ */
279
+ noKeychain?: boolean;
280
+
265
281
  /**
266
282
  * Turn off Dotenvx Armor features. Alias for `noArmor`.
267
283
  *
@@ -347,6 +363,14 @@ export interface GetOptions {
347
363
  */
348
364
  noArmor?: boolean;
349
365
 
366
+ /**
367
+ * Turn off macOS Keychain features.
368
+ *
369
+ * @default false
370
+ * @example require('@dotenvx/dotenvx').get('KEY', { noKeychain: true })
371
+ */
372
+ noKeychain?: boolean;
373
+
350
374
  /**
351
375
  * Turn off Dotenvx Armor features. Alias for `noArmor`.
352
376
  *
package/src/lib/main.js CHANGED
@@ -66,6 +66,7 @@ const config = function (options = {}) {
66
66
 
67
67
  // dotenvx-armor related
68
68
  const noArmor = resolveNoArmor(options)
69
+ const noKeychain = resolveNoKeychain(options)
69
70
 
70
71
  try {
71
72
  let envs = buildEnvs(options)
@@ -81,6 +82,7 @@ const config = function (options = {}) {
81
82
  processEnv,
82
83
  envKeysFile,
83
84
  noArmor,
85
+ noKeychain,
84
86
  noSpinner: options.noSpinner,
85
87
  token: options.token
86
88
  })
@@ -227,6 +229,7 @@ const set = async function (key, value, options = {}) {
227
229
  const envKeysFilepath = options.envKeysFile
228
230
  const noCreate = options.create === false
229
231
  const noArmor = resolveNoArmor(options)
232
+ const noKeychain = resolveNoKeychain(options)
230
233
 
231
234
  const {
232
235
  keysSrc,
@@ -239,6 +242,7 @@ const set = async function (key, value, options = {}) {
239
242
  value,
240
243
  fk: envKeysFilepath,
241
244
  noArmor,
245
+ noKeychain,
242
246
  noCreate,
243
247
  encrypt
244
248
  })
@@ -306,6 +310,7 @@ const get = async function (key, options = {}) {
306
310
 
307
311
  const envs = buildEnvs(options)
308
312
  const noArmor = resolveNoArmor(options)
313
+ const noKeychain = resolveNoKeychain(options)
309
314
 
310
315
  // ignore
311
316
  const ignore = options.ignore || []
@@ -316,7 +321,8 @@ const get = async function (key, options = {}) {
316
321
  overload: options.overload,
317
322
  all: options.all,
318
323
  envKeysFile: options.envKeysFile,
319
- noArmor
324
+ noArmor,
325
+ noKeychain
320
326
  })
321
327
 
322
328
  for (const error of errors || []) {
@@ -377,6 +383,10 @@ function resolveNoArmor (options = {}) {
377
383
  return options.noArmor === true || options.noOps === true || (!options.token && sesh.noArmorSync())
378
384
  }
379
385
 
386
+ function resolveNoKeychain (options = {}) {
387
+ return options.noKeychain === true || options.keychain === false
388
+ }
389
+
380
390
  module.exports = {
381
391
  // dotenv proxies
382
392
  config,
@@ -1,25 +1,41 @@
1
1
  const Session = require('./../../db/session')
2
2
 
3
3
  const armorProvider = require('./armor/index')
4
+ const keychainProvider = require('./keychain/index')
5
+
4
6
  function syncArmorProvider (publicKeyHex) {
5
7
  const { createSyncFn } = require('synckit')
6
8
  const runProviderSync = createSyncFn(require.resolve('./worker.js'))
7
9
  return runProviderSync(require.resolve('./armor/index'), publicKeyHex)
8
10
  }
9
11
 
10
- async function providers (options = {}) {
11
- if (Object.prototype.hasOwnProperty.call(options, 'provider')) {
12
- return options.provider
13
- }
12
+ function hasKey (keyring, publicKeyHex) {
13
+ return keyring && keyring[publicKeyHex]
14
+ }
14
15
 
15
- if (options.noArmor || options.armor === false) {
16
- return null
16
+ function composeProviders (providerFns) {
17
+ return async function provider (publicKeyHex) {
18
+ for (const providerFn of providerFns) {
19
+ const keyring = await providerFn(publicKeyHex)
20
+ if (hasKey(keyring, publicKeyHex)) return keyring
21
+ }
22
+
23
+ return {}
17
24
  }
25
+ }
18
26
 
19
- const sesh = new Session()
20
- const noArmor = !options.token && await sesh.noArmor()
21
- if (noArmor) return null
27
+ function composeProvidersSync (providerFns) {
28
+ return function providerSync (publicKeyHex) {
29
+ for (const providerFn of providerFns) {
30
+ const keyring = providerFn(publicKeyHex)
31
+ if (hasKey(keyring, publicKeyHex)) return keyring
32
+ }
22
33
 
34
+ return {}
35
+ }
36
+ }
37
+
38
+ function armorProviderForOptions (options) {
23
39
  return (publicKeyHex) => armorProvider(publicKeyHex, {
24
40
  onStatus: options.onStatus,
25
41
  token: options.token,
@@ -27,20 +43,65 @@ async function providers (options = {}) {
27
43
  })
28
44
  }
29
45
 
46
+ function useKeychain (options) {
47
+ if (process.platform !== 'darwin') return false
48
+ if (process.env.CI) return false
49
+ return options.noKeychain !== true && options.keychain !== false
50
+ }
51
+
52
+ function useArmor (options, noArmor) {
53
+ return options.noArmor !== true && options.armor !== false && !noArmor
54
+ }
55
+
56
+ function providerFrom (providerFns, compose) {
57
+ if (providerFns.length === 0) return null
58
+ if (providerFns.length === 1) return providerFns[0]
59
+
60
+ return compose(providerFns)
61
+ }
62
+
63
+ async function providers (options = {}) {
64
+ if (Object.prototype.hasOwnProperty.call(options, 'provider')) {
65
+ return options.provider
66
+ }
67
+
68
+ const providerFns = []
69
+
70
+ if (useKeychain(options)) {
71
+ providerFns.push(keychainProvider)
72
+ }
73
+
74
+ if (options.noArmor !== true && options.armor !== false) {
75
+ const sesh = new Session()
76
+ const noArmor = !options.token && await sesh.noArmor()
77
+ if (useArmor(options, noArmor)) {
78
+ providerFns.push(armorProviderForOptions(options))
79
+ }
80
+ }
81
+
82
+ return providerFrom(providerFns, composeProviders)
83
+ }
84
+
30
85
  providers.sync = function providersSync (options = {}) {
31
86
  if (Object.prototype.hasOwnProperty.call(options, 'provider')) {
32
87
  return options.provider
33
88
  }
34
89
 
35
- if (options.noArmor || options.armor === false) {
36
- return null
90
+ const providerFns = []
91
+
92
+ if (useKeychain(options)) {
93
+ providerFns.push(keychainProvider)
37
94
  }
38
95
 
39
- const sesh = new Session()
40
- const noArmor = !options.token && sesh.noArmorSync()
41
- if (noArmor) return null
96
+ if (options.noArmor !== true && options.armor !== false) {
97
+ const sesh = new Session()
98
+ const noArmor = !options.token && sesh.noArmorSync()
99
+ if (useArmor(options, noArmor)) {
100
+ providerFns.push(syncArmorProvider)
101
+ }
102
+ }
42
103
 
43
- return syncArmorProvider
104
+ return providerFrom(providerFns, composeProvidersSync)
44
105
  }
45
106
 
46
107
  module.exports = providers
@@ -0,0 +1,20 @@
1
+ const { execFileSync } = require('child_process')
2
+
3
+ const SECURITY_BIN = '/usr/bin/security'
4
+ const SERVICE = 'dotenvx'
5
+
6
+ function index (publicKeyHex) {
7
+ if (process.platform !== 'darwin') return {}
8
+
9
+ try {
10
+ const stdout = execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKeyHex, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
11
+ const privateKeyHex = stdout.trim()
12
+ if (!privateKeyHex) return {}
13
+
14
+ return { [publicKeyHex]: privateKeyHex }
15
+ } catch {
16
+ return {}
17
+ }
18
+ }
19
+
20
+ module.exports = index
@@ -235,7 +235,7 @@ async function envs (options = {}) {
235
235
  const readableFilepaths = new Set()
236
236
  const processEnv = options.processEnv || process.env
237
237
  const envKeysFilepath = options.envKeysFilepath || options.envKeysFile || null
238
- const provider = options.noArmor ? null : await providers(options)
238
+ const provider = await providers(options)
239
239
 
240
240
  for (const env of options.envs || []) {
241
241
  if (env.type === TYPE_ENV_FILE) {
@@ -269,7 +269,7 @@ function envsSync (options = {}) {
269
269
  const readableFilepaths = new Set()
270
270
  const processEnv = options.processEnv || process.env
271
271
  const envKeysFilepath = options.envKeysFilepath || options.envKeysFile || null
272
- const provider = options.noArmor ? null : providers.sync(options)
272
+ const provider = providers.sync(options)
273
273
 
274
274
  for (const env of options.envs || []) {
275
275
  if (env.type === TYPE_ENV_FILE) {
@@ -50,6 +50,7 @@ function buildOptions (options, processEnv) {
50
50
  processEnv,
51
51
  envKeysFilepath: options.envKeysFilepath || options.envKeysFile || null,
52
52
  noArmor: options.noArmor,
53
+ noKeychain: options.noKeychain,
53
54
  onStatus: options.onStatus
54
55
  }
55
56
  }
@@ -0,0 +1,57 @@
1
+ const { execFileSync } = require('child_process')
2
+
3
+ const keynames = require('../conventions/keynames')
4
+ const readEnvKey = require('../helpers/readEnvKey')
5
+ const upsertEnvKey = require('../helpers/upsertEnvKey')
6
+
7
+ const SECURITY_BIN = '/usr/bin/security'
8
+ const SERVICE = 'dotenvx'
9
+
10
+ class KeychainDown {
11
+ constructor (envFile = '.env', envKeysFile = '.env.keys') {
12
+ this.envFile = envFile
13
+ this.envKeysFile = envKeysFile
14
+ }
15
+
16
+ run () {
17
+ const envFile = this.envFile
18
+ const envKeysFile = this.envKeysFile
19
+
20
+ const {
21
+ publicKeyName,
22
+ privateKeyName
23
+ } = keynames(envFile)
24
+
25
+ const publicKey = readEnvKey(publicKeyName, envFile, { strict: true, ignore: ['MISSING_PRIVATE_KEY'] })
26
+ let privateKey
27
+
28
+ try {
29
+ privateKey = execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
30
+ } catch (error) {
31
+ privateKey = readEnvKey(privateKeyName, envKeysFile, { strict: false })
32
+
33
+ if (privateKey) {
34
+ return {
35
+ changed: false,
36
+ privateKeyName,
37
+ privateKeyValue: privateKey,
38
+ publicKeyValue: publicKey
39
+ }
40
+ }
41
+
42
+ throw error
43
+ }
44
+
45
+ upsertEnvKey(privateKeyName, privateKey, envKeysFile)
46
+ execFileSync(SECURITY_BIN, ['delete-generic-password', '-s', SERVICE, '-a', publicKey], { stdio: 'ignore' })
47
+
48
+ return {
49
+ changed: true,
50
+ privateKeyName,
51
+ privateKeyValue: privateKey,
52
+ publicKeyValue: publicKey
53
+ }
54
+ }
55
+ }
56
+
57
+ module.exports = KeychainDown
@@ -0,0 +1,46 @@
1
+ const { execFileSync } = require('child_process')
2
+
3
+ const keynames = require('../conventions/keynames')
4
+ const readEnvKey = require('../helpers/readEnvKey')
5
+ const upsertEnvKey = require('../helpers/upsertEnvKey')
6
+ const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
7
+
8
+ const SECURITY_BIN = '/usr/bin/security'
9
+ const SERVICE = 'dotenvx'
10
+
11
+ class KeychainPull {
12
+ constructor (envFile = '.env', envKeysFile = '.env.keys') {
13
+ this.envFile = envFile
14
+ this.envKeysFile = envKeysFile
15
+ }
16
+
17
+ run () {
18
+ const envFile = this.envFile
19
+ const envKeysFile = this.envKeysFile
20
+
21
+ const {
22
+ publicKeyName,
23
+ privateKeyName
24
+ } = keynames(envFile)
25
+
26
+ const publicKey = readEnvKey(publicKeyName, envFile, { strict: true, ignore: ['MISSING_PRIVATE_KEY'] })
27
+ let privateKey
28
+
29
+ try {
30
+ privateKey = execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
31
+ } catch {
32
+ throw new Error(`[NOT_FOUND] private key not found in macOS Keychain (${armoredKeyDisplay(publicKey)}). fix: [dotenvx keychain up]`)
33
+ }
34
+
35
+ const result = upsertEnvKey(privateKeyName, privateKey, envKeysFile)
36
+
37
+ return {
38
+ changed: result.changed,
39
+ privateKeyName,
40
+ privateKeyValue: privateKey,
41
+ publicKeyValue: publicKey
42
+ }
43
+ }
44
+ }
45
+
46
+ module.exports = KeychainPull
@@ -0,0 +1,63 @@
1
+ const { execFileSync } = require('child_process')
2
+
3
+ const keynames = require('../conventions/keynames')
4
+ const readEnvKey = require('../helpers/readEnvKey')
5
+ const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
6
+
7
+ const SECURITY_BIN = '/usr/bin/security'
8
+ const SERVICE = 'dotenvx'
9
+
10
+ function addGenericPassword (publicKey, label, privateKey) {
11
+ try {
12
+ execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', publicKey, '-l', label, '-w', privateKey], { stdio: 'ignore' })
13
+ } catch {
14
+ throw new Error('failed to save private key to macOS Keychain')
15
+ }
16
+ }
17
+
18
+ class KeychainPush {
19
+ constructor (envFile = '.env', envKeysFile = '.env.keys') {
20
+ this.envFile = envFile
21
+ this.envKeysFile = envKeysFile
22
+ }
23
+
24
+ run () {
25
+ const envFile = this.envFile
26
+ const envKeysFile = this.envKeysFile
27
+
28
+ const {
29
+ publicKeyName,
30
+ privateKeyName
31
+ } = keynames(envFile)
32
+
33
+ const publicKey = readEnvKey(publicKeyName, envFile, { strict: true, ignore: ['MISSING_PRIVATE_KEY'] })
34
+ const privateKey = readEnvKey(privateKeyName, envKeysFile, { strict: true })
35
+ const label = `dotenvx (${armoredKeyDisplay(publicKey)})`
36
+
37
+ try {
38
+ const existingPrivateKey = execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
39
+
40
+ if (existingPrivateKey === privateKey) {
41
+ return {
42
+ changed: false,
43
+ label,
44
+ privateKeyName,
45
+ privateKeyValue: privateKey,
46
+ publicKeyValue: publicKey
47
+ }
48
+ }
49
+ } catch {}
50
+
51
+ addGenericPassword(publicKey, label, privateKey)
52
+
53
+ return {
54
+ changed: true,
55
+ label,
56
+ privateKeyName,
57
+ privateKeyValue: privateKey,
58
+ publicKeyValue: publicKey
59
+ }
60
+ }
61
+ }
62
+
63
+ module.exports = KeychainPush
@@ -0,0 +1,81 @@
1
+ const { execFileSync } = require('child_process')
2
+
3
+ const keynames = require('../conventions/keynames')
4
+ const readEnvKey = require('../helpers/readEnvKey')
5
+ const removeEnvKey = require('../helpers/removeEnvKey')
6
+ const armoredKeyDisplay = require('../helpers/armoredKeyDisplay')
7
+
8
+ const SECURITY_BIN = '/usr/bin/security'
9
+ const SERVICE = 'dotenvx'
10
+
11
+ function addGenericPassword (publicKey, label, privateKey) {
12
+ try {
13
+ execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', publicKey, '-l', label, '-w', privateKey], { stdio: 'ignore' })
14
+ } catch {
15
+ throw new Error('failed to save private key to macOS Keychain')
16
+ }
17
+ }
18
+
19
+ class KeychainUp {
20
+ constructor (envFile = '.env', envKeysFile = '.env.keys') {
21
+ this.envFile = envFile
22
+ this.envKeysFile = envKeysFile
23
+ }
24
+
25
+ run () {
26
+ const envFile = this.envFile
27
+ const envKeysFile = this.envKeysFile
28
+
29
+ const {
30
+ publicKeyName,
31
+ privateKeyName
32
+ } = keynames(envFile)
33
+
34
+ const publicKey = readEnvKey(publicKeyName, envFile, { strict: true, ignore: ['MISSING_PRIVATE_KEY'] })
35
+ const label = `dotenvx (${armoredKeyDisplay(publicKey)})`
36
+ let existingPrivateKey
37
+
38
+ try {
39
+ existingPrivateKey = execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
40
+ } catch {}
41
+
42
+ const privateKey = readEnvKey(privateKeyName, envKeysFile, { strict: !existingPrivateKey })
43
+
44
+ if (existingPrivateKey) {
45
+ if (!privateKey) {
46
+ return {
47
+ changed: false,
48
+ label,
49
+ privateKeyName,
50
+ privateKeyValue: existingPrivateKey,
51
+ publicKeyValue: publicKey
52
+ }
53
+ }
54
+
55
+ if (existingPrivateKey === privateKey) {
56
+ const result = removeEnvKey(privateKeyName, envKeysFile)
57
+
58
+ return {
59
+ changed: result.changed,
60
+ label,
61
+ privateKeyName,
62
+ privateKeyValue: privateKey,
63
+ publicKeyValue: publicKey
64
+ }
65
+ }
66
+ }
67
+
68
+ addGenericPassword(publicKey, label, privateKey)
69
+ removeEnvKey(privateKeyName, envKeysFile)
70
+
71
+ return {
72
+ changed: true,
73
+ label,
74
+ privateKeyName,
75
+ privateKeyValue: privateKey,
76
+ publicKeyValue: publicKey
77
+ }
78
+ }
79
+ }
80
+
81
+ module.exports = KeychainUp
@@ -22,7 +22,7 @@ async function decrypt (options = {}) {
22
22
  const ik = options.ik
23
23
  const ek = options.ek
24
24
  const fk = options.fk
25
- const provider = options.noArmor ? null : await providers(options)
25
+ const provider = await providers(options)
26
26
 
27
27
  const processedEnvs = []
28
28
  const changedFilepaths = []
@@ -36,6 +36,7 @@ async function setTransform (options = {}) {
36
36
  const value = options.value
37
37
  const fk = options.fk || '.env.keys'
38
38
  let noArmor = options.noArmor // key storage selector below
39
+ const noKeychain = options.noKeychain
39
40
  const noCreate = options.noCreate
40
41
  const noEncrypt = !options.encrypt || isPlainKey(key)
41
42
 
@@ -149,7 +150,8 @@ async function setTransform (options = {}) {
149
150
  envs: [env],
150
151
  all: true,
151
152
  envKeysFile: fk,
152
- noArmor
153
+ noArmor,
154
+ noKeychain
153
155
  })
154
156
 
155
157
  const before = parsed[key]