rechrome 1.40.0 → 1.41.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/fill-secret.ts CHANGED
@@ -21,7 +21,8 @@ export const SECRET_MASK = "***";
21
21
  export const SECRET_MASK_TTL_MS = 15 * 60_000;
22
22
  /** Shorter values are never masked: masking "1" everywhere would turn every 1 in every output
23
23
  * into ***, corrupting ids and refs for the daemon's lifetime. A TOTP code (6 digits) still is.
24
- * The fill-secret command's own output is still scrubbed of its value, whatever the length. */
24
+ * A short value is kept out of fill-secret's own reply by dropping the echoed code that holds it,
25
+ * and out of its error message by an exact replace; anything the page itself shows is not masked. */
25
26
  export const MIN_MASK_LENGTH = 6;
26
27
  export const maskable = (value: string | undefined | null): value is string => !!value && value.length >= MIN_MASK_LENGTH;
27
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
@@ -23,6 +23,9 @@
23
23
  "listeners.js",
24
24
  "fill-secret.ts",
25
25
  "fill-secret.js",
26
+ "redact.ts",
27
+ "redact.js",
28
+ "secret-rules",
26
29
  "extension",
27
30
  "rechrome.js",
28
31
  "rechrome.ts",
@@ -50,7 +53,7 @@
50
53
  "vendor-cli": "bash scripts/vendor-cli.sh",
51
54
  "prepare": "bash scripts/vendor-cli.sh --prepare",
52
55
  "check-pack": "bash scripts/check-pack.sh",
53
- "prepublishOnly": "bash scripts/vendor-cli.sh && sed 's/\\.ts/\\.js/g' rechrome.ts > rechrome.js && sed 's/\\.ts/\\.js/g' serve.ts > serve.js && bun build extension-token.ts --target=bun --outfile=extension-token.js && bun build daemon-manager.ts --target=bun --outfile=daemon-manager.js && bun build listeners.ts --target=bun --outfile=listeners.js && bun build fill-secret.ts --target=bun --outfile=fill-secret.js && bun build scoped-upload.ts --target=bun --outfile=scoped-upload.js && bun build secret.ts --target=bun --external node-datachannel --outfile=secret.js && bun build shares.ts --target=bun --external node-datachannel --outfile=shares.js && bun build e2e.ts --target=bun --external node-datachannel --outfile=e2e.js && bun build webrtc.ts --target=bun --external node-datachannel --outfile=webrtc.js && bun build signal-relay.ts --target=bun --external node-datachannel --outfile=signal-relay.js && bash scripts/check-pack.sh"
56
+ "prepublishOnly": "bash scripts/vendor-cli.sh && sed 's/\\.ts/\\.js/g' rechrome.ts > rechrome.js && sed 's/\\.ts/\\.js/g' serve.ts > serve.js && bun build extension-token.ts --target=bun --outfile=extension-token.js && bun build daemon-manager.ts --target=bun --outfile=daemon-manager.js && bun build listeners.ts --target=bun --outfile=listeners.js && bun build fill-secret.ts --target=bun --outfile=fill-secret.js && bun build scoped-upload.ts --target=bun --outfile=scoped-upload.js && bun build secret.ts --target=bun --external node-datachannel --outfile=secret.js && bun build shares.ts --target=bun --external node-datachannel --outfile=shares.js && bun build e2e.ts --target=bun --external node-datachannel --outfile=e2e.js && bun build webrtc.ts --target=bun --external node-datachannel --outfile=webrtc.js && bun build signal-relay.ts --target=bun --external node-datachannel --outfile=signal-relay.js && bun build redact.ts --target=bun --outfile=redact.js && bash scripts/check-pack.sh"
54
57
  },
55
58
  "devDependencies": {
56
59
  "@types/bun": "latest",
package/rechrome.js CHANGED
@@ -5,6 +5,7 @@ import { readListeners, writeListeners as writeListenersFile, listenerAddress, i
5
5
  import { file } from "bun";
6
6
  import yargs from "yargs";
7
7
  import { readExtensionTokenFromProfile } from "./extension-token.js";
8
+ import { redact } from "./redact.js";
8
9
  import { FILL_SECRET_USAGE, SECRET_MASK, parseFillSecretArgs, fillSecretWireArgs, readSecretSource, totpCode, totpWaitMs } from "./fill-secret.js";
9
10
  import { createHash, randomBytes } from "crypto";
10
11
  import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync, unlinkSync, readFileSync, readdirSync, renameSync, readlinkSync, rmdirSync, constants as fsConstants } from "fs";
@@ -642,7 +643,7 @@ export async function getOrCreateUrl(persist = true): Promise<string> {
642
643
  const envRaw = await file(globalEnvFile).text().catch(() => "");
643
644
  const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
644
645
  const content = [...lines, newLine, ""].join("\n");
645
- await Bun.write(globalEnvFile, content);
646
+ await writeAtomic(globalEnvFile, content, 0o600); // holds the management key
646
647
  }
647
648
  process.env[ENV_KEY] = url;
648
649
  return url;
@@ -1779,7 +1780,7 @@ async function printProfileUri(selector?: string, listener?: string, opts: { loc
1779
1780
  if (!picked.startsWith(SETUP)) return picked;
1780
1781
  const dir = picked.slice(SETUP.length);
1781
1782
  console.error(`[rech] ${dir} isn't set up yet; running rech setup --profile ${JSON.stringify(dir)} first.`);
1782
- await setup({ profile: dir });
1783
+ await setup({ profile: dir, skipShare: true });
1783
1784
  const after = await readTokenRegistry();
1784
1785
  const key = unregisteredChromeProfiles(after, { [dir]: {} }).length ? undefined : canonicalProfileKeys(after).find(k => after[k].profileDir === dir);
1785
1786
  if (process.exitCode || !key) throw new Error(`Setup didn't register ${dir}; nothing shared. Retry with: rech setup --profile ${JSON.stringify(dir)}`);
@@ -2188,7 +2189,13 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
2188
2189
  }
2189
2190
  const served = await callServe(url, args, overrideEnv, identity, false, upload, secret);
2190
2191
  // The daemon already masks the value; this is a second line of defence for an older daemon.
2191
- const scrub = (s: string) => secret && s ? s.replaceAll(secret, SECRET_MASK) : s;
2192
+ // Then any secret-shaped string (API keys, tokens) in page text, unless RECH_REDACT=0.
2193
+ const scrub = (s: string) => {
2194
+ if (!s) return s;
2195
+ if (secret) s = s.replaceAll(secret, SECRET_MASK);
2196
+ if (process.env.RECH_REDACT === "0") return s;
2197
+ try { return redact(s).text; } catch { return s; } // missing rules must not swallow output
2198
+ };
2192
2199
  const { files, existingSession } = served;
2193
2200
  const status = served.status, stdout = scrub(served.stdout), stderr = scrub(served.stderr);
2194
2201
  if (!opts.bridgeRetried && isBridgeTimeout(args, { status, stderr })) {
@@ -2730,7 +2737,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
2730
2737
  const envRaw = await file(globalEnvFile).text().catch(() => "");
2731
2738
  const lines = envRaw.trimEnd().split("\n");
2732
2739
  const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
2733
- await Bun.write(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"));
2740
+ await writeAtomic(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"), 0o600);
2734
2741
 
2735
2742
  const home = HOME;
2736
2743
  const bunBin = Bun.which("bun") ?? process.execPath;
@@ -3427,7 +3434,6 @@ async function saveGlobalUrl(url: string): Promise<string> {
3427
3434
  return writeEnvUrl(GLOBAL_ENV_FILE, url);
3428
3435
  }
3429
3436
 
3430
- /** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
3431
3437
  /** Write a file whole or not at all: a temp file beside it, then rename (atomic on one filesystem). */
3432
3438
  export async function writeAtomic(path: string, data: string, mode?: number): Promise<void> {
3433
3439
  // A symlinked .env.local stays a link: write the file it points to.
@@ -3464,6 +3470,7 @@ export async function ensureProjectDir(dir: string): Promise<void> {
3464
3470
  if (!existsSync(readme)) await writeAtomic(readme, PROJECT_DIR_README);
3465
3471
  }
3466
3472
 
3473
+ /** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
3467
3474
  async function writeEnvUrl(envPath: string, url: string): Promise<string> {
3468
3475
  const lines = (await file(envPath).text().catch(() => "")).split("\n").filter(l => l.trim() && !l.startsWith(`${ENV_KEY}=`));
3469
3476
  await writeAtomic(envPath, [...lines, envAssignment(ENV_KEY, url), ""].join("\n"), 0o600);
@@ -3974,7 +3981,7 @@ async function setup(opts: SetupOptions = {}): Promise<void> {
3974
3981
  guide?.close();
3975
3982
  console.log(` Saved token for ${extraDisplay}`);
3976
3983
  }
3977
- if (bridgeVerified && isTTY && !opts.listen) await setupShareStep(ask, profileEmail);
3984
+ if (bridgeVerified && isTTY && !opts.listen && !opts.skipShare) await setupShareStep(ask, profileEmail);
3978
3985
  rl?.close();
3979
3986
  envWatcher?.close();
3980
3987
  if (bridgeVerified)
@@ -4423,7 +4430,7 @@ async function status(): Promise<void> {
4423
4430
  }
4424
4431
 
4425
4432
 
4426
- export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean };
4433
+ export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean; /** Called from `rech share`, which shares the profile itself. */ skipShare?: boolean };
4427
4434
  export type RechHandlers = {
4428
4435
  serve(): Promise<void> | void;
4429
4436
  status(): Promise<void>;
package/rechrome.ts CHANGED
@@ -5,6 +5,7 @@ import { readListeners, writeListeners as writeListenersFile, listenerAddress, i
5
5
  import { file } from "bun";
6
6
  import yargs from "yargs";
7
7
  import { readExtensionTokenFromProfile } from "./extension-token.ts";
8
+ import { redact } from "./redact.ts";
8
9
  import { FILL_SECRET_USAGE, SECRET_MASK, parseFillSecretArgs, fillSecretWireArgs, readSecretSource, totpCode, totpWaitMs } from "./fill-secret.ts";
9
10
  import { createHash, randomBytes } from "crypto";
10
11
  import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync, unlinkSync, readFileSync, readdirSync, renameSync, readlinkSync, rmdirSync, constants as fsConstants } from "fs";
@@ -642,7 +643,7 @@ export async function getOrCreateUrl(persist = true): Promise<string> {
642
643
  const envRaw = await file(globalEnvFile).text().catch(() => "");
643
644
  const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
644
645
  const content = [...lines, newLine, ""].join("\n");
645
- await Bun.write(globalEnvFile, content);
646
+ await writeAtomic(globalEnvFile, content, 0o600); // holds the management key
646
647
  }
647
648
  process.env[ENV_KEY] = url;
648
649
  return url;
@@ -1779,7 +1780,7 @@ async function printProfileUri(selector?: string, listener?: string, opts: { loc
1779
1780
  if (!picked.startsWith(SETUP)) return picked;
1780
1781
  const dir = picked.slice(SETUP.length);
1781
1782
  console.error(`[rech] ${dir} isn't set up yet; running rech setup --profile ${JSON.stringify(dir)} first.`);
1782
- await setup({ profile: dir });
1783
+ await setup({ profile: dir, skipShare: true });
1783
1784
  const after = await readTokenRegistry();
1784
1785
  const key = unregisteredChromeProfiles(after, { [dir]: {} }).length ? undefined : canonicalProfileKeys(after).find(k => after[k].profileDir === dir);
1785
1786
  if (process.exitCode || !key) throw new Error(`Setup didn't register ${dir}; nothing shared. Retry with: rech setup --profile ${JSON.stringify(dir)}`);
@@ -2188,7 +2189,13 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
2188
2189
  }
2189
2190
  const served = await callServe(url, args, overrideEnv, identity, false, upload, secret);
2190
2191
  // The daemon already masks the value; this is a second line of defence for an older daemon.
2191
- const scrub = (s: string) => secret && s ? s.replaceAll(secret, SECRET_MASK) : s;
2192
+ // Then any secret-shaped string (API keys, tokens) in page text, unless RECH_REDACT=0.
2193
+ const scrub = (s: string) => {
2194
+ if (!s) return s;
2195
+ if (secret) s = s.replaceAll(secret, SECRET_MASK);
2196
+ if (process.env.RECH_REDACT === "0") return s;
2197
+ try { return redact(s).text; } catch { return s; } // missing rules must not swallow output
2198
+ };
2192
2199
  const { files, existingSession } = served;
2193
2200
  const status = served.status, stdout = scrub(served.stdout), stderr = scrub(served.stderr);
2194
2201
  if (!opts.bridgeRetried && isBridgeTimeout(args, { status, stderr })) {
@@ -2730,7 +2737,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
2730
2737
  const envRaw = await file(globalEnvFile).text().catch(() => "");
2731
2738
  const lines = envRaw.trimEnd().split("\n");
2732
2739
  const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
2733
- await Bun.write(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"));
2740
+ await writeAtomic(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"), 0o600);
2734
2741
 
2735
2742
  const home = HOME;
2736
2743
  const bunBin = Bun.which("bun") ?? process.execPath;
@@ -3427,7 +3434,6 @@ async function saveGlobalUrl(url: string): Promise<string> {
3427
3434
  return writeEnvUrl(GLOBAL_ENV_FILE, url);
3428
3435
  }
3429
3436
 
3430
- /** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
3431
3437
  /** Write a file whole or not at all: a temp file beside it, then rename (atomic on one filesystem). */
3432
3438
  export async function writeAtomic(path: string, data: string, mode?: number): Promise<void> {
3433
3439
  // A symlinked .env.local stays a link: write the file it points to.
@@ -3464,6 +3470,7 @@ export async function ensureProjectDir(dir: string): Promise<void> {
3464
3470
  if (!existsSync(readme)) await writeAtomic(readme, PROJECT_DIR_README);
3465
3471
  }
3466
3472
 
3473
+ /** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
3467
3474
  async function writeEnvUrl(envPath: string, url: string): Promise<string> {
3468
3475
  const lines = (await file(envPath).text().catch(() => "")).split("\n").filter(l => l.trim() && !l.startsWith(`${ENV_KEY}=`));
3469
3476
  await writeAtomic(envPath, [...lines, envAssignment(ENV_KEY, url), ""].join("\n"), 0o600);
@@ -3974,7 +3981,7 @@ async function setup(opts: SetupOptions = {}): Promise<void> {
3974
3981
  guide?.close();
3975
3982
  console.log(` Saved token for ${extraDisplay}`);
3976
3983
  }
3977
- if (bridgeVerified && isTTY && !opts.listen) await setupShareStep(ask, profileEmail);
3984
+ if (bridgeVerified && isTTY && !opts.listen && !opts.skipShare) await setupShareStep(ask, profileEmail);
3978
3985
  rl?.close();
3979
3986
  envWatcher?.close();
3980
3987
  if (bridgeVerified)
@@ -4423,7 +4430,7 @@ async function status(): Promise<void> {
4423
4430
  }
4424
4431
 
4425
4432
 
4426
- export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean };
4433
+ export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean; /** Called from `rech share`, which shares the profile itself. */ skipShare?: boolean };
4427
4434
  export type RechHandlers = {
4428
4435
  serve(): Promise<void> | void;
4429
4436
  status(): Promise<void>;
package/redact.js ADDED
@@ -0,0 +1,236 @@
1
+ // @bun
2
+ // redact.ts
3
+ import { readFileSync } from "fs";
4
+ function goToJs(src) {
5
+ let flags = "";
6
+ for (;; ) {
7
+ const m = /^\(\?([is]+)\)/.exec(src);
8
+ if (!m)
9
+ break;
10
+ flags += m[1];
11
+ src = src.slice(m[0].length);
12
+ }
13
+ src = src.replaceAll("(?P<", "(?<").replaceAll("\\z", "$").replaceAll("[[:alnum:]]", "[A-Za-z0-9]").replaceAll("[:alnum:]", "A-Za-z0-9").replaceAll("[:alpha:]", "A-Za-z").replaceAll("[:digit:]", "0-9").replaceAll("[:space:]", "\\s");
14
+ let out = "";
15
+ const closers = [];
16
+ let extra = 0;
17
+ for (let i = 0;i < src.length; i++) {
18
+ const c = src[i];
19
+ if (c === "\\") {
20
+ out += c + (src[i + 1] ?? "");
21
+ i++;
22
+ continue;
23
+ }
24
+ if (c === "[") {
25
+ let j = i + 1;
26
+ if (src[j] === "^")
27
+ j++;
28
+ if (src[j] === "]")
29
+ j++;
30
+ while (j < src.length && src[j] !== "]") {
31
+ if (src[j] === "\\")
32
+ j++;
33
+ j++;
34
+ }
35
+ out += src.slice(i, j + 1);
36
+ i = j;
37
+ continue;
38
+ }
39
+ if (src.startsWith("(?i)", i)) {
40
+ out += "(?i:";
41
+ extra++;
42
+ i += 3;
43
+ continue;
44
+ }
45
+ if (c === "(") {
46
+ closers.push(extra);
47
+ extra = 0;
48
+ out += c;
49
+ continue;
50
+ }
51
+ if (c === ")") {
52
+ out += ")".repeat(extra);
53
+ extra = closers.pop() ?? 0;
54
+ out += c;
55
+ continue;
56
+ }
57
+ if (c === "|" && extra) {
58
+ out += ")".repeat(extra) + "|" + "(?i:".repeat(extra);
59
+ continue;
60
+ }
61
+ out += c;
62
+ }
63
+ out += ")".repeat(extra);
64
+ return { source: out, flags: [...new Set(flags)].join("") };
65
+ }
66
+ function loadGitleaksRules(toml) {
67
+ const c = Bun.TOML.parse(toml);
68
+ const compile = (r) => {
69
+ const j = goToJs(r);
70
+ return new RegExp(j.source, j.flags);
71
+ };
72
+ const gAllow = (c.allowlist?.regexes ?? []).map(compile);
73
+ const gStop = (c.allowlist?.stopwords ?? []).map((s) => s.toLowerCase());
74
+ const rules = [];
75
+ for (const r of c.rules) {
76
+ if (!r.regex)
77
+ continue;
78
+ const allow = [...gAllow];
79
+ const allowMatch = [];
80
+ const allowLine = [];
81
+ const stop = [...gStop];
82
+ for (const a of [...r.allowlists ?? [], ...r.allowlist ? [r.allowlist] : []]) {
83
+ const into = a.regexTarget === "match" ? allowMatch : a.regexTarget === "line" ? allowLine : allow;
84
+ for (const x of a.regexes ?? [])
85
+ into.push(compile(x));
86
+ for (const s of a.stopwords ?? [])
87
+ stop.push(String(s).toLowerCase());
88
+ }
89
+ const j = goToJs(r.regex);
90
+ rules.push({
91
+ id: r.id,
92
+ re: new RegExp(j.source, j.flags + "gd"),
93
+ keywords: (r.keywords ?? []).map((k) => k.toLowerCase()),
94
+ ...r.entropy ? { entropy: r.entropy } : {},
95
+ ...r.secretGroup ? { group: r.secretGroup } : {},
96
+ allow,
97
+ ...allowMatch.length ? { allowMatch } : {},
98
+ ...allowLine.length ? { allowLine } : {},
99
+ stop
100
+ });
101
+ }
102
+ return rules;
103
+ }
104
+ var OWN_RULES = [
105
+ { id: "url-secret-param", re: /[?&](?:access_token|token|key|api_key|apikey|secret|client_secret|password|passwd|pwd|sig|signature)=([^&\s#<>"'|]{6,})/gid, group: 1, keywords: ["="], allow: [/^(?:x+|\*+|\.\.\.|your[-_]?\w*|xxx\w*)$/i], stop: [] },
106
+ { id: "sk-key", re: /\b(sk-(?:proj-|ant-|or-v1-|svcacct-)?[A-Za-z0-9_-]{20,})/gd, group: 1, keywords: ["sk-"], entropy: 3.5, allow: [], stop: [] },
107
+ { id: "otp-code", re: /(?:code|\u30B3\u30FC\u30C9|otp|pin|passcode|\u8A8D\u8A3C|\u78BA\u8A8D\u756A\u53F7|\u30EF\u30F3\u30BF\u30A4\u30E0)[^\d\n]{0,20}(?<![\d.])(\d{6})(?![\d.])/gid, group: 1, keywords: [], allow: [], stop: [] }
108
+ ];
109
+ var cached;
110
+ function secretRules() {
111
+ cached ??= [...loadGitleaksRules(readFileSync(new URL("./secret-rules/gitleaks.toml", import.meta.url), "utf8")), ...OWN_RULES];
112
+ return cached;
113
+ }
114
+ function shannon(s) {
115
+ const f = new Map;
116
+ for (const ch of s)
117
+ f.set(ch, (f.get(ch) ?? 0) + 1);
118
+ let h = 0;
119
+ for (const n of f.values()) {
120
+ const p = n / s.length;
121
+ h -= p * Math.log2(p);
122
+ }
123
+ return h;
124
+ }
125
+ function findKnown(text, known) {
126
+ const forms = new Set;
127
+ for (const v of known) {
128
+ if (v.length < 6)
129
+ continue;
130
+ forms.add(v);
131
+ forms.add(JSON.stringify(v).slice(1, -1));
132
+ }
133
+ const out = [];
134
+ for (const f of [...forms].sort((a, b) => b.length - a.length)) {
135
+ for (let i = text.indexOf(f);i >= 0; i = text.indexOf(f, i + f.length))
136
+ out.push({ id: "known-secret", start: i, end: i + f.length });
137
+ }
138
+ return out;
139
+ }
140
+ var OPAQUE_RUN = /[A-Za-z0-9+/=_-]{4096,}/g;
141
+ function findSecrets(text, opts = {}) {
142
+ if (text.length >= 4096 && OPAQUE_RUN.test(text)) {
143
+ OPAQUE_RUN.lastIndex = 0;
144
+ const out = opts.known?.length ? findKnown(text, opts.known) : [];
145
+ let at = 0;
146
+ for (const m of [...text.matchAll(OPAQUE_RUN), { index: text.length, 0: "" }]) {
147
+ const piece = text.slice(at, m.index);
148
+ if (piece)
149
+ for (const h of findSecrets(piece, { ...opts, known: [] }))
150
+ out.push({ ...h, start: h.start + at, end: h.end + at });
151
+ at = m.index + m[0].length;
152
+ }
153
+ return out;
154
+ }
155
+ OPAQUE_RUN.lastIndex = 0;
156
+ const rules = opts.rules ?? secretRules();
157
+ const lower = text.toLowerCase();
158
+ const out = opts.known?.length ? findKnown(text, opts.known) : [];
159
+ for (const r of rules) {
160
+ if (r.keywords.length && !r.keywords.some((k) => lower.includes(k)))
161
+ continue;
162
+ for (const m of text.matchAll(r.re)) {
163
+ const gi = r.group ?? m.findIndex((g, i) => i > 0 && !!g);
164
+ const secret = gi > 0 ? m[gi] : m[0];
165
+ if (!secret)
166
+ continue;
167
+ if (r.entropy && shannon(secret) < r.entropy)
168
+ continue;
169
+ if (r.allow.some((a) => a.test(secret)))
170
+ continue;
171
+ if (r.allowMatch?.some((a) => a.test(m[0])))
172
+ continue;
173
+ if (r.allowLine) {
174
+ const ls = text.lastIndexOf(`
175
+ `, m.index) + 1;
176
+ const le = text.indexOf(`
177
+ `, m.index + m[0].length);
178
+ if (r.allowLine.some((a) => a.test(text.slice(ls, le < 0 ? undefined : le))))
179
+ continue;
180
+ }
181
+ const sl = secret.toLowerCase();
182
+ if (r.stop.some((s) => sl.includes(s)))
183
+ continue;
184
+ const span = r.group ? m.indices?.[r.group] : undefined;
185
+ if (span)
186
+ out.push({ id: r.id, start: span[0], end: span[1] });
187
+ else {
188
+ let a = m.index;
189
+ let b = m.index + m[0].length;
190
+ const edge = /[\s'"`;=:,]/;
191
+ while (a < b && edge.test(text[a]))
192
+ a++;
193
+ for (;; ) {
194
+ if (b > a && edge.test(text[b - 1]))
195
+ b--;
196
+ else if (b - 2 >= a && /\\[nr]/.test(text.slice(b - 2, b)))
197
+ b -= 2;
198
+ else
199
+ break;
200
+ }
201
+ out.push({ id: r.id, start: a, end: b });
202
+ }
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+ function redact(text, opts = {}) {
208
+ const hits = findSecrets(text, opts).sort((a, b) => a.start - b.start || b.end - a.end);
209
+ if (!hits.length)
210
+ return { text, kinds: [] };
211
+ const spans = [];
212
+ for (const h of hits) {
213
+ const last = spans[spans.length - 1];
214
+ if (last && h.start < last.end) {
215
+ if (h.end - h.start > last.end - last.start)
216
+ last.id = h.id;
217
+ last.end = Math.max(last.end, h.end);
218
+ } else
219
+ spans.push({ ...h });
220
+ }
221
+ let out = "";
222
+ let at = 0;
223
+ for (const s of spans) {
224
+ out += text.slice(at, s.start) + `[redacted:${s.id}]`;
225
+ at = s.end;
226
+ }
227
+ return { text: out + text.slice(at), kinds: [...new Set(spans.map((s) => s.id))] };
228
+ }
229
+ export {
230
+ OWN_RULES,
231
+ findSecrets,
232
+ goToJs,
233
+ loadGitleaksRules,
234
+ redact,
235
+ secretRules
236
+ };