@dotenvx/dotenvx 2.8.0 → 2.10.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 +13 -1
- package/README.md +33 -0
- package/package.json +1 -1
- package/src/cli/actions/ls.js +58 -6
- package/src/cli/actions/run.js +12 -4
- package/src/cli/commands/ext.js +1 -0
- package/src/cli/dotenvx.js +2 -0
- package/src/lib/helpers/catchAndLog.js +0 -5
- package/src/lib/helpers/decryptedValues.js +32 -0
- package/src/lib/helpers/executeCommand.js +20 -3
- package/src/lib/helpers/fsx.js +19 -2
- package/src/lib/helpers/redact.js +9 -0
- package/src/lib/helpers/redactOutput.js +120 -0
- package/src/lib/main.d.ts +4 -2
- package/src/lib/main.js +2 -2
- package/src/lib/resolvers/ls.js +31 -3
- package/src/lib/services/prebuild.js +1 -1
- package/src/lib/services/precommit.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
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/v2.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.10.0...main)
|
|
6
|
+
|
|
7
|
+
## [2.10.0](https://github.com/dotenvx/dotenvx/compare/v2.9.0...v2.10.0) (2026-07-15)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add `run --redact` to keep secrets out of command output. ([#894](https://github.com/dotenvx/dotenvx/pull/894))
|
|
12
|
+
|
|
13
|
+
## [2.9.0](https://github.com/dotenvx/dotenvx/compare/v2.8.0...v2.9.0) (2026-07-14)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
* BREAKING: `ls` from `@dotenvx/dotenvx` is now async/await. ([#893](https://github.com/dotenvx/dotenvx/pull/893))
|
|
6
18
|
|
|
7
19
|
## [2.8.0](https://github.com/dotenvx/dotenvx/compare/v2.7.3...v2.8.0) (2026-07-14)
|
|
8
20
|
|
package/README.md
CHANGED
|
@@ -1471,6 +1471,24 @@ $ dotenvx set HELLO Dotenvx -fk .env.keys -f apps/app1/.env
|
|
|
1471
1471
|
$ dotenvx run -fk .env.keys -f apps/app1/.env -- yourcommand
|
|
1472
1472
|
```
|
|
1473
1473
|
|
|
1474
|
+
</details>
|
|
1475
|
+
<details><summary>`run --redact`</summary><br>
|
|
1476
|
+
|
|
1477
|
+
Redact successfully decrypted values from the command's stdout and stderr. The command still receives the real values; only its output is filtered.
|
|
1478
|
+
|
|
1479
|
+
```sh
|
|
1480
|
+
$ touch .env
|
|
1481
|
+
$ dotenvx set SECRET super-secret-value
|
|
1482
|
+
$ echo "console.log(process.env.SECRET)" > index.js
|
|
1483
|
+
|
|
1484
|
+
$ dotenvx run --redact --quiet -- node index.js
|
|
1485
|
+
[REDACTED]
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
Redaction is off by default. It applies only to values that were encrypted, successfully decrypted, and injected. Plaintext values are left unchanged. Matching is exact, so transformed or derived values are not redacted.
|
|
1489
|
+
|
|
1490
|
+
Because redaction filters stdout and stderr, interactive commands that require a TTY may behave differently.
|
|
1491
|
+
|
|
1474
1492
|
</details>
|
|
1475
1493
|
<details><summary>`run --mask`</summary><br>
|
|
1476
1494
|
|
|
@@ -2335,6 +2353,21 @@ $ dotenvx ls -ef '**/.env.prod*'
|
|
|
2335
2353
|
└─ .env
|
|
2336
2354
|
```
|
|
2337
2355
|
|
|
2356
|
+
</details>
|
|
2357
|
+
<details><summary>`ls --json`</summary><br>
|
|
2358
|
+
|
|
2359
|
+
Print all matching `.env` files as a JSON array of absolute filepaths. Progress and summary details are written to stderr, so stdout can be safely piped to another command or file.
|
|
2360
|
+
|
|
2361
|
+
```sh
|
|
2362
|
+
$ dotenvx ls --json
|
|
2363
|
+
[
|
|
2364
|
+
"/path/to/project/.env",
|
|
2365
|
+
"/path/to/project/apps/backend/.env"
|
|
2366
|
+
]
|
|
2367
|
+
|
|
2368
|
+
$ dotenvx ls --json > dotenv-files.json
|
|
2369
|
+
```
|
|
2370
|
+
|
|
2338
2371
|
</details>
|
|
2339
2372
|
<details><summary>`genexample`</summary><br>
|
|
2340
2373
|
|
package/package.json
CHANGED
package/src/cli/actions/ls.js
CHANGED
|
@@ -1,24 +1,76 @@
|
|
|
1
1
|
const { objectTreeify: treeify } = require('@dotenvx/tooling')
|
|
2
|
+
const path = require('path')
|
|
2
3
|
|
|
3
4
|
const { logger } = require('./../../shared/logger')
|
|
4
5
|
|
|
5
6
|
const main = require('./../../lib/main')
|
|
6
7
|
const ArrayToTree = require('./../../lib/helpers/arrayToTree')
|
|
8
|
+
const catchAndLog = require('../../lib/helpers/catchAndLog')
|
|
9
|
+
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
7
10
|
|
|
8
|
-
function ls (directory) {
|
|
11
|
+
async function ls (directory) {
|
|
9
12
|
// debug args
|
|
10
13
|
logger.debug(`directory: ${directory}`)
|
|
11
14
|
|
|
12
15
|
const options = this.opts()
|
|
16
|
+
let spinnerOptions
|
|
17
|
+
if (typeof this.optsWithGlobals === 'function') {
|
|
18
|
+
spinnerOptions = this.optsWithGlobals()
|
|
19
|
+
} else {
|
|
20
|
+
spinnerOptions = options
|
|
21
|
+
}
|
|
22
|
+
const spinner = await createSpinner({ ...spinnerOptions, ...options, text: 'traversing' })
|
|
23
|
+
const startedAt = Date.now()
|
|
24
|
+
let directoryCount = 1
|
|
13
25
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
14
26
|
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
try {
|
|
28
|
+
const filepaths = await main.ls(directory, options.envFile, options.excludeEnvFile, (filepath) => {
|
|
29
|
+
directoryCount += 1
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
if (spinner) {
|
|
32
|
+
const elapsedSeconds = Math.floor((Date.now() - startedAt) / 1000)
|
|
33
|
+
let directoryLabel = 'directories'
|
|
34
|
+
if (directoryCount === 1) {
|
|
35
|
+
directoryLabel = 'directory'
|
|
36
|
+
}
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
spinner.text = `traversing ${directoryCount.toLocaleString()} ${directoryLabel} (${elapsedSeconds}s) — ${filepath}`
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
logger.debug(`filepaths: ${JSON.stringify(filepaths)}`)
|
|
42
|
+
|
|
43
|
+
if (spinner) spinner.stop()
|
|
44
|
+
|
|
45
|
+
if (options.json) {
|
|
46
|
+
const cwd = path.resolve(directory || '.')
|
|
47
|
+
const absoluteFilepaths = filepaths.map(filepath => path.resolve(cwd, filepath))
|
|
48
|
+
console.log(JSON.stringify(absoluteFilepaths, null, 2))
|
|
49
|
+
} else {
|
|
50
|
+
const tree = new ArrayToTree(filepaths).run()
|
|
51
|
+
logger.debug(`tree: ${JSON.stringify(tree)}`)
|
|
52
|
+
logger.info(treeify(tree))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!spinnerOptions.quiet && !options.quiet) {
|
|
56
|
+
const elapsedSeconds = Math.floor((Date.now() - startedAt) / 1000)
|
|
57
|
+
const matchedDirectoryCount = new Set(filepaths.map(filepath => path.dirname(filepath))).size
|
|
58
|
+
let fileLabel = 'files'
|
|
59
|
+
if (filepaths.length === 1) {
|
|
60
|
+
fileLabel = 'file'
|
|
61
|
+
}
|
|
62
|
+
let directoryLabel = 'directories'
|
|
63
|
+
if (matchedDirectoryCount === 1) {
|
|
64
|
+
directoryLabel = 'directory'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.error(`▣ found ${filepaths.length.toLocaleString()} .env ${fileLabel} across ${matchedDirectoryCount.toLocaleString()} ${directoryLabel} of ${directoryCount.toLocaleString()} scanned in ${elapsedSeconds}s`)
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (spinner) spinner.stop()
|
|
71
|
+
catchAndLog(error)
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
22
74
|
}
|
|
23
75
|
|
|
24
76
|
module.exports = ls
|
package/src/cli/actions/run.js
CHANGED
|
@@ -13,6 +13,8 @@ const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
|
|
|
13
13
|
const mask = require('../../lib/helpers/mask')
|
|
14
14
|
const maskEnvSrc = require('../../lib/helpers/maskEnvSrc')
|
|
15
15
|
const maskProcessedEnvs = require('../../lib/helpers/maskProcessedEnvs')
|
|
16
|
+
const decryptedValues = require('../../lib/helpers/decryptedValues')
|
|
17
|
+
const { redactOutput } = require('../../lib/helpers/redactOutput')
|
|
16
18
|
|
|
17
19
|
const { determine } = require('./../../lib/helpers/envResolution')
|
|
18
20
|
|
|
@@ -53,11 +55,13 @@ async function run () {
|
|
|
53
55
|
const options = normalizeDotenvConfigConvention(normalizeDotenvConfigQuiet(this.opts()))
|
|
54
56
|
const spinnerOptions = typeof this.optsWithGlobals === 'function' ? this.optsWithGlobals() : options
|
|
55
57
|
const maskEnabled = options.mask !== undefined
|
|
58
|
+
const redactEnabled = options.redact === true
|
|
56
59
|
let showChar = options.mask
|
|
57
60
|
if (options.mask === true) {
|
|
58
61
|
showChar = 6
|
|
59
62
|
}
|
|
60
63
|
let commandEnv = process.env
|
|
64
|
+
let sensitiveValues = []
|
|
61
65
|
|
|
62
66
|
let commandArgs = this.args
|
|
63
67
|
if (commandArgs.length < 1) {
|
|
@@ -122,6 +126,10 @@ async function run () {
|
|
|
122
126
|
}
|
|
123
127
|
})
|
|
124
128
|
|
|
129
|
+
if (redactEnabled) {
|
|
130
|
+
sensitiveValues = decryptedValues(processedEnvs)
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
if (maskEnabled) {
|
|
126
134
|
commandEnv = { ...process.env }
|
|
127
135
|
maskProcessedEnvs(processedEnvs, commandEnv, showChar)
|
|
@@ -156,18 +164,18 @@ async function run () {
|
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
// debug parsed
|
|
159
|
-
logger.debug(processedEnv.parsed)
|
|
167
|
+
logger.debug(redactOutput(processedEnv.parsed, sensitiveValues))
|
|
160
168
|
|
|
161
169
|
// verbose/debug injected key/value
|
|
162
170
|
for (const [key, value] of Object.entries(processedEnv.injected || {})) {
|
|
163
171
|
logger.verbose(`${key} set`)
|
|
164
|
-
logger.debug(`${key} set to ${value}
|
|
172
|
+
logger.debug(redactOutput(`${key} set to ${value}`, sensitiveValues))
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
// verbose/debug existed key/value
|
|
168
176
|
for (const [key, value] of Object.entries(processedEnv.existed || {})) {
|
|
169
177
|
logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
|
|
170
|
-
logger.debug(`${key} pre-exists as ${value} (protip: use --overload to override)
|
|
178
|
+
logger.debug(redactOutput(`${key} pre-exists as ${value} (protip: use --overload to override)`, sensitiveValues))
|
|
171
179
|
}
|
|
172
180
|
}
|
|
173
181
|
|
|
@@ -189,7 +197,7 @@ async function run () {
|
|
|
189
197
|
process.exit(1)
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
await executeCommand(commandArgs, commandEnv)
|
|
200
|
+
await executeCommand(commandArgs, commandEnv, sensitiveValues)
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
module.exports = run
|
package/src/cli/commands/ext.js
CHANGED
|
@@ -24,6 +24,7 @@ ext.command('ls')
|
|
|
24
24
|
.argument('[directory]', 'directory to list .env files from', '.')
|
|
25
25
|
.option('-f, --env-file <filenames...>', 'path(s) to your env file(s)', '.env*')
|
|
26
26
|
.option('-ef, --exclude-env-file <excludeFilenames...>', 'path(s) to exclude from your env file(s) (default: none)')
|
|
27
|
+
.option('--json', 'output a JSON array of absolute filepaths')
|
|
27
28
|
.action(function (...args) {
|
|
28
29
|
return require('./../actions/ls').apply(this, args)
|
|
29
30
|
})
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -70,6 +70,7 @@ program.command('run')
|
|
|
70
70
|
.option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
|
|
71
71
|
.option('--token <token>', 'set Armor ⛨ token')
|
|
72
72
|
.option('--mask [characters]', 'inject masked values, optionally setting visible characters')
|
|
73
|
+
.option('--redact', 'redact decrypted values from command output', false)
|
|
73
74
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
74
75
|
.option('--no-native', 'disable OS secret store features')
|
|
75
76
|
.action(function (...args) {
|
|
@@ -176,6 +177,7 @@ program.command('ls')
|
|
|
176
177
|
.argument('[directory]', 'directory to list .env files from', '.')
|
|
177
178
|
.option('-f, --env-file <filenames...>', 'path(s) to your env file(s)', '.env*')
|
|
178
179
|
.option('-ef, --exclude-env-file <excludeFilenames...>', 'path(s) to exclude from your env file(s) (default: none)')
|
|
180
|
+
.option('--json', 'output a JSON array of absolute filepaths')
|
|
179
181
|
.action(function (...args) {
|
|
180
182
|
return require('./actions/ls').apply(this, args)
|
|
181
183
|
})
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
const { logger } = require('./../../shared/logger')
|
|
2
|
-
const Errors = require('./errors')
|
|
3
2
|
|
|
4
3
|
function catchAndLog (error) {
|
|
5
|
-
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
6
|
-
error = new Errors({ filepath: error.path }).fileNotWritable()
|
|
7
|
-
}
|
|
8
|
-
|
|
9
4
|
const msg = error.messageWithHelp || error.message
|
|
10
5
|
if (msg) {
|
|
11
6
|
logger.error(msg)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const { encrypted, scan } = require('@dotenvx/primitives')
|
|
2
|
+
|
|
3
|
+
function decryptedValues (processedEnvs) {
|
|
4
|
+
const result = new Set()
|
|
5
|
+
|
|
6
|
+
for (const processedEnv of processedEnvs || []) {
|
|
7
|
+
const src = processedEnv.src || processedEnv.string
|
|
8
|
+
if (!src) continue
|
|
9
|
+
|
|
10
|
+
let rawParsed
|
|
11
|
+
try {
|
|
12
|
+
rawParsed = scan(src).parsed
|
|
13
|
+
} catch (error) {
|
|
14
|
+
continue
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const [key, rawValues] of Object.entries(rawParsed || {})) {
|
|
18
|
+
const rawValue = rawValues[rawValues.length - 1]
|
|
19
|
+
const injectedValue = (processedEnv.injected || {})[key]
|
|
20
|
+
|
|
21
|
+
if (!encrypted(rawValue)) continue
|
|
22
|
+
if (injectedValue === undefined || injectedValue === null || injectedValue === '') continue
|
|
23
|
+
if (encrypted(injectedValue)) continue
|
|
24
|
+
|
|
25
|
+
result.add(`${injectedValue}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [...result]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = decryptedValues
|
|
@@ -3,8 +3,9 @@ const { which } = require('@dotenvx/tooling')
|
|
|
3
3
|
const execute = require('./../../lib/helpers/execute')
|
|
4
4
|
const { logger } = require('./../../shared/logger')
|
|
5
5
|
const Errors = require('./errors')
|
|
6
|
+
const { createRedactedStreamWriter, redactOutput } = require('./redactOutput')
|
|
6
7
|
|
|
7
|
-
async function executeCommand (commandArgs, env) {
|
|
8
|
+
async function executeCommand (commandArgs, env, sensitiveValues = []) {
|
|
8
9
|
const FORWARD_SIGNAL_GRACE_MS = 1000
|
|
9
10
|
const FORCE_KILL_GRACE_MS = 1000
|
|
10
11
|
const signals = [
|
|
@@ -130,11 +131,27 @@ async function executeCommand (commandArgs, env) {
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
const redactStdout = sensitiveValues.length > 0
|
|
135
|
+
const redactStderr = sensitiveValues.length > 0
|
|
136
|
+
|
|
133
137
|
child = execute.execa(commandArgs[0], commandArgs.slice(1), {
|
|
134
|
-
stdio: 'inherit',
|
|
138
|
+
stdio: ['inherit', redactStdout ? 'pipe' : 'inherit', redactStderr ? 'pipe' : 'inherit'],
|
|
139
|
+
buffer: false,
|
|
135
140
|
env: { ...process.env, ...env }
|
|
136
141
|
})
|
|
137
142
|
|
|
143
|
+
if (redactStdout && child.stdout) {
|
|
144
|
+
const stdoutWriter = createRedactedStreamWriter(process.stdout, sensitiveValues, child.stdout)
|
|
145
|
+
child.stdout.on('data', stdoutWriter.write)
|
|
146
|
+
child.stdout.once('end', stdoutWriter.flush)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (redactStderr && child.stderr) {
|
|
150
|
+
const stderrWriter = createRedactedStreamWriter(process.stderr, sensitiveValues, child.stderr)
|
|
151
|
+
child.stderr.on('data', stderrWriter.write)
|
|
152
|
+
child.stderr.once('end', stderrWriter.flush)
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
process.on('SIGINT', sigintHandler)
|
|
139
156
|
process.on('SIGTERM', sigtermHandler)
|
|
140
157
|
|
|
@@ -157,7 +174,7 @@ async function executeCommand (commandArgs, env) {
|
|
|
157
174
|
if (error.code === 'ENOENT') {
|
|
158
175
|
logger.error(`Unknown command: ${error.command}`)
|
|
159
176
|
} else {
|
|
160
|
-
logger.error(error.message)
|
|
177
|
+
logger.error(redactOutput(error.message, sensitiveValues))
|
|
161
178
|
}
|
|
162
179
|
}
|
|
163
180
|
|
package/src/lib/helpers/fsx.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
+
const Errors = require('./errors')
|
|
2
3
|
|
|
3
4
|
const ENCODING = 'utf8'
|
|
4
5
|
|
|
@@ -19,11 +20,27 @@ function readFileXSync (filepath, encoding = null) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function writeFileXSync (filepath, str) {
|
|
22
|
-
|
|
23
|
+
try {
|
|
24
|
+
return fs.writeFileSync(filepath, str, ENCODING) // utf8 always
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
27
|
+
throw new Errors({ filepath }).fileNotWritable()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
async function writeFileX (filepath, str) {
|
|
26
|
-
|
|
35
|
+
try {
|
|
36
|
+
return await fs.promises.writeFile(filepath, str, ENCODING)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
39
|
+
throw new Errors({ filepath }).fileNotWritable()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
async function exists (filepath) {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const redact = require('./redact')
|
|
2
|
+
const { StringDecoder } = require('string_decoder')
|
|
3
|
+
|
|
4
|
+
function normalizedValues (values) {
|
|
5
|
+
return [...new Set((values || [])
|
|
6
|
+
.filter(value => value !== undefined && value !== null && `${value}`.length > 0)
|
|
7
|
+
.map(value => `${value}`))]
|
|
8
|
+
.sort((a, b) => b.length - a.length)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function redactOutput (value, sensitiveValues) {
|
|
12
|
+
const values = normalizedValues(sensitiveValues)
|
|
13
|
+
if (values.length < 1 || value === undefined || value === null) return value
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map(item => redactOutput(item, values))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
20
|
+
const result = {}
|
|
21
|
+
for (const [key, item] of Object.entries(value)) {
|
|
22
|
+
result[key] = redactOutput(item, values)
|
|
23
|
+
}
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof value !== 'string') return value
|
|
28
|
+
|
|
29
|
+
let result = value
|
|
30
|
+
for (const sensitiveValue of values) {
|
|
31
|
+
result = result.split(sensitiveValue).join(redact(sensitiveValue))
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function partialMatchLength (value, sensitiveValues) {
|
|
37
|
+
let longest = 0
|
|
38
|
+
|
|
39
|
+
for (const sensitiveValue of sensitiveValues) {
|
|
40
|
+
const maxLength = Math.min(value.length, sensitiveValue.length - 1)
|
|
41
|
+
for (let length = maxLength; length > longest; length--) {
|
|
42
|
+
if (sensitiveValue.startsWith(value.slice(-length))) {
|
|
43
|
+
longest = length
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return longest
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function safeBoundary (value, boundary, sensitiveValues) {
|
|
53
|
+
let result = boundary
|
|
54
|
+
let changed = true
|
|
55
|
+
|
|
56
|
+
while (changed) {
|
|
57
|
+
changed = false
|
|
58
|
+
for (const sensitiveValue of sensitiveValues) {
|
|
59
|
+
let index = value.indexOf(sensitiveValue)
|
|
60
|
+
while (index !== -1) {
|
|
61
|
+
const end = index + sensitiveValue.length
|
|
62
|
+
if (index < result && end > result) {
|
|
63
|
+
result = index
|
|
64
|
+
changed = true
|
|
65
|
+
}
|
|
66
|
+
index = value.indexOf(sensitiveValue, index + 1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createRedactedStreamWriter (stream, sensitiveValues, source) {
|
|
75
|
+
const values = normalizedValues(sensitiveValues)
|
|
76
|
+
const decoder = new StringDecoder('utf8')
|
|
77
|
+
let pending = ''
|
|
78
|
+
let waitingForDrain = false
|
|
79
|
+
|
|
80
|
+
const writeToStream = (value) => {
|
|
81
|
+
if (!value) return
|
|
82
|
+
|
|
83
|
+
const canContinue = stream.write(redactOutput(value, values))
|
|
84
|
+
if (!canContinue && source && !waitingForDrain) {
|
|
85
|
+
waitingForDrain = true
|
|
86
|
+
source.pause()
|
|
87
|
+
stream.once('drain', () => {
|
|
88
|
+
waitingForDrain = false
|
|
89
|
+
source.resume()
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const flush = () => {
|
|
95
|
+
pending += decoder.end()
|
|
96
|
+
if (!pending) return
|
|
97
|
+
writeToStream(pending)
|
|
98
|
+
pending = ''
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const write = (chunk) => {
|
|
102
|
+
pending += Buffer.isBuffer(chunk) ? decoder.write(chunk) : `${chunk}`
|
|
103
|
+
|
|
104
|
+
const holdbackLength = partialMatchLength(pending, values)
|
|
105
|
+
let boundary = pending.length - holdbackLength
|
|
106
|
+
boundary = safeBoundary(pending, boundary, values)
|
|
107
|
+
|
|
108
|
+
const output = pending.slice(0, boundary)
|
|
109
|
+
pending = pending.slice(boundary)
|
|
110
|
+
|
|
111
|
+
writeToStream(output)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { write, flush }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
redactOutput,
|
|
119
|
+
createRedactedStreamWriter
|
|
120
|
+
}
|
package/src/lib/main.d.ts
CHANGED
|
@@ -391,9 +391,11 @@ export function get(
|
|
|
391
391
|
* @param directory - current working directory
|
|
392
392
|
* @param envFile - glob pattern to match env files
|
|
393
393
|
* @param excludeEnvFile - glob pattern to exclude env files
|
|
394
|
+
* @param onDirectory - called as each directory is traversed
|
|
394
395
|
*/
|
|
395
396
|
export function ls(
|
|
396
397
|
directory: string,
|
|
397
398
|
envFile: string | string[],
|
|
398
|
-
excludeEnvFile: string | string[]
|
|
399
|
-
|
|
399
|
+
excludeEnvFile: string | string[],
|
|
400
|
+
onDirectory?: (directory: string) => void
|
|
401
|
+
): Promise<string[]>;
|
package/src/lib/main.js
CHANGED
|
@@ -389,8 +389,8 @@ const get = async function (key, options = {}) {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/** @type {import('./main').ls} */
|
|
392
|
-
const ls = function (directory, envFile, excludeEnvFile) {
|
|
393
|
-
return lsResolver({ directory, envFile, excludeEnvFile })
|
|
392
|
+
const ls = async function (directory, envFile, excludeEnvFile, onDirectory) {
|
|
393
|
+
return await lsResolver({ directory, envFile, excludeEnvFile, onDirectory })
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
function resolveNoArmor (options = {}) {
|
package/src/lib/resolvers/ls.js
CHANGED
|
@@ -2,6 +2,14 @@ const { Fdir } = require('@dotenvx/tooling')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const { match } = require('@dotenvx/primitives')
|
|
4
4
|
|
|
5
|
+
const DEFAULT_EXCLUDED_DIRECTORY_EXTENSIONS = new Set([
|
|
6
|
+
'.app',
|
|
7
|
+
'.key',
|
|
8
|
+
'.numbers',
|
|
9
|
+
'.pages',
|
|
10
|
+
'.photoslibrary'
|
|
11
|
+
])
|
|
12
|
+
|
|
5
13
|
function patternsFor (value) {
|
|
6
14
|
if (!Array.isArray(value)) {
|
|
7
15
|
return [`**/${value}`]
|
|
@@ -18,24 +26,44 @@ function excludePatternsFor (value) {
|
|
|
18
26
|
return value.map(part => `**/${part}`)
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
function
|
|
29
|
+
function crawler (options = {}) {
|
|
22
30
|
const ignore = ['node_modules/**', '**/node_modules/**', '.git/**', '**/.git/**']
|
|
23
31
|
const cwd = path.resolve(options.directory || './')
|
|
24
32
|
const envFile = options.envFile || ['.env*']
|
|
25
33
|
const excludeEnvFile = options.excludeEnvFile || []
|
|
26
34
|
const excludePatterns = excludePatternsFor(excludeEnvFile)
|
|
27
|
-
|
|
35
|
+
let excludes
|
|
36
|
+
if (excludePatterns.length > 0) {
|
|
37
|
+
excludes = ignore.concat(excludePatterns)
|
|
38
|
+
} else {
|
|
39
|
+
excludes = ignore
|
|
40
|
+
}
|
|
28
41
|
const exclude = match(excludes, { dot: true })
|
|
29
42
|
const include = match(patternsFor(envFile), {
|
|
30
43
|
dot: true,
|
|
31
44
|
ignore: excludes
|
|
32
45
|
})
|
|
46
|
+
const onDirectory = options.onDirectory || (() => {})
|
|
33
47
|
|
|
34
48
|
return new Fdir()
|
|
35
49
|
.withRelativePaths()
|
|
50
|
+
.exclude((dirname, directory) => {
|
|
51
|
+
if (dirname === 'node_modules' || dirname === '.git') return true
|
|
52
|
+
if (DEFAULT_EXCLUDED_DIRECTORY_EXTENSIONS.has(path.extname(dirname).toLowerCase())) return true
|
|
53
|
+
|
|
54
|
+
onDirectory(path.relative(cwd, directory) || '.')
|
|
55
|
+
return false
|
|
56
|
+
})
|
|
36
57
|
.filter((filepath) => !exclude(filepath) && include(filepath))
|
|
37
58
|
.crawl(cwd)
|
|
38
|
-
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function ls (options = {}) {
|
|
62
|
+
return await crawler(options).withPromise()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ls.sync = function (options = {}) {
|
|
66
|
+
return crawler(options).sync()
|
|
39
67
|
}
|
|
40
68
|
|
|
41
69
|
module.exports = ls
|