@dotenvx/dotenvx 2.1.5 → 2.2.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.
@@ -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)')
@@ -180,6 +186,15 @@ program.command('ls')
180
186
  return require('./actions/ls').apply(this, args)
181
187
  })
182
188
 
189
+ // dotenvx genexample
190
+ program.command('genexample')
191
+ .description('generate .env.example')
192
+ .argument('[directory]', 'directory to generate from', '.')
193
+ .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', '.env')
194
+ .action(function (...args) {
195
+ return require('./actions/ext/genexample').apply(this, args)
196
+ })
197
+
183
198
  // dotenvx gitignore
184
199
  program.command('gitignore')
185
200
  .description('append to .gitignore')
@@ -255,7 +270,11 @@ program.addHelpText('after', ' ')
255
270
  program.addHelpText('after', 'Professional Security: ')
256
271
  program.addHelpText('after', ' login log in to move keys off-device, share with your team, and audit access')
257
272
  program.addHelpText('after', ' logout log out of connected security features')
258
- program.addHelpText('after', ' armor ⛨ move private keys off-device [www.dotenvx.com/armor]')
273
+ program.addHelpText('after', ' keychain ⌥ move private keys into macOS Keychain')
274
+ program.addHelpText('after', ' armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
275
+
276
+ // dotenvx keychain
277
+ require('./commands/keychain')(program.command('keychain', { hidden: true }))
259
278
 
260
279
  // dotenvx armor
261
280
  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
  }
@@ -45,7 +45,7 @@ class Genexample {
45
45
  const filepath = path.resolve(this.directory, envFilepath)
46
46
  if (!fsx.existsSync(filepath)) {
47
47
  const error = new Errors({ envFilepath, filepath }).missingEnvFile()
48
- error.help = `? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx ext genexample]`
48
+ error.help = `? add it with [echo "HELLO=World" > ${envFilepath}] and then run [dotenvx genexample]`
49
49
  throw error
50
50
  }
51
51
 
@@ -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