brakit 0.8.5 → 0.8.7

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.
@@ -9,6 +9,91 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/constants/limits.ts
13
+ var PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_OBJECT_SCAN_DEPTH, ISSUE_PRUNE_TTL_MS;
14
+ var init_limits = __esm({
15
+ "src/constants/limits.ts"() {
16
+ "use strict";
17
+ PROJECT_HASH_LENGTH = 8;
18
+ SECRET_SCAN_ARRAY_LIMIT = 5;
19
+ PII_SCAN_ARRAY_LIMIT = 10;
20
+ MIN_SECRET_VALUE_LENGTH = 8;
21
+ FULL_RECORD_MIN_FIELDS = 8;
22
+ LIST_PII_MIN_ITEMS = 2;
23
+ MAX_OBJECT_SCAN_DEPTH = 5;
24
+ ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
25
+ }
26
+ });
27
+
28
+ // src/utils/log.ts
29
+ function brakitDebug(message) {
30
+ if (process.env.DEBUG_BRAKIT) {
31
+ process.stderr.write(`${PREFIX}:debug ${message}
32
+ `);
33
+ }
34
+ }
35
+ var PREFIX;
36
+ var init_log = __esm({
37
+ "src/utils/log.ts"() {
38
+ "use strict";
39
+ PREFIX = "[brakit]";
40
+ }
41
+ });
42
+
43
+ // src/constants/lifecycle.ts
44
+ var VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
45
+ var init_lifecycle = __esm({
46
+ "src/constants/lifecycle.ts"() {
47
+ "use strict";
48
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
49
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
50
+ VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
51
+ }
52
+ });
53
+
54
+ // src/utils/type-guards.ts
55
+ function isNonEmptyString(val) {
56
+ return typeof val === "string" && val.trim().length > 0;
57
+ }
58
+ function getErrorMessage(err) {
59
+ if (err instanceof Error) return err.message;
60
+ if (typeof err === "string") return err;
61
+ return String(err);
62
+ }
63
+ function isValidIssueState(val) {
64
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
65
+ }
66
+ function isValidAiFixStatus(val) {
67
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
68
+ }
69
+ var init_type_guards = __esm({
70
+ "src/utils/type-guards.ts"() {
71
+ "use strict";
72
+ init_lifecycle();
73
+ init_limits();
74
+ }
75
+ });
76
+
77
+ // src/constants/metrics.ts
78
+ var METRICS_DIR, PORT_FILE;
79
+ var init_metrics = __esm({
80
+ "src/constants/metrics.ts"() {
81
+ "use strict";
82
+ METRICS_DIR = ".brakit";
83
+ PORT_FILE = ".brakit/port";
84
+ }
85
+ });
86
+
87
+ // src/constants/thresholds.ts
88
+ var OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS;
89
+ var init_thresholds = __esm({
90
+ "src/constants/thresholds.ts"() {
91
+ "use strict";
92
+ OVERFETCH_UNWRAP_MIN_SIZE = 3;
93
+ STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
94
+ }
95
+ });
96
+
12
97
  // src/constants/routes.ts
13
98
  var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS;
14
99
  var init_routes = __esm({
@@ -47,24 +132,6 @@ var init_routes = __esm({
47
132
  }
48
133
  });
49
134
 
50
- // src/constants/limits.ts
51
- var FINDING_ID_HASH_LENGTH;
52
- var init_limits = __esm({
53
- "src/constants/limits.ts"() {
54
- "use strict";
55
- FINDING_ID_HASH_LENGTH = 16;
56
- }
57
- });
58
-
59
- // src/constants/thresholds.ts
60
- var OVERFETCH_UNWRAP_MIN_SIZE;
61
- var init_thresholds = __esm({
62
- "src/constants/thresholds.ts"() {
63
- "use strict";
64
- OVERFETCH_UNWRAP_MIN_SIZE = 3;
65
- }
66
- });
67
-
68
135
  // src/constants/transport.ts
69
136
  var init_transport = __esm({
70
137
  "src/constants/transport.ts"() {
@@ -72,16 +139,6 @@ var init_transport = __esm({
72
139
  }
73
140
  });
74
141
 
75
- // src/constants/metrics.ts
76
- var METRICS_DIR, PORT_FILE;
77
- var init_metrics = __esm({
78
- "src/constants/metrics.ts"() {
79
- "use strict";
80
- METRICS_DIR = ".brakit";
81
- PORT_FILE = ".brakit/port";
82
- }
83
- });
84
-
85
142
  // src/constants/headers.ts
86
143
  var init_headers = __esm({
87
144
  "src/constants/headers.ts"() {
@@ -115,7 +172,7 @@ var init_mcp = __esm({
115
172
  MAX_TIMELINE_EVENTS = 20;
116
173
  MAX_RESOLVED_DISPLAY = 5;
117
174
  ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
118
- MCP_SERVER_VERSION = "0.8.5";
175
+ MCP_SERVER_VERSION = "0.8.7";
119
176
  }
120
177
  });
121
178
 
@@ -140,14 +197,35 @@ var init_telemetry = __esm({
140
197
  }
141
198
  });
142
199
 
143
- // src/constants/lifecycle.ts
144
- var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
145
- var init_lifecycle = __esm({
146
- "src/constants/lifecycle.ts"() {
200
+ // src/constants/cli.ts
201
+ var SUPPORTED_SOURCE_EXTENSIONS, BUILD_CACHE_DIRS, FALLBACK_SCAN_DIRS;
202
+ var init_cli = __esm({
203
+ "src/constants/cli.ts"() {
204
+ "use strict";
205
+ SUPPORTED_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
206
+ ".ts",
207
+ ".tsx",
208
+ ".js",
209
+ ".jsx",
210
+ ".mjs",
211
+ ".mts"
212
+ ]);
213
+ BUILD_CACHE_DIRS = [".next", ".nuxt", ".output"];
214
+ FALLBACK_SCAN_DIRS = ["src", "."];
215
+ }
216
+ });
217
+
218
+ // src/constants/timeline.ts
219
+ var init_timeline = __esm({
220
+ "src/constants/timeline.ts"() {
221
+ "use strict";
222
+ }
223
+ });
224
+
225
+ // src/constants/sdk-events.ts
226
+ var init_sdk_events = __esm({
227
+ "src/constants/sdk-events.ts"() {
147
228
  "use strict";
148
- VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
149
- VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
150
- VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
151
229
  }
152
230
  });
153
231
 
@@ -167,51 +245,9 @@ var init_constants = __esm({
167
245
  init_severity();
168
246
  init_telemetry();
169
247
  init_lifecycle();
170
- }
171
- });
172
-
173
- // src/utils/log.ts
174
- function brakitDebug(message) {
175
- if (process.env.DEBUG_BRAKIT) {
176
- process.stderr.write(`${PREFIX}:debug ${message}
177
- `);
178
- }
179
- }
180
- var PREFIX;
181
- var init_log = __esm({
182
- "src/utils/log.ts"() {
183
- "use strict";
184
- PREFIX = "[brakit]";
185
- }
186
- });
187
-
188
- // src/utils/type-guards.ts
189
- function isNonEmptyString(val) {
190
- return typeof val === "string" && val.trim().length > 0;
191
- }
192
- function isValidFindingState(val) {
193
- return typeof val === "string" && VALID_FINDING_STATES.has(val);
194
- }
195
- function isValidAiFixStatus(val) {
196
- return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
197
- }
198
- var init_type_guards = __esm({
199
- "src/utils/type-guards.ts"() {
200
- "use strict";
201
- init_lifecycle();
202
- }
203
- });
204
-
205
- // src/store/finding-id.ts
206
- import { createHash } from "crypto";
207
- function computeInsightId(type, endpoint, desc) {
208
- const key = `${type}:${endpoint}:${desc}`;
209
- return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
210
- }
211
- var init_finding_id = __esm({
212
- "src/store/finding-id.ts"() {
213
- "use strict";
214
- init_limits();
248
+ init_cli();
249
+ init_timeline();
250
+ init_sdk_events();
215
251
  }
216
252
  });
217
253
 
@@ -249,11 +285,11 @@ var init_client = __esm({
249
285
  if (params?.offset) url.searchParams.set("offset", String(params.offset));
250
286
  return this.fetchJson(url);
251
287
  }
252
- async getSecurityFindings() {
253
- return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_SECURITY}`);
254
- }
255
- async getInsights() {
256
- return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
288
+ async getIssues(params) {
289
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
290
+ if (params?.state) url.searchParams.set("state", params.state);
291
+ if (params?.category) url.searchParams.set("category", params.category);
292
+ return this.fetchJson(url);
257
293
  }
258
294
  async getQueries(requestId) {
259
295
  const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
@@ -325,7 +361,7 @@ var init_client = __esm({
325
361
  });
326
362
 
327
363
  // src/mcp/discovery.ts
328
- import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
364
+ import { readFile as readFile6, readdir as readdir3, stat } from "fs/promises";
329
365
  import { resolve as resolve5, dirname as dirname2 } from "path";
330
366
  async function readPort(portPath) {
331
367
  try {
@@ -341,7 +377,7 @@ async function portInDir(dir) {
341
377
  }
342
378
  async function portInChildren(dir) {
343
379
  try {
344
- const entries = await readdir2(dir);
380
+ const entries = await readdir3(dir);
345
381
  for (const entry of entries) {
346
382
  if (entry.startsWith(".") || entry === "node_modules") continue;
347
383
  const child = resolve5(dir, entry);
@@ -408,14 +444,16 @@ var init_discovery = __esm({
408
444
 
409
445
  // src/mcp/enrichment.ts
410
446
  async function enrichFindings(client) {
411
- const [securityData, insightsData] = await Promise.all([
412
- client.getSecurityFindings(),
413
- client.getInsights()
414
- ]);
447
+ const issuesData = await client.getIssues();
448
+ const issues = issuesData.issues.filter(
449
+ (si) => si.state !== "resolved" && si.state !== "stale"
450
+ );
415
451
  const contexts = await Promise.all(
416
- securityData.findings.map(async (sf) => {
452
+ issues.map(async (si) => {
453
+ const endpoint = si.issue.endpoint;
454
+ if (!endpoint) return si.issue.detail ?? "";
417
455
  try {
418
- const { path } = parseEndpointKey(sf.finding.endpoint);
456
+ const { path } = parseEndpointKey(endpoint);
419
457
  const reqData = await client.getRequests({ search: path, limit: 1 });
420
458
  if (reqData.requests.length > 0) {
421
459
  const req = reqData.requests[0];
@@ -429,38 +467,22 @@ async function enrichFindings(client) {
429
467
  } catch {
430
468
  return "(context unavailable)";
431
469
  }
432
- return "";
470
+ return si.issue.detail ?? "";
433
471
  })
434
472
  );
435
- const enriched = securityData.findings.map((sf, i) => {
436
- const f = sf.finding;
437
- return {
438
- findingId: sf.findingId,
439
- severity: f.severity,
440
- title: f.title,
441
- endpoint: f.endpoint,
442
- description: f.desc,
443
- hint: f.hint,
444
- occurrences: f.count,
445
- context: contexts[i],
446
- aiStatus: sf.aiStatus,
447
- aiNotes: sf.aiNotes
448
- };
449
- });
450
- for (const si of insightsData.insights) {
451
- if (si.state === "resolved") continue;
452
- const i = si.insight;
453
- if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
454
- const endpoint = i.nav ?? "global";
473
+ const enriched = [];
474
+ for (let i = 0; i < issues.length; i++) {
475
+ const si = issues[i];
476
+ if (!ENRICHMENT_SEVERITY_FILTER.includes(si.issue.severity)) continue;
455
477
  enriched.push({
456
- findingId: computeInsightId(i.type, endpoint, i.desc),
457
- severity: i.severity,
458
- title: i.title,
459
- endpoint,
460
- description: i.desc,
461
- hint: i.hint,
462
- occurrences: 1,
463
- context: i.detail ?? "",
478
+ findingId: si.issueId,
479
+ severity: si.issue.severity,
480
+ title: si.issue.title,
481
+ endpoint: si.issue.endpoint ?? "global",
482
+ description: si.issue.desc,
483
+ hint: si.issue.hint,
484
+ occurrences: si.occurrences,
485
+ context: contexts[i],
464
486
  aiStatus: si.aiStatus,
465
487
  aiNotes: si.aiNotes
466
488
  });
@@ -518,7 +540,6 @@ var init_enrichment = __esm({
518
540
  "src/mcp/enrichment.ts"() {
519
541
  "use strict";
520
542
  init_mcp();
521
- init_finding_id();
522
543
  init_endpoint();
523
544
  }
524
545
  });
@@ -544,8 +565,8 @@ var init_get_findings = __esm({
544
565
  },
545
566
  state: {
546
567
  type: "string",
547
- enum: ["open", "fixing", "resolved"],
548
- description: "Filter by finding state (from finding lifecycle)"
568
+ enum: ["open", "fixing", "resolved", "stale", "regressed"],
569
+ description: "Filter by issue state"
549
570
  }
550
571
  }
551
572
  },
@@ -555,17 +576,17 @@ var init_get_findings = __esm({
555
576
  if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
556
577
  return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
557
578
  }
558
- if (state && !isValidFindingState(state)) {
559
- return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
579
+ if (state && !isValidIssueState(state)) {
580
+ return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved, stale, regressed.` }], isError: true };
560
581
  }
561
582
  let findings = await enrichFindings(client);
562
583
  if (severity) {
563
584
  findings = findings.filter((f) => f.severity === severity);
564
585
  }
565
586
  if (state) {
566
- const stateful = await client.getFindings(state);
567
- const statefulIds = new Set(stateful.findings.map((f) => f.findingId));
568
- findings = findings.filter((f) => statefulIds.has(f.findingId));
587
+ const issuesData = await client.getIssues({ state });
588
+ const issueIds = new Set(issuesData.issues.map((i) => i.issueId));
589
+ findings = findings.filter((f) => issueIds.has(f.findingId));
569
590
  }
570
591
  if (findings.length === 0) {
571
592
  return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
@@ -744,20 +765,21 @@ var init_verify_fix = __esm({
744
765
  }
745
766
  if (findingId) {
746
767
  const data = await client.getFindings();
747
- const finding = data.findings.find((f) => f.findingId === findingId);
768
+ const finding = data.findings.find((f) => f.issueId === findingId);
748
769
  if (!finding) {
749
770
  return {
750
771
  content: [{
751
772
  type: "text",
752
773
  text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
753
- }]
774
+ }],
775
+ isError: true
754
776
  };
755
777
  }
756
778
  if (finding.state === "resolved") {
757
779
  return {
758
780
  content: [{
759
781
  type: "text",
760
- text: `RESOLVED: "${finding.finding.title}" on ${finding.finding.endpoint} is no longer detected. The fix worked.`
782
+ text: `RESOLVED: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"} is no longer detected. The fix worked.`
761
783
  }]
762
784
  };
763
785
  }
@@ -765,12 +787,12 @@ var init_verify_fix = __esm({
765
787
  content: [{
766
788
  type: "text",
767
789
  text: [
768
- `STILL PRESENT: "${finding.finding.title}" on ${finding.finding.endpoint}`,
790
+ `STILL PRESENT: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"}`,
769
791
  ` State: ${finding.state}`,
770
792
  ` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
771
793
  ` Occurrences: ${finding.occurrences}`,
772
- ` Issue: ${finding.finding.desc}`,
773
- ` Hint: ${finding.finding.hint}`,
794
+ ` Issue: ${finding.issue.desc}`,
795
+ ` Hint: ${finding.issue.hint}`,
774
796
  "",
775
797
  "Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
776
798
  ].join("\n")
@@ -780,7 +802,7 @@ var init_verify_fix = __esm({
780
802
  if (endpoint) {
781
803
  const data = await client.getFindings();
782
804
  const endpointFindings = data.findings.filter(
783
- (f) => f.finding.endpoint === endpoint || f.finding.endpoint.endsWith(` ${endpoint}`)
805
+ (f) => f.issue.endpoint === endpoint || f.issue.endpoint && f.issue.endpoint.endsWith(` ${endpoint}`)
784
806
  );
785
807
  if (endpointFindings.length === 0) {
786
808
  return {
@@ -790,7 +812,7 @@ var init_verify_fix = __esm({
790
812
  }]
791
813
  };
792
814
  }
793
- const open = endpointFindings.filter((f) => f.state === "open");
815
+ const open = endpointFindings.filter((f) => f.state === "open" || f.state === "regressed");
794
816
  const resolved = endpointFindings.filter((f) => f.state === "resolved");
795
817
  const lines = [
796
818
  `Endpoint: ${endpoint}`,
@@ -799,10 +821,10 @@ var init_verify_fix = __esm({
799
821
  ""
800
822
  ];
801
823
  for (const f of open) {
802
- lines.push(` [${f.finding.severity}] ${f.finding.title}: ${f.finding.desc}`);
824
+ lines.push(` [${f.issue.severity}] ${f.issue.title}: ${f.issue.desc}`);
803
825
  }
804
826
  for (const f of resolved) {
805
- lines.push(` [resolved] ${f.finding.title}`);
827
+ lines.push(` [resolved] ${f.issue.title}`);
806
828
  }
807
829
  return { content: [{ type: "text", text: lines.join("\n") }] };
808
830
  }
@@ -810,7 +832,8 @@ var init_verify_fix = __esm({
810
832
  content: [{
811
833
  type: "text",
812
834
  text: "Please provide either a finding_id or an endpoint to verify."
813
- }]
835
+ }],
836
+ isError: true
814
837
  };
815
838
  }
816
839
  };
@@ -831,51 +854,52 @@ var init_get_report = __esm({
831
854
  properties: {}
832
855
  },
833
856
  async handler(client, _args) {
834
- const [findingsData, securityData, insightsData, metricsData] = await Promise.all([
835
- client.getFindings(),
836
- client.getSecurityFindings(),
837
- client.getInsights(),
857
+ const [issuesData, metricsData] = await Promise.all([
858
+ client.getIssues(),
838
859
  client.getLiveMetrics()
839
860
  ]);
840
- const findings = findingsData.findings;
841
- const open = findings.filter((f) => f.state === "open");
842
- const resolved = findings.filter((f) => f.state === "resolved");
843
- const fixing = findings.filter((f) => f.state === "fixing");
844
- const criticalOpen = open.filter((f) => f.finding.severity === "critical");
845
- const warningOpen = open.filter((f) => f.finding.severity === "warning");
861
+ const issues = issuesData.issues;
862
+ const open = issues.filter((f) => f.state === "open" || f.state === "regressed");
863
+ const resolved = issues.filter((f) => f.state === "resolved");
864
+ const fixing = issues.filter((f) => f.state === "fixing");
865
+ const stale = issues.filter((f) => f.state === "stale");
866
+ const criticalOpen = open.filter((f) => f.issue.severity === "critical");
867
+ const warningOpen = open.filter((f) => f.issue.severity === "warning");
868
+ const securityIssues = issues.filter((f) => f.category === "security");
869
+ const perfIssues = issues.filter((f) => f.category === "performance");
846
870
  const totalRequests = metricsData.endpoints.reduce(
847
871
  (s, ep) => s + ep.summary.totalRequests,
848
872
  0
849
873
  );
850
- const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
851
874
  const lines = [
852
875
  "=== Brakit Report ===",
853
876
  "",
854
877
  `Endpoints observed: ${metricsData.endpoints.length}`,
855
878
  `Total requests captured: ${totalRequests}`,
856
- `Active security rules: ${securityData.findings.length} finding(s)`,
857
- `Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
879
+ `Security issues: ${securityIssues.length}`,
880
+ `Performance issues: ${perfIssues.length}`,
858
881
  "",
859
- "--- Finding Summary ---",
860
- `Total: ${findings.length}`,
882
+ "--- Issue Summary ---",
883
+ `Total: ${issues.length}`,
861
884
  ` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
862
885
  ` In progress: ${fixing.length}`,
863
- ` Resolved: ${resolved.length}`
886
+ ` Resolved: ${resolved.length}`,
887
+ ` Stale: ${stale.length}`
864
888
  ];
865
889
  if (criticalOpen.length > 0) {
866
890
  lines.push("");
867
891
  lines.push("--- Critical Issues (fix first) ---");
868
892
  for (const f of criticalOpen) {
869
- lines.push(` [CRITICAL] ${f.finding.title} \u2014 ${f.finding.endpoint}`);
870
- lines.push(` ${f.finding.desc}`);
871
- lines.push(` Fix: ${f.finding.hint}`);
893
+ lines.push(` [CRITICAL] ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
894
+ lines.push(` ${f.issue.desc}`);
895
+ lines.push(` Fix: ${f.issue.hint}`);
872
896
  }
873
897
  }
874
898
  if (resolved.length > 0) {
875
899
  lines.push("");
876
900
  lines.push("--- Recently Resolved ---");
877
901
  for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
878
- lines.push(` \u2713 ${f.finding.title} \u2014 ${f.finding.endpoint}`);
902
+ lines.push(` \u2713 ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
879
903
  }
880
904
  if (resolved.length > MAX_RESOLVED_DISPLAY) {
881
905
  lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
@@ -1128,21 +1152,31 @@ import { runMain } from "citty";
1128
1152
 
1129
1153
  // src/cli/commands/install.ts
1130
1154
  import { defineCommand } from "citty";
1131
- import { resolve as resolve3, join as join2, dirname } from "path";
1155
+ import { resolve as resolve3, join as join3, dirname } from "path";
1132
1156
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1133
1157
  import { execSync } from "child_process";
1134
1158
  import { existsSync as existsSync5 } from "fs";
1135
1159
  import pc from "picocolors";
1136
1160
 
1137
- // src/store/finding-store.ts
1161
+ // src/store/issue-store.ts
1138
1162
  import { readFile as readFile2 } from "fs/promises";
1139
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
1163
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
1140
1164
  import { resolve as resolve2 } from "path";
1141
1165
 
1142
1166
  // src/utils/fs.ts
1167
+ init_limits();
1168
+ init_log();
1169
+ init_type_guards();
1143
1170
  import { access, readFile, writeFile } from "fs/promises";
1144
1171
  import { existsSync, readFileSync, writeFileSync } from "fs";
1145
- import { resolve } from "path";
1172
+ import { createHash } from "crypto";
1173
+ import { homedir } from "os";
1174
+ import { resolve, join } from "path";
1175
+ function getProjectDataDir(projectRoot) {
1176
+ const absolute = resolve(projectRoot);
1177
+ const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
1178
+ return join(homedir(), ".brakit", "projects", hash);
1179
+ }
1146
1180
  async function fileExists(path) {
1147
1181
  try {
1148
1182
  await access(path);
@@ -1152,8 +1186,10 @@ async function fileExists(path) {
1152
1186
  }
1153
1187
  }
1154
1188
 
1155
- // src/store/finding-store.ts
1156
- init_constants();
1189
+ // src/store/issue-store.ts
1190
+ init_metrics();
1191
+ init_limits();
1192
+ init_thresholds();
1157
1193
  init_limits();
1158
1194
 
1159
1195
  // src/utils/atomic-writer.ts
@@ -1167,14 +1203,18 @@ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1167
1203
  init_log();
1168
1204
  init_type_guards();
1169
1205
 
1170
- // src/store/finding-store.ts
1206
+ // src/store/issue-store.ts
1171
1207
  init_log();
1172
- init_finding_id();
1208
+ init_type_guards();
1209
+
1210
+ // src/utils/issue-id.ts
1211
+ init_limits();
1212
+ import { createHash as createHash2 } from "crypto";
1173
1213
 
1174
1214
  // src/detect/project.ts
1175
1215
  import { readFile as readFile3, readdir } from "fs/promises";
1176
1216
  import { existsSync as existsSync4 } from "fs";
1177
- import { join, relative } from "path";
1217
+ import { join as join2, relative } from "path";
1178
1218
  var FRAMEWORKS = [
1179
1219
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
1180
1220
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -1183,24 +1223,24 @@ var FRAMEWORKS = [
1183
1223
  { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
1184
1224
  ];
1185
1225
  async function detectProject(rootDir) {
1186
- const pkgPath = join(rootDir, "package.json");
1226
+ const pkgPath = join2(rootDir, "package.json");
1187
1227
  const raw = await readFile3(pkgPath, "utf-8");
1188
1228
  const pkg = JSON.parse(raw);
1189
1229
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1190
1230
  const framework = detectFrameworkFromDeps(allDeps);
1191
1231
  const matched = FRAMEWORKS.find((f) => f.name === framework);
1192
1232
  const devCommand = matched?.devCmd ?? "";
1193
- const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
1233
+ const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
1194
1234
  const defaultPort = matched?.defaultPort ?? 3e3;
1195
1235
  const packageManager = await detectPackageManager(rootDir);
1196
1236
  return { framework, devCommand, devBin, defaultPort, packageManager };
1197
1237
  }
1198
1238
  async function detectPackageManager(rootDir) {
1199
- if (await fileExists(join(rootDir, "bun.lockb"))) return "bun";
1200
- if (await fileExists(join(rootDir, "bun.lock"))) return "bun";
1201
- if (await fileExists(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1202
- if (await fileExists(join(rootDir, "yarn.lock"))) return "yarn";
1203
- if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
1239
+ if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
1240
+ if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
1241
+ if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1242
+ if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
1243
+ if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
1204
1244
  return "unknown";
1205
1245
  }
1206
1246
  function detectFrameworkFromDeps(allDeps) {
@@ -1231,9 +1271,9 @@ var PYTHON_DEFAULT_PORTS = {
1231
1271
  unknown: 8e3
1232
1272
  };
1233
1273
  async function detectPythonProject(rootDir) {
1234
- const hasPyproject = await fileExists(join(rootDir, "pyproject.toml"));
1235
- const hasRequirements = await fileExists(join(rootDir, "requirements.txt"));
1236
- const hasSetupPy = await fileExists(join(rootDir, "setup.py"));
1274
+ const hasPyproject = await fileExists(join2(rootDir, "pyproject.toml"));
1275
+ const hasRequirements = await fileExists(join2(rootDir, "requirements.txt"));
1276
+ const hasSetupPy = await fileExists(join2(rootDir, "setup.py"));
1237
1277
  if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
1238
1278
  const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
1239
1279
  const packageManager = await detectPythonPackageManager(rootDir);
@@ -1248,7 +1288,7 @@ async function detectPythonProject(rootDir) {
1248
1288
  async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1249
1289
  if (hasPyproject) {
1250
1290
  try {
1251
- const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
1291
+ const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
1252
1292
  for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1253
1293
  if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
1254
1294
  return fw;
@@ -1259,7 +1299,7 @@ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1259
1299
  }
1260
1300
  if (hasRequirements) {
1261
1301
  try {
1262
- const content = await readFile3(join(rootDir, "requirements.txt"), "utf-8");
1302
+ const content = await readFile3(join2(rootDir, "requirements.txt"), "utf-8");
1263
1303
  const lines = content.toLowerCase().split("\n");
1264
1304
  for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1265
1305
  if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
@@ -1272,13 +1312,13 @@ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1272
1312
  return "unknown";
1273
1313
  }
1274
1314
  async function detectPythonPackageManager(rootDir) {
1275
- if (await fileExists(join(rootDir, "uv.lock"))) return "uv";
1276
- if (await fileExists(join(rootDir, "poetry.lock"))) return "poetry";
1277
- if (await fileExists(join(rootDir, "Pipfile.lock"))) return "pipenv";
1278
- if (await fileExists(join(rootDir, "Pipfile"))) return "pipenv";
1279
- if (await fileExists(join(rootDir, "requirements.txt"))) return "pip";
1315
+ if (await fileExists(join2(rootDir, "uv.lock"))) return "uv";
1316
+ if (await fileExists(join2(rootDir, "poetry.lock"))) return "poetry";
1317
+ if (await fileExists(join2(rootDir, "Pipfile.lock"))) return "pipenv";
1318
+ if (await fileExists(join2(rootDir, "Pipfile"))) return "pipenv";
1319
+ if (await fileExists(join2(rootDir, "requirements.txt"))) return "pip";
1280
1320
  try {
1281
- const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
1321
+ const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
1282
1322
  if (content.includes("[tool.poetry]")) return "poetry";
1283
1323
  if (content.includes("[tool.uv]")) return "uv";
1284
1324
  } catch {
@@ -1287,7 +1327,7 @@ async function detectPythonPackageManager(rootDir) {
1287
1327
  }
1288
1328
  async function detectPythonEntry(rootDir) {
1289
1329
  for (const candidate of PYTHON_ENTRY_CANDIDATES) {
1290
- if (await fileExists(join(rootDir, candidate))) {
1330
+ if (await fileExists(join2(rootDir, candidate))) {
1291
1331
  return candidate;
1292
1332
  }
1293
1333
  }
@@ -1315,7 +1355,7 @@ async function scanForProjects(rootDir) {
1315
1355
  const entries = await readdir(rootDir, { withFileTypes: true });
1316
1356
  for (const entry of entries) {
1317
1357
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
1318
- const childDir = join(rootDir, entry.name);
1358
+ const childDir = join2(rootDir, entry.name);
1319
1359
  await detectInDir(childDir, rootDir, projects);
1320
1360
  }
1321
1361
  } catch {
@@ -1324,7 +1364,7 @@ async function scanForProjects(rootDir) {
1324
1364
  }
1325
1365
  async function detectInDir(dir, rootDir, projects) {
1326
1366
  const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
1327
- if (await fileExists(join(dir, "package.json"))) {
1367
+ if (await fileExists(join2(dir, "package.json"))) {
1328
1368
  try {
1329
1369
  const node = await detectProject(dir);
1330
1370
  projects.push({ dir, relDir: rel, type: "node", node });
@@ -1337,6 +1377,31 @@ async function detectInDir(dir, rootDir, projects) {
1337
1377
  }
1338
1378
  }
1339
1379
 
1380
+ // src/utils/response.ts
1381
+ init_thresholds();
1382
+ function unwrapResponse(parsed) {
1383
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1384
+ const obj = parsed;
1385
+ const keys = Object.keys(obj);
1386
+ if (keys.length > 3) return parsed;
1387
+ let best = null;
1388
+ let bestSize = 0;
1389
+ for (const key of keys) {
1390
+ const val = obj[key];
1391
+ if (Array.isArray(val) && val.length > bestSize) {
1392
+ best = val;
1393
+ bestSize = val.length;
1394
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
1395
+ const size = Object.keys(val).length;
1396
+ if (size > bestSize) {
1397
+ best = val;
1398
+ bestSize = size;
1399
+ }
1400
+ }
1401
+ }
1402
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1403
+ }
1404
+
1340
1405
  // src/analysis/rules/patterns.ts
1341
1406
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1342
1407
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
@@ -1350,6 +1415,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
1350
1415
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
1351
1416
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
1352
1417
  var INTERNAL_ID_SUFFIX = /Id$|_id$/;
1418
+ var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
1419
+ var SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
1353
1420
  var RULE_HINTS = {
1354
1421
  "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
1355
1422
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
@@ -1362,30 +1429,34 @@ var RULE_HINTS = {
1362
1429
  };
1363
1430
 
1364
1431
  // src/analysis/rules/exposed-secret.ts
1365
- function tryParseJson(body) {
1366
- if (!body) return null;
1367
- try {
1368
- return JSON.parse(body);
1369
- } catch {
1370
- return null;
1371
- }
1432
+ init_limits();
1433
+
1434
+ // src/utils/http-status.ts
1435
+ function isErrorStatus(code) {
1436
+ return code >= 400;
1437
+ }
1438
+ function isRedirect(code) {
1439
+ return code >= 300 && code < 400;
1372
1440
  }
1373
- function findSecretKeys(obj, prefix) {
1441
+
1442
+ // src/analysis/rules/exposed-secret.ts
1443
+ function findSecretKeys(obj, prefix, depth = 0) {
1374
1444
  const found = [];
1445
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
1375
1446
  if (!obj || typeof obj !== "object") return found;
1376
1447
  if (Array.isArray(obj)) {
1377
- for (let i = 0; i < Math.min(obj.length, 5); i++) {
1378
- found.push(...findSecretKeys(obj[i], prefix));
1448
+ for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
1449
+ found.push(...findSecretKeys(obj[i], prefix, depth + 1));
1379
1450
  }
1380
1451
  return found;
1381
1452
  }
1382
1453
  for (const k of Object.keys(obj)) {
1383
1454
  const val = obj[k];
1384
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= 8 && !MASKED_RE.test(val)) {
1455
+ if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
1385
1456
  found.push(k);
1386
1457
  }
1387
1458
  if (typeof val === "object" && val !== null) {
1388
- found.push(...findSecretKeys(val, prefix + k + "."));
1459
+ found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
1389
1460
  }
1390
1461
  }
1391
1462
  return found;
@@ -1399,8 +1470,8 @@ var exposedSecretRule = {
1399
1470
  const findings = [];
1400
1471
  const seen = /* @__PURE__ */ new Map();
1401
1472
  for (const r of ctx.requests) {
1402
- if (r.statusCode >= 400) continue;
1403
- const parsed = tryParseJson(r.responseBody);
1473
+ if (isErrorStatus(r.statusCode)) continue;
1474
+ const parsed = ctx.parsedBodies.response.get(r.id);
1404
1475
  if (!parsed) continue;
1405
1476
  const keys = findSecretKeys(parsed, "");
1406
1477
  if (keys.length === 0) continue;
@@ -1553,7 +1624,7 @@ var errorInfoLeakRule = {
1553
1624
 
1554
1625
  // src/analysis/rules/insecure-cookie.ts
1555
1626
  function isFrameworkResponse(r) {
1556
- if (r.statusCode >= 300 && r.statusCode < 400) return true;
1627
+ if (isRedirect(r.statusCode)) return true;
1557
1628
  if (r.path?.startsWith("/__")) return true;
1558
1629
  if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
1559
1630
  return false;
@@ -1659,49 +1730,16 @@ var corsCredentialsRule = {
1659
1730
  }
1660
1731
  };
1661
1732
 
1662
- // src/utils/response.ts
1663
- init_thresholds();
1664
- function unwrapResponse(parsed) {
1665
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1666
- const obj = parsed;
1667
- const keys = Object.keys(obj);
1668
- if (keys.length > 3) return parsed;
1669
- let best = null;
1670
- let bestSize = 0;
1671
- for (const key of keys) {
1672
- const val = obj[key];
1673
- if (Array.isArray(val) && val.length > bestSize) {
1674
- best = val;
1675
- bestSize = val.length;
1676
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
1677
- const size = Object.keys(val).length;
1678
- if (size > bestSize) {
1679
- best = val;
1680
- bestSize = size;
1681
- }
1682
- }
1683
- }
1684
- return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1685
- }
1686
-
1687
1733
  // src/analysis/rules/response-pii-leak.ts
1734
+ init_limits();
1688
1735
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1689
- var FULL_RECORD_MIN_FIELDS = 5;
1690
- var LIST_PII_MIN_ITEMS = 2;
1691
- function tryParseJson2(body) {
1692
- if (!body) return null;
1693
- try {
1694
- return JSON.parse(body);
1695
- } catch {
1696
- return null;
1697
- }
1698
- }
1699
- function findEmails(obj) {
1736
+ function findEmails(obj, depth = 0) {
1700
1737
  const emails = [];
1738
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
1701
1739
  if (!obj || typeof obj !== "object") return emails;
1702
1740
  if (Array.isArray(obj)) {
1703
- for (let i = 0; i < Math.min(obj.length, 10); i++) {
1704
- emails.push(...findEmails(obj[i]));
1741
+ for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
1742
+ emails.push(...findEmails(obj[i], depth + 1));
1705
1743
  }
1706
1744
  return emails;
1707
1745
  }
@@ -1709,7 +1747,7 @@ function findEmails(obj) {
1709
1747
  if (typeof v === "string" && EMAIL_RE.test(v)) {
1710
1748
  emails.push(v);
1711
1749
  } else if (typeof v === "object" && v !== null) {
1712
- emails.push(...findEmails(v));
1750
+ emails.push(...findEmails(v, depth + 1));
1713
1751
  }
1714
1752
  }
1715
1753
  return emails;
@@ -1728,6 +1766,15 @@ function hasInternalIds(obj) {
1728
1766
  }
1729
1767
  return false;
1730
1768
  }
1769
+ function hasSensitiveFieldNames(obj, depth = 0) {
1770
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
1771
+ if (!obj || typeof obj !== "object") return false;
1772
+ if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
1773
+ for (const key of Object.keys(obj)) {
1774
+ if (SENSITIVE_FIELD_NAMES.test(key)) return true;
1775
+ }
1776
+ return false;
1777
+ }
1731
1778
  function detectEchoPII(method, reqBody, target) {
1732
1779
  if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
1733
1780
  const reqEmails = findEmails(reqBody);
@@ -1749,10 +1796,17 @@ function detectFullRecordPII(target) {
1749
1796
  if (emails.length === 0) return null;
1750
1797
  return { reason: "full-record", emailCount: emails.length };
1751
1798
  }
1799
+ function detectSensitiveFieldPII(target) {
1800
+ const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
1801
+ if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
1802
+ if (!hasSensitiveFieldNames(inspect)) return null;
1803
+ if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
1804
+ return { reason: "sensitive-fields", emailCount: 0 };
1805
+ }
1752
1806
  function detectListPII(target) {
1753
1807
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
1754
1808
  let itemsWithEmail = 0;
1755
- for (let i = 0; i < Math.min(target.length, 10); i++) {
1809
+ for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
1756
1810
  const item = target[i];
1757
1811
  if (item && typeof item === "object" && findEmails(item).length > 0) {
1758
1812
  itemsWithEmail++;
@@ -1767,12 +1821,13 @@ function detectListPII(target) {
1767
1821
  }
1768
1822
  function detectPII(method, reqBody, resBody) {
1769
1823
  const target = unwrapResponse(resBody);
1770
- return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
1824
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
1771
1825
  }
1772
1826
  var REASON_LABELS = {
1773
1827
  echo: "echoes back PII from the request body",
1774
1828
  "full-record": "returns a full record with email and internal IDs",
1775
- "list-pii": "returns a list of records containing email addresses"
1829
+ "list-pii": "returns a list of records containing email addresses",
1830
+ "sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
1776
1831
  };
1777
1832
  var responsePiiLeakRule = {
1778
1833
  id: "response-pii-leak",
@@ -1783,15 +1838,15 @@ var responsePiiLeakRule = {
1783
1838
  const findings = [];
1784
1839
  const seen = /* @__PURE__ */ new Map();
1785
1840
  for (const r of ctx.requests) {
1786
- if (r.statusCode >= 400) continue;
1787
- const resJson = tryParseJson2(r.responseBody);
1841
+ if (isErrorStatus(r.statusCode)) continue;
1842
+ if (SELF_SERVICE_PATH.test(r.path)) continue;
1843
+ const resJson = ctx.parsedBodies.response.get(r.id);
1788
1844
  if (!resJson) continue;
1789
- const reqJson = tryParseJson2(r.requestBody);
1845
+ const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
1790
1846
  const detection = detectPII(r.method, reqJson, resJson);
1791
1847
  if (!detection) continue;
1792
1848
  const ep = `${r.method} ${r.path}`;
1793
- const dedupKey = `${ep}:${detection.reason}`;
1794
- const existing = seen.get(dedupKey);
1849
+ const existing = seen.get(ep);
1795
1850
  if (existing) {
1796
1851
  existing.count++;
1797
1852
  continue;
@@ -1800,12 +1855,12 @@ var responsePiiLeakRule = {
1800
1855
  severity: "warning",
1801
1856
  rule: "response-pii-leak",
1802
1857
  title: "PII Leak in Response",
1803
- desc: `${ep} \u2014 ${REASON_LABELS[detection.reason]}`,
1804
- hint: this.hint,
1858
+ desc: `${ep} \u2014 exposes PII in response`,
1859
+ hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
1805
1860
  endpoint: ep,
1806
1861
  count: 1
1807
1862
  };
1808
- seen.set(dedupKey, finding);
1863
+ seen.set(ep, finding);
1809
1864
  findings.push(finding);
1810
1865
  }
1811
1866
  return findings;
@@ -1870,13 +1925,11 @@ init_constants();
1870
1925
  // src/analysis/insights/rules/regression.ts
1871
1926
  init_constants();
1872
1927
 
1873
- // src/analysis/insight-tracker.ts
1928
+ // src/analysis/issue-mappers.ts
1874
1929
  init_endpoint();
1875
- init_finding_id();
1876
- init_thresholds();
1877
1930
 
1878
1931
  // src/index.ts
1879
- var VERSION = "0.8.5";
1932
+ var VERSION = "0.8.7";
1880
1933
 
1881
1934
  // src/cli/commands/install.ts
1882
1935
  init_constants();
@@ -1884,10 +1937,28 @@ init_constants();
1884
1937
  // src/cli/templates.ts
1885
1938
  var IMPORT_LINE = `import "brakit";`;
1886
1939
  var IMPORT_MARKER = "brakit";
1940
+ var BRAKIT_IMPORT_PATTERNS = [
1941
+ 'import("brakit")',
1942
+ 'import "brakit"',
1943
+ "import 'brakit'",
1944
+ 'require("brakit")',
1945
+ "require('brakit')"
1946
+ ];
1947
+ function containsBrakitImport(content) {
1948
+ return BRAKIT_IMPORT_PATTERNS.some((p) => content.includes(p));
1949
+ }
1950
+ function removeBrakitImportLines(lines) {
1951
+ return lines.filter(
1952
+ (line) => !BRAKIT_IMPORT_PATTERNS.some((p) => line.includes(p))
1953
+ );
1954
+ }
1887
1955
  var CREATED_FILES = [
1888
1956
  "src/instrumentation.ts",
1957
+ "src/instrumentation.js",
1889
1958
  "instrumentation.ts",
1890
- "server/plugins/brakit.ts"
1959
+ "instrumentation.js",
1960
+ "server/plugins/brakit.ts",
1961
+ "server/plugins/brakit.js"
1891
1962
  ];
1892
1963
  var ENTRY_CANDIDATES = [
1893
1964
  "src/index.ts",
@@ -2012,7 +2083,7 @@ var install_default = defineCommand({
2012
2083
  }
2013
2084
  });
2014
2085
  async function installPackage(rootDir, pm) {
2015
- const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
2086
+ const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2016
2087
  const pkg = JSON.parse(pkgRaw);
2017
2088
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2018
2089
  if (allDeps["brakit"]) return false;
@@ -2044,9 +2115,9 @@ async function setupInstrumentation(rootDir, framework) {
2044
2115
  }
2045
2116
  }
2046
2117
  async function setupNextjs(rootDir) {
2047
- const hasSrc = await fileExists(join2(rootDir, "src"));
2118
+ const hasSrc = await fileExists(join3(rootDir, "src"));
2048
2119
  const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
2049
- const absPath = join2(rootDir, relPath);
2120
+ const absPath = join3(rootDir, relPath);
2050
2121
  if (await fileExists(absPath)) {
2051
2122
  const content2 = await readFile4(absPath, "utf-8");
2052
2123
  if (content2.includes(IMPORT_MARKER)) {
@@ -2060,7 +2131,7 @@ async function setupNextjs(rootDir) {
2060
2131
  }
2061
2132
  async function setupNuxt(rootDir) {
2062
2133
  const relPath = "server/plugins/brakit.ts";
2063
- const absPath = join2(rootDir, relPath);
2134
+ const absPath = join3(rootDir, relPath);
2064
2135
  if (await fileExists(absPath)) {
2065
2136
  const content2 = await readFile4(absPath, "utf-8");
2066
2137
  if (content2.includes(IMPORT_MARKER)) {
@@ -2069,7 +2140,7 @@ async function setupNuxt(rootDir) {
2069
2140
  return { action: "manual", file: relPath };
2070
2141
  }
2071
2142
  const content = BRAKIT_TEMPLATES.nuxt + "\n";
2072
- const dir = join2(rootDir, "server/plugins");
2143
+ const dir = join3(rootDir, "server/plugins");
2073
2144
  const { mkdirSync: mkdirSync3 } = await import("fs");
2074
2145
  mkdirSync3(dir, { recursive: true });
2075
2146
  await writeFile3(absPath, content);
@@ -2077,7 +2148,7 @@ async function setupNuxt(rootDir) {
2077
2148
  }
2078
2149
  async function setupPrepend(rootDir, ...candidates) {
2079
2150
  for (const relPath of candidates) {
2080
- const absPath = join2(rootDir, relPath);
2151
+ const absPath = join3(rootDir, relPath);
2081
2152
  if (!await fileExists(absPath)) continue;
2082
2153
  const content = await readFile4(absPath, "utf-8");
2083
2154
  if (content.includes(IMPORT_MARKER)) {
@@ -2091,7 +2162,7 @@ ${content}`);
2091
2162
  }
2092
2163
  async function setupGeneric(rootDir) {
2093
2164
  try {
2094
- const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
2165
+ const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2095
2166
  const pkg = JSON.parse(pkgRaw);
2096
2167
  if (pkg.main && typeof pkg.main === "string") {
2097
2168
  const result2 = await setupPrepend(rootDir, pkg.main);
@@ -2112,7 +2183,7 @@ var MCP_CONFIG = {
2112
2183
  }
2113
2184
  };
2114
2185
  async function setupMcp(rootDir, config = MCP_CONFIG) {
2115
- const mcpPath = join2(rootDir, ".mcp.json");
2186
+ const mcpPath = join3(rootDir, ".mcp.json");
2116
2187
  if (await fileExists(mcpPath)) {
2117
2188
  const raw = await readFile4(mcpPath, "utf-8");
2118
2189
  try {
@@ -2130,7 +2201,7 @@ async function setupMcp(rootDir, config = MCP_CONFIG) {
2130
2201
  return "created";
2131
2202
  }
2132
2203
  async function ensureGitignoreEntry(rootDir, entry) {
2133
- const gitignorePath = join2(rootDir, ".gitignore");
2204
+ const gitignorePath = join3(rootDir, ".gitignore");
2134
2205
  try {
2135
2206
  if (await fileExists(gitignorePath)) {
2136
2207
  const content = await readFile4(gitignorePath, "utf-8");
@@ -2145,7 +2216,7 @@ async function ensureGitignoreEntry(rootDir, entry) {
2145
2216
  function findGitRoot(startDir) {
2146
2217
  let dir = resolve3(startDir);
2147
2218
  while (true) {
2148
- if (existsSync5(join2(dir, ".git"))) return dir;
2219
+ if (existsSync5(join3(dir, ".git"))) return dir;
2149
2220
  const parent = dirname(dir);
2150
2221
  if (parent === dir) return null;
2151
2222
  dir = parent;
@@ -2170,11 +2241,13 @@ function printManualInstructions(framework) {
2170
2241
 
2171
2242
  // src/cli/commands/uninstall.ts
2172
2243
  import { defineCommand as defineCommand2 } from "citty";
2173
- import { resolve as resolve4, join as join3 } from "path";
2174
- import { readFile as readFile5, writeFile as writeFile4, unlink, rm } from "fs/promises";
2244
+ import { resolve as resolve4, join as join4, relative as relative2 } from "path";
2245
+ import { readFile as readFile5, writeFile as writeFile4, unlink, rm, readdir as readdir2 } from "fs/promises";
2175
2246
  import { execSync as execSync2 } from "child_process";
2176
2247
  import pc2 from "picocolors";
2177
2248
  init_constants();
2249
+ init_log();
2250
+ init_type_guards();
2178
2251
  var PREPENDED_FILES = [
2179
2252
  "app/entry.server.tsx",
2180
2253
  "app/entry.server.ts",
@@ -2196,82 +2269,139 @@ var uninstall_default = defineCommand2({
2196
2269
  },
2197
2270
  async run({ args }) {
2198
2271
  const rootDir = resolve4(args.dir);
2199
- let project = null;
2272
+ let projects = [];
2200
2273
  try {
2201
- project = await detectProject(rootDir);
2202
- } catch {
2274
+ const scanned = await scanForProjects(rootDir);
2275
+ projects = scanned.filter((p) => p.type === "node" && p.node).map((p) => ({ dir: p.dir, pm: p.node.packageManager }));
2276
+ } catch (err) {
2277
+ brakitDebug(`uninstall: project scan failed: ${getErrorMessage(err)}`);
2278
+ }
2279
+ if (projects.length === 0) {
2280
+ projects = [{ dir: rootDir, pm: "npm" }];
2203
2281
  }
2204
2282
  console.log();
2205
2283
  console.log(pc2.bold(" \u25C6 brakit uninstall"));
2206
2284
  console.log();
2207
- let removed = false;
2208
- for (const relPath of CREATED_FILES) {
2209
- const absPath = join3(rootDir, relPath);
2210
- if (!await fileExists(absPath)) continue;
2211
- const content = await readFile5(absPath, "utf-8");
2212
- if (!content.includes("brakit")) continue;
2213
- if (isExactBrakitTemplate(content)) {
2214
- await unlink(absPath);
2215
- console.log(pc2.green(` \u2713 Removed ${relPath}`));
2216
- removed = true;
2217
- break;
2218
- }
2219
- const lines = content.split("\n");
2220
- const cleaned = lines.filter(
2221
- (line) => !line.includes('import("brakit")') && !line.includes('import "brakit"')
2222
- );
2223
- if (cleaned.length < lines.length) {
2224
- await writeFile4(absPath, cleaned.join("\n"));
2225
- console.log(pc2.green(` \u2713 Removed brakit lines from ${relPath}`));
2226
- removed = true;
2227
- break;
2228
- }
2229
- }
2230
- if (!removed) {
2231
- const candidates = [...PREPENDED_FILES];
2232
- try {
2233
- const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
2234
- const pkg = JSON.parse(pkgRaw);
2235
- if (pkg.main) candidates.unshift(pkg.main);
2236
- } catch {
2285
+ for (const project of projects) {
2286
+ const suffix = projects.length > 1 ? ` in ${relative2(rootDir, project.dir) || "."}` : "";
2287
+ const removed = await removeInstrumentation(project.dir);
2288
+ if (removed) {
2289
+ console.log(pc2.green(` \u2713 ${removed}${suffix}`));
2290
+ } else {
2291
+ console.log(pc2.dim(` No brakit instrumentation files found${suffix}.`));
2237
2292
  }
2238
- for (const relPath of candidates) {
2239
- const absPath = join3(rootDir, relPath);
2240
- if (!await fileExists(absPath)) continue;
2241
- const content = await readFile5(absPath, "utf-8");
2242
- if (!content.includes(IMPORT_LINE)) continue;
2243
- const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
2244
- await writeFile4(absPath, updated);
2245
- console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
2246
- removed = true;
2247
- break;
2293
+ const uninstalled = await uninstallPackage(project.dir, project.pm);
2294
+ if (uninstalled === true) {
2295
+ console.log(pc2.green(` \u2713 Removed brakit from devDependencies${suffix}`));
2296
+ } else if (uninstalled === "failed") {
2248
2297
  }
2249
2298
  }
2250
- if (!removed) {
2251
- console.log(pc2.dim(" No brakit instrumentation files found."));
2252
- }
2253
2299
  const mcpRemoved = await removeMcpConfig(rootDir);
2254
2300
  if (mcpRemoved) {
2255
2301
  console.log(pc2.green(" \u2713 Removed brakit MCP configuration"));
2256
2302
  }
2257
2303
  const dataRemoved = await removeBrakitData(rootDir);
2258
2304
  if (dataRemoved) {
2259
- console.log(pc2.green(" \u2713 Removed .brakit directory"));
2305
+ console.log(pc2.green(" \u2713 Removed .brakit data"));
2260
2306
  }
2261
2307
  const gitignoreCleaned = await cleanGitignore(rootDir);
2262
2308
  if (gitignoreCleaned) {
2263
2309
  console.log(pc2.green(" \u2713 Removed .brakit from .gitignore"));
2264
2310
  }
2265
- const pm = project?.packageManager ?? "npm";
2266
- const uninstalled = await uninstallPackage(rootDir, pm);
2267
- if (uninstalled) {
2268
- console.log(pc2.green(" \u2713 Removed brakit from devDependencies"));
2311
+ const cacheCleared = await clearBuildCaches(rootDir);
2312
+ if (cacheCleared) {
2313
+ console.log(pc2.green(" \u2713 Cleared build cache"));
2269
2314
  }
2270
2315
  console.log();
2271
2316
  }
2272
2317
  });
2318
+ async function removeInstrumentation(projectDir) {
2319
+ for (const relPath of CREATED_FILES) {
2320
+ const result2 = await tryRemoveBrakitFromFile(projectDir, relPath);
2321
+ if (result2) return result2;
2322
+ }
2323
+ const candidates = [...PREPENDED_FILES];
2324
+ try {
2325
+ const pkgRaw = await readFile5(join4(projectDir, "package.json"), "utf-8");
2326
+ const pkg = JSON.parse(pkgRaw);
2327
+ if (pkg.main) candidates.unshift(pkg.main);
2328
+ } catch (err) {
2329
+ brakitDebug(`uninstall: no package.json main: ${getErrorMessage(err)}`);
2330
+ }
2331
+ for (const relPath of candidates) {
2332
+ const result2 = await tryRemoveImportLine(projectDir, relPath);
2333
+ if (result2) return result2;
2334
+ }
2335
+ const result = await fallbackSearchAndRemove(projectDir);
2336
+ if (result) return result;
2337
+ return null;
2338
+ }
2339
+ async function tryRemoveBrakitFromFile(projectDir, relPath) {
2340
+ const absPath = join4(projectDir, relPath);
2341
+ if (!await fileExists(absPath)) return null;
2342
+ const content = await readFile5(absPath, "utf-8");
2343
+ if (!content.includes("brakit")) return null;
2344
+ if (isExactBrakitTemplate(content)) {
2345
+ await unlink(absPath);
2346
+ return `Removed ${relPath}`;
2347
+ }
2348
+ const lines = content.split("\n");
2349
+ const cleaned = removeBrakitImportLines(lines);
2350
+ if (cleaned.length < lines.length) {
2351
+ await writeFile4(absPath, cleaned.join("\n"));
2352
+ return `Removed brakit lines from ${relPath}`;
2353
+ }
2354
+ return null;
2355
+ }
2356
+ async function tryRemoveImportLine(projectDir, relPath) {
2357
+ const absPath = join4(projectDir, relPath);
2358
+ if (!await fileExists(absPath)) return null;
2359
+ const content = await readFile5(absPath, "utf-8");
2360
+ if (!content.includes(IMPORT_LINE)) return null;
2361
+ const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
2362
+ await writeFile4(absPath, updated);
2363
+ return `Removed brakit import from ${relPath}`;
2364
+ }
2365
+ async function fallbackSearchAndRemove(projectDir) {
2366
+ const dirsToScan = FALLBACK_SCAN_DIRS;
2367
+ for (const dir of dirsToScan) {
2368
+ const absDir = join4(projectDir, dir);
2369
+ if (!await fileExists(absDir)) continue;
2370
+ let entries;
2371
+ try {
2372
+ entries = await readdir2(absDir);
2373
+ } catch (err) {
2374
+ brakitDebug(`uninstall: could not read ${absDir}: ${getErrorMessage(err)}`);
2375
+ continue;
2376
+ }
2377
+ for (const entry of entries) {
2378
+ const ext = entry.slice(entry.lastIndexOf("."));
2379
+ if (!SUPPORTED_SOURCE_EXTENSIONS.has(ext)) continue;
2380
+ const relPath = dir === "." ? entry : `${dir}/${entry}`;
2381
+ const absPath = join4(projectDir, relPath);
2382
+ try {
2383
+ const content = await readFile5(absPath, "utf-8");
2384
+ if (!containsBrakitImport(content)) continue;
2385
+ if (isExactBrakitTemplate(content)) {
2386
+ await unlink(absPath);
2387
+ return `Removed ${relPath}`;
2388
+ }
2389
+ const lines = content.split("\n");
2390
+ const cleaned = removeBrakitImportLines(lines);
2391
+ if (cleaned.length < lines.length) {
2392
+ await writeFile4(absPath, cleaned.join("\n"));
2393
+ return `Removed brakit import from ${relPath}`;
2394
+ }
2395
+ } catch (err) {
2396
+ brakitDebug(`uninstall: fallback scan failed for ${relPath}: ${getErrorMessage(err)}`);
2397
+ continue;
2398
+ }
2399
+ }
2400
+ }
2401
+ return null;
2402
+ }
2273
2403
  async function removeMcpConfig(rootDir) {
2274
- const mcpPath = join3(rootDir, ".mcp.json");
2404
+ const mcpPath = join4(rootDir, ".mcp.json");
2275
2405
  if (!await fileExists(mcpPath)) return false;
2276
2406
  try {
2277
2407
  const raw = await readFile5(mcpPath, "utf-8");
@@ -2284,16 +2414,18 @@ async function removeMcpConfig(rootDir) {
2284
2414
  await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
2285
2415
  }
2286
2416
  return true;
2287
- } catch {
2417
+ } catch (err) {
2418
+ brakitDebug(`uninstall: MCP config cleanup failed: ${getErrorMessage(err)}`);
2288
2419
  return false;
2289
2420
  }
2290
2421
  }
2291
2422
  async function uninstallPackage(rootDir, pm) {
2292
2423
  try {
2293
- const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
2424
+ const pkgRaw = await readFile5(join4(rootDir, "package.json"), "utf-8");
2294
2425
  const pkg = JSON.parse(pkgRaw);
2295
2426
  if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
2296
- } catch {
2427
+ } catch (err) {
2428
+ brakitDebug(`uninstall: could not read package.json: ${getErrorMessage(err)}`);
2297
2429
  return false;
2298
2430
  }
2299
2431
  const cmds = {
@@ -2305,23 +2437,36 @@ async function uninstallPackage(rootDir, pm) {
2305
2437
  const cmd = cmds[pm] ?? cmds.npm;
2306
2438
  try {
2307
2439
  execSync2(cmd, { cwd: rootDir, stdio: "pipe" });
2440
+ return true;
2308
2441
  } catch {
2309
2442
  console.warn(pc2.yellow(` \u26A0 Failed to run "${cmd}". Remove brakit manually.`));
2443
+ return "failed";
2310
2444
  }
2311
- return true;
2312
2445
  }
2313
2446
  async function removeBrakitData(rootDir) {
2314
- const dataDir = join3(rootDir, METRICS_DIR);
2315
- if (!await fileExists(dataDir)) return false;
2316
- try {
2317
- await rm(dataDir, { recursive: true, force: true });
2318
- return true;
2319
- } catch {
2320
- return false;
2447
+ let removed = false;
2448
+ const projectDir = join4(rootDir, METRICS_DIR);
2449
+ if (await fileExists(projectDir)) {
2450
+ try {
2451
+ await rm(projectDir, { recursive: true, force: true });
2452
+ removed = true;
2453
+ } catch (err) {
2454
+ brakitDebug(`uninstall: could not remove ${projectDir}: ${getErrorMessage(err)}`);
2455
+ }
2321
2456
  }
2457
+ const homeDataDir = getProjectDataDir(rootDir);
2458
+ if (await fileExists(homeDataDir)) {
2459
+ try {
2460
+ await rm(homeDataDir, { recursive: true, force: true });
2461
+ removed = true;
2462
+ } catch (err) {
2463
+ brakitDebug(`uninstall: could not remove ${homeDataDir}: ${getErrorMessage(err)}`);
2464
+ }
2465
+ }
2466
+ return removed;
2322
2467
  }
2323
2468
  async function cleanGitignore(rootDir) {
2324
- const gitignorePath = join3(rootDir, ".gitignore");
2469
+ const gitignorePath = join4(rootDir, ".gitignore");
2325
2470
  if (!await fileExists(gitignorePath)) return false;
2326
2471
  try {
2327
2472
  const content = await readFile5(gitignorePath, "utf-8");
@@ -2330,10 +2475,25 @@ async function cleanGitignore(rootDir) {
2330
2475
  if (filtered.length === lines.length) return false;
2331
2476
  await writeFile4(gitignorePath, filtered.join("\n"));
2332
2477
  return true;
2333
- } catch {
2478
+ } catch (err) {
2479
+ brakitDebug(`uninstall: gitignore cleanup failed: ${getErrorMessage(err)}`);
2334
2480
  return false;
2335
2481
  }
2336
2482
  }
2483
+ async function clearBuildCaches(rootDir) {
2484
+ let cleared = false;
2485
+ for (const dir of BUILD_CACHE_DIRS) {
2486
+ const absDir = join4(rootDir, dir);
2487
+ if (!await fileExists(absDir)) continue;
2488
+ try {
2489
+ await rm(absDir, { recursive: true, force: true });
2490
+ cleared = true;
2491
+ } catch (err) {
2492
+ brakitDebug(`uninstall: could not clear cache ${absDir}: ${getErrorMessage(err)}`);
2493
+ }
2494
+ }
2495
+ return cleared;
2496
+ }
2337
2497
 
2338
2498
  // bin/brakit.ts
2339
2499
  var sub = process.argv[2];