@kaynooo/env-tools 0.5.0 → 0.6.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 +3 -2
- package/dist/ev.js +212 -80
- package/dist/redact.js +150 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,13 +16,14 @@ bun install -g @kaynooo/env-tools
|
|
|
16
16
|
| `ev show <rev>:<file>` | an env file as a git revision holds it, values masked |
|
|
17
17
|
| `ev set KEY [file]` | sets a key in `.env` (or the file), the value read from stdin or `-F <file>` so it never appears in a command line, `--empty` for an empty value |
|
|
18
18
|
| `ev unset KEY [file]` | removes a key |
|
|
19
|
+
| `ev cp KEY --from <file> --to <file>` | copies a key's value to another file without printing it, replacing the key there or appending it, `--as NEWKEY` to rename |
|
|
19
20
|
| `ev mv KEY --from <file> --to <file>` | moves a key's line as written, `--force` to replace one the target has |
|
|
20
21
|
| `ev diff <a> <b>` | names the keys set in one file and not the other, exit 1 when they differ |
|
|
21
22
|
| `ev missing [file]` | names the keys of `.env.example` (`--against`) that the file lacks, exit 1 when some are |
|
|
22
23
|
| `ev audit [dir...]` | lists the `.env` files any account can read or git tracks, and the compose files any account can read that write a secret or URL password out, with the fix for each, exit 1 when it finds one |
|
|
23
24
|
| `ev audit --fix [dir...]` | moves each secret a listed compose file writes out into the `.env` beside it as `${KEY}` (a key the `.env` sets to another value stays put), puts it back when `docker compose config` resolves differently, and shuts other accounts out of both files and of every listed `.env`; exit 1 when something is left |
|
|
24
25
|
|
|
25
|
-
A value is masked when its key looks like `TOKEN|SECRET|PASSWORD|KEY|PRIVATE|DSN`, when it is a private key, or in the password part of any URL (`postgres://user:***@host/db`) and any known token format. A masked value keeps a few leading characters and its length (`ghp***(40)`) so two values can be told apart. There is no flag that prints a secret unmasked.
|
|
26
|
+
A value is masked when its key looks like `TOKEN|SECRET|PASSWORD|KEY|PRIVATE|DSN`, when it is a private key, or in the password part of any URL (`postgres://user:***@host/db`) and any known token format. A URL is also masked where it carries a token, whatever its key is named: the last segment of a Discord, Slack, Uptime Kuma push or healthchecks.io ping URL, any path segment of 20 or more characters of `A-Za-z0-9_-` mixing lowercase, uppercase and digits, any path segment of 32 or more letters and digits of any case (hex, base32, dashless UUIDs), and the value of a query parameter named `token`, `key`, `apikey`, `secret`, `password`, `sig`, `signature`, `auth` or ending in `_token` or `_secret`; the host and the rest of the URL stay readable. A masked value keeps a few leading characters and its length (`ghp***(40)`) so two values can be told apart. There is no flag that prints a secret unmasked.
|
|
26
27
|
|
|
27
28
|
Files are parsed and written back preserving comments, order, `export KEY=` prefixes, quoting and multiline values; only the lines a command touches change.
|
|
28
29
|
|
|
@@ -36,7 +37,7 @@ git diff --cached | redact --check
|
|
|
36
37
|
`redact` copies stdin to stdout line by line, masking:
|
|
37
38
|
|
|
38
39
|
- known token formats: `ghp_`, `github_pat_`, `glpat-`, `sk-`, `xox?-`, JWTs and the body of PEM private key blocks
|
|
39
|
-
- the password of any URL
|
|
40
|
+
- the password and the tokens of any URL (webhook and push URLs, opaque path segments, token query parameters)
|
|
40
41
|
- the values of secret-looking keys in the `.env*` files of the current folder (whole words only, from 6 characters), `--no-env` skips them
|
|
41
42
|
|
|
42
43
|
`redact --check` prints nothing of the input: it lists `<file>:<line>: <kind>` for each secret found and exits 1. On a diff it reads the added lines only and names the new file's line numbers.
|
package/dist/ev.js
CHANGED
|
@@ -343,6 +343,26 @@ function printJson(value) {
|
|
|
343
343
|
console.log(JSON.stringify(value, undefined, 2));
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// ../kit/src/text.ts
|
|
347
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
348
|
+
function distance(a, b) {
|
|
349
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
350
|
+
for (const [i, left] of [...a].entries()) {
|
|
351
|
+
const current = [i + 1];
|
|
352
|
+
for (const [j, right] of [...b].entries())
|
|
353
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
354
|
+
previous = current;
|
|
355
|
+
}
|
|
356
|
+
return previous[b.length] ?? 0;
|
|
357
|
+
}
|
|
358
|
+
function closest(name, names) {
|
|
359
|
+
const started = names.find((known) => known.startsWith(name));
|
|
360
|
+
if (started !== undefined)
|
|
361
|
+
return started;
|
|
362
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
363
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
346
366
|
// ../kit/src/args.ts
|
|
347
367
|
function jsonOption(holds) {
|
|
348
368
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
@@ -362,8 +382,8 @@ function fail(message, code = 2, hint) {
|
|
|
362
382
|
}
|
|
363
383
|
|
|
364
384
|
class UsageError extends Exit {
|
|
365
|
-
constructor(message) {
|
|
366
|
-
super(message, 2);
|
|
385
|
+
constructor(message, hint) {
|
|
386
|
+
super(message, 2, hint);
|
|
367
387
|
}
|
|
368
388
|
}
|
|
369
389
|
function badUsage(message) {
|
|
@@ -386,6 +406,7 @@ var completion = { active: false };
|
|
|
386
406
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
387
407
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
388
408
|
var FULL_STOP = /\.$/;
|
|
409
|
+
var DASHES = /^-+/;
|
|
389
410
|
function usageLine(usage, colors = paint(process4.stdout)) {
|
|
390
411
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
391
412
|
}
|
|
@@ -422,10 +443,10 @@ function optionRows(options, colors = paint(process4.stdout)) {
|
|
|
422
443
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
423
444
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
424
445
|
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 };
|
|
446
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
|
|
426
447
|
});
|
|
427
448
|
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)}`);
|
|
449
|
+
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
429
450
|
}
|
|
430
451
|
var OUTPUT_OPTIONS = {
|
|
431
452
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
@@ -457,7 +478,8 @@ function parse(args, usage, options, settings = {}) {
|
|
|
457
478
|
try {
|
|
458
479
|
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
459
480
|
} catch (error) {
|
|
460
|
-
|
|
481
|
+
const message = parseError(error);
|
|
482
|
+
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
461
483
|
}
|
|
462
484
|
const values = parsed.values;
|
|
463
485
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
@@ -500,6 +522,22 @@ function parseError(error) {
|
|
|
500
522
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
501
523
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
502
524
|
}
|
|
525
|
+
var UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
|
|
526
|
+
function nearestOption(message, options, allowNegative) {
|
|
527
|
+
const typed = UNKNOWN_OPTION.exec(message)?.[1];
|
|
528
|
+
if (typed === undefined)
|
|
529
|
+
return;
|
|
530
|
+
const word = typed.replace(DASHES, "");
|
|
531
|
+
if (word.length < 2 && !typed.startsWith("--"))
|
|
532
|
+
return;
|
|
533
|
+
const known = Object.entries(options).flatMap(([name, option]) => [
|
|
534
|
+
...option.short === undefined ? [] : [`-${option.short}`],
|
|
535
|
+
`--${name}`,
|
|
536
|
+
...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
|
|
537
|
+
]);
|
|
538
|
+
const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
|
|
539
|
+
return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
|
|
540
|
+
}
|
|
503
541
|
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
504
542
|
if (path === "-") {
|
|
505
543
|
if (process4.stdin.isTTY)
|
|
@@ -513,33 +551,19 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
513
551
|
}
|
|
514
552
|
// ../kit/src/cli.ts
|
|
515
553
|
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
554
|
var LEVEL_OPTIONS = {
|
|
532
555
|
...OUTPUT_OPTIONS,
|
|
533
556
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
534
557
|
};
|
|
535
558
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
536
|
-
function commandRows(commands, colors) {
|
|
559
|
+
async function commandRows(commands, colors) {
|
|
537
560
|
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
538
561
|
const groups = new Map([["Commands:", []]]);
|
|
539
|
-
|
|
562
|
+
const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
|
|
563
|
+
for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
|
|
540
564
|
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
541
565
|
const rows = groups.get(heading) ?? [];
|
|
542
|
-
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(
|
|
566
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
543
567
|
groups.set(heading, rows);
|
|
544
568
|
}
|
|
545
569
|
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
@@ -559,28 +583,38 @@ function takeOutputFlags(argv) {
|
|
|
559
583
|
}
|
|
560
584
|
return argv.slice(index);
|
|
561
585
|
}
|
|
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;
|
|
586
|
+
async function resolveSummary(summary) {
|
|
587
|
+
return typeof summary === "string" ? summary : summary();
|
|
568
588
|
}
|
|
569
|
-
|
|
589
|
+
function scopeOf(commands, extra, aliases) {
|
|
590
|
+
return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
|
|
591
|
+
}
|
|
592
|
+
async function expand(scope, words) {
|
|
593
|
+
if (scope.lazy.length === 0)
|
|
594
|
+
return scope;
|
|
595
|
+
const commands = { ...scope.commands };
|
|
596
|
+
for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
|
|
597
|
+
for (const [name, command] of Object.entries(added))
|
|
598
|
+
commands[name] ??= command;
|
|
599
|
+
}
|
|
600
|
+
return { ...scope, commands, lazy: [] };
|
|
601
|
+
}
|
|
602
|
+
async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
|
|
570
603
|
const colors = paint(stream);
|
|
571
604
|
const prefix = path.join(" ");
|
|
605
|
+
const summary = await resolveSummary(lazySummary);
|
|
572
606
|
if (wantsHelpJson())
|
|
573
607
|
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
574
608
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
575
609
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
576
610
|
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
577
611
|
return [
|
|
578
|
-
header(colors),
|
|
612
|
+
header(colors, summary),
|
|
579
613
|
"",
|
|
580
614
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
581
615
|
...replacesRows(replaces, colors),
|
|
582
616
|
"",
|
|
583
|
-
...commandRows(commands, colors),
|
|
617
|
+
...await commandRows(commands, colors),
|
|
584
618
|
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
585
619
|
"",
|
|
586
620
|
colors.bold("Options:"),
|
|
@@ -626,11 +660,11 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
626
660
|
}
|
|
627
661
|
const [name, ...args] = argv;
|
|
628
662
|
if (name === undefined && level.path.length > 1) {
|
|
629
|
-
console.error(await help(level, process5.stderr));
|
|
663
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stderr));
|
|
630
664
|
return 2;
|
|
631
665
|
}
|
|
632
666
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
633
|
-
console.log(await help(level, process5.stdout));
|
|
667
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stdout));
|
|
634
668
|
return 0;
|
|
635
669
|
}
|
|
636
670
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -639,6 +673,10 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
639
673
|
}
|
|
640
674
|
let command = level.commands[level.aliases?.[name] ?? name];
|
|
641
675
|
let rest = args;
|
|
676
|
+
if (command === undefined && level.lazy.length > 0) {
|
|
677
|
+
level = await expand(level, [...level.path.slice(1), ...argv]);
|
|
678
|
+
command = level.commands[level.aliases?.[name] ?? name];
|
|
679
|
+
}
|
|
642
680
|
if (command === undefined && level.defaultCommand !== undefined) {
|
|
643
681
|
command = level.commands[level.defaultCommand];
|
|
644
682
|
rest = argv;
|
|
@@ -650,7 +688,7 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
650
688
|
}
|
|
651
689
|
if ("commands" in command) {
|
|
652
690
|
const { summary } = command;
|
|
653
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${
|
|
691
|
+
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
692
|
}
|
|
655
693
|
try {
|
|
656
694
|
return await command.run(rest);
|
|
@@ -675,18 +713,24 @@ async function optionsOf(run) {
|
|
|
675
713
|
async function leafCandidates(run, words) {
|
|
676
714
|
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
677
715
|
return [FILES];
|
|
678
|
-
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
716
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
679
717
|
}
|
|
680
|
-
async function candidates(
|
|
718
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
681
719
|
const [word = "", ...rest] = words;
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
720
|
+
const listing = rest.length === 0 && !word.startsWith("-");
|
|
721
|
+
let { commands, aliases } = scope;
|
|
722
|
+
let command = commands[aliases?.[word] ?? word];
|
|
723
|
+
if (listing || command === undefined) {
|
|
724
|
+
({ commands, aliases } = await expand(scope, [...before, ...words]));
|
|
725
|
+
command = commands[aliases?.[word] ?? word];
|
|
726
|
+
}
|
|
727
|
+
if (listing)
|
|
728
|
+
return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
|
|
685
729
|
if (command === undefined) {
|
|
686
730
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
687
731
|
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
688
732
|
}
|
|
689
|
-
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
733
|
+
return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
|
|
690
734
|
}
|
|
691
735
|
async function printCandidates(found) {
|
|
692
736
|
for (const line of await found)
|
|
@@ -695,8 +739,8 @@ async function printCandidates(found) {
|
|
|
695
739
|
}
|
|
696
740
|
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
697
741
|
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)} ${
|
|
742
|
+
return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
|
|
743
|
+
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
744
|
}
|
|
701
745
|
async function runMain({ name, version, run, redact }, argv = process5.argv.slice(2)) {
|
|
702
746
|
if (argv[0] === COMPLETE)
|
|
@@ -712,20 +756,21 @@ async function runMain({ name, version, run, redact }, argv = process5.argv.slic
|
|
|
712
756
|
}
|
|
713
757
|
}
|
|
714
758
|
// package.json
|
|
715
|
-
var version = "0.
|
|
759
|
+
var version = "0.6.0";
|
|
716
760
|
|
|
717
761
|
// src/commands/audit.ts
|
|
718
|
-
import { stat as
|
|
762
|
+
import { stat as stat4 } from "fs/promises";
|
|
719
763
|
import { relative, resolve } from "path";
|
|
720
764
|
import process7 from "process";
|
|
721
765
|
|
|
722
766
|
// src/audit.ts
|
|
723
|
-
import { readdir as readdir2, stat } from "fs/promises";
|
|
724
|
-
import { dirname, join } from "path";
|
|
767
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
768
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
725
769
|
var {$ } = globalThis.Bun;
|
|
726
770
|
|
|
727
771
|
// src/files.ts
|
|
728
|
-
import { readdir } from "fs/promises";
|
|
772
|
+
import { chmod, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
773
|
+
import { basename, dirname, join } from "path";
|
|
729
774
|
|
|
730
775
|
// src/dotenv.ts
|
|
731
776
|
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
@@ -848,6 +893,17 @@ var TOKEN_FORMATS = [
|
|
|
848
893
|
["Slack token", /\bxox[abprs]-[\w-]{10,}/g],
|
|
849
894
|
["JWT", /\beyJ[\w-]+\.[\w-]+\.[\w-]+/g]
|
|
850
895
|
];
|
|
896
|
+
var URL_PARTS = /\b[a-z][a-z0-9+.-]*:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
897
|
+
var PATH_SEGMENT = /[^/]+/g;
|
|
898
|
+
var QUERY_PARAM = /([^&=]+)=([^&]*)/g;
|
|
899
|
+
var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
|
|
900
|
+
var WEBHOOK_FORMATS = [
|
|
901
|
+
["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
|
|
902
|
+
["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
|
|
903
|
+
["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
|
|
904
|
+
["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
|
|
905
|
+
];
|
|
906
|
+
var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
|
|
851
907
|
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
852
908
|
var PEM_END = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
853
909
|
function isSecretKey(key) {
|
|
@@ -871,6 +927,39 @@ function knownSecrets(pairs) {
|
|
|
871
927
|
function urlPasswords(text) {
|
|
872
928
|
return [...text.matchAll(URL_PASSWORD)].flatMap((url) => url[1] ?? []);
|
|
873
929
|
}
|
|
930
|
+
function urlTokens(line) {
|
|
931
|
+
const found = [];
|
|
932
|
+
const add = (kind, start, value) => {
|
|
933
|
+
found.push({ start, end: start + value.length, kind, mask: hint(value) });
|
|
934
|
+
};
|
|
935
|
+
for (const url of line.matchAll(URL_PARTS)) {
|
|
936
|
+
const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
|
|
937
|
+
const authorityStart = url.indices?.[1]?.[0];
|
|
938
|
+
const pathStart = url.indices?.[2]?.[0];
|
|
939
|
+
const queryStart = url.indices?.[3]?.[0];
|
|
940
|
+
if (authorityStart === undefined || pathStart === undefined)
|
|
941
|
+
continue;
|
|
942
|
+
const hostAt = authority.lastIndexOf("@") + 1;
|
|
943
|
+
const target = authority.slice(hostAt) + path;
|
|
944
|
+
for (const [kind, pattern] of WEBHOOK_FORMATS) {
|
|
945
|
+
const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
|
|
946
|
+
if (start !== undefined && end !== undefined)
|
|
947
|
+
add(kind, authorityStart + hostAt + start, target.slice(start, end));
|
|
948
|
+
}
|
|
949
|
+
for (const segment of path.matchAll(PATH_SEGMENT)) {
|
|
950
|
+
if (OPAQUE_SEGMENT.test(segment[0]))
|
|
951
|
+
add("URL token", pathStart + segment.index, segment[0]);
|
|
952
|
+
}
|
|
953
|
+
if (query === undefined || queryStart === undefined)
|
|
954
|
+
continue;
|
|
955
|
+
for (const param of query.matchAll(QUERY_PARAM)) {
|
|
956
|
+
const [name, value] = [param[1] ?? "", param[2] ?? ""];
|
|
957
|
+
if (value !== "" && SECRET_PARAM.test(name))
|
|
958
|
+
add("URL token", queryStart + param.index + name.length + 1, value);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return found.filter((match) => match.end > match.start);
|
|
962
|
+
}
|
|
874
963
|
function findSecrets(line, known) {
|
|
875
964
|
const found = [];
|
|
876
965
|
for (const url of line.matchAll(URL_PASSWORD)) {
|
|
@@ -882,6 +971,7 @@ function findSecrets(line, known) {
|
|
|
882
971
|
for (const token of line.matchAll(pattern))
|
|
883
972
|
found.push({ start: token.index, end: token.index + token[0].length, kind, mask: hint(token[0]) });
|
|
884
973
|
}
|
|
974
|
+
found.push(...urlTokens(line));
|
|
885
975
|
if (known?.pattern !== undefined) {
|
|
886
976
|
for (const value of line.matchAll(known.pattern))
|
|
887
977
|
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
@@ -923,7 +1013,20 @@ async function readEnv(path) {
|
|
|
923
1013
|
return parseDotenv(await file.text());
|
|
924
1014
|
}
|
|
925
1015
|
async function writeEnv(path, file) {
|
|
926
|
-
|
|
1016
|
+
let target = path;
|
|
1017
|
+
let mode = 384;
|
|
1018
|
+
try {
|
|
1019
|
+
target = await realpath(path);
|
|
1020
|
+
mode = (await stat(target)).mode & 511;
|
|
1021
|
+
} catch {}
|
|
1022
|
+
const temp = join(dirname(target), `.${basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1023
|
+
try {
|
|
1024
|
+
await writeFile(temp, serializeDotenv(file), { mode: 384, flag: "wx" });
|
|
1025
|
+
await chmod(temp, mode);
|
|
1026
|
+
await rename(temp, target);
|
|
1027
|
+
} finally {
|
|
1028
|
+
await rm(temp, { force: true });
|
|
1029
|
+
}
|
|
927
1030
|
}
|
|
928
1031
|
async function loadKnown(folder = process.cwd()) {
|
|
929
1032
|
const pairs = [];
|
|
@@ -959,16 +1062,16 @@ function inlineSecrets(compose) {
|
|
|
959
1062
|
return [...new Set(names)];
|
|
960
1063
|
}
|
|
961
1064
|
async function openToOthers(path) {
|
|
962
|
-
if (((await
|
|
1065
|
+
if (((await stat2(path)).mode & 4) === 0)
|
|
963
1066
|
return false;
|
|
964
|
-
for (let dir =
|
|
965
|
-
if (((await
|
|
1067
|
+
for (let dir = dirname2(path);dir !== "/"; dir = dirname2(dir)) {
|
|
1068
|
+
if (((await stat2(dir)).mode & 1) === 0)
|
|
966
1069
|
return false;
|
|
967
1070
|
}
|
|
968
1071
|
return true;
|
|
969
1072
|
}
|
|
970
1073
|
async function tracked(path) {
|
|
971
|
-
return (await $`git ls-files --error-unmatch -- ${path}`.cwd(
|
|
1074
|
+
return (await $`git ls-files --error-unmatch -- ${path}`.cwd(dirname2(path)).quiet().nothrow()).exitCode === 0;
|
|
972
1075
|
}
|
|
973
1076
|
async function* candidates2(root, unread, depth = 0) {
|
|
974
1077
|
let found;
|
|
@@ -979,7 +1082,7 @@ async function* candidates2(root, unread, depth = 0) {
|
|
|
979
1082
|
return;
|
|
980
1083
|
}
|
|
981
1084
|
for (const entry of found) {
|
|
982
|
-
const path =
|
|
1085
|
+
const path = join2(root, entry.name);
|
|
983
1086
|
if (entry.isDirectory() && depth < MAX_DEPTH && !SKIPPED.has(entry.name))
|
|
984
1087
|
yield* candidates2(path, unread, depth + 1);
|
|
985
1088
|
else if (entry.isFile() && !TEMPLATE.test(entry.name) && (ENV_FILE.test(entry.name) || COMPOSE_FILE.test(entry.name)))
|
|
@@ -1008,8 +1111,8 @@ async function audit(roots) {
|
|
|
1008
1111
|
}
|
|
1009
1112
|
|
|
1010
1113
|
// src/fix.ts
|
|
1011
|
-
import { chmod, chown, rm, stat as
|
|
1012
|
-
import { dirname as
|
|
1114
|
+
import { chmod as chmod2, chown, rm as rm2, stat as stat3, writeFile as writeFile2 } from "fs/promises";
|
|
1115
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1013
1116
|
var MAP_ENTRY = /^(\s*)(["']?)([A-Z0-9_]+)\2(\s*:)(.*)$/;
|
|
1014
1117
|
var LIST_ENTRY = /^(\s*-[ \t]*)(["']?)([A-Z0-9_]+)=(.*)$/;
|
|
1015
1118
|
var TRAILING = /^\s*(?:#.*)?$/;
|
|
@@ -1071,7 +1174,7 @@ function envLine(key, value) {
|
|
|
1071
1174
|
`) ? undefined : `${key}='${value}'`;
|
|
1072
1175
|
}
|
|
1073
1176
|
async function resolved(path) {
|
|
1074
|
-
const run = Bun.spawn(["docker", "compose", "-f", path, "config", "--format", "json"], { cwd:
|
|
1177
|
+
const run = Bun.spawn(["docker", "compose", "-f", path, "config", "--format", "json"], { cwd: dirname3(path), stdout: "pipe", stderr: "ignore", stdin: "ignore" });
|
|
1075
1178
|
const [out, code] = await Promise.all([new Response(run.stdout).text(), run.exited]);
|
|
1076
1179
|
return code === 0 ? out : undefined;
|
|
1077
1180
|
}
|
|
@@ -1082,7 +1185,7 @@ function gainedBy(before, after, key) {
|
|
|
1082
1185
|
}
|
|
1083
1186
|
async function shutOthersOut(path) {
|
|
1084
1187
|
try {
|
|
1085
|
-
await
|
|
1188
|
+
await chmod2(path, (await stat3(path)).mode & 504);
|
|
1086
1189
|
return true;
|
|
1087
1190
|
} catch (error) {
|
|
1088
1191
|
if (error.code === "EPERM")
|
|
@@ -1091,7 +1194,7 @@ async function shutOthersOut(path) {
|
|
|
1091
1194
|
}
|
|
1092
1195
|
}
|
|
1093
1196
|
async function fixCompose(path) {
|
|
1094
|
-
const env =
|
|
1197
|
+
const env = join3(dirname3(path), ".env");
|
|
1095
1198
|
const compose = await Bun.file(path).text();
|
|
1096
1199
|
const envBefore = await Bun.file(env).exists() ? await Bun.file(env).text() : undefined;
|
|
1097
1200
|
const dotenv = parseDotenv(envBefore ?? "");
|
|
@@ -1143,15 +1246,15 @@ async function move(fix, compose, composeAfter, envBefore, envAfter) {
|
|
|
1143
1246
|
const docker = Bun.which("docker") !== null;
|
|
1144
1247
|
const before = docker ? await resolved(path) : undefined;
|
|
1145
1248
|
if (envBefore === undefined) {
|
|
1146
|
-
await
|
|
1147
|
-
const { uid, gid } = await
|
|
1249
|
+
await writeFile2(env, envAfter, { mode: 384 });
|
|
1250
|
+
const { uid, gid } = await stat3(path);
|
|
1148
1251
|
try {
|
|
1149
1252
|
await chown(env, uid, gid);
|
|
1150
1253
|
} catch {}
|
|
1151
1254
|
} else {
|
|
1152
|
-
await
|
|
1255
|
+
await writeFile2(env, envAfter);
|
|
1153
1256
|
}
|
|
1154
|
-
await
|
|
1257
|
+
await writeFile2(path, composeAfter);
|
|
1155
1258
|
if (!docker) {
|
|
1156
1259
|
fix.check = "no docker";
|
|
1157
1260
|
return;
|
|
@@ -1165,11 +1268,11 @@ async function move(fix, compose, composeAfter, envBefore, envAfter) {
|
|
|
1165
1268
|
fix.check = "same";
|
|
1166
1269
|
return;
|
|
1167
1270
|
}
|
|
1168
|
-
await
|
|
1271
|
+
await writeFile2(path, compose);
|
|
1169
1272
|
if (envBefore === undefined)
|
|
1170
|
-
await
|
|
1273
|
+
await rm2(env);
|
|
1171
1274
|
else
|
|
1172
|
-
await
|
|
1275
|
+
await writeFile2(env, envBefore);
|
|
1173
1276
|
fix.check = "rolled back";
|
|
1174
1277
|
fix.left.push(...fix.moved.map((key) => {
|
|
1175
1278
|
const gained = after === undefined ? [] : gainedBy(before, after, key);
|
|
@@ -1199,7 +1302,7 @@ var CHECKS = {
|
|
|
1199
1302
|
};
|
|
1200
1303
|
async function isFolder(path) {
|
|
1201
1304
|
try {
|
|
1202
|
-
return (await
|
|
1305
|
+
return (await stat4(path)).isDirectory();
|
|
1203
1306
|
} catch {
|
|
1204
1307
|
return false;
|
|
1205
1308
|
}
|
|
@@ -1266,7 +1369,7 @@ async function audit2(args) {
|
|
|
1266
1369
|
}
|
|
1267
1370
|
const uid = process7.getuid?.();
|
|
1268
1371
|
for (const { path, readable, tracked, secrets } of exposed) {
|
|
1269
|
-
const sudo = uid !== undefined && uid !== 0 && (await
|
|
1372
|
+
const sudo = uid !== undefined && uid !== 0 && (await stat4(path)).uid !== uid ? "sudo " : "";
|
|
1270
1373
|
const problems = [
|
|
1271
1374
|
...secrets === undefined ? [] : [`writes out ${secrets.join(", ")}: ev audit --fix moves the values into a .env beside it`],
|
|
1272
1375
|
...readable ? [`readable by any account: ${sudo}chmod o-rwx ${quoted2(shown(path))}`] : [],
|
|
@@ -1343,6 +1446,11 @@ var mvUsage = `ev mv KEY --from <file> --to <file>
|
|
|
1343
1446
|
Moves KEY's line from one file to another, as written. Refuses to replace a key the target already sets unless --force.
|
|
1344
1447
|
|
|
1345
1448
|
Exits 0 when it moved the key, 1 when the source lacks it or the target already sets it, 2 on a usage error.`;
|
|
1449
|
+
var cpUsage = `ev cp KEY --from <file> --to <file> [--as NEWKEY]
|
|
1450
|
+
|
|
1451
|
+
Copies KEY's value from one file to another without printing it, replacing the key's line in the target in place or appending one, and prints the masked result as ev set does. --as stores the copy under another key.
|
|
1452
|
+
|
|
1453
|
+
Exits 0 when it copied the key, 1 when the source lacks it, 2 on a usage error.`;
|
|
1346
1454
|
async function readValue(from) {
|
|
1347
1455
|
const input = await readInput(from ?? "-", { noStdin: "pipe the value on stdin or give -F <file>, it is never taken from the command line" });
|
|
1348
1456
|
if (input === "")
|
|
@@ -1386,6 +1494,22 @@ ${unsetUsage}`);
|
|
|
1386
1494
|
console.log(`${target}: removed ${key}`);
|
|
1387
1495
|
return 0;
|
|
1388
1496
|
}
|
|
1497
|
+
async function transfer(key, from, to, newKey, replace) {
|
|
1498
|
+
if (from === to && newKey === undefined)
|
|
1499
|
+
fail("--from and --to are the same file");
|
|
1500
|
+
const source = await readEnv(from);
|
|
1501
|
+
const line = lastLine(source, key);
|
|
1502
|
+
if (line === undefined)
|
|
1503
|
+
fail(`${key} is not set in ${from}`, 1);
|
|
1504
|
+
const target = from === to ? source : await readOrEmpty(to);
|
|
1505
|
+
const name = newKey ?? key;
|
|
1506
|
+
if (!replace && entries(target).has(name))
|
|
1507
|
+
fail(`${to} already sets ${name}, --force replaces it`, 1);
|
|
1508
|
+
const value = line.value ?? "";
|
|
1509
|
+
setLine(target, name, value, newKey === undefined && !entries(target).has(name) ? line.raw : undefined);
|
|
1510
|
+
await writeEnv(to, target);
|
|
1511
|
+
return { source, value, name };
|
|
1512
|
+
}
|
|
1389
1513
|
async function mv(args) {
|
|
1390
1514
|
const parsed = parse(args, mvUsage, {
|
|
1391
1515
|
from: { type: "string", description: "the file to take KEY from" },
|
|
@@ -1398,28 +1522,35 @@ async function mv(args) {
|
|
|
1398
1522
|
fail(`needs KEY, --from and --to
|
|
1399
1523
|
|
|
1400
1524
|
${mvUsage}`);
|
|
1401
|
-
|
|
1402
|
-
fail("--from and --to are the same file");
|
|
1403
|
-
const source = await readEnv(from);
|
|
1404
|
-
const line = lastLine(source, key);
|
|
1405
|
-
if (line === undefined)
|
|
1406
|
-
fail(`${key} is not set in ${from}`, 1);
|
|
1407
|
-
const target = await readOrEmpty(to);
|
|
1408
|
-
if (entries(target).has(key) && force !== true)
|
|
1409
|
-
fail(`${to} already sets ${key}, --force replaces it`, 1);
|
|
1410
|
-
setLine(target, key, line.value ?? "", line.raw);
|
|
1411
|
-
await writeEnv(to, target);
|
|
1525
|
+
const { source } = await transfer(key, from, to, undefined, force === true);
|
|
1412
1526
|
removeKey(source, key);
|
|
1413
1527
|
await writeEnv(from, source);
|
|
1414
1528
|
console.log(`${key}: ${from} -> ${to}`);
|
|
1415
1529
|
return 0;
|
|
1416
1530
|
}
|
|
1531
|
+
async function cp(args) {
|
|
1532
|
+
const parsed = parse(args, cpUsage, {
|
|
1533
|
+
from: { type: "string", description: "the file to take KEY from" },
|
|
1534
|
+
to: { type: "string", description: "the file to put the copy in, created when missing" },
|
|
1535
|
+
as: { type: "string", description: "store the copy under this key instead" }
|
|
1536
|
+
});
|
|
1537
|
+
const [key] = parsed.positionals;
|
|
1538
|
+
const { from, to, as } = parsed.values;
|
|
1539
|
+
if (key === undefined || from === undefined || to === undefined)
|
|
1540
|
+
fail(`needs KEY, --from and --to
|
|
1541
|
+
|
|
1542
|
+
${cpUsage}`);
|
|
1543
|
+
const { value, name } = await transfer(key, from, to, as, true);
|
|
1544
|
+
console.log(`${to}: ${name}=${maskValue(name, value).replaceAll(`
|
|
1545
|
+
`, "\\n")}`);
|
|
1546
|
+
return 0;
|
|
1547
|
+
}
|
|
1417
1548
|
|
|
1418
1549
|
// src/commands/read.ts
|
|
1419
1550
|
var {$: $2 } = globalThis.Bun;
|
|
1420
1551
|
var listUsage = `ev list [file]
|
|
1421
1552
|
|
|
1422
|
-
Prints every key of the folder's .env* files, or of one file, with secret values masked: a few leading characters and the length under a key named like TOKEN, SECRET, PASSWORD, KEY, PRIVATE or DSN, and the password of any URL.
|
|
1553
|
+
Prints every key of the folder's .env* files, or of one file, with secret values masked: a few leading characters and the length under a key named like TOKEN, SECRET, PASSWORD, KEY, PRIVATE or DSN, and the password and tokens of any URL.
|
|
1423
1554
|
|
|
1424
1555
|
Exits 0 when it printed them, 1 when there is no .env* file, 2 on a usage error.`;
|
|
1425
1556
|
var getUsage = `ev get KEY [file]
|
|
@@ -1533,6 +1664,7 @@ process8.exitCode = await runCli({
|
|
|
1533
1664
|
show: { run: show, summary: "an env file at a git revision, values masked" },
|
|
1534
1665
|
set: { run: set, summary: "set a key, the value read from stdin or -F" },
|
|
1535
1666
|
unset: { run: unset, summary: "remove a key from a file" },
|
|
1667
|
+
cp: { run: cp, summary: "copy a key from one file to another, the value never printed" },
|
|
1536
1668
|
mv: { run: mv, summary: "move a key from one file to another" },
|
|
1537
1669
|
diff: { run: diff, summary: "keys set in one file and not the other" },
|
|
1538
1670
|
missing: { run: missing, summary: "keys of .env.example that .env lacks" },
|
package/dist/redact.js
CHANGED
|
@@ -343,6 +343,26 @@ function printJson(value) {
|
|
|
343
343
|
console.log(JSON.stringify(value, undefined, 2));
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// ../kit/src/text.ts
|
|
347
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
348
|
+
function distance(a, b) {
|
|
349
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
350
|
+
for (const [i, left] of [...a].entries()) {
|
|
351
|
+
const current = [i + 1];
|
|
352
|
+
for (const [j, right] of [...b].entries())
|
|
353
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
354
|
+
previous = current;
|
|
355
|
+
}
|
|
356
|
+
return previous[b.length] ?? 0;
|
|
357
|
+
}
|
|
358
|
+
function closest(name, names) {
|
|
359
|
+
const started = names.find((known) => known.startsWith(name));
|
|
360
|
+
if (started !== undefined)
|
|
361
|
+
return started;
|
|
362
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
363
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
346
366
|
// ../kit/src/args.ts
|
|
347
367
|
function jsonOption(holds) {
|
|
348
368
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
@@ -362,8 +382,8 @@ function fail(message, code = 2, hint) {
|
|
|
362
382
|
}
|
|
363
383
|
|
|
364
384
|
class UsageError extends Exit {
|
|
365
|
-
constructor(message) {
|
|
366
|
-
super(message, 2);
|
|
385
|
+
constructor(message, hint) {
|
|
386
|
+
super(message, 2, hint);
|
|
367
387
|
}
|
|
368
388
|
}
|
|
369
389
|
function badUsage(message) {
|
|
@@ -386,6 +406,7 @@ var completion = { active: false };
|
|
|
386
406
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
387
407
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
388
408
|
var FULL_STOP = /\.$/;
|
|
409
|
+
var DASHES = /^-+/;
|
|
389
410
|
function usageLine(usage, colors = paint(process4.stdout)) {
|
|
390
411
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
391
412
|
}
|
|
@@ -422,10 +443,10 @@ function optionRows(options, colors = paint(process4.stdout)) {
|
|
|
422
443
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
423
444
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
424
445
|
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 };
|
|
446
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
|
|
426
447
|
});
|
|
427
448
|
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)}`);
|
|
449
|
+
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
429
450
|
}
|
|
430
451
|
var OUTPUT_OPTIONS = {
|
|
431
452
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
@@ -457,7 +478,8 @@ function parse(args, usage, options, settings = {}) {
|
|
|
457
478
|
try {
|
|
458
479
|
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
459
480
|
} catch (error) {
|
|
460
|
-
|
|
481
|
+
const message = parseError(error);
|
|
482
|
+
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
461
483
|
}
|
|
462
484
|
const values = parsed.values;
|
|
463
485
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
@@ -500,6 +522,22 @@ function parseError(error) {
|
|
|
500
522
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
501
523
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
502
524
|
}
|
|
525
|
+
var UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
|
|
526
|
+
function nearestOption(message, options, allowNegative) {
|
|
527
|
+
const typed = UNKNOWN_OPTION.exec(message)?.[1];
|
|
528
|
+
if (typed === undefined)
|
|
529
|
+
return;
|
|
530
|
+
const word = typed.replace(DASHES, "");
|
|
531
|
+
if (word.length < 2 && !typed.startsWith("--"))
|
|
532
|
+
return;
|
|
533
|
+
const known = Object.entries(options).flatMap(([name, option]) => [
|
|
534
|
+
...option.short === undefined ? [] : [`-${option.short}`],
|
|
535
|
+
`--${name}`,
|
|
536
|
+
...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
|
|
537
|
+
]);
|
|
538
|
+
const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
|
|
539
|
+
return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
|
|
540
|
+
}
|
|
503
541
|
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
504
542
|
if (path === "-") {
|
|
505
543
|
if (process4.stdin.isTTY)
|
|
@@ -513,33 +551,19 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
513
551
|
}
|
|
514
552
|
// ../kit/src/cli.ts
|
|
515
553
|
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
554
|
var LEVEL_OPTIONS = {
|
|
532
555
|
...OUTPUT_OPTIONS,
|
|
533
556
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
534
557
|
};
|
|
535
558
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
536
|
-
function commandRows(commands, colors) {
|
|
559
|
+
async function commandRows(commands, colors) {
|
|
537
560
|
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
538
561
|
const groups = new Map([["Commands:", []]]);
|
|
539
|
-
|
|
562
|
+
const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
|
|
563
|
+
for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
|
|
540
564
|
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
541
565
|
const rows = groups.get(heading) ?? [];
|
|
542
|
-
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(
|
|
566
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
543
567
|
groups.set(heading, rows);
|
|
544
568
|
}
|
|
545
569
|
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
@@ -559,28 +583,38 @@ function takeOutputFlags(argv) {
|
|
|
559
583
|
}
|
|
560
584
|
return argv.slice(index);
|
|
561
585
|
}
|
|
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;
|
|
586
|
+
async function resolveSummary(summary) {
|
|
587
|
+
return typeof summary === "string" ? summary : summary();
|
|
568
588
|
}
|
|
569
|
-
|
|
589
|
+
function scopeOf(commands, extra, aliases) {
|
|
590
|
+
return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
|
|
591
|
+
}
|
|
592
|
+
async function expand(scope, words) {
|
|
593
|
+
if (scope.lazy.length === 0)
|
|
594
|
+
return scope;
|
|
595
|
+
const commands = { ...scope.commands };
|
|
596
|
+
for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
|
|
597
|
+
for (const [name, command] of Object.entries(added))
|
|
598
|
+
commands[name] ??= command;
|
|
599
|
+
}
|
|
600
|
+
return { ...scope, commands, lazy: [] };
|
|
601
|
+
}
|
|
602
|
+
async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
|
|
570
603
|
const colors = paint(stream);
|
|
571
604
|
const prefix = path.join(" ");
|
|
605
|
+
const summary = await resolveSummary(lazySummary);
|
|
572
606
|
if (wantsHelpJson())
|
|
573
607
|
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
574
608
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
575
609
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
576
610
|
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
577
611
|
return [
|
|
578
|
-
header(colors),
|
|
612
|
+
header(colors, summary),
|
|
579
613
|
"",
|
|
580
614
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
581
615
|
...replacesRows(replaces, colors),
|
|
582
616
|
"",
|
|
583
|
-
...commandRows(commands, colors),
|
|
617
|
+
...await commandRows(commands, colors),
|
|
584
618
|
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
585
619
|
"",
|
|
586
620
|
colors.bold("Options:"),
|
|
@@ -626,11 +660,11 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
626
660
|
}
|
|
627
661
|
const [name, ...args] = argv;
|
|
628
662
|
if (name === undefined && level.path.length > 1) {
|
|
629
|
-
console.error(await help(level, process5.stderr));
|
|
663
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stderr));
|
|
630
664
|
return 2;
|
|
631
665
|
}
|
|
632
666
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
633
|
-
console.log(await help(level, process5.stdout));
|
|
667
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stdout));
|
|
634
668
|
return 0;
|
|
635
669
|
}
|
|
636
670
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -639,6 +673,10 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
639
673
|
}
|
|
640
674
|
let command = level.commands[level.aliases?.[name] ?? name];
|
|
641
675
|
let rest = args;
|
|
676
|
+
if (command === undefined && level.lazy.length > 0) {
|
|
677
|
+
level = await expand(level, [...level.path.slice(1), ...argv]);
|
|
678
|
+
command = level.commands[level.aliases?.[name] ?? name];
|
|
679
|
+
}
|
|
642
680
|
if (command === undefined && level.defaultCommand !== undefined) {
|
|
643
681
|
command = level.commands[level.defaultCommand];
|
|
644
682
|
rest = argv;
|
|
@@ -650,7 +688,7 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
650
688
|
}
|
|
651
689
|
if ("commands" in command) {
|
|
652
690
|
const { summary } = command;
|
|
653
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${
|
|
691
|
+
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
692
|
}
|
|
655
693
|
try {
|
|
656
694
|
return await command.run(rest);
|
|
@@ -675,18 +713,24 @@ async function optionsOf(run) {
|
|
|
675
713
|
async function leafCandidates(run, words) {
|
|
676
714
|
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
677
715
|
return [FILES];
|
|
678
|
-
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
716
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
679
717
|
}
|
|
680
|
-
async function candidates(
|
|
718
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
681
719
|
const [word = "", ...rest] = words;
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
720
|
+
const listing = rest.length === 0 && !word.startsWith("-");
|
|
721
|
+
let { commands, aliases } = scope;
|
|
722
|
+
let command = commands[aliases?.[word] ?? word];
|
|
723
|
+
if (listing || command === undefined) {
|
|
724
|
+
({ commands, aliases } = await expand(scope, [...before, ...words]));
|
|
725
|
+
command = commands[aliases?.[word] ?? word];
|
|
726
|
+
}
|
|
727
|
+
if (listing)
|
|
728
|
+
return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
|
|
685
729
|
if (command === undefined) {
|
|
686
730
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
687
731
|
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
688
732
|
}
|
|
689
|
-
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
733
|
+
return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
|
|
690
734
|
}
|
|
691
735
|
async function printCandidates(found) {
|
|
692
736
|
for (const line of await found)
|
|
@@ -695,8 +739,8 @@ async function printCandidates(found) {
|
|
|
695
739
|
}
|
|
696
740
|
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
697
741
|
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)} ${
|
|
742
|
+
return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
|
|
743
|
+
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
744
|
}
|
|
701
745
|
async function runMain({ name, version, run, redact }, argv = process5.argv.slice(2)) {
|
|
702
746
|
if (argv[0] === COMPLETE)
|
|
@@ -712,10 +756,11 @@ async function runMain({ name, version, run, redact }, argv = process5.argv.slic
|
|
|
712
756
|
}
|
|
713
757
|
}
|
|
714
758
|
// package.json
|
|
715
|
-
var version = "0.
|
|
759
|
+
var version = "0.6.0";
|
|
716
760
|
|
|
717
761
|
// src/files.ts
|
|
718
|
-
import { readdir } from "fs/promises";
|
|
762
|
+
import { chmod, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
763
|
+
import { basename, dirname, join } from "path";
|
|
719
764
|
|
|
720
765
|
// src/dotenv.ts
|
|
721
766
|
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
@@ -838,6 +883,17 @@ var TOKEN_FORMATS = [
|
|
|
838
883
|
["Slack token", /\bxox[abprs]-[\w-]{10,}/g],
|
|
839
884
|
["JWT", /\beyJ[\w-]+\.[\w-]+\.[\w-]+/g]
|
|
840
885
|
];
|
|
886
|
+
var URL_PARTS = /\b[a-z][a-z0-9+.-]*:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
887
|
+
var PATH_SEGMENT = /[^/]+/g;
|
|
888
|
+
var QUERY_PARAM = /([^&=]+)=([^&]*)/g;
|
|
889
|
+
var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
|
|
890
|
+
var WEBHOOK_FORMATS = [
|
|
891
|
+
["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
|
|
892
|
+
["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
|
|
893
|
+
["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
|
|
894
|
+
["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
|
|
895
|
+
];
|
|
896
|
+
var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
|
|
841
897
|
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
842
898
|
var PEM_END = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
843
899
|
function isSecretKey(key) {
|
|
@@ -861,6 +917,39 @@ function knownSecrets(pairs) {
|
|
|
861
917
|
function urlPasswords(text) {
|
|
862
918
|
return [...text.matchAll(URL_PASSWORD)].flatMap((url) => url[1] ?? []);
|
|
863
919
|
}
|
|
920
|
+
function urlTokens(line) {
|
|
921
|
+
const found = [];
|
|
922
|
+
const add = (kind, start, value) => {
|
|
923
|
+
found.push({ start, end: start + value.length, kind, mask: hint(value) });
|
|
924
|
+
};
|
|
925
|
+
for (const url of line.matchAll(URL_PARTS)) {
|
|
926
|
+
const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
|
|
927
|
+
const authorityStart = url.indices?.[1]?.[0];
|
|
928
|
+
const pathStart = url.indices?.[2]?.[0];
|
|
929
|
+
const queryStart = url.indices?.[3]?.[0];
|
|
930
|
+
if (authorityStart === undefined || pathStart === undefined)
|
|
931
|
+
continue;
|
|
932
|
+
const hostAt = authority.lastIndexOf("@") + 1;
|
|
933
|
+
const target = authority.slice(hostAt) + path;
|
|
934
|
+
for (const [kind, pattern] of WEBHOOK_FORMATS) {
|
|
935
|
+
const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
|
|
936
|
+
if (start !== undefined && end !== undefined)
|
|
937
|
+
add(kind, authorityStart + hostAt + start, target.slice(start, end));
|
|
938
|
+
}
|
|
939
|
+
for (const segment of path.matchAll(PATH_SEGMENT)) {
|
|
940
|
+
if (OPAQUE_SEGMENT.test(segment[0]))
|
|
941
|
+
add("URL token", pathStart + segment.index, segment[0]);
|
|
942
|
+
}
|
|
943
|
+
if (query === undefined || queryStart === undefined)
|
|
944
|
+
continue;
|
|
945
|
+
for (const param of query.matchAll(QUERY_PARAM)) {
|
|
946
|
+
const [name, value] = [param[1] ?? "", param[2] ?? ""];
|
|
947
|
+
if (value !== "" && SECRET_PARAM.test(name))
|
|
948
|
+
add("URL token", queryStart + param.index + name.length + 1, value);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return found.filter((match) => match.end > match.start);
|
|
952
|
+
}
|
|
864
953
|
function findSecrets(line, known) {
|
|
865
954
|
const found = [];
|
|
866
955
|
for (const url of line.matchAll(URL_PASSWORD)) {
|
|
@@ -872,6 +961,7 @@ function findSecrets(line, known) {
|
|
|
872
961
|
for (const token of line.matchAll(pattern))
|
|
873
962
|
found.push({ start: token.index, end: token.index + token[0].length, kind, mask: hint(token[0]) });
|
|
874
963
|
}
|
|
964
|
+
found.push(...urlTokens(line));
|
|
875
965
|
if (known?.pattern !== undefined) {
|
|
876
966
|
for (const value of line.matchAll(known.pattern))
|
|
877
967
|
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
@@ -913,7 +1003,20 @@ async function readEnv(path) {
|
|
|
913
1003
|
return parseDotenv(await file.text());
|
|
914
1004
|
}
|
|
915
1005
|
async function writeEnv(path, file) {
|
|
916
|
-
|
|
1006
|
+
let target = path;
|
|
1007
|
+
let mode = 384;
|
|
1008
|
+
try {
|
|
1009
|
+
target = await realpath(path);
|
|
1010
|
+
mode = (await stat(target)).mode & 511;
|
|
1011
|
+
} catch {}
|
|
1012
|
+
const temp = join(dirname(target), `.${basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1013
|
+
try {
|
|
1014
|
+
await writeFile(temp, serializeDotenv(file), { mode: 384, flag: "wx" });
|
|
1015
|
+
await chmod(temp, mode);
|
|
1016
|
+
await rename(temp, target);
|
|
1017
|
+
} finally {
|
|
1018
|
+
await rm(temp, { force: true });
|
|
1019
|
+
}
|
|
917
1020
|
}
|
|
918
1021
|
async function loadKnown(folder = process.cwd()) {
|
|
919
1022
|
const pairs = [];
|
|
@@ -1028,7 +1131,7 @@ function check(text, known) {
|
|
|
1028
1131
|
// src/redact.ts
|
|
1029
1132
|
var usage = `redact [--check]
|
|
1030
1133
|
|
|
1031
|
-
Copies stdin to stdout line by line with secrets masked: known token formats (ghp_, github_pat_, glpat-, sk-, xox?-, JWTs), private key blocks, the password of any URL and the values of secret-looking keys in this folder's .env* files.
|
|
1134
|
+
Copies stdin to stdout line by line with secrets masked: known token formats (ghp_, github_pat_, glpat-, sk-, xox?-, JWTs), 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
1135
|
--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
1136
|
|
|
1034
1137
|
Exits 0 when it copied the input or found no secret, 1 when --check finds one, 2 on a usage error.`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/env-tools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "ev lists and edits .env files with secrets masked, redact masks secrets in any piped output.",
|
|
6
6
|
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|