@indigoai-us/hq-cli 5.345.84 → 5.345.86

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.
@@ -0,0 +1,209 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { readProcStarttime } from "./spawn.js";
5
+ // The runner writes loop.json after node start-up and a CPU sample, measured at
6
+ // 0.8 to 1.8 s past the whole second `ps lstart` reports. A process that reused
7
+ // the pid started after the recorded runner was alive at started_at, so its
8
+ // lstart cannot fall a full second before started_at; the upper bound only
9
+ // caps how slow a start-up is believed.
10
+ const LOOP_START_WINDOW_MS = 60_000;
11
+ function loopStartMatches(processStartedAt, loopStartedAt) {
12
+ if (processStartedAt === null || !Number.isFinite(loopStartedAt))
13
+ return false;
14
+ const delta = loopStartedAt - processStartedAt;
15
+ return delta > -1_000 && delta < LOOP_START_WINDOW_MS;
16
+ }
17
+ function poolSlots(text) {
18
+ const slots = [];
19
+ let inBlock = false;
20
+ let current;
21
+ const save = () => {
22
+ if (current?.workerId && current.status && current.pid && current.runDir)
23
+ slots.push(current);
24
+ current = undefined;
25
+ };
26
+ for (const line of text.split(/\r?\n/)) {
27
+ if (!inBlock) {
28
+ if (line === "conduct_pool:")
29
+ inBlock = true;
30
+ continue;
31
+ }
32
+ if (/^[^ \t]/.test(line)) {
33
+ save();
34
+ break;
35
+ }
36
+ const worker = /^ {2}- worker_id:\s*"?([^"\s]+)"?\s*$/.exec(line);
37
+ if (worker) {
38
+ save();
39
+ current = { workerId: worker[1] };
40
+ continue;
41
+ }
42
+ if (!current)
43
+ continue;
44
+ const field = /^ {4}(status|pid|run_dir):\s*(.*?)\s*$/.exec(line);
45
+ if (!field)
46
+ continue;
47
+ const value = field[2].replace(/^"|"$/g, "");
48
+ if (field[1] === "status")
49
+ current.status = value;
50
+ if (field[1] === "pid" && /^\d+$/.test(value))
51
+ current.pid = Number(value);
52
+ if (field[1] === "run_dir")
53
+ current.runDir = value;
54
+ }
55
+ save();
56
+ return slots;
57
+ }
58
+ function processSnapshot(pid) {
59
+ const argsResult = spawnSync("ps", ["-ww", "-p", String(pid), "-o", "args="], { encoding: "utf8", windowsHide: true });
60
+ if (argsResult.status !== 0)
61
+ return null;
62
+ const startResult = spawnSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", windowsHide: true });
63
+ const startedAt = startResult.status === 0 ? Date.parse(startResult.stdout.trim()) : NaN;
64
+ return { args: argsResult.stdout.trim(), startedAt: Number.isFinite(startedAt) ? startedAt : null };
65
+ }
66
+ function readProcessList() {
67
+ const result = spawnSync("ps", ["-ww", "-axo", "pid=,ppid=,args="], { encoding: "utf8", windowsHide: true });
68
+ if (result.status !== 0)
69
+ return [];
70
+ return result.stdout.split("\n").flatMap((line) => {
71
+ const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
72
+ return match ? [{ pid: Number(match[1]), ppid: Number(match[2]), args: match[3] ?? "" }] : [];
73
+ });
74
+ }
75
+ function hasLiveSessionAncestor(pid, processes) {
76
+ const byPid = new Map(processes.map((proc) => [proc.pid, proc]));
77
+ let current = byPid.get(pid);
78
+ const seen = new Set();
79
+ for (let hops = 0; current && current.pid > 1 && hops < 64; hops++) {
80
+ if (seen.has(current.pid))
81
+ return true;
82
+ seen.add(current.pid);
83
+ const argv = current.args.trim().split(/\s+/);
84
+ const base = (arg) => arg?.split(/[\\/]/).at(-1)?.toLowerCase();
85
+ const names = new Set(["claude", "codex", "codex-code-mode-host", "grok", "grok-cli"]);
86
+ if (names.has(base(argv[0]) ?? "") || ((["bash", "sh", "zsh", "dash", "ksh", "node", "env"].includes(base(argv[0]) ?? "")) && names.has(base(argv[1]) ?? "")))
87
+ return true;
88
+ current = byPid.get(current.ppid);
89
+ }
90
+ return false;
91
+ }
92
+ function sessionCompany(meta) {
93
+ return /^company_slug:\s*["']?([A-Za-z0-9._-]+)["']?\s*$/m.exec(meta)?.[1];
94
+ }
95
+ function loopRecords(root) {
96
+ const sessions = path.join(root, "workspace", "sessions");
97
+ const runnerRoot = path.resolve(root, "workspace", "tmp", "workflow-runner");
98
+ let sessionIds;
99
+ try {
100
+ sessionIds = fs.readdirSync(sessions);
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ const records = [];
106
+ for (const sessionId of sessionIds) {
107
+ const metaPath = path.join(sessions, sessionId, "meta.yaml");
108
+ let slots;
109
+ let meta;
110
+ try {
111
+ meta = fs.readFileSync(metaPath, "utf8");
112
+ slots = poolSlots(meta);
113
+ }
114
+ catch {
115
+ continue;
116
+ }
117
+ const companySlug = sessionCompany(meta);
118
+ for (const slot of slots) {
119
+ if (!Number.isSafeInteger(slot.pid) || slot.pid <= 1 || !["running", "waiting"].includes(slot.status))
120
+ continue;
121
+ const runDir = path.resolve(slot.runDir);
122
+ if (!runDir.startsWith(`${runnerRoot}${path.sep}`))
123
+ continue;
124
+ let loop;
125
+ try {
126
+ loop = JSON.parse(fs.readFileSync(path.join(runDir, "loop.json"), "utf8"));
127
+ }
128
+ catch {
129
+ continue;
130
+ }
131
+ if (loop.pid !== slot.pid || path.resolve(loop.run_dir ?? "") !== runDir)
132
+ continue;
133
+ records.push({ sessionId, ...(companySlug ? { companySlug } : {}), slot, loop });
134
+ }
135
+ }
136
+ return records;
137
+ }
138
+ export function findLegacyPoolLanes(root, deps = {}) {
139
+ const processes = (deps.processes ?? (() => {
140
+ const result = spawnSync("ps", ["-ww", "-axo", "pid=,ppid=,args="], { encoding: "utf8", windowsHide: true });
141
+ if (result.status !== 0)
142
+ return [];
143
+ return result.stdout.split("\n").flatMap((line) => {
144
+ const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
145
+ return match ? [{ pid: Number(match[1]), ppid: Number(match[2]), args: match[3] ?? "" }] : [];
146
+ });
147
+ }))();
148
+ const alive = deps.isAlive ?? ((pid) => { try {
149
+ process.kill(pid, 0);
150
+ return true;
151
+ }
152
+ catch {
153
+ return false;
154
+ } });
155
+ const readStart = deps.readStarttime ?? readProcStarttime;
156
+ const now = deps.now?.() ?? new Date();
157
+ return loopRecords(root).flatMap(({ sessionId, companySlug, slot, loop }) => {
158
+ if (!alive(slot.pid))
159
+ return [];
160
+ const proc = (deps.snapshotProcess ?? processSnapshot)(slot.pid);
161
+ if (!proc)
162
+ return [];
163
+ const argsMatch = /(?:^|[\\/])workflow-runner\.mjs(?:\s|$)/.test(proc.args) && /(?:^|\s)--loop(?:\s|$)/.test(proc.args);
164
+ const identityMatches = argsMatch && loopStartMatches(proc.startedAt, Date.parse(loop.started_at ?? ""));
165
+ const ownerState = deps.sessionState?.(root, sessionId, now) ?? null;
166
+ if (deps.sessionState ? ownerState !== "ended" && ownerState !== "lost" : hasLiveSessionAncestor(slot.pid, processes))
167
+ return [];
168
+ const laneId = `legacy-${sessionId}-${slot.workerId}`;
169
+ return [{
170
+ lane_id: laneId,
171
+ legacy: true,
172
+ orphaned: true,
173
+ reason: identityMatches ? "legacy_conduct_session_gone" : "legacy_pid_reused_or_identity_unproven",
174
+ age_minutes: Number.isFinite(Date.parse(loop.started_at ?? ""))
175
+ ? Math.max(0, Math.floor((now.getTime() - Date.parse(loop.started_at)) / 60_000))
176
+ : 0,
177
+ session_id: sessionId,
178
+ ...(companySlug ? { company_slug: companySlug } : {}),
179
+ pid: slot.pid,
180
+ process_starttime: readStart(slot.pid),
181
+ loop_started_at: loop.started_at ?? "",
182
+ run_dir: slot.runDir,
183
+ ...(!identityMatches ? { would_stop: false } : {}),
184
+ }];
185
+ }).sort((a, b) => a.lane_id.localeCompare(b.lane_id));
186
+ }
187
+ export function stopLegacyPoolLane(root, finding, deps = {}) {
188
+ const processes = (deps.processes ?? readProcessList)();
189
+ const proc = (deps.snapshotProcess ?? processSnapshot)(finding.pid);
190
+ const readStart = deps.readStarttime ?? readProcStarttime;
191
+ if (!proc || finding.process_starttime === null || readStart(finding.pid) !== finding.process_starttime || !loopStartMatches(proc.startedAt, Date.parse(finding.loop_started_at)) || !/(?:^|[\\/])workflow-runner\.mjs(?:\s|$)/.test(proc.args) || !/(?:^|\s)--loop(?:\s|$)/.test(proc.args))
192
+ return false;
193
+ const signal = deps.signal ?? ((pid, value) => process.kill(pid, value));
194
+ if (processes.length > 0 && hasLiveSessionAncestor(finding.pid, processes))
195
+ return false;
196
+ if (deps.sessionState) {
197
+ const ownerState = deps.sessionState(root, finding.session_id, deps.now?.() ?? new Date());
198
+ if (ownerState !== "ended" && ownerState !== "lost")
199
+ return false;
200
+ }
201
+ try {
202
+ signal(finding.pid, "SIGTERM");
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ return true;
208
+ }
209
+ //# sourceMappingURL=legacy-pool.js.map
@@ -1,4 +1,5 @@
1
1
  import type { LaneRecord } from "./types.js";
2
+ import { type LegacyPoolFinding } from "./legacy-pool.js";
2
3
  export declare const DEFAULT_REAP_MIN_AGE_MINUTES = 120;
3
4
  export declare const REAP_SINGLE_FLIGHT_LOCK_STALE_MS: number;
4
5
  export declare const REAP_RECENT_SWEEP_MS: number;
@@ -20,6 +21,8 @@ export interface ReapFinding {
20
21
  stop_complete?: boolean;
21
22
  stop_error?: string;
22
23
  stale_state?: string[];
24
+ legacy?: true;
25
+ run_dir?: string;
23
26
  }
24
27
  export interface ReapDeps {
25
28
  now?: () => Date;
@@ -31,6 +34,15 @@ export interface ReapDeps {
31
34
  stopEnabled?: (lane: LaneRecord) => Promise<boolean> | boolean;
32
35
  stopLane?: (laneId: string, reason: string) => Promise<void>;
33
36
  sleep?: (ms: number) => void;
37
+ rootScopedStateEnabled?: () => Promise<boolean> | boolean;
38
+ waitingSeniorEnabled?: () => Promise<boolean> | boolean;
39
+ legacyEnabled?: () => Promise<boolean> | boolean;
40
+ legacyStopEnabled?: () => Promise<boolean> | boolean;
41
+ stopLegacy?: (finding: LegacyPoolFinding) => boolean;
42
+ snapshotProcess?: (pid: number) => {
43
+ args: string;
44
+ startedAt: number | null;
45
+ } | null;
34
46
  }
35
47
  export declare function parseReapMinAge(value: string | undefined): number;
36
48
  export declare function isProcessRunning(pid: number): boolean;
@@ -39,6 +51,7 @@ export interface ReapOptions {
39
51
  minAgeMinutes: number;
40
52
  stop: boolean;
41
53
  applyStaleState: boolean;
54
+ legacy?: boolean;
42
55
  reason: string;
43
56
  json: boolean;
44
57
  }
@@ -1,5 +1,6 @@
1
1
  /** Session-start orphan lane sweep. Every destructive action is opt-in and gated. */
2
2
  import * as fs from "node:fs";
3
+ import { createHash } from "node:crypto";
3
4
  import * as os from "node:os";
4
5
  import * as path from "node:path";
5
6
  import { spawnSync } from "node:child_process";
@@ -7,6 +8,7 @@ import { laneRunsOnHost } from "./admission.js";
7
8
  import { laneRunDir, isPidRecordAlive, readPidRecord, readProcStarttime } from "./spawn.js";
8
9
  import { readAllLanes, readLane } from "./store.js";
9
10
  import { LanesError } from "./errors.js";
11
+ import { findLegacyPoolLanes, stopLegacyPoolLane } from "./legacy-pool.js";
10
12
  export const DEFAULT_REAP_MIN_AGE_MINUTES = 120;
11
13
  export const REAP_SINGLE_FLIGHT_LOCK_STALE_MS = 60 * 60 * 1000;
12
14
  export const REAP_RECENT_SWEEP_MS = 30 * 60 * 1000;
@@ -15,6 +17,14 @@ const staleMarkerSnapshots = new WeakMap();
15
17
  function safeHostComponent(hostname) {
16
18
  return hostname.replace(/[^A-Za-z0-9._-]/g, "_") || "unknown-host";
17
19
  }
20
+ function resolvedRootKey(root) {
21
+ let resolved = path.resolve(root);
22
+ try {
23
+ resolved = fs.realpathSync(root);
24
+ }
25
+ catch { /* use the normalized input path */ }
26
+ return createHash("sha256").update(resolved).digest("hex").slice(0, 12);
27
+ }
18
28
  function reaperStateDirectory() {
19
29
  const stateRoot = process.env.HQ_LANES_REAPER_STATE_HOME?.trim() ||
20
30
  process.env.XDG_STATE_HOME?.trim() || path.join(os.homedir(), ".local", "state");
@@ -231,8 +241,15 @@ export async function classifyLocalLanes(root, now, minAgeMinutes, deps = {}) {
231
241
  const sessionState = deps.sessionState?.(root, lane.senior.id, now) ?? null;
232
242
  if (sessionState !== "ended" && sessionState !== "lost")
233
243
  finding.reason = sessionState === "live" ? "senior_session_alive" : "senior_session_not_proven_dead";
234
- else if (lane.state !== "running")
235
- finding.reason = "lane_not_running";
244
+ else if (lane.state !== "running") {
245
+ const waitingEnabled = deps.waitingSeniorEnabled ? await deps.waitingSeniorEnabled() : true;
246
+ if (waitingEnabled && ["queued", "awaiting_input", "blocked"].includes(lane.state)) {
247
+ finding.orphaned = true;
248
+ finding.reason = "senior_ended_while_waiting";
249
+ }
250
+ else
251
+ finding.reason = "lane_not_running";
252
+ }
236
253
  else if (lane.keep_alive === true || lane.keep_alive_standing === true)
237
254
  finding.reason = "keep_alive";
238
255
  else if (lane.senior_history.some((entry) => entry.reason === "reparented"))
@@ -276,7 +293,10 @@ export async function reapLocalLanes(root, options, deps = {}) {
276
293
  const stateDir = reaperStateDirectory();
277
294
  fs.mkdirSync(stateDir, { recursive: true });
278
295
  const host = safeHostComponent(deps.hostname?.() ?? os.hostname());
279
- const lockDir = path.join(stateDir, `.lock-${host}`);
296
+ const rootScoped = deps.rootScopedStateEnabled ? await deps.rootScopedStateEnabled() : true;
297
+ const rootKey = resolvedRootKey(root);
298
+ const stateKey = rootScoped ? `${host}-${rootKey}` : host;
299
+ const lockDir = path.join(stateDir, `.lock-${stateKey}`);
280
300
  try {
281
301
  fs.mkdirSync(lockDir);
282
302
  }
@@ -301,7 +321,7 @@ export async function reapLocalLanes(root, options, deps = {}) {
301
321
  return { ok: true, action: "lanes.reap", generated_at: now.toISOString(), min_age_minutes: options.minAgeMinutes, mode: "report", stop_enabled: false, skipped: "already_running", lanes: [] };
302
322
  }
303
323
  }
304
- const stamp = path.join(stateDir, `.last-sweep-${host}`);
324
+ const stamp = path.join(stateDir, `.last-sweep-${stateKey}`);
305
325
  if (!options.stop && !options.applyStaleState) {
306
326
  try {
307
327
  const stampText = fs.readFileSync(stamp, "utf8").trim();
@@ -319,6 +339,9 @@ export async function reapLocalLanes(root, options, deps = {}) {
319
339
  }
320
340
  try {
321
341
  const findings = await classifyLocalLanes(root, now, options.minAgeMinutes, deps);
342
+ const legacyEnabled = options.legacy && (deps.legacyEnabled ? await deps.legacyEnabled() : true);
343
+ const legacyFindings = legacyEnabled ? findLegacyPoolLanes(root, deps) : [];
344
+ findings.push(...legacyFindings);
322
345
  const isAlive = deps.isAlive ?? isProcessRunning;
323
346
  const readStart = deps.readStarttime ?? readProcStarttime;
324
347
  const clearStaleMarkerIfUnchanged = (finding, marker) => {
@@ -341,10 +364,25 @@ export async function reapLocalLanes(root, options, deps = {}) {
341
364
  clearStaleMarkerIfUnchanged(finding, marker);
342
365
  }
343
366
  if (!finding.orphaned || !options.stop || finding.pid === undefined) {
344
- if (finding.orphaned && options.stop)
367
+ if (finding.orphaned && options.stop && finding.pid !== undefined)
345
368
  finding.would_stop = true;
346
369
  continue;
347
370
  }
371
+ if (finding.legacy) {
372
+ const enabled = await (deps.legacyEnabled?.() ?? true) && await (deps.legacyStopEnabled?.() ?? false);
373
+ stopEnabled ||= enabled;
374
+ if (!enabled || finding.reason === "legacy_pid_reused_or_identity_unproven") {
375
+ finding.would_stop = false;
376
+ continue;
377
+ }
378
+ const legacy = finding;
379
+ const stopped = (deps.stopLegacy ?? ((item) => stopLegacyPoolLane(root, item, deps)))(legacy);
380
+ finding.stopped = stopped;
381
+ finding.would_stop = !stopped;
382
+ if (!stopped)
383
+ finding.reason = "legacy_identity_changed_before_stop";
384
+ continue;
385
+ }
348
386
  const lane = readLane(root, finding.lane_id);
349
387
  const enabled = await (deps.stopEnabled?.(lane) ?? false);
350
388
  stopEnabled ||= enabled;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Secret detection over text fragments, following gitleaks' detection model:
3
+ * global path allowlist, per-rule path filter, keyword prefilter, regex match,
4
+ * secret-group selection, Shannon-entropy floor, allowlists (OR/AND; target
5
+ * secret, match, or line; stopwords), and the inline `gitleaks:allow` marker.
6
+ *
7
+ * Findings carry rule, file and line only. The secret value never leaves this
8
+ * module.
9
+ */
10
+ import { type CompiledRuleSet } from "./rules.js";
11
+ import type { SecretFinding } from "./types.js";
12
+ /** Inline markers that suppress a finding on that line. */
13
+ export declare const ALLOW_MARKERS: readonly ["gitleaks:allow", "hq-secret-scan:allow"];
14
+ export interface Fragment {
15
+ file: string;
16
+ text: string;
17
+ /** 1-based line number of the first line of `text` in the file. */
18
+ startLine: number;
19
+ }
20
+ /** Shannon entropy in bits per character (gitleaks' definition). */
21
+ export declare function shannonEntropy(data: string): number;
22
+ /** True when the global allowlist excludes this path from scanning. */
23
+ export declare function pathIgnored(file: string, rules?: CompiledRuleSet): boolean;
24
+ /** Path-only rules (e.g. a committed .p12 keystore); for binary or whole-file adds. */
25
+ export declare function scanPath(file: string, rules?: CompiledRuleSet): SecretFinding[];
26
+ export declare function scanFragment(frag: Fragment, rules?: CompiledRuleSet): SecretFinding[];
27
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Secret detection over text fragments, following gitleaks' detection model:
3
+ * global path allowlist, per-rule path filter, keyword prefilter, regex match,
4
+ * secret-group selection, Shannon-entropy floor, allowlists (OR/AND; target
5
+ * secret, match, or line; stopwords), and the inline `gitleaks:allow` marker.
6
+ *
7
+ * Findings carry rule, file and line only. The secret value never leaves this
8
+ * module.
9
+ */
10
+ import { compiledRules } from "./rules.js";
11
+ /** Inline markers that suppress a finding on that line. */
12
+ export const ALLOW_MARKERS = ["gitleaks:allow", "hq-secret-scan:allow"];
13
+ /** Shannon entropy in bits per character (gitleaks' definition). */
14
+ export function shannonEntropy(data) {
15
+ if (!data)
16
+ return 0;
17
+ const counts = new Map();
18
+ for (const ch of data)
19
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
20
+ const len = Buffer.byteLength(data, "utf8");
21
+ let entropy = 0;
22
+ for (const count of counts.values()) {
23
+ const freq = count / len;
24
+ entropy -= freq * Math.log2(freq);
25
+ }
26
+ return entropy;
27
+ }
28
+ function allowed(al, c) {
29
+ const target = al.target === "match" ? c.match : al.target === "line" ? c.line : c.secret;
30
+ const checks = [];
31
+ if (al.paths.length)
32
+ checks.push(al.paths.some((p) => p.test(c.file)));
33
+ if (al.regexes.length)
34
+ checks.push(al.regexes.some((r) => r.test(target)));
35
+ if (al.stopwords.length) {
36
+ const lower = c.secret.toLowerCase();
37
+ checks.push(al.stopwords.some((s) => lower.includes(s)));
38
+ }
39
+ if (!checks.length)
40
+ return false;
41
+ return al.condition === "AND" ? checks.every(Boolean) : checks.some(Boolean);
42
+ }
43
+ function lineBounds(text, start, end) {
44
+ const from = text.lastIndexOf("\n", start - 1) + 1;
45
+ const nl = text.indexOf("\n", Math.max(end - 1, start));
46
+ const to = nl < 0 ? text.length : nl;
47
+ let offset = 0;
48
+ for (let i = 0; i < from; i++)
49
+ if (text.charCodeAt(i) === 10)
50
+ offset++;
51
+ return { line: text.slice(from, to), offset };
52
+ }
53
+ /** True when the global allowlist excludes this path from scanning. */
54
+ export function pathIgnored(file, rules = compiledRules()) {
55
+ return rules.global.paths.some((p) => p.test(file));
56
+ }
57
+ /** Path-only rules (e.g. a committed .p12 keystore); for binary or whole-file adds. */
58
+ export function scanPath(file, rules = compiledRules()) {
59
+ if (pathIgnored(file, rules))
60
+ return [];
61
+ return rules.rules
62
+ .filter((r) => !r.regex && r.path?.test(file))
63
+ .map((r) => ({ ruleId: r.id, description: r.description, file, line: 1 }));
64
+ }
65
+ export function scanFragment(frag, rules = compiledRules()) {
66
+ const { file, text, startLine } = frag;
67
+ if (!text || pathIgnored(file, rules))
68
+ return [];
69
+ const lower = text.toLowerCase();
70
+ const seen = new Set();
71
+ const findings = [];
72
+ for (const rule of rules.rules) {
73
+ if (!rule.regex)
74
+ continue;
75
+ if (rule.path && !rule.path.test(file))
76
+ continue;
77
+ if (rule.keywords.length && !rule.keywords.some((k) => lower.includes(k)))
78
+ continue;
79
+ const re = rule.regex;
80
+ re.lastIndex = 0;
81
+ let m;
82
+ while ((m = re.exec(text)) !== null) {
83
+ if (m[0].length === 0) {
84
+ re.lastIndex++;
85
+ continue;
86
+ }
87
+ let secret = m[0];
88
+ if (m.length > 1) {
89
+ if (rule.secretGroup && rule.secretGroup > 0)
90
+ secret = m[rule.secretGroup] ?? "";
91
+ else
92
+ secret = m.slice(1).find((g) => g !== undefined && g.length > 0) ?? m[0];
93
+ }
94
+ if (!secret)
95
+ continue;
96
+ if (rule.entropy !== undefined && rule.entropy !== 0 && shannonEntropy(secret) <= rule.entropy)
97
+ continue;
98
+ const { line, offset } = lineBounds(text, m.index, m.index + m[0].length);
99
+ if (ALLOW_MARKERS.some((mk) => line.includes(mk)))
100
+ continue;
101
+ const cand = { file, secret, match: m[0], line };
102
+ if (allowed(rules.global, cand))
103
+ continue;
104
+ if (rule.allowlists.some((al) => allowed(al, cand)))
105
+ continue;
106
+ const lineNo = startLine + offset;
107
+ const key = `${rule.id}:${lineNo}`;
108
+ if (seen.has(key))
109
+ continue;
110
+ seen.add(key);
111
+ findings.push({ ruleId: rule.id, description: rule.description, file, line: lineNo });
112
+ }
113
+ }
114
+ return dedupeByLine(findings).sort((a, b) => a.line - b.line || a.ruleId.localeCompare(b.ruleId));
115
+ }
116
+ /**
117
+ * One line, one report: when a provider-specific rule fired on a line, drop the
118
+ * catch-all `generic-api-key` and HQ fallback (`hq-*`) findings for that line,
119
+ * as gitleaks does for its generic rule.
120
+ */
121
+ function dedupeByLine(findings) {
122
+ const fallback = (id) => id === "generic-api-key" || id.startsWith("hq-");
123
+ const specific = new Set(findings.filter((f) => !fallback(f.ruleId)).map((f) => f.line));
124
+ const out = findings.filter((f) => !fallback(f.ruleId) || !specific.has(f.line));
125
+ // Between the two fallbacks, prefer the HQ rule (names the provider).
126
+ const hqLines = new Set(out.filter((f) => f.ruleId.startsWith("hq-")).map((f) => f.line));
127
+ return out.filter((f) => f.ruleId !== "generic-api-key" || !hqLines.has(f.line));
128
+ }
129
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1,6 @@
1
+ import type { GitleaksConfig } from "./types.js";
2
+ export declare const GITLEAKS_UPSTREAM_VERSION = "v8.30.1";
3
+ /** Upstream license; shipped with the derived rules as MIT requires. */
4
+ export declare const GITLEAKS_LICENSE = "MIT License\n\nCopyright (c) 2019 Zachary Rice\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n";
5
+ export declare const GITLEAKS_CONFIG: GitleaksConfig;
6
+ //# sourceMappingURL=gitleaks-rules.generated.d.ts.map