@dotenvx/dotenvx 1.35.0 → 1.37.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 +15 -1
- package/README.md +136 -0
- package/package.json +1 -1
- package/src/cli/actions/rotate.js +83 -0
- package/src/cli/actions/run.js +2 -2
- package/src/cli/dotenvx.js +14 -0
- package/src/lib/helpers/append.js +61 -0
- package/src/lib/main.js +5 -4
- package/src/lib/services/rotate.js +160 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,21 @@
|
|
|
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.37.0...main)
|
|
6
|
+
|
|
7
|
+
## [1.37.0](https://github.com/dotenvx/dotenvx/compare/v1.36.0...v1.37.0)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* add `dotenvx rotate` command 🎉 ([#530](https://github.com/dotenvx/dotenvx/pull/530))
|
|
12
|
+
|
|
13
|
+
also: [our whitepaper](https://dotenvx.com/dotenvx.pdf) is released as a draft.
|
|
14
|
+
|
|
15
|
+
## [1.36.0](https://github.com/dotenvx/dotenvx/compare/v1.35.0...v1.36.0)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
* `--strict` flag respects (doesn't throw) anything in `--ignore` flag ([#527](https://github.com/dotenvx/dotenvx/pull/527))
|
|
6
20
|
|
|
7
21
|
## [1.35.0](https://github.com/dotenvx/dotenvx/compare/v1.34.0...v1.35.0)
|
|
8
22
|
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* multi-environment
|
|
7
7
|
* encrypted envs
|
|
8
8
|
|
|
9
|
+
[Read the whitepaper](https://dotenvx.com/dotenvx.pdf?v=README)
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
|
|
@@ -524,6 +526,8 @@ More examples
|
|
|
524
526
|
Hello local
|
|
525
527
|
```
|
|
526
528
|
|
|
529
|
+
Note subsequent files do NOT override pre-existing variables defined in previous files or env. This follows historic principle. For example, above `local` wins – from the first file.
|
|
530
|
+
|
|
527
531
|
</details>
|
|
528
532
|
|
|
529
533
|
* <details><summary>`--overload` flag</summary><br>
|
|
@@ -538,6 +542,8 @@ More examples
|
|
|
538
542
|
Hello World
|
|
539
543
|
```
|
|
540
544
|
|
|
545
|
+
Note that with `--overload` subsequent files DO override pre-existing variables defined in previous files.
|
|
546
|
+
|
|
541
547
|
* <details><summary>`--verbose` flag</summary><br>
|
|
542
548
|
|
|
543
549
|
```sh
|
|
@@ -798,6 +804,8 @@ More examples
|
|
|
798
804
|
Hello local
|
|
799
805
|
```
|
|
800
806
|
|
|
807
|
+
Note subsequent files do NOT override pre-existing variables defined in previous files or env. This follows historic principle. For example, above `local` wins – from the first file.
|
|
808
|
+
|
|
801
809
|
</details>
|
|
802
810
|
* <details><summary>`run --env HELLO=String`</summary><br>
|
|
803
811
|
|
|
@@ -827,6 +835,8 @@ More examples
|
|
|
827
835
|
Hello World
|
|
828
836
|
```
|
|
829
837
|
|
|
838
|
+
Note that with `--overload` subsequent files DO override pre-existing variables defined in previous files.
|
|
839
|
+
|
|
830
840
|
</details>
|
|
831
841
|
* <details><summary>`DOTENV_PRIVATE_KEY=key run`</summary><br>
|
|
832
842
|
|
|
@@ -1675,7 +1685,117 @@ More examples
|
|
|
1675
1685
|
```
|
|
1676
1686
|
|
|
1677
1687
|
</details>
|
|
1688
|
+
* <details><summary>`rotate`</summary><br>
|
|
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.
|
|
1678
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>
|
|
1679
1799
|
* <details><summary>`help`</summary><br>
|
|
1680
1800
|
|
|
1681
1801
|
Output help for `dotenvx`.
|
|
@@ -2124,6 +2244,22 @@ More examples
|
|
|
2124
2244
|
|
|
2125
2245
|
## FAQ
|
|
2126
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
|
+
|
|
2127
2263
|
#### Why am I getting the error `node: .env: not found`?
|
|
2128
2264
|
|
|
2129
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
|
@@ -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
|
package/src/cli/actions/run.js
CHANGED
|
@@ -62,13 +62,13 @@ async function run () {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
for (const error of processedEnv.errors || []) {
|
|
65
|
-
if (options.strict) throw error // throw immediately if strict
|
|
66
|
-
|
|
67
65
|
if (ignore.includes(error.code)) {
|
|
68
66
|
logger.verbose(`ignored: ${error.message}`)
|
|
69
67
|
continue // ignore error
|
|
70
68
|
}
|
|
71
69
|
|
|
70
|
+
if (options.strict) throw error // throw if strict and not ignored
|
|
71
|
+
|
|
72
72
|
if (error.code === 'MISSING_ENV_FILE') {
|
|
73
73
|
if (!options.convention) { // do not output error for conventions (too noisy)
|
|
74
74
|
console.error(error.message)
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -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
|
package/src/lib/main.js
CHANGED
|
@@ -69,12 +69,13 @@ const config = function (options = {}) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
for (const error of processedEnv.errors || []) {
|
|
72
|
-
if (strict) throw error // throw immediately if strict
|
|
73
|
-
|
|
74
72
|
if (ignore.includes(error.code)) {
|
|
73
|
+
logger.verbose(`ignored: ${error.message}`)
|
|
75
74
|
continue // ignore error
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
if (strict) throw error // throw if strict and not ignored
|
|
78
|
+
|
|
78
79
|
lastError = error // surface later in { error }
|
|
79
80
|
|
|
80
81
|
if (error.code === 'MISSING_ENV_FILE') {
|
|
@@ -244,12 +245,12 @@ const get = function (key, options = {}) {
|
|
|
244
245
|
const { parsed, errors } = new Get(key, envs, options.overload, process.env.DOTENV_KEY, options.all, options.envKeysFile).run()
|
|
245
246
|
|
|
246
247
|
for (const error of errors || []) {
|
|
247
|
-
if (options.strict) throw error // throw immediately if strict
|
|
248
|
-
|
|
249
248
|
if (ignore.includes(error.code)) {
|
|
250
249
|
continue // ignore error
|
|
251
250
|
}
|
|
252
251
|
|
|
252
|
+
if (options.strict) throw error // throw immediately if strict
|
|
253
|
+
|
|
253
254
|
console.error(error.message)
|
|
254
255
|
if (error.help) {
|
|
255
256
|
console.error(error.help)
|
|
@@ -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
|