@dotenvx/dotenvx 1.36.0 → 1.38.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,22 @@
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.36.0...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.38.0...main)
6
+
7
+ ## [1.38.0](https://github.com/dotenvx/dotenvx/compare/v1.37.0...v1.38.0)
8
+
9
+ ### Changed
10
+
11
+ * Command substitution failures no longer halt further processing of keys in a .env file ([#533](https://github.com/dotenvx/dotenvx/pull/533))
12
+ * A more helpful error is raised if a command substitution failure occurs ([#533](https://github.com/dotenvx/dotenvx/pull/533))
13
+
14
+ ## [1.37.0](https://github.com/dotenvx/dotenvx/compare/v1.36.0...v1.37.0)
15
+
16
+ ### Added
17
+
18
+ * add `dotenvx rotate` command 🎉 ([#530](https://github.com/dotenvx/dotenvx/pull/530))
19
+
20
+ also: [our whitepaper](https://dotenvx.com/dotenvx.pdf) is released as a draft.
6
21
 
7
22
  ## [1.36.0](https://github.com/dotenvx/dotenvx/compare/v1.35.0...v1.36.0)
8
23
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  * multi-environment
7
7
  * encrypted envs
8
8
 
9
- [Read the whitepaper](https://dotenvx.com/dotenvx.pdf)
9
+ [Read the whitepaper](https://dotenvx.com/dotenvx.pdf?v=README)
10
10
 
11
11
   
12
12
 
@@ -1685,7 +1685,117 @@ More examples
1685
1685
  ```
1686
1686
 
1687
1687
  </details>
1688
+ * <details><summary>`rotate`</summary><br>
1688
1689
 
1690
+ Rotate public/private keys for `.env` file and re-encrypt all encrypted values.
1691
+
1692
+ ```sh
1693
+ $ echo "HELLO=World" > .env
1694
+ $ dotenvx encrypt
1695
+ ✔ encrypted (.env)
1696
+ $ dotenvx rotate
1697
+ ✔ rotated (.env)
1698
+ ```
1699
+
1700
+ </details>
1701
+ * <details><summary>`rotate -f`</summary><br>
1702
+
1703
+ Rotate public/private keys for a specified encrypted `.env` file and re-encrypt all encrypted values.
1704
+
1705
+ ```sh
1706
+ $ echo "HELLO=World" > .env
1707
+ $ echo "HELLO=Production" > .env.production
1708
+
1709
+ $ dotenvx encrypt -f .env.production
1710
+ ✔ encrypted (.env.production)
1711
+ $ dotenvx rotate -f .env.production
1712
+ ✔ rotated (.env.production)
1713
+ ```
1714
+
1715
+ </details>
1716
+ * <details><summary>`rotate -fk`</summary><br>
1717
+
1718
+ Specify path to `.env.keys`. This is useful with monorepos.
1719
+
1720
+ ```sh
1721
+ $ mkdir -p apps/app1
1722
+ $ echo "HELLO=World" > apps/app1/.env
1723
+
1724
+ $ dotenvx encrypt -fk .env.keys -f apps/app1/.env
1725
+ ✔ encrypted (apps/app1/.env)
1726
+ $ dotenvx rotate -fk .env.keys -f apps/app1/.env
1727
+ ✔ rotated (apps/app1/.env)
1728
+ ```
1729
+
1730
+ </details>
1731
+ * <details><summary>`rotate -k`</summary><br>
1732
+
1733
+ Rotate the contents of a specified key inside an encrypted `.env` file.
1734
+
1735
+ ```sh
1736
+ $ echo "HELLO=World\nHOLA=Mundo" > .env
1737
+ $ dotenvx encrypt
1738
+ ✔ encrypted (.env)
1739
+ $ dotenvx rotate -k HELLO
1740
+ ✔ rotated (.env)
1741
+ ```
1742
+
1743
+ Even specify a glob pattern.
1744
+
1745
+ ```sh
1746
+ $ echo "HELLO=World\nHOLA=Mundo" > .env
1747
+ $ dotenvx encrypt
1748
+ ✔ encrypted (.env)
1749
+ $ dotenvx rotate -k "HE*"
1750
+ ✔ rotated (.env)
1751
+ ```
1752
+
1753
+ </details>
1754
+ * <details><summary>`rotate -ek`</summary><br>
1755
+
1756
+ Rotate the encrypted contents inside an encrypted `.env` file except for an exluded key.
1757
+
1758
+ ```sh
1759
+ $ echo "HELLO=World\nHOLA=Mundo" > .env
1760
+ $ dotenvx encrypt
1761
+ ✔ encrypted (.env)
1762
+ $ dotenvx rotate -ek HOLA
1763
+ ✔ rotated (.env)
1764
+ ```
1765
+
1766
+ Even specify a glob pattern.
1767
+
1768
+ ```sh
1769
+ $ echo "HELLO=World\nHOLA=Mundo" > .env
1770
+ $ dotenvx encrypt
1771
+ ✔ encrypted (.env)
1772
+ $ dotenvx rotate -ek "HO*"
1773
+ ✔ rotated (.env)
1774
+ ```
1775
+
1776
+ </details>
1777
+ * <details><summary>`rotate --stdout`</summary><br>
1778
+
1779
+ Rotate the contents of an encrypted `.env` file and send to stdout.
1780
+
1781
+ ```sh
1782
+ $ dotenvx rotate --stdout
1783
+ #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
1784
+ #/ public-key encryption for .env files /
1785
+ #/ [how it works](https://dotenvx.com/encryption) /
1786
+ #/----------------------------------------------------------/
1787
+ DOTENV_PUBLIC_KEY="034af93e93708b994c10f236c96ef88e47291066946cce2e8d98c9e02c741ced45"
1788
+ # .env
1789
+ HELLO="encrypted:12345"
1790
+ ```
1791
+
1792
+ or send to a file:
1793
+
1794
+ ```sh
1795
+ $ dotenvx rotate --stdout > somefile.txt
1796
+ ```
1797
+
1798
+ </details>
1689
1799
  * <details><summary>`help`</summary><br>
1690
1800
 
1691
1801
  Output help for `dotenvx`.
@@ -2134,6 +2244,22 @@ More examples
2134
2244
 
2135
2245
  ## FAQ
2136
2246
 
2247
+ #### How does encryption work?
2248
+
2249
+ Dotenvx uses Elliptic Curve Integrated Encryption Scheme (ECIES) to encrypt each secret with a unique ephemeral key, while ensuring it can be decrypted using a long-term private key.
2250
+
2251
+ When you initialize encryption, a DOTENV_PUBLIC_KEY (encryption key) and DOTENV_PRIVATE_KEY (decryption key) are generated. The DOTENV_PUBLIC_KEY is used to encrypt secrets, and the DOTENV_PRIVATE_KEY is securely stored in your cloud secrets manager or .env.keys file.
2252
+
2253
+ Your encrypted .env file is then safely committed to code. Even if the file is exposed, secrets remain protected since decryption requires the separate DOTENV_PRIVATE_KEY, which is never stored alongside it. Read [the whitepaper](https://dotenvx.com/dotenvx.pdf?v=README) for more details.
2254
+
2255
+ #### Is it safe to commit an encrypted .env file to code?
2256
+
2257
+ Yes. Dotenvx encrypts secrets using AES-256 with ephemeral keys, ensuring that even if the encrypted .env file is exposed, its contents remain secure. The encryption keys themselves are protected using Secp256k1 elliptic curve cryptography, which is widely used for secure key exchange in technologies like Bitcoin.
2258
+
2259
+ This means that every secret in the .env file is encrypted with a unique AES-256 key, and that key is further encrypted using a public key (Secp256k1). Even if an attacker obtains the encrypted .env file, they would still need the corresponding private key—stored separately in a secrets manager—to decrypt anything.
2260
+
2261
+ Breaking this encryption would require brute-forcing both AES-256 and elliptic curve cryptography, which is computationally infeasible with current technology. Read [the whitepaper](https://dotenvx.com/dotenvx.pdf?v=README) for more details.
2262
+
2137
2263
  #### Why am I getting the error `node: .env: not found`?
2138
2264
 
2139
2265
  You are using Node 20 or greater and it adds a differing implementation of `--env-file` flag support. Rather than warn on a missing `.env` file (like dotenv has historically done), it raises an error: `node: .env: not found`.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.36.0",
2
+ "version": "1.38.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -0,0 +1,83 @@
1
+ const fsx = require('./../../lib/helpers/fsx')
2
+ const { logger } = require('./../../shared/logger')
3
+
4
+ const Rotate = require('./../../lib/services/rotate')
5
+
6
+ const catchAndLog = require('../../lib/helpers/catchAndLog')
7
+ const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys')
8
+
9
+ function rotate () {
10
+ const options = this.opts()
11
+ logger.debug(`options: ${JSON.stringify(options)}`)
12
+
13
+ const envs = this.envs
14
+
15
+ // stdout - should not have a try so that exit codes can surface to stdout
16
+ if (options.stdout) {
17
+ const {
18
+ processedEnvs
19
+ } = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run()
20
+
21
+ for (const processedEnv of processedEnvs) {
22
+ console.log(processedEnv.envSrc)
23
+ console.log('')
24
+ console.log(processedEnv.envKeysSrc)
25
+ }
26
+ process.exit(0) // exit early
27
+ } else {
28
+ try {
29
+ const {
30
+ processedEnvs,
31
+ changedFilepaths,
32
+ unchangedFilepaths
33
+ } = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run()
34
+
35
+ for (const processedEnv of processedEnvs) {
36
+ logger.verbose(`rotating ${processedEnv.envFilepath} (${processedEnv.filepath})`)
37
+ if (processedEnv.error) {
38
+ if (processedEnv.error.code === 'MISSING_ENV_FILE') {
39
+ logger.warn(processedEnv.error.message)
40
+ logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx rotate]`)
41
+ } else {
42
+ logger.warn(processedEnv.error.message)
43
+ if (processedEnv.error.help) {
44
+ logger.help(processedEnv.error.help)
45
+ }
46
+ }
47
+ } else if (processedEnv.changed) {
48
+ fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
49
+ fsx.writeFileX(processedEnv.envKeysFilepath, processedEnv.envKeysSrc)
50
+
51
+ logger.verbose(`rotated ${processedEnv.envFilepath} (${processedEnv.filepath})`)
52
+ } else {
53
+ logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`)
54
+ }
55
+ }
56
+
57
+ if (changedFilepaths.length > 0) {
58
+ logger.success(`✔ rotated (${changedFilepaths.join(',')})`)
59
+ } else if (unchangedFilepaths.length > 0) {
60
+ logger.info(`no changes (${unchangedFilepaths})`)
61
+ } else {
62
+ // do nothing - scenario when no .env files found
63
+ }
64
+
65
+ for (const processedEnv of processedEnvs) {
66
+ if (processedEnv.privateKeyAdded) {
67
+ logger.success(`✔ key added to .env.keys (${processedEnv.privateKeyName})`)
68
+
69
+ if (!isIgnoringDotenvKeys()) {
70
+ logger.help('⮕ next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys')
71
+ }
72
+
73
+ logger.help(`⮕ next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx get] to test decryption locally`)
74
+ }
75
+ }
76
+ } catch (error) {
77
+ catchAndLog(error)
78
+ process.exit(1)
79
+ }
80
+ }
81
+ }
82
+
83
+ module.exports = rotate
@@ -156,6 +156,20 @@ program.command('ls')
156
156
  .option('-ef, --exclude-env-file <excludeFilenames...>', 'path(s) to exclude from your env file(s) (default: none)')
157
157
  .action(lsAction)
158
158
 
159
+ // dotenvx rotate
160
+ const rotateAction = require('./actions/rotate')
161
+ program.command('rotate')
162
+ .description('rotate keypair(s) and re-encrypt .env file(s)')
163
+ .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
164
+ .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
165
+ .option('-k, --key <keys...>', 'keys(s) to encrypt (default: all keys in file)')
166
+ .option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from encryption (default: none)')
167
+ .option('--stdout', 'send to stdout')
168
+ .action(function (...args) {
169
+ this.envs = envs
170
+ rotateAction.apply(this, args)
171
+ })
172
+
159
173
  // dotenvx help
160
174
  program.command('help [command]')
161
175
  .description('display help for command')
@@ -0,0 +1,61 @@
1
+ const quotes = require('./quotes')
2
+ const dotenvParse = require('./dotenvParse')
3
+ const escapeForRegex = require('./escapeForRegex')
4
+ const escapeDollarSigns = require('./escapeDollarSigns')
5
+
6
+ function append (src, key, appendValue) {
7
+ let output
8
+ let newPart = ''
9
+
10
+ const parsed = dotenvParse(src, true) // skip expanding \n
11
+ const _quotes = quotes(src)
12
+ if (Object.prototype.hasOwnProperty.call(parsed, key)) {
13
+ const quote = _quotes[key]
14
+ const originalValue = parsed[key]
15
+
16
+ newPart += `${key}=${quote}${originalValue},${appendValue}${quote}`
17
+
18
+ const escapedOriginalValue = escapeForRegex(originalValue)
19
+
20
+ // conditionally enforce end of line
21
+ let enforceEndOfLine = ''
22
+ if (escapedOriginalValue === '') {
23
+ enforceEndOfLine = '$' // EMPTY scenario
24
+ }
25
+
26
+ const currentPart = new RegExp(
27
+ '^' + // start of line
28
+ '(\\s*)?' + // spaces
29
+ '(export\\s+)?' + // export
30
+ key + // KEY
31
+ '\\s*=\\s*' + // spaces (KEY = value)
32
+ '["\'`]?' + // open quote
33
+ escapedOriginalValue + // escaped value
34
+ '["\'`]?' + // close quote
35
+ enforceEndOfLine
36
+ ,
37
+ 'gm' // (g)lobal (m)ultiline
38
+ )
39
+
40
+ const saferInput = escapeDollarSigns(newPart) // cleanse user inputted capture groups ($1, $2 etc)
41
+
42
+ // $1 preserves spaces
43
+ // $2 preserves export
44
+ output = src.replace(currentPart, `$1$2${saferInput}`)
45
+ } else {
46
+ newPart += `${key}="${appendValue}"`
47
+
48
+ // append
49
+ if (src.endsWith('\n')) {
50
+ newPart = newPart + '\n'
51
+ } else {
52
+ newPart = '\n' + newPart
53
+ }
54
+
55
+ output = src + newPart
56
+ }
57
+
58
+ return output
59
+ }
60
+
61
+ module.exports = append
@@ -8,6 +8,7 @@ class Errors {
8
8
  this.key = options.key
9
9
  this.privateKey = options.privateKey
10
10
  this.privateKeyName = options.privateKeyName
11
+ this.command = options.command
11
12
 
12
13
  this.message = options.message
13
14
  }
@@ -84,6 +85,17 @@ class Errors {
84
85
  e.code = code
85
86
  return e
86
87
  }
88
+
89
+ commandSubstitutionFailed () {
90
+ const code = 'COMMAND_SUBSTITUTION_FAILED'
91
+ const message = `[${code}] could not eval ${this.key} containing command '${this.command}': ${this.message}`
92
+ const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/532`
93
+
94
+ const e = new Error(message)
95
+ e.code = code
96
+ e.help = help
97
+ return e
98
+ }
87
99
  }
88
100
 
89
101
  module.exports = Errors
@@ -0,0 +1,23 @@
1
+ const { execSync } = require('child_process')
2
+ const chomp = require('./chomp')
3
+ const Errors = require('./errors')
4
+
5
+ function evalKeyValue (key, value, processEnv, runningParsed) {
6
+ // Match everything between the outermost $() using a regex with non-capturing groups
7
+ const matches = value.match(/\$\(([^)]+(?:\)[^(]*)*)\)/g) || []
8
+ return matches.reduce((newValue, match) => {
9
+ const command = match.slice(2, -1) // Extract command by removing $() wrapper
10
+ let result
11
+
12
+ try {
13
+ result = execSync(command, { env: { ...processEnv, ...runningParsed } }).toString() // execute command (including runningParsed)
14
+ } catch (e) {
15
+ throw new Errors({ key, command, message: e.message.trim() }).commandSubstitutionFailed()
16
+ }
17
+
18
+ result = chomp(result) // chomp it
19
+ return newValue.replace(match, result) // Replace match with result
20
+ }, value)
21
+ }
22
+
23
+ module.exports = evalKeyValue
@@ -1,7 +1,6 @@
1
- const chomp = require('./chomp')
2
1
  const decryptKeyValue = require('./decryptKeyValue')
2
+ const evalKeyValue = require('./evalKeyValue')
3
3
  const resolveEscapeSequences = require('./resolveEscapeSequences')
4
- const { execSync } = require('child_process')
5
4
 
6
5
  class Parse {
7
6
  static LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
@@ -49,7 +48,12 @@ class Parse {
49
48
  let evaled = false
50
49
  if (quote !== "'" && (!this.inProcessEnv(key) || this.processEnv[key] === this.parsed[key])) {
51
50
  const priorEvaled = this.parsed[key]
52
- this.parsed[key] = this.eval(priorEvaled)
51
+ // eval
52
+ try {
53
+ this.parsed[key] = this.eval(key, priorEvaled)
54
+ } catch (e) {
55
+ this.errors.push(e)
56
+ }
53
57
  if (priorEvaled !== this.parsed[key]) {
54
58
  evaled = true
55
59
  }
@@ -133,14 +137,8 @@ class Parse {
133
137
  return decryptKeyValue(key, value, this.privateKeyName, this.privateKey)
134
138
  }
135
139
 
136
- eval (value) {
137
- // Match everything between the outermost $() using a regex with non-capturing groups
138
- const matches = value.match(/\$\(([^)]+(?:\)[^(]*)*)\)/g) || []
139
- return matches.reduce((newValue, match) => {
140
- const command = match.slice(2, -1) // Extract command by removing $() wrapper
141
- const result = chomp(execSync(command, { env: { ...this.processEnv, ...this.runningParsed } }).toString()) // execute command (including runningParsed)
142
- return newValue.replace(match, result) // Replace match with result
143
- }, value)
140
+ eval (key, value) {
141
+ return evalKeyValue(key, value, this.processEnv, this.runningParsed)
144
142
  }
145
143
 
146
144
  expand (value) {
@@ -0,0 +1,160 @@
1
+ const fsx = require('./../helpers/fsx')
2
+ const path = require('path')
3
+ const picomatch = require('picomatch')
4
+
5
+ const TYPE_ENV_FILE = 'envFile'
6
+
7
+ const Errors = require('./../helpers/errors')
8
+ const guessPrivateKeyName = require('./../helpers/guessPrivateKeyName')
9
+ const guessPublicKeyName = require('./../helpers/guessPublicKeyName')
10
+ const encryptValue = require('./../helpers/encryptValue')
11
+ const isEncrypted = require('./../helpers/isEncrypted')
12
+ const dotenvParse = require('./../helpers/dotenvParse')
13
+ const replace = require('./../helpers/replace')
14
+ const append = require('./../helpers/append')
15
+ const detectEncoding = require('./../helpers/detectEncoding')
16
+ const determineEnvs = require('./../helpers/determineEnvs')
17
+ const findPrivateKey = require('./../helpers/findPrivateKey')
18
+ const decryptKeyValue = require('./../helpers/decryptKeyValue')
19
+ const keypair = require('./../helpers/keypair')
20
+
21
+ class Rotate {
22
+ constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null) {
23
+ this.envs = determineEnvs(envs, process.env)
24
+ this.key = key
25
+ this.excludeKey = excludeKey
26
+ this.envKeysFilepath = envKeysFilepath
27
+
28
+ this.processedEnvs = []
29
+ this.changedFilepaths = new Set()
30
+ this.unchangedFilepaths = new Set()
31
+
32
+ this.envKeysSources = {}
33
+ }
34
+
35
+ run () {
36
+ // example
37
+ // envs [
38
+ // { type: 'envFile', value: '.env' }
39
+ // ]
40
+
41
+ this.keys = this._keys()
42
+ const excludeKeys = this._excludeKeys()
43
+
44
+ this.exclude = picomatch(excludeKeys)
45
+ this.include = picomatch(this.keys, { ignore: excludeKeys })
46
+
47
+ for (const env of this.envs) {
48
+ if (env.type === TYPE_ENV_FILE) {
49
+ this._rotateEnvFile(env.value)
50
+ }
51
+ }
52
+
53
+ return {
54
+ processedEnvs: this.processedEnvs,
55
+ changedFilepaths: [...this.changedFilepaths],
56
+ unchangedFilepaths: [...this.unchangedFilepaths]
57
+ }
58
+ }
59
+
60
+ _rotateEnvFile (envFilepath) {
61
+ const row = {}
62
+ row.keys = []
63
+ row.type = TYPE_ENV_FILE
64
+
65
+ const filepath = path.resolve(envFilepath)
66
+ row.filepath = filepath
67
+ row.envFilepath = envFilepath
68
+
69
+ try {
70
+ const encoding = this._detectEncoding(filepath)
71
+ let envSrc = fsx.readFileX(filepath, { encoding })
72
+ const envParsed = dotenvParse(envSrc)
73
+
74
+ const publicKeyName = guessPublicKeyName(envFilepath)
75
+ const privateKeyName = guessPrivateKeyName(envFilepath)
76
+ const existingPrivateKey = findPrivateKey(envFilepath, this.envKeysFilepath)
77
+
78
+ let envKeysFilepath = path.join(path.dirname(filepath), '.env.keys')
79
+ if (this.envKeysFilepath) {
80
+ envKeysFilepath = path.resolve(this.envKeysFilepath)
81
+ }
82
+ const keysEncoding = this._detectEncoding(envKeysFilepath)
83
+
84
+ row.envKeysFilepath = envKeysFilepath
85
+ this.envKeysSources[envKeysFilepath] ||= fsx.readFileX(envKeysFilepath, { encoding: keysEncoding })
86
+ let envKeysSrc = this.envKeysSources[envKeysFilepath]
87
+
88
+ // new keypair
89
+ const nkp = keypair() // generates a fresh keypair in memory
90
+ const newPublicKey = nkp.publicKey
91
+ const newPrivateKey = nkp.privateKey
92
+
93
+ // .env
94
+ envSrc = replace(envSrc, publicKeyName, newPublicKey) // replace publicKey
95
+ row.changed = true // track change
96
+ for (const [key, value] of Object.entries(envParsed)) { // re-encrypt each individual key
97
+ // key excluded - don't re-encrypt it
98
+ if (this.exclude(key)) {
99
+ continue
100
+ }
101
+
102
+ // key effectively excluded (by not being in the list of includes) - don't re-encrypt it
103
+ if (this.keys.length > 0 && !this.include(key)) {
104
+ continue
105
+ }
106
+
107
+ if (isEncrypted(value)) { // only re-encrypt those already encrypted
108
+ row.keys.push(key) // track key(s)
109
+
110
+ const decryptedValue = decryptKeyValue(key, value, privateKeyName, existingPrivateKey) // get decrypted value
111
+
112
+ const encryptedValue = encryptValue(decryptedValue, newPublicKey) // encrypt with the new publicKey
113
+
114
+ envSrc = replace(envSrc, key, encryptedValue)
115
+ }
116
+ }
117
+ row.envSrc = envSrc
118
+
119
+ // .env.keys - TODO: for dotenvx pro .env.keys file does not exist
120
+ row.privateKeyAdded = true
121
+ row.privateKeyName = privateKeyName
122
+ row.privateKey = newPrivateKey
123
+ envKeysSrc = append(envKeysSrc, privateKeyName, newPrivateKey) // append privateKey
124
+ this.envKeysSources[envKeysFilepath] = envKeysSrc
125
+ row.envKeysSrc = envKeysSrc
126
+
127
+ this.changedFilepaths.add(envFilepath)
128
+ } catch (e) {
129
+ if (e.code === 'ENOENT') {
130
+ row.error = new Errors({ envFilepath, filepath }).missingEnvFile()
131
+ } else {
132
+ row.error = e
133
+ }
134
+ }
135
+
136
+ this.processedEnvs.push(row)
137
+ }
138
+
139
+ _keys () {
140
+ if (!Array.isArray(this.key)) {
141
+ return [this.key]
142
+ }
143
+
144
+ return this.key
145
+ }
146
+
147
+ _excludeKeys () {
148
+ if (!Array.isArray(this.excludeKey)) {
149
+ return [this.excludeKey]
150
+ }
151
+
152
+ return this.excludeKey
153
+ }
154
+
155
+ _detectEncoding (filepath) {
156
+ return detectEncoding(filepath)
157
+ }
158
+ }
159
+
160
+ module.exports = Rotate