@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/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.64.0",
2
+ "version": "1.65.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "secrets for agents–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -42,7 +42,7 @@ class Ops {
42
42
  if (publicKey) args.push(publicKey)
43
43
 
44
44
  try {
45
- return JSON.parse(await this._exec(binary, args))
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._execSync(binary, args))
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 replace (src, key, replaceValue) {
7
- let output
8
- let newPart = ''
4
+ function replaceExistingValue (src, key, originalValue, replaceValue) {
5
+ const escapedKey = escapeForRegex(key)
6
+ const escapedOriginalValue = escapeForRegex(originalValue)
9
7
 
10
- const parsed = dotenvParse(src, true, true) // skip expanding \n and skip converting \r\n
11
- const _quotes = quotes(src)
12
- if (Object.prototype.hasOwnProperty.call(parsed, key)) {
13
- const quote = _quotes[key]
14
- newPart += `${key}=${quote}${replaceValue}${quote}`
8
+ // conditionally enforce end of line
9
+ let enforceEndOfLine = ''
10
+ if (escapedOriginalValue === '') {
11
+ enforceEndOfLine = '$' // EMPTY scenario
12
+ }
15
13
 
16
- const originalValue = parsed[key]
17
- const escapedOriginalValue = escapeForRegex(originalValue)
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
- // conditionally enforce end of line
20
- let enforceEndOfLine = ''
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
- // if empty quote and consecutive newlines
25
- const newlineMatch = src.match(new RegExp(`${key}\\s*=\\s*\n\n`, 'm')) // match any consecutive newline scenario for a blank value
26
- if (quote === '' && newlineMatch) {
27
- const newlineCount = (newlineMatch[0].match(/\n/g)).length - 1
28
- for (let i = 0; i < newlineCount; i++) {
29
- newPart += '\n' // re-append the extra newline to preserve user's format choice
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
- const currentPart = new RegExp(
35
- '^' + // start of line
36
- '(\\s*)?' + // spaces
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
- const saferInput = escapeDollarSigns(newPart) // cleanse user inputted capture groups ($1, $2 etc)
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
- // $1 preserves spaces
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, value] of Object.entries(envParsed)) {
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 decryptedValue = decryptKeyValue(key, value, privateKeyName, privateKeyValue)
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, decryptedValue)
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, value] of Object.entries(envParsed)) {
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
- let encryptedValue
141
- try {
142
- encryptedValue = encryptValue(value, publicKey)
143
- } catch {
144
- throw new Errors({ publicKeyName, publicKey }).invalidPublicKey()
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, encryptedValue)
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, value] of Object.entries(envParsed)) { // re-encrypt each individual 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
- if (isEncrypted(value)) { // only re-encrypt those already encrypted
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
@@ -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