@dotenvx/dotenvx 1.55.0 → 1.56.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 +18 -1
- package/package.json +3 -3
- package/src/cli/actions/encrypt.js +0 -3
- package/src/cli/actions/get.js +4 -2
- package/src/cli/actions/keypair.js +3 -2
- package/src/cli/actions/rotate.js +10 -5
- package/src/cli/actions/run.js +1 -14
- package/src/cli/actions/set.js +3 -2
- package/src/cli/commands/ext.js +0 -3
- package/src/cli/dotenvx.js +12 -9
- package/src/db/session.js +21 -0
- package/src/lib/extensions/ops.js +98 -0
- package/src/lib/helpers/buildEnvs.js +2 -15
- package/src/lib/helpers/{decryptKeyValue.js → cryptography/decryptKeyValue.js} +1 -1
- package/src/lib/helpers/cryptography/index.js +14 -0
- package/src/lib/helpers/{isPublicKey.js → cryptography/isPublicKey.js} +1 -1
- package/src/lib/helpers/{keypair.js → cryptography/localKeypair.js} +2 -2
- package/src/lib/helpers/cryptography/mutateKeysSrc.js +38 -0
- package/src/lib/helpers/cryptography/mutateSrc.js +24 -0
- package/src/lib/helpers/cryptography/opsKeypair.js +14 -0
- package/src/lib/helpers/cryptography/provision.js +47 -0
- package/src/lib/helpers/cryptography/provisionWithPrivateKey.js +26 -0
- package/src/lib/helpers/envResolution/determine.js +46 -0
- package/src/lib/helpers/{guessEnvironment.js → envResolution/environment.js} +4 -6
- package/src/lib/helpers/envResolution/index.js +8 -0
- package/src/lib/helpers/errors.js +13 -0
- package/src/lib/helpers/findEnvFiles.js +1 -1
- package/src/lib/helpers/isFullyEncrypted.js +3 -3
- package/src/lib/helpers/keyResolution/index.js +13 -0
- package/src/lib/helpers/keyResolution/keyNames.js +24 -0
- package/src/lib/helpers/keyResolution/keyValues.js +85 -0
- package/src/lib/helpers/keyResolution/readFileKey.js +15 -0
- package/src/lib/helpers/keyResolution/readProcessKey.js +7 -0
- package/src/lib/helpers/parse.js +1 -1
- package/src/lib/helpers/prependPublicKey.js +17 -0
- package/src/lib/helpers/preserveShebang.js +16 -0
- package/src/lib/main.d.ts +19 -3
- package/src/lib/main.js +9 -17
- package/src/lib/services/decrypt.js +23 -19
- package/src/lib/services/encrypt.js +41 -136
- package/src/lib/services/get.js +3 -3
- package/src/lib/services/keypair.js +12 -16
- package/src/lib/services/prebuild.js +2 -2
- package/src/lib/services/precommit.js +2 -2
- package/src/lib/services/rotate.js +59 -42
- package/src/lib/services/run.js +29 -112
- package/src/lib/services/sets.js +40 -124
- package/src/lib/helpers/decrypt.js +0 -31
- package/src/lib/helpers/deprecationNotice.js +0 -17
- package/src/lib/helpers/determineEnvs.js +0 -65
- package/src/lib/helpers/encrypt.js +0 -29
- package/src/lib/helpers/findPrivateKey.js +0 -25
- package/src/lib/helpers/findPublicKey.js +0 -15
- package/src/lib/helpers/guessPrivateKeyName.js +0 -18
- package/src/lib/helpers/guessPublicKeyName.js +0 -18
- package/src/lib/helpers/parseEncryptionKeyFromDotenvKey.js +0 -19
- package/src/lib/helpers/parseEnvironmentFromDotenvKey.js +0 -19
- package/src/lib/helpers/smartDotenvPrivateKey.js +0 -89
- package/src/lib/helpers/smartDotenvPublicKey.js +0 -42
- package/src/lib/services/ops.js +0 -111
- /package/src/lib/helpers/{encryptValue.js → cryptography/encryptValue.js} +0 -0
- /package/src/lib/helpers/{isEncrypted.js → cryptography/isEncrypted.js} +0 -0
|
@@ -5,23 +5,31 @@ const picomatch = require('picomatch')
|
|
|
5
5
|
const TYPE_ENV_FILE = 'envFile'
|
|
6
6
|
|
|
7
7
|
const Errors = require('./../helpers/errors')
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
determine
|
|
11
|
+
} = require('./../helpers/envResolution')
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
keyNames,
|
|
15
|
+
keyValues
|
|
16
|
+
} = require('./../helpers/keyResolution')
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
encryptValue,
|
|
20
|
+
isEncrypted,
|
|
21
|
+
isPublicKey,
|
|
22
|
+
provision,
|
|
23
|
+
provisionWithPrivateKey
|
|
24
|
+
} = require('./../helpers/cryptography')
|
|
25
|
+
|
|
13
26
|
const replace = require('./../helpers/replace')
|
|
27
|
+
const dotenvParse = require('./../helpers/dotenvParse')
|
|
14
28
|
const detectEncoding = require('./../helpers/detectEncoding')
|
|
15
|
-
const determineEnvs = require('./../helpers/determineEnvs')
|
|
16
|
-
const { findPrivateKey } = require('./../helpers/findPrivateKey')
|
|
17
|
-
const findPublicKey = require('./../helpers/findPublicKey')
|
|
18
|
-
const keypair = require('./../helpers/keypair')
|
|
19
|
-
const truncate = require('./../helpers/truncate')
|
|
20
|
-
const isPublicKey = require('./../helpers/isPublicKey')
|
|
21
29
|
|
|
22
30
|
class Encrypt {
|
|
23
|
-
constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null, opsOn =
|
|
24
|
-
this.envs =
|
|
31
|
+
constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null, opsOn = false) {
|
|
32
|
+
this.envs = determine(envs, process.env)
|
|
25
33
|
this.key = key
|
|
26
34
|
this.excludeKey = excludeKey
|
|
27
35
|
this.envKeysFilepath = envKeysFilepath
|
|
@@ -62,105 +70,36 @@ class Encrypt {
|
|
|
62
70
|
row.keys = []
|
|
63
71
|
row.type = TYPE_ENV_FILE
|
|
64
72
|
|
|
65
|
-
const filename = path.basename(envFilepath)
|
|
66
73
|
const filepath = path.resolve(envFilepath)
|
|
67
74
|
row.filepath = filepath
|
|
68
75
|
row.envFilepath = envFilepath
|
|
69
76
|
|
|
70
77
|
try {
|
|
71
|
-
const encoding =
|
|
78
|
+
const encoding = detectEncoding(filepath)
|
|
72
79
|
let envSrc = fsx.readFileX(filepath, { encoding })
|
|
73
80
|
const envParsed = dotenvParse(envSrc)
|
|
74
81
|
|
|
75
82
|
let publicKey
|
|
76
83
|
let privateKey
|
|
77
84
|
|
|
78
|
-
const publicKeyName =
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (existingPublicKey && existingPublicKey !== publicKey) {
|
|
97
|
-
const error = new Error(`derived public key (${truncate(publicKey)}) does not match the existing public key (${truncate(existingPublicKey)})`)
|
|
98
|
-
error.code = 'INVALID_DOTENV_PRIVATE_KEY'
|
|
99
|
-
error.help = `debug info: ${privateKeyName}=${truncate(existingPrivateKey)} (derived ${publicKeyName}=${truncate(publicKey)} vs existing ${publicKeyName}=${truncate(existingPublicKey)})`
|
|
100
|
-
throw error
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// typical scenario when encrypting a monorepo second .env file from a prior generated -fk .env.keys file
|
|
104
|
-
if (!existingPublicKey) {
|
|
105
|
-
const ps = this._preserveShebang(envSrc)
|
|
106
|
-
const firstLinePreserved = ps.firstLinePreserved
|
|
107
|
-
envSrc = ps.envSrc
|
|
108
|
-
|
|
109
|
-
const prependPublicKey = this._prependPublicKey(publicKeyName, publicKey, filename, relativeFilepath)
|
|
110
|
-
|
|
111
|
-
envSrc = `${firstLinePreserved}${prependPublicKey}\n${envSrc}`
|
|
112
|
-
}
|
|
113
|
-
} else if (existingPublicKey) {
|
|
114
|
-
// throw new Error('implement for remote Ops existingPrivateKey')
|
|
115
|
-
publicKey = existingPublicKey
|
|
116
|
-
} else {
|
|
117
|
-
// .env.keys
|
|
118
|
-
let keysSrc = ''
|
|
119
|
-
|
|
120
|
-
if (fsx.existsSync(envKeysFilepath)) {
|
|
121
|
-
keysSrc = fsx.readFileX(envKeysFilepath)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const ps = this._preserveShebang(envSrc)
|
|
125
|
-
const firstLinePreserved = ps.firstLinePreserved
|
|
126
|
-
envSrc = ps.envSrc
|
|
127
|
-
|
|
128
|
-
// TODO: instead get this from API
|
|
129
|
-
const kp = keypair() // generates a fresh keypair in memory
|
|
130
|
-
publicKey = kp.publicKey
|
|
131
|
-
privateKey = kp.privateKey
|
|
132
|
-
// Ops hook point (first-time key generation):
|
|
133
|
-
// if Ops is installed and opsOff is not set, send privateKey/privateKeyName/envFilepath
|
|
134
|
-
// to your Ops service before persisting or immediately after writing below.
|
|
135
|
-
|
|
136
|
-
const prependPublicKey = this._prependPublicKey(publicKeyName, publicKey, filename, relativeFilepath)
|
|
137
|
-
|
|
138
|
-
// privateKey
|
|
139
|
-
const firstTimeKeysSrc = [
|
|
140
|
-
'#/------------------!DOTENV_PRIVATE_KEYS!-------------------/',
|
|
141
|
-
'#/ private decryption keys. DO NOT commit to source control /',
|
|
142
|
-
'#/ [how it works](https://dotenvx.com/encryption) /',
|
|
143
|
-
// '#/ backup with: `dotenvx ops backup` /',
|
|
144
|
-
'#/----------------------------------------------------------/'
|
|
145
|
-
].join('\n')
|
|
146
|
-
const appendPrivateKey = [
|
|
147
|
-
`# ${filename}`,
|
|
148
|
-
`${privateKeyName}=${privateKey}`,
|
|
149
|
-
''
|
|
150
|
-
].join('\n')
|
|
151
|
-
|
|
152
|
-
envSrc = `${firstLinePreserved}${prependPublicKey}\n${envSrc}`
|
|
153
|
-
keysSrc = keysSrc.length > 1 ? keysSrc : `${firstTimeKeysSrc}\n`
|
|
154
|
-
keysSrc = `${keysSrc}\n${appendPrivateKey}`
|
|
155
|
-
|
|
156
|
-
// write to .env.keys
|
|
157
|
-
fsx.writeFileX(envKeysFilepath, keysSrc)
|
|
158
|
-
// Ops hook point (after persistence):
|
|
159
|
-
// if Ops is installed and opsOff is not set, trigger backup/registration now that
|
|
160
|
-
// .env.keys has been written and row.privateKeyAdded will be true for callers.
|
|
161
|
-
|
|
162
|
-
row.privateKeyAdded = true
|
|
163
|
-
row.envKeysFilepath = this.envKeysFilepath || path.join(path.dirname(envFilepath), path.basename(envKeysFilepath))
|
|
85
|
+
const { publicKeyName, privateKeyName } = keyNames(envFilepath)
|
|
86
|
+
const { publicKeyValue, privateKeyValue } = keyValues(envFilepath, { keysFilepath: this.envKeysFilepath, opsOn: this.opsOn })
|
|
87
|
+
|
|
88
|
+
// first pass - provision
|
|
89
|
+
if (!privateKeyValue && !publicKeyValue) {
|
|
90
|
+
const prov = provision({ envSrc, envFilepath, keysFilepath: this.envKeysFilepath, opsOn: this.opsOn })
|
|
91
|
+
envSrc = prov.envSrc
|
|
92
|
+
publicKey = prov.publicKey
|
|
93
|
+
privateKey = prov.privateKey
|
|
94
|
+
row.privateKeyAdded = prov.privateKeyAdded
|
|
95
|
+
row.envKeysFilepath = prov.envKeysFilepath
|
|
96
|
+
} else if (privateKeyValue) {
|
|
97
|
+
const prov = provisionWithPrivateKey({ envSrc, envFilepath, keysFilepath: this.envKeysFilepath, privateKeyValue, publicKeyValue, publicKeyName })
|
|
98
|
+
publicKey = prov.publicKey
|
|
99
|
+
privateKey = prov.privateKey
|
|
100
|
+
envSrc = prov.envSrc
|
|
101
|
+
} else if (publicKeyValue) {
|
|
102
|
+
publicKey = publicKeyValue
|
|
164
103
|
}
|
|
165
104
|
|
|
166
105
|
row.publicKey = publicKey
|
|
@@ -179,7 +118,7 @@ class Encrypt {
|
|
|
179
118
|
continue
|
|
180
119
|
}
|
|
181
120
|
|
|
182
|
-
const encrypted = isEncrypted(value) || isPublicKey(key
|
|
121
|
+
const encrypted = isEncrypted(value) || isPublicKey(key)
|
|
183
122
|
if (!encrypted) {
|
|
184
123
|
row.keys.push(key) // track key(s)
|
|
185
124
|
|
|
@@ -224,40 +163,6 @@ class Encrypt {
|
|
|
224
163
|
|
|
225
164
|
return this.excludeKey
|
|
226
165
|
}
|
|
227
|
-
|
|
228
|
-
_detectEncoding (filepath) {
|
|
229
|
-
return detectEncoding(filepath)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
_prependPublicKey (publicKeyName, publicKey, filename, relativeFilepath = '') {
|
|
233
|
-
const comment = relativeFilepath === '.env.keys' ? '' : ` # ${relativeFilepath}`
|
|
234
|
-
|
|
235
|
-
return [
|
|
236
|
-
'#/-------------------[DOTENV_PUBLIC_KEY]--------------------/',
|
|
237
|
-
'#/ public-key encryption for .env files /',
|
|
238
|
-
'#/ [how it works](https://dotenvx.com/encryption) /',
|
|
239
|
-
'#/----------------------------------------------------------/',
|
|
240
|
-
`${publicKeyName}="${publicKey}"${comment}`,
|
|
241
|
-
'',
|
|
242
|
-
`# ${filename}`
|
|
243
|
-
].join('\n')
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
_preserveShebang (envSrc) {
|
|
247
|
-
// preserve shebang
|
|
248
|
-
const [firstLine, ...remainingLines] = envSrc.split('\n')
|
|
249
|
-
let firstLinePreserved = ''
|
|
250
|
-
|
|
251
|
-
if (firstLine.startsWith('#!')) {
|
|
252
|
-
firstLinePreserved = firstLine + '\n'
|
|
253
|
-
envSrc = remainingLines.join('\n')
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
firstLinePreserved,
|
|
258
|
-
envSrc
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
166
|
}
|
|
262
167
|
|
|
263
168
|
module.exports = Encrypt
|
package/src/lib/services/get.js
CHANGED
|
@@ -2,18 +2,18 @@ const Run = require('./run')
|
|
|
2
2
|
const Errors = require('./../helpers/errors')
|
|
3
3
|
|
|
4
4
|
class Get {
|
|
5
|
-
constructor (key, envs = [], overload = false,
|
|
5
|
+
constructor (key, envs = [], overload = false, all = false, envKeysFilepath = null, opsOn = true) {
|
|
6
6
|
this.key = key
|
|
7
7
|
this.envs = envs
|
|
8
8
|
this.overload = overload
|
|
9
|
-
this.DOTENV_KEY = DOTENV_KEY
|
|
10
9
|
this.all = all
|
|
11
10
|
this.envKeysFilepath = envKeysFilepath
|
|
11
|
+
this.opsOn = opsOn
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
run () {
|
|
15
15
|
const processEnv = { ...process.env }
|
|
16
|
-
const { processedEnvs } = new Run(this.envs, this.overload, this.
|
|
16
|
+
const { processedEnvs } = new Run(this.envs, this.overload, processEnv, this.envKeysFilepath, this.opsOn).run()
|
|
17
17
|
|
|
18
18
|
const errors = []
|
|
19
19
|
for (const processedEnv of processedEnvs) {
|
|
@@ -1,35 +1,31 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const {
|
|
2
|
+
keyNames,
|
|
3
|
+
keyValues
|
|
4
|
+
} = require('./../helpers/keyResolution')
|
|
5
5
|
|
|
6
6
|
class Keypair {
|
|
7
|
-
constructor (envFile = '.env', envKeysFilepath = null) {
|
|
7
|
+
constructor (envFile = '.env', envKeysFilepath = null, opsOn = false) {
|
|
8
8
|
this.envFile = envFile
|
|
9
9
|
this.envKeysFilepath = envKeysFilepath
|
|
10
|
+
this.opsOn = opsOn
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
run () {
|
|
13
14
|
const out = {}
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
for (const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const publicKeyValue = smartDotenvPublicKey(envFilepath)
|
|
20
|
-
out[publicKeyName] = publicKeyValue
|
|
21
|
-
|
|
22
|
-
// private key
|
|
23
|
-
const privateKeyName = guessPrivateKeyName(envFilepath)
|
|
24
|
-
const privateKeyValue = smartDotenvPrivateKey(envFilepath, this.envKeysFilepath)
|
|
16
|
+
const filepaths = this._filepaths()
|
|
17
|
+
for (const filepath of filepaths) {
|
|
18
|
+
const { publicKeyName, privateKeyName } = keyNames(filepath)
|
|
19
|
+
const { publicKeyValue, privateKeyValue } = keyValues(filepath, { keysFilepath: this.envKeysFilepath, opsOn: this.opsOn })
|
|
25
20
|
|
|
21
|
+
out[publicKeyName] = publicKeyValue
|
|
26
22
|
out[privateKeyName] = privateKeyValue
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
return out
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
|
|
28
|
+
_filepaths () {
|
|
33
29
|
if (!Array.isArray(this.envFile)) {
|
|
34
30
|
return [this.envFile]
|
|
35
31
|
}
|
|
@@ -41,13 +41,13 @@ class Prebuild {
|
|
|
41
41
|
|
|
42
42
|
// check if that file is being ignored
|
|
43
43
|
if (ig.ignores(file)) {
|
|
44
|
-
if (file === '.env.example' || file === '.env.
|
|
44
|
+
if (file === '.env.example' || file === '.env.x') {
|
|
45
45
|
const warning = new Error(`[dotenvx@${packageJson.version}][prebuild] ${file} (currently ignored but should not be)`)
|
|
46
46
|
warning.help = `[dotenvx@${packageJson.version}][prebuild] ⮕ run [dotenvx ext gitignore --pattern !${file}]`
|
|
47
47
|
warnings.push(warning)
|
|
48
48
|
}
|
|
49
49
|
} else {
|
|
50
|
-
if (file !== '.env.example' && file !== '.env.
|
|
50
|
+
if (file !== '.env.example' && file !== '.env.x') {
|
|
51
51
|
const src = fsx.readFileX(file)
|
|
52
52
|
const encrypted = isFullyEncrypted(src)
|
|
53
53
|
|
|
@@ -57,13 +57,13 @@ class Precommit {
|
|
|
57
57
|
if (this._isFileToBeCommitted(file)) {
|
|
58
58
|
// check if that file is being ignored
|
|
59
59
|
if (ig.ignores(file)) {
|
|
60
|
-
if (file === '.env.example' || file === '.env.
|
|
60
|
+
if (file === '.env.example' || file === '.env.x') {
|
|
61
61
|
const warning = new Error(`[dotenvx@${packageJson.version}][precommit] ${file} (currently ignored but should not be)`)
|
|
62
62
|
warning.help = `[dotenvx@${packageJson.version}][precommit] ⮕ run [dotenvx ext gitignore --pattern !${file}]`
|
|
63
63
|
warnings.push(warning)
|
|
64
64
|
}
|
|
65
65
|
} else {
|
|
66
|
-
if (file !== '.env.example' && file !== '.env.
|
|
66
|
+
if (file !== '.env.example' && file !== '.env.x') {
|
|
67
67
|
const src = fsx.readFileX(file)
|
|
68
68
|
const encrypted = isFullyEncrypted(src)
|
|
69
69
|
|
|
@@ -5,26 +5,36 @@ const picomatch = require('picomatch')
|
|
|
5
5
|
const TYPE_ENV_FILE = 'envFile'
|
|
6
6
|
|
|
7
7
|
const Errors = require('./../helpers/errors')
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
determine
|
|
11
|
+
} = require('./../helpers/envResolution')
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
keyNames,
|
|
15
|
+
keyValues
|
|
16
|
+
} = require('./../helpers/keyResolution')
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
opsKeypair,
|
|
20
|
+
localKeypair,
|
|
21
|
+
encryptValue,
|
|
22
|
+
decryptKeyValue,
|
|
23
|
+
isEncrypted
|
|
24
|
+
} = require('./../helpers/cryptography')
|
|
25
|
+
|
|
14
26
|
const append = require('./../helpers/append')
|
|
27
|
+
const replace = require('./../helpers/replace')
|
|
28
|
+
const dotenvParse = require('./../helpers/dotenvParse')
|
|
15
29
|
const detectEncoding = require('./../helpers/detectEncoding')
|
|
16
|
-
const determineEnvs = require('./../helpers/determineEnvs')
|
|
17
|
-
const { findPrivateKey } = require('./../helpers/findPrivateKey')
|
|
18
|
-
const findPublicKey = require('./../helpers/findPublicKey')
|
|
19
|
-
const decryptKeyValue = require('./../helpers/decryptKeyValue')
|
|
20
|
-
const keypair = require('./../helpers/keypair')
|
|
21
30
|
|
|
22
31
|
class Rotate {
|
|
23
|
-
constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null) {
|
|
24
|
-
this.envs =
|
|
32
|
+
constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null, opsOn = false) {
|
|
33
|
+
this.envs = determine(envs, process.env)
|
|
25
34
|
this.key = key
|
|
26
35
|
this.excludeKey = excludeKey
|
|
27
36
|
this.envKeysFilepath = envKeysFilepath
|
|
37
|
+
this.opsOn = opsOn
|
|
28
38
|
|
|
29
39
|
this.processedEnvs = []
|
|
30
40
|
this.changedFilepaths = new Set()
|
|
@@ -68,33 +78,44 @@ class Rotate {
|
|
|
68
78
|
row.envFilepath = envFilepath
|
|
69
79
|
|
|
70
80
|
try {
|
|
71
|
-
const encoding =
|
|
81
|
+
const encoding = detectEncoding(filepath)
|
|
72
82
|
let envSrc = fsx.readFileX(filepath, { encoding })
|
|
73
83
|
const envParsed = dotenvParse(envSrc)
|
|
74
84
|
|
|
75
|
-
const publicKeyName =
|
|
76
|
-
const
|
|
77
|
-
const existingPublicKey = findPublicKey(envFilepath)
|
|
78
|
-
const existingPrivateKey = findPrivateKey(envFilepath, this.envKeysFilepath, false, existingPublicKey)
|
|
85
|
+
const { publicKeyName, privateKeyName } = keyNames(envFilepath)
|
|
86
|
+
const { privateKeyValue } = keyValues(envFilepath, { keysFilepath: this.envKeysFilepath, opsOn: this.opsOn })
|
|
79
87
|
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const keysEncoding = this._detectEncoding(envKeysFilepath)
|
|
88
|
+
let newPublicKey
|
|
89
|
+
let newPrivateKey
|
|
90
|
+
let envKeysFilepath
|
|
91
|
+
let envKeysSrc
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
if (this.opsOn) {
|
|
94
|
+
const kp = opsKeypair()
|
|
95
|
+
newPublicKey = kp.publicKey
|
|
96
|
+
newPrivateKey = kp.privateKey
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
row.privateKeyAdded = false // TODO: change to localPrivateKeyAdded
|
|
99
|
+
} else {
|
|
100
|
+
envKeysFilepath = path.join(path.dirname(filepath), '.env.keys')
|
|
101
|
+
if (this.envKeysFilepath) {
|
|
102
|
+
envKeysFilepath = path.resolve(this.envKeysFilepath)
|
|
103
|
+
}
|
|
104
|
+
row.envKeysFilepath = envKeysFilepath
|
|
105
|
+
this.envKeysSources[envKeysFilepath] ||= fsx.readFileX(envKeysFilepath, { encoding: detectEncoding(envKeysFilepath) })
|
|
106
|
+
envKeysSrc = this.envKeysSources[envKeysFilepath]
|
|
107
|
+
|
|
108
|
+
const kp = localKeypair()
|
|
109
|
+
newPublicKey = kp.publicKey
|
|
110
|
+
newPrivateKey = kp.privateKey
|
|
111
|
+
|
|
112
|
+
row.privateKeyAdded = true
|
|
113
|
+
}
|
|
94
114
|
|
|
95
115
|
// .env
|
|
96
116
|
envSrc = replace(envSrc, publicKeyName, newPublicKey) // replace publicKey
|
|
97
117
|
row.changed = true // track change
|
|
118
|
+
|
|
98
119
|
for (const [key, value] of Object.entries(envParsed)) { // re-encrypt each individual key
|
|
99
120
|
// key excluded - don't re-encrypt it
|
|
100
121
|
if (this.exclude(key)) {
|
|
@@ -109,22 +130,22 @@ class Rotate {
|
|
|
109
130
|
if (isEncrypted(value)) { // only re-encrypt those already encrypted
|
|
110
131
|
row.keys.push(key) // track key(s)
|
|
111
132
|
|
|
112
|
-
const decryptedValue = decryptKeyValue(key, value, privateKeyName,
|
|
113
|
-
|
|
133
|
+
const decryptedValue = decryptKeyValue(key, value, privateKeyName, privateKeyValue) // get decrypted value
|
|
114
134
|
const encryptedValue = encryptValue(decryptedValue, newPublicKey) // encrypt with the new publicKey
|
|
115
135
|
|
|
116
136
|
envSrc = replace(envSrc, key, encryptedValue)
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
row.envSrc = envSrc
|
|
120
|
-
|
|
121
|
-
// .env.keys - TODO: for dotenvx pro .env.keys file does not exist
|
|
122
|
-
row.privateKeyAdded = true
|
|
123
140
|
row.privateKeyName = privateKeyName
|
|
124
141
|
row.privateKey = newPrivateKey
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
142
|
+
|
|
143
|
+
if (!this.opsOn) {
|
|
144
|
+
// keys src only for ops
|
|
145
|
+
envKeysSrc = append(envKeysSrc, privateKeyName, newPrivateKey) // append privateKey
|
|
146
|
+
this.envKeysSources[envKeysFilepath] = envKeysSrc
|
|
147
|
+
row.envKeysSrc = envKeysSrc
|
|
148
|
+
}
|
|
128
149
|
|
|
129
150
|
this.changedFilepaths.add(envFilepath)
|
|
130
151
|
} catch (e) {
|
|
@@ -153,10 +174,6 @@ class Rotate {
|
|
|
153
174
|
|
|
154
175
|
return this.excludeKey
|
|
155
176
|
}
|
|
156
|
-
|
|
157
|
-
_detectEncoding (filepath) {
|
|
158
|
-
return detectEncoding(filepath)
|
|
159
|
-
}
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
module.exports = Rotate
|
package/src/lib/services/run.js
CHANGED
|
@@ -3,24 +3,24 @@ const path = require('path')
|
|
|
3
3
|
|
|
4
4
|
const TYPE_ENV = 'env'
|
|
5
5
|
const TYPE_ENV_FILE = 'envFile'
|
|
6
|
-
const TYPE_ENV_VAULT_FILE = 'envVaultFile'
|
|
7
6
|
|
|
8
|
-
const decrypt = require('./../helpers/decrypt')
|
|
9
7
|
const Parse = require('./../helpers/parse')
|
|
10
8
|
const Errors = require('./../helpers/errors')
|
|
11
|
-
const dotenvParse = require('./../helpers/dotenvParse')
|
|
12
|
-
const parseEnvironmentFromDotenvKey = require('./../helpers/parseEnvironmentFromDotenvKey')
|
|
13
9
|
const detectEncoding = require('./../helpers/detectEncoding')
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
keyNames,
|
|
13
|
+
keyValues
|
|
14
|
+
} = require('./../helpers/keyResolution')
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
determine
|
|
18
|
+
} = require('./../helpers/envResolution')
|
|
18
19
|
|
|
19
20
|
class Run {
|
|
20
|
-
constructor (envs = [], overload = false,
|
|
21
|
-
this.envs =
|
|
21
|
+
constructor (envs = [], overload = false, processEnv = process.env, envKeysFilepath = null, opsOn = false) {
|
|
22
|
+
this.envs = determine(envs, processEnv)
|
|
22
23
|
this.overload = overload
|
|
23
|
-
this.DOTENV_KEY = DOTENV_KEY
|
|
24
24
|
this.processEnv = processEnv
|
|
25
25
|
this.envKeysFilepath = envKeysFilepath
|
|
26
26
|
this.opsOn = opsOn
|
|
@@ -35,16 +35,13 @@ class Run {
|
|
|
35
35
|
run () {
|
|
36
36
|
// example
|
|
37
37
|
// envs [
|
|
38
|
-
// { type: 'envVaultFile', value: '.env.vault' },
|
|
39
38
|
// { type: 'env', value: 'HELLO=one' },
|
|
40
39
|
// { type: 'envFile', value: '.env' },
|
|
41
40
|
// { type: 'env', value: 'HELLO=three' }
|
|
42
41
|
// ]
|
|
43
42
|
|
|
44
43
|
for (const env of this.envs) {
|
|
45
|
-
if (env.type ===
|
|
46
|
-
this._injectEnvVaultFile(env.value)
|
|
47
|
-
} else if (env.type === TYPE_ENV_FILE) {
|
|
44
|
+
if (env.type === TYPE_ENV_FILE) {
|
|
48
45
|
this._injectEnvFile(env.value)
|
|
49
46
|
} else if (env.type === TYPE_ENV) {
|
|
50
47
|
this._injectEnv(env.value)
|
|
@@ -67,7 +64,13 @@ class Run {
|
|
|
67
64
|
row.string = env
|
|
68
65
|
|
|
69
66
|
try {
|
|
70
|
-
const {
|
|
67
|
+
const {
|
|
68
|
+
parsed,
|
|
69
|
+
errors,
|
|
70
|
+
injected,
|
|
71
|
+
preExisted
|
|
72
|
+
} = new Parse(env, null, this.processEnv, this.overload).run()
|
|
73
|
+
|
|
71
74
|
row.parsed = parsed
|
|
72
75
|
row.errors = errors
|
|
73
76
|
row.injected = injected
|
|
@@ -98,13 +101,18 @@ class Run {
|
|
|
98
101
|
const src = fsx.readFileX(filepath, { encoding })
|
|
99
102
|
this.readableFilepaths.add(envFilepath)
|
|
100
103
|
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const {
|
|
104
|
+
const { privateKeyName } = keyNames(filepath)
|
|
105
|
+
const { privateKeyValue } = keyValues(filepath, { keysFilepath: this.envKeysFilepath, opsOn: this.opsOn })
|
|
106
|
+
|
|
107
|
+
const {
|
|
108
|
+
parsed,
|
|
109
|
+
errors,
|
|
110
|
+
injected,
|
|
111
|
+
preExisted
|
|
112
|
+
} = new Parse(src, privateKeyValue, this.processEnv, this.overload, privateKeyName).run()
|
|
105
113
|
|
|
106
114
|
row.privateKeyName = privateKeyName
|
|
107
|
-
row.privateKey =
|
|
115
|
+
row.privateKey = privateKeyValue
|
|
108
116
|
row.src = src
|
|
109
117
|
row.parsed = parsed
|
|
110
118
|
row.errors = errors
|
|
@@ -127,102 +135,11 @@ class Run {
|
|
|
127
135
|
this.processedEnvs.push(row)
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
_injectEnvVaultFile (envVaultFilepath) {
|
|
131
|
-
const row = {}
|
|
132
|
-
row.type = TYPE_ENV_VAULT_FILE
|
|
133
|
-
row.filepath = envVaultFilepath
|
|
134
|
-
|
|
135
|
-
const filepath = path.resolve(envVaultFilepath)
|
|
136
|
-
this.readableFilepaths.add(envVaultFilepath)
|
|
137
|
-
|
|
138
|
-
if (!fsx.existsSync(filepath)) {
|
|
139
|
-
const code = 'MISSING_ENV_VAULT_FILE'
|
|
140
|
-
const message = `you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`
|
|
141
|
-
const error = new Error(message)
|
|
142
|
-
error.code = code
|
|
143
|
-
throw error
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (this.DOTENV_KEY.length < 1) {
|
|
147
|
-
const code = 'MISSING_DOTENV_KEY'
|
|
148
|
-
const message = `your DOTENV_KEY appears to be blank: '${this.DOTENV_KEY}'`
|
|
149
|
-
const error = new Error(message)
|
|
150
|
-
error.code = code
|
|
151
|
-
throw error
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let decrypted
|
|
155
|
-
const dotenvKeys = this._dotenvKeys()
|
|
156
|
-
const parsedVault = this._parsedVault(filepath)
|
|
157
|
-
for (let i = 0; i < dotenvKeys.length; i++) {
|
|
158
|
-
try {
|
|
159
|
-
const dotenvKey = dotenvKeys[i].trim() // dotenv://key_1234@...?environment=prod
|
|
160
|
-
|
|
161
|
-
decrypted = this._decrypted(dotenvKey, parsedVault)
|
|
162
|
-
|
|
163
|
-
break
|
|
164
|
-
} catch (error) {
|
|
165
|
-
// last key
|
|
166
|
-
if (i + 1 >= dotenvKeys.length) {
|
|
167
|
-
throw error
|
|
168
|
-
}
|
|
169
|
-
// try next key
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
// parse this. it's the equivalent of the .env file
|
|
175
|
-
const { parsed, errors, injected, preExisted } = new Parse(decrypted, null, this.processEnv, this.overload).run()
|
|
176
|
-
row.parsed = parsed
|
|
177
|
-
row.errors = errors
|
|
178
|
-
row.injected = injected
|
|
179
|
-
row.preExisted = preExisted
|
|
180
|
-
|
|
181
|
-
this.inject(row.parsed) // inject
|
|
182
|
-
|
|
183
|
-
for (const key of Object.keys(injected)) {
|
|
184
|
-
this.uniqueInjectedKeys.add(key) // track uniqueInjectedKeys across multiple files
|
|
185
|
-
}
|
|
186
|
-
} catch (e) {
|
|
187
|
-
row.errors = [e]
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
this.processedEnvs.push(row)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
138
|
inject (parsed) {
|
|
194
139
|
for (const key of Object.keys(parsed)) {
|
|
195
140
|
this.processEnv[key] = parsed[key] // inject to process.env
|
|
196
141
|
}
|
|
197
142
|
}
|
|
198
|
-
|
|
199
|
-
// handle scenario for comma separated keys - for use with key rotation
|
|
200
|
-
// example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
|
|
201
|
-
_dotenvKeys () {
|
|
202
|
-
return this.DOTENV_KEY.split(',')
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// { "DOTENV_VAULT_DEVELOPMENT": "<ciphertext>" }
|
|
206
|
-
_parsedVault (filepath) {
|
|
207
|
-
const src = fsx.readFileX(filepath)
|
|
208
|
-
return dotenvParse(src)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_decrypted (dotenvKey, parsedVault) {
|
|
212
|
-
const environment = parseEnvironmentFromDotenvKey(dotenvKey)
|
|
213
|
-
|
|
214
|
-
// DOTENV_KEY_PRODUCTION
|
|
215
|
-
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
216
|
-
const ciphertext = parsedVault[environmentKey]
|
|
217
|
-
if (!ciphertext) {
|
|
218
|
-
const error = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
|
|
219
|
-
error.code = 'NOT_FOUND_DOTENV_ENVIRONMENT'
|
|
220
|
-
|
|
221
|
-
throw error
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return decrypt(ciphertext, dotenvKey)
|
|
225
|
-
}
|
|
226
143
|
}
|
|
227
144
|
|
|
228
145
|
module.exports = Run
|