@dotenvx/dotenvx 1.64.0 → 1.65.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.
- package/CHANGELOG.md +233 -221
- package/package.json +1 -1
- package/src/lib/extensions/ops.js +32 -2
- package/src/lib/helpers/replace.js +53 -40
- package/src/lib/services/decrypt.js +12 -5
- package/src/lib/services/encrypt.js +15 -10
- package/src/lib/services/rotate.js +4 -3
- package/src/lib/services/sets.js +8 -2
package/package.json
CHANGED
|
@@ -42,7 +42,7 @@ class Ops {
|
|
|
42
42
|
if (publicKey) args.push(publicKey)
|
|
43
43
|
|
|
44
44
|
try {
|
|
45
|
-
return JSON.parse(await this.
|
|
45
|
+
return JSON.parse(await this._execInteractive(binary, args))
|
|
46
46
|
} catch (_e) {
|
|
47
47
|
return {}
|
|
48
48
|
}
|
|
@@ -58,7 +58,7 @@ class Ops {
|
|
|
58
58
|
if (publicKey) args.push(publicKey)
|
|
59
59
|
|
|
60
60
|
try {
|
|
61
|
-
return JSON.parse(this.
|
|
61
|
+
return JSON.parse(this._execInteractiveSync(binary, args))
|
|
62
62
|
} catch (_e) {
|
|
63
63
|
return {}
|
|
64
64
|
}
|
|
@@ -104,6 +104,36 @@ class Ops {
|
|
|
104
104
|
return childProcess.execFileSync(binary, args).toString().trim()
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
_execInteractive (binary, args) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const subprocess = childProcess.spawn(binary, args, {
|
|
110
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
111
|
+
})
|
|
112
|
+
let stdout = ''
|
|
113
|
+
|
|
114
|
+
subprocess.stdout.on('data', (data) => {
|
|
115
|
+
stdout += data.toString()
|
|
116
|
+
})
|
|
117
|
+
subprocess.on('error', reject)
|
|
118
|
+
subprocess.on('close', (code) => {
|
|
119
|
+
if (code !== 0) {
|
|
120
|
+
reject(new Error(`${binary} ${args.join(' ')} exited with code ${code}`))
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
resolve(stdout.trim())
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_execInteractiveSync (binary, args) {
|
|
130
|
+
logger.debug(binary)
|
|
131
|
+
logger.debug(args)
|
|
132
|
+
return childProcess.execFileSync(binary, args, {
|
|
133
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
134
|
+
}).toString().trim()
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
async _resolveBinary () {
|
|
108
138
|
if (this._binaryPromise) return this._binaryPromise
|
|
109
139
|
|
|
@@ -1,55 +1,68 @@
|
|
|
1
|
-
const quotes = require('./quotes')
|
|
2
1
|
const dotenvParse = require('./dotenvParse')
|
|
3
2
|
const escapeForRegex = require('./escapeForRegex')
|
|
4
|
-
const escapeDollarSigns = require('./escapeDollarSigns')
|
|
5
3
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
function replaceExistingValue (src, key, originalValue, replaceValue) {
|
|
5
|
+
const escapedKey = escapeForRegex(key)
|
|
6
|
+
const escapedOriginalValue = escapeForRegex(originalValue)
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
// conditionally enforce end of line
|
|
9
|
+
let enforceEndOfLine = ''
|
|
10
|
+
if (escapedOriginalValue === '') {
|
|
11
|
+
enforceEndOfLine = '$' // EMPTY scenario
|
|
12
|
+
}
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const currentPart = new RegExp(
|
|
15
|
+
'^' + // start of line
|
|
16
|
+
'(\\s*)?' + // spaces
|
|
17
|
+
'(export\\s+)?' + // export
|
|
18
|
+
escapedKey + // KEY
|
|
19
|
+
'\\s*=\\s*' + // spaces (KEY = value)
|
|
20
|
+
'(["\'`]?)' + // open quote
|
|
21
|
+
escapedOriginalValue + // escaped value
|
|
22
|
+
'\\3' + // close quote
|
|
23
|
+
enforceEndOfLine
|
|
24
|
+
,
|
|
25
|
+
'gm' // (g)lobal (m)ultiline
|
|
26
|
+
)
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
if (escapedOriginalValue === '') {
|
|
22
|
-
enforceEndOfLine = '$' // EMPTY scenario
|
|
28
|
+
return src.replace(currentPart, function (match, spaces = '', exportPart = '', quote = '') {
|
|
29
|
+
let newPart = `${key}=${quote}${replaceValue}${quote}`
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
+
// if empty quote and consecutive newlines
|
|
32
|
+
const newlineMatch = src.match(new RegExp(`${escapedKey}\\s*=\\s*\n\n`, 'm')) // match any consecutive newline scenario for a blank value
|
|
33
|
+
if (escapedOriginalValue === '' && quote === '' && newlineMatch) {
|
|
34
|
+
const newlineCount = (newlineMatch[0].match(/\n/g)).length - 1
|
|
35
|
+
for (let i = 0; i < newlineCount; i++) {
|
|
36
|
+
newPart += '\n' // re-append the extra newline to preserve user's format choice
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'(export\\s+)?' + // export
|
|
38
|
-
key + // KEY
|
|
39
|
-
'\\s*=\\s*' + // spaces (KEY = value)
|
|
40
|
-
'["\'`]?' + // open quote
|
|
41
|
-
escapedOriginalValue + // escaped value
|
|
42
|
-
'["\'`]?' + // close quote
|
|
43
|
-
enforceEndOfLine
|
|
44
|
-
,
|
|
45
|
-
'gm' // (g)lobal (m)ultiline
|
|
46
|
-
)
|
|
40
|
+
return `${spaces}${exportPart}${newPart}`
|
|
41
|
+
})
|
|
42
|
+
}
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
function replace (src, key, replaceValue) {
|
|
45
|
+
let output
|
|
46
|
+
let newPart = ''
|
|
47
|
+
|
|
48
|
+
const parsed = dotenvParse(src, true, true, true) // skip expanding \n and skip converting \r\n
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
|
|
50
|
+
const allValues = parsed[key]
|
|
51
|
+
let duplicateOutput = src
|
|
52
|
+
const replacements = Array.isArray(replaceValue) ? replaceValue : allValues.map(() => replaceValue)
|
|
53
|
+
const replacementByValue = new Map()
|
|
54
|
+
|
|
55
|
+
allValues.forEach((value, index) => {
|
|
56
|
+
if (!replacementByValue.has(value)) {
|
|
57
|
+
replacementByValue.set(value, replacements[index])
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
for (const [value, replacement] of replacementByValue) {
|
|
62
|
+
duplicateOutput = replaceExistingValue(duplicateOutput, key, value, replacement)
|
|
63
|
+
}
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
// $2 preserves export
|
|
52
|
-
output = src.replace(currentPart, `$1$2${saferInput}`)
|
|
65
|
+
return duplicateOutput
|
|
53
66
|
} else {
|
|
54
67
|
newPart += `${key}="${replaceValue}"`
|
|
55
68
|
|
|
@@ -74,7 +74,7 @@ class Decrypt {
|
|
|
74
74
|
try {
|
|
75
75
|
const encoding = await detectEncoding(filepath)
|
|
76
76
|
let envSrc = await fsx.readFileX(filepath, { encoding })
|
|
77
|
-
const envParsed = dotenvParse(envSrc)
|
|
77
|
+
const envParsed = dotenvParse(envSrc, false, false, true)
|
|
78
78
|
|
|
79
79
|
const { privateKeyName } = keyNames(envFilepath)
|
|
80
80
|
const { privateKeyValue } = await keyValues(envFilepath, { keysFilepath: this.envKeysFilepath, noOps: this.noOps })
|
|
@@ -83,7 +83,7 @@ class Decrypt {
|
|
|
83
83
|
row.privateKeyName = privateKeyName
|
|
84
84
|
row.changed = false // track possible changes
|
|
85
85
|
|
|
86
|
-
for (const [key,
|
|
86
|
+
for (const [key, values] of Object.entries(envParsed)) {
|
|
87
87
|
// key excluded - don't decrypt it
|
|
88
88
|
if (this.exclude(key)) {
|
|
89
89
|
continue
|
|
@@ -94,13 +94,20 @@ class Decrypt {
|
|
|
94
94
|
continue
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const encrypted = isEncrypted(value)
|
|
97
|
+
const encrypted = values.some(value => isEncrypted(value))
|
|
98
98
|
if (encrypted) {
|
|
99
99
|
row.keys.push(key) // track key(s)
|
|
100
100
|
|
|
101
|
-
const
|
|
101
|
+
const decryptedValues = values.map(value => {
|
|
102
|
+
if (!isEncrypted(value)) {
|
|
103
|
+
return value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return decryptKeyValue(key, value, privateKeyName, privateKeyValue)
|
|
107
|
+
})
|
|
108
|
+
|
|
102
109
|
// once newSrc is built write it out
|
|
103
|
-
envSrc = replace(envSrc, key,
|
|
110
|
+
envSrc = replace(envSrc, key, decryptedValues)
|
|
104
111
|
|
|
105
112
|
row.changed = true // track change
|
|
106
113
|
}
|
|
@@ -91,7 +91,7 @@ class Encrypt {
|
|
|
91
91
|
row.kitCreated = 'sample'
|
|
92
92
|
row.changed = true
|
|
93
93
|
}
|
|
94
|
-
const envParsed = dotenvParse(envSrc)
|
|
94
|
+
const envParsed = dotenvParse(envSrc, false, false, true)
|
|
95
95
|
|
|
96
96
|
let publicKey
|
|
97
97
|
let privateKey
|
|
@@ -122,7 +122,7 @@ class Encrypt {
|
|
|
122
122
|
row.privateKeyName = privateKeyName
|
|
123
123
|
|
|
124
124
|
// iterate over all non-encrypted values and encrypt them
|
|
125
|
-
for (const [key,
|
|
125
|
+
for (const [key, values] of Object.entries(envParsed)) {
|
|
126
126
|
// key excluded - don't encrypt it
|
|
127
127
|
if (this.exclude(key)) {
|
|
128
128
|
continue
|
|
@@ -133,19 +133,24 @@ class Encrypt {
|
|
|
133
133
|
continue
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const encrypted = isEncrypted(value) || isPublicKey(key)
|
|
136
|
+
const encrypted = values.every(value => isEncrypted(value) || isPublicKey(key))
|
|
137
137
|
if (!encrypted) {
|
|
138
138
|
row.keys.push(key) // track key(s)
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
const encryptedValues = values.map(value => {
|
|
141
|
+
if (isEncrypted(value) || isPublicKey(key)) {
|
|
142
|
+
return value
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
return encryptValue(value, publicKey)
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Errors({ publicKeyName, publicKey }).invalidPublicKey()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
146
151
|
|
|
147
152
|
// once newSrc is built write it out
|
|
148
|
-
envSrc = replace(envSrc, key,
|
|
153
|
+
envSrc = replace(envSrc, key, encryptedValues)
|
|
149
154
|
|
|
150
155
|
row.changed = true // track change
|
|
151
156
|
}
|
|
@@ -80,7 +80,7 @@ class Rotate {
|
|
|
80
80
|
try {
|
|
81
81
|
const encoding = await detectEncoding(filepath)
|
|
82
82
|
let envSrc = await fsx.readFileX(filepath, { encoding })
|
|
83
|
-
const envParsed = dotenvParse(envSrc)
|
|
83
|
+
const envParsed = dotenvParse(envSrc, false, false, true)
|
|
84
84
|
|
|
85
85
|
const { publicKeyName, privateKeyName } = keyNames(envFilepath)
|
|
86
86
|
const { privateKeyValue } = await keyValues(envFilepath, { keysFilepath: this.envKeysFilepath, noOps: this.noOps })
|
|
@@ -116,7 +116,7 @@ class Rotate {
|
|
|
116
116
|
envSrc = replace(envSrc, publicKeyName, newPublicKey) // replace publicKey
|
|
117
117
|
row.changed = true // track change
|
|
118
118
|
|
|
119
|
-
for (const [key,
|
|
119
|
+
for (const [key, values] of Object.entries(envParsed)) { // re-encrypt each individual key
|
|
120
120
|
// key excluded - don't re-encrypt it
|
|
121
121
|
if (this.exclude(key)) {
|
|
122
122
|
continue
|
|
@@ -127,7 +127,8 @@ class Rotate {
|
|
|
127
127
|
continue
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
const value = [...values].reverse().find(value => isEncrypted(value))
|
|
131
|
+
if (value) { // only re-encrypt those already encrypted
|
|
131
132
|
row.keys.push(key) // track key(s)
|
|
132
133
|
|
|
133
134
|
const decryptedValue = decryptKeyValue(key, value, privateKeyName, privateKeyValue) // get decrypted value
|
package/src/lib/services/sets.js
CHANGED
|
@@ -29,6 +29,10 @@ const dotenvParse = require('./../helpers/dotenvParse')
|
|
|
29
29
|
const detectEncoding = require('./../helpers/detectEncoding')
|
|
30
30
|
const detectEncodingSync = require('./../helpers/detectEncodingSync')
|
|
31
31
|
|
|
32
|
+
function allValuesForKey (envSrc, key) {
|
|
33
|
+
return dotenvParse(envSrc, false, false, true)[key] || []
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
class Sets {
|
|
33
37
|
constructor (key, value, envs = [], encrypt = true, envKeysFilepath = null, noOps = false, noCreate = false) {
|
|
34
38
|
this.envs = determine(envs, process.env)
|
|
@@ -163,13 +167,14 @@ class Sets {
|
|
|
163
167
|
|
|
164
168
|
const goingFromPlainTextToEncrypted = wasPlainText && this.encrypt
|
|
165
169
|
const valueChanged = this.value !== row.originalValue
|
|
170
|
+
const duplicateKey = allValuesForKey(envSrc, row.key).length > 1
|
|
166
171
|
const shouldPersistSeededPlainValue = seededWithInitialKey && !this.encrypt
|
|
167
172
|
|
|
168
173
|
if (shouldPersistSeededPlainValue) {
|
|
169
174
|
row.envSrc = envSrc
|
|
170
175
|
this.changedFilepaths.add(envFilepath)
|
|
171
176
|
row.changed = true
|
|
172
|
-
} else if (goingFromPlainTextToEncrypted || valueChanged) {
|
|
177
|
+
} else if (goingFromPlainTextToEncrypted || valueChanged || duplicateKey) {
|
|
173
178
|
row.envSrc = replace(envSrc, this.key, row.encryptedValue || this.value)
|
|
174
179
|
this.changedFilepaths.add(envFilepath)
|
|
175
180
|
row.changed = true
|
|
@@ -269,13 +274,14 @@ class Sets {
|
|
|
269
274
|
|
|
270
275
|
const goingFromPlainTextToEncrypted = wasPlainText && this.encrypt
|
|
271
276
|
const valueChanged = this.value !== row.originalValue
|
|
277
|
+
const duplicateKey = allValuesForKey(envSrc, row.key).length > 1
|
|
272
278
|
const shouldPersistSeededPlainValue = seededWithInitialKey && !this.encrypt
|
|
273
279
|
|
|
274
280
|
if (shouldPersistSeededPlainValue) {
|
|
275
281
|
row.envSrc = envSrc
|
|
276
282
|
this.changedFilepaths.add(envFilepath)
|
|
277
283
|
row.changed = true
|
|
278
|
-
} else if (goingFromPlainTextToEncrypted || valueChanged) {
|
|
284
|
+
} else if (goingFromPlainTextToEncrypted || valueChanged || duplicateKey) {
|
|
279
285
|
row.envSrc = replace(envSrc, this.key, row.encryptedValue || this.value)
|
|
280
286
|
this.changedFilepaths.add(envFilepath)
|
|
281
287
|
row.changed = true
|