brakit 0.8.6 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -10,17 +10,49 @@ import { createHash } from "crypto";
10
10
  import { homedir } from "os";
11
11
  import { resolve, join } from "path";
12
12
 
13
- // src/constants/limits.ts
13
+ // src/constants/config.ts
14
14
  var ANALYSIS_DEBOUNCE_MS = 300;
15
15
  var ISSUE_ID_HASH_LENGTH = 16;
16
16
  var ISSUES_DATA_VERSION = 2;
17
17
  var SECRET_SCAN_ARRAY_LIMIT = 5;
18
18
  var PII_SCAN_ARRAY_LIMIT = 10;
19
19
  var MIN_SECRET_VALUE_LENGTH = 8;
20
- var FULL_RECORD_MIN_FIELDS = 5;
20
+ var FULL_RECORD_MIN_FIELDS = 8;
21
21
  var LIST_PII_MIN_ITEMS = 2;
22
22
  var MAX_OBJECT_SCAN_DEPTH = 5;
23
23
  var ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
24
+ var FLOW_GAP_MS = 5e3;
25
+ var SLOW_REQUEST_THRESHOLD_MS = 2e3;
26
+ var MIN_POLLING_SEQUENCE = 3;
27
+ var ENDPOINT_TRUNCATE_LENGTH = 12;
28
+ var N1_QUERY_THRESHOLD = 5;
29
+ var ERROR_RATE_THRESHOLD_PCT = 20;
30
+ var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
31
+ var MIN_REQUESTS_FOR_INSIGHT = 2;
32
+ var HIGH_QUERY_COUNT_PER_REQ = 5;
33
+ var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
34
+ var CROSS_ENDPOINT_PCT = 50;
35
+ var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
36
+ var REDUNDANT_QUERY_MIN_COUNT = 2;
37
+ var LARGE_RESPONSE_BYTES = 51200;
38
+ var HIGH_ROW_COUNT = 100;
39
+ var OVERFETCH_MIN_REQUESTS = 2;
40
+ var OVERFETCH_MIN_FIELDS = 8;
41
+ var OVERFETCH_MIN_INTERNAL_IDS = 2;
42
+ var OVERFETCH_NULL_RATIO = 0.3;
43
+ var REGRESSION_PCT_THRESHOLD = 50;
44
+ var REGRESSION_MIN_INCREASE_MS = 200;
45
+ var REGRESSION_MIN_REQUESTS = 5;
46
+ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
47
+ var OVERFETCH_MANY_FIELDS = 12;
48
+ var OVERFETCH_UNWRAP_MIN_SIZE = 3;
49
+ var MAX_DUPLICATE_INSIGHTS = 3;
50
+ var INSIGHT_WINDOW_PER_ENDPOINT = 20;
51
+ var CLEAN_HITS_FOR_RESOLUTION = 5;
52
+ var STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
53
+ var STRICT_MODE_MAX_GAP_MS = 2e3;
54
+ var ISSUES_FILE = "issues.json";
55
+ var ISSUES_FLUSH_INTERVAL_MS = 1e4;
24
56
 
25
57
  // src/utils/log.ts
26
58
  var PREFIX = "[brakit]";
@@ -42,7 +74,9 @@ function getErrorMessage(err) {
42
74
  return String(err);
43
75
  }
44
76
  function validateIssuesData(parsed) {
45
- if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
77
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
78
+ const obj = parsed;
79
+ if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
46
80
  return parsed;
47
81
  }
48
82
  return null;
@@ -86,41 +120,6 @@ async function ensureGitignoreAsync(dir, entry) {
86
120
  }
87
121
  }
88
122
 
89
- // src/constants/metrics.ts
90
- var ISSUES_FILE = "issues.json";
91
- var ISSUES_FLUSH_INTERVAL_MS = 1e4;
92
-
93
- // src/constants/thresholds.ts
94
- var FLOW_GAP_MS = 5e3;
95
- var SLOW_REQUEST_THRESHOLD_MS = 2e3;
96
- var MIN_POLLING_SEQUENCE = 3;
97
- var ENDPOINT_TRUNCATE_LENGTH = 12;
98
- var N1_QUERY_THRESHOLD = 5;
99
- var ERROR_RATE_THRESHOLD_PCT = 20;
100
- var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
101
- var MIN_REQUESTS_FOR_INSIGHT = 2;
102
- var HIGH_QUERY_COUNT_PER_REQ = 5;
103
- var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
104
- var CROSS_ENDPOINT_PCT = 50;
105
- var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
106
- var REDUNDANT_QUERY_MIN_COUNT = 2;
107
- var LARGE_RESPONSE_BYTES = 51200;
108
- var HIGH_ROW_COUNT = 100;
109
- var OVERFETCH_MIN_REQUESTS = 2;
110
- var OVERFETCH_MIN_FIELDS = 8;
111
- var OVERFETCH_MIN_INTERNAL_IDS = 2;
112
- var OVERFETCH_NULL_RATIO = 0.3;
113
- var REGRESSION_PCT_THRESHOLD = 50;
114
- var REGRESSION_MIN_INCREASE_MS = 200;
115
- var REGRESSION_MIN_REQUESTS = 5;
116
- var QUERY_COUNT_REGRESSION_RATIO = 1.5;
117
- var OVERFETCH_MANY_FIELDS = 12;
118
- var OVERFETCH_UNWRAP_MIN_SIZE = 3;
119
- var MAX_DUPLICATE_INSIGHTS = 3;
120
- var INSIGHT_WINDOW_PER_ENDPOINT = 20;
121
- var CLEAN_HITS_FOR_RESOLUTION = 5;
122
- var STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
123
-
124
123
  // src/utils/atomic-writer.ts
125
124
  import {
126
125
  writeFileSync as writeFileSync2,
@@ -132,11 +131,10 @@ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
132
131
  var AtomicWriter = class {
133
132
  constructor(opts) {
134
133
  this.opts = opts;
134
+ this.writing = false;
135
+ this.pendingContent = null;
135
136
  this.tmpPath = opts.filePath + ".tmp";
136
137
  }
137
- tmpPath;
138
- writing = false;
139
- pendingContent = null;
140
138
  writeSync(content) {
141
139
  try {
142
140
  this.ensureDir();
@@ -198,6 +196,9 @@ function computeIssueId(issue) {
198
196
  var IssueStore = class {
199
197
  constructor(dataDir) {
200
198
  this.dataDir = dataDir;
199
+ this.issues = /* @__PURE__ */ new Map();
200
+ this.flushTimer = null;
201
+ this.dirty = false;
201
202
  this.issuesPath = resolve2(dataDir, ISSUES_FILE);
202
203
  this.writer = new AtomicWriter({
203
204
  dir: dataDir,
@@ -205,11 +206,6 @@ var IssueStore = class {
205
206
  label: "issues"
206
207
  });
207
208
  }
208
- issues = /* @__PURE__ */ new Map();
209
- flushTimer = null;
210
- dirty = false;
211
- writer;
212
- issuesPath;
213
209
  start() {
214
210
  this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
215
211
  this.flushTimer = setInterval(
@@ -429,8 +425,10 @@ function detectFrameworkFromDeps(allDeps) {
429
425
 
430
426
  // src/instrument/adapter-registry.ts
431
427
  var AdapterRegistry = class {
432
- adapters = [];
433
- active = [];
428
+ constructor() {
429
+ this.adapters = [];
430
+ this.active = [];
431
+ }
434
432
  register(adapter) {
435
433
  this.adapters.push(adapter);
436
434
  }
@@ -468,11 +466,12 @@ function tryParseJson(body) {
468
466
  return null;
469
467
  }
470
468
  }
469
+ var MAX_WRAPPER_KEYS = 3;
471
470
  function unwrapResponse(parsed) {
472
471
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
473
472
  const obj = parsed;
474
473
  const keys = Object.keys(obj);
475
- if (keys.length > 3) return parsed;
474
+ if (keys.length > MAX_WRAPPER_KEYS) return parsed;
476
475
  let best = null;
477
476
  let bestSize = 0;
478
477
  for (const key of keys) {
@@ -504,6 +503,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
504
503
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
505
504
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
506
505
  var INTERNAL_ID_SUFFIX = /Id$|_id$/;
506
+ var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
507
+ 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;
507
508
  var SELECT_STAR_RE = /^SELECT\s+\*/i;
508
509
  var SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
509
510
  var RULE_HINTS = {
@@ -528,27 +529,87 @@ function isRedirect(code) {
528
529
  return code >= 300 && code < 400;
529
530
  }
530
531
 
531
- // src/analysis/rules/exposed-secret.ts
532
- function findSecretKeys(obj, prefix, depth = 0) {
533
- const found = [];
534
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
535
- if (!obj || typeof obj !== "object") return found;
536
- if (Array.isArray(obj)) {
537
- for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
538
- found.push(...findSecretKeys(obj[i], prefix, depth + 1));
532
+ // src/utils/collections.ts
533
+ function getOrCreate(map, key, create) {
534
+ let value = map.get(key);
535
+ if (value === void 0) {
536
+ value = create();
537
+ map.set(key, value);
538
+ }
539
+ return value;
540
+ }
541
+ function deduplicateFindings(items, extract) {
542
+ const seen = /* @__PURE__ */ new Map();
543
+ const findings = [];
544
+ for (const item of items) {
545
+ const result = extract(item);
546
+ if (!result) continue;
547
+ const existing = seen.get(result.key);
548
+ if (existing) {
549
+ existing.count++;
550
+ continue;
551
+ }
552
+ seen.set(result.key, result.finding);
553
+ findings.push(result.finding);
554
+ }
555
+ return findings;
556
+ }
557
+ function groupBy(items, keyFn) {
558
+ const map = /* @__PURE__ */ new Map();
559
+ for (const item of items) {
560
+ const key = keyFn(item);
561
+ if (key == null) continue;
562
+ let arr = map.get(key);
563
+ if (!arr) {
564
+ arr = [];
565
+ map.set(key, arr);
539
566
  }
540
- return found;
567
+ arr.push(item);
541
568
  }
542
- for (const k of Object.keys(obj)) {
543
- const val = obj[k];
544
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
545
- found.push(k);
569
+ return map;
570
+ }
571
+
572
+ // src/utils/object-scan.ts
573
+ var DEFAULTS = {
574
+ maxDepth: MAX_OBJECT_SCAN_DEPTH,
575
+ arrayLimit: SECRET_SCAN_ARRAY_LIMIT
576
+ };
577
+ function walkObject(obj, visitor, options) {
578
+ const opts = { ...DEFAULTS, ...options };
579
+ walk(obj, visitor, opts, 0);
580
+ }
581
+ function walk(obj, visitor, opts, depth) {
582
+ if (depth >= opts.maxDepth) return;
583
+ if (!obj || typeof obj !== "object") return;
584
+ if (Array.isArray(obj)) {
585
+ for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
586
+ walk(obj[i], visitor, opts, depth + 1);
546
587
  }
588
+ return;
589
+ }
590
+ for (const key of Object.keys(obj)) {
591
+ const val = obj[key];
592
+ visitor(key, val, depth);
547
593
  if (typeof val === "object" && val !== null) {
548
- found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
594
+ walk(val, visitor, opts, depth + 1);
549
595
  }
550
596
  }
551
- return found;
597
+ }
598
+ function collectFromObject(obj, match, options) {
599
+ const results = [];
600
+ walkObject(obj, (key, value) => {
601
+ const result = match(key, value);
602
+ if (result !== null) results.push(result);
603
+ }, options);
604
+ return results;
605
+ }
606
+
607
+ // src/analysis/rules/auth-rules.ts
608
+ function findSecretKeys(obj) {
609
+ return collectFromObject(
610
+ obj,
611
+ (key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
612
+ );
552
613
  }
553
614
  var exposedSecretRule = {
554
615
  id: "exposed-secret",
@@ -556,50 +617,38 @@ var exposedSecretRule = {
556
617
  name: "Exposed Secret in Response",
557
618
  hint: RULE_HINTS["exposed-secret"],
558
619
  check(ctx) {
559
- const findings = [];
560
- const seen = /* @__PURE__ */ new Map();
561
- for (const r of ctx.requests) {
562
- if (isErrorStatus(r.statusCode)) continue;
563
- const parsed = ctx.parsedBodies.response.get(r.id);
564
- if (!parsed) continue;
565
- const keys = findSecretKeys(parsed, "");
566
- if (keys.length === 0) continue;
567
- const ep = `${r.method} ${r.path}`;
568
- const dedupKey = `${ep}:${keys.sort().join(",")}`;
569
- const existing = seen.get(dedupKey);
570
- if (existing) {
571
- existing.count++;
572
- continue;
573
- }
574
- const finding = {
575
- severity: "critical",
576
- rule: "exposed-secret",
577
- title: "Exposed Secret in Response",
578
- desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
579
- hint: this.hint,
580
- endpoint: ep,
581
- count: 1
620
+ return deduplicateFindings(ctx.requests, (request) => {
621
+ if (isErrorStatus(request.statusCode)) return null;
622
+ const parsed = ctx.parsedBodies.response.get(request.id);
623
+ if (!parsed) return null;
624
+ const keys = findSecretKeys(parsed);
625
+ if (keys.length === 0) return null;
626
+ const ep = `${request.method} ${request.path}`;
627
+ return {
628
+ key: `${ep}:${keys.sort().join(",")}`,
629
+ finding: {
630
+ severity: "critical",
631
+ rule: "exposed-secret",
632
+ title: "Exposed Secret in Response",
633
+ desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
634
+ hint: this.hint,
635
+ endpoint: ep,
636
+ count: 1
637
+ }
582
638
  };
583
- seen.set(dedupKey, finding);
584
- findings.push(finding);
585
- }
586
- return findings;
639
+ });
587
640
  }
588
641
  };
589
-
590
- // src/analysis/rules/token-in-url.ts
591
642
  var tokenInUrlRule = {
592
643
  id: "token-in-url",
593
644
  severity: "critical",
594
645
  name: "Auth Token in URL",
595
646
  hint: RULE_HINTS["token-in-url"],
596
647
  check(ctx) {
597
- const findings = [];
598
- const seen = /* @__PURE__ */ new Map();
599
- for (const r of ctx.requests) {
600
- const qIdx = r.url.indexOf("?");
601
- if (qIdx === -1) continue;
602
- const params = r.url.substring(qIdx + 1).split("&");
648
+ return deduplicateFindings(ctx.requests, (request) => {
649
+ const qIdx = request.url.indexOf("?");
650
+ if (qIdx === -1) return null;
651
+ const params = request.url.substring(qIdx + 1).split("&");
603
652
  const flagged = [];
604
653
  for (const param of params) {
605
654
  const [name, ...rest] = param.split("=");
@@ -609,65 +658,124 @@ var tokenInUrlRule = {
609
658
  flagged.push(name);
610
659
  }
611
660
  }
612
- if (flagged.length === 0) continue;
613
- const ep = `${r.method} ${r.path}`;
614
- const dedupKey = `${ep}:${flagged.sort().join(",")}`;
615
- const existing = seen.get(dedupKey);
616
- if (existing) {
617
- existing.count++;
618
- continue;
661
+ if (flagged.length === 0) return null;
662
+ const ep = `${request.method} ${request.path}`;
663
+ return {
664
+ key: `${ep}:${flagged.sort().join(",")}`,
665
+ finding: {
666
+ severity: "critical",
667
+ rule: "token-in-url",
668
+ title: "Auth Token in URL",
669
+ desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
670
+ hint: this.hint,
671
+ endpoint: ep,
672
+ count: 1
673
+ }
674
+ };
675
+ });
676
+ }
677
+ };
678
+ function isFrameworkResponse(request) {
679
+ if (isRedirect(request.statusCode)) return true;
680
+ if (request.path?.startsWith("/__")) return true;
681
+ if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
682
+ return false;
683
+ }
684
+ var insecureCookieRule = {
685
+ id: "insecure-cookie",
686
+ severity: "warning",
687
+ name: "Insecure Cookie",
688
+ hint: RULE_HINTS["insecure-cookie"],
689
+ check(ctx) {
690
+ const cookieEntries = [];
691
+ for (const request of ctx.requests) {
692
+ if (!request.responseHeaders) continue;
693
+ if (isFrameworkResponse(request)) continue;
694
+ const setCookie = request.responseHeaders["set-cookie"];
695
+ if (!setCookie) continue;
696
+ const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
697
+ for (const cookie of cookies) {
698
+ cookieEntries.push({ cookie });
619
699
  }
620
- const finding = {
621
- severity: "critical",
622
- rule: "token-in-url",
623
- title: "Auth Token in URL",
624
- desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
700
+ }
701
+ return deduplicateFindings(cookieEntries, ({ cookie }) => {
702
+ const cookieName = cookie.trim().split("=")[0].trim();
703
+ const lower = cookie.toLowerCase();
704
+ const issues = [];
705
+ if (!lower.includes("httponly")) issues.push("HttpOnly");
706
+ if (!lower.includes("samesite")) issues.push("SameSite");
707
+ if (issues.length === 0) return null;
708
+ return {
709
+ key: `${cookieName}:${issues.join(",")}`,
710
+ finding: {
711
+ severity: "warning",
712
+ rule: "insecure-cookie",
713
+ title: "Insecure Cookie",
714
+ desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
715
+ hint: this.hint,
716
+ endpoint: cookieName,
717
+ count: 1
718
+ }
719
+ };
720
+ });
721
+ }
722
+ };
723
+ var corsCredentialsRule = {
724
+ id: "cors-credentials",
725
+ severity: "warning",
726
+ name: "CORS Credentials with Wildcard",
727
+ hint: RULE_HINTS["cors-credentials"],
728
+ check(ctx) {
729
+ const findings = [];
730
+ const seen = /* @__PURE__ */ new Set();
731
+ for (const request of ctx.requests) {
732
+ if (!request.responseHeaders) continue;
733
+ const origin = request.responseHeaders["access-control-allow-origin"];
734
+ const creds = request.responseHeaders["access-control-allow-credentials"];
735
+ if (origin !== "*" || creds !== "true") continue;
736
+ const ep = `${request.method} ${request.path}`;
737
+ if (seen.has(ep)) continue;
738
+ seen.add(ep);
739
+ findings.push({
740
+ severity: "warning",
741
+ rule: "cors-credentials",
742
+ title: "CORS Credentials with Wildcard",
743
+ desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
625
744
  hint: this.hint,
626
745
  endpoint: ep,
627
746
  count: 1
628
- };
629
- seen.set(dedupKey, finding);
630
- findings.push(finding);
747
+ });
631
748
  }
632
749
  return findings;
633
750
  }
634
751
  };
635
752
 
636
- // src/analysis/rules/stack-trace-leak.ts
753
+ // src/analysis/rules/data-rules.ts
637
754
  var stackTraceLeakRule = {
638
755
  id: "stack-trace-leak",
639
756
  severity: "critical",
640
757
  name: "Stack Trace Leaked to Client",
641
758
  hint: RULE_HINTS["stack-trace-leak"],
642
759
  check(ctx) {
643
- const findings = [];
644
- const seen = /* @__PURE__ */ new Map();
645
- for (const r of ctx.requests) {
646
- if (!r.responseBody) continue;
647
- if (!STACK_TRACE_RE.test(r.responseBody)) continue;
648
- const ep = `${r.method} ${r.path}`;
649
- const existing = seen.get(ep);
650
- if (existing) {
651
- existing.count++;
652
- continue;
653
- }
654
- const finding = {
655
- severity: "critical",
656
- rule: "stack-trace-leak",
657
- title: "Stack Trace Leaked to Client",
658
- desc: `${ep} \u2014 response exposes internal stack trace`,
659
- hint: this.hint,
660
- endpoint: ep,
661
- count: 1
760
+ return deduplicateFindings(ctx.requests, (request) => {
761
+ if (!request.responseBody) return null;
762
+ if (!STACK_TRACE_RE.test(request.responseBody)) return null;
763
+ const ep = `${request.method} ${request.path}`;
764
+ return {
765
+ key: ep,
766
+ finding: {
767
+ severity: "critical",
768
+ rule: "stack-trace-leak",
769
+ title: "Stack Trace Leaked to Client",
770
+ desc: `${ep} \u2014 response exposes internal stack trace`,
771
+ hint: this.hint,
772
+ endpoint: ep,
773
+ count: 1
774
+ }
662
775
  };
663
- seen.set(ep, finding);
664
- findings.push(finding);
665
- }
666
- return findings;
776
+ });
667
777
  }
668
778
  };
669
-
670
- // src/analysis/rules/error-info-leak.ts
671
779
  var CRITICAL_PATTERNS = [
672
780
  { re: DB_CONN_RE, label: "database connection string" },
673
781
  { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
@@ -679,90 +787,34 @@ var errorInfoLeakRule = {
679
787
  name: "Sensitive Data in Error Response",
680
788
  hint: RULE_HINTS["error-info-leak"],
681
789
  check(ctx) {
682
- const findings = [];
683
- const seen = /* @__PURE__ */ new Map();
684
- for (const r of ctx.requests) {
685
- if (r.statusCode < 400) continue;
686
- if (!r.responseBody) continue;
687
- if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
688
- const ep = `${r.method} ${r.path}`;
689
- for (const p of CRITICAL_PATTERNS) {
690
- if (!p.re.test(r.responseBody)) continue;
691
- const dedupKey = `${ep}:${p.label}`;
692
- const existing = seen.get(dedupKey);
693
- if (existing) {
694
- existing.count++;
695
- continue;
790
+ const entries = [];
791
+ for (const request of ctx.requests) {
792
+ if (request.statusCode < 400) continue;
793
+ if (!request.responseBody) continue;
794
+ if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
795
+ const ep = `${request.method} ${request.path}`;
796
+ for (const pattern of CRITICAL_PATTERNS) {
797
+ if (pattern.re.test(request.responseBody)) {
798
+ entries.push({ ep, pattern, body: request.responseBody });
696
799
  }
697
- const finding = {
800
+ }
801
+ }
802
+ return deduplicateFindings(entries, ({ ep, pattern }) => {
803
+ return {
804
+ key: `${ep}:${pattern.label}`,
805
+ finding: {
698
806
  severity: "critical",
699
807
  rule: "error-info-leak",
700
808
  title: "Sensitive Data in Error Response",
701
- desc: `${ep} \u2014 error response exposes ${p.label}`,
809
+ desc: `${ep} \u2014 error response exposes ${pattern.label}`,
702
810
  hint: this.hint,
703
811
  endpoint: ep,
704
812
  count: 1
705
- };
706
- seen.set(dedupKey, finding);
707
- findings.push(finding);
708
- }
709
- }
710
- return findings;
711
- }
712
- };
713
-
714
- // src/analysis/rules/insecure-cookie.ts
715
- function isFrameworkResponse(r) {
716
- if (isRedirect(r.statusCode)) return true;
717
- if (r.path?.startsWith("/__")) return true;
718
- if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
719
- return false;
720
- }
721
- var insecureCookieRule = {
722
- id: "insecure-cookie",
723
- severity: "warning",
724
- name: "Insecure Cookie",
725
- hint: RULE_HINTS["insecure-cookie"],
726
- check(ctx) {
727
- const findings = [];
728
- const seen = /* @__PURE__ */ new Map();
729
- for (const r of ctx.requests) {
730
- if (!r.responseHeaders) continue;
731
- if (isFrameworkResponse(r)) continue;
732
- const setCookie = r.responseHeaders["set-cookie"];
733
- if (!setCookie) continue;
734
- const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
735
- for (const cookie of cookies) {
736
- const cookieName = cookie.trim().split("=")[0].trim();
737
- const lower = cookie.toLowerCase();
738
- const issues = [];
739
- if (!lower.includes("httponly")) issues.push("HttpOnly");
740
- if (!lower.includes("samesite")) issues.push("SameSite");
741
- if (issues.length === 0) continue;
742
- const dedupKey = `${cookieName}:${issues.join(",")}`;
743
- const existing = seen.get(dedupKey);
744
- if (existing) {
745
- existing.count++;
746
- continue;
747
813
  }
748
- const finding = {
749
- severity: "warning",
750
- rule: "insecure-cookie",
751
- title: "Insecure Cookie",
752
- desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
753
- hint: this.hint,
754
- endpoint: cookieName,
755
- count: 1
756
- };
757
- seen.set(dedupKey, finding);
758
- findings.push(finding);
759
- }
760
- }
761
- return findings;
814
+ };
815
+ });
762
816
  }
763
817
  };
764
-
765
- // src/analysis/rules/sensitive-logs.ts
766
818
  var sensitiveLogsRule = {
767
819
  id: "sensitive-logs",
768
820
  severity: "warning",
@@ -787,60 +839,15 @@ var sensitiveLogsRule = {
787
839
  }];
788
840
  }
789
841
  };
790
-
791
- // src/analysis/rules/cors-credentials.ts
792
- var corsCredentialsRule = {
793
- id: "cors-credentials",
794
- severity: "warning",
795
- name: "CORS Credentials with Wildcard",
796
- hint: RULE_HINTS["cors-credentials"],
797
- check(ctx) {
798
- const findings = [];
799
- const seen = /* @__PURE__ */ new Set();
800
- for (const r of ctx.requests) {
801
- if (!r.responseHeaders) continue;
802
- const origin = r.responseHeaders["access-control-allow-origin"];
803
- const creds = r.responseHeaders["access-control-allow-credentials"];
804
- if (origin !== "*" || creds !== "true") continue;
805
- const ep = `${r.method} ${r.path}`;
806
- if (seen.has(ep)) continue;
807
- seen.add(ep);
808
- findings.push({
809
- severity: "warning",
810
- rule: "cors-credentials",
811
- title: "CORS Credentials with Wildcard",
812
- desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
813
- hint: this.hint,
814
- endpoint: ep,
815
- count: 1
816
- });
817
- }
818
- return findings;
819
- }
820
- };
821
-
822
- // src/analysis/rules/response-pii-leak.ts
823
842
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
824
- function findEmails(obj, depth = 0) {
825
- const emails = [];
826
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
827
- if (!obj || typeof obj !== "object") return emails;
828
- if (Array.isArray(obj)) {
829
- for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
830
- emails.push(...findEmails(obj[i], depth + 1));
831
- }
832
- return emails;
833
- }
834
- for (const v of Object.values(obj)) {
835
- if (typeof v === "string" && EMAIL_RE.test(v)) {
836
- emails.push(v);
837
- } else if (typeof v === "object" && v !== null) {
838
- emails.push(...findEmails(v, depth + 1));
839
- }
840
- }
841
- return emails;
842
- }
843
- function topLevelFieldCount(obj) {
843
+ function findEmails(obj) {
844
+ return collectFromObject(
845
+ obj,
846
+ (_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
847
+ { arrayLimit: PII_SCAN_ARRAY_LIMIT }
848
+ );
849
+ }
850
+ function topLevelFieldCount(obj) {
844
851
  if (Array.isArray(obj)) {
845
852
  return obj.length > 0 ? topLevelFieldCount(obj[0]) : 0;
846
853
  }
@@ -854,6 +861,15 @@ function hasInternalIds(obj) {
854
861
  }
855
862
  return false;
856
863
  }
864
+ function hasSensitiveFieldNames(obj, depth = 0) {
865
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
866
+ if (!obj || typeof obj !== "object") return false;
867
+ if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
868
+ for (const key of Object.keys(obj)) {
869
+ if (SENSITIVE_FIELD_NAMES.test(key)) return true;
870
+ }
871
+ return false;
872
+ }
857
873
  function detectEchoPII(method, reqBody, target) {
858
874
  if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
859
875
  const reqEmails = findEmails(reqBody);
@@ -875,6 +891,13 @@ function detectFullRecordPII(target) {
875
891
  if (emails.length === 0) return null;
876
892
  return { reason: "full-record", emailCount: emails.length };
877
893
  }
894
+ function detectSensitiveFieldPII(target) {
895
+ const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
896
+ if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
897
+ if (!hasSensitiveFieldNames(inspect)) return null;
898
+ if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
899
+ return { reason: "sensitive-fields", emailCount: 0 };
900
+ }
878
901
  function detectListPII(target) {
879
902
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
880
903
  let itemsWithEmail = 0;
@@ -893,12 +916,13 @@ function detectListPII(target) {
893
916
  }
894
917
  function detectPII(method, reqBody, resBody) {
895
918
  const target = unwrapResponse(resBody);
896
- return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
919
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
897
920
  }
898
921
  var REASON_LABELS = {
899
922
  echo: "echoes back PII from the request body",
900
923
  "full-record": "returns a full record with email and internal IDs",
901
- "list-pii": "returns a list of records containing email addresses"
924
+ "list-pii": "returns a list of records containing email addresses",
925
+ "sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
902
926
  };
903
927
  var responsePiiLeakRule = {
904
928
  id: "response-pii-leak",
@@ -906,35 +930,28 @@ var responsePiiLeakRule = {
906
930
  name: "PII Leak in Response",
907
931
  hint: RULE_HINTS["response-pii-leak"],
908
932
  check(ctx) {
909
- const findings = [];
910
- const seen = /* @__PURE__ */ new Map();
911
- for (const r of ctx.requests) {
912
- if (isErrorStatus(r.statusCode)) continue;
913
- const resJson = ctx.parsedBodies.response.get(r.id);
914
- if (!resJson) continue;
915
- const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
916
- const detection = detectPII(r.method, reqJson, resJson);
917
- if (!detection) continue;
918
- const ep = `${r.method} ${r.path}`;
919
- const dedupKey = `${ep}:${detection.reason}`;
920
- const existing = seen.get(dedupKey);
921
- if (existing) {
922
- existing.count++;
923
- continue;
924
- }
925
- const finding = {
926
- severity: "warning",
927
- rule: "response-pii-leak",
928
- title: "PII Leak in Response",
929
- desc: `${ep} \u2014 ${REASON_LABELS[detection.reason]}`,
930
- hint: this.hint,
931
- endpoint: ep,
932
- count: 1
933
+ return deduplicateFindings(ctx.requests, (request) => {
934
+ if (isErrorStatus(request.statusCode)) return null;
935
+ if (SELF_SERVICE_PATH.test(request.path)) return null;
936
+ const resJson = ctx.parsedBodies.response.get(request.id);
937
+ if (!resJson) return null;
938
+ const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
939
+ const detection = detectPII(request.method, reqJson, resJson);
940
+ if (!detection) return null;
941
+ const ep = `${request.method} ${request.path}`;
942
+ return {
943
+ key: ep,
944
+ finding: {
945
+ severity: "warning",
946
+ rule: "response-pii-leak",
947
+ title: "PII Leak in Response",
948
+ desc: `${ep} \u2014 exposes PII in response`,
949
+ hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
950
+ endpoint: ep,
951
+ count: 1
952
+ }
933
953
  };
934
- seen.set(dedupKey, finding);
935
- findings.push(finding);
936
- }
937
- return findings;
954
+ });
938
955
  }
939
956
  };
940
957
 
@@ -942,20 +959,22 @@ var responsePiiLeakRule = {
942
959
  function buildBodyCache(requests) {
943
960
  const response = /* @__PURE__ */ new Map();
944
961
  const request = /* @__PURE__ */ new Map();
945
- for (const r of requests) {
946
- if (r.responseBody) {
947
- const parsed = tryParseJson(r.responseBody);
948
- if (parsed != null) response.set(r.id, parsed);
962
+ for (const req of requests) {
963
+ if (req.responseBody) {
964
+ const parsed = tryParseJson(req.responseBody);
965
+ if (parsed != null) response.set(req.id, parsed);
949
966
  }
950
- if (r.requestBody) {
951
- const parsed = tryParseJson(r.requestBody);
952
- if (parsed != null) request.set(r.id, parsed);
967
+ if (req.requestBody) {
968
+ const parsed = tryParseJson(req.requestBody);
969
+ if (parsed != null) request.set(req.id, parsed);
953
970
  }
954
971
  }
955
972
  return { response, request };
956
973
  }
957
974
  var SecurityScanner = class {
958
- rules = [];
975
+ constructor() {
976
+ this.rules = [];
977
+ }
959
978
  register(rule) {
960
979
  this.rules.push(rule);
961
980
  }
@@ -968,7 +987,8 @@ var SecurityScanner = class {
968
987
  for (const rule of this.rules) {
969
988
  try {
970
989
  findings.push(...rule.check(ctx));
971
- } catch {
990
+ } catch (e) {
991
+ brakitDebug(`rule ${rule.id} failed: ${getErrorMessage(e)}`);
972
992
  }
973
993
  }
974
994
  return findings;
@@ -992,7 +1012,9 @@ function createDefaultScanner() {
992
1012
 
993
1013
  // src/core/disposable.ts
994
1014
  var SubscriptionBag = class {
995
- items = [];
1015
+ constructor() {
1016
+ this.items = [];
1017
+ }
996
1018
  add(teardown) {
997
1019
  this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
998
1020
  }
@@ -1005,7 +1027,7 @@ var SubscriptionBag = class {
1005
1027
  // src/analysis/group.ts
1006
1028
  import { randomUUID } from "crypto";
1007
1029
 
1008
- // src/constants/routes.ts
1030
+ // src/constants/labels.ts
1009
1031
  var DASHBOARD_PREFIX = "/__brakit";
1010
1032
  var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
1011
1033
  var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
@@ -1037,13 +1059,40 @@ var VALID_TABS_TUPLE = [
1037
1059
  ];
1038
1060
  var VALID_TABS = new Set(VALID_TABS_TUPLE);
1039
1061
 
1040
- // src/constants/network.ts
1062
+ // src/constants/features.ts
1041
1063
  var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
1042
1064
 
1065
+ // src/utils/endpoint.ts
1066
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1067
+ var NUMERIC_ID_RE = /^\d+$/;
1068
+ var HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
1069
+ var ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
1070
+ function isDynamicSegment(segment) {
1071
+ return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
1072
+ }
1073
+ var DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
1074
+ function normalizePath(path) {
1075
+ const qIdx = path.indexOf("?");
1076
+ const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
1077
+ return pathname.split("/").map((seg) => seg && isDynamicSegment(seg) ? DYNAMIC_SEGMENT_PLACEHOLDER : seg).join("/");
1078
+ }
1079
+ function getEndpointKey(method, path) {
1080
+ return `${method} ${normalizePath(path)}`;
1081
+ }
1082
+ var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1083
+ function extractEndpointFromDesc(desc) {
1084
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1085
+ }
1086
+ function stripQueryString(path) {
1087
+ const i = path.indexOf("?");
1088
+ return i === -1 ? path : path.slice(0, i);
1089
+ }
1090
+
1043
1091
  // src/analysis/categorize.ts
1044
1092
  function detectCategory(req) {
1045
1093
  const { method, url, statusCode, responseHeaders } = req;
1046
1094
  if (req.isStatic) return "static";
1095
+ if (req.isHealthCheck) return "health-check";
1047
1096
  if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
1048
1097
  return "auth-handshake";
1049
1098
  }
@@ -1195,20 +1244,42 @@ function capitalize(s) {
1195
1244
  }
1196
1245
 
1197
1246
  // src/analysis/transforms.ts
1198
- function markDuplicates(requests) {
1247
+ var DUPLICATE_CATEGORIES = /* @__PURE__ */ new Set(["data-fetch", "auth-check"]);
1248
+ function isDuplicateCandidate(req) {
1249
+ return DUPLICATE_CATEGORIES.has(req.category);
1250
+ }
1251
+ function buildRequestKey(req) {
1252
+ return `${req.method} ${stripQueryString(getEffectivePath(req))}`;
1253
+ }
1254
+ function isStrictModePattern(requests, counts) {
1255
+ if (counts.size === 0 || ![...counts.values()].every((c) => c === 2)) {
1256
+ return false;
1257
+ }
1258
+ const firstByKey = /* @__PURE__ */ new Map();
1259
+ for (const req of requests) {
1260
+ if (!isDuplicateCandidate(req)) continue;
1261
+ const key = buildRequestKey(req);
1262
+ const first = firstByKey.get(key);
1263
+ if (!first) {
1264
+ firstByKey.set(key, req);
1265
+ } else if (Math.abs(req.startedAt - first.startedAt) > STRICT_MODE_MAX_GAP_MS) {
1266
+ return false;
1267
+ }
1268
+ }
1269
+ return true;
1270
+ }
1271
+ function flagDuplicateRequests(requests) {
1199
1272
  const counts = /* @__PURE__ */ new Map();
1200
1273
  for (const req of requests) {
1201
- if (req.category !== "data-fetch" && req.category !== "auth-check")
1202
- continue;
1203
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
1274
+ if (!isDuplicateCandidate(req)) continue;
1275
+ const key = buildRequestKey(req);
1204
1276
  counts.set(key, (counts.get(key) ?? 0) + 1);
1205
1277
  }
1206
- const isStrictMode = counts.size > 0 && [...counts.values()].every((c) => c === 2);
1278
+ const isStrictMode = isStrictModePattern(requests, counts);
1207
1279
  const seen = /* @__PURE__ */ new Set();
1208
1280
  for (const req of requests) {
1209
- if (req.category !== "data-fetch" && req.category !== "auth-check")
1210
- continue;
1211
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
1281
+ if (!isDuplicateCandidate(req)) continue;
1282
+ const key = buildRequestKey(req);
1212
1283
  if (seen.has(key)) {
1213
1284
  if (isStrictMode) {
1214
1285
  req.isStrictModeDupe = true;
@@ -1220,20 +1291,20 @@ function markDuplicates(requests) {
1220
1291
  }
1221
1292
  }
1222
1293
  }
1223
- function collapsePolling(requests) {
1294
+ function mergePollingSequences(requests) {
1224
1295
  const result = [];
1225
1296
  let i = 0;
1226
1297
  while (i < requests.length) {
1227
1298
  const current = requests[i];
1228
- const currentEffective = getEffectivePath(current).split("?")[0];
1299
+ const currentEffective = stripQueryString(getEffectivePath(current));
1229
1300
  if (current.method === "GET" && current.category === "data-fetch") {
1230
- let j = i + 1;
1231
- while (j < requests.length && requests[j].method === "GET" && getEffectivePath(requests[j]).split("?")[0] === currentEffective) {
1232
- j++;
1301
+ let nextIndex = i + 1;
1302
+ while (nextIndex < requests.length && requests[nextIndex].method === "GET" && stripQueryString(getEffectivePath(requests[nextIndex])) === currentEffective) {
1303
+ nextIndex++;
1233
1304
  }
1234
- const count = j - i;
1305
+ const count = nextIndex - i;
1235
1306
  if (count >= MIN_POLLING_SEQUENCE) {
1236
- const last = requests[j - 1];
1307
+ const last = requests[nextIndex - 1];
1237
1308
  const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
1238
1309
  const endpointName = prettifyEndpoint(currentEffective);
1239
1310
  result.push({
@@ -1244,7 +1315,7 @@ function collapsePolling(requests) {
1244
1315
  pollingDurationMs: pollingDuration,
1245
1316
  isDuplicate: false
1246
1317
  });
1247
- i = j;
1318
+ i = nextIndex;
1248
1319
  continue;
1249
1320
  }
1250
1321
  }
@@ -1257,18 +1328,18 @@ function formatDurationLabel(ms) {
1257
1328
  if (ms < 1e3) return `${ms}ms`;
1258
1329
  return `${(ms / 1e3).toFixed(1)}s`;
1259
1330
  }
1260
- function detectWarnings(requests) {
1331
+ function collectRequestWarnings(requests) {
1261
1332
  const warnings = [];
1262
1333
  const duplicateCount = requests.filter((r) => r.isDuplicate).length;
1263
1334
  if (duplicateCount > 0) {
1264
1335
  const unique = new Set(
1265
- requests.filter((r) => r.isDuplicate).map((r) => `${r.method} ${getEffectivePath(r).split("?")[0]}`)
1336
+ requests.filter((r) => r.isDuplicate).map((r) => buildRequestKey(r))
1266
1337
  );
1267
1338
  const endpoints = unique.size;
1268
1339
  const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
1269
- const key = `${r.method} ${getEffectivePath(r).split("?")[0]}`;
1340
+ const key = buildRequestKey(r);
1270
1341
  const first = requests.find(
1271
- (o) => !o.isDuplicate && `${o.method} ${getEffectivePath(o).split("?")[0]}` === key
1342
+ (o) => !o.isDuplicate && buildRequestKey(o) === key
1272
1343
  );
1273
1344
  return first && first.responseBody === r.responseBody;
1274
1345
  });
@@ -1291,6 +1362,14 @@ function detectWarnings(requests) {
1291
1362
  }
1292
1363
 
1293
1364
  // src/analysis/group.ts
1365
+ function shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, startedAt) {
1366
+ if (currentRequests.length === 0) return false;
1367
+ const sourcePage = labeled.sourcePage;
1368
+ const isNewPage = sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1369
+ const isTimeGap = startedAt - lastEndTime > FLOW_GAP_MS;
1370
+ const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1371
+ return isNewPage || isTimeGap || isPageLoad;
1372
+ }
1294
1373
  function groupRequestsIntoFlows(requests) {
1295
1374
  if (requests.length === 0) return [];
1296
1375
  const flows = [];
@@ -1301,17 +1380,12 @@ function groupRequestsIntoFlows(requests) {
1301
1380
  if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
1302
1381
  const labeled = labelRequest(req);
1303
1382
  if (labeled.category === "static") continue;
1304
- const sourcePage = labeled.sourcePage;
1305
- const gap = currentRequests.length > 0 ? req.startedAt - lastEndTime : 0;
1306
- const isNewPage = currentRequests.length > 0 && sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1307
- const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1308
- const isTimeGap = currentRequests.length > 0 && gap > FLOW_GAP_MS;
1309
- if (currentRequests.length > 0 && (isNewPage || isTimeGap || isPageLoad)) {
1383
+ if (shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, req.startedAt)) {
1310
1384
  flows.push(buildFlow(currentRequests));
1311
1385
  currentRequests = [];
1312
1386
  }
1313
1387
  currentRequests.push(labeled);
1314
- currentSourcePage = sourcePage ?? currentSourcePage;
1388
+ currentSourcePage = labeled.sourcePage ?? currentSourcePage;
1315
1389
  lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
1316
1390
  }
1317
1391
  if (currentRequests.length > 0) {
@@ -1320,8 +1394,8 @@ function groupRequestsIntoFlows(requests) {
1320
1394
  return flows;
1321
1395
  }
1322
1396
  function buildFlow(rawRequests) {
1323
- markDuplicates(rawRequests);
1324
- const requests = collapsePolling(rawRequests);
1397
+ flagDuplicateRequests(rawRequests);
1398
+ const requests = mergePollingSequences(rawRequests);
1325
1399
  const first = requests[0];
1326
1400
  const startTime = first.startedAt;
1327
1401
  const endTime = Math.max(
@@ -1340,7 +1414,7 @@ function buildFlow(rawRequests) {
1340
1414
  startTime,
1341
1415
  totalDurationMs: Math.round(endTime - startTime),
1342
1416
  hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
1343
- warnings: detectWarnings(rawRequests),
1417
+ warnings: collectRequestWarnings(rawRequests),
1344
1418
  sourcePage,
1345
1419
  redundancyPct
1346
1420
  };
@@ -1352,20 +1426,20 @@ function getDominantSourcePage(requests) {
1352
1426
  counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
1353
1427
  }
1354
1428
  }
1355
- let best = "";
1356
- let bestCount = 0;
1429
+ let mostCommonPage = "";
1430
+ let highestCount = 0;
1357
1431
  for (const [page, count] of counts) {
1358
- if (count > bestCount) {
1359
- best = page;
1360
- bestCount = count;
1432
+ if (count > highestCount) {
1433
+ mostCommonPage = page;
1434
+ highestCount = count;
1361
1435
  }
1362
1436
  }
1363
- return best || requests[0]?.path?.split("?")[0] || "/";
1437
+ return mostCommonPage || (requests[0]?.path ? stripQueryString(requests[0].path) : "") || "/";
1364
1438
  }
1365
1439
  function deriveFlowLabel(requests, sourcePage) {
1366
1440
  const trigger = requests.find((r) => r.category === "api-call") ?? requests.find((r) => r.category === "server-action") ?? requests.find((r) => r.category === "page-load") ?? requests.find((r) => r.category === "navigation") ?? requests.find((r) => r.category === "data-fetch") ?? requests[0];
1367
1441
  if (trigger.category === "page-load" || trigger.category === "navigation") {
1368
- const pageName = prettifyPageName(trigger.path.split("?")[0]);
1442
+ const pageName = prettifyPageName(stripQueryString(trigger.path));
1369
1443
  return `${pageName} Page`;
1370
1444
  }
1371
1445
  if (trigger.category === "api-call") {
@@ -1390,67 +1464,23 @@ function deriveFlowLabel(requests, sourcePage) {
1390
1464
  return trigger.label;
1391
1465
  }
1392
1466
 
1393
- // src/utils/collections.ts
1394
- function groupBy(items, keyFn) {
1395
- const map = /* @__PURE__ */ new Map();
1396
- for (const item of items) {
1397
- const key = keyFn(item);
1398
- if (key == null) continue;
1399
- let arr = map.get(key);
1400
- if (!arr) {
1401
- arr = [];
1402
- map.set(key, arr);
1403
- }
1404
- arr.push(item);
1405
- }
1406
- return map;
1407
- }
1408
-
1409
- // src/utils/endpoint.ts
1410
- var DYNAMIC_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\d+$|^[0-9a-f]{12,}$|^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/i;
1411
- function normalizePath(path) {
1412
- const qIdx = path.indexOf("?");
1413
- const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
1414
- return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
1415
- }
1416
- function getEndpointKey(method, path) {
1417
- return `${method} ${normalizePath(path)}`;
1418
- }
1419
- var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1420
- function extractEndpointFromDesc(desc) {
1421
- return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1422
- }
1423
-
1424
1467
  // src/instrument/adapters/normalize.ts
1468
+ var VALID_OPS = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPDATE", "DELETE"]);
1469
+ var TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
1425
1470
  function normalizeSQL(sql) {
1426
1471
  if (!sql) return { op: "OTHER", table: "" };
1427
1472
  const trimmed = sql.trim();
1428
- const op = trimmed.split(/\s+/)[0].toUpperCase();
1429
- if (/SELECT\s+COUNT/i.test(trimmed)) {
1430
- const match = trimmed.match(/FROM\s+"?\w+"?\."?(\w+)"?/i);
1431
- return { op: "SELECT", table: match?.[1] ?? "" };
1432
- }
1433
- const tableMatch = trimmed.match(/(?:FROM|INTO|UPDATE)\s+"?\w+"?\."?(\w+)"?/i);
1434
- const table = tableMatch?.[1] ?? "";
1435
- switch (op) {
1436
- case "SELECT":
1437
- return { op: "SELECT", table };
1438
- case "INSERT":
1439
- return { op: "INSERT", table };
1440
- case "UPDATE":
1441
- return { op: "UPDATE", table };
1442
- case "DELETE":
1443
- return { op: "DELETE", table };
1444
- default:
1445
- return { op: "OTHER", table };
1446
- }
1447
- }
1473
+ const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
1474
+ const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
1475
+ const table = trimmed.match(TABLE_RE)?.[1] ?? "";
1476
+ return { op, table };
1477
+ }
1478
+ var SQL_PARAM_MARKER = /\$\d+/g;
1479
+ var SQL_STRING_LITERAL = /'[^']*'/g;
1480
+ var SQL_NUMBER_LITERAL = /\b\d+(\.\d+)?\b/g;
1448
1481
  function normalizeQueryParams(sql) {
1449
1482
  if (!sql) return null;
1450
- let n = sql.replace(/'[^']*'/g, "?");
1451
- n = n.replace(/\b\d+(\.\d+)?\b/g, "?");
1452
- n = n.replace(/\$\d+/g, "?");
1453
- return n;
1483
+ return sql.replace(SQL_PARAM_MARKER, "?").replace(SQL_STRING_LITERAL, "?").replace(SQL_NUMBER_LITERAL, "?");
1454
1484
  }
1455
1485
 
1456
1486
  // src/analysis/insights/query-helpers.ts
@@ -1467,7 +1497,7 @@ function getQueryInfo(q) {
1467
1497
  }
1468
1498
 
1469
1499
  // src/analysis/insights/prepare.ts
1470
- function createEndpointGroup() {
1500
+ function emptyEndpointGroup() {
1471
1501
  return {
1472
1502
  total: 0,
1473
1503
  errors: 0,
@@ -1479,16 +1509,12 @@ function createEndpointGroup() {
1479
1509
  queryShapeDurations: /* @__PURE__ */ new Map()
1480
1510
  };
1481
1511
  }
1482
- function windowByEndpoint(requests) {
1512
+ function keepRecentPerEndpoint(requests) {
1483
1513
  const byEndpoint = /* @__PURE__ */ new Map();
1484
- for (const r of requests) {
1485
- const ep = getEndpointKey(r.method, r.path);
1486
- let list = byEndpoint.get(ep);
1487
- if (!list) {
1488
- list = [];
1489
- byEndpoint.set(ep, list);
1490
- }
1491
- list.push(r);
1514
+ for (const request of requests) {
1515
+ const endpointKey = getEndpointKey(request.method, request.path);
1516
+ const list = getOrCreate(byEndpoint, endpointKey, () => []);
1517
+ list.push(request);
1492
1518
  }
1493
1519
  const windowed = [];
1494
1520
  for (const [, reqs] of byEndpoint) {
@@ -1496,54 +1522,67 @@ function windowByEndpoint(requests) {
1496
1522
  }
1497
1523
  return windowed;
1498
1524
  }
1525
+ function filterUserRequests(requests) {
1526
+ return requests.filter(
1527
+ (request) => !request.isStatic && !request.isHealthCheck && (!request.path || !request.path.startsWith(DASHBOARD_PREFIX))
1528
+ );
1529
+ }
1499
1530
  function extractActiveEndpoints(requests) {
1500
1531
  const endpoints = /* @__PURE__ */ new Set();
1501
- for (const r of requests) {
1502
- if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
1503
- endpoints.add(getEndpointKey(r.method, r.path));
1504
- }
1532
+ for (const request of filterUserRequests(requests)) {
1533
+ endpoints.add(getEndpointKey(request.method, request.path));
1505
1534
  }
1506
1535
  return endpoints;
1507
1536
  }
1508
- function prepareContext(ctx) {
1509
- const nonStatic = ctx.requests.filter(
1510
- (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
1511
- );
1512
- const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
1513
- const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
1514
- const reqById = new Map(nonStatic.map((r) => [r.id, r]));
1515
- const recent = windowByEndpoint(nonStatic);
1537
+ function aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq) {
1516
1538
  const endpointGroups = /* @__PURE__ */ new Map();
1517
- for (const r of recent) {
1518
- const ep = getEndpointKey(r.method, r.path);
1519
- let g = endpointGroups.get(ep);
1520
- if (!g) {
1521
- g = createEndpointGroup();
1522
- endpointGroups.set(ep, g);
1523
- }
1524
- g.total++;
1525
- if (isErrorStatus(r.statusCode)) g.errors++;
1526
- g.totalDuration += r.durationMs;
1527
- g.totalSize += r.responseSize ?? 0;
1528
- const reqQueries = queriesByReq.get(r.id) ?? [];
1529
- g.queryCount += reqQueries.length;
1530
- for (const q of reqQueries) {
1531
- g.totalQueryTimeMs += q.durationMs;
1532
- const shape = getQueryShape(q);
1533
- const info = getQueryInfo(q);
1534
- let sd = g.queryShapeDurations.get(shape);
1535
- if (!sd) {
1536
- sd = { totalMs: 0, count: 0, label: info.op + (info.table ? ` ${info.table}` : "") };
1537
- g.queryShapeDurations.set(shape, sd);
1538
- }
1539
- sd.totalMs += q.durationMs;
1540
- sd.count++;
1541
- }
1542
- const reqFetches = fetchesByReq.get(r.id) ?? [];
1543
- for (const f of reqFetches) {
1544
- g.totalFetchTimeMs += f.durationMs;
1545
- }
1546
- }
1539
+ for (const request of recent) {
1540
+ const endpointKey = getEndpointKey(request.method, request.path);
1541
+ const group = getOrCreate(endpointGroups, endpointKey, emptyEndpointGroup);
1542
+ group.total++;
1543
+ if (isErrorStatus(request.statusCode)) group.errors++;
1544
+ group.totalDuration += request.durationMs;
1545
+ group.totalSize += request.responseSize ?? 0;
1546
+ const reqQueries = queriesByReq.get(request.id) ?? [];
1547
+ group.queryCount += reqQueries.length;
1548
+ for (const query of reqQueries) {
1549
+ group.totalQueryTimeMs += query.durationMs;
1550
+ const shape = getQueryShape(query);
1551
+ const info = getQueryInfo(query);
1552
+ const shapeDuration = getOrCreate(group.queryShapeDurations, shape, () => ({
1553
+ totalMs: 0,
1554
+ count: 0,
1555
+ label: info.op + (info.table ? ` ${info.table}` : "")
1556
+ }));
1557
+ shapeDuration.totalMs += query.durationMs;
1558
+ shapeDuration.count++;
1559
+ }
1560
+ const reqFetches = fetchesByReq.get(request.id) ?? [];
1561
+ for (const fetch of reqFetches) {
1562
+ group.totalFetchTimeMs += fetch.durationMs;
1563
+ }
1564
+ }
1565
+ return endpointGroups;
1566
+ }
1567
+ function collectStrictModeDupeIds(ctx) {
1568
+ const ids = /* @__PURE__ */ new Set();
1569
+ for (const flow of ctx.flows) {
1570
+ for (const req of flow.requests) {
1571
+ if (req.isStrictModeDupe) ids.add(req.id);
1572
+ }
1573
+ }
1574
+ return ids;
1575
+ }
1576
+ function buildInsightContext(ctx) {
1577
+ const strictModeDupeIds = collectStrictModeDupeIds(ctx);
1578
+ const nonStatic = filterUserRequests(ctx.requests).filter((req) => !strictModeDupeIds.has(req.id));
1579
+ const filteredQueries = strictModeDupeIds.size > 0 ? ctx.queries.filter((q) => !q.parentRequestId || !strictModeDupeIds.has(q.parentRequestId)) : ctx.queries;
1580
+ const filteredFetches = strictModeDupeIds.size > 0 ? ctx.fetches.filter((f) => !f.parentRequestId || !strictModeDupeIds.has(f.parentRequestId)) : ctx.fetches;
1581
+ const queriesByReq = groupBy(filteredQueries, (query) => query.parentRequestId);
1582
+ const fetchesByReq = groupBy(filteredFetches, (fetch) => fetch.parentRequestId);
1583
+ const reqById = new Map(nonStatic.map((request) => [request.id, request]));
1584
+ const recent = keepRecentPerEndpoint(nonStatic);
1585
+ const endpointGroups = aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq);
1547
1586
  return {
1548
1587
  ...ctx,
1549
1588
  nonStatic,
@@ -1557,17 +1596,20 @@ function prepareContext(ctx) {
1557
1596
  // src/analysis/insights/runner.ts
1558
1597
  var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
1559
1598
  var InsightRunner = class {
1560
- rules = [];
1599
+ constructor() {
1600
+ this.rules = [];
1601
+ }
1561
1602
  register(rule) {
1562
1603
  this.rules.push(rule);
1563
1604
  }
1564
1605
  run(ctx) {
1565
- const prepared = prepareContext(ctx);
1606
+ const prepared = buildInsightContext(ctx);
1566
1607
  const insights = [];
1567
1608
  for (const rule of this.rules) {
1568
1609
  try {
1569
1610
  insights.push(...rule.check(prepared));
1570
- } catch {
1611
+ } catch (e) {
1612
+ brakitDebug(`insight rule ${rule.id} failed: ${getErrorMessage(e)}`);
1571
1613
  }
1572
1614
  }
1573
1615
  insights.sort(
@@ -1577,7 +1619,7 @@ var InsightRunner = class {
1577
1619
  }
1578
1620
  };
1579
1621
 
1580
- // src/analysis/insights/rules/n1.ts
1622
+ // src/analysis/insights/rules/query-rules.ts
1581
1623
  var n1Rule = {
1582
1624
  id: "n1",
1583
1625
  check(ctx) {
@@ -1588,15 +1630,15 @@ var n1Rule = {
1588
1630
  if (!req) continue;
1589
1631
  const endpoint = getEndpointKey(req.method, req.path);
1590
1632
  const shapeGroups = /* @__PURE__ */ new Map();
1591
- for (const q of reqQueries) {
1592
- const shape = getQueryShape(q);
1633
+ for (const query of reqQueries) {
1634
+ const shape = getQueryShape(query);
1593
1635
  let group = shapeGroups.get(shape);
1594
1636
  if (!group) {
1595
- group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
1637
+ group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: query };
1596
1638
  shapeGroups.set(shape, group);
1597
1639
  }
1598
1640
  group.count++;
1599
- group.distinctSql.add(q.sql ?? shape);
1641
+ group.distinctSql.add(query.sql ?? shape);
1600
1642
  }
1601
1643
  for (const [, sg] of shapeGroups) {
1602
1644
  if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
@@ -1617,57 +1659,6 @@ var n1Rule = {
1617
1659
  return insights;
1618
1660
  }
1619
1661
  };
1620
-
1621
- // src/analysis/insights/rules/cross-endpoint.ts
1622
- var crossEndpointRule = {
1623
- id: "cross-endpoint",
1624
- check(ctx) {
1625
- const insights = [];
1626
- const queryMap = /* @__PURE__ */ new Map();
1627
- const allEndpoints = /* @__PURE__ */ new Set();
1628
- for (const [reqId, reqQueries] of ctx.queriesByReq) {
1629
- const req = ctx.reqById.get(reqId);
1630
- if (!req) continue;
1631
- const endpoint = getEndpointKey(req.method, req.path);
1632
- allEndpoints.add(endpoint);
1633
- const seenInReq = /* @__PURE__ */ new Set();
1634
- for (const q of reqQueries) {
1635
- const shape = getQueryShape(q);
1636
- let entry = queryMap.get(shape);
1637
- if (!entry) {
1638
- entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
1639
- queryMap.set(shape, entry);
1640
- }
1641
- entry.count++;
1642
- if (!seenInReq.has(shape)) {
1643
- seenInReq.add(shape);
1644
- entry.endpoints.add(endpoint);
1645
- }
1646
- }
1647
- }
1648
- if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
1649
- for (const [, cem] of queryMap) {
1650
- if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
1651
- if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
1652
- const p = Math.round(cem.endpoints.size / allEndpoints.size * 100);
1653
- if (p < CROSS_ENDPOINT_PCT) continue;
1654
- const info = getQueryInfo(cem.first);
1655
- const label = info.op + (info.table ? ` ${info.table}` : "");
1656
- insights.push({
1657
- severity: "warning",
1658
- type: "cross-endpoint",
1659
- title: "Repeated Query Across Endpoints",
1660
- desc: `${label} runs on ${cem.endpoints.size} of ${allEndpoints.size} endpoints (${p}%).`,
1661
- hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
1662
- nav: "queries"
1663
- });
1664
- }
1665
- }
1666
- return insights;
1667
- }
1668
- };
1669
-
1670
- // src/analysis/insights/rules/redundant-query.ts
1671
1662
  var redundantQueryRule = {
1672
1663
  id: "redundant-query",
1673
1664
  check(ctx) {
@@ -1678,27 +1669,27 @@ var redundantQueryRule = {
1678
1669
  if (!req) continue;
1679
1670
  const endpoint = getEndpointKey(req.method, req.path);
1680
1671
  const exact = /* @__PURE__ */ new Map();
1681
- for (const q of reqQueries) {
1682
- if (!q.sql) continue;
1683
- let entry = exact.get(q.sql);
1672
+ for (const query of reqQueries) {
1673
+ if (!query.sql) continue;
1674
+ let entry = exact.get(query.sql);
1684
1675
  if (!entry) {
1685
- entry = { count: 0, first: q };
1686
- exact.set(q.sql, entry);
1676
+ entry = { count: 0, first: query };
1677
+ exact.set(query.sql, entry);
1687
1678
  }
1688
1679
  entry.count++;
1689
1680
  }
1690
- for (const [, e] of exact) {
1691
- if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
1692
- const info = getQueryInfo(e.first);
1681
+ for (const [, entry] of exact) {
1682
+ if (entry.count < REDUNDANT_QUERY_MIN_COUNT) continue;
1683
+ const info = getQueryInfo(entry.first);
1693
1684
  const label = info.op + (info.table ? ` ${info.table}` : "");
1694
- const dedupKey = `${endpoint}:${label}`;
1695
- if (seen.has(dedupKey)) continue;
1696
- seen.add(dedupKey);
1685
+ const deduplicationKey = `${endpoint}:${label}`;
1686
+ if (seen.has(deduplicationKey)) continue;
1687
+ seen.add(deduplicationKey);
1697
1688
  insights.push({
1698
1689
  severity: "warning",
1699
1690
  type: "redundant-query",
1700
1691
  title: "Redundant Query",
1701
- desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
1692
+ desc: `${label} runs ${entry.count}x with identical params in ${endpoint}.`,
1702
1693
  hint: "The exact same query with identical parameters runs multiple times in one request. Cache the first result or lift the query to a shared function.",
1703
1694
  nav: "queries"
1704
1695
  });
@@ -1707,179 +1698,16 @@ var redundantQueryRule = {
1707
1698
  return insights;
1708
1699
  }
1709
1700
  };
1710
-
1711
- // src/analysis/insights/rules/error.ts
1712
- var errorRule = {
1713
- id: "error",
1714
- check(ctx) {
1715
- if (ctx.errors.length === 0) return [];
1716
- const insights = [];
1717
- const groups = /* @__PURE__ */ new Map();
1718
- for (const e of ctx.errors) {
1719
- const name = e.name || "Error";
1720
- groups.set(name, (groups.get(name) ?? 0) + 1);
1721
- }
1722
- for (const [name, cnt] of groups) {
1723
- insights.push({
1724
- severity: "critical",
1725
- type: "error",
1726
- title: "Unhandled Error",
1727
- desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
1728
- hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
1729
- nav: "errors"
1730
- });
1731
- }
1732
- return insights;
1733
- }
1734
- };
1735
-
1736
- // src/analysis/insights/rules/error-hotspot.ts
1737
- var errorHotspotRule = {
1738
- id: "error-hotspot",
1739
- check(ctx) {
1740
- const insights = [];
1741
- for (const [ep, g] of ctx.endpointGroups) {
1742
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1743
- const errorRate = Math.round(g.errors / g.total * 100);
1744
- if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
1745
- insights.push({
1746
- severity: "critical",
1747
- type: "error-hotspot",
1748
- title: "Error Hotspot",
1749
- desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
1750
- hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
1751
- nav: "requests"
1752
- });
1753
- }
1754
- }
1755
- return insights;
1756
- }
1757
- };
1758
-
1759
- // src/analysis/insights/rules/duplicate.ts
1760
- var duplicateRule = {
1761
- id: "duplicate",
1762
- check(ctx) {
1763
- const dupCounts = /* @__PURE__ */ new Map();
1764
- const flowCount = /* @__PURE__ */ new Map();
1765
- for (const flow of ctx.flows) {
1766
- if (!flow.requests) continue;
1767
- const seenInFlow = /* @__PURE__ */ new Set();
1768
- for (const fr of flow.requests) {
1769
- if (!fr.isDuplicate) continue;
1770
- const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
1771
- dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
1772
- if (!seenInFlow.has(dupKey)) {
1773
- seenInFlow.add(dupKey);
1774
- flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
1775
- }
1776
- }
1777
- }
1778
- const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
1779
- const insights = [];
1780
- for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
1781
- const d = dupEntries[i];
1782
- insights.push({
1783
- severity: "warning",
1784
- type: "duplicate",
1785
- title: "Duplicate API Call",
1786
- desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
1787
- hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR.",
1788
- nav: "actions"
1789
- });
1790
- }
1791
- return insights;
1792
- }
1793
- };
1794
-
1795
- // src/utils/format.ts
1796
- function formatDuration(ms) {
1797
- if (ms < 1e3) return `${ms}ms`;
1798
- return `${(ms / 1e3).toFixed(1)}s`;
1799
- }
1800
- function formatSize(bytes) {
1801
- if (bytes < 1024) return `${bytes}B`;
1802
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1803
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1804
- }
1805
- function pct(part, total) {
1806
- return total > 0 ? Math.round(part / total * 100) : 0;
1807
- }
1808
-
1809
- // src/analysis/insights/rules/slow.ts
1810
- var slowRule = {
1811
- id: "slow",
1812
- check(ctx) {
1813
- const insights = [];
1814
- for (const [ep, g] of ctx.endpointGroups) {
1815
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1816
- const avgMs = Math.round(g.totalDuration / g.total);
1817
- if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
1818
- const avgQueryMs = Math.round(g.totalQueryTimeMs / g.total);
1819
- const avgFetchMs = Math.round(g.totalFetchTimeMs / g.total);
1820
- const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
1821
- const parts = [];
1822
- if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
1823
- if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
1824
- if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
1825
- const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
1826
- let detail;
1827
- let slowestMs = 0;
1828
- for (const [, sd] of g.queryShapeDurations) {
1829
- const avgShapeMs = sd.totalMs / sd.count;
1830
- if (avgShapeMs > slowestMs) {
1831
- slowestMs = avgShapeMs;
1832
- detail = `Slowest query: ${sd.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${sd.count}x)`;
1833
- }
1834
- }
1835
- insights.push({
1836
- severity: "warning",
1837
- type: "slow",
1838
- title: "Slow Endpoint",
1839
- desc: `${ep} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
1840
- hint: avgQueryMs >= avgFetchMs && avgQueryMs >= avgAppMs ? "Most time is in database queries. Check the Queries tab for slow or redundant queries." : avgFetchMs >= avgQueryMs && avgFetchMs >= avgAppMs ? "Most time is in outbound HTTP calls. Check if upstream services are slow or if calls can be parallelized." : "Most time is in application code. Profile the handler for CPU-heavy operations or blocking calls.",
1841
- detail,
1842
- nav: "requests"
1843
- });
1844
- }
1845
- return insights;
1846
- }
1847
- };
1848
-
1849
- // src/analysis/insights/rules/query-heavy.ts
1850
- var queryHeavyRule = {
1851
- id: "query-heavy",
1852
- check(ctx) {
1853
- const insights = [];
1854
- for (const [ep, g] of ctx.endpointGroups) {
1855
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1856
- const avgQueries = Math.round(g.queryCount / g.total);
1857
- if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
1858
- insights.push({
1859
- severity: "warning",
1860
- type: "query-heavy",
1861
- title: "Query-Heavy Endpoint",
1862
- desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
1863
- hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
1864
- nav: "queries"
1865
- });
1866
- }
1867
- }
1868
- return insights;
1869
- }
1870
- };
1871
-
1872
- // src/analysis/insights/rules/select-star.ts
1873
1701
  var selectStarRule = {
1874
1702
  id: "select-star",
1875
1703
  check(ctx) {
1876
1704
  const seen = /* @__PURE__ */ new Map();
1877
1705
  for (const [, reqQueries] of ctx.queriesByReq) {
1878
- for (const q of reqQueries) {
1879
- if (!q.sql) continue;
1880
- const isSelectStar = SELECT_STAR_RE.test(q.sql.trim()) || SELECT_DOT_STAR_RE.test(q.sql);
1706
+ for (const query of reqQueries) {
1707
+ if (!query.sql) continue;
1708
+ const isSelectStar = SELECT_STAR_RE.test(query.sql.trim()) || SELECT_DOT_STAR_RE.test(query.sql);
1881
1709
  if (!isSelectStar) continue;
1882
- const info = getQueryInfo(q);
1710
+ const info = getQueryInfo(query);
1883
1711
  const key = info.table || "unknown";
1884
1712
  seen.set(key, (seen.get(key) ?? 0) + 1);
1885
1713
  }
@@ -1899,16 +1727,14 @@ var selectStarRule = {
1899
1727
  return insights;
1900
1728
  }
1901
1729
  };
1902
-
1903
- // src/analysis/insights/rules/high-rows.ts
1904
1730
  var highRowsRule = {
1905
1731
  id: "high-rows",
1906
1732
  check(ctx) {
1907
1733
  const seen = /* @__PURE__ */ new Map();
1908
1734
  for (const [, reqQueries] of ctx.queriesByReq) {
1909
- for (const q of reqQueries) {
1910
- if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
1911
- const info = getQueryInfo(q);
1735
+ for (const query of reqQueries) {
1736
+ if (!query.rowCount || query.rowCount <= HIGH_ROW_COUNT) continue;
1737
+ const info = getQueryInfo(query);
1912
1738
  const key = `${info.op} ${info.table || "unknown"}`;
1913
1739
  let entry = seen.get(key);
1914
1740
  if (!entry) {
@@ -1916,7 +1742,7 @@ var highRowsRule = {
1916
1742
  seen.set(key, entry);
1917
1743
  }
1918
1744
  entry.count++;
1919
- if (q.rowCount > entry.max) entry.max = q.rowCount;
1745
+ if (query.rowCount > entry.max) entry.max = query.rowCount;
1920
1746
  }
1921
1747
  }
1922
1748
  const insights = [];
@@ -1934,21 +1760,57 @@ var highRowsRule = {
1934
1760
  return insights;
1935
1761
  }
1936
1762
  };
1763
+ var queryHeavyRule = {
1764
+ id: "query-heavy",
1765
+ check(ctx) {
1766
+ const insights = [];
1767
+ for (const [endpointKey, group] of ctx.endpointGroups) {
1768
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1769
+ const avgQueries = Math.round(group.queryCount / group.total);
1770
+ if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
1771
+ insights.push({
1772
+ severity: "warning",
1773
+ type: "query-heavy",
1774
+ title: "Query-Heavy Endpoint",
1775
+ desc: `${endpointKey} \u2014 avg ${avgQueries} queries/request`,
1776
+ hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
1777
+ nav: "queries"
1778
+ });
1779
+ }
1780
+ }
1781
+ return insights;
1782
+ }
1783
+ };
1784
+
1785
+ // src/utils/format.ts
1786
+ function formatDuration(ms) {
1787
+ if (ms < 1e3) return `${ms}ms`;
1788
+ return `${(ms / 1e3).toFixed(1)}s`;
1789
+ }
1790
+ function formatSize(bytes) {
1791
+ if (bytes < 1024) return `${bytes}B`;
1792
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1793
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1794
+ }
1795
+ function pct(part, total) {
1796
+ return total > 0 ? Math.round(part / total * 100) : 0;
1797
+ }
1937
1798
 
1938
- // src/analysis/insights/rules/response-overfetch.ts
1799
+ // src/analysis/insights/rules/response-rules.ts
1939
1800
  var responseOverfetchRule = {
1940
1801
  id: "response-overfetch",
1941
1802
  check(ctx) {
1942
1803
  const insights = [];
1943
1804
  const seen = /* @__PURE__ */ new Set();
1944
- for (const r of ctx.nonStatic) {
1945
- if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
1946
- const ep = getEndpointKey(r.method, r.path);
1947
- if (seen.has(ep)) continue;
1805
+ for (const request of ctx.nonStatic) {
1806
+ if (isErrorStatus(request.statusCode) || !request.responseBody) continue;
1807
+ const endpointKey = getEndpointKey(request.method, request.path);
1808
+ if (seen.has(endpointKey)) continue;
1948
1809
  let parsed;
1949
1810
  try {
1950
- parsed = JSON.parse(r.responseBody);
1951
- } catch {
1811
+ parsed = JSON.parse(request.responseBody);
1812
+ } catch (e) {
1813
+ brakitDebug(`json parse: ${getErrorMessage(e)}`);
1952
1814
  continue;
1953
1815
  }
1954
1816
  const target = unwrapResponse(parsed);
@@ -1971,12 +1833,12 @@ var responseOverfetchRule = {
1971
1833
  reasons.push(`${fields.length} fields returned`);
1972
1834
  }
1973
1835
  if (reasons.length > 0) {
1974
- seen.add(ep);
1836
+ seen.add(endpointKey);
1975
1837
  insights.push({
1976
1838
  severity: "info",
1977
1839
  type: "response-overfetch",
1978
1840
  title: "Response Overfetch",
1979
- desc: `${ep} \u2014 ${reasons.join(", ")}`,
1841
+ desc: `${endpointKey} \u2014 ${reasons.join(", ")}`,
1980
1842
  hint: "This response returns more data than the client likely needs. Use a DTO or select only required fields to reduce payload size and avoid leaking internal structure.",
1981
1843
  nav: "requests"
1982
1844
  });
@@ -1985,21 +1847,19 @@ var responseOverfetchRule = {
1985
1847
  return insights;
1986
1848
  }
1987
1849
  };
1988
-
1989
- // src/analysis/insights/rules/large-response.ts
1990
1850
  var largeResponseRule = {
1991
1851
  id: "large-response",
1992
1852
  check(ctx) {
1993
1853
  const insights = [];
1994
- for (const [ep, g] of ctx.endpointGroups) {
1995
- if (g.total < OVERFETCH_MIN_REQUESTS) continue;
1996
- const avgSize = Math.round(g.totalSize / g.total);
1854
+ for (const [endpointKey, group] of ctx.endpointGroups) {
1855
+ if (group.total < OVERFETCH_MIN_REQUESTS) continue;
1856
+ const avgSize = Math.round(group.totalSize / group.total);
1997
1857
  if (avgSize > LARGE_RESPONSE_BYTES) {
1998
1858
  insights.push({
1999
1859
  severity: "info",
2000
1860
  type: "large-response",
2001
1861
  title: "Large Response",
2002
- desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
1862
+ desc: `${endpointKey} \u2014 avg ${formatSize(avgSize)} response`,
2003
1863
  hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
2004
1864
  nav: "requests"
2005
1865
  });
@@ -2009,7 +1869,51 @@ var largeResponseRule = {
2009
1869
  }
2010
1870
  };
2011
1871
 
2012
- // src/analysis/insights/rules/regression.ts
1872
+ // src/analysis/insights/rules/reliability-rules.ts
1873
+ var errorRule = {
1874
+ id: "error",
1875
+ check(ctx) {
1876
+ if (ctx.errors.length === 0) return [];
1877
+ const insights = [];
1878
+ const groups = /* @__PURE__ */ new Map();
1879
+ for (const error of ctx.errors) {
1880
+ const name = error.name || "Error";
1881
+ groups.set(name, (groups.get(name) ?? 0) + 1);
1882
+ }
1883
+ for (const [name, cnt] of groups) {
1884
+ insights.push({
1885
+ severity: "critical",
1886
+ type: "error",
1887
+ title: "Unhandled Error",
1888
+ desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
1889
+ hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
1890
+ nav: "errors"
1891
+ });
1892
+ }
1893
+ return insights;
1894
+ }
1895
+ };
1896
+ var errorHotspotRule = {
1897
+ id: "error-hotspot",
1898
+ check(ctx) {
1899
+ const insights = [];
1900
+ for (const [endpointKey, group] of ctx.endpointGroups) {
1901
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1902
+ const errorRate = Math.round(group.errors / group.total * 100);
1903
+ if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
1904
+ insights.push({
1905
+ severity: "critical",
1906
+ type: "error-hotspot",
1907
+ title: "Error Hotspot",
1908
+ desc: `${endpointKey} \u2014 ${errorRate}% error rate (${group.errors}/${group.total} requests)`,
1909
+ hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
1910
+ nav: "requests"
1911
+ });
1912
+ }
1913
+ }
1914
+ return insights;
1915
+ }
1916
+ };
2013
1917
  var regressionRule = {
2014
1918
  id: "regression",
2015
1919
  check(ctx) {
@@ -2046,6 +1950,127 @@ var regressionRule = {
2046
1950
  return insights;
2047
1951
  }
2048
1952
  };
1953
+ var slowRule = {
1954
+ id: "slow",
1955
+ check(ctx) {
1956
+ const insights = [];
1957
+ for (const [endpointKey, group] of ctx.endpointGroups) {
1958
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1959
+ const avgMs = Math.round(group.totalDuration / group.total);
1960
+ if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
1961
+ const avgQueryMs = Math.round(group.totalQueryTimeMs / group.total);
1962
+ const avgFetchMs = Math.round(group.totalFetchTimeMs / group.total);
1963
+ const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
1964
+ const parts = [];
1965
+ if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
1966
+ if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
1967
+ if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
1968
+ const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
1969
+ let detail;
1970
+ let slowestMs = 0;
1971
+ for (const [, shapeDuration] of group.queryShapeDurations) {
1972
+ const avgShapeMs = shapeDuration.totalMs / shapeDuration.count;
1973
+ if (avgShapeMs > slowestMs) {
1974
+ slowestMs = avgShapeMs;
1975
+ detail = `Slowest query: ${shapeDuration.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${shapeDuration.count}x)`;
1976
+ }
1977
+ }
1978
+ insights.push({
1979
+ severity: "warning",
1980
+ type: "slow",
1981
+ title: "Slow Endpoint",
1982
+ desc: `${endpointKey} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
1983
+ hint: avgQueryMs >= avgFetchMs && avgQueryMs >= avgAppMs ? "Most time is in database queries. Check the Queries tab for slow or redundant queries." : avgFetchMs >= avgQueryMs && avgFetchMs >= avgAppMs ? "Most time is in outbound HTTP calls. Check if upstream services are slow or if calls can be parallelized." : "Most time is in application code. Profile the handler for CPU-heavy operations or blocking calls.",
1984
+ detail,
1985
+ nav: "requests"
1986
+ });
1987
+ }
1988
+ return insights;
1989
+ }
1990
+ };
1991
+
1992
+ // src/analysis/insights/rules/pattern-rules.ts
1993
+ var duplicateRule = {
1994
+ id: "duplicate",
1995
+ check(ctx) {
1996
+ const dupCounts = /* @__PURE__ */ new Map();
1997
+ const flowCount = /* @__PURE__ */ new Map();
1998
+ for (const flow of ctx.flows) {
1999
+ if (!flow.requests) continue;
2000
+ const seenInFlow = /* @__PURE__ */ new Set();
2001
+ for (const request of flow.requests) {
2002
+ if (!request.isDuplicate) continue;
2003
+ const deduplicationKey = `${request.method} ${request.label ?? request.path ?? request.url}`;
2004
+ dupCounts.set(deduplicationKey, (dupCounts.get(deduplicationKey) ?? 0) + 1);
2005
+ if (!seenInFlow.has(deduplicationKey)) {
2006
+ seenInFlow.add(deduplicationKey);
2007
+ flowCount.set(deduplicationKey, (flowCount.get(deduplicationKey) ?? 0) + 1);
2008
+ }
2009
+ }
2010
+ }
2011
+ const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
2012
+ const insights = [];
2013
+ for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
2014
+ const duplicate = dupEntries[i];
2015
+ insights.push({
2016
+ severity: "warning",
2017
+ type: "duplicate",
2018
+ title: "Duplicate API Call",
2019
+ desc: `${duplicate.key} loaded ${duplicate.count}x as duplicate across ${duplicate.flows} action${duplicate.flows !== 1 ? "s" : ""}`,
2020
+ hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR.",
2021
+ nav: "actions"
2022
+ });
2023
+ }
2024
+ return insights;
2025
+ }
2026
+ };
2027
+ var crossEndpointRule = {
2028
+ id: "cross-endpoint",
2029
+ check(ctx) {
2030
+ const insights = [];
2031
+ const queryMap = /* @__PURE__ */ new Map();
2032
+ const allEndpoints = /* @__PURE__ */ new Set();
2033
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
2034
+ const req = ctx.reqById.get(reqId);
2035
+ if (!req) continue;
2036
+ const endpoint = getEndpointKey(req.method, req.path);
2037
+ allEndpoints.add(endpoint);
2038
+ const seenInReq = /* @__PURE__ */ new Set();
2039
+ for (const query of reqQueries) {
2040
+ const shape = getQueryShape(query);
2041
+ let entry = queryMap.get(shape);
2042
+ if (!entry) {
2043
+ entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
2044
+ queryMap.set(shape, entry);
2045
+ }
2046
+ entry.count++;
2047
+ if (!seenInReq.has(shape)) {
2048
+ seenInReq.add(shape);
2049
+ entry.endpoints.add(endpoint);
2050
+ }
2051
+ }
2052
+ }
2053
+ if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
2054
+ for (const [, queryMetric] of queryMap) {
2055
+ if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
2056
+ if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
2057
+ const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
2058
+ if (coveragePct < CROSS_ENDPOINT_PCT) continue;
2059
+ const info = getQueryInfo(queryMetric.first);
2060
+ const label = info.op + (info.table ? ` ${info.table}` : "");
2061
+ insights.push({
2062
+ severity: "warning",
2063
+ type: "cross-endpoint",
2064
+ title: "Repeated Query Across Endpoints",
2065
+ desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
2066
+ hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
2067
+ nav: "queries"
2068
+ });
2069
+ }
2070
+ }
2071
+ return insights;
2072
+ }
2073
+ };
2049
2074
 
2050
2075
  // src/analysis/insights/rules/security.ts
2051
2076
  var securityRule = {
@@ -2120,18 +2145,17 @@ function securityFindingToIssue(finding) {
2120
2145
 
2121
2146
  // src/analysis/engine.ts
2122
2147
  var AnalysisEngine = class {
2123
- constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
2124
- this.registry = registry;
2148
+ constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
2149
+ this.services = services;
2125
2150
  this.debounceMs = debounceMs;
2151
+ this.cachedInsights = [];
2152
+ this.cachedFindings = [];
2153
+ this.debounceTimer = null;
2154
+ this.subs = new SubscriptionBag();
2126
2155
  this.scanner = createDefaultScanner();
2127
2156
  }
2128
- scanner;
2129
- cachedInsights = [];
2130
- cachedFindings = [];
2131
- debounceTimer = null;
2132
- subs = new SubscriptionBag();
2133
2157
  start() {
2134
- const bus = this.registry.get("event-bus");
2158
+ const bus = this.services.bus;
2135
2159
  this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
2136
2160
  this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
2137
2161
  this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
@@ -2158,12 +2182,12 @@ var AnalysisEngine = class {
2158
2182
  }, this.debounceMs);
2159
2183
  }
2160
2184
  recompute() {
2161
- const allRequests = this.registry.get("request-store").getAll();
2162
- const queries = this.registry.get("query-store").getAll();
2163
- const errors = this.registry.get("error-store").getAll();
2164
- const logs = this.registry.get("log-store").getAll();
2165
- const fetches = this.registry.get("fetch-store").getAll();
2166
- const requests = windowByEndpoint(allRequests);
2185
+ const allRequests = this.services.requestStore.getAll();
2186
+ const queries = this.services.queryStore.getAll();
2187
+ const errors = this.services.errorStore.getAll();
2188
+ const logs = this.services.logStore.getAll();
2189
+ const fetches = this.services.fetchStore.getAll();
2190
+ const requests = keepRecentPerEndpoint(allRequests);
2167
2191
  const flows = groupRequestsIntoFlows(requests);
2168
2192
  this.cachedFindings = this.scanner.scan({ requests, logs });
2169
2193
  this.cachedInsights = computeInsights({
@@ -2172,38 +2196,34 @@ var AnalysisEngine = class {
2172
2196
  errors,
2173
2197
  flows,
2174
2198
  fetches,
2175
- previousMetrics: this.registry.get("metrics-store").getAll(),
2199
+ previousMetrics: this.services.metricsStore.getAll(),
2176
2200
  securityFindings: this.cachedFindings
2177
2201
  });
2178
- if (this.registry.has("issue-store")) {
2179
- const issueStore = this.registry.get("issue-store");
2180
- for (const finding of this.cachedFindings) {
2181
- issueStore.upsert(securityFindingToIssue(finding), "passive");
2182
- }
2183
- for (const insight of this.cachedInsights) {
2184
- issueStore.upsert(insightToIssue(insight), "passive");
2185
- }
2186
- const currentIssueIds = /* @__PURE__ */ new Set();
2187
- for (const finding of this.cachedFindings) {
2188
- currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
2189
- }
2190
- for (const insight of this.cachedInsights) {
2191
- currentIssueIds.add(computeIssueId(insightToIssue(insight)));
2192
- }
2193
- const activeEndpoints = extractActiveEndpoints(allRequests);
2194
- issueStore.reconcile(currentIssueIds, activeEndpoints);
2195
- const update = {
2196
- insights: this.cachedInsights,
2197
- findings: this.cachedFindings,
2198
- issues: issueStore.getAll()
2199
- };
2200
- this.registry.get("event-bus").emit("analysis:updated", update);
2201
- }
2202
+ const issueStore = this.services.issueStore;
2203
+ const currentIssueIds = /* @__PURE__ */ new Set();
2204
+ for (const finding of this.cachedFindings) {
2205
+ const issue = securityFindingToIssue(finding);
2206
+ issueStore.upsert(issue, "passive");
2207
+ currentIssueIds.add(computeIssueId(issue));
2208
+ }
2209
+ for (const insight of this.cachedInsights) {
2210
+ const issue = insightToIssue(insight);
2211
+ issueStore.upsert(issue, "passive");
2212
+ currentIssueIds.add(computeIssueId(issue));
2213
+ }
2214
+ const activeEndpoints = extractActiveEndpoints(allRequests);
2215
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
2216
+ const update = {
2217
+ insights: this.cachedInsights,
2218
+ findings: this.cachedFindings,
2219
+ issues: issueStore.getAll()
2220
+ };
2221
+ this.services.bus.emit("analysis:updated", update);
2202
2222
  }
2203
2223
  };
2204
2224
 
2205
2225
  // src/index.ts
2206
- var VERSION = "0.8.6";
2226
+ var VERSION = "9.0.0";
2207
2227
  export {
2208
2228
  AdapterRegistry,
2209
2229
  AnalysisEngine,