@kaynooo/env-tools 0.5.0 → 0.7.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/README.md +5 -3
- package/dist/ev.js +788 -161
- package/dist/redact.js +411 -108
- package/package.json +1 -1
- package/src/dotenv.ts +16 -4
package/dist/redact.js
CHANGED
|
@@ -276,12 +276,27 @@ function le(i = {}) {
|
|
|
276
276
|
var t = le();
|
|
277
277
|
|
|
278
278
|
// ../kit/src/style.ts
|
|
279
|
+
var MARKS = {
|
|
280
|
+
ok: { symbol: "\u2713", word: "ok:", paint: (colors) => colors.green },
|
|
281
|
+
warn: { symbol: "!", word: "warning:", paint: (colors) => colors.yellow },
|
|
282
|
+
problem: { symbol: "\u2717", word: "error:", paint: (colors) => colors.red },
|
|
283
|
+
note: { symbol: "\xB7", word: "note:", paint: (colors) => colors.dim }
|
|
284
|
+
};
|
|
279
285
|
function takesControlCodes(stream, env = process2.env) {
|
|
280
286
|
return stream.isTTY === true && env.NO_COLOR === undefined && env.NODE_DISABLE_COLORS === undefined && env.TERM !== "dumb";
|
|
281
287
|
}
|
|
282
288
|
function paint(stream, env = process2.env) {
|
|
283
289
|
return le({ stream: { isTTY: takesControlCodes(stream, env) }, env });
|
|
284
290
|
}
|
|
291
|
+
function marked(mark, text, stream, detail, env = process2.env) {
|
|
292
|
+
const colors = paint(stream, env);
|
|
293
|
+
const { symbol, word, paint: tint } = MARKS[mark];
|
|
294
|
+
const head = takesControlCodes(stream, env) ? tint(colors)(symbol) : word;
|
|
295
|
+
return `${head} ${text}${detail === undefined ? "" : ` ${colors.dim(detail)}`}`;
|
|
296
|
+
}
|
|
297
|
+
function ok(text, detail) {
|
|
298
|
+
console.log(marked("ok", text, process2.stdout, detail));
|
|
299
|
+
}
|
|
285
300
|
var STATUSES = {
|
|
286
301
|
passed: { symbol: "\u2713", words: ["passed", "success", "succeeded", "ok"], paint: (colors) => colors.green },
|
|
287
302
|
failed: { symbol: "\u2717", words: ["failed", "failure", "error", "errored"], paint: (colors) => colors.red },
|
|
@@ -343,6 +358,26 @@ function printJson(value) {
|
|
|
343
358
|
console.log(JSON.stringify(value, undefined, 2));
|
|
344
359
|
}
|
|
345
360
|
|
|
361
|
+
// ../kit/src/text.ts
|
|
362
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
363
|
+
function distance(a, b) {
|
|
364
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
365
|
+
for (const [i, left] of [...a].entries()) {
|
|
366
|
+
const current = [i + 1];
|
|
367
|
+
for (const [j, right] of [...b].entries())
|
|
368
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
369
|
+
previous = current;
|
|
370
|
+
}
|
|
371
|
+
return previous[b.length] ?? 0;
|
|
372
|
+
}
|
|
373
|
+
function closest(name, names) {
|
|
374
|
+
const started = names.find((known) => known.startsWith(name));
|
|
375
|
+
if (started !== undefined)
|
|
376
|
+
return started;
|
|
377
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
378
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
346
381
|
// ../kit/src/args.ts
|
|
347
382
|
function jsonOption(holds) {
|
|
348
383
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
@@ -362,8 +397,8 @@ function fail(message, code = 2, hint) {
|
|
|
362
397
|
}
|
|
363
398
|
|
|
364
399
|
class UsageError extends Exit {
|
|
365
|
-
constructor(message) {
|
|
366
|
-
super(message, 2);
|
|
400
|
+
constructor(message, hint) {
|
|
401
|
+
super(message, 2, hint);
|
|
367
402
|
}
|
|
368
403
|
}
|
|
369
404
|
function badUsage(message) {
|
|
@@ -386,6 +421,7 @@ var completion = { active: false };
|
|
|
386
421
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
387
422
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
388
423
|
var FULL_STOP = /\.$/;
|
|
424
|
+
var DASHES = /^-+/;
|
|
389
425
|
function usageLine(usage, colors = paint(process4.stdout)) {
|
|
390
426
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
391
427
|
}
|
|
@@ -422,10 +458,10 @@ function optionRows(options, colors = paint(process4.stdout)) {
|
|
|
422
458
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
423
459
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
424
460
|
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
425
|
-
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
|
|
461
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
|
|
426
462
|
});
|
|
427
463
|
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
428
|
-
return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
464
|
+
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
429
465
|
}
|
|
430
466
|
var OUTPUT_OPTIONS = {
|
|
431
467
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
@@ -457,7 +493,8 @@ function parse(args, usage, options, settings = {}) {
|
|
|
457
493
|
try {
|
|
458
494
|
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
459
495
|
} catch (error) {
|
|
460
|
-
|
|
496
|
+
const message = parseError(error);
|
|
497
|
+
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
461
498
|
}
|
|
462
499
|
const values = parsed.values;
|
|
463
500
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
@@ -500,6 +537,22 @@ function parseError(error) {
|
|
|
500
537
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
501
538
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
502
539
|
}
|
|
540
|
+
var UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
|
|
541
|
+
function nearestOption(message, options, allowNegative) {
|
|
542
|
+
const typed = UNKNOWN_OPTION.exec(message)?.[1];
|
|
543
|
+
if (typed === undefined)
|
|
544
|
+
return;
|
|
545
|
+
const word = typed.replace(DASHES, "");
|
|
546
|
+
if (word.length < 2 && !typed.startsWith("--"))
|
|
547
|
+
return;
|
|
548
|
+
const known = Object.entries(options).flatMap(([name, option]) => [
|
|
549
|
+
...option.short === undefined ? [] : [`-${option.short}`],
|
|
550
|
+
`--${name}`,
|
|
551
|
+
...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
|
|
552
|
+
]);
|
|
553
|
+
const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
|
|
554
|
+
return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
|
|
555
|
+
}
|
|
503
556
|
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
504
557
|
if (path === "-") {
|
|
505
558
|
if (process4.stdin.isTTY)
|
|
@@ -511,35 +564,26 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
511
564
|
fail(`no file ${path}`);
|
|
512
565
|
return file.text();
|
|
513
566
|
}
|
|
567
|
+
// ../kit/src/progress.ts
|
|
568
|
+
var ESC = String.fromCharCode(27);
|
|
569
|
+
var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
|
|
570
|
+
var RESET_STYLE = `${ESC}[0m`;
|
|
571
|
+
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
514
572
|
// ../kit/src/cli.ts
|
|
515
573
|
import process5 from "process";
|
|
516
|
-
|
|
517
|
-
// ../kit/src/text.ts
|
|
518
|
-
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
519
|
-
function distance(a, b) {
|
|
520
|
-
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
521
|
-
for (const [i, left] of [...a].entries()) {
|
|
522
|
-
const current = [i + 1];
|
|
523
|
-
for (const [j, right] of [...b].entries())
|
|
524
|
-
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
525
|
-
previous = current;
|
|
526
|
-
}
|
|
527
|
-
return previous[b.length] ?? 0;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ../kit/src/cli.ts
|
|
531
574
|
var LEVEL_OPTIONS = {
|
|
532
575
|
...OUTPUT_OPTIONS,
|
|
533
576
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
534
577
|
};
|
|
535
578
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
536
|
-
function commandRows(commands, colors) {
|
|
579
|
+
async function commandRows(commands, colors) {
|
|
537
580
|
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
538
581
|
const groups = new Map([["Commands:", []]]);
|
|
539
|
-
|
|
582
|
+
const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
|
|
583
|
+
for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
|
|
540
584
|
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
541
585
|
const rows = groups.get(heading) ?? [];
|
|
542
|
-
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(
|
|
586
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
543
587
|
groups.set(heading, rows);
|
|
544
588
|
}
|
|
545
589
|
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
@@ -559,28 +603,38 @@ function takeOutputFlags(argv) {
|
|
|
559
603
|
}
|
|
560
604
|
return argv.slice(index);
|
|
561
605
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
564
|
-
if (started !== undefined)
|
|
565
|
-
return started;
|
|
566
|
-
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
567
|
-
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
606
|
+
async function resolveSummary(summary) {
|
|
607
|
+
return typeof summary === "string" ? summary : summary();
|
|
568
608
|
}
|
|
569
|
-
|
|
609
|
+
function scopeOf(commands, extra, aliases) {
|
|
610
|
+
return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
|
|
611
|
+
}
|
|
612
|
+
async function expand(scope, words) {
|
|
613
|
+
if (scope.lazy.length === 0)
|
|
614
|
+
return scope;
|
|
615
|
+
const commands = { ...scope.commands };
|
|
616
|
+
for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
|
|
617
|
+
for (const [name, command] of Object.entries(added))
|
|
618
|
+
commands[name] ??= command;
|
|
619
|
+
}
|
|
620
|
+
return { ...scope, commands, lazy: [] };
|
|
621
|
+
}
|
|
622
|
+
async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
|
|
570
623
|
const colors = paint(stream);
|
|
571
624
|
const prefix = path.join(" ");
|
|
625
|
+
const summary = await resolveSummary(lazySummary);
|
|
572
626
|
if (wantsHelpJson())
|
|
573
627
|
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
574
628
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
575
629
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
576
630
|
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
577
631
|
return [
|
|
578
|
-
header(colors),
|
|
632
|
+
header(colors, summary),
|
|
579
633
|
"",
|
|
580
634
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
581
635
|
...replacesRows(replaces, colors),
|
|
582
636
|
"",
|
|
583
|
-
...commandRows(commands, colors),
|
|
637
|
+
...await commandRows(commands, colors),
|
|
584
638
|
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
585
639
|
"",
|
|
586
640
|
colors.bold("Options:"),
|
|
@@ -626,11 +680,11 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
626
680
|
}
|
|
627
681
|
const [name, ...args] = argv;
|
|
628
682
|
if (name === undefined && level.path.length > 1) {
|
|
629
|
-
console.error(await help(level, process5.stderr));
|
|
683
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stderr));
|
|
630
684
|
return 2;
|
|
631
685
|
}
|
|
632
686
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
633
|
-
console.log(await help(level, process5.stdout));
|
|
687
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stdout));
|
|
634
688
|
return 0;
|
|
635
689
|
}
|
|
636
690
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -639,6 +693,10 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
639
693
|
}
|
|
640
694
|
let command = level.commands[level.aliases?.[name] ?? name];
|
|
641
695
|
let rest = args;
|
|
696
|
+
if (command === undefined && level.lazy.length > 0) {
|
|
697
|
+
level = await expand(level, [...level.path.slice(1), ...argv]);
|
|
698
|
+
command = level.commands[level.aliases?.[name] ?? name];
|
|
699
|
+
}
|
|
642
700
|
if (command === undefined && level.defaultCommand !== undefined) {
|
|
643
701
|
command = level.commands[level.defaultCommand];
|
|
644
702
|
rest = argv;
|
|
@@ -650,7 +708,7 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
650
708
|
}
|
|
651
709
|
if ("commands" in command) {
|
|
652
710
|
const { summary } = command;
|
|
653
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${
|
|
711
|
+
return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
|
|
654
712
|
}
|
|
655
713
|
try {
|
|
656
714
|
return await command.run(rest);
|
|
@@ -675,18 +733,24 @@ async function optionsOf(run) {
|
|
|
675
733
|
async function leafCandidates(run, words) {
|
|
676
734
|
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
677
735
|
return [FILES];
|
|
678
|
-
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
736
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
679
737
|
}
|
|
680
|
-
async function candidates(
|
|
738
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
681
739
|
const [word = "", ...rest] = words;
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
740
|
+
const listing = rest.length === 0 && !word.startsWith("-");
|
|
741
|
+
let { commands, aliases } = scope;
|
|
742
|
+
let command = commands[aliases?.[word] ?? word];
|
|
743
|
+
if (listing || command === undefined) {
|
|
744
|
+
({ commands, aliases } = await expand(scope, [...before, ...words]));
|
|
745
|
+
command = commands[aliases?.[word] ?? word];
|
|
746
|
+
}
|
|
747
|
+
if (listing)
|
|
748
|
+
return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
|
|
685
749
|
if (command === undefined) {
|
|
686
750
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
687
751
|
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
688
752
|
}
|
|
689
|
-
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
753
|
+
return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
|
|
690
754
|
}
|
|
691
755
|
async function printCandidates(found) {
|
|
692
756
|
for (const line of await found)
|
|
@@ -695,8 +759,8 @@ async function printCandidates(found) {
|
|
|
695
759
|
}
|
|
696
760
|
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
697
761
|
if (argv[0] === COMPLETE)
|
|
698
|
-
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
699
|
-
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${
|
|
762
|
+
return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
|
|
763
|
+
return dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${about}`, summary: cli.about, ...scopeOf(cli.commands, cli.extraCommands, cli.aliases), defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
|
|
700
764
|
}
|
|
701
765
|
async function runMain({ name, version, run, redact }, argv = process5.argv.slice(2)) {
|
|
702
766
|
if (argv[0] === COMPLETE)
|
|
@@ -711,11 +775,21 @@ async function runMain({ name, version, run, redact }, argv = process5.argv.slic
|
|
|
711
775
|
return reportThrown(name, error, redact, wantsJson(argv));
|
|
712
776
|
}
|
|
713
777
|
}
|
|
778
|
+
// ../kit/src/logs.ts
|
|
779
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
780
|
+
// ../kit/src/secrets.ts
|
|
781
|
+
var SECRET_PART = /token|secret|passw|credential|private|dsn|apikey|accesskey|key$/i;
|
|
782
|
+
var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
|
|
783
|
+
var WORD_BREAKS = /[^a-zA-Z\d]+|(?<=[a-z\d])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/;
|
|
784
|
+
function isSecretName(name) {
|
|
785
|
+
return SECRET_PART.test(name) || name.split(WORD_BREAKS).some((word) => SECRET_WORDS.has(word.toLowerCase()));
|
|
786
|
+
}
|
|
714
787
|
// package.json
|
|
715
|
-
var version = "0.
|
|
788
|
+
var version = "0.7.0";
|
|
716
789
|
|
|
717
790
|
// src/files.ts
|
|
718
|
-
import { readdir } from "fs/promises";
|
|
791
|
+
import { chmod, chown, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
792
|
+
import { basename, dirname, join } from "path";
|
|
719
793
|
|
|
720
794
|
// src/dotenv.ts
|
|
721
795
|
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
@@ -723,7 +797,9 @@ var EXPORTED = /^\s*export\s/;
|
|
|
723
797
|
var INLINE_COMMENT = /\s+#.*$/;
|
|
724
798
|
var CARRIAGE_RETURN = /\r$/;
|
|
725
799
|
var ESCAPES = { n: `
|
|
726
|
-
`, r: "\r", t: "\t", '"': '"', "\\": "\\" };
|
|
800
|
+
`, r: "\r", t: "\t", '"': '"', "\\": "\\", $: "$" };
|
|
801
|
+
var LINE_BREAK = /[\r\n]/;
|
|
802
|
+
var RAW_ONLY = /["\\]/;
|
|
727
803
|
var NEEDS_QUOTES = /[\s#"'\\$`]/;
|
|
728
804
|
function closingQuote(text, quote) {
|
|
729
805
|
for (let index = 0;index < text.length; index++) {
|
|
@@ -734,6 +810,9 @@ function closingQuote(text, quote) {
|
|
|
734
810
|
}
|
|
735
811
|
return -1;
|
|
736
812
|
}
|
|
813
|
+
function unescapeDollar(text) {
|
|
814
|
+
return text.replaceAll("\\$", "$");
|
|
815
|
+
}
|
|
737
816
|
function unescapeDouble(text) {
|
|
738
817
|
let out = "";
|
|
739
818
|
for (let index = 0;index < text.length; index++) {
|
|
@@ -762,7 +841,7 @@ function parseDotenv(text) {
|
|
|
762
841
|
const start = rest.trimStart();
|
|
763
842
|
const quote = start[0];
|
|
764
843
|
if (quote !== '"' && quote !== "'" && quote !== "`") {
|
|
765
|
-
lines.push({ raw: line, key, value: start.replace(INLINE_COMMENT, "").trim() });
|
|
844
|
+
lines.push({ raw: line, key, value: unescapeDollar(start.replace(INLINE_COMMENT, "").trim()) });
|
|
766
845
|
continue;
|
|
767
846
|
}
|
|
768
847
|
let body = start.slice(1);
|
|
@@ -777,7 +856,7 @@ ${physical[index] ?? ""}`;
|
|
|
777
856
|
end = closingQuote(body, quote);
|
|
778
857
|
}
|
|
779
858
|
const inner = end === -1 ? body : body.slice(0, end);
|
|
780
|
-
lines.push({ raw, key, value: quote === '"' ? unescapeDouble(inner) : inner });
|
|
859
|
+
lines.push({ raw, key, value: quote === '"' ? unescapeDouble(inner) : unescapeDollar(inner) });
|
|
781
860
|
}
|
|
782
861
|
return { lines };
|
|
783
862
|
}
|
|
@@ -796,8 +875,10 @@ function entries(file) {
|
|
|
796
875
|
function formatAssignment(key, value) {
|
|
797
876
|
if (value === "" || !NEEDS_QUOTES.test(value))
|
|
798
877
|
return `${key}=${value}`;
|
|
878
|
+
if (RAW_ONLY.test(value) && !value.includes("'") && !LINE_BREAK.test(value))
|
|
879
|
+
return `${key}='${value.replaceAll("$", "\\$")}'`;
|
|
799
880
|
const escaped = value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("\r", "\\r").replaceAll(`
|
|
800
|
-
`, "\\n");
|
|
881
|
+
`, "\\n").replaceAll("$", "\\$");
|
|
801
882
|
return `${key}="${escaped}"`;
|
|
802
883
|
}
|
|
803
884
|
function setLine(file, key, value, raw) {
|
|
@@ -827,51 +908,137 @@ function lastLine(file, key) {
|
|
|
827
908
|
}
|
|
828
909
|
|
|
829
910
|
// src/mask.ts
|
|
830
|
-
var MIN_KNOWN_LENGTH =
|
|
831
|
-
var
|
|
832
|
-
var
|
|
911
|
+
var MIN_KNOWN_LENGTH = 4;
|
|
912
|
+
var KEY_MASK = "*** private key redacted ***";
|
|
913
|
+
var PLAIN_VALUE = /^(?:true|false|null|none|nil|yes|no|on|off|undefined)$/i;
|
|
914
|
+
var PLACEHOLDER = /^[$%{<(*/]/;
|
|
915
|
+
var URL_PASSWORD = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]*:([^\s'"]{1,1024})@/dgi;
|
|
916
|
+
var PORT = /^\d{1,5}$/;
|
|
833
917
|
var ESCAPE2 = /[.*+?^${}()|[\]\\]/g;
|
|
918
|
+
function isPlaceholder(value) {
|
|
919
|
+
return PLACEHOLDER.test(value) || PLAIN_VALUE.test(value);
|
|
920
|
+
}
|
|
834
921
|
var TOKEN_FORMATS = [
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
922
|
+
{ kind: "GitHub token", pattern: /\b(?:gh[pousr]_|github_pat_)\w{20,}/dg },
|
|
923
|
+
{ kind: "GitLab token", pattern: /\bglpat-[\w-]{20,}/dg },
|
|
924
|
+
{ kind: "API key", pattern: /\bsk-[\w-]{20,}/dg },
|
|
925
|
+
{ kind: "Slack token", pattern: /\bx(?:ox[abprs]|app)-[\w-]{10,}/dg },
|
|
926
|
+
{ kind: "JWT", pattern: /\beyJ[\w-]+\.[\w-]+\.[\w-]+/dg },
|
|
927
|
+
{ kind: "AWS access key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/dg },
|
|
928
|
+
{ kind: "npm token", pattern: /\bnpm_[A-Za-z0-9]{30,}/dg },
|
|
929
|
+
{ kind: "Stripe key", pattern: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{10,}/dg },
|
|
930
|
+
{ kind: "Google API key", pattern: /\bAIza[\w-]{35}/dg },
|
|
931
|
+
{ kind: "SendGrid key", pattern: /\bSG\.[\w-]{16,}\.[\w-]{16,}/dg },
|
|
932
|
+
{ kind: "Authorization header", pattern: /\bAuthorization["']?\s*[:=]\s*["']?(?:Bearer|Basic|Token|Digest)\s+([^\s"',;]{8,})/dgi, skip: isPlaceholder },
|
|
933
|
+
{ kind: "Bearer token", pattern: /\bBearer\s+((?=[\w.~+/=-]*\d)[\w.~+/=-]{20,})/dg },
|
|
934
|
+
{ kind: "password", pattern: /(?<![\w-])(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?=([^\s"'&;,]{4,})/dgi, skip: isPlaceholder },
|
|
935
|
+
{ kind: "password", pattern: /(?<![\w-])["']?(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?["']?\s*[:=]\s*(["'])((?:\\.|(?!\1)[^\\\n]){4,})\1/dgi, skip: isPlaceholder }
|
|
840
936
|
];
|
|
841
|
-
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY
|
|
842
|
-
var PEM_END = /-----END [A-Z ]*PRIVATE KEY
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
937
|
+
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
938
|
+
var PEM_END = /-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
939
|
+
var PEM_BODY = /(?<=-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)(?:(?!-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)[\s\S])+/dg;
|
|
940
|
+
var PEM_BEGIN_ALL = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
941
|
+
var URL_PARTS = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
942
|
+
var PATH_SEGMENT = /[^/]+/g;
|
|
943
|
+
var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
|
|
944
|
+
var WEBHOOK_FORMATS = [
|
|
945
|
+
["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
|
|
946
|
+
["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
|
|
947
|
+
["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
|
|
948
|
+
["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
|
|
949
|
+
];
|
|
950
|
+
var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
|
|
846
951
|
function hint(value) {
|
|
847
952
|
const shown = Math.min(3, Math.floor(value.length / 4));
|
|
848
953
|
return `${value.slice(0, shown)}***(${value.length})`;
|
|
849
954
|
}
|
|
955
|
+
function forms(value) {
|
|
956
|
+
return [...new Set([value, encodeURIComponent(value), JSON.stringify(value).slice(1, -1)])];
|
|
957
|
+
}
|
|
850
958
|
function knownSecrets(pairs) {
|
|
851
959
|
const keys = new Map;
|
|
852
960
|
for (const [key, value] of pairs) {
|
|
853
|
-
if (value.length
|
|
854
|
-
|
|
961
|
+
if (value.length < MIN_KNOWN_LENGTH || PLAIN_VALUE.test(value))
|
|
962
|
+
continue;
|
|
963
|
+
for (const form of forms(value)) {
|
|
964
|
+
if (!keys.has(form))
|
|
965
|
+
keys.set(form, key);
|
|
966
|
+
}
|
|
855
967
|
}
|
|
856
968
|
if (keys.size === 0)
|
|
857
969
|
return { pattern: undefined, keys };
|
|
858
970
|
const alternatives = [...keys.keys()].sort((a, b) => b.length - a.length).map((value) => value.replace(ESCAPE2, "\\$&"));
|
|
859
971
|
return { pattern: new RegExp(`(?<!\\w)(?:${alternatives.join("|")})(?!\\w)`, "g"), keys };
|
|
860
972
|
}
|
|
973
|
+
function passwordSpans(text) {
|
|
974
|
+
const spans = [];
|
|
975
|
+
for (const url of text.matchAll(URL_PASSWORD)) {
|
|
976
|
+
const [start, end] = url.indices?.[1] ?? [];
|
|
977
|
+
const password = url[1] ?? "";
|
|
978
|
+
const slash = password.indexOf("/");
|
|
979
|
+
if (start !== undefined && end !== undefined && !(slash !== -1 && PORT.test(password.slice(0, slash))))
|
|
980
|
+
spans.push([start, end]);
|
|
981
|
+
}
|
|
982
|
+
return spans;
|
|
983
|
+
}
|
|
861
984
|
function urlPasswords(text) {
|
|
862
|
-
return
|
|
985
|
+
return passwordSpans(text).map(([start, end]) => text.slice(start, end));
|
|
986
|
+
}
|
|
987
|
+
function urlTokens(line) {
|
|
988
|
+
const found = [];
|
|
989
|
+
const add = (kind, start, value) => {
|
|
990
|
+
found.push({ start, end: start + value.length, kind, mask: hint(value) });
|
|
991
|
+
};
|
|
992
|
+
for (const url of line.matchAll(URL_PARTS)) {
|
|
993
|
+
const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
|
|
994
|
+
const authorityStart = url.indices?.[1]?.[0];
|
|
995
|
+
const pathStart = url.indices?.[2]?.[0];
|
|
996
|
+
const queryStart = url.indices?.[3]?.[0];
|
|
997
|
+
if (authorityStart === undefined || pathStart === undefined)
|
|
998
|
+
continue;
|
|
999
|
+
const hostAt = authority.lastIndexOf("@") + 1;
|
|
1000
|
+
const target = authority.slice(hostAt) + path;
|
|
1001
|
+
for (const [kind, pattern] of WEBHOOK_FORMATS) {
|
|
1002
|
+
const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
|
|
1003
|
+
if (start !== undefined && end !== undefined)
|
|
1004
|
+
add(kind, authorityStart + hostAt + start, target.slice(start, end));
|
|
1005
|
+
}
|
|
1006
|
+
for (const segment of path.matchAll(PATH_SEGMENT)) {
|
|
1007
|
+
if (OPAQUE_SEGMENT.test(segment[0]))
|
|
1008
|
+
add("URL token", pathStart + segment.index, segment[0]);
|
|
1009
|
+
}
|
|
1010
|
+
if (query === undefined || queryStart === undefined)
|
|
1011
|
+
continue;
|
|
1012
|
+
let offset = 0;
|
|
1013
|
+
for (const param of query.split("&")) {
|
|
1014
|
+
const equals = param.indexOf("=");
|
|
1015
|
+
if (equals > 0 && equals < param.length - 1 && SECRET_PARAM.test(param.slice(0, equals)))
|
|
1016
|
+
add("URL token", queryStart + offset + equals + 1, param.slice(equals + 1));
|
|
1017
|
+
offset += param.length + 1;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return found.filter((match) => match.end > match.start);
|
|
863
1021
|
}
|
|
864
1022
|
function findSecrets(line, known) {
|
|
865
1023
|
const found = [];
|
|
866
|
-
for (const
|
|
867
|
-
|
|
1024
|
+
for (const [start, end] of passwordSpans(line))
|
|
1025
|
+
found.push({ start, end, kind: "URL password", mask: "***" });
|
|
1026
|
+
for (const body of line.matchAll(PEM_BODY)) {
|
|
1027
|
+
const [start, end] = body.indices?.[0] ?? [];
|
|
868
1028
|
if (start !== undefined && end !== undefined)
|
|
869
|
-
found.push({ start, end, kind: "
|
|
1029
|
+
found.push({ start, end, kind: "private key", mask: KEY_MASK });
|
|
870
1030
|
}
|
|
871
|
-
for (const
|
|
872
|
-
for (const token of line.matchAll(pattern))
|
|
873
|
-
|
|
1031
|
+
for (const { kind, pattern, skip } of TOKEN_FORMATS) {
|
|
1032
|
+
for (const token of line.matchAll(pattern)) {
|
|
1033
|
+
const [start, end] = token.indices?.at(-1) ?? [];
|
|
1034
|
+
if (start === undefined || end === undefined)
|
|
1035
|
+
continue;
|
|
1036
|
+
const value = line.slice(start, end);
|
|
1037
|
+
if (!skip?.(value))
|
|
1038
|
+
found.push({ start, end, kind, mask: hint(value) });
|
|
1039
|
+
}
|
|
874
1040
|
}
|
|
1041
|
+
found.push(...urlTokens(line));
|
|
875
1042
|
if (known?.pattern !== undefined) {
|
|
876
1043
|
for (const value of line.matchAll(known.pattern))
|
|
877
1044
|
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
@@ -884,6 +1051,13 @@ function findSecrets(line, known) {
|
|
|
884
1051
|
}
|
|
885
1052
|
return kept;
|
|
886
1053
|
}
|
|
1054
|
+
function openKeyBody(line) {
|
|
1055
|
+
const last = [...line.matchAll(PEM_BEGIN_ALL)].at(-1);
|
|
1056
|
+
if (last === undefined)
|
|
1057
|
+
return;
|
|
1058
|
+
const after = line.slice(last.index + last[0].length);
|
|
1059
|
+
return PEM_END.test(after) ? undefined : after.trim();
|
|
1060
|
+
}
|
|
887
1061
|
function maskLine(line, known) {
|
|
888
1062
|
let out = "";
|
|
889
1063
|
let at = 0;
|
|
@@ -893,18 +1067,38 @@ function maskLine(line, known) {
|
|
|
893
1067
|
}
|
|
894
1068
|
return out + line.slice(at);
|
|
895
1069
|
}
|
|
896
|
-
function maskValue(key, value) {
|
|
1070
|
+
function maskValue(key, value, known) {
|
|
897
1071
|
if (value === "")
|
|
898
1072
|
return value;
|
|
899
|
-
return
|
|
1073
|
+
return isSecretName(key) || PEM_BEGIN.test(value) ? hint(value) : maskLine(value, known);
|
|
900
1074
|
}
|
|
901
1075
|
|
|
902
1076
|
// src/files.ts
|
|
903
|
-
var ENV_FILE =
|
|
1077
|
+
var ENV_FILE = /^(?:\.env(?:\..+)?|.+\.env|\.envrc)$/;
|
|
904
1078
|
var TEMPLATE = /\.(?:example|sample|template|dist|defaults)$/;
|
|
1079
|
+
var KNOWN_DEPTH = 3;
|
|
1080
|
+
var SKIPPED = new Set(["node_modules", ".git", "vendor"]);
|
|
1081
|
+
var LOCK_WAIT_MS = 5000;
|
|
1082
|
+
var LOCK_STALE_MS = 30000;
|
|
1083
|
+
async function isFileEntry(entry, path) {
|
|
1084
|
+
if (entry.isFile())
|
|
1085
|
+
return true;
|
|
1086
|
+
if (!entry.isSymbolicLink())
|
|
1087
|
+
return false;
|
|
1088
|
+
try {
|
|
1089
|
+
return (await stat(path)).isFile();
|
|
1090
|
+
} catch {
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
905
1094
|
async function envFiles(folder = process.cwd()) {
|
|
906
1095
|
const found = await readdir(folder, { withFileTypes: true });
|
|
907
|
-
|
|
1096
|
+
const names = [];
|
|
1097
|
+
for (const entry of found) {
|
|
1098
|
+
if (ENV_FILE.test(entry.name) && await isFileEntry(entry, join(folder, entry.name)))
|
|
1099
|
+
names.push(entry.name);
|
|
1100
|
+
}
|
|
1101
|
+
return names.sort();
|
|
908
1102
|
}
|
|
909
1103
|
async function readEnv(path) {
|
|
910
1104
|
const file = Bun.file(path);
|
|
@@ -912,18 +1106,109 @@ async function readEnv(path) {
|
|
|
912
1106
|
fail(`${path} does not exist`);
|
|
913
1107
|
return parseDotenv(await file.text());
|
|
914
1108
|
}
|
|
1109
|
+
async function writeAtomic(path, text) {
|
|
1110
|
+
let target = path;
|
|
1111
|
+
let mode = 384;
|
|
1112
|
+
let owner;
|
|
1113
|
+
try {
|
|
1114
|
+
target = await realpath(path);
|
|
1115
|
+
const current = await stat(target);
|
|
1116
|
+
mode = current.mode & 511;
|
|
1117
|
+
owner = current;
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
if (error.code !== "ENOENT")
|
|
1120
|
+
throw error;
|
|
1121
|
+
}
|
|
1122
|
+
const temp = join(dirname(target), `.${basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1123
|
+
try {
|
|
1124
|
+
await writeFile(temp, text, { mode: 384, flag: "wx" });
|
|
1125
|
+
if (owner !== undefined)
|
|
1126
|
+
await keepOwner(temp, owner);
|
|
1127
|
+
await chmod(temp, mode);
|
|
1128
|
+
await rename(temp, target);
|
|
1129
|
+
} finally {
|
|
1130
|
+
await rm(temp, { force: true });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async function keepOwner(path, owner) {
|
|
1134
|
+
try {
|
|
1135
|
+
await chown(path, owner.uid, owner.gid);
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (error.code !== "EPERM")
|
|
1138
|
+
throw error;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
915
1141
|
async function writeEnv(path, file) {
|
|
916
|
-
await
|
|
1142
|
+
await writeAtomic(path, serializeDotenv(file));
|
|
917
1143
|
}
|
|
918
|
-
async function
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
for (const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1144
|
+
async function withLocks(paths, fn) {
|
|
1145
|
+
const held = [];
|
|
1146
|
+
try {
|
|
1147
|
+
for (const path of [...new Set(paths)].sort())
|
|
1148
|
+
held.push(await acquire(path));
|
|
1149
|
+
return await fn();
|
|
1150
|
+
} finally {
|
|
1151
|
+
for (const lock of held)
|
|
1152
|
+
await rm(lock, { force: true });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async function acquire(path) {
|
|
1156
|
+
let real = path;
|
|
1157
|
+
try {
|
|
1158
|
+
real = await realpath(path);
|
|
1159
|
+
} catch {}
|
|
1160
|
+
const lock = join(dirname(real), `.${basename(real)}.lock`);
|
|
1161
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1162
|
+
while (true) {
|
|
1163
|
+
try {
|
|
1164
|
+
await writeFile(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
1165
|
+
return lock;
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
if (error.code !== "EEXIST")
|
|
1168
|
+
throw error;
|
|
926
1169
|
}
|
|
1170
|
+
try {
|
|
1171
|
+
if (Date.now() - (await stat(lock)).mtimeMs > LOCK_STALE_MS)
|
|
1172
|
+
await rm(lock, { force: true });
|
|
1173
|
+
} catch {}
|
|
1174
|
+
if (Date.now() > deadline)
|
|
1175
|
+
fail(`${path} is locked by another ev run (${lock})`, 1);
|
|
1176
|
+
await Bun.sleep(25);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function secretPairs(file) {
|
|
1180
|
+
const pairs = [];
|
|
1181
|
+
for (const [key, value] of entries(file)) {
|
|
1182
|
+
if (isSecretName(key))
|
|
1183
|
+
pairs.push([key, value]);
|
|
1184
|
+
else
|
|
1185
|
+
pairs.push(...urlPasswords(value).map((password) => [key, password]));
|
|
1186
|
+
}
|
|
1187
|
+
return pairs;
|
|
1188
|
+
}
|
|
1189
|
+
async function envFilesBelow(folder, depth = 0) {
|
|
1190
|
+
const found = [];
|
|
1191
|
+
let children;
|
|
1192
|
+
try {
|
|
1193
|
+
children = await readdir(folder, { withFileTypes: true });
|
|
1194
|
+
} catch {
|
|
1195
|
+
return found;
|
|
1196
|
+
}
|
|
1197
|
+
for (const entry of children) {
|
|
1198
|
+
const path = join(folder, entry.name);
|
|
1199
|
+
if (entry.isDirectory() && depth < KNOWN_DEPTH && !SKIPPED.has(entry.name))
|
|
1200
|
+
found.push(...await envFilesBelow(path, depth + 1));
|
|
1201
|
+
else if (ENV_FILE.test(entry.name) && await isFileEntry(entry, path))
|
|
1202
|
+
found.push(path);
|
|
1203
|
+
}
|
|
1204
|
+
return found;
|
|
1205
|
+
}
|
|
1206
|
+
async function loadKnown(folder = process.cwd(), extra = []) {
|
|
1207
|
+
const pairs = extra.flatMap(secretPairs);
|
|
1208
|
+
for (const path of await envFilesBelow(folder)) {
|
|
1209
|
+
try {
|
|
1210
|
+
pairs.push(...secretPairs(parseDotenv(await Bun.file(path).text())));
|
|
1211
|
+
} catch {}
|
|
927
1212
|
}
|
|
928
1213
|
return knownSecrets(pairs);
|
|
929
1214
|
}
|
|
@@ -932,6 +1217,8 @@ async function loadKnown(folder = process.cwd()) {
|
|
|
932
1217
|
var DIFF_START = /^(?:diff --git |@@ )/;
|
|
933
1218
|
var NEW_FILE = /^\+\+\+ (?:b\/)?(.+)$/;
|
|
934
1219
|
var HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)/;
|
|
1220
|
+
var MAX_PENDING = 1 << 18;
|
|
1221
|
+
var OVERLAP = 1 << 13;
|
|
935
1222
|
|
|
936
1223
|
class Redactor {
|
|
937
1224
|
known;
|
|
@@ -942,17 +1229,26 @@ class Redactor {
|
|
|
942
1229
|
this.known = known;
|
|
943
1230
|
}
|
|
944
1231
|
push(chunk) {
|
|
945
|
-
|
|
946
|
-
const end = this.pending.lastIndexOf(`
|
|
1232
|
+
const end = chunk.lastIndexOf(`
|
|
947
1233
|
`);
|
|
1234
|
+
this.pending += chunk;
|
|
948
1235
|
if (end === -1)
|
|
949
|
-
return "";
|
|
950
|
-
const
|
|
951
|
-
|
|
1236
|
+
return this.pending.length > MAX_PENDING ? this.cut() : "";
|
|
1237
|
+
const split = this.pending.length - chunk.length + end + 1;
|
|
1238
|
+
const complete = this.pending.slice(0, split);
|
|
1239
|
+
this.pending = this.pending.slice(split);
|
|
952
1240
|
return complete.split(`
|
|
953
1241
|
`).slice(0, -1).map((line) => this.line(line)).filter((line) => line !== undefined).map((line) => `${line}
|
|
954
1242
|
`).join("");
|
|
955
1243
|
}
|
|
1244
|
+
cut() {
|
|
1245
|
+
const limit = this.pending.length - OVERLAP;
|
|
1246
|
+
const space = this.pending.lastIndexOf(" ", limit);
|
|
1247
|
+
const at = space > 0 ? space + 1 : limit;
|
|
1248
|
+
const head = this.pending.slice(0, at);
|
|
1249
|
+
this.pending = this.pending.slice(at);
|
|
1250
|
+
return this.line(head) ?? "";
|
|
1251
|
+
}
|
|
956
1252
|
end() {
|
|
957
1253
|
const rest = this.pending;
|
|
958
1254
|
this.pending = "";
|
|
@@ -960,19 +1256,20 @@ class Redactor {
|
|
|
960
1256
|
}
|
|
961
1257
|
line(line) {
|
|
962
1258
|
if (this.inKey) {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1259
|
+
const end = PEM_END.exec(line);
|
|
1260
|
+
if (end === null) {
|
|
1261
|
+
if (this.hidden)
|
|
1262
|
+
return;
|
|
1263
|
+
this.hidden = true;
|
|
1264
|
+
return KEY_MASK;
|
|
966
1265
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
this.hidden = true;
|
|
970
|
-
return "*** private key redacted ***";
|
|
1266
|
+
this.inKey = false;
|
|
1267
|
+
return (end.index > 0 && !this.hidden ? KEY_MASK : "") + this.line(line.slice(end.index));
|
|
971
1268
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
this.
|
|
975
|
-
|
|
1269
|
+
const body = openKeyBody(line);
|
|
1270
|
+
if (body !== undefined) {
|
|
1271
|
+
this.inKey = true;
|
|
1272
|
+
this.hidden = body !== "";
|
|
976
1273
|
}
|
|
977
1274
|
return maskLine(line, this.known);
|
|
978
1275
|
}
|
|
@@ -1012,15 +1309,21 @@ function check(text, known) {
|
|
|
1012
1309
|
number = index + 1;
|
|
1013
1310
|
}
|
|
1014
1311
|
if (inKey) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1312
|
+
const end = PEM_END.exec(line);
|
|
1313
|
+
inKey = end === null;
|
|
1314
|
+
if (end === null)
|
|
1315
|
+
continue;
|
|
1316
|
+
line = line.slice(end.index);
|
|
1017
1317
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1318
|
+
const found = findSecrets(line, known).map((match) => match.kind);
|
|
1319
|
+
const body = openKeyBody(line);
|
|
1320
|
+
if (body !== undefined) {
|
|
1321
|
+
inKey = true;
|
|
1322
|
+
if (body === "")
|
|
1323
|
+
found.push("private key");
|
|
1021
1324
|
}
|
|
1022
|
-
for (const
|
|
1023
|
-
findings.push({ file, line: number, kind
|
|
1325
|
+
for (const kind of found)
|
|
1326
|
+
findings.push({ file, line: number, kind });
|
|
1024
1327
|
}
|
|
1025
1328
|
return findings;
|
|
1026
1329
|
}
|
|
@@ -1028,7 +1331,7 @@ function check(text, known) {
|
|
|
1028
1331
|
// src/redact.ts
|
|
1029
1332
|
var usage = `redact [--check]
|
|
1030
1333
|
|
|
1031
|
-
Copies stdin to stdout line by line with secrets masked: known token formats (
|
|
1334
|
+
Copies stdin to stdout line by line with secrets masked: known token formats (GitHub, GitLab, Slack, AWS, npm, Stripe, Google and SendGrid keys, sk-, JWTs, Authorization headers, password= and secret= pairs), private key blocks, the password and the tokens of any URL and the values of secret-looking keys in this folder's .env* files.
|
|
1032
1335
|
--check prints nothing of the input and lists the file and line of each secret found, exiting 1 when there is any: \`git diff --cached | redact --check\`.
|
|
1033
1336
|
|
|
1034
1337
|
Exits 0 when it copied the input or found no secret, 1 when --check finds one, 2 on a usage error.`;
|