@dotenvx/dotenvx 1.23.0 → 1.24.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 +25 -1
- package/package.json +3 -5
- package/src/lib/helpers/chomp.js +5 -0
- package/src/lib/helpers/execute.js +4 -1
- package/src/lib/helpers/parse.js +208 -0
- package/src/lib/helpers/resolveEscapeSequences.js +5 -0
- package/src/lib/main.d.ts +0 -11
- package/src/lib/main.js +17 -11
- package/src/lib/services/run.js +17 -14
- package/src/lib/helpers/dotenvEval.js +0 -54
- package/src/lib/helpers/dotenvExpand.js +0 -73
- package/src/lib/helpers/inject.js +0 -27
- package/src/lib/helpers/parseDecryptEvalExpand.js +0 -84
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,31 @@
|
|
|
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.24.0...main)
|
|
6
|
+
|
|
7
|
+
## 1.24.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* support progressive append/update ([#445](https://github.com/dotenvx/dotenvx/pull/445))
|
|
12
|
+
|
|
13
|
+
```ini
|
|
14
|
+
FOO=foo
|
|
15
|
+
FOO=${FOO}bar
|
|
16
|
+
# foobar
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
* support alternate value expansion ([#445](https://github.com/dotenvx/dotenvx/pull/445))
|
|
20
|
+
|
|
21
|
+
<img width="1608" alt="image" src="https://github.com/user-attachments/assets/fdd55a0a-9b36-4cb3-b0c6-6b019441aef4">
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
* `dotenvx.parse` now maps to dotenvx's internal parser. (prior it was mapping to [dotenv's](https://github.com/motdotla/dotenv))
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
* removed `dotenvx.configDotenv()`. use `dotenvx.config()` ([#445](https://github.com/dotenvx/dotenvx/pull/445))
|
|
6
30
|
|
|
7
31
|
## 1.23.0
|
|
8
32
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.24.0",
|
|
3
3
|
"name": "@dotenvx/dotenvx",
|
|
4
4
|
"description": "a better dotenv–from the creator of `dotenv`",
|
|
5
5
|
"author": "@motdotla",
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
"test-single": "tap run --coverage-report=none tests/cli/actions/decrypt.test.js",
|
|
32
32
|
"testshell": "bash shellspec",
|
|
33
33
|
"prerelease": "npm test && npm run testshell",
|
|
34
|
-
"release": "standard-version"
|
|
35
|
-
"patch": "patch-package"
|
|
34
|
+
"release": "standard-version"
|
|
36
35
|
},
|
|
37
36
|
"funding": "https://dotenvx.com",
|
|
38
37
|
"dependencies": {
|
|
@@ -47,9 +46,8 @@
|
|
|
47
46
|
"which": "^4.0.0"
|
|
48
47
|
},
|
|
49
48
|
"devDependencies": {
|
|
49
|
+
"@yao-pkg/pkg": "^5.14.2",
|
|
50
50
|
"capture-console": "^1.0.2",
|
|
51
|
-
"patch-package": "^8.0.0",
|
|
52
|
-
"pkg": "^5.8.1",
|
|
53
51
|
"proxyquire": "^2.1.3",
|
|
54
52
|
"sinon": "^14.0.1",
|
|
55
53
|
"standard": "^17.1.0",
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const execa = require('execa')
|
|
2
|
+
/* c8 ignore start */
|
|
3
|
+
const pkgArgs = process.pkg ? { PKG_EXECPATH: '' } : {}
|
|
4
|
+
/* c8 ignore stop */
|
|
2
5
|
|
|
3
6
|
const execute = {
|
|
4
7
|
execa (command, args, options) {
|
|
5
|
-
return execa(command, args, options)
|
|
8
|
+
return execa(command, args, { ...options, env: { ...options.env, ...pkgArgs } })
|
|
6
9
|
}
|
|
7
10
|
}
|
|
8
11
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const chomp = require('./chomp')
|
|
2
|
+
const truncate = require('./truncate')
|
|
3
|
+
const decryptValue = require('./decryptValue')
|
|
4
|
+
const resolveEscapeSequences = require('./resolveEscapeSequences')
|
|
5
|
+
const { execSync } = require('child_process')
|
|
6
|
+
|
|
7
|
+
class Parse {
|
|
8
|
+
static LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
|
|
9
|
+
|
|
10
|
+
constructor (src, privateKey = null, processEnv = process.env, overload = false) {
|
|
11
|
+
this.src = src
|
|
12
|
+
this.privateKey = privateKey
|
|
13
|
+
this.processEnv = processEnv
|
|
14
|
+
this.overload = overload
|
|
15
|
+
|
|
16
|
+
this.parsed = {}
|
|
17
|
+
this.preExisted = {}
|
|
18
|
+
this.injected = {}
|
|
19
|
+
this.warnings = []
|
|
20
|
+
|
|
21
|
+
// for use with progressive expansion
|
|
22
|
+
this.runningParsed = {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
run () {
|
|
26
|
+
const lines = this.getLines()
|
|
27
|
+
|
|
28
|
+
let match
|
|
29
|
+
while ((match = Parse.LINE.exec(lines)) !== null) {
|
|
30
|
+
const key = match[1]
|
|
31
|
+
const value = match[2]
|
|
32
|
+
const quote = this.quote(value) // must be raw match
|
|
33
|
+
this.parsed[key] = this.clean(value, quote) // file value
|
|
34
|
+
|
|
35
|
+
if (!this.overload && this.inProcessEnv(key)) {
|
|
36
|
+
this.parsed[key] = this.processEnv[key] // use process.env pre-existing value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// decrypt
|
|
40
|
+
try {
|
|
41
|
+
this.parsed[key] = this.decrypt(this.parsed[key])
|
|
42
|
+
} catch (e) {
|
|
43
|
+
this.warnings.push(this.warning(e, key))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// eval empty, double, or backticks
|
|
47
|
+
if (quote !== "'" && (!this.inProcessEnv(key) || this.processEnv[key] === this.parsed[key])) {
|
|
48
|
+
this.parsed[key] = this.eval(this.parsed[key])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// expand empty, double, or backticks
|
|
52
|
+
if (quote !== "'") {
|
|
53
|
+
this.parsed[key] = resolveEscapeSequences(this.expand(this.parsed[key]))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// for use with progressive expansion
|
|
57
|
+
this.runningParsed[key] = this.parsed[key]
|
|
58
|
+
|
|
59
|
+
if (Object.prototype.hasOwnProperty.call(this.processEnv, key) && !this.overload) {
|
|
60
|
+
this.preExisted[key] = this.processEnv[key] // track preExisted
|
|
61
|
+
} else {
|
|
62
|
+
this.injected[key] = this.parsed[key] // track injected
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
parsed: this.parsed,
|
|
68
|
+
processEnv: this.processEnv,
|
|
69
|
+
injected: this.injected,
|
|
70
|
+
warnings: this.warnings,
|
|
71
|
+
preExisted: this.preExisted
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
trimmer (value) {
|
|
76
|
+
// Default undefined or null to empty string
|
|
77
|
+
return (value || '').trim()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
quote (value) {
|
|
81
|
+
const v = this.trimmer(value)
|
|
82
|
+
const maybeQuote = v[0]
|
|
83
|
+
let q = ''
|
|
84
|
+
switch (maybeQuote) {
|
|
85
|
+
// single
|
|
86
|
+
case "'":
|
|
87
|
+
q = "'"
|
|
88
|
+
break
|
|
89
|
+
// double
|
|
90
|
+
case '"':
|
|
91
|
+
q = '"'
|
|
92
|
+
break
|
|
93
|
+
// backtick
|
|
94
|
+
case '`':
|
|
95
|
+
q = '`'
|
|
96
|
+
break
|
|
97
|
+
// empty
|
|
98
|
+
default:
|
|
99
|
+
q = ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return q
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
clean (value, _quote) {
|
|
106
|
+
let v = this.trimmer(value)
|
|
107
|
+
|
|
108
|
+
// Remove surrounding quotes
|
|
109
|
+
v = v.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
|
|
110
|
+
|
|
111
|
+
// Expand newlines if double quoted
|
|
112
|
+
if (_quote === '"') {
|
|
113
|
+
v = v.replace(/\\n/g, '\n')
|
|
114
|
+
v = v.replace(/\\r/g, '\r')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return v
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
decrypt (value) {
|
|
121
|
+
return decryptValue(value, this.privateKey)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
eval (value) {
|
|
125
|
+
const matches = value.match(/\$\([^()]+\)/) || []
|
|
126
|
+
|
|
127
|
+
return matches.reduce(function (newValue, match) {
|
|
128
|
+
const command = match.substring(2, match.length - 1) // get command
|
|
129
|
+
const value = chomp(execSync(command).toString()) // execute command
|
|
130
|
+
return newValue.replace(match, value) // replace with command value
|
|
131
|
+
}, value)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
expand (value) {
|
|
135
|
+
let env = { ...this.runningParsed, ...this.processEnv } // typically process.env wins
|
|
136
|
+
if (this.overload) {
|
|
137
|
+
env = { ...this.processEnv, ...this.runningParsed } // parsed wins
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const regex = /(?<!\\)\${([^{}]+)}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g
|
|
141
|
+
|
|
142
|
+
let result = value
|
|
143
|
+
let match
|
|
144
|
+
const seen = new Set() // self-referential checker
|
|
145
|
+
|
|
146
|
+
while ((match = regex.exec(result)) !== null) {
|
|
147
|
+
seen.add(result)
|
|
148
|
+
|
|
149
|
+
const [template, bracedExpression, unbracedExpression] = match
|
|
150
|
+
const expression = bracedExpression || unbracedExpression
|
|
151
|
+
|
|
152
|
+
// match the operators `:+`, `+`, `:-`, and `-`
|
|
153
|
+
const opRegex = /(:\+|\+|:-|-)/
|
|
154
|
+
// find first match
|
|
155
|
+
const opMatch = expression.match(opRegex)
|
|
156
|
+
const splitter = opMatch ? opMatch[0] : null
|
|
157
|
+
|
|
158
|
+
const r = expression.split(splitter)
|
|
159
|
+
|
|
160
|
+
let key
|
|
161
|
+
let defaultValue
|
|
162
|
+
let value
|
|
163
|
+
|
|
164
|
+
if ([':+', '+'].includes(splitter)) {
|
|
165
|
+
key = r.shift()
|
|
166
|
+
defaultValue = env[key] ? r.join(splitter) : ''
|
|
167
|
+
value = null
|
|
168
|
+
} else {
|
|
169
|
+
key = r.shift()
|
|
170
|
+
defaultValue = r.join(splitter)
|
|
171
|
+
value = env[key]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (value) {
|
|
175
|
+
// self-referential check
|
|
176
|
+
if (seen.has(value)) {
|
|
177
|
+
result = result.replace(template, defaultValue)
|
|
178
|
+
} else {
|
|
179
|
+
result = result.replace(template, value)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
result = result.replace(template, defaultValue)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
regex.lastIndex = 0 // reset regex search position to re-evaluate after each replacement
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
inProcessEnv (key) {
|
|
192
|
+
return Object.prototype.hasOwnProperty.call(this.processEnv, key)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getLines () {
|
|
196
|
+
return (this.src || '').toString().replace(/\r\n?/mg, '\n') // Convert buffer to string and Convert line breaks to same format
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
warning (e, key) {
|
|
200
|
+
const warning = new Error(`[${e.code}] could not decrypt ${key} using private key '${truncate(this.privateKey)}'`)
|
|
201
|
+
warning.code = e.code
|
|
202
|
+
warning.help = `[${e.code}] ? ${e.message}`
|
|
203
|
+
|
|
204
|
+
return warning
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = Parse
|
package/src/lib/main.d.ts
CHANGED
|
@@ -124,17 +124,6 @@ export interface DotenvPopulateInput {
|
|
|
124
124
|
*/
|
|
125
125
|
export function config(options?: DotenvConfigOptions): DotenvConfigOutput;
|
|
126
126
|
|
|
127
|
-
/**
|
|
128
|
-
* Loads `.env` file contents into process.env.
|
|
129
|
-
*
|
|
130
|
-
* @see https://dotenvx.com/docs
|
|
131
|
-
*
|
|
132
|
-
* @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
|
|
133
|
-
* @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
|
|
134
|
-
*
|
|
135
|
-
*/
|
|
136
|
-
export function configDotenv(options?: DotenvConfigOptions): DotenvConfigOutput;
|
|
137
|
-
|
|
138
127
|
/**
|
|
139
128
|
* Decrypt ciphertext
|
|
140
129
|
*
|
package/src/lib/main.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
const path = require('path')
|
|
3
|
-
const dotenv = require('dotenv')
|
|
4
3
|
|
|
5
4
|
// shared
|
|
6
5
|
const { setLogLevel, logger } = require('./../shared/logger')
|
|
@@ -16,8 +15,7 @@ const Genexample = require('./services/genexample')
|
|
|
16
15
|
// helpers
|
|
17
16
|
const conventions = require('./helpers/conventions')
|
|
18
17
|
const dotenvOptionPaths = require('./helpers/dotenvOptionPaths')
|
|
19
|
-
|
|
20
|
-
// proxies to dotenv
|
|
18
|
+
const Parse = require('./helpers/parse')
|
|
21
19
|
|
|
22
20
|
/** @type {import('./main').config} */
|
|
23
21
|
const config = function (options = {}) {
|
|
@@ -162,14 +160,23 @@ const config = function (options = {}) {
|
|
|
162
160
|
}
|
|
163
161
|
}
|
|
164
162
|
|
|
165
|
-
/** @type {import('./main').configDotenv} */
|
|
166
|
-
const configDotenv = function (options) {
|
|
167
|
-
return dotenv.configDotenv(options)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
163
|
/** @type {import('./main').parse} */
|
|
171
|
-
const parse = function (src) {
|
|
172
|
-
|
|
164
|
+
const parse = function (src, options = {}) {
|
|
165
|
+
// allow user to set processEnv to read from
|
|
166
|
+
let processEnv = process.env
|
|
167
|
+
if (options && options.processEnv != null) {
|
|
168
|
+
processEnv = options.processEnv
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// private decryption key
|
|
172
|
+
const privateKey = null // implement later
|
|
173
|
+
|
|
174
|
+
// overload
|
|
175
|
+
const overload = options.overload || options.override
|
|
176
|
+
|
|
177
|
+
const { parsed } = new Parse(src, privateKey, processEnv, overload).run()
|
|
178
|
+
|
|
179
|
+
return parsed
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
/** @type {import('./main').ls} */
|
|
@@ -201,7 +208,6 @@ const keypair = function (envFile, key) {
|
|
|
201
208
|
module.exports = {
|
|
202
209
|
// dotenv proxies
|
|
203
210
|
config,
|
|
204
|
-
configDotenv,
|
|
205
211
|
parse,
|
|
206
212
|
// actions related
|
|
207
213
|
ls,
|
package/src/lib/services/run.js
CHANGED
|
@@ -6,9 +6,8 @@ const TYPE_ENV = 'env'
|
|
|
6
6
|
const TYPE_ENV_FILE = 'envFile'
|
|
7
7
|
const TYPE_ENV_VAULT_FILE = 'envVaultFile'
|
|
8
8
|
|
|
9
|
-
const inject = require('./../helpers/inject')
|
|
10
9
|
const decrypt = require('./../helpers/decrypt')
|
|
11
|
-
const
|
|
10
|
+
const Parse = require('./../helpers/parse')
|
|
12
11
|
const parseEnvironmentFromDotenvKey = require('./../helpers/parseEnvironmentFromDotenvKey')
|
|
13
12
|
const detectEncoding = require('./../helpers/detectEncoding')
|
|
14
13
|
const findPrivateKey = require('./../helpers/findPrivateKey')
|
|
@@ -60,15 +59,16 @@ class Run {
|
|
|
60
59
|
row.string = env
|
|
61
60
|
|
|
62
61
|
try {
|
|
63
|
-
const { parsed,
|
|
62
|
+
const { parsed, warnings, injected, preExisted } = new Parse(env, null, this.processEnv, this.overload).run()
|
|
64
63
|
row.parsed = parsed
|
|
65
64
|
row.warnings = warnings
|
|
66
|
-
this.readableStrings.add(env)
|
|
67
|
-
|
|
68
|
-
const { injected, preExisted } = this._inject(processEnv, parsed, this.overload, this.processEnv)
|
|
69
65
|
row.injected = injected
|
|
70
66
|
row.preExisted = preExisted
|
|
71
67
|
|
|
68
|
+
this.inject(row.parsed) // inject
|
|
69
|
+
|
|
70
|
+
this.readableStrings.add(env)
|
|
71
|
+
|
|
72
72
|
for (const key of Object.keys(injected)) {
|
|
73
73
|
this.uniqueInjectedKeys.add(key) // track uniqueInjectedKeys across multiple files
|
|
74
74
|
}
|
|
@@ -91,14 +91,15 @@ class Run {
|
|
|
91
91
|
this.readableFilepaths.add(envFilepath)
|
|
92
92
|
|
|
93
93
|
const privateKey = findPrivateKey(envFilepath)
|
|
94
|
-
const { parsed,
|
|
94
|
+
const { parsed, warnings, injected, preExisted } = new Parse(src, privateKey, this.processEnv, this.overload).run()
|
|
95
|
+
|
|
95
96
|
row.parsed = parsed
|
|
96
97
|
row.warnings = warnings
|
|
97
|
-
|
|
98
|
-
const { injected, preExisted } = this._inject(processEnv, parsed, this.overload, this.processEnv)
|
|
99
98
|
row.injected = injected
|
|
100
99
|
row.preExisted = preExisted
|
|
101
100
|
|
|
101
|
+
this.inject(row.parsed) // inject
|
|
102
|
+
|
|
102
103
|
for (const key of Object.keys(injected)) {
|
|
103
104
|
this.uniqueInjectedKeys.add(key) // track uniqueInjectedKeys across multiple files
|
|
104
105
|
}
|
|
@@ -161,14 +162,14 @@ class Run {
|
|
|
161
162
|
|
|
162
163
|
try {
|
|
163
164
|
// parse this. it's the equivalent of the .env file
|
|
164
|
-
const { parsed,
|
|
165
|
+
const { parsed, warnings, injected, preExisted } = new Parse(decrypted, null, this.processEnv, this.overload).run()
|
|
165
166
|
row.parsed = parsed
|
|
166
167
|
row.warnings = warnings
|
|
167
|
-
|
|
168
|
-
const { injected, preExisted } = this._inject(processEnv, parsed, this.overload, this.processEnv)
|
|
169
168
|
row.injected = injected
|
|
170
169
|
row.preExisted = preExisted
|
|
171
170
|
|
|
171
|
+
this.inject(row.parsed) // inject
|
|
172
|
+
|
|
172
173
|
for (const key of Object.keys(injected)) {
|
|
173
174
|
this.uniqueInjectedKeys.add(key) // track uniqueInjectedKeys across multiple files
|
|
174
175
|
}
|
|
@@ -179,8 +180,10 @@ class Run {
|
|
|
179
180
|
this.processedEnvs.push(row)
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
inject (parsed) {
|
|
184
|
+
for (const key of Object.keys(parsed)) {
|
|
185
|
+
this.processEnv[key] = parsed[key] // inject to process.env
|
|
186
|
+
}
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
// handle scenario for comma separated keys - for use with key rotation
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
const { execSync } = require('child_process')
|
|
2
|
-
|
|
3
|
-
function _chomp (value) {
|
|
4
|
-
return value.replace(/[\r\n]+$/, '')
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function interpolate (value, processEnv, parsed) {
|
|
8
|
-
const matches = value.match(/\$\([^()]+\)/) || []
|
|
9
|
-
|
|
10
|
-
return matches.reduce(function (newValue, match) {
|
|
11
|
-
// get command
|
|
12
|
-
const command = match.substring(2, match.length - 1)
|
|
13
|
-
// execute command
|
|
14
|
-
const value = _chomp(execSync(command).toString())
|
|
15
|
-
// replace with command value
|
|
16
|
-
return newValue.replace(match, value)
|
|
17
|
-
}, value)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function evaluate (options) {
|
|
21
|
-
let processEnv = process.env
|
|
22
|
-
if (options && options.processEnv != null) {
|
|
23
|
-
processEnv = options.processEnv
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
for (const key in options.parsed) {
|
|
27
|
-
let value = options.parsed[key]
|
|
28
|
-
|
|
29
|
-
const inProcessEnv = Object.prototype.hasOwnProperty.call(processEnv, key)
|
|
30
|
-
|
|
31
|
-
if (inProcessEnv) {
|
|
32
|
-
if (processEnv[key] === options.parsed[key]) {
|
|
33
|
-
// assume was set to processEnv from the .env file if the values match and therefore interpolate
|
|
34
|
-
value = interpolate(value, processEnv, options.parsed)
|
|
35
|
-
} else {
|
|
36
|
-
// do not interpolate - assume processEnv had the intended value even if containing a $.
|
|
37
|
-
value = processEnv[key]
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
// not inProcessEnv so assume interpolation for this .env key
|
|
41
|
-
value = interpolate(value, processEnv, options.parsed)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
options.parsed[key] = value
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
for (const processKey in options.parsed) {
|
|
48
|
-
processEnv[processKey] = options.parsed[processKey]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return options
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
module.exports.eval = evaluate
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
function _resolveEscapeSequences (value) {
|
|
2
|
-
return value.replace(/\\\$/g, '$')
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function interpolate (value, env) {
|
|
6
|
-
const regex = /(?<!\\)\${([^{}]+)}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g
|
|
7
|
-
|
|
8
|
-
let result = value
|
|
9
|
-
let match
|
|
10
|
-
const seen = new Set() // self-referential checker
|
|
11
|
-
|
|
12
|
-
while ((match = regex.exec(result)) !== null) {
|
|
13
|
-
seen.add(result)
|
|
14
|
-
|
|
15
|
-
const [template, bracedExpression, unbracedExpression] = match
|
|
16
|
-
const expression = bracedExpression || unbracedExpression
|
|
17
|
-
const r = expression.split(/:-|-/)
|
|
18
|
-
const key = r.shift()
|
|
19
|
-
const defaultValue = r.join('-')
|
|
20
|
-
const value = env[key]
|
|
21
|
-
|
|
22
|
-
if (value) {
|
|
23
|
-
// self-referential check
|
|
24
|
-
if (seen.has(value)) {
|
|
25
|
-
result = result.replace(template, defaultValue)
|
|
26
|
-
} else {
|
|
27
|
-
result = result.replace(template, value)
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
result = result.replace(template, defaultValue)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
regex.lastIndex = 0 // reset regex search position to re-evaluate after each replacement
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return result
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function expand (options) {
|
|
40
|
-
const processEnv = options.processEnv || {}
|
|
41
|
-
const parsed = options.parsed || {}
|
|
42
|
-
|
|
43
|
-
const combined = { ...processEnv, ...parsed }
|
|
44
|
-
const combinedReversed = { ...parsed, ...processEnv }
|
|
45
|
-
|
|
46
|
-
for (const key in parsed) {
|
|
47
|
-
const value = parsed[key]
|
|
48
|
-
|
|
49
|
-
// interpolate using both file and processEnv (file interpolation wins. used for --overload later)
|
|
50
|
-
const fileValue = _resolveEscapeSequences(interpolate(value, combined))
|
|
51
|
-
parsed[key] = fileValue
|
|
52
|
-
|
|
53
|
-
if (fileValue === _resolveEscapeSequences(value)) {
|
|
54
|
-
continue // no change means no expansion, move on
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (processEnv[key]) {
|
|
58
|
-
continue // already has a value in processEnv, move on
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const processEnvValue = interpolate(value, combinedReversed) // could be empty string ''
|
|
62
|
-
if (processEnvValue) {
|
|
63
|
-
processEnv[key] = _resolveEscapeSequences(processEnvValue) // set it
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
parsed,
|
|
69
|
-
processEnv
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
module.exports.expand = expand
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
function inject (clonedProcessEnv = {}, parsed = {}, overload = false, processEnv = process.env) {
|
|
2
|
-
const injected = {}
|
|
3
|
-
const preExisted = {}
|
|
4
|
-
|
|
5
|
-
// set processEnv
|
|
6
|
-
for (const key of Object.keys(parsed)) {
|
|
7
|
-
if (Object.prototype.hasOwnProperty.call(clonedProcessEnv, key)) {
|
|
8
|
-
if (overload === true) {
|
|
9
|
-
processEnv[key] = parsed[key]
|
|
10
|
-
injected[key] = parsed[key] // track injected key/value
|
|
11
|
-
} else {
|
|
12
|
-
processEnv[key] = clonedProcessEnv[key]
|
|
13
|
-
preExisted[key] = clonedProcessEnv[key] // track preExisted key/value
|
|
14
|
-
}
|
|
15
|
-
} else {
|
|
16
|
-
processEnv[key] = parsed[key]
|
|
17
|
-
injected[key] = parsed[key] // track injected key/value
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
injected,
|
|
23
|
-
preExisted
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
module.exports = inject
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
const dotenv = require('dotenv')
|
|
2
|
-
const dotenvEval = require('./dotenvEval')
|
|
3
|
-
const dotenvExpand = require('./dotenvExpand')
|
|
4
|
-
const decryptValue = require('./decryptValue')
|
|
5
|
-
const truncate = require('./truncate')
|
|
6
|
-
const quotes = require('./quotes')
|
|
7
|
-
|
|
8
|
-
function warning (e, key, privateKey = null) {
|
|
9
|
-
const warning = new Error(`[${e.code}] could not decrypt ${key} using private key '${truncate(privateKey)}'`)
|
|
10
|
-
warning.code = e.code
|
|
11
|
-
warning.help = `[${e.code}] ? ${e.message}`
|
|
12
|
-
|
|
13
|
-
return warning
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function parseDecryptEvalExpand (src, privateKey = null, processEnv = process.env) {
|
|
17
|
-
const warnings = []
|
|
18
|
-
|
|
19
|
-
// parse and quotes
|
|
20
|
-
const parsed = dotenv.parse(src)
|
|
21
|
-
const _quotes = quotes(src)
|
|
22
|
-
const originalParsed = { ...parsed }
|
|
23
|
-
const originalProcessEnv = { ...processEnv }
|
|
24
|
-
for (const key in parsed) {
|
|
25
|
-
try {
|
|
26
|
-
const decryptedValue = decryptValue(parsed[key], privateKey)
|
|
27
|
-
parsed[key] = decryptedValue
|
|
28
|
-
} catch (_e) {
|
|
29
|
-
// do nothing. warnings tracked further below.
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// eval parsed only. do NOT eval process.env ever. too risky/dangerous.
|
|
34
|
-
const inputParsed = {
|
|
35
|
-
processEnv: {},
|
|
36
|
-
parsed
|
|
37
|
-
}
|
|
38
|
-
const evaled = dotenvEval.eval(inputParsed).parsed
|
|
39
|
-
|
|
40
|
-
// expanded
|
|
41
|
-
const inputEvaled = {
|
|
42
|
-
processEnv,
|
|
43
|
-
parsed: evaled
|
|
44
|
-
}
|
|
45
|
-
const expanded = dotenvExpand.expand(inputEvaled)
|
|
46
|
-
|
|
47
|
-
for (const key in expanded.parsed) {
|
|
48
|
-
// unset eval and expansion for single quotes
|
|
49
|
-
if (_quotes[key] === "'") {
|
|
50
|
-
expanded.parsed[key] = originalParsed[key] // reset to original
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const decryptedValue = decryptValue(expanded.parsed[key], privateKey)
|
|
55
|
-
expanded.parsed[key] = decryptedValue
|
|
56
|
-
} catch (e) {
|
|
57
|
-
warnings.push(warning(e, key, privateKey))
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (const key in processEnv) {
|
|
62
|
-
// unset eval and expansion for single quotes
|
|
63
|
-
if (_quotes[key] === "'") {
|
|
64
|
-
processEnv[key] = originalProcessEnv[key] || originalParsed[key] // reset to original
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const decryptedValue = decryptValue(processEnv[key], privateKey)
|
|
69
|
-
processEnv[key] = decryptedValue
|
|
70
|
-
} catch (e) {
|
|
71
|
-
warnings.push(warning(e, key, privateKey))
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// for logging only log the original keys existing in parsed. this feels unnecessarily complex - like dotenv-expand should support the ability to inject additional `process.env` or objects as it sees fit to the object it wants to expand
|
|
76
|
-
const result = {}
|
|
77
|
-
for (const key in parsed) {
|
|
78
|
-
result[key] = expanded.parsed[key]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return { parsed: result, processEnv, warnings }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
module.exports = parseDecryptEvalExpand
|