@indigoai-us/hq-cli 5.345.156 → 5.345.160
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +255 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +535 -10
- package/dist/command-catalog.generated.js +678 -31
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/agents.js +5 -11
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +157 -17
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +71 -8
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/meetings.d.ts +4 -4
- package/dist/commands/meetings.js +17 -8
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/skill.js +6 -6
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
package/dist/lib/mesh/api.js
CHANGED
|
@@ -515,6 +515,81 @@ export async function registerMeshProject(token, companyUid, projectId, channelI
|
|
|
515
515
|
channelCreated: result?.channelCreated === true,
|
|
516
516
|
};
|
|
517
517
|
}
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Project lifecycle (active | completed | archived)
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
export const PROJECT_LIFECYCLE_STATUSES = ["active", "completed", "archived"];
|
|
522
|
+
function lifecycleFromRow(row) {
|
|
523
|
+
const rec = asRecord(row);
|
|
524
|
+
const projectId = String(rec?.projectId || "").trim();
|
|
525
|
+
const status = String(rec?.status || "").trim();
|
|
526
|
+
const version = Number(rec?.version);
|
|
527
|
+
if (!projectId || !Number.isSafeInteger(version) || version < 0)
|
|
528
|
+
return null;
|
|
529
|
+
if (!PROJECT_LIFECYCLE_STATUSES.includes(status))
|
|
530
|
+
return null;
|
|
531
|
+
return {
|
|
532
|
+
projectId,
|
|
533
|
+
status: status,
|
|
534
|
+
version,
|
|
535
|
+
updatedAt: String(rec?.updatedAt || ""),
|
|
536
|
+
updatedBy: String(rec?.updatedBy || ""),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* List every lifecycle row for a company (follows the 25-row server pages).
|
|
541
|
+
* A project with no row has never left `active` and is not listed.
|
|
542
|
+
*/
|
|
543
|
+
export async function listProjectLifecycles(token, companyUid) {
|
|
544
|
+
const rows = [];
|
|
545
|
+
let cursor;
|
|
546
|
+
// Bounded so a server that keeps returning the same cursor cannot loop forever.
|
|
547
|
+
for (let page = 0; page < 200; page += 1) {
|
|
548
|
+
const data = asRecord(await meshJson(token, "/v1/work-mesh/project-lifecycle", {
|
|
549
|
+
query: { companyUid, ...(cursor ? { cursor } : {}) },
|
|
550
|
+
}));
|
|
551
|
+
const projects = Array.isArray(data?.projects) ? data.projects : [];
|
|
552
|
+
for (const row of projects) {
|
|
553
|
+
const parsed = lifecycleFromRow(row);
|
|
554
|
+
if (parsed)
|
|
555
|
+
rows.push(parsed);
|
|
556
|
+
}
|
|
557
|
+
const next = typeof data?.nextCursor === "string" ? data.nextCursor.trim() : "";
|
|
558
|
+
if (!next || next === cursor)
|
|
559
|
+
break;
|
|
560
|
+
cursor = next;
|
|
561
|
+
}
|
|
562
|
+
return rows;
|
|
563
|
+
}
|
|
564
|
+
/** The lifecycle row for one project, or null when it has never been changed. */
|
|
565
|
+
export async function getProjectLifecycle(token, companyUid, projectId) {
|
|
566
|
+
const rows = await listProjectLifecycles(token, companyUid);
|
|
567
|
+
return rows.find((row) => row.projectId === projectId) ?? null;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* PATCH /v1/work-mesh/projects/{id}/lifecycle. `expectedVersion` is the
|
|
571
|
+
* current lifecycle row version (0 when the project has no row yet). The
|
|
572
|
+
* server answers 409 BOARD_NONEMPTY when archiving a Board that still has
|
|
573
|
+
* stories unless `confirmNonempty` is set, and a plain 409 on a version
|
|
574
|
+
* mismatch. Nothing is deleted: stories, folder, channel, members and
|
|
575
|
+
* history stay; the project only leaves the active list and the organize
|
|
576
|
+
* candidates.
|
|
577
|
+
*/
|
|
578
|
+
export async function setProjectLifecycle(token, companyUid, projectId, change) {
|
|
579
|
+
const data = await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/lifecycle`, {
|
|
580
|
+
method: "PATCH",
|
|
581
|
+
query: { companyUid },
|
|
582
|
+
body: {
|
|
583
|
+
status: change.status,
|
|
584
|
+
expectedVersion: change.expectedVersion,
|
|
585
|
+
...(change.confirmNonempty ? { confirmNonempty: true } : {}),
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
const parsed = lifecycleFromRow(data);
|
|
589
|
+
if (!parsed)
|
|
590
|
+
throw new Error("Work mesh returned an invalid project lifecycle row");
|
|
591
|
+
return parsed;
|
|
592
|
+
}
|
|
518
593
|
export function pairDmUidsFromInbox(inbox) {
|
|
519
594
|
const rec = asRecord(inbox);
|
|
520
595
|
const ids = new Set();
|
|
@@ -79,22 +79,6 @@ export declare function recordPlanLimitStatus(body: unknown, opts?: {
|
|
|
79
79
|
* never claims the workspace has stopped working.
|
|
80
80
|
*/
|
|
81
81
|
export declare function buildOverLine(overEntries: Array<[string, PlanLimitEntry]>, upgradeUrl: string): string;
|
|
82
|
-
/**
|
|
83
|
-
* My Telemetry is an Individual-plan feature on a personal scope (owner
|
|
84
|
-
* decision 9 / us-044-individual-plan-design.md §11). When hq-pro refuses it,
|
|
85
|
-
* the CLI prints this notice INSTEAD of telemetry output.
|
|
86
|
-
*
|
|
87
|
-
* It lives beside the nag copy so plan wording stays in one file, but it is
|
|
88
|
-
* NOT part of the nag model: no persistence, no throttle, no episode state. It
|
|
89
|
-
* is rendered once, on the spot, by whoever received the refusal. The refusal
|
|
90
|
-
* is a plan fact, so it neither counts toward the nag bands nor is silenced by
|
|
91
|
-
* the nag cadence.
|
|
92
|
-
*
|
|
93
|
-
* Copy names the plan and its price only — no Outposts, no bot pricing (owner
|
|
94
|
-
* decisions 10 and 11).
|
|
95
|
-
*/
|
|
96
|
-
export declare const INDIVIDUAL_PLAN_NAME = "Individual";
|
|
97
|
-
export declare const INDIVIDUAL_PRICE_LABEL = "$50/mo";
|
|
98
82
|
/**
|
|
99
83
|
* The notice a personal feature refusal prints. `upgradeUrl` is the server's
|
|
100
84
|
* own checkout target; an older hq-pro sends none, and the line then names the
|
|
@@ -367,16 +367,18 @@ export function buildOverLine(overEntries, upgradeUrl) {
|
|
|
367
367
|
const facts = overEntries
|
|
368
368
|
.map(([key, entry]) => `${PLAN_LIMIT_DIMENSION_LABELS[key] ?? key} ${entry.used} of ${entry.limit}`)
|
|
369
369
|
.join(", ");
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
// US-024: names no plan and offers no upgrade; the link is the Billing page.
|
|
371
|
+
return (`⚠ Over this account's limits: ${facts}. New files and new secrets are paused. ` +
|
|
372
|
+
`Billing: ${upgradeUrl}`);
|
|
372
373
|
}
|
|
373
374
|
// ---------------------------------------------------------------------------
|
|
374
|
-
//
|
|
375
|
+
// Personal feature notice (US-054, copy per US-024)
|
|
375
376
|
// ---------------------------------------------------------------------------
|
|
376
377
|
/**
|
|
377
|
-
* My Telemetry is
|
|
378
|
-
*
|
|
379
|
-
*
|
|
378
|
+
* My Telemetry is a paid feature on a personal scope (owner decision 9 /
|
|
379
|
+
* us-044-individual-plan-design.md §11). When hq-pro refuses it, the CLI
|
|
380
|
+
* prints this notice INSTEAD of telemetry output. The flat personal plan is
|
|
381
|
+
* closed (US-024), so the notice names no plan or price.
|
|
380
382
|
*
|
|
381
383
|
* It lives beside the nag copy so plan wording stays in one file, but it is
|
|
382
384
|
* NOT part of the nag model: no persistence, no throttle, no episode state. It
|
|
@@ -384,11 +386,8 @@ export function buildOverLine(overEntries, upgradeUrl) {
|
|
|
384
386
|
* is a plan fact, so it neither counts toward the nag bands nor is silenced by
|
|
385
387
|
* the nag cadence.
|
|
386
388
|
*
|
|
387
|
-
*
|
|
388
|
-
* decisions 10 and 11).
|
|
389
|
+
* No Outposts and no bot pricing in the copy (owner decisions 10 and 11).
|
|
389
390
|
*/
|
|
390
|
-
export const INDIVIDUAL_PLAN_NAME = "Individual";
|
|
391
|
-
export const INDIVIDUAL_PRICE_LABEL = "$50/mo";
|
|
392
391
|
/** Human names for the features a personal scope can be refused. */
|
|
393
392
|
const PERSONAL_FEATURE_LABELS = {
|
|
394
393
|
telemetry: "My Telemetry",
|
|
@@ -403,11 +402,11 @@ export function renderPersonalPlanFeatureNotice(feature, upgradeUrl, opts = {})
|
|
|
403
402
|
// A person on usage pricing has no Individual plan to buy (US-012).
|
|
404
403
|
if (isUsageOnly(opts.pricingPlan))
|
|
405
404
|
return usageOnlyPersonalFeatureNotice(label);
|
|
406
|
-
const headline = `${label} is included
|
|
405
|
+
const headline = `${label} is not included on your personal account's current billing.`;
|
|
407
406
|
const action = typeof upgradeUrl === "string" && upgradeUrl.trim().length > 0
|
|
408
|
-
? `
|
|
409
|
-
: "
|
|
410
|
-
return `${headline}
|
|
407
|
+
? `Billing: ${upgradeUrl.trim()}`
|
|
408
|
+
: "See your HQ billing settings.";
|
|
409
|
+
return `${headline} ${action}`;
|
|
411
410
|
}
|
|
412
411
|
/** `"{resource}:{band}"` — the persisted key, matching the server's own. */
|
|
413
412
|
function crossingKey(crossing) {
|
|
@@ -429,9 +428,9 @@ export function renderThresholdNotice(crossing, upgradeUrl) {
|
|
|
429
428
|
const name = dimensionName(crossing.resource);
|
|
430
429
|
const counts = `${crossing.used} of ${crossing.limit}`;
|
|
431
430
|
const headline = crossing.band === "over"
|
|
432
|
-
? `
|
|
433
|
-
: `
|
|
434
|
-
return `${headline}
|
|
431
|
+
? `You are past your ${name} limit (${counts}).`
|
|
432
|
+
: `You have reached 80% of your ${name} limit (${counts}).`;
|
|
433
|
+
return `${headline} Billing: ${upgradeUrl}`;
|
|
435
434
|
}
|
|
436
435
|
/**
|
|
437
436
|
* Print every crossing this machine has not already printed for this episode.
|
|
@@ -521,7 +520,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
521
520
|
if (worst === null)
|
|
522
521
|
return;
|
|
523
522
|
warningShownThisSession = true;
|
|
524
|
-
const line = `⚠
|
|
523
|
+
const line = `⚠ Nearing this account's limits: ${formatEntryLine(worst.key, worst.entry)}. Make room before you hit the cap. Billing: ${resolvedUpgradeUrl}`;
|
|
525
524
|
write(chalk.yellow(line) + "\n");
|
|
526
525
|
if (isCliLimitResourceKind(worst.key)) {
|
|
527
526
|
queueCliLimitNoticeExposure(worst.key, "advisory");
|
|
@@ -36,6 +36,8 @@ function allowed(al, c) {
|
|
|
36
36
|
const lower = c.secret.toLowerCase();
|
|
37
37
|
checks.push(al.stopwords.some((s) => lower.includes(s)));
|
|
38
38
|
}
|
|
39
|
+
if (al.commits.length)
|
|
40
|
+
checks.push(c.commit !== undefined && al.commits.includes(c.commit.toLowerCase()));
|
|
39
41
|
if (!checks.length)
|
|
40
42
|
return false;
|
|
41
43
|
return al.condition === "AND" ? checks.every(Boolean) : checks.some(Boolean);
|
|
@@ -50,6 +52,10 @@ function lineBounds(text, start, end) {
|
|
|
50
52
|
offset++;
|
|
51
53
|
return { line: text.slice(from, to), offset };
|
|
52
54
|
}
|
|
55
|
+
/** A repo allowlist applies to this rule (no targetRules means every rule). */
|
|
56
|
+
function repoAllowed(rules, ruleId, c) {
|
|
57
|
+
return (rules.repo ?? []).some((al) => (!al.targetRules.length || al.targetRules.includes(ruleId)) && allowed(al, c));
|
|
58
|
+
}
|
|
53
59
|
/** True when the global allowlist excludes this path from scanning. */
|
|
54
60
|
export function pathIgnored(file, rules = compiledRules()) {
|
|
55
61
|
return rules.global.paths.some((p) => p.test(file));
|
|
@@ -60,6 +66,7 @@ export function scanPath(file, rules = compiledRules()) {
|
|
|
60
66
|
return [];
|
|
61
67
|
return rules.rules
|
|
62
68
|
.filter((r) => !r.regex && r.path?.test(file))
|
|
69
|
+
.filter((r) => !repoAllowed(rules, r.id, { file, secret: "", match: "", line: "" }))
|
|
63
70
|
.map((r) => ({ ruleId: r.id, description: r.description, file, line: 1 }));
|
|
64
71
|
}
|
|
65
72
|
export function scanFragment(frag, rules = compiledRules()) {
|
|
@@ -103,6 +110,8 @@ export function scanFragment(frag, rules = compiledRules()) {
|
|
|
103
110
|
continue;
|
|
104
111
|
if (rule.allowlists.some((al) => allowed(al, cand)))
|
|
105
112
|
continue;
|
|
113
|
+
if (repoAllowed(rules, rule.id, cand))
|
|
114
|
+
continue;
|
|
106
115
|
const lineNo = startLine + offset;
|
|
107
116
|
const key = `${rule.id}:${lineNo}`;
|
|
108
117
|
if (seen.has(key))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level allowlists: `[[allowlists]]` (and the legacy `[allowlist]`) from
|
|
3
|
+
* the scanned repo's own `.gitleaks.toml`, with gitleaks semantics for
|
|
4
|
+
* `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules` and
|
|
5
|
+
* `condition`. They apply on top of the built-in rules and allowlists, to that
|
|
6
|
+
* repo only. Repo `[[rules]]`, `[extend]` and the rest of the file are ignored:
|
|
7
|
+
* a repo can allow known false positives, not change what is detected.
|
|
8
|
+
*
|
|
9
|
+
* The file is read from git, never the working tree:
|
|
10
|
+
* - The staged (index) copy applies only when it is identical to the copy
|
|
11
|
+
* already committed at HEAD (or at MERGE_HEAD during a merge). A commit
|
|
12
|
+
* that changes the allowlist is scanned with the HEAD allowlist, so
|
|
13
|
+
* widening the allowlist and adding the secret cannot happen in one commit.
|
|
14
|
+
* - A changed staged copy is still validated, so a broken file fails now.
|
|
15
|
+
* - A symlinked .gitleaks.toml is rejected.
|
|
16
|
+
*
|
|
17
|
+
* Narrow by construction: every allowlist needs `paths`, each anchored with a
|
|
18
|
+
* literal prefix (`^golden/...`); `regexes` and `stopwords` need
|
|
19
|
+
* `condition = "AND"`, so they never reach files outside those paths.
|
|
20
|
+
*
|
|
21
|
+
* Fails closed: anything not acceptable is a ScanConfigError, and the scan
|
|
22
|
+
* exits 2 rather than running with a guess.
|
|
23
|
+
*/
|
|
24
|
+
import { type CompiledAllowlist } from "./rules.js";
|
|
25
|
+
export declare class ScanConfigError extends Error {
|
|
26
|
+
}
|
|
27
|
+
export declare const REPO_CONFIG_FILE = ".gitleaks.toml";
|
|
28
|
+
/**
|
|
29
|
+
* Length of the literal text right after a leading `^`: letters, digits,
|
|
30
|
+
* `/ _ - @ ~` and backslash-escaped punctuation. A quantifier on the last
|
|
31
|
+
* literal makes it optional, so it does not count.
|
|
32
|
+
*/
|
|
33
|
+
export declare function literalPathPrefix(src: string): number;
|
|
34
|
+
/** Validated allowlists from the text of a .gitleaks.toml. */
|
|
35
|
+
export declare function parseRepoAllowlists(text: string, file?: string): CompiledAllowlist[];
|
|
36
|
+
/**
|
|
37
|
+
* Compiled allowlists for a staged scan of the repo at `repoRoot`, read from
|
|
38
|
+
* git as described at the top of this file. [] when the repo has none.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadRepoAllowlists(repoRoot: string): CompiledAllowlist[];
|
|
41
|
+
//# sourceMappingURL=repo-config.d.ts.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level allowlists: `[[allowlists]]` (and the legacy `[allowlist]`) from
|
|
3
|
+
* the scanned repo's own `.gitleaks.toml`, with gitleaks semantics for
|
|
4
|
+
* `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules` and
|
|
5
|
+
* `condition`. They apply on top of the built-in rules and allowlists, to that
|
|
6
|
+
* repo only. Repo `[[rules]]`, `[extend]` and the rest of the file are ignored:
|
|
7
|
+
* a repo can allow known false positives, not change what is detected.
|
|
8
|
+
*
|
|
9
|
+
* The file is read from git, never the working tree:
|
|
10
|
+
* - The staged (index) copy applies only when it is identical to the copy
|
|
11
|
+
* already committed at HEAD (or at MERGE_HEAD during a merge). A commit
|
|
12
|
+
* that changes the allowlist is scanned with the HEAD allowlist, so
|
|
13
|
+
* widening the allowlist and adding the secret cannot happen in one commit.
|
|
14
|
+
* - A changed staged copy is still validated, so a broken file fails now.
|
|
15
|
+
* - A symlinked .gitleaks.toml is rejected.
|
|
16
|
+
*
|
|
17
|
+
* Narrow by construction: every allowlist needs `paths`, each anchored with a
|
|
18
|
+
* literal prefix (`^golden/...`); `regexes` and `stopwords` need
|
|
19
|
+
* `condition = "AND"`, so they never reach files outside those paths.
|
|
20
|
+
*
|
|
21
|
+
* Fails closed: anything not acceptable is a ScanConfigError, and the scan
|
|
22
|
+
* exits 2 rather than running with a guess.
|
|
23
|
+
*/
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { parse } from "smol-toml";
|
|
26
|
+
import { compileGoRegex } from "./go-regex.js";
|
|
27
|
+
import { compileAllowlist } from "./rules.js";
|
|
28
|
+
export class ScanConfigError extends Error {
|
|
29
|
+
}
|
|
30
|
+
export const REPO_CONFIG_FILE = ".gitleaks.toml";
|
|
31
|
+
const KEYS = new Set(["description", "condition", "regexTarget", "paths", "regexes", "stopwords", "commits", "targetRules"]);
|
|
32
|
+
const MIN_PATH_PREFIX = 3;
|
|
33
|
+
const MIN_STOPWORD = 3;
|
|
34
|
+
/** User regexes run on JavaScript's backtracking engine, not RE2. */
|
|
35
|
+
const MAX_REGEX_LENGTH = 256;
|
|
36
|
+
/** A quantified group that itself contains a quantifier, e.g. (a+)+ or (?:x*y)*. */
|
|
37
|
+
const NESTED_QUANTIFIER = /\((?:[^()\\]|\\.)*[*+}](?:[^()\\]|\\.)*\)[*+{]/;
|
|
38
|
+
/** Paths no real allowlist path should match all of. */
|
|
39
|
+
const PROBE_PATHS = ["a", ".env", "src/index.ts", "config/credentials.yml", "deep/nested/dir/file.json"];
|
|
40
|
+
function fail(file, where, msg) {
|
|
41
|
+
throw new ScanConfigError(`${file}: ${where}: ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
function stringList(file, where, key, v) {
|
|
44
|
+
if (v === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string"))
|
|
47
|
+
fail(file, where, `${key} must be a list of strings`);
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Length of the literal text right after a leading `^`: letters, digits,
|
|
52
|
+
* `/ _ - @ ~` and backslash-escaped punctuation. A quantifier on the last
|
|
53
|
+
* literal makes it optional, so it does not count.
|
|
54
|
+
*/
|
|
55
|
+
export function literalPathPrefix(src) {
|
|
56
|
+
if (!src.startsWith("^"))
|
|
57
|
+
return 0;
|
|
58
|
+
let n = 0;
|
|
59
|
+
let i = 1;
|
|
60
|
+
while (i < src.length) {
|
|
61
|
+
const ch = src[i];
|
|
62
|
+
if (/[A-Za-z0-9/_\-@~]/.test(ch)) {
|
|
63
|
+
n++;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
else if (ch === "\\" && i + 1 < src.length && /[^A-Za-z0-9]/.test(src[i + 1])) {
|
|
67
|
+
n++;
|
|
68
|
+
i += 2;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (n > 0 && /^[*?{]/.test(src.slice(i)))
|
|
75
|
+
n--;
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
/** True when `src` has a `|` outside any group, which would bypass the `^` prefix. */
|
|
79
|
+
function topLevelAlternation(src) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let inClass = false;
|
|
82
|
+
for (let i = 0; i < src.length; i++) {
|
|
83
|
+
const ch = src[i];
|
|
84
|
+
if (ch === "\\")
|
|
85
|
+
i++;
|
|
86
|
+
else if (inClass)
|
|
87
|
+
inClass = ch !== "]";
|
|
88
|
+
else if (ch === "[")
|
|
89
|
+
inClass = true;
|
|
90
|
+
else if (ch === "(")
|
|
91
|
+
depth++;
|
|
92
|
+
else if (ch === ")")
|
|
93
|
+
depth--;
|
|
94
|
+
else if (ch === "|" && depth === 0)
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
function compileChecked(file, where, key, src) {
|
|
100
|
+
if (src.length > MAX_REGEX_LENGTH)
|
|
101
|
+
fail(file, where, `${key} entry is longer than ${MAX_REGEX_LENGTH} characters`);
|
|
102
|
+
if (NESTED_QUANTIFIER.test(src))
|
|
103
|
+
fail(file, where, `${key} entry ${JSON.stringify(src)} has a nested quantifier; simplify it`);
|
|
104
|
+
try {
|
|
105
|
+
return compileGoRegex(src);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
fail(file, where, `${key} entry ${JSON.stringify(src)} is not a supported regex (${err instanceof Error ? err.message : String(err)})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validate(file, where, raw) {
|
|
112
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
113
|
+
fail(file, where, "must be a table");
|
|
114
|
+
const t = raw;
|
|
115
|
+
for (const k of Object.keys(t))
|
|
116
|
+
if (!KEYS.has(k))
|
|
117
|
+
fail(file, where, `unknown key "${k}"`);
|
|
118
|
+
if (t.description !== undefined && typeof t.description !== "string")
|
|
119
|
+
fail(file, where, "description must be a string");
|
|
120
|
+
let condition;
|
|
121
|
+
if (t.condition !== undefined) {
|
|
122
|
+
const c = typeof t.condition === "string" ? t.condition.trim().toUpperCase() : "";
|
|
123
|
+
if (c !== "OR" && c !== "AND")
|
|
124
|
+
fail(file, where, 'condition must be "AND" or "OR"');
|
|
125
|
+
condition = c;
|
|
126
|
+
}
|
|
127
|
+
if (t.regexTarget !== undefined && t.regexTarget !== "secret" && t.regexTarget !== "match" && t.regexTarget !== "line") {
|
|
128
|
+
fail(file, where, 'regexTarget must be "secret", "match" or "line"');
|
|
129
|
+
}
|
|
130
|
+
const al = {
|
|
131
|
+
condition,
|
|
132
|
+
regexTarget: t.regexTarget,
|
|
133
|
+
paths: stringList(file, where, "paths", t.paths),
|
|
134
|
+
regexes: stringList(file, where, "regexes", t.regexes),
|
|
135
|
+
stopwords: stringList(file, where, "stopwords", t.stopwords),
|
|
136
|
+
commits: stringList(file, where, "commits", t.commits),
|
|
137
|
+
targetRules: stringList(file, where, "targetRules", t.targetRules),
|
|
138
|
+
};
|
|
139
|
+
// A repo may narrow what is reported, never switch the scanner off: every
|
|
140
|
+
// allowlist is scoped to files named by an anchored literal prefix.
|
|
141
|
+
if (!al.paths?.length) {
|
|
142
|
+
fail(file, where, "needs paths, each starting with ^ and a literal directory or file name (e.g. '^fixtures/')");
|
|
143
|
+
}
|
|
144
|
+
for (const src of al.paths) {
|
|
145
|
+
const re = compileChecked(file, where, "paths", src);
|
|
146
|
+
if (literalPathPrefix(src) < MIN_PATH_PREFIX || topLevelAlternation(src)) {
|
|
147
|
+
fail(file, where, `paths entry ${JSON.stringify(src)} must start with ^ and at least ${MIN_PATH_PREFIX} literal characters (e.g. '^fixtures/')`);
|
|
148
|
+
}
|
|
149
|
+
if (re.test("") || PROBE_PATHS.every((p) => re.test(p))) {
|
|
150
|
+
fail(file, where, `paths entry ${JSON.stringify(src)} matches everything; allowlist specific files`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ((al.regexes?.length || al.stopwords?.length) && al.condition !== "AND") {
|
|
154
|
+
fail(file, where, 'regexes and stopwords need condition = "AND", so they only apply inside paths');
|
|
155
|
+
}
|
|
156
|
+
for (const src of al.regexes ?? []) {
|
|
157
|
+
if (compileChecked(file, where, "regexes", src).test(""))
|
|
158
|
+
fail(file, where, `regexes entry ${JSON.stringify(src)} matches everything`);
|
|
159
|
+
}
|
|
160
|
+
for (const s of al.stopwords ?? []) {
|
|
161
|
+
if (s.trim().length < MIN_STOPWORD)
|
|
162
|
+
fail(file, where, `stopword ${JSON.stringify(s)} is shorter than ${MIN_STOPWORD} characters`);
|
|
163
|
+
}
|
|
164
|
+
return al;
|
|
165
|
+
}
|
|
166
|
+
/** Validated allowlists from the text of a .gitleaks.toml. */
|
|
167
|
+
export function parseRepoAllowlists(text, file = REPO_CONFIG_FILE) {
|
|
168
|
+
let doc;
|
|
169
|
+
try {
|
|
170
|
+
doc = parse(text);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
throw new ScanConfigError(`${file}: could not be parsed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
174
|
+
}
|
|
175
|
+
const out = [];
|
|
176
|
+
if (doc.allowlist !== undefined)
|
|
177
|
+
out.push(validate(file, "[allowlist]", doc.allowlist));
|
|
178
|
+
if (doc.allowlists !== undefined) {
|
|
179
|
+
if (!Array.isArray(doc.allowlists))
|
|
180
|
+
fail(file, "allowlists", "must be an array of tables ([[allowlists]])");
|
|
181
|
+
doc.allowlists.forEach((a, i) => out.push(validate(file, `[[allowlists]] #${i + 1}`, a)));
|
|
182
|
+
}
|
|
183
|
+
return out.map(compileAllowlist);
|
|
184
|
+
}
|
|
185
|
+
function git(repoRoot, args) {
|
|
186
|
+
try {
|
|
187
|
+
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** `<mode> <type> <blob>\t<path>` (ls-tree, blobField 2) or `<mode> <blob> <stage>\t<path>` (ls-files -s, blobField 1). */
|
|
194
|
+
function entry(out, blobField) {
|
|
195
|
+
const line = out?.split("\n").find(Boolean);
|
|
196
|
+
if (!line)
|
|
197
|
+
return null;
|
|
198
|
+
const f = line.split("\t")[0].split(" ");
|
|
199
|
+
return { mode: f[0], blob: f[blobField] };
|
|
200
|
+
}
|
|
201
|
+
function readEntry(repoRoot, e, where) {
|
|
202
|
+
if (e.mode === "120000")
|
|
203
|
+
throw new ScanConfigError(`${REPO_CONFIG_FILE} (${where}) is a symlink; commit a regular file`);
|
|
204
|
+
const text = git(repoRoot, ["cat-file", "blob", e.blob]);
|
|
205
|
+
if (text === null)
|
|
206
|
+
throw new ScanConfigError(`${REPO_CONFIG_FILE} (${where}) could not be read from git`);
|
|
207
|
+
return text;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Compiled allowlists for a staged scan of the repo at `repoRoot`, read from
|
|
211
|
+
* git as described at the top of this file. [] when the repo has none.
|
|
212
|
+
*/
|
|
213
|
+
export function loadRepoAllowlists(repoRoot) {
|
|
214
|
+
const lsFiles = git(repoRoot, ["ls-files", "-s", "--", REPO_CONFIG_FILE]);
|
|
215
|
+
// Only a resolved (stage 0) index entry counts. A conflicted .gitleaks.toml
|
|
216
|
+
// has stages 1-3 and no stage 0; it is not trusted and the HEAD copy is used.
|
|
217
|
+
const stage0 = lsFiles?.split("\n").find((l) => l.split("\t")[0].split(" ")[2] === "0") ?? null;
|
|
218
|
+
const staged = entry(stage0, 1);
|
|
219
|
+
if (!staged)
|
|
220
|
+
return lsFiles?.trim() ? headAllowlists(repoRoot) : [];
|
|
221
|
+
const stagedLists = parseRepoAllowlists(readEntry(repoRoot, staged, "staged"));
|
|
222
|
+
const committed = ["HEAD", "MERGE_HEAD"]
|
|
223
|
+
.filter((rev) => git(repoRoot, ["rev-parse", "--verify", "-q", `${rev}^{commit}`]) !== null)
|
|
224
|
+
.map((rev) => ({ rev, e: entry(git(repoRoot, ["ls-tree", rev, "--", REPO_CONFIG_FILE]), 2) }))
|
|
225
|
+
.filter((c) => c.e !== null);
|
|
226
|
+
if (committed.some((c) => c.e.blob === staged.blob && c.e.mode === staged.mode))
|
|
227
|
+
return stagedLists;
|
|
228
|
+
// Changed in this commit: scan with the version already committed at HEAD.
|
|
229
|
+
return headAllowlists(repoRoot);
|
|
230
|
+
}
|
|
231
|
+
function headAllowlists(repoRoot) {
|
|
232
|
+
if (git(repoRoot, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]) === null)
|
|
233
|
+
return [];
|
|
234
|
+
const head = entry(git(repoRoot, ["ls-tree", "HEAD", "--", REPO_CONFIG_FILE]), 2);
|
|
235
|
+
return head ? parseRepoAllowlists(readEntry(repoRoot, head, "HEAD"), `${REPO_CONFIG_FILE} (HEAD)`) : [];
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=repo-config.js.map
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* Compiled rule set: the vendored gitleaks default rules plus HQ's own token
|
|
3
3
|
* patterns (src/lib/core-utils/secret-patterns.ts). Compiled once per process.
|
|
4
4
|
*/
|
|
5
|
-
import type { GitleaksRule } from "./types.js";
|
|
5
|
+
import type { GitleaksAllowlist, GitleaksRule } from "./types.js";
|
|
6
6
|
export interface CompiledAllowlist {
|
|
7
7
|
condition: "OR" | "AND";
|
|
8
8
|
target: "secret" | "match" | "line";
|
|
9
9
|
paths: RegExp[];
|
|
10
10
|
regexes: RegExp[];
|
|
11
11
|
stopwords: string[];
|
|
12
|
+
commits: string[];
|
|
13
|
+
/** Empty: applies to every rule. */
|
|
14
|
+
targetRules: string[];
|
|
12
15
|
}
|
|
13
16
|
export interface CompiledRule {
|
|
14
17
|
id: string;
|
|
@@ -24,7 +27,10 @@ export interface CompiledRule {
|
|
|
24
27
|
export interface CompiledRuleSet {
|
|
25
28
|
global: CompiledAllowlist;
|
|
26
29
|
rules: CompiledRule[];
|
|
30
|
+
/** Allowlists from the scanned repo's own .gitleaks.toml (see repo-config.ts). */
|
|
31
|
+
repo?: CompiledAllowlist[];
|
|
27
32
|
}
|
|
33
|
+
export declare function compileAllowlist(a: GitleaksAllowlist): CompiledAllowlist;
|
|
28
34
|
/** HQ's own patterns, expressed as gitleaks-shaped rules. */
|
|
29
35
|
export declare function hqRules(): GitleaksRule[];
|
|
30
36
|
export declare function compiledRules(): CompiledRuleSet;
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
import { SECRET_PATTERNS } from "../core-utils/secret-patterns.js";
|
|
6
6
|
import { GITLEAKS_CONFIG } from "./gitleaks-rules.generated.js";
|
|
7
7
|
import { compileGoRegex } from "./go-regex.js";
|
|
8
|
-
function compileAllowlist(a) {
|
|
8
|
+
export function compileAllowlist(a) {
|
|
9
9
|
return {
|
|
10
10
|
condition: a.condition === "AND" ? "AND" : "OR",
|
|
11
11
|
target: a.regexTarget ?? "secret",
|
|
12
12
|
paths: (a.paths ?? []).map((p) => compileGoRegex(p)),
|
|
13
13
|
regexes: (a.regexes ?? []).map((r) => compileGoRegex(r)),
|
|
14
14
|
stopwords: (a.stopwords ?? []).map((s) => s.toLowerCase()),
|
|
15
|
+
commits: (a.commits ?? []).map((c) => c.toLowerCase()),
|
|
16
|
+
targetRules: a.targetRules ?? [],
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
function compileRule(r) {
|
|
@@ -12,6 +12,11 @@ export interface ParsedDiff {
|
|
|
12
12
|
/** Parse `git diff --unified=0` output into one fragment of added lines per hunk. */
|
|
13
13
|
export declare function parseUnifiedDiff(diff: string): ParsedDiff;
|
|
14
14
|
export declare function stagedDiff(cwd: string): string;
|
|
15
|
+
export declare function repoRoot(cwd: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
|
|
18
|
+
* relative to the repo root, as gitleaks' allowlist paths are.
|
|
19
|
+
*/
|
|
15
20
|
export declare function scanStaged(cwd: string): SecretFinding[];
|
|
16
21
|
export declare function scanFiles(paths: string[]): SecretFinding[];
|
|
17
22
|
//# sourceMappingURL=staged.d.ts.map
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { readFileSync, statSync } from "node:fs";
|
|
6
6
|
import { scanFragment, scanPath } from "./detect.js";
|
|
7
|
+
import { loadRepoAllowlists } from "./repo-config.js";
|
|
8
|
+
import { compiledRules } from "./rules.js";
|
|
7
9
|
export class ScanInputError extends Error {
|
|
8
10
|
}
|
|
9
11
|
const DIFF_ARGS = [
|
|
@@ -89,9 +91,24 @@ export function stagedDiff(cwd) {
|
|
|
89
91
|
throw new ScanInputError(`could not read the staged changes: ${detail || "git diff failed"}`);
|
|
90
92
|
}
|
|
91
93
|
}
|
|
94
|
+
export function repoRoot(cwd) {
|
|
95
|
+
try {
|
|
96
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const detail = err instanceof Error && "stderr" in err ? String(err.stderr).trim() : String(err);
|
|
100
|
+
throw new ScanInputError(`could not find the repo root: ${detail || "git rev-parse failed"}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
|
|
105
|
+
* relative to the repo root, as gitleaks' allowlist paths are.
|
|
106
|
+
*/
|
|
92
107
|
export function scanStaged(cwd) {
|
|
93
|
-
const
|
|
94
|
-
|
|
108
|
+
const diff = stagedDiff(cwd);
|
|
109
|
+
const rules = { ...compiledRules(), repo: loadRepoAllowlists(repoRoot(cwd)) };
|
|
110
|
+
const { fragments, binaryFiles } = parseUnifiedDiff(diff);
|
|
111
|
+
return [...fragments.flatMap((f) => scanFragment(f, rules)), ...binaryFiles.flatMap((f) => scanPath(f, rules))];
|
|
95
112
|
}
|
|
96
113
|
export function scanFiles(paths) {
|
|
97
114
|
const out = [];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Company-relative folders the server owns and a client push never uploads. */
|
|
2
|
+
export declare const SERVER_OWNED_FOLDERS: readonly string[];
|
|
3
|
+
/**
|
|
4
|
+
* True for a company-relative path a client push refuses as server-owned.
|
|
5
|
+
* Mirrors hq-cloud's `isLocallyIneligibleForPush` with the entity exception:
|
|
6
|
+
* `ontology/entities/` can be client-owned, so it is not reported here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isServerOwnedNeverPushed(rel: string): boolean;
|
|
9
|
+
export type ServerOwnedLocalOnlyResult = {
|
|
10
|
+
files: string[];
|
|
11
|
+
total: number;
|
|
12
|
+
};
|
|
13
|
+
export type FindServerOwnedLocalOnlyOptions = {
|
|
14
|
+
hqRoot: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
/** Company-relative keys already in the sync journal (live or tombstoned). */
|
|
17
|
+
journalKeys: ReadonlySet<string>;
|
|
18
|
+
/** Engine ignore filter over absolute paths; true = would sync. */
|
|
19
|
+
shouldSync?: (absPath: string, isDir?: boolean) => boolean;
|
|
20
|
+
/** Max paths returned; `total` still counts every match. */
|
|
21
|
+
limit?: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Walk the server-owned folders under `companies/<slug>/` and return files
|
|
25
|
+
* that are not in the journal. A journaled file arrived by pull and is in
|
|
26
|
+
* sync; anything else was written locally and will never upload.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findServerOwnedLocalOnly(opts: FindServerOwnedLocalOnlyOptions): ServerOwnedLocalOnlyResult;
|
|
29
|
+
//# sourceMappingURL=server-owned-local-only.d.ts.map
|