brakit 0.8.7 → 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.
@@ -14,48 +14,10 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
- // src/constants/routes.ts
18
- 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;
19
- var init_routes = __esm({
20
- "src/constants/routes.ts"() {
21
- "use strict";
22
- DASHBOARD_PREFIX = "/__brakit";
23
- DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
24
- DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
25
- DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
26
- DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
27
- DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
28
- DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
29
- DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
30
- DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
31
- DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
32
- DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
33
- DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
34
- DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
35
- DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
36
- DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
37
- DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
38
- DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
39
- DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
40
- VALID_TABS_TUPLE = [
41
- "overview",
42
- "actions",
43
- "requests",
44
- "fetches",
45
- "queries",
46
- "errors",
47
- "logs",
48
- "performance",
49
- "security"
50
- ];
51
- VALID_TABS = new Set(VALID_TABS_TUPLE);
52
- }
53
- });
54
-
55
- // src/constants/limits.ts
56
- var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS;
57
- var init_limits = __esm({
58
- "src/constants/limits.ts"() {
17
+ // src/constants/config.ts
18
+ var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS, FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS, STRICT_MODE_MAX_GAP_MS, METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS, SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS, VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES;
19
+ var init_config = __esm({
20
+ "src/constants/config.ts"() {
59
21
  "use strict";
60
22
  MAX_REQUEST_ENTRIES = 1e3;
61
23
  DEFAULT_MAX_BODY_CAPTURE = 10240;
@@ -82,14 +44,6 @@ var init_limits = __esm({
82
44
  MAX_UNIQUE_ENDPOINTS = 500;
83
45
  MAX_ACCUMULATOR_ENTRIES = 1e3;
84
46
  ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
85
- }
86
- });
87
-
88
- // src/constants/thresholds.ts
89
- var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS;
90
- var init_thresholds = __esm({
91
- "src/constants/thresholds.ts"() {
92
- "use strict";
93
47
  FLOW_GAP_MS = 5e3;
94
48
  SLOW_REQUEST_THRESHOLD_MS = 2e3;
95
49
  MIN_POLLING_SEQUENCE = 3;
@@ -119,25 +73,7 @@ var init_thresholds = __esm({
119
73
  INSIGHT_WINDOW_PER_ENDPOINT = 20;
120
74
  CLEAN_HITS_FOR_RESOLUTION = 5;
121
75
  STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
122
- }
123
- });
124
-
125
- // src/constants/transport.ts
126
- var SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS;
127
- var init_transport = __esm({
128
- "src/constants/transport.ts"() {
129
- "use strict";
130
- SSE_HEARTBEAT_INTERVAL_MS = 3e4;
131
- NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
132
- NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
133
- }
134
- });
135
-
136
- // src/constants/metrics.ts
137
- var METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS;
138
- var init_metrics = __esm({
139
- "src/constants/metrics.ts"() {
140
- "use strict";
76
+ STRICT_MODE_MAX_GAP_MS = 2e3;
141
77
  METRICS_DIR = ".brakit";
142
78
  METRICS_FILE = "metrics.json";
143
79
  PORT_FILE = ".brakit/port";
@@ -146,14 +82,50 @@ var init_metrics = __esm({
146
82
  METRICS_MAX_SESSIONS = 50;
147
83
  METRICS_MAX_DATA_POINTS = 200;
148
84
  ISSUES_FLUSH_INTERVAL_MS = 1e4;
85
+ SSE_HEARTBEAT_INTERVAL_MS = 3e4;
86
+ NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
87
+ NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
88
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
89
+ VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
90
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
149
91
  }
150
92
  });
151
93
 
152
- // src/constants/headers.ts
153
- var BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES;
154
- var init_headers = __esm({
155
- "src/constants/headers.ts"() {
94
+ // src/constants/labels.ts
95
+ 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, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
96
+ var init_labels = __esm({
97
+ "src/constants/labels.ts"() {
156
98
  "use strict";
99
+ DASHBOARD_PREFIX = "/__brakit";
100
+ DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
101
+ DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
102
+ DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
103
+ DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
104
+ DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
105
+ DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
106
+ DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
107
+ DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
108
+ DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
109
+ DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
110
+ DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
111
+ DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
112
+ DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
113
+ DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
114
+ DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
115
+ DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
116
+ DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
117
+ VALID_TABS_TUPLE = [
118
+ "overview",
119
+ "actions",
120
+ "requests",
121
+ "fetches",
122
+ "queries",
123
+ "errors",
124
+ "logs",
125
+ "performance",
126
+ "security"
127
+ ];
128
+ VALID_TABS = new Set(VALID_TABS_TUPLE);
157
129
  BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
158
130
  BRAKIT_FETCH_ID_HEADER = "x-brakit-fetch-id";
159
131
  SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
@@ -164,13 +136,53 @@ var init_headers = __esm({
164
136
  "x-api-key",
165
137
  "x-auth-token"
166
138
  ]);
139
+ HTTP_OK = 200;
140
+ HTTP_NO_CONTENT = 204;
141
+ HTTP_BAD_REQUEST = 400;
142
+ HTTP_NOT_FOUND = 404;
143
+ HTTP_METHOD_NOT_ALLOWED = 405;
144
+ HTTP_PAYLOAD_TOO_LARGE = 413;
145
+ HTTP_INTERNAL_ERROR = 500;
146
+ SECURITY_HEADERS = {
147
+ "x-content-type-options": "nosniff",
148
+ "x-frame-options": "DENY",
149
+ "referrer-policy": "no-referrer",
150
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
151
+ };
152
+ CONTENT_ENCODING_GZIP = "gzip";
153
+ CONTENT_ENCODING_BR = "br";
154
+ CONTENT_ENCODING_DEFLATE = "deflate";
155
+ SEVERITY_ICON = {
156
+ critical: "\u2717",
157
+ warning: "\u26A0",
158
+ info: "\u2139"
159
+ };
160
+ SSE_EVENT_FETCH = "fetch";
161
+ SSE_EVENT_LOG = "log";
162
+ SSE_EVENT_ERROR = "error_event";
163
+ SSE_EVENT_QUERY = "query";
164
+ SSE_EVENT_ISSUES = "issues";
165
+ SDK_EVENT_REQUEST = "request";
166
+ SDK_EVENT_DB_QUERY = "db.query";
167
+ SDK_EVENT_FETCH = "fetch";
168
+ SDK_EVENT_LOG = "log";
169
+ SDK_EVENT_ERROR = "error";
170
+ SDK_EVENT_AUTH_CHECK = "auth.check";
171
+ POSTHOG_HOST = "https://us.i.posthog.com";
172
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/";
173
+ POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
174
+ SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
175
+ TIMELINE_FETCH = "fetch";
176
+ TIMELINE_LOG = "log";
177
+ TIMELINE_ERROR = "error";
178
+ TIMELINE_QUERY = "query";
167
179
  }
168
180
  });
169
181
 
170
- // src/constants/network.ts
182
+ // src/constants/features.ts
171
183
  var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE, DIR_MODE_OWNER_ONLY, FILE_MODE_OWNER_ONLY;
172
- var init_network = __esm({
173
- "src/constants/network.ts"() {
184
+ var init_features = __esm({
185
+ "src/constants/features.ts"() {
174
186
  "use strict";
175
187
  CLOUD_SIGNALS = [
176
188
  "VERCEL",
@@ -203,112 +215,13 @@ var init_network = __esm({
203
215
  }
204
216
  });
205
217
 
206
- // src/constants/mcp.ts
207
- var init_mcp = __esm({
208
- "src/constants/mcp.ts"() {
209
- "use strict";
210
- }
211
- });
212
-
213
- // src/constants/encoding.ts
214
- var CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE;
215
- var init_encoding = __esm({
216
- "src/constants/encoding.ts"() {
217
- "use strict";
218
- CONTENT_ENCODING_GZIP = "gzip";
219
- CONTENT_ENCODING_BR = "br";
220
- CONTENT_ENCODING_DEFLATE = "deflate";
221
- }
222
- });
223
-
224
- // src/constants/severity.ts
225
- var SEVERITY_ICON;
226
- var init_severity = __esm({
227
- "src/constants/severity.ts"() {
228
- "use strict";
229
- SEVERITY_ICON = {
230
- critical: "\u2717",
231
- warning: "\u26A0",
232
- info: "\u2139"
233
- };
234
- }
235
- });
236
-
237
- // src/constants/telemetry.ts
238
- var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS;
239
- var init_telemetry = __esm({
240
- "src/constants/telemetry.ts"() {
241
- "use strict";
242
- POSTHOG_HOST = "https://us.i.posthog.com";
243
- POSTHOG_CAPTURE_PATH = "/i/v0/e/";
244
- POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
245
- SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
246
- }
247
- });
248
-
249
- // src/constants/lifecycle.ts
250
- var VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES;
251
- var init_lifecycle = __esm({
252
- "src/constants/lifecycle.ts"() {
253
- "use strict";
254
- VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
255
- VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
256
- VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
257
- }
258
- });
259
-
260
- // src/constants/cli.ts
261
- var init_cli = __esm({
262
- "src/constants/cli.ts"() {
263
- "use strict";
264
- }
265
- });
266
-
267
- // src/constants/timeline.ts
268
- var TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
269
- var init_timeline = __esm({
270
- "src/constants/timeline.ts"() {
271
- "use strict";
272
- TIMELINE_FETCH = "fetch";
273
- TIMELINE_LOG = "log";
274
- TIMELINE_ERROR = "error";
275
- TIMELINE_QUERY = "query";
276
- }
277
- });
278
-
279
- // src/constants/sdk-events.ts
280
- var SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK;
281
- var init_sdk_events = __esm({
282
- "src/constants/sdk-events.ts"() {
283
- "use strict";
284
- SDK_EVENT_REQUEST = "request";
285
- SDK_EVENT_DB_QUERY = "db.query";
286
- SDK_EVENT_FETCH = "fetch";
287
- SDK_EVENT_LOG = "log";
288
- SDK_EVENT_ERROR = "error";
289
- SDK_EVENT_AUTH_CHECK = "auth.check";
290
- }
291
- });
292
-
293
218
  // src/constants/index.ts
294
219
  var init_constants = __esm({
295
220
  "src/constants/index.ts"() {
296
221
  "use strict";
297
- init_routes();
298
- init_limits();
299
- init_thresholds();
300
- init_transport();
301
- init_metrics();
302
- init_headers();
303
- init_network();
304
- init_mcp();
305
- init_encoding();
306
- init_severity();
307
- init_telemetry();
308
- init_lifecycle();
309
- init_cli();
310
- init_timeline();
311
- init_sdk_events();
222
+ init_config();
223
+ init_labels();
224
+ init_features();
312
225
  }
313
226
  });
314
227
 
@@ -377,7 +290,7 @@ function setupFetchHook(emit) {
377
290
  statusCode: msg.response.statusCode ?? 0,
378
291
  durationMs: Math.round(performance.now() - info.startTime),
379
292
  parentRequestId: info.parentRequestId,
380
- timestamp: Date.now()
293
+ timestamp: performance.now()
381
294
  }
382
295
  });
383
296
  });
@@ -407,7 +320,7 @@ function setupConsoleHook(emit) {
407
320
  const ctx = getRequestContext();
408
321
  if (!ctx) return;
409
322
  const message = format(...args);
410
- const timestamp = Date.now();
323
+ const timestamp = performance.now();
411
324
  const parentRequestId = ctx.requestId;
412
325
  if (level === "error") {
413
326
  const errorArg = args.find((a) => a instanceof Error);
@@ -474,7 +387,7 @@ function createCaptureError(emit) {
474
387
  message: error.message,
475
388
  stack: error.stack ?? "",
476
389
  parentRequestId: ctx?.requestId ?? null,
477
- timestamp: Date.now()
390
+ timestamp: performance.now()
478
391
  }
479
392
  });
480
393
  };
@@ -538,65 +451,28 @@ var init_adapter_registry = __esm({
538
451
  }
539
452
  });
540
453
 
541
- // src/instrument/adapters/shared.ts
542
- import { createRequire } from "module";
543
- function tryRequire(id) {
544
- try {
545
- return appRequire(id);
546
- } catch {
547
- return null;
548
- }
549
- }
550
- function captureRequestId() {
551
- return getRequestContext()?.requestId ?? null;
552
- }
553
- var appRequire;
554
- var init_shared = __esm({
555
- "src/instrument/adapters/shared.ts"() {
556
- "use strict";
557
- init_context();
558
- appRequire = createRequire(process.cwd() + "/index.js");
559
- }
560
- });
561
-
562
454
  // src/instrument/adapters/normalize.ts
563
455
  function normalizeSQL(sql) {
564
456
  if (!sql) return { op: "OTHER", table: "" };
565
457
  const trimmed = sql.trim();
566
- const op = trimmed.split(/\s+/)[0].toUpperCase();
567
- if (/SELECT\s+COUNT/i.test(trimmed)) {
568
- const match = trimmed.match(/FROM\s+"?\w+"?\."?(\w+)"?/i);
569
- return { op: "SELECT", table: match?.[1] ?? "" };
570
- }
571
- const tableMatch = trimmed.match(/(?:FROM|INTO|UPDATE)\s+"?\w+"?\."?(\w+)"?/i);
572
- const table = tableMatch?.[1] ?? "";
573
- switch (op) {
574
- case "SELECT":
575
- return { op: "SELECT", table };
576
- case "INSERT":
577
- return { op: "INSERT", table };
578
- case "UPDATE":
579
- return { op: "UPDATE", table };
580
- case "DELETE":
581
- return { op: "DELETE", table };
582
- default:
583
- return { op: "OTHER", table };
584
- }
458
+ const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
459
+ const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
460
+ const table = trimmed.match(TABLE_RE)?.[1] ?? "";
461
+ return { op, table };
585
462
  }
586
463
  function normalizePrismaOp(operation) {
587
464
  return PRISMA_OP_MAP[operation] ?? "OTHER";
588
465
  }
589
466
  function normalizeQueryParams(sql) {
590
467
  if (!sql) return null;
591
- let n = sql.replace(/'[^']*'/g, "?");
592
- n = n.replace(/\b\d+(\.\d+)?\b/g, "?");
593
- n = n.replace(/\$\d+/g, "?");
594
- return n;
468
+ return sql.replace(SQL_PARAM_MARKER, "?").replace(SQL_STRING_LITERAL, "?").replace(SQL_NUMBER_LITERAL, "?");
595
469
  }
596
- var PRISMA_OP_MAP;
470
+ var VALID_OPS, TABLE_RE, PRISMA_OP_MAP, SQL_PARAM_MARKER, SQL_STRING_LITERAL, SQL_NUMBER_LITERAL;
597
471
  var init_normalize = __esm({
598
472
  "src/instrument/adapters/normalize.ts"() {
599
473
  "use strict";
474
+ VALID_OPS = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPDATE", "DELETE"]);
475
+ TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
600
476
  PRISMA_OP_MAP = {
601
477
  findUnique: "SELECT",
602
478
  findUniqueOrThrow: "SELECT",
@@ -615,79 +491,199 @@ var init_normalize = __esm({
615
491
  delete: "DELETE",
616
492
  deleteMany: "DELETE"
617
493
  };
494
+ SQL_PARAM_MARKER = /\$\d+/g;
495
+ SQL_STRING_LITERAL = /'[^']*'/g;
496
+ SQL_NUMBER_LITERAL = /\b\d+(\.\d+)?\b/g;
618
497
  }
619
498
  });
620
499
 
621
- // src/instrument/adapters/pg.ts
622
- var origQuery, proto, pgAdapter;
623
- var init_pg = __esm({
624
- "src/instrument/adapters/pg.ts"() {
500
+ // src/utils/type-guards.ts
501
+ function isString(val) {
502
+ return typeof val === "string";
503
+ }
504
+ function isNumber(val) {
505
+ return typeof val === "number" && !isNaN(val);
506
+ }
507
+ function isBoolean(val) {
508
+ return typeof val === "boolean";
509
+ }
510
+ function isThenable(value) {
511
+ return value != null && typeof value.then === "function";
512
+ }
513
+ function getErrorMessage(err) {
514
+ if (err instanceof Error) return err.message;
515
+ if (typeof err === "string") return err;
516
+ return String(err);
517
+ }
518
+ function isValidIssueState(val) {
519
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
520
+ }
521
+ function isValidIssueCategory(val) {
522
+ return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
523
+ }
524
+ function isValidAiFixStatus(val) {
525
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
526
+ }
527
+ function validateIssuesData(parsed) {
528
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
529
+ const obj = parsed;
530
+ if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
531
+ return parsed;
532
+ }
533
+ return null;
534
+ }
535
+ function validateMetricsData(parsed) {
536
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
537
+ const obj = parsed;
538
+ if (obj.version === 1 && Array.isArray(obj.endpoints)) {
539
+ return parsed;
540
+ }
541
+ return null;
542
+ }
543
+ var init_type_guards = __esm({
544
+ "src/utils/type-guards.ts"() {
625
545
  "use strict";
626
- init_shared();
546
+ init_config();
547
+ }
548
+ });
549
+
550
+ // src/utils/log.ts
551
+ function brakitWarn(message) {
552
+ process.stderr.write(`${PREFIX} ${message}
553
+ `);
554
+ }
555
+ function brakitDebug(message) {
556
+ if (process.env.DEBUG_BRAKIT) {
557
+ process.stderr.write(`${PREFIX}:debug ${message}
558
+ `);
559
+ }
560
+ }
561
+ var PREFIX;
562
+ var init_log = __esm({
563
+ "src/utils/log.ts"() {
564
+ "use strict";
565
+ PREFIX = "[brakit]";
566
+ }
567
+ });
568
+
569
+ // src/instrument/adapters/shared.ts
570
+ import { createRequire } from "module";
571
+ function tryRequire(id) {
572
+ try {
573
+ return appRequire(id);
574
+ } catch {
575
+ return null;
576
+ }
577
+ }
578
+ function getActiveRequestId() {
579
+ return getRequestContext()?.requestId ?? null;
580
+ }
581
+ function getPrototype(lib, className) {
582
+ const defaultExport = lib.default;
583
+ const cls = defaultExport?.[className] ?? lib[className];
584
+ if (!cls || typeof cls !== "function") return null;
585
+ return cls.prototype ?? null;
586
+ }
587
+ function buildQueryEvent(config, sql, op, table, start, requestId, rowCount) {
588
+ return {
589
+ type: "query",
590
+ data: {
591
+ driver: config.driver,
592
+ source: config.driver,
593
+ sql,
594
+ normalizedOp: op,
595
+ table,
596
+ durationMs: Math.round(performance.now() - start),
597
+ rowCount: rowCount ?? void 0,
598
+ parentRequestId: requestId,
599
+ timestamp: performance.now()
600
+ }
601
+ };
602
+ }
603
+ function wrapQueryMethod(original, emit, config) {
604
+ return function(...args) {
605
+ const sql = config.extractSql(args);
606
+ const start = performance.now();
607
+ const requestId = getActiveRequestId();
608
+ const { op, table } = normalizeSQL(sql ?? "");
609
+ const emitQuery = (result2) => {
610
+ const rowCount = config.extractRowCount?.(result2);
611
+ emit(buildQueryEvent(config, sql, op, table, start, requestId, rowCount));
612
+ };
613
+ const lastIdx = args.length - 1;
614
+ if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
615
+ const originalCallback = args[lastIdx];
616
+ args[lastIdx] = function(...callbackArgs) {
617
+ emitQuery(callbackArgs[1]);
618
+ return originalCallback.apply(this, callbackArgs);
619
+ };
620
+ return original.apply(this, args);
621
+ }
622
+ const result = original.apply(this, args);
623
+ if (isThenable(result)) {
624
+ return result.then((res) => {
625
+ try {
626
+ emitQuery(res);
627
+ } catch (e) {
628
+ brakitDebug(`query telemetry: ${getErrorMessage(e)}`);
629
+ }
630
+ return res;
631
+ });
632
+ }
633
+ if (config.supportsEventEmitter && result && typeof result.on === "function") {
634
+ result.on(
635
+ "end",
636
+ (res) => emitQuery(res)
637
+ );
638
+ return result;
639
+ }
640
+ return result;
641
+ };
642
+ }
643
+ var appRequire;
644
+ var init_shared = __esm({
645
+ "src/instrument/adapters/shared.ts"() {
646
+ "use strict";
647
+ init_context();
627
648
  init_normalize();
649
+ init_type_guards();
650
+ init_log();
651
+ appRequire = createRequire(process.cwd() + "/index.js");
652
+ }
653
+ });
654
+
655
+ // src/instrument/adapters/pg.ts
656
+ var origQuery, proto, pgConfig, pgAdapter;
657
+ var init_pg = __esm({
658
+ "src/instrument/adapters/pg.ts"() {
659
+ "use strict";
660
+ init_shared();
628
661
  origQuery = null;
629
662
  proto = null;
663
+ pgConfig = {
664
+ driver: "pg",
665
+ extractSql: (args) => {
666
+ const q = args[0];
667
+ if (typeof q === "string") return q;
668
+ if (typeof q === "object" && q !== null && "text" in q) return q.text;
669
+ return void 0;
670
+ },
671
+ extractRowCount: (result) => result?.rowCount,
672
+ supportsEventEmitter: true
673
+ };
630
674
  pgAdapter = {
631
675
  name: "pg",
632
676
  detect() {
633
677
  return tryRequire("pg") !== null;
634
678
  },
679
+ /** Monkeypatches pg's Client prototype to intercept database queries and emit telemetry events. */
635
680
  patch(emit) {
636
681
  const pg = tryRequire("pg");
637
682
  if (!pg) return;
638
- const Client = pg.default?.Client ?? pg.Client;
639
- if (!Client || typeof Client !== "function") return;
640
- proto = Client.prototype ?? null;
683
+ proto = getPrototype(pg, "Client");
641
684
  if (!proto?.query) return;
642
685
  origQuery = proto.query;
643
- const saved = origQuery;
644
- proto.query = function(...args) {
645
- const first = args[0];
646
- const sql = typeof first === "string" ? first : typeof first === "object" && first !== null && "text" in first ? first.text : void 0;
647
- const start = performance.now();
648
- const requestId = captureRequestId();
649
- const { op, table } = normalizeSQL(sql ?? "");
650
- const emitQuery = (rowCount) => {
651
- emit({
652
- type: "query",
653
- data: {
654
- driver: "pg",
655
- source: "pg",
656
- sql,
657
- normalizedOp: op,
658
- table,
659
- durationMs: Math.round(performance.now() - start),
660
- rowCount: rowCount ?? void 0,
661
- parentRequestId: requestId,
662
- timestamp: Date.now()
663
- }
664
- });
665
- };
666
- const lastIdx = args.length - 1;
667
- if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
668
- const origCb = args[lastIdx];
669
- args[lastIdx] = function(err, res) {
670
- emitQuery(res?.rowCount ?? void 0);
671
- return origCb.call(this, err, res);
672
- };
673
- return saved.apply(this, args);
674
- }
675
- const result = saved.apply(this, args);
676
- if (result && typeof result.then === "function") {
677
- return result.then((res) => {
678
- try {
679
- emitQuery(res?.rowCount ?? void 0);
680
- } catch {
681
- }
682
- return res;
683
- });
684
- }
685
- if (result && typeof result.on === "function") {
686
- result.on("end", (res) => emitQuery(res?.rowCount ?? void 0));
687
- return result;
688
- }
689
- return result;
690
- };
686
+ proto.query = wrapQueryMethod(origQuery, emit, pgConfig);
691
687
  },
692
688
  unpatch() {
693
689
  if (proto && origQuery) {
@@ -701,70 +697,34 @@ var init_pg = __esm({
701
697
  });
702
698
 
703
699
  // src/instrument/adapters/mysql2.ts
704
- var originals2, proto2, mysql2Adapter;
700
+ var PATCHED_METHODS, originals2, proto2, mysql2Config, mysql2Adapter;
705
701
  var init_mysql2 = __esm({
706
702
  "src/instrument/adapters/mysql2.ts"() {
707
703
  "use strict";
708
704
  init_shared();
709
- init_normalize();
705
+ PATCHED_METHODS = ["query", "execute"];
710
706
  originals2 = /* @__PURE__ */ new Map();
711
707
  proto2 = null;
708
+ mysql2Config = {
709
+ driver: "mysql2",
710
+ extractSql: (args) => typeof args[0] === "string" ? args[0] : void 0
711
+ };
712
712
  mysql2Adapter = {
713
713
  name: "mysql2",
714
714
  detect() {
715
715
  return tryRequire("mysql2") !== null;
716
716
  },
717
+ /** Monkeypatches mysql2's Connection prototype to intercept database queries and emit telemetry events. */
717
718
  patch(emit) {
718
719
  const mysql2 = tryRequire("mysql2");
719
720
  if (!mysql2) return;
720
- proto2 = mysql2.Connection?.prototype ?? null;
721
+ proto2 = getPrototype(mysql2, "Connection");
721
722
  if (!proto2) return;
722
- for (const method of ["query", "execute"]) {
723
+ for (const method of PATCHED_METHODS) {
723
724
  const orig = proto2[method];
724
725
  if (typeof orig !== "function") continue;
725
726
  originals2.set(method, orig);
726
- proto2[method] = function(...args) {
727
- const first = args[0];
728
- const sql = typeof first === "string" ? first : void 0;
729
- const start = performance.now();
730
- const requestId = captureRequestId();
731
- const { op, table } = normalizeSQL(sql ?? "");
732
- const emitQuery = () => {
733
- emit({
734
- type: "query",
735
- data: {
736
- driver: "mysql2",
737
- source: "mysql2",
738
- sql,
739
- normalizedOp: op,
740
- table,
741
- durationMs: Math.round(performance.now() - start),
742
- parentRequestId: requestId,
743
- timestamp: Date.now()
744
- }
745
- });
746
- };
747
- const lastIdx = args.length - 1;
748
- if (lastIdx >= 0 && typeof args[lastIdx] === "function") {
749
- const origCb = args[lastIdx];
750
- args[lastIdx] = function() {
751
- emitQuery();
752
- return origCb.apply(this, arguments);
753
- };
754
- return orig.apply(this, args);
755
- }
756
- const result = orig.apply(this, args);
757
- if (result && typeof result.then === "function") {
758
- return result.then((res) => {
759
- try {
760
- emitQuery();
761
- } catch {
762
- }
763
- return res;
764
- });
765
- }
766
- return result;
767
- };
727
+ proto2[method] = wrapQueryMethod(orig, emit, mysql2Config);
768
728
  }
769
729
  },
770
730
  unpatch() {
@@ -797,9 +757,8 @@ var init_prisma = __esm({
797
757
  patch(emit) {
798
758
  const prismaModule = tryRequire("@prisma/client");
799
759
  if (!prismaModule) return;
800
- const PrismaClient = prismaModule.default?.PrismaClient ?? prismaModule.PrismaClient;
801
- if (!PrismaClient || typeof PrismaClient !== "function") return;
802
- prismaProto = PrismaClient.prototype;
760
+ prismaProto = getPrototype(prismaModule, "PrismaClient");
761
+ if (!prismaProto) return;
803
762
  origConnect = prismaProto.$connect;
804
763
  if (typeof origConnect !== "function") return;
805
764
  const saved = origConnect;
@@ -815,7 +774,7 @@ var init_prisma = __esm({
815
774
  args: opArgs,
816
775
  query
817
776
  }) {
818
- const requestId = captureRequestId();
777
+ const requestId = getActiveRequestId();
819
778
  const start = performance.now();
820
779
  const result = await query(opArgs);
821
780
  emit({
@@ -829,7 +788,7 @@ var init_prisma = __esm({
829
788
  table: model,
830
789
  durationMs: Math.round(performance.now() - start),
831
790
  parentRequestId: requestId,
832
- timestamp: Date.now()
791
+ timestamp: performance.now()
833
792
  }
834
793
  });
835
794
  return result;
@@ -874,24 +833,35 @@ var init_adapters = __esm({
874
833
  }
875
834
  });
876
835
 
877
- // src/constants/http.ts
878
- var HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS;
879
- var init_http = __esm({
880
- "src/constants/http.ts"() {
836
+ // src/utils/endpoint.ts
837
+ function isDynamicSegment(segment) {
838
+ return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
839
+ }
840
+ function normalizePath(path) {
841
+ const qIdx = path.indexOf("?");
842
+ const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
843
+ return pathname.split("/").map((seg) => seg && isDynamicSegment(seg) ? DYNAMIC_SEGMENT_PLACEHOLDER : seg).join("/");
844
+ }
845
+ function getEndpointKey(method, path) {
846
+ return `${method} ${normalizePath(path)}`;
847
+ }
848
+ function extractEndpointFromDesc(desc) {
849
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
850
+ }
851
+ function stripQueryString(path) {
852
+ const i = path.indexOf("?");
853
+ return i === -1 ? path : path.slice(0, i);
854
+ }
855
+ var UUID_RE, NUMERIC_ID_RE, HEX_HASH_RE, ALPHA_TOKEN_RE, DYNAMIC_SEGMENT_PLACEHOLDER, ENDPOINT_PREFIX_RE;
856
+ var init_endpoint = __esm({
857
+ "src/utils/endpoint.ts"() {
881
858
  "use strict";
882
- HTTP_OK = 200;
883
- HTTP_NO_CONTENT = 204;
884
- HTTP_BAD_REQUEST = 400;
885
- HTTP_NOT_FOUND = 404;
886
- HTTP_METHOD_NOT_ALLOWED = 405;
887
- HTTP_PAYLOAD_TOO_LARGE = 413;
888
- HTTP_INTERNAL_ERROR = 500;
889
- SECURITY_HEADERS = {
890
- "x-content-type-options": "nosniff",
891
- "x-frame-options": "DENY",
892
- "referrer-policy": "no-referrer",
893
- "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
894
- };
859
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
860
+ NUMERIC_ID_RE = /^\d+$/;
861
+ HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
862
+ ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
863
+ DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
864
+ ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
895
865
  }
896
866
  });
897
867
 
@@ -915,6 +885,7 @@ var init_http_status = __esm({
915
885
  function detectCategory(req) {
916
886
  const { method, url, statusCode, responseHeaders } = req;
917
887
  if (req.isStatic) return "static";
888
+ if (req.isHealthCheck) return "health-check";
918
889
  if (statusCode === 307 && (url.includes("__clerk_handshake") || url.includes("__clerk_db_jwt"))) {
919
890
  return "auth-handshake";
920
891
  }
@@ -1079,20 +1050,41 @@ var init_label = __esm({
1079
1050
  });
1080
1051
 
1081
1052
  // src/analysis/transforms.ts
1082
- function markDuplicates(requests) {
1053
+ function isDuplicateCandidate(req) {
1054
+ return DUPLICATE_CATEGORIES.has(req.category);
1055
+ }
1056
+ function buildRequestKey(req) {
1057
+ return `${req.method} ${stripQueryString(getEffectivePath(req))}`;
1058
+ }
1059
+ function isStrictModePattern(requests, counts) {
1060
+ if (counts.size === 0 || ![...counts.values()].every((c) => c === 2)) {
1061
+ return false;
1062
+ }
1063
+ const firstByKey = /* @__PURE__ */ new Map();
1064
+ for (const req of requests) {
1065
+ if (!isDuplicateCandidate(req)) continue;
1066
+ const key = buildRequestKey(req);
1067
+ const first = firstByKey.get(key);
1068
+ if (!first) {
1069
+ firstByKey.set(key, req);
1070
+ } else if (Math.abs(req.startedAt - first.startedAt) > STRICT_MODE_MAX_GAP_MS) {
1071
+ return false;
1072
+ }
1073
+ }
1074
+ return true;
1075
+ }
1076
+ function flagDuplicateRequests(requests) {
1083
1077
  const counts = /* @__PURE__ */ new Map();
1084
1078
  for (const req of requests) {
1085
- if (req.category !== "data-fetch" && req.category !== "auth-check")
1086
- continue;
1087
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
1079
+ if (!isDuplicateCandidate(req)) continue;
1080
+ const key = buildRequestKey(req);
1088
1081
  counts.set(key, (counts.get(key) ?? 0) + 1);
1089
1082
  }
1090
- const isStrictMode = counts.size > 0 && [...counts.values()].every((c) => c === 2);
1083
+ const isStrictMode = isStrictModePattern(requests, counts);
1091
1084
  const seen = /* @__PURE__ */ new Set();
1092
1085
  for (const req of requests) {
1093
- if (req.category !== "data-fetch" && req.category !== "auth-check")
1094
- continue;
1095
- const key = `${req.method} ${getEffectivePath(req).split("?")[0]}`;
1086
+ if (!isDuplicateCandidate(req)) continue;
1087
+ const key = buildRequestKey(req);
1096
1088
  if (seen.has(key)) {
1097
1089
  if (isStrictMode) {
1098
1090
  req.isStrictModeDupe = true;
@@ -1104,20 +1096,20 @@ function markDuplicates(requests) {
1104
1096
  }
1105
1097
  }
1106
1098
  }
1107
- function collapsePolling(requests) {
1099
+ function mergePollingSequences(requests) {
1108
1100
  const result = [];
1109
1101
  let i = 0;
1110
1102
  while (i < requests.length) {
1111
1103
  const current = requests[i];
1112
- const currentEffective = getEffectivePath(current).split("?")[0];
1104
+ const currentEffective = stripQueryString(getEffectivePath(current));
1113
1105
  if (current.method === "GET" && current.category === "data-fetch") {
1114
- let j = i + 1;
1115
- while (j < requests.length && requests[j].method === "GET" && getEffectivePath(requests[j]).split("?")[0] === currentEffective) {
1116
- j++;
1106
+ let nextIndex = i + 1;
1107
+ while (nextIndex < requests.length && requests[nextIndex].method === "GET" && stripQueryString(getEffectivePath(requests[nextIndex])) === currentEffective) {
1108
+ nextIndex++;
1117
1109
  }
1118
- const count = j - i;
1110
+ const count = nextIndex - i;
1119
1111
  if (count >= MIN_POLLING_SEQUENCE) {
1120
- const last = requests[j - 1];
1112
+ const last = requests[nextIndex - 1];
1121
1113
  const pollingDuration = last.startedAt + last.durationMs - current.startedAt;
1122
1114
  const endpointName = prettifyEndpoint(currentEffective);
1123
1115
  result.push({
@@ -1128,7 +1120,7 @@ function collapsePolling(requests) {
1128
1120
  pollingDurationMs: pollingDuration,
1129
1121
  isDuplicate: false
1130
1122
  });
1131
- i = j;
1123
+ i = nextIndex;
1132
1124
  continue;
1133
1125
  }
1134
1126
  }
@@ -1141,18 +1133,18 @@ function formatDurationLabel(ms) {
1141
1133
  if (ms < 1e3) return `${ms}ms`;
1142
1134
  return `${(ms / 1e3).toFixed(1)}s`;
1143
1135
  }
1144
- function detectWarnings(requests) {
1136
+ function collectRequestWarnings(requests) {
1145
1137
  const warnings = [];
1146
1138
  const duplicateCount = requests.filter((r) => r.isDuplicate).length;
1147
1139
  if (duplicateCount > 0) {
1148
1140
  const unique = new Set(
1149
- requests.filter((r) => r.isDuplicate).map((r) => `${r.method} ${getEffectivePath(r).split("?")[0]}`)
1141
+ requests.filter((r) => r.isDuplicate).map((r) => buildRequestKey(r))
1150
1142
  );
1151
1143
  const endpoints = unique.size;
1152
1144
  const sameData = requests.filter((r) => r.isDuplicate).every((r) => {
1153
- const key = `${r.method} ${getEffectivePath(r).split("?")[0]}`;
1145
+ const key = buildRequestKey(r);
1154
1146
  const first = requests.find(
1155
- (o) => !o.isDuplicate && `${o.method} ${getEffectivePath(o).split("?")[0]}` === key
1147
+ (o) => !o.isDuplicate && buildRequestKey(o) === key
1156
1148
  );
1157
1149
  return first && first.responseBody === r.responseBody;
1158
1150
  });
@@ -1173,18 +1165,30 @@ function detectWarnings(requests) {
1173
1165
  }
1174
1166
  return warnings;
1175
1167
  }
1168
+ var DUPLICATE_CATEGORIES;
1176
1169
  var init_transforms = __esm({
1177
1170
  "src/analysis/transforms.ts"() {
1178
1171
  "use strict";
1179
1172
  init_constants();
1173
+ init_config();
1180
1174
  init_categorize();
1181
1175
  init_label();
1182
1176
  init_http_status();
1177
+ init_endpoint();
1178
+ DUPLICATE_CATEGORIES = /* @__PURE__ */ new Set(["data-fetch", "auth-check"]);
1183
1179
  }
1184
1180
  });
1185
1181
 
1186
1182
  // src/analysis/group.ts
1187
1183
  import { randomUUID as randomUUID3 } from "crypto";
1184
+ function shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, startedAt) {
1185
+ if (currentRequests.length === 0) return false;
1186
+ const sourcePage = labeled.sourcePage;
1187
+ const isNewPage = sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1188
+ const isTimeGap = startedAt - lastEndTime > FLOW_GAP_MS;
1189
+ const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1190
+ return isNewPage || isTimeGap || isPageLoad;
1191
+ }
1188
1192
  function groupRequestsIntoFlows(requests) {
1189
1193
  if (requests.length === 0) return [];
1190
1194
  const flows = [];
@@ -1195,17 +1199,12 @@ function groupRequestsIntoFlows(requests) {
1195
1199
  if (req.path.startsWith(DASHBOARD_PREFIX)) continue;
1196
1200
  const labeled = labelRequest(req);
1197
1201
  if (labeled.category === "static") continue;
1198
- const sourcePage = labeled.sourcePage;
1199
- const gap = currentRequests.length > 0 ? req.startedAt - lastEndTime : 0;
1200
- const isNewPage = currentRequests.length > 0 && sourcePage !== void 0 && currentSourcePage !== void 0 && sourcePage !== currentSourcePage;
1201
- const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
1202
- const isTimeGap = currentRequests.length > 0 && gap > FLOW_GAP_MS;
1203
- if (currentRequests.length > 0 && (isNewPage || isTimeGap || isPageLoad)) {
1202
+ if (shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, req.startedAt)) {
1204
1203
  flows.push(buildFlow(currentRequests));
1205
1204
  currentRequests = [];
1206
1205
  }
1207
1206
  currentRequests.push(labeled);
1208
- currentSourcePage = sourcePage ?? currentSourcePage;
1207
+ currentSourcePage = labeled.sourcePage ?? currentSourcePage;
1209
1208
  lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
1210
1209
  }
1211
1210
  if (currentRequests.length > 0) {
@@ -1214,8 +1213,8 @@ function groupRequestsIntoFlows(requests) {
1214
1213
  return flows;
1215
1214
  }
1216
1215
  function buildFlow(rawRequests) {
1217
- markDuplicates(rawRequests);
1218
- const requests = collapsePolling(rawRequests);
1216
+ flagDuplicateRequests(rawRequests);
1217
+ const requests = mergePollingSequences(rawRequests);
1219
1218
  const first = requests[0];
1220
1219
  const startTime = first.startedAt;
1221
1220
  const endTime = Math.max(
@@ -1234,7 +1233,7 @@ function buildFlow(rawRequests) {
1234
1233
  startTime,
1235
1234
  totalDurationMs: Math.round(endTime - startTime),
1236
1235
  hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
1237
- warnings: detectWarnings(rawRequests),
1236
+ warnings: collectRequestWarnings(rawRequests),
1238
1237
  sourcePage,
1239
1238
  redundancyPct
1240
1239
  };
@@ -1246,20 +1245,20 @@ function getDominantSourcePage(requests) {
1246
1245
  counts.set(req.sourcePage, (counts.get(req.sourcePage) ?? 0) + 1);
1247
1246
  }
1248
1247
  }
1249
- let best = "";
1250
- let bestCount = 0;
1248
+ let mostCommonPage = "";
1249
+ let highestCount = 0;
1251
1250
  for (const [page, count] of counts) {
1252
- if (count > bestCount) {
1253
- best = page;
1254
- bestCount = count;
1251
+ if (count > highestCount) {
1252
+ mostCommonPage = page;
1253
+ highestCount = count;
1255
1254
  }
1256
1255
  }
1257
- return best || requests[0]?.path?.split("?")[0] || "/";
1256
+ return mostCommonPage || (requests[0]?.path ? stripQueryString(requests[0].path) : "") || "/";
1258
1257
  }
1259
1258
  function deriveFlowLabel(requests, sourcePage) {
1260
1259
  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];
1261
1260
  if (trigger.category === "page-load" || trigger.category === "navigation") {
1262
- const pageName = prettifyPageName(trigger.path.split("?")[0]);
1261
+ const pageName = prettifyPageName(stripQueryString(trigger.path));
1263
1262
  return `${pageName} Page`;
1264
1263
  }
1265
1264
  if (trigger.category === "api-call") {
@@ -1288,6 +1287,7 @@ var init_group = __esm({
1288
1287
  "use strict";
1289
1288
  init_constants();
1290
1289
  init_http_status();
1290
+ init_endpoint();
1291
1291
  init_label();
1292
1292
  init_categorize();
1293
1293
  init_transforms();
@@ -1385,12 +1385,28 @@ var init_shared2 = __esm({
1385
1385
  "src/dashboard/api/shared.ts"() {
1386
1386
  "use strict";
1387
1387
  init_constants();
1388
- init_limits();
1389
- init_http();
1388
+ init_config();
1389
+ init_labels();
1390
1390
  }
1391
1391
  });
1392
1392
 
1393
1393
  // src/dashboard/api/handlers.ts
1394
+ function filterByStatusRange(requests, statusStr) {
1395
+ if (statusStr.endsWith("xx")) {
1396
+ const prefix = parseInt(statusStr[0], 10);
1397
+ return requests.filter(
1398
+ (r) => Math.floor(r.statusCode / 100) === prefix
1399
+ );
1400
+ }
1401
+ const code = parseInt(statusStr, 10);
1402
+ return requests.filter((r) => r.statusCode === code);
1403
+ }
1404
+ function filterBySearch(requests, searchQuery) {
1405
+ const lower = searchQuery.toLowerCase();
1406
+ return requests.filter(
1407
+ (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1408
+ );
1409
+ }
1394
1410
  function sanitizeRequest(r) {
1395
1411
  return {
1396
1412
  ...r,
@@ -1398,7 +1414,7 @@ function sanitizeRequest(r) {
1398
1414
  responseHeaders: maskSensitiveHeaders(r.responseHeaders)
1399
1415
  };
1400
1416
  }
1401
- function createRequestsHandler(registry) {
1417
+ function createRequestsHandler(services) {
1402
1418
  return (req, res) => {
1403
1419
  if (!requireGet(req, res)) return;
1404
1420
  const url = parseRequestUrl(req);
@@ -1408,26 +1424,15 @@ function createRequestsHandler(registry) {
1408
1424
  const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
1409
1425
  const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
1410
1426
  const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
1411
- let results = [...registry.get("request-store").getAll()].reverse();
1427
+ let results = [...services.requestStore.getAll()].reverse();
1412
1428
  if (method) {
1413
1429
  results = results.filter((r) => r.method === method.toUpperCase());
1414
1430
  }
1415
1431
  if (status) {
1416
- if (status.endsWith("xx")) {
1417
- const prefix = parseInt(status[0], 10);
1418
- results = results.filter(
1419
- (r) => Math.floor(r.statusCode / 100) === prefix
1420
- );
1421
- } else {
1422
- const code = parseInt(status, 10);
1423
- results = results.filter((r) => r.statusCode === code);
1424
- }
1432
+ results = filterByStatusRange(results, status);
1425
1433
  }
1426
1434
  if (search) {
1427
- const lower = search.toLowerCase();
1428
- results = results.filter(
1429
- (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1430
- );
1435
+ results = filterBySearch(results, search);
1431
1436
  }
1432
1437
  const total = results.length;
1433
1438
  results = results.slice(offset, offset + limit);
@@ -1435,96 +1440,84 @@ function createRequestsHandler(registry) {
1435
1440
  sendJson(req, res, HTTP_OK, { total, requests: sanitized });
1436
1441
  };
1437
1442
  }
1438
- function createFlowsHandler(registry) {
1443
+ function createFlowsHandler(services) {
1439
1444
  return (req, res) => {
1440
1445
  if (!requireGet(req, res)) return;
1441
- const flows = groupRequestsIntoFlows(registry.get("request-store").getAll()).reverse().map((flow) => ({
1446
+ const flows = groupRequestsIntoFlows(services.requestStore.getAll()).reverse().map((flow) => ({
1442
1447
  ...flow,
1443
1448
  requests: flow.requests.map(sanitizeRequest)
1444
1449
  }));
1445
1450
  sendJson(req, res, HTTP_OK, { total: flows.length, flows });
1446
1451
  };
1447
1452
  }
1448
- function createClearHandler(registry) {
1453
+ function createClearHandler(services) {
1449
1454
  return (req, res) => {
1450
1455
  if (req.method !== "POST") {
1451
1456
  sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1452
1457
  return;
1453
1458
  }
1454
- registry.get("request-store").clear();
1455
- registry.get("fetch-store").clear();
1456
- registry.get("log-store").clear();
1457
- registry.get("error-store").clear();
1458
- registry.get("query-store").clear();
1459
- registry.get("metrics-store").reset();
1460
- if (registry.has("issue-store")) registry.get("issue-store").clear();
1461
- registry.get("event-bus").emit("store:cleared", void 0);
1459
+ services.requestStore.clear();
1460
+ services.fetchStore.clear();
1461
+ services.logStore.clear();
1462
+ services.errorStore.clear();
1463
+ services.queryStore.clear();
1464
+ services.metricsStore.reset();
1465
+ services.issueStore.clear();
1466
+ services.bus.emit("store:cleared", void 0);
1462
1467
  sendJson(req, res, HTTP_OK, { cleared: true });
1463
1468
  };
1464
1469
  }
1465
- function createFetchesHandler(registry) {
1466
- return (req, res) => handleTelemetryGet(req, res, registry.get("fetch-store"));
1470
+ function createFetchesHandler(services) {
1471
+ return (req, res) => handleTelemetryGet(req, res, services.fetchStore);
1467
1472
  }
1468
- function createLogsHandler(registry) {
1469
- return (req, res) => handleTelemetryGet(req, res, registry.get("log-store"));
1473
+ function createLogsHandler(services) {
1474
+ return (req, res) => handleTelemetryGet(req, res, services.logStore);
1470
1475
  }
1471
- function createErrorsHandler(registry) {
1472
- return (req, res) => handleTelemetryGet(req, res, registry.get("error-store"));
1476
+ function createErrorsHandler(services) {
1477
+ return (req, res) => handleTelemetryGet(req, res, services.errorStore);
1473
1478
  }
1474
- function createQueriesHandler(registry) {
1475
- return (req, res) => handleTelemetryGet(req, res, registry.get("query-store"));
1479
+ function createQueriesHandler(services) {
1480
+ return (req, res) => handleTelemetryGet(req, res, services.queryStore);
1476
1481
  }
1477
1482
  var init_handlers = __esm({
1478
1483
  "src/dashboard/api/handlers.ts"() {
1479
1484
  "use strict";
1480
1485
  init_group();
1481
1486
  init_constants();
1482
- init_http();
1487
+ init_labels();
1483
1488
  init_shared2();
1484
1489
  }
1485
1490
  });
1486
1491
 
1487
- // src/utils/type-guards.ts
1488
- function isString(val) {
1489
- return typeof val === "string";
1490
- }
1491
- function isNumber(val) {
1492
- return typeof val === "number" && !isNaN(val);
1493
- }
1494
- function isBoolean(val) {
1495
- return typeof val === "boolean";
1496
- }
1497
- function getErrorMessage(err) {
1498
- if (err instanceof Error) return err.message;
1499
- if (typeof err === "string") return err;
1500
- return String(err);
1501
- }
1502
- function isValidIssueState(val) {
1503
- return typeof val === "string" && VALID_ISSUE_STATES.has(val);
1504
- }
1505
- function isValidIssueCategory(val) {
1506
- return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
1507
- }
1508
- function isValidAiFixStatus(val) {
1509
- return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
1510
- }
1511
- function validateIssuesData(parsed) {
1512
- if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
1513
- return parsed;
1514
- }
1515
- return null;
1492
+ // src/utils/static-patterns.ts
1493
+ function isStaticPath(urlPath) {
1494
+ return STATIC_PATTERNS.some((p) => p.test(urlPath));
1516
1495
  }
1517
- function validateMetricsData(parsed) {
1518
- if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === 1 && Array.isArray(parsed.endpoints)) {
1519
- return parsed;
1520
- }
1521
- return null;
1496
+ function isHealthCheckPath(urlPath) {
1497
+ return HEALTH_CHECK_PATTERNS.some((p) => p.test(urlPath));
1522
1498
  }
1523
- var init_type_guards = __esm({
1524
- "src/utils/type-guards.ts"() {
1499
+ var STATIC_PATTERNS, HEALTH_CHECK_PATTERNS;
1500
+ var init_static_patterns = __esm({
1501
+ "src/utils/static-patterns.ts"() {
1525
1502
  "use strict";
1526
- init_lifecycle();
1527
- init_limits();
1503
+ STATIC_PATTERNS = [
1504
+ /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
1505
+ /^\/favicon/,
1506
+ /^\/node_modules\//,
1507
+ // Framework-specific static/internal paths
1508
+ /^\/_next\//,
1509
+ /^\/__nextjs/,
1510
+ /^\/@vite\//,
1511
+ /^\/__vite/
1512
+ ];
1513
+ HEALTH_CHECK_PATTERNS = [
1514
+ /^\/health(z|check)?$/i,
1515
+ /^\/ping$/i,
1516
+ /^\/(ready|readiness|liveness)$/i,
1517
+ /^\/status$/i,
1518
+ /^\/__health$/i,
1519
+ /^\/api\/health(z|check)?$/i
1520
+ ];
1528
1521
  }
1529
1522
  });
1530
1523
 
@@ -1545,8 +1538,8 @@ function numOrUndef(val) {
1545
1538
  function headers(val) {
1546
1539
  if (val && typeof val === "object" && !Array.isArray(val)) {
1547
1540
  const result = {};
1548
- for (const [k, v] of Object.entries(val)) {
1549
- if (typeof v === "string") result[k] = v;
1541
+ for (const [key, value] of Object.entries(val)) {
1542
+ if (typeof value === "string") result[key] = value;
1550
1543
  }
1551
1544
  return result;
1552
1545
  }
@@ -1610,7 +1603,7 @@ function parseRequestEvent(data, ts) {
1610
1603
  id: str(data.id, randomUUID4()),
1611
1604
  method: str(data.method, "GET"),
1612
1605
  url,
1613
- path: url.split("?")[0],
1606
+ path: stripQueryString(url),
1614
1607
  headers: headers(data.headers),
1615
1608
  requestBody: isString(data.requestBody) ? data.requestBody : null,
1616
1609
  statusCode: num(data.statusCode, 200),
@@ -1619,7 +1612,8 @@ function parseRequestEvent(data, ts) {
1619
1612
  startedAt: ts,
1620
1613
  durationMs: num(data.durationMs, 0),
1621
1614
  responseSize: num(data.responseSize, 0),
1622
- isStatic: isBoolean(data.isStatic) ? data.isStatic : false
1615
+ isStatic: isBoolean(data.isStatic) ? data.isStatic : false,
1616
+ isHealthCheck: isBoolean(data.isHealthCheck) ? data.isHealthCheck : isHealthCheckPath(stripQueryString(url))
1623
1617
  };
1624
1618
  }
1625
1619
  function routeSDKEvent(event, stores) {
@@ -1653,7 +1647,9 @@ var init_sdk_event_parser = __esm({
1653
1647
  "src/dashboard/api/sdk-event-parser.ts"() {
1654
1648
  "use strict";
1655
1649
  init_type_guards();
1656
- init_sdk_events();
1650
+ init_labels();
1651
+ init_static_patterns();
1652
+ init_endpoint();
1657
1653
  LOG_LEVEL_MAP = {
1658
1654
  debug: "debug",
1659
1655
  info: "info",
@@ -1673,28 +1669,28 @@ function isBrakitBatch(msg) {
1673
1669
  function isSDKPayload(msg) {
1674
1670
  return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
1675
1671
  }
1676
- function createIngestHandler(registry) {
1672
+ function createIngestHandler(services) {
1677
1673
  const routeEvent = (event) => {
1678
1674
  switch (event.type) {
1679
1675
  case TIMELINE_FETCH:
1680
- registry.get("fetch-store").add(event.data);
1676
+ services.fetchStore.add(event.data);
1681
1677
  break;
1682
1678
  case TIMELINE_LOG:
1683
- registry.get("log-store").add(event.data);
1679
+ services.logStore.add(event.data);
1684
1680
  break;
1685
1681
  case TIMELINE_ERROR:
1686
- registry.get("error-store").add(event.data);
1682
+ services.errorStore.add(event.data);
1687
1683
  break;
1688
1684
  case TIMELINE_QUERY:
1689
- registry.get("query-store").add(event.data);
1685
+ services.queryStore.add(event.data);
1690
1686
  break;
1691
1687
  }
1692
1688
  };
1693
- const queryStore = registry.get("query-store");
1694
- const fetchStore = registry.get("fetch-store");
1695
- const logStore = registry.get("log-store");
1696
- const errorStore = registry.get("error-store");
1697
- const requestStore = registry.get("request-store");
1689
+ const queryStore = services.queryStore;
1690
+ const fetchStore = services.fetchStore;
1691
+ const logStore = services.logStore;
1692
+ const errorStore = services.errorStore;
1693
+ const requestStore = services.requestStore;
1698
1694
  const stores = {
1699
1695
  addQuery: (data) => queryStore.add(data),
1700
1696
  addFetch: (data) => fetchStore.add(data),
@@ -1754,9 +1750,8 @@ function createIngestHandler(registry) {
1754
1750
  var init_ingest = __esm({
1755
1751
  "src/dashboard/api/ingest.ts"() {
1756
1752
  "use strict";
1757
- init_limits();
1758
- init_http();
1759
- init_timeline();
1753
+ init_config();
1754
+ init_labels();
1760
1755
  init_shared2();
1761
1756
  init_sdk_event_parser();
1762
1757
  }
@@ -1776,11 +1771,11 @@ function createMetricsHandler(metricsStore) {
1776
1771
  sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
1777
1772
  };
1778
1773
  }
1779
- var init_metrics2 = __esm({
1774
+ var init_metrics = __esm({
1780
1775
  "src/dashboard/api/metrics.ts"() {
1781
1776
  "use strict";
1782
1777
  init_shared2();
1783
- init_http();
1778
+ init_labels();
1784
1779
  }
1785
1780
  });
1786
1781
 
@@ -1798,61 +1793,55 @@ var init_metrics_live = __esm({
1798
1793
  }
1799
1794
  });
1800
1795
 
1801
- // src/utils/log.ts
1802
- function brakitWarn(message) {
1803
- process.stderr.write(`${PREFIX} ${message}
1804
- `);
1805
- }
1806
- function brakitDebug(message) {
1807
- if (process.env.DEBUG_BRAKIT) {
1808
- process.stderr.write(`${PREFIX}:debug ${message}
1809
- `);
1810
- }
1811
- }
1812
- var PREFIX;
1813
- var init_log = __esm({
1814
- "src/utils/log.ts"() {
1815
- "use strict";
1816
- PREFIX = "[brakit]";
1817
- }
1818
- });
1819
-
1820
1796
  // src/dashboard/api/activity.ts
1821
- function createActivityHandler(registry) {
1797
+ function buildTimeline(services, requestId) {
1798
+ const fetches = services.fetchStore.getByRequest(requestId);
1799
+ const logs = services.logStore.getByRequest(requestId);
1800
+ const errors = services.errorStore.getByRequest(requestId);
1801
+ const queries = services.queryStore.getByRequest(requestId);
1802
+ const timeline = [];
1803
+ for (const fetch of fetches)
1804
+ timeline.push({ type: TIMELINE_FETCH, timestamp: fetch.timestamp, data: fetch });
1805
+ for (const log of logs)
1806
+ timeline.push({ type: TIMELINE_LOG, timestamp: log.timestamp, data: log });
1807
+ for (const error of errors)
1808
+ timeline.push({ type: TIMELINE_ERROR, timestamp: error.timestamp, data: error });
1809
+ for (const query of queries)
1810
+ timeline.push({ type: TIMELINE_QUERY, timestamp: query.timestamp, data: query });
1811
+ timeline.sort((a, b) => a.timestamp - b.timestamp);
1812
+ return {
1813
+ total: timeline.length,
1814
+ timeline,
1815
+ counts: {
1816
+ fetches: fetches.length,
1817
+ logs: logs.length,
1818
+ errors: errors.length,
1819
+ queries: queries.length
1820
+ }
1821
+ };
1822
+ }
1823
+ function createActivityHandler(services) {
1822
1824
  return (req, res) => {
1823
1825
  if (!requireGet(req, res)) return;
1824
1826
  try {
1825
1827
  const url = parseRequestUrl(req);
1826
1828
  const requestId = url.searchParams.get("requestId");
1827
- if (!requestId) {
1828
- sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId parameter required" });
1829
+ const requestIds = url.searchParams.get("requestIds");
1830
+ if (!requestId && !requestIds) {
1831
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId or requestIds parameter required" });
1829
1832
  return;
1830
1833
  }
1831
- const fetches = registry.get("fetch-store").getByRequest(requestId);
1832
- const logs = registry.get("log-store").getByRequest(requestId);
1833
- const errors = registry.get("error-store").getByRequest(requestId);
1834
- const queries = registry.get("query-store").getByRequest(requestId);
1835
- const timeline = [];
1836
- for (const f of fetches)
1837
- timeline.push({ type: TIMELINE_FETCH, timestamp: f.timestamp, data: f });
1838
- for (const l of logs)
1839
- timeline.push({ type: TIMELINE_LOG, timestamp: l.timestamp, data: l });
1840
- for (const e of errors)
1841
- timeline.push({ type: TIMELINE_ERROR, timestamp: e.timestamp, data: e });
1842
- for (const q of queries)
1843
- timeline.push({ type: TIMELINE_QUERY, timestamp: q.timestamp, data: q });
1844
- timeline.sort((a, b) => a.timestamp - b.timestamp);
1845
- sendJson(req, res, HTTP_OK, {
1846
- requestId,
1847
- total: timeline.length,
1848
- timeline,
1849
- counts: {
1850
- fetches: fetches.length,
1851
- logs: logs.length,
1852
- errors: errors.length,
1853
- queries: queries.length
1854
- }
1855
- });
1834
+ if (requestId) {
1835
+ const result = buildTimeline(services, requestId);
1836
+ sendJson(req, res, HTTP_OK, { requestId, ...result });
1837
+ return;
1838
+ }
1839
+ const ids = (requestIds || "").split(",").filter(Boolean).slice(0, MAX_BATCH_IDS);
1840
+ const activities = {};
1841
+ for (const id of ids) {
1842
+ activities[id] = buildTimeline(services, id);
1843
+ }
1844
+ sendJson(req, res, HTTP_OK, { requestIds: ids, activities });
1856
1845
  } catch (err) {
1857
1846
  brakitDebug(`activity handler error: ${err}`);
1858
1847
  if (!res.headersSent) {
@@ -1861,13 +1850,14 @@ function createActivityHandler(registry) {
1861
1850
  }
1862
1851
  };
1863
1852
  }
1853
+ var MAX_BATCH_IDS;
1864
1854
  var init_activity = __esm({
1865
1855
  "src/dashboard/api/activity.ts"() {
1866
1856
  "use strict";
1867
1857
  init_shared2();
1868
- init_http();
1869
- init_timeline();
1858
+ init_labels();
1870
1859
  init_log();
1860
+ MAX_BATCH_IDS = 50;
1871
1861
  }
1872
1862
  });
1873
1863
 
@@ -1877,7 +1867,7 @@ var init_api = __esm({
1877
1867
  "use strict";
1878
1868
  init_handlers();
1879
1869
  init_ingest();
1880
- init_metrics2();
1870
+ init_metrics();
1881
1871
  init_metrics_live();
1882
1872
  init_activity();
1883
1873
  }
@@ -1952,25 +1942,12 @@ var init_issues = __esm({
1952
1942
  "use strict";
1953
1943
  init_shared2();
1954
1944
  init_type_guards();
1955
- init_http();
1956
- }
1957
- });
1958
-
1959
- // src/constants/events.ts
1960
- var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES;
1961
- var init_events = __esm({
1962
- "src/constants/events.ts"() {
1963
- "use strict";
1964
- SSE_EVENT_FETCH = "fetch";
1965
- SSE_EVENT_LOG = "log";
1966
- SSE_EVENT_ERROR = "error_event";
1967
- SSE_EVENT_QUERY = "query";
1968
- SSE_EVENT_ISSUES = "issues";
1945
+ init_labels();
1969
1946
  }
1970
1947
  });
1971
1948
 
1972
1949
  // src/dashboard/sse.ts
1973
- function createSSEHandler(registry) {
1950
+ function createSSEHandler(services) {
1974
1951
  const clients = /* @__PURE__ */ new Set();
1975
1952
  function broadcast(eventType, data) {
1976
1953
  if (clients.size === 0) return;
@@ -1992,7 +1969,7 @@ data: ${data}
1992
1969
  }
1993
1970
  }
1994
1971
  }
1995
- const bus = registry.get("event-bus");
1972
+ const bus = services.bus;
1996
1973
  bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
1997
1974
  bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
1998
1975
  bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
@@ -2042,8 +2019,7 @@ var init_sse = __esm({
2042
2019
  "src/dashboard/sse.ts"() {
2043
2020
  "use strict";
2044
2021
  init_constants();
2045
- init_http();
2046
- init_events();
2022
+ init_labels();
2047
2023
  init_shared2();
2048
2024
  }
2049
2025
  });
@@ -2098,7 +2074,7 @@ async function ensureGitignoreAsync(dir, entry) {
2098
2074
  var init_fs = __esm({
2099
2075
  "src/utils/fs.ts"() {
2100
2076
  "use strict";
2101
- init_limits();
2077
+ init_config();
2102
2078
  init_log();
2103
2079
  init_type_guards();
2104
2080
  }
@@ -2187,7 +2163,7 @@ function computeIssueId(issue) {
2187
2163
  var init_issue_id = __esm({
2188
2164
  "src/utils/issue-id.ts"() {
2189
2165
  "use strict";
2190
- init_limits();
2166
+ init_config();
2191
2167
  }
2192
2168
  });
2193
2169
 
@@ -2200,10 +2176,7 @@ var init_issue_store = __esm({
2200
2176
  "src/store/issue-store.ts"() {
2201
2177
  "use strict";
2202
2178
  init_fs();
2203
- init_metrics();
2204
- init_limits();
2205
- init_thresholds();
2206
- init_limits();
2179
+ init_config();
2207
2180
  init_atomic_writer();
2208
2181
  init_log();
2209
2182
  init_type_guards();
@@ -2446,7 +2419,7 @@ function unwrapResponse(parsed) {
2446
2419
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
2447
2420
  const obj = parsed;
2448
2421
  const keys = Object.keys(obj);
2449
- if (keys.length > 3) return parsed;
2422
+ if (keys.length > MAX_WRAPPER_KEYS) return parsed;
2450
2423
  let best = null;
2451
2424
  let bestSize = 0;
2452
2425
  for (const key of keys) {
@@ -2464,10 +2437,12 @@ function unwrapResponse(parsed) {
2464
2437
  }
2465
2438
  return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
2466
2439
  }
2440
+ var MAX_WRAPPER_KEYS;
2467
2441
  var init_response = __esm({
2468
2442
  "src/utils/response.ts"() {
2469
2443
  "use strict";
2470
- init_thresholds();
2444
+ init_config();
2445
+ MAX_WRAPPER_KEYS = 3;
2471
2446
  }
2472
2447
  });
2473
2448
 
@@ -2505,92 +2480,153 @@ var init_patterns = __esm({
2505
2480
  }
2506
2481
  });
2507
2482
 
2508
- // src/analysis/rules/exposed-secret.ts
2509
- function findSecretKeys(obj, prefix, depth = 0) {
2510
- const found = [];
2511
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
2512
- if (!obj || typeof obj !== "object") return found;
2513
- if (Array.isArray(obj)) {
2514
- for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
2515
- found.push(...findSecretKeys(obj[i], prefix, depth + 1));
2516
- }
2517
- return found;
2483
+ // src/utils/collections.ts
2484
+ function getOrCreate(map, key, create) {
2485
+ let value = map.get(key);
2486
+ if (value === void 0) {
2487
+ value = create();
2488
+ map.set(key, value);
2518
2489
  }
2519
- for (const k of Object.keys(obj)) {
2520
- const val = obj[k];
2521
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
2522
- found.push(k);
2523
- }
2524
- if (typeof val === "object" && val !== null) {
2525
- found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
2490
+ return value;
2491
+ }
2492
+ function deduplicateFindings(items, extract) {
2493
+ const seen = /* @__PURE__ */ new Map();
2494
+ const findings = [];
2495
+ for (const item of items) {
2496
+ const result = extract(item);
2497
+ if (!result) continue;
2498
+ const existing = seen.get(result.key);
2499
+ if (existing) {
2500
+ existing.count++;
2501
+ continue;
2526
2502
  }
2503
+ seen.set(result.key, result.finding);
2504
+ findings.push(result.finding);
2527
2505
  }
2528
- return found;
2506
+ return findings;
2529
2507
  }
2530
- var exposedSecretRule;
2531
- var init_exposed_secret = __esm({
2532
- "src/analysis/rules/exposed-secret.ts"() {
2533
- "use strict";
2534
- init_patterns();
2535
- init_limits();
2536
- init_http_status();
2537
- exposedSecretRule = {
2538
- id: "exposed-secret",
2539
- severity: "critical",
2540
- name: "Exposed Secret in Response",
2541
- hint: RULE_HINTS["exposed-secret"],
2542
- check(ctx) {
2543
- const findings = [];
2544
- const seen = /* @__PURE__ */ new Map();
2545
- for (const r of ctx.requests) {
2546
- if (isErrorStatus(r.statusCode)) continue;
2547
- const parsed = ctx.parsedBodies.response.get(r.id);
2548
- if (!parsed) continue;
2549
- const keys = findSecretKeys(parsed, "");
2550
- if (keys.length === 0) continue;
2551
- const ep = `${r.method} ${r.path}`;
2552
- const dedupKey = `${ep}:${keys.sort().join(",")}`;
2553
- const existing = seen.get(dedupKey);
2554
- if (existing) {
2555
- existing.count++;
2556
- continue;
2557
- }
2558
- const finding = {
2559
- severity: "critical",
2560
- rule: "exposed-secret",
2561
- title: "Exposed Secret in Response",
2562
- desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
2563
- hint: this.hint,
2564
- endpoint: ep,
2565
- count: 1
2566
- };
2567
- seen.set(dedupKey, finding);
2568
- findings.push(finding);
2569
- }
2570
- return findings;
2571
- }
2508
+ function groupBy(items, keyFn) {
2509
+ const map = /* @__PURE__ */ new Map();
2510
+ for (const item of items) {
2511
+ const key = keyFn(item);
2512
+ if (key == null) continue;
2513
+ let arr = map.get(key);
2514
+ if (!arr) {
2515
+ arr = [];
2516
+ map.set(key, arr);
2517
+ }
2518
+ arr.push(item);
2519
+ }
2520
+ return map;
2521
+ }
2522
+ var init_collections = __esm({
2523
+ "src/utils/collections.ts"() {
2524
+ "use strict";
2525
+ }
2526
+ });
2527
+
2528
+ // src/utils/object-scan.ts
2529
+ function walkObject(obj, visitor, options) {
2530
+ const opts = { ...DEFAULTS, ...options };
2531
+ walk(obj, visitor, opts, 0);
2532
+ }
2533
+ function walk(obj, visitor, opts, depth) {
2534
+ if (depth >= opts.maxDepth) return;
2535
+ if (!obj || typeof obj !== "object") return;
2536
+ if (Array.isArray(obj)) {
2537
+ for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
2538
+ walk(obj[i], visitor, opts, depth + 1);
2539
+ }
2540
+ return;
2541
+ }
2542
+ for (const key of Object.keys(obj)) {
2543
+ const val = obj[key];
2544
+ visitor(key, val, depth);
2545
+ if (typeof val === "object" && val !== null) {
2546
+ walk(val, visitor, opts, depth + 1);
2547
+ }
2548
+ }
2549
+ }
2550
+ function collectFromObject(obj, match, options) {
2551
+ const results = [];
2552
+ walkObject(obj, (key, value) => {
2553
+ const result = match(key, value);
2554
+ if (result !== null) results.push(result);
2555
+ }, options);
2556
+ return results;
2557
+ }
2558
+ var DEFAULTS;
2559
+ var init_object_scan = __esm({
2560
+ "src/utils/object-scan.ts"() {
2561
+ "use strict";
2562
+ init_config();
2563
+ DEFAULTS = {
2564
+ maxDepth: MAX_OBJECT_SCAN_DEPTH,
2565
+ arrayLimit: SECRET_SCAN_ARRAY_LIMIT
2572
2566
  };
2573
2567
  }
2574
2568
  });
2575
2569
 
2576
- // src/analysis/rules/token-in-url.ts
2577
- var tokenInUrlRule;
2578
- var init_token_in_url = __esm({
2579
- "src/analysis/rules/token-in-url.ts"() {
2570
+ // src/analysis/rules/auth-rules.ts
2571
+ function findSecretKeys(obj) {
2572
+ return collectFromObject(
2573
+ obj,
2574
+ (key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
2575
+ );
2576
+ }
2577
+ function isFrameworkResponse(request) {
2578
+ if (isRedirect(request.statusCode)) return true;
2579
+ if (request.path?.startsWith("/__")) return true;
2580
+ if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
2581
+ return false;
2582
+ }
2583
+ var exposedSecretRule, tokenInUrlRule, insecureCookieRule, corsCredentialsRule;
2584
+ var init_auth_rules = __esm({
2585
+ "src/analysis/rules/auth-rules.ts"() {
2580
2586
  "use strict";
2581
2587
  init_patterns();
2588
+ init_config();
2589
+ init_http_status();
2590
+ init_collections();
2591
+ init_object_scan();
2592
+ exposedSecretRule = {
2593
+ id: "exposed-secret",
2594
+ severity: "critical",
2595
+ name: "Exposed Secret in Response",
2596
+ hint: RULE_HINTS["exposed-secret"],
2597
+ check(ctx) {
2598
+ return deduplicateFindings(ctx.requests, (request) => {
2599
+ if (isErrorStatus(request.statusCode)) return null;
2600
+ const parsed = ctx.parsedBodies.response.get(request.id);
2601
+ if (!parsed) return null;
2602
+ const keys = findSecretKeys(parsed);
2603
+ if (keys.length === 0) return null;
2604
+ const ep = `${request.method} ${request.path}`;
2605
+ return {
2606
+ key: `${ep}:${keys.sort().join(",")}`,
2607
+ finding: {
2608
+ severity: "critical",
2609
+ rule: "exposed-secret",
2610
+ title: "Exposed Secret in Response",
2611
+ desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
2612
+ hint: this.hint,
2613
+ endpoint: ep,
2614
+ count: 1
2615
+ }
2616
+ };
2617
+ });
2618
+ }
2619
+ };
2582
2620
  tokenInUrlRule = {
2583
2621
  id: "token-in-url",
2584
2622
  severity: "critical",
2585
2623
  name: "Auth Token in URL",
2586
2624
  hint: RULE_HINTS["token-in-url"],
2587
2625
  check(ctx) {
2588
- const findings = [];
2589
- const seen = /* @__PURE__ */ new Map();
2590
- for (const r of ctx.requests) {
2591
- const qIdx = r.url.indexOf("?");
2592
- if (qIdx === -1) continue;
2593
- const params = r.url.substring(qIdx + 1).split("&");
2626
+ return deduplicateFindings(ctx.requests, (request) => {
2627
+ const qIdx = request.url.indexOf("?");
2628
+ if (qIdx === -1) return null;
2629
+ const params = request.url.substring(qIdx + 1).split("&");
2594
2630
  const flagged = [];
2595
2631
  for (const param of params) {
2596
2632
  const [name, ...rest] = param.split("=");
@@ -2600,165 +2636,50 @@ var init_token_in_url = __esm({
2600
2636
  flagged.push(name);
2601
2637
  }
2602
2638
  }
2603
- if (flagged.length === 0) continue;
2604
- const ep = `${r.method} ${r.path}`;
2605
- const dedupKey = `${ep}:${flagged.sort().join(",")}`;
2606
- const existing = seen.get(dedupKey);
2607
- if (existing) {
2608
- existing.count++;
2609
- continue;
2610
- }
2611
- const finding = {
2612
- severity: "critical",
2613
- rule: "token-in-url",
2614
- title: "Auth Token in URL",
2615
- desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
2616
- hint: this.hint,
2617
- endpoint: ep,
2618
- count: 1
2619
- };
2620
- seen.set(dedupKey, finding);
2621
- findings.push(finding);
2622
- }
2623
- return findings;
2624
- }
2625
- };
2626
- }
2627
- });
2628
-
2629
- // src/analysis/rules/stack-trace-leak.ts
2630
- var stackTraceLeakRule;
2631
- var init_stack_trace_leak = __esm({
2632
- "src/analysis/rules/stack-trace-leak.ts"() {
2633
- "use strict";
2634
- init_patterns();
2635
- stackTraceLeakRule = {
2636
- id: "stack-trace-leak",
2637
- severity: "critical",
2638
- name: "Stack Trace Leaked to Client",
2639
- hint: RULE_HINTS["stack-trace-leak"],
2640
- check(ctx) {
2641
- const findings = [];
2642
- const seen = /* @__PURE__ */ new Map();
2643
- for (const r of ctx.requests) {
2644
- if (!r.responseBody) continue;
2645
- if (!STACK_TRACE_RE.test(r.responseBody)) continue;
2646
- const ep = `${r.method} ${r.path}`;
2647
- const existing = seen.get(ep);
2648
- if (existing) {
2649
- existing.count++;
2650
- continue;
2651
- }
2652
- const finding = {
2653
- severity: "critical",
2654
- rule: "stack-trace-leak",
2655
- title: "Stack Trace Leaked to Client",
2656
- desc: `${ep} \u2014 response exposes internal stack trace`,
2657
- hint: this.hint,
2658
- endpoint: ep,
2659
- count: 1
2660
- };
2661
- seen.set(ep, finding);
2662
- findings.push(finding);
2663
- }
2664
- return findings;
2665
- }
2666
- };
2667
- }
2668
- });
2669
-
2670
- // src/analysis/rules/error-info-leak.ts
2671
- var CRITICAL_PATTERNS, errorInfoLeakRule;
2672
- var init_error_info_leak = __esm({
2673
- "src/analysis/rules/error-info-leak.ts"() {
2674
- "use strict";
2675
- init_patterns();
2676
- CRITICAL_PATTERNS = [
2677
- { re: DB_CONN_RE, label: "database connection string" },
2678
- { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
2679
- { re: SECRET_VAL_RE, label: "secret value" }
2680
- ];
2681
- errorInfoLeakRule = {
2682
- id: "error-info-leak",
2683
- severity: "critical",
2684
- name: "Sensitive Data in Error Response",
2685
- hint: RULE_HINTS["error-info-leak"],
2686
- check(ctx) {
2687
- const findings = [];
2688
- const seen = /* @__PURE__ */ new Map();
2689
- for (const r of ctx.requests) {
2690
- if (r.statusCode < 400) continue;
2691
- if (!r.responseBody) continue;
2692
- if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
2693
- const ep = `${r.method} ${r.path}`;
2694
- for (const p of CRITICAL_PATTERNS) {
2695
- if (!p.re.test(r.responseBody)) continue;
2696
- const dedupKey = `${ep}:${p.label}`;
2697
- const existing = seen.get(dedupKey);
2698
- if (existing) {
2699
- existing.count++;
2700
- continue;
2701
- }
2702
- const finding = {
2639
+ if (flagged.length === 0) return null;
2640
+ const ep = `${request.method} ${request.path}`;
2641
+ return {
2642
+ key: `${ep}:${flagged.sort().join(",")}`,
2643
+ finding: {
2703
2644
  severity: "critical",
2704
- rule: "error-info-leak",
2705
- title: "Sensitive Data in Error Response",
2706
- desc: `${ep} \u2014 error response exposes ${p.label}`,
2645
+ rule: "token-in-url",
2646
+ title: "Auth Token in URL",
2647
+ desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
2707
2648
  hint: this.hint,
2708
2649
  endpoint: ep,
2709
2650
  count: 1
2710
- };
2711
- seen.set(dedupKey, finding);
2712
- findings.push(finding);
2713
- }
2714
- }
2715
- return findings;
2651
+ }
2652
+ };
2653
+ });
2716
2654
  }
2717
2655
  };
2718
- }
2719
- });
2720
-
2721
- // src/analysis/rules/insecure-cookie.ts
2722
- function isFrameworkResponse(r) {
2723
- if (isRedirect(r.statusCode)) return true;
2724
- if (r.path?.startsWith("/__")) return true;
2725
- if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
2726
- return false;
2727
- }
2728
- var insecureCookieRule;
2729
- var init_insecure_cookie = __esm({
2730
- "src/analysis/rules/insecure-cookie.ts"() {
2731
- "use strict";
2732
- init_patterns();
2733
- init_http_status();
2734
2656
  insecureCookieRule = {
2735
2657
  id: "insecure-cookie",
2736
2658
  severity: "warning",
2737
2659
  name: "Insecure Cookie",
2738
2660
  hint: RULE_HINTS["insecure-cookie"],
2739
2661
  check(ctx) {
2740
- const findings = [];
2741
- const seen = /* @__PURE__ */ new Map();
2742
- for (const r of ctx.requests) {
2743
- if (!r.responseHeaders) continue;
2744
- if (isFrameworkResponse(r)) continue;
2745
- const setCookie = r.responseHeaders["set-cookie"];
2662
+ const cookieEntries = [];
2663
+ for (const request of ctx.requests) {
2664
+ if (!request.responseHeaders) continue;
2665
+ if (isFrameworkResponse(request)) continue;
2666
+ const setCookie = request.responseHeaders["set-cookie"];
2746
2667
  if (!setCookie) continue;
2747
2668
  const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
2748
2669
  for (const cookie of cookies) {
2749
- const cookieName = cookie.trim().split("=")[0].trim();
2750
- const lower = cookie.toLowerCase();
2751
- const issues = [];
2752
- if (!lower.includes("httponly")) issues.push("HttpOnly");
2753
- if (!lower.includes("samesite")) issues.push("SameSite");
2754
- if (issues.length === 0) continue;
2755
- const dedupKey = `${cookieName}:${issues.join(",")}`;
2756
- const existing = seen.get(dedupKey);
2757
- if (existing) {
2758
- existing.count++;
2759
- continue;
2760
- }
2761
- const finding = {
2670
+ cookieEntries.push({ cookie });
2671
+ }
2672
+ }
2673
+ return deduplicateFindings(cookieEntries, ({ cookie }) => {
2674
+ const cookieName = cookie.trim().split("=")[0].trim();
2675
+ const lower = cookie.toLowerCase();
2676
+ const issues = [];
2677
+ if (!lower.includes("httponly")) issues.push("HttpOnly");
2678
+ if (!lower.includes("samesite")) issues.push("SameSite");
2679
+ if (issues.length === 0) return null;
2680
+ return {
2681
+ key: `${cookieName}:${issues.join(",")}`,
2682
+ finding: {
2762
2683
  severity: "warning",
2763
2684
  rule: "insecure-cookie",
2764
2685
  title: "Insecure Cookie",
@@ -2766,56 +2687,11 @@ var init_insecure_cookie = __esm({
2766
2687
  hint: this.hint,
2767
2688
  endpoint: cookieName,
2768
2689
  count: 1
2769
- };
2770
- seen.set(dedupKey, finding);
2771
- findings.push(finding);
2772
- }
2773
- }
2774
- return findings;
2775
- }
2776
- };
2777
- }
2778
- });
2779
-
2780
- // src/analysis/rules/sensitive-logs.ts
2781
- var sensitiveLogsRule;
2782
- var init_sensitive_logs = __esm({
2783
- "src/analysis/rules/sensitive-logs.ts"() {
2784
- "use strict";
2785
- init_patterns();
2786
- sensitiveLogsRule = {
2787
- id: "sensitive-logs",
2788
- severity: "warning",
2789
- name: "Sensitive Data in Logs",
2790
- hint: RULE_HINTS["sensitive-logs"],
2791
- check(ctx) {
2792
- let count = 0;
2793
- for (const log of ctx.logs) {
2794
- if (!log.message) continue;
2795
- if (log.message.startsWith("[brakit]")) continue;
2796
- if (LOG_SECRET_RE.test(log.message)) count++;
2797
- }
2798
- if (count === 0) return [];
2799
- return [{
2800
- severity: "warning",
2801
- rule: "sensitive-logs",
2802
- title: "Sensitive Data in Logs",
2803
- desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
2804
- hint: this.hint,
2805
- endpoint: "console",
2806
- count
2807
- }];
2690
+ }
2691
+ };
2692
+ });
2808
2693
  }
2809
2694
  };
2810
- }
2811
- });
2812
-
2813
- // src/analysis/rules/cors-credentials.ts
2814
- var corsCredentialsRule;
2815
- var init_cors_credentials = __esm({
2816
- "src/analysis/rules/cors-credentials.ts"() {
2817
- "use strict";
2818
- init_patterns();
2819
2695
  corsCredentialsRule = {
2820
2696
  id: "cors-credentials",
2821
2697
  severity: "warning",
@@ -2824,12 +2700,12 @@ var init_cors_credentials = __esm({
2824
2700
  check(ctx) {
2825
2701
  const findings = [];
2826
2702
  const seen = /* @__PURE__ */ new Set();
2827
- for (const r of ctx.requests) {
2828
- if (!r.responseHeaders) continue;
2829
- const origin = r.responseHeaders["access-control-allow-origin"];
2830
- const creds = r.responseHeaders["access-control-allow-credentials"];
2703
+ for (const request of ctx.requests) {
2704
+ if (!request.responseHeaders) continue;
2705
+ const origin = request.responseHeaders["access-control-allow-origin"];
2706
+ const creds = request.responseHeaders["access-control-allow-credentials"];
2831
2707
  if (origin !== "*" || creds !== "true") continue;
2832
- const ep = `${r.method} ${r.path}`;
2708
+ const ep = `${request.method} ${request.path}`;
2833
2709
  if (seen.has(ep)) continue;
2834
2710
  seen.add(ep);
2835
2711
  findings.push({
@@ -2848,25 +2724,13 @@ var init_cors_credentials = __esm({
2848
2724
  }
2849
2725
  });
2850
2726
 
2851
- // src/analysis/rules/response-pii-leak.ts
2852
- function findEmails(obj, depth = 0) {
2853
- const emails = [];
2854
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
2855
- if (!obj || typeof obj !== "object") return emails;
2856
- if (Array.isArray(obj)) {
2857
- for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
2858
- emails.push(...findEmails(obj[i], depth + 1));
2859
- }
2860
- return emails;
2861
- }
2862
- for (const v of Object.values(obj)) {
2863
- if (typeof v === "string" && EMAIL_RE.test(v)) {
2864
- emails.push(v);
2865
- } else if (typeof v === "object" && v !== null) {
2866
- emails.push(...findEmails(v, depth + 1));
2867
- }
2868
- }
2869
- return emails;
2727
+ // src/analysis/rules/data-rules.ts
2728
+ function findEmails(obj) {
2729
+ return collectFromObject(
2730
+ obj,
2731
+ (_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
2732
+ { arrayLimit: PII_SCAN_ARRAY_LIMIT }
2733
+ );
2870
2734
  }
2871
2735
  function topLevelFieldCount(obj) {
2872
2736
  if (Array.isArray(obj)) {
@@ -2939,14 +2803,104 @@ function detectPII(method, reqBody, resBody) {
2939
2803
  const target = unwrapResponse(resBody);
2940
2804
  return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
2941
2805
  }
2942
- var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
2943
- var init_response_pii_leak = __esm({
2944
- "src/analysis/rules/response-pii-leak.ts"() {
2806
+ var stackTraceLeakRule, CRITICAL_PATTERNS, errorInfoLeakRule, sensitiveLogsRule, WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
2807
+ var init_data_rules = __esm({
2808
+ "src/analysis/rules/data-rules.ts"() {
2945
2809
  "use strict";
2810
+ init_collections();
2946
2811
  init_patterns();
2947
2812
  init_response();
2948
- init_limits();
2813
+ init_config();
2949
2814
  init_http_status();
2815
+ init_object_scan();
2816
+ stackTraceLeakRule = {
2817
+ id: "stack-trace-leak",
2818
+ severity: "critical",
2819
+ name: "Stack Trace Leaked to Client",
2820
+ hint: RULE_HINTS["stack-trace-leak"],
2821
+ check(ctx) {
2822
+ return deduplicateFindings(ctx.requests, (request) => {
2823
+ if (!request.responseBody) return null;
2824
+ if (!STACK_TRACE_RE.test(request.responseBody)) return null;
2825
+ const ep = `${request.method} ${request.path}`;
2826
+ return {
2827
+ key: ep,
2828
+ finding: {
2829
+ severity: "critical",
2830
+ rule: "stack-trace-leak",
2831
+ title: "Stack Trace Leaked to Client",
2832
+ desc: `${ep} \u2014 response exposes internal stack trace`,
2833
+ hint: this.hint,
2834
+ endpoint: ep,
2835
+ count: 1
2836
+ }
2837
+ };
2838
+ });
2839
+ }
2840
+ };
2841
+ CRITICAL_PATTERNS = [
2842
+ { re: DB_CONN_RE, label: "database connection string" },
2843
+ { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
2844
+ { re: SECRET_VAL_RE, label: "secret value" }
2845
+ ];
2846
+ errorInfoLeakRule = {
2847
+ id: "error-info-leak",
2848
+ severity: "critical",
2849
+ name: "Sensitive Data in Error Response",
2850
+ hint: RULE_HINTS["error-info-leak"],
2851
+ check(ctx) {
2852
+ const entries = [];
2853
+ for (const request of ctx.requests) {
2854
+ if (request.statusCode < 400) continue;
2855
+ if (!request.responseBody) continue;
2856
+ if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
2857
+ const ep = `${request.method} ${request.path}`;
2858
+ for (const pattern of CRITICAL_PATTERNS) {
2859
+ if (pattern.re.test(request.responseBody)) {
2860
+ entries.push({ ep, pattern, body: request.responseBody });
2861
+ }
2862
+ }
2863
+ }
2864
+ return deduplicateFindings(entries, ({ ep, pattern }) => {
2865
+ return {
2866
+ key: `${ep}:${pattern.label}`,
2867
+ finding: {
2868
+ severity: "critical",
2869
+ rule: "error-info-leak",
2870
+ title: "Sensitive Data in Error Response",
2871
+ desc: `${ep} \u2014 error response exposes ${pattern.label}`,
2872
+ hint: this.hint,
2873
+ endpoint: ep,
2874
+ count: 1
2875
+ }
2876
+ };
2877
+ });
2878
+ }
2879
+ };
2880
+ sensitiveLogsRule = {
2881
+ id: "sensitive-logs",
2882
+ severity: "warning",
2883
+ name: "Sensitive Data in Logs",
2884
+ hint: RULE_HINTS["sensitive-logs"],
2885
+ check(ctx) {
2886
+ let count = 0;
2887
+ for (const log of ctx.logs) {
2888
+ if (!log.message) continue;
2889
+ if (log.message.startsWith("[brakit]")) continue;
2890
+ if (LOG_SECRET_RE.test(log.message)) count++;
2891
+ }
2892
+ if (count === 0) return [];
2893
+ return [{
2894
+ severity: "warning",
2895
+ rule: "sensitive-logs",
2896
+ title: "Sensitive Data in Logs",
2897
+ desc: `Console output contains secret/token values \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
2898
+ hint: this.hint,
2899
+ endpoint: "console",
2900
+ count
2901
+ }];
2902
+ }
2903
+ };
2950
2904
  WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
2951
2905
  REASON_LABELS = {
2952
2906
  echo: "echoes back PII from the request body",
@@ -2960,35 +2914,28 @@ var init_response_pii_leak = __esm({
2960
2914
  name: "PII Leak in Response",
2961
2915
  hint: RULE_HINTS["response-pii-leak"],
2962
2916
  check(ctx) {
2963
- const findings = [];
2964
- const seen = /* @__PURE__ */ new Map();
2965
- for (const r of ctx.requests) {
2966
- if (isErrorStatus(r.statusCode)) continue;
2967
- if (SELF_SERVICE_PATH.test(r.path)) continue;
2968
- const resJson = ctx.parsedBodies.response.get(r.id);
2969
- if (!resJson) continue;
2970
- const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
2971
- const detection = detectPII(r.method, reqJson, resJson);
2972
- if (!detection) continue;
2973
- const ep = `${r.method} ${r.path}`;
2974
- const existing = seen.get(ep);
2975
- if (existing) {
2976
- existing.count++;
2977
- continue;
2978
- }
2979
- const finding = {
2980
- severity: "warning",
2981
- rule: "response-pii-leak",
2982
- title: "PII Leak in Response",
2983
- desc: `${ep} \u2014 exposes PII in response`,
2984
- hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
2985
- endpoint: ep,
2986
- count: 1
2917
+ return deduplicateFindings(ctx.requests, (request) => {
2918
+ if (isErrorStatus(request.statusCode)) return null;
2919
+ if (SELF_SERVICE_PATH.test(request.path)) return null;
2920
+ const resJson = ctx.parsedBodies.response.get(request.id);
2921
+ if (!resJson) return null;
2922
+ const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
2923
+ const detection = detectPII(request.method, reqJson, resJson);
2924
+ if (!detection) return null;
2925
+ const ep = `${request.method} ${request.path}`;
2926
+ return {
2927
+ key: ep,
2928
+ finding: {
2929
+ severity: "warning",
2930
+ rule: "response-pii-leak",
2931
+ title: "PII Leak in Response",
2932
+ desc: `${ep} \u2014 exposes PII in response`,
2933
+ hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
2934
+ endpoint: ep,
2935
+ count: 1
2936
+ }
2987
2937
  };
2988
- seen.set(ep, finding);
2989
- findings.push(finding);
2990
- }
2991
- return findings;
2938
+ });
2992
2939
  }
2993
2940
  };
2994
2941
  }
@@ -2998,14 +2945,14 @@ var init_response_pii_leak = __esm({
2998
2945
  function buildBodyCache(requests) {
2999
2946
  const response = /* @__PURE__ */ new Map();
3000
2947
  const request = /* @__PURE__ */ new Map();
3001
- for (const r of requests) {
3002
- if (r.responseBody) {
3003
- const parsed = tryParseJson(r.responseBody);
3004
- if (parsed != null) response.set(r.id, parsed);
2948
+ for (const req of requests) {
2949
+ if (req.responseBody) {
2950
+ const parsed = tryParseJson(req.responseBody);
2951
+ if (parsed != null) response.set(req.id, parsed);
3005
2952
  }
3006
- if (r.requestBody) {
3007
- const parsed = tryParseJson(r.requestBody);
3008
- if (parsed != null) request.set(r.id, parsed);
2953
+ if (req.requestBody) {
2954
+ const parsed = tryParseJson(req.requestBody);
2955
+ if (parsed != null) request.set(req.id, parsed);
3009
2956
  }
3010
2957
  }
3011
2958
  return { response, request };
@@ -3027,14 +2974,10 @@ var init_scanner = __esm({
3027
2974
  "src/analysis/rules/scanner.ts"() {
3028
2975
  "use strict";
3029
2976
  init_response();
3030
- init_exposed_secret();
3031
- init_token_in_url();
3032
- init_stack_trace_leak();
3033
- init_error_info_leak();
3034
- init_insecure_cookie();
3035
- init_sensitive_logs();
3036
- init_cors_credentials();
3037
- init_response_pii_leak();
2977
+ init_log();
2978
+ init_type_guards();
2979
+ init_auth_rules();
2980
+ init_data_rules();
3038
2981
  SecurityScanner = class {
3039
2982
  constructor() {
3040
2983
  this.rules = [];
@@ -3051,7 +2994,8 @@ var init_scanner = __esm({
3051
2994
  for (const rule of this.rules) {
3052
2995
  try {
3053
2996
  findings.push(...rule.check(ctx));
3054
- } catch {
2997
+ } catch (e) {
2998
+ brakitDebug(`rule ${rule.id} failed: ${getErrorMessage(e)}`);
3055
2999
  }
3056
3000
  }
3057
3001
  return findings;
@@ -3068,14 +3012,8 @@ var init_rules = __esm({
3068
3012
  "src/analysis/rules/index.ts"() {
3069
3013
  "use strict";
3070
3014
  init_scanner();
3071
- init_exposed_secret();
3072
- init_token_in_url();
3073
- init_stack_trace_leak();
3074
- init_error_info_leak();
3075
- init_insecure_cookie();
3076
- init_sensitive_logs();
3077
- init_cors_credentials();
3078
- init_response_pii_leak();
3015
+ init_auth_rules();
3016
+ init_data_rules();
3079
3017
  }
3080
3018
  });
3081
3019
 
@@ -3088,56 +3026,14 @@ var init_disposable = __esm({
3088
3026
  constructor() {
3089
3027
  this.items = [];
3090
3028
  }
3091
- add(teardown) {
3092
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3093
- }
3094
- dispose() {
3095
- for (const d of this.items) d.dispose();
3096
- this.items.length = 0;
3097
- }
3098
- };
3099
- }
3100
- });
3101
-
3102
- // src/utils/collections.ts
3103
- function groupBy(items, keyFn) {
3104
- const map = /* @__PURE__ */ new Map();
3105
- for (const item of items) {
3106
- const key = keyFn(item);
3107
- if (key == null) continue;
3108
- let arr = map.get(key);
3109
- if (!arr) {
3110
- arr = [];
3111
- map.set(key, arr);
3112
- }
3113
- arr.push(item);
3114
- }
3115
- return map;
3116
- }
3117
- var init_collections = __esm({
3118
- "src/utils/collections.ts"() {
3119
- "use strict";
3120
- }
3121
- });
3122
-
3123
- // src/utils/endpoint.ts
3124
- function normalizePath(path) {
3125
- const qIdx = path.indexOf("?");
3126
- const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
3127
- return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
3128
- }
3129
- function getEndpointKey(method, path) {
3130
- return `${method} ${normalizePath(path)}`;
3131
- }
3132
- function extractEndpointFromDesc(desc) {
3133
- return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
3134
- }
3135
- var DYNAMIC_SEGMENT_RE, ENDPOINT_PREFIX_RE;
3136
- var init_endpoint = __esm({
3137
- "src/utils/endpoint.ts"() {
3138
- "use strict";
3139
- 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;
3140
- ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
3029
+ add(teardown) {
3030
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3031
+ }
3032
+ dispose() {
3033
+ for (const d of this.items) d.dispose();
3034
+ this.items.length = 0;
3035
+ }
3036
+ };
3141
3037
  }
3142
3038
  });
3143
3039
 
@@ -3161,7 +3057,7 @@ var init_query_helpers = __esm({
3161
3057
  });
3162
3058
 
3163
3059
  // src/analysis/insights/prepare.ts
3164
- function createEndpointGroup() {
3060
+ function emptyEndpointGroup() {
3165
3061
  return {
3166
3062
  total: 0,
3167
3063
  errors: 0,
@@ -3173,16 +3069,12 @@ function createEndpointGroup() {
3173
3069
  queryShapeDurations: /* @__PURE__ */ new Map()
3174
3070
  };
3175
3071
  }
3176
- function windowByEndpoint(requests) {
3072
+ function keepRecentPerEndpoint(requests) {
3177
3073
  const byEndpoint = /* @__PURE__ */ new Map();
3178
- for (const r of requests) {
3179
- const ep = getEndpointKey(r.method, r.path);
3180
- let list = byEndpoint.get(ep);
3181
- if (!list) {
3182
- list = [];
3183
- byEndpoint.set(ep, list);
3184
- }
3185
- list.push(r);
3074
+ for (const request of requests) {
3075
+ const endpointKey = getEndpointKey(request.method, request.path);
3076
+ const list = getOrCreate(byEndpoint, endpointKey, () => []);
3077
+ list.push(request);
3186
3078
  }
3187
3079
  const windowed = [];
3188
3080
  for (const [, reqs] of byEndpoint) {
@@ -3190,54 +3082,67 @@ function windowByEndpoint(requests) {
3190
3082
  }
3191
3083
  return windowed;
3192
3084
  }
3085
+ function filterUserRequests(requests) {
3086
+ return requests.filter(
3087
+ (request) => !request.isStatic && !request.isHealthCheck && (!request.path || !request.path.startsWith(DASHBOARD_PREFIX))
3088
+ );
3089
+ }
3193
3090
  function extractActiveEndpoints(requests) {
3194
3091
  const endpoints = /* @__PURE__ */ new Set();
3195
- for (const r of requests) {
3196
- if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
3197
- endpoints.add(getEndpointKey(r.method, r.path));
3198
- }
3092
+ for (const request of filterUserRequests(requests)) {
3093
+ endpoints.add(getEndpointKey(request.method, request.path));
3199
3094
  }
3200
3095
  return endpoints;
3201
3096
  }
3202
- function prepareContext(ctx) {
3203
- const nonStatic = ctx.requests.filter(
3204
- (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
3205
- );
3206
- const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
3207
- const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
3208
- const reqById = new Map(nonStatic.map((r) => [r.id, r]));
3209
- const recent = windowByEndpoint(nonStatic);
3097
+ function aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq) {
3210
3098
  const endpointGroups = /* @__PURE__ */ new Map();
3211
- for (const r of recent) {
3212
- const ep = getEndpointKey(r.method, r.path);
3213
- let g = endpointGroups.get(ep);
3214
- if (!g) {
3215
- g = createEndpointGroup();
3216
- endpointGroups.set(ep, g);
3099
+ for (const request of recent) {
3100
+ const endpointKey = getEndpointKey(request.method, request.path);
3101
+ const group = getOrCreate(endpointGroups, endpointKey, emptyEndpointGroup);
3102
+ group.total++;
3103
+ if (isErrorStatus(request.statusCode)) group.errors++;
3104
+ group.totalDuration += request.durationMs;
3105
+ group.totalSize += request.responseSize ?? 0;
3106
+ const reqQueries = queriesByReq.get(request.id) ?? [];
3107
+ group.queryCount += reqQueries.length;
3108
+ for (const query of reqQueries) {
3109
+ group.totalQueryTimeMs += query.durationMs;
3110
+ const shape = getQueryShape(query);
3111
+ const info = getQueryInfo(query);
3112
+ const shapeDuration = getOrCreate(group.queryShapeDurations, shape, () => ({
3113
+ totalMs: 0,
3114
+ count: 0,
3115
+ label: info.op + (info.table ? ` ${info.table}` : "")
3116
+ }));
3117
+ shapeDuration.totalMs += query.durationMs;
3118
+ shapeDuration.count++;
3217
3119
  }
3218
- g.total++;
3219
- if (isErrorStatus(r.statusCode)) g.errors++;
3220
- g.totalDuration += r.durationMs;
3221
- g.totalSize += r.responseSize ?? 0;
3222
- const reqQueries = queriesByReq.get(r.id) ?? [];
3223
- g.queryCount += reqQueries.length;
3224
- for (const q of reqQueries) {
3225
- g.totalQueryTimeMs += q.durationMs;
3226
- const shape = getQueryShape(q);
3227
- const info = getQueryInfo(q);
3228
- let sd = g.queryShapeDurations.get(shape);
3229
- if (!sd) {
3230
- sd = { totalMs: 0, count: 0, label: info.op + (info.table ? ` ${info.table}` : "") };
3231
- g.queryShapeDurations.set(shape, sd);
3232
- }
3233
- sd.totalMs += q.durationMs;
3234
- sd.count++;
3120
+ const reqFetches = fetchesByReq.get(request.id) ?? [];
3121
+ for (const fetch of reqFetches) {
3122
+ group.totalFetchTimeMs += fetch.durationMs;
3235
3123
  }
3236
- const reqFetches = fetchesByReq.get(r.id) ?? [];
3237
- for (const f of reqFetches) {
3238
- g.totalFetchTimeMs += f.durationMs;
3124
+ }
3125
+ return endpointGroups;
3126
+ }
3127
+ function collectStrictModeDupeIds(ctx) {
3128
+ const ids = /* @__PURE__ */ new Set();
3129
+ for (const flow of ctx.flows) {
3130
+ for (const req of flow.requests) {
3131
+ if (req.isStrictModeDupe) ids.add(req.id);
3239
3132
  }
3240
3133
  }
3134
+ return ids;
3135
+ }
3136
+ function buildInsightContext(ctx) {
3137
+ const strictModeDupeIds = collectStrictModeDupeIds(ctx);
3138
+ const nonStatic = filterUserRequests(ctx.requests).filter((req) => !strictModeDupeIds.has(req.id));
3139
+ const filteredQueries = strictModeDupeIds.size > 0 ? ctx.queries.filter((q) => !q.parentRequestId || !strictModeDupeIds.has(q.parentRequestId)) : ctx.queries;
3140
+ const filteredFetches = strictModeDupeIds.size > 0 ? ctx.fetches.filter((f) => !f.parentRequestId || !strictModeDupeIds.has(f.parentRequestId)) : ctx.fetches;
3141
+ const queriesByReq = groupBy(filteredQueries, (query) => query.parentRequestId);
3142
+ const fetchesByReq = groupBy(filteredFetches, (fetch) => fetch.parentRequestId);
3143
+ const reqById = new Map(nonStatic.map((request) => [request.id, request]));
3144
+ const recent = keepRecentPerEndpoint(nonStatic);
3145
+ const endpointGroups = aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq);
3241
3146
  return {
3242
3147
  ...ctx,
3243
3148
  nonStatic,
@@ -3254,7 +3159,7 @@ var init_prepare = __esm({
3254
3159
  init_endpoint();
3255
3160
  init_constants();
3256
3161
  init_http_status();
3257
- init_thresholds();
3162
+ init_config();
3258
3163
  init_query_helpers();
3259
3164
  }
3260
3165
  });
@@ -3265,6 +3170,8 @@ var init_runner = __esm({
3265
3170
  "src/analysis/insights/runner.ts"() {
3266
3171
  "use strict";
3267
3172
  init_prepare();
3173
+ init_log();
3174
+ init_type_guards();
3268
3175
  SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
3269
3176
  InsightRunner = class {
3270
3177
  constructor() {
@@ -3274,12 +3181,13 @@ var init_runner = __esm({
3274
3181
  this.rules.push(rule);
3275
3182
  }
3276
3183
  run(ctx) {
3277
- const prepared = prepareContext(ctx);
3184
+ const prepared = buildInsightContext(ctx);
3278
3185
  const insights = [];
3279
3186
  for (const rule of this.rules) {
3280
3187
  try {
3281
3188
  insights.push(...rule.check(prepared));
3282
- } catch {
3189
+ } catch (e) {
3190
+ brakitDebug(`insight rule ${rule.id} failed: ${getErrorMessage(e)}`);
3283
3191
  }
3284
3192
  }
3285
3193
  insights.sort(
@@ -3291,14 +3199,15 @@ var init_runner = __esm({
3291
3199
  }
3292
3200
  });
3293
3201
 
3294
- // src/analysis/insights/rules/n1.ts
3295
- var n1Rule;
3296
- var init_n1 = __esm({
3297
- "src/analysis/insights/rules/n1.ts"() {
3202
+ // src/analysis/insights/rules/query-rules.ts
3203
+ var n1Rule, redundantQueryRule, selectStarRule, highRowsRule, queryHeavyRule;
3204
+ var init_query_rules = __esm({
3205
+ "src/analysis/insights/rules/query-rules.ts"() {
3298
3206
  "use strict";
3299
3207
  init_query_helpers();
3300
3208
  init_endpoint();
3301
3209
  init_constants();
3210
+ init_patterns();
3302
3211
  n1Rule = {
3303
3212
  id: "n1",
3304
3213
  check(ctx) {
@@ -3309,15 +3218,15 @@ var init_n1 = __esm({
3309
3218
  if (!req) continue;
3310
3219
  const endpoint = getEndpointKey(req.method, req.path);
3311
3220
  const shapeGroups = /* @__PURE__ */ new Map();
3312
- for (const q of reqQueries) {
3313
- const shape = getQueryShape(q);
3221
+ for (const query of reqQueries) {
3222
+ const shape = getQueryShape(query);
3314
3223
  let group = shapeGroups.get(shape);
3315
3224
  if (!group) {
3316
- group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
3225
+ group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: query };
3317
3226
  shapeGroups.set(shape, group);
3318
3227
  }
3319
3228
  group.count++;
3320
- group.distinctSql.add(q.sql ?? shape);
3229
+ group.distinctSql.add(query.sql ?? shape);
3321
3230
  }
3322
3231
  for (const [, sg] of shapeGroups) {
3323
3232
  if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
@@ -3338,75 +3247,6 @@ var init_n1 = __esm({
3338
3247
  return insights;
3339
3248
  }
3340
3249
  };
3341
- }
3342
- });
3343
-
3344
- // src/analysis/insights/rules/cross-endpoint.ts
3345
- var crossEndpointRule;
3346
- var init_cross_endpoint = __esm({
3347
- "src/analysis/insights/rules/cross-endpoint.ts"() {
3348
- "use strict";
3349
- init_query_helpers();
3350
- init_endpoint();
3351
- init_constants();
3352
- crossEndpointRule = {
3353
- id: "cross-endpoint",
3354
- check(ctx) {
3355
- const insights = [];
3356
- const queryMap = /* @__PURE__ */ new Map();
3357
- const allEndpoints = /* @__PURE__ */ new Set();
3358
- for (const [reqId, reqQueries] of ctx.queriesByReq) {
3359
- const req = ctx.reqById.get(reqId);
3360
- if (!req) continue;
3361
- const endpoint = getEndpointKey(req.method, req.path);
3362
- allEndpoints.add(endpoint);
3363
- const seenInReq = /* @__PURE__ */ new Set();
3364
- for (const q of reqQueries) {
3365
- const shape = getQueryShape(q);
3366
- let entry = queryMap.get(shape);
3367
- if (!entry) {
3368
- entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
3369
- queryMap.set(shape, entry);
3370
- }
3371
- entry.count++;
3372
- if (!seenInReq.has(shape)) {
3373
- seenInReq.add(shape);
3374
- entry.endpoints.add(endpoint);
3375
- }
3376
- }
3377
- }
3378
- if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
3379
- for (const [, cem] of queryMap) {
3380
- if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
3381
- if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
3382
- const p = Math.round(cem.endpoints.size / allEndpoints.size * 100);
3383
- if (p < CROSS_ENDPOINT_PCT) continue;
3384
- const info = getQueryInfo(cem.first);
3385
- const label = info.op + (info.table ? ` ${info.table}` : "");
3386
- insights.push({
3387
- severity: "warning",
3388
- type: "cross-endpoint",
3389
- title: "Repeated Query Across Endpoints",
3390
- desc: `${label} runs on ${cem.endpoints.size} of ${allEndpoints.size} endpoints (${p}%).`,
3391
- hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
3392
- nav: "queries"
3393
- });
3394
- }
3395
- }
3396
- return insights;
3397
- }
3398
- };
3399
- }
3400
- });
3401
-
3402
- // src/analysis/insights/rules/redundant-query.ts
3403
- var redundantQueryRule;
3404
- var init_redundant_query = __esm({
3405
- "src/analysis/insights/rules/redundant-query.ts"() {
3406
- "use strict";
3407
- init_query_helpers();
3408
- init_endpoint();
3409
- init_constants();
3410
3250
  redundantQueryRule = {
3411
3251
  id: "redundant-query",
3412
3252
  check(ctx) {
@@ -3417,27 +3257,27 @@ var init_redundant_query = __esm({
3417
3257
  if (!req) continue;
3418
3258
  const endpoint = getEndpointKey(req.method, req.path);
3419
3259
  const exact = /* @__PURE__ */ new Map();
3420
- for (const q of reqQueries) {
3421
- if (!q.sql) continue;
3422
- let entry = exact.get(q.sql);
3260
+ for (const query of reqQueries) {
3261
+ if (!query.sql) continue;
3262
+ let entry = exact.get(query.sql);
3423
3263
  if (!entry) {
3424
- entry = { count: 0, first: q };
3425
- exact.set(q.sql, entry);
3264
+ entry = { count: 0, first: query };
3265
+ exact.set(query.sql, entry);
3426
3266
  }
3427
3267
  entry.count++;
3428
3268
  }
3429
- for (const [, e] of exact) {
3430
- if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
3431
- const info = getQueryInfo(e.first);
3269
+ for (const [, entry] of exact) {
3270
+ if (entry.count < REDUNDANT_QUERY_MIN_COUNT) continue;
3271
+ const info = getQueryInfo(entry.first);
3432
3272
  const label = info.op + (info.table ? ` ${info.table}` : "");
3433
- const dedupKey = `${endpoint}:${label}`;
3434
- if (seen.has(dedupKey)) continue;
3435
- seen.add(dedupKey);
3273
+ const deduplicationKey = `${endpoint}:${label}`;
3274
+ if (seen.has(deduplicationKey)) continue;
3275
+ seen.add(deduplicationKey);
3436
3276
  insights.push({
3437
3277
  severity: "warning",
3438
3278
  type: "redundant-query",
3439
3279
  title: "Redundant Query",
3440
- desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
3280
+ desc: `${label} runs ${entry.count}x with identical params in ${endpoint}.`,
3441
3281
  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.",
3442
3282
  nav: "queries"
3443
3283
  });
@@ -3446,228 +3286,16 @@ var init_redundant_query = __esm({
3446
3286
  return insights;
3447
3287
  }
3448
3288
  };
3449
- }
3450
- });
3451
-
3452
- // src/analysis/insights/rules/error.ts
3453
- var errorRule;
3454
- var init_error = __esm({
3455
- "src/analysis/insights/rules/error.ts"() {
3456
- "use strict";
3457
- errorRule = {
3458
- id: "error",
3459
- check(ctx) {
3460
- if (ctx.errors.length === 0) return [];
3461
- const insights = [];
3462
- const groups = /* @__PURE__ */ new Map();
3463
- for (const e of ctx.errors) {
3464
- const name = e.name || "Error";
3465
- groups.set(name, (groups.get(name) ?? 0) + 1);
3466
- }
3467
- for (const [name, cnt] of groups) {
3468
- insights.push({
3469
- severity: "critical",
3470
- type: "error",
3471
- title: "Unhandled Error",
3472
- desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
3473
- hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
3474
- nav: "errors"
3475
- });
3476
- }
3477
- return insights;
3478
- }
3479
- };
3480
- }
3481
- });
3482
-
3483
- // src/analysis/insights/rules/error-hotspot.ts
3484
- var errorHotspotRule;
3485
- var init_error_hotspot = __esm({
3486
- "src/analysis/insights/rules/error-hotspot.ts"() {
3487
- "use strict";
3488
- init_constants();
3489
- errorHotspotRule = {
3490
- id: "error-hotspot",
3491
- check(ctx) {
3492
- const insights = [];
3493
- for (const [ep, g] of ctx.endpointGroups) {
3494
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3495
- const errorRate = Math.round(g.errors / g.total * 100);
3496
- if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
3497
- insights.push({
3498
- severity: "critical",
3499
- type: "error-hotspot",
3500
- title: "Error Hotspot",
3501
- desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
3502
- hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
3503
- nav: "requests"
3504
- });
3505
- }
3506
- }
3507
- return insights;
3508
- }
3509
- };
3510
- }
3511
- });
3512
-
3513
- // src/analysis/insights/rules/duplicate.ts
3514
- var duplicateRule;
3515
- var init_duplicate = __esm({
3516
- "src/analysis/insights/rules/duplicate.ts"() {
3517
- "use strict";
3518
- init_constants();
3519
- duplicateRule = {
3520
- id: "duplicate",
3521
- check(ctx) {
3522
- const dupCounts = /* @__PURE__ */ new Map();
3523
- const flowCount = /* @__PURE__ */ new Map();
3524
- for (const flow of ctx.flows) {
3525
- if (!flow.requests) continue;
3526
- const seenInFlow = /* @__PURE__ */ new Set();
3527
- for (const fr of flow.requests) {
3528
- if (!fr.isDuplicate) continue;
3529
- const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
3530
- dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
3531
- if (!seenInFlow.has(dupKey)) {
3532
- seenInFlow.add(dupKey);
3533
- flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
3534
- }
3535
- }
3536
- }
3537
- const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
3538
- const insights = [];
3539
- for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
3540
- const d = dupEntries[i];
3541
- insights.push({
3542
- severity: "warning",
3543
- type: "duplicate",
3544
- title: "Duplicate API Call",
3545
- desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
3546
- 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.",
3547
- nav: "actions"
3548
- });
3549
- }
3550
- return insights;
3551
- }
3552
- };
3553
- }
3554
- });
3555
-
3556
- // src/utils/format.ts
3557
- function formatDuration(ms) {
3558
- if (ms < 1e3) return `${ms}ms`;
3559
- return `${(ms / 1e3).toFixed(1)}s`;
3560
- }
3561
- function formatSize(bytes) {
3562
- if (bytes < 1024) return `${bytes}B`;
3563
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3564
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
3565
- }
3566
- function pct(part, total) {
3567
- return total > 0 ? Math.round(part / total * 100) : 0;
3568
- }
3569
- var init_format = __esm({
3570
- "src/utils/format.ts"() {
3571
- "use strict";
3572
- }
3573
- });
3574
-
3575
- // src/analysis/insights/rules/slow.ts
3576
- var slowRule;
3577
- var init_slow = __esm({
3578
- "src/analysis/insights/rules/slow.ts"() {
3579
- "use strict";
3580
- init_format();
3581
- init_constants();
3582
- slowRule = {
3583
- id: "slow",
3584
- check(ctx) {
3585
- const insights = [];
3586
- for (const [ep, g] of ctx.endpointGroups) {
3587
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3588
- const avgMs = Math.round(g.totalDuration / g.total);
3589
- if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
3590
- const avgQueryMs = Math.round(g.totalQueryTimeMs / g.total);
3591
- const avgFetchMs = Math.round(g.totalFetchTimeMs / g.total);
3592
- const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
3593
- const parts = [];
3594
- if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
3595
- if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
3596
- if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
3597
- const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
3598
- let detail;
3599
- let slowestMs = 0;
3600
- for (const [, sd] of g.queryShapeDurations) {
3601
- const avgShapeMs = sd.totalMs / sd.count;
3602
- if (avgShapeMs > slowestMs) {
3603
- slowestMs = avgShapeMs;
3604
- detail = `Slowest query: ${sd.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${sd.count}x)`;
3605
- }
3606
- }
3607
- insights.push({
3608
- severity: "warning",
3609
- type: "slow",
3610
- title: "Slow Endpoint",
3611
- desc: `${ep} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
3612
- 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.",
3613
- detail,
3614
- nav: "requests"
3615
- });
3616
- }
3617
- return insights;
3618
- }
3619
- };
3620
- }
3621
- });
3622
-
3623
- // src/analysis/insights/rules/query-heavy.ts
3624
- var queryHeavyRule;
3625
- var init_query_heavy = __esm({
3626
- "src/analysis/insights/rules/query-heavy.ts"() {
3627
- "use strict";
3628
- init_constants();
3629
- queryHeavyRule = {
3630
- id: "query-heavy",
3631
- check(ctx) {
3632
- const insights = [];
3633
- for (const [ep, g] of ctx.endpointGroups) {
3634
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3635
- const avgQueries = Math.round(g.queryCount / g.total);
3636
- if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
3637
- insights.push({
3638
- severity: "warning",
3639
- type: "query-heavy",
3640
- title: "Query-Heavy Endpoint",
3641
- desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
3642
- hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
3643
- nav: "queries"
3644
- });
3645
- }
3646
- }
3647
- return insights;
3648
- }
3649
- };
3650
- }
3651
- });
3652
-
3653
- // src/analysis/insights/rules/select-star.ts
3654
- var selectStarRule;
3655
- var init_select_star = __esm({
3656
- "src/analysis/insights/rules/select-star.ts"() {
3657
- "use strict";
3658
- init_query_helpers();
3659
- init_constants();
3660
- init_patterns();
3661
3289
  selectStarRule = {
3662
3290
  id: "select-star",
3663
3291
  check(ctx) {
3664
3292
  const seen = /* @__PURE__ */ new Map();
3665
3293
  for (const [, reqQueries] of ctx.queriesByReq) {
3666
- for (const q of reqQueries) {
3667
- if (!q.sql) continue;
3668
- const isSelectStar = SELECT_STAR_RE.test(q.sql.trim()) || SELECT_DOT_STAR_RE.test(q.sql);
3294
+ for (const query of reqQueries) {
3295
+ if (!query.sql) continue;
3296
+ const isSelectStar = SELECT_STAR_RE.test(query.sql.trim()) || SELECT_DOT_STAR_RE.test(query.sql);
3669
3297
  if (!isSelectStar) continue;
3670
- const info = getQueryInfo(q);
3298
+ const info = getQueryInfo(query);
3671
3299
  const key = info.table || "unknown";
3672
3300
  seen.set(key, (seen.get(key) ?? 0) + 1);
3673
3301
  }
@@ -3687,24 +3315,14 @@ var init_select_star = __esm({
3687
3315
  return insights;
3688
3316
  }
3689
3317
  };
3690
- }
3691
- });
3692
-
3693
- // src/analysis/insights/rules/high-rows.ts
3694
- var highRowsRule;
3695
- var init_high_rows = __esm({
3696
- "src/analysis/insights/rules/high-rows.ts"() {
3697
- "use strict";
3698
- init_query_helpers();
3699
- init_constants();
3700
3318
  highRowsRule = {
3701
3319
  id: "high-rows",
3702
3320
  check(ctx) {
3703
3321
  const seen = /* @__PURE__ */ new Map();
3704
3322
  for (const [, reqQueries] of ctx.queriesByReq) {
3705
- for (const q of reqQueries) {
3706
- if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
3707
- const info = getQueryInfo(q);
3323
+ for (const query of reqQueries) {
3324
+ if (!query.rowCount || query.rowCount <= HIGH_ROW_COUNT) continue;
3325
+ const info = getQueryInfo(query);
3708
3326
  const key = `${info.op} ${info.table || "unknown"}`;
3709
3327
  let entry = seen.get(key);
3710
3328
  if (!entry) {
@@ -3712,7 +3330,7 @@ var init_high_rows = __esm({
3712
3330
  seen.set(key, entry);
3713
3331
  }
3714
3332
  entry.count++;
3715
- if (q.rowCount > entry.max) entry.max = q.rowCount;
3333
+ if (query.rowCount > entry.max) entry.max = query.rowCount;
3716
3334
  }
3717
3335
  }
3718
3336
  const insights = [];
@@ -3730,32 +3348,76 @@ var init_high_rows = __esm({
3730
3348
  return insights;
3731
3349
  }
3732
3350
  };
3351
+ queryHeavyRule = {
3352
+ id: "query-heavy",
3353
+ check(ctx) {
3354
+ const insights = [];
3355
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3356
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3357
+ const avgQueries = Math.round(group.queryCount / group.total);
3358
+ if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
3359
+ insights.push({
3360
+ severity: "warning",
3361
+ type: "query-heavy",
3362
+ title: "Query-Heavy Endpoint",
3363
+ desc: `${endpointKey} \u2014 avg ${avgQueries} queries/request`,
3364
+ hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
3365
+ nav: "queries"
3366
+ });
3367
+ }
3368
+ }
3369
+ return insights;
3370
+ }
3371
+ };
3372
+ }
3373
+ });
3374
+
3375
+ // src/utils/format.ts
3376
+ function formatDuration(ms) {
3377
+ if (ms < 1e3) return `${ms}ms`;
3378
+ return `${(ms / 1e3).toFixed(1)}s`;
3379
+ }
3380
+ function formatSize(bytes) {
3381
+ if (bytes < 1024) return `${bytes}B`;
3382
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3383
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
3384
+ }
3385
+ function pct(part, total) {
3386
+ return total > 0 ? Math.round(part / total * 100) : 0;
3387
+ }
3388
+ var init_format = __esm({
3389
+ "src/utils/format.ts"() {
3390
+ "use strict";
3733
3391
  }
3734
3392
  });
3735
3393
 
3736
- // src/analysis/insights/rules/response-overfetch.ts
3737
- var responseOverfetchRule;
3738
- var init_response_overfetch = __esm({
3739
- "src/analysis/insights/rules/response-overfetch.ts"() {
3394
+ // src/analysis/insights/rules/response-rules.ts
3395
+ var responseOverfetchRule, largeResponseRule;
3396
+ var init_response_rules = __esm({
3397
+ "src/analysis/insights/rules/response-rules.ts"() {
3740
3398
  "use strict";
3741
3399
  init_endpoint();
3742
3400
  init_response();
3743
3401
  init_http_status();
3402
+ init_format();
3744
3403
  init_patterns();
3404
+ init_log();
3405
+ init_type_guards();
3745
3406
  init_constants();
3746
3407
  responseOverfetchRule = {
3747
3408
  id: "response-overfetch",
3748
3409
  check(ctx) {
3749
3410
  const insights = [];
3750
3411
  const seen = /* @__PURE__ */ new Set();
3751
- for (const r of ctx.nonStatic) {
3752
- if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
3753
- const ep = getEndpointKey(r.method, r.path);
3754
- if (seen.has(ep)) continue;
3412
+ for (const request of ctx.nonStatic) {
3413
+ if (isErrorStatus(request.statusCode) || !request.responseBody) continue;
3414
+ const endpointKey = getEndpointKey(request.method, request.path);
3415
+ if (seen.has(endpointKey)) continue;
3755
3416
  let parsed;
3756
3417
  try {
3757
- parsed = JSON.parse(r.responseBody);
3758
- } catch {
3418
+ parsed = JSON.parse(request.responseBody);
3419
+ } catch (e) {
3420
+ brakitDebug(`json parse: ${getErrorMessage(e)}`);
3759
3421
  continue;
3760
3422
  }
3761
3423
  const target = unwrapResponse(parsed);
@@ -3778,12 +3440,12 @@ var init_response_overfetch = __esm({
3778
3440
  reasons.push(`${fields.length} fields returned`);
3779
3441
  }
3780
3442
  if (reasons.length > 0) {
3781
- seen.add(ep);
3443
+ seen.add(endpointKey);
3782
3444
  insights.push({
3783
3445
  severity: "info",
3784
3446
  type: "response-overfetch",
3785
3447
  title: "Response Overfetch",
3786
- desc: `${ep} \u2014 ${reasons.join(", ")}`,
3448
+ desc: `${endpointKey} \u2014 ${reasons.join(", ")}`,
3787
3449
  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.",
3788
3450
  nav: "requests"
3789
3451
  });
@@ -3792,29 +3454,19 @@ var init_response_overfetch = __esm({
3792
3454
  return insights;
3793
3455
  }
3794
3456
  };
3795
- }
3796
- });
3797
-
3798
- // src/analysis/insights/rules/large-response.ts
3799
- var largeResponseRule;
3800
- var init_large_response = __esm({
3801
- "src/analysis/insights/rules/large-response.ts"() {
3802
- "use strict";
3803
- init_format();
3804
- init_constants();
3805
3457
  largeResponseRule = {
3806
3458
  id: "large-response",
3807
3459
  check(ctx) {
3808
3460
  const insights = [];
3809
- for (const [ep, g] of ctx.endpointGroups) {
3810
- if (g.total < OVERFETCH_MIN_REQUESTS) continue;
3811
- const avgSize = Math.round(g.totalSize / g.total);
3461
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3462
+ if (group.total < OVERFETCH_MIN_REQUESTS) continue;
3463
+ const avgSize = Math.round(group.totalSize / group.total);
3812
3464
  if (avgSize > LARGE_RESPONSE_BYTES) {
3813
3465
  insights.push({
3814
3466
  severity: "info",
3815
3467
  type: "large-response",
3816
3468
  title: "Large Response",
3817
- desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
3469
+ desc: `${endpointKey} \u2014 avg ${formatSize(avgSize)} response`,
3818
3470
  hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
3819
3471
  nav: "requests"
3820
3472
  });
@@ -3826,13 +3478,57 @@ var init_large_response = __esm({
3826
3478
  }
3827
3479
  });
3828
3480
 
3829
- // src/analysis/insights/rules/regression.ts
3830
- var regressionRule;
3831
- var init_regression = __esm({
3832
- "src/analysis/insights/rules/regression.ts"() {
3481
+ // src/analysis/insights/rules/reliability-rules.ts
3482
+ var errorRule, errorHotspotRule, regressionRule, slowRule;
3483
+ var init_reliability_rules = __esm({
3484
+ "src/analysis/insights/rules/reliability-rules.ts"() {
3833
3485
  "use strict";
3834
3486
  init_format();
3835
3487
  init_constants();
3488
+ errorRule = {
3489
+ id: "error",
3490
+ check(ctx) {
3491
+ if (ctx.errors.length === 0) return [];
3492
+ const insights = [];
3493
+ const groups = /* @__PURE__ */ new Map();
3494
+ for (const error of ctx.errors) {
3495
+ const name = error.name || "Error";
3496
+ groups.set(name, (groups.get(name) ?? 0) + 1);
3497
+ }
3498
+ for (const [name, cnt] of groups) {
3499
+ insights.push({
3500
+ severity: "critical",
3501
+ type: "error",
3502
+ title: "Unhandled Error",
3503
+ desc: `${name} \u2014 occurred ${cnt} time${cnt !== 1 ? "s" : ""}`,
3504
+ hint: "Unhandled errors crash request handlers. Wrap async code in try/catch or add error-handling middleware.",
3505
+ nav: "errors"
3506
+ });
3507
+ }
3508
+ return insights;
3509
+ }
3510
+ };
3511
+ errorHotspotRule = {
3512
+ id: "error-hotspot",
3513
+ check(ctx) {
3514
+ const insights = [];
3515
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3516
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3517
+ const errorRate = Math.round(group.errors / group.total * 100);
3518
+ if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
3519
+ insights.push({
3520
+ severity: "critical",
3521
+ type: "error-hotspot",
3522
+ title: "Error Hotspot",
3523
+ desc: `${endpointKey} \u2014 ${errorRate}% error rate (${group.errors}/${group.total} requests)`,
3524
+ hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
3525
+ nav: "requests"
3526
+ });
3527
+ }
3528
+ }
3529
+ return insights;
3530
+ }
3531
+ };
3836
3532
  regressionRule = {
3837
3533
  id: "regression",
3838
3534
  check(ctx) {
@@ -3869,6 +3565,136 @@ var init_regression = __esm({
3869
3565
  return insights;
3870
3566
  }
3871
3567
  };
3568
+ slowRule = {
3569
+ id: "slow",
3570
+ check(ctx) {
3571
+ const insights = [];
3572
+ for (const [endpointKey, group] of ctx.endpointGroups) {
3573
+ if (group.total < MIN_REQUESTS_FOR_INSIGHT) continue;
3574
+ const avgMs = Math.round(group.totalDuration / group.total);
3575
+ if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
3576
+ const avgQueryMs = Math.round(group.totalQueryTimeMs / group.total);
3577
+ const avgFetchMs = Math.round(group.totalFetchTimeMs / group.total);
3578
+ const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
3579
+ const parts = [];
3580
+ if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
3581
+ if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
3582
+ if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
3583
+ const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
3584
+ let detail;
3585
+ let slowestMs = 0;
3586
+ for (const [, shapeDuration] of group.queryShapeDurations) {
3587
+ const avgShapeMs = shapeDuration.totalMs / shapeDuration.count;
3588
+ if (avgShapeMs > slowestMs) {
3589
+ slowestMs = avgShapeMs;
3590
+ detail = `Slowest query: ${shapeDuration.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${shapeDuration.count}x)`;
3591
+ }
3592
+ }
3593
+ insights.push({
3594
+ severity: "warning",
3595
+ type: "slow",
3596
+ title: "Slow Endpoint",
3597
+ desc: `${endpointKey} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
3598
+ 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.",
3599
+ detail,
3600
+ nav: "requests"
3601
+ });
3602
+ }
3603
+ return insights;
3604
+ }
3605
+ };
3606
+ }
3607
+ });
3608
+
3609
+ // src/analysis/insights/rules/pattern-rules.ts
3610
+ var duplicateRule, crossEndpointRule;
3611
+ var init_pattern_rules = __esm({
3612
+ "src/analysis/insights/rules/pattern-rules.ts"() {
3613
+ "use strict";
3614
+ init_query_helpers();
3615
+ init_endpoint();
3616
+ init_constants();
3617
+ duplicateRule = {
3618
+ id: "duplicate",
3619
+ check(ctx) {
3620
+ const dupCounts = /* @__PURE__ */ new Map();
3621
+ const flowCount = /* @__PURE__ */ new Map();
3622
+ for (const flow of ctx.flows) {
3623
+ if (!flow.requests) continue;
3624
+ const seenInFlow = /* @__PURE__ */ new Set();
3625
+ for (const request of flow.requests) {
3626
+ if (!request.isDuplicate) continue;
3627
+ const deduplicationKey = `${request.method} ${request.label ?? request.path ?? request.url}`;
3628
+ dupCounts.set(deduplicationKey, (dupCounts.get(deduplicationKey) ?? 0) + 1);
3629
+ if (!seenInFlow.has(deduplicationKey)) {
3630
+ seenInFlow.add(deduplicationKey);
3631
+ flowCount.set(deduplicationKey, (flowCount.get(deduplicationKey) ?? 0) + 1);
3632
+ }
3633
+ }
3634
+ }
3635
+ const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
3636
+ const insights = [];
3637
+ for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
3638
+ const duplicate = dupEntries[i];
3639
+ insights.push({
3640
+ severity: "warning",
3641
+ type: "duplicate",
3642
+ title: "Duplicate API Call",
3643
+ desc: `${duplicate.key} loaded ${duplicate.count}x as duplicate across ${duplicate.flows} action${duplicate.flows !== 1 ? "s" : ""}`,
3644
+ 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.",
3645
+ nav: "actions"
3646
+ });
3647
+ }
3648
+ return insights;
3649
+ }
3650
+ };
3651
+ crossEndpointRule = {
3652
+ id: "cross-endpoint",
3653
+ check(ctx) {
3654
+ const insights = [];
3655
+ const queryMap = /* @__PURE__ */ new Map();
3656
+ const allEndpoints = /* @__PURE__ */ new Set();
3657
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
3658
+ const req = ctx.reqById.get(reqId);
3659
+ if (!req) continue;
3660
+ const endpoint = getEndpointKey(req.method, req.path);
3661
+ allEndpoints.add(endpoint);
3662
+ const seenInReq = /* @__PURE__ */ new Set();
3663
+ for (const query of reqQueries) {
3664
+ const shape = getQueryShape(query);
3665
+ let entry = queryMap.get(shape);
3666
+ if (!entry) {
3667
+ entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: query };
3668
+ queryMap.set(shape, entry);
3669
+ }
3670
+ entry.count++;
3671
+ if (!seenInReq.has(shape)) {
3672
+ seenInReq.add(shape);
3673
+ entry.endpoints.add(endpoint);
3674
+ }
3675
+ }
3676
+ }
3677
+ if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
3678
+ for (const [, queryMetric] of queryMap) {
3679
+ if (queryMetric.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
3680
+ if (queryMetric.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
3681
+ const coveragePct = Math.round(queryMetric.endpoints.size / allEndpoints.size * 100);
3682
+ if (coveragePct < CROSS_ENDPOINT_PCT) continue;
3683
+ const info = getQueryInfo(queryMetric.first);
3684
+ const label = info.op + (info.table ? ` ${info.table}` : "");
3685
+ insights.push({
3686
+ severity: "warning",
3687
+ type: "cross-endpoint",
3688
+ title: "Repeated Query Across Endpoints",
3689
+ desc: `${label} runs on ${queryMetric.endpoints.size} of ${allEndpoints.size} endpoints (${coveragePct}%).`,
3690
+ hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
3691
+ nav: "queries"
3692
+ });
3693
+ }
3694
+ }
3695
+ return insights;
3696
+ }
3697
+ };
3872
3698
  }
3873
3699
  });
3874
3700
 
@@ -3898,19 +3724,10 @@ var init_security = __esm({
3898
3724
  var init_rules2 = __esm({
3899
3725
  "src/analysis/insights/rules/index.ts"() {
3900
3726
  "use strict";
3901
- init_n1();
3902
- init_cross_endpoint();
3903
- init_redundant_query();
3904
- init_error();
3905
- init_error_hotspot();
3906
- init_duplicate();
3907
- init_slow();
3908
- init_query_heavy();
3909
- init_select_star();
3910
- init_high_rows();
3911
- init_response_overfetch();
3912
- init_large_response();
3913
- init_regression();
3727
+ init_query_rules();
3728
+ init_response_rules();
3729
+ init_reliability_rules();
3730
+ init_pattern_rules();
3914
3731
  init_security();
3915
3732
  }
3916
3733
  });
@@ -3997,7 +3814,7 @@ var AnalysisEngine;
3997
3814
  var init_engine = __esm({
3998
3815
  "src/analysis/engine.ts"() {
3999
3816
  "use strict";
4000
- init_limits();
3817
+ init_config();
4001
3818
  init_disposable();
4002
3819
  init_group();
4003
3820
  init_rules();
@@ -4006,8 +3823,8 @@ var init_engine = __esm({
4006
3823
  init_issue_id();
4007
3824
  init_prepare();
4008
3825
  AnalysisEngine = class {
4009
- constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4010
- this.registry = registry;
3826
+ constructor(services, debounceMs = ANALYSIS_DEBOUNCE_MS) {
3827
+ this.services = services;
4011
3828
  this.debounceMs = debounceMs;
4012
3829
  this.cachedInsights = [];
4013
3830
  this.cachedFindings = [];
@@ -4016,7 +3833,7 @@ var init_engine = __esm({
4016
3833
  this.scanner = createDefaultScanner();
4017
3834
  }
4018
3835
  start() {
4019
- const bus = this.registry.get("event-bus");
3836
+ const bus = this.services.bus;
4020
3837
  this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4021
3838
  this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4022
3839
  this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
@@ -4043,12 +3860,12 @@ var init_engine = __esm({
4043
3860
  }, this.debounceMs);
4044
3861
  }
4045
3862
  recompute() {
4046
- const allRequests = this.registry.get("request-store").getAll();
4047
- const queries = this.registry.get("query-store").getAll();
4048
- const errors = this.registry.get("error-store").getAll();
4049
- const logs = this.registry.get("log-store").getAll();
4050
- const fetches = this.registry.get("fetch-store").getAll();
4051
- const requests = windowByEndpoint(allRequests);
3863
+ const allRequests = this.services.requestStore.getAll();
3864
+ const queries = this.services.queryStore.getAll();
3865
+ const errors = this.services.errorStore.getAll();
3866
+ const logs = this.services.logStore.getAll();
3867
+ const fetches = this.services.fetchStore.getAll();
3868
+ const requests = keepRecentPerEndpoint(allRequests);
4052
3869
  const flows = groupRequestsIntoFlows(requests);
4053
3870
  this.cachedFindings = this.scanner.scan({ requests, logs });
4054
3871
  this.cachedInsights = computeInsights({
@@ -4057,33 +3874,29 @@ var init_engine = __esm({
4057
3874
  errors,
4058
3875
  flows,
4059
3876
  fetches,
4060
- previousMetrics: this.registry.get("metrics-store").getAll(),
3877
+ previousMetrics: this.services.metricsStore.getAll(),
4061
3878
  securityFindings: this.cachedFindings
4062
3879
  });
4063
- if (this.registry.has("issue-store")) {
4064
- const issueStore = this.registry.get("issue-store");
4065
- for (const finding of this.cachedFindings) {
4066
- issueStore.upsert(securityFindingToIssue(finding), "passive");
4067
- }
4068
- for (const insight of this.cachedInsights) {
4069
- issueStore.upsert(insightToIssue(insight), "passive");
4070
- }
4071
- const currentIssueIds = /* @__PURE__ */ new Set();
4072
- for (const finding of this.cachedFindings) {
4073
- currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
4074
- }
4075
- for (const insight of this.cachedInsights) {
4076
- currentIssueIds.add(computeIssueId(insightToIssue(insight)));
4077
- }
4078
- const activeEndpoints = extractActiveEndpoints(allRequests);
4079
- issueStore.reconcile(currentIssueIds, activeEndpoints);
4080
- const update = {
4081
- insights: this.cachedInsights,
4082
- findings: this.cachedFindings,
4083
- issues: issueStore.getAll()
4084
- };
4085
- this.registry.get("event-bus").emit("analysis:updated", update);
3880
+ const issueStore = this.services.issueStore;
3881
+ const currentIssueIds = /* @__PURE__ */ new Set();
3882
+ for (const finding of this.cachedFindings) {
3883
+ const issue = securityFindingToIssue(finding);
3884
+ issueStore.upsert(issue, "passive");
3885
+ currentIssueIds.add(computeIssueId(issue));
4086
3886
  }
3887
+ for (const insight of this.cachedInsights) {
3888
+ const issue = insightToIssue(insight);
3889
+ issueStore.upsert(issue, "passive");
3890
+ currentIssueIds.add(computeIssueId(issue));
3891
+ }
3892
+ const activeEndpoints = extractActiveEndpoints(allRequests);
3893
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
3894
+ const update = {
3895
+ insights: this.cachedInsights,
3896
+ findings: this.cachedFindings,
3897
+ issues: issueStore.getAll()
3898
+ };
3899
+ this.services.bus.emit("analysis:updated", update);
4087
3900
  }
4088
3901
  };
4089
3902
  }
@@ -4101,7 +3914,7 @@ var init_src = __esm({
4101
3914
  init_engine();
4102
3915
  init_insights2();
4103
3916
  init_insights();
4104
- VERSION = "0.8.7";
3917
+ VERSION = "9.0.0";
4105
3918
  }
4106
3919
  });
4107
3920
 
@@ -4121,6 +3934,7 @@ function getBaseStyles() {
4121
3934
  --red:#dc2626;
4122
3935
  --cyan:#0891b2;
4123
3936
  --green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
3937
+ --amber-bg:rgba(217,119,6,0.07);--red-bg:rgba(220,38,38,0.07);--blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
4124
3938
  --sidebar-width:232px;--header-height:52px;
4125
3939
  --radius:8px;--radius-sm:6px;
4126
3940
  --shadow-sm:0 1px 2px rgba(0,0,0,0.05);
@@ -4243,8 +4057,8 @@ function getFlowStyles() {
4243
4057
  .flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
4244
4058
  .flow-badge-pill{font-size:11px;flex-shrink:0;font-family:var(--mono);font-weight:600;padding:2px 10px;border-radius:10px;text-align:center}
4245
4059
  .flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
4246
- .flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
4247
- .flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
4060
+ .flow-badge-pill.badge-warn{background:var(--amber-bg);color:var(--amber)}
4061
+ .flow-badge-pill.badge-error{background:var(--red-bg);color:var(--red)}
4248
4062
  .flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
4249
4063
 
4250
4064
  /* Flow expand panel */
@@ -4263,23 +4077,23 @@ function getFlowStyles() {
4263
4077
  /* Method badges */
4264
4078
  .method-badge{display:inline-flex;align-items:center;justify-content:center;padding:3px 8px;border-radius:5px;font-size:10px;font-weight:700;font-family:var(--mono);letter-spacing:.3px;flex-shrink:0}
4265
4079
  .method-badge-GET{background:var(--green-bg);color:var(--green)}
4266
- .method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
4267
- .method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
4268
- .method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
4080
+ .method-badge-POST{background:var(--blue-bg);color:var(--blue)}
4081
+ .method-badge-PUT,.method-badge-PATCH{background:var(--amber-bg);color:var(--amber)}
4082
+ .method-badge-DELETE{background:var(--red-bg);color:var(--red)}
4269
4083
  .method-badge-HEAD,.method-badge-OPTIONS{background:var(--bg-muted);color:var(--text-muted)}
4270
4084
 
4271
4085
  /* Status pills */
4272
4086
  .status-pill{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:600;font-family:var(--mono);flex-shrink:0}
4273
4087
  .status-pill-2xx{background:var(--green-bg);color:var(--green)}
4274
- .status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
4275
- .status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
4276
- .status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
4088
+ .status-pill-3xx{background:var(--cyan-bg);color:var(--cyan)}
4089
+ .status-pill-4xx{background:var(--amber-bg);color:var(--amber)}
4090
+ .status-pill-5xx{background:var(--red-bg);color:var(--red)}
4277
4091
 
4278
4092
  .traffic-card-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;font-size:13px}
4279
4093
  .traffic-card-path.is-dup{color:var(--text-muted);font-weight:400}
4280
4094
  .traffic-card-dur{color:var(--text-muted);font-size:12px;flex-shrink:0}
4281
4095
  .traffic-card-size{color:var(--text-muted);font-size:11px;flex-shrink:0}
4282
- .traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
4096
+ .traffic-card-dup{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
4283
4097
 
4284
4098
  /* Body toggles */
4285
4099
  .traffic-body{padding:0;margin-top:8px}
@@ -4308,7 +4122,7 @@ function getFlowStyles() {
4308
4122
  .flow-subreq .subreq-label.is-dup{color:var(--text-muted);font-weight:400}
4309
4123
  .flow-subreq .subreq-status{flex-shrink:0}
4310
4124
  .flow-subreq .subreq-dur{color:var(--text-muted);font-size:12px;text-align:right;flex-shrink:0}
4311
- .flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:rgba(217,119,6,0.07);padding:1px 7px;border-radius:4px}
4125
+ .flow-subreq .subreq-dup-tag{font-size:10px;color:var(--amber);flex-shrink:0;font-weight:600;background:var(--amber-bg);padding:1px 7px;border-radius:4px}
4312
4126
  .flow-subreq-detail{display:none;padding:12px 0;border-bottom:1px solid var(--border-subtle)}
4313
4127
  .flow-subreq-detail.open{display:block}
4314
4128
 
@@ -4337,6 +4151,41 @@ function getFlowStyles() {
4337
4151
  /* Strict Mode duplicate banner */
4338
4152
  .strict-mode-dupe{opacity:0.55}
4339
4153
  .strict-mode-banner{font-size:11px;color:var(--text-muted);padding:6px 0 0;font-family:var(--mono)}
4154
+
4155
+ /* Flow detail tabs */
4156
+ .flow-detail-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
4157
+ .flow-tab{padding:8px 16px;font-size:12px;font-family:var(--mono);font-weight:600;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;letter-spacing:.3px}
4158
+ .flow-tab:hover{color:var(--text)}
4159
+ .flow-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
4160
+
4161
+ /* Waterfall chart \u2014 request bars on time axis, sub-events as text rows */
4162
+ .flow-waterfall{padding:0;font-family:var(--mono);font-size:11px}
4163
+ .wf-time-axis{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);padding:0 0 6px;margin-left:180px;margin-right:56px;border-bottom:1px solid var(--border);margin-bottom:2px}
4164
+ .wf-rows{display:flex;flex-direction:column;gap:0}
4165
+
4166
+ /* Request group \u2014 request bar + its sub-events */
4167
+ .wf-request-group{border-bottom:1px solid var(--border-subtle);padding:2px 0}
4168
+ .wf-request-group:last-child{border-bottom:none}
4169
+
4170
+ /* Request row \u2014 label | bar on time axis | duration */
4171
+ .wf-req-row{display:flex;align-items:center;gap:0;height:24px;transition:background .1s}
4172
+ .wf-req-row:hover{background:var(--bg-hover)}
4173
+ .wf-req-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:500;padding:0 10px 0 0;font-size:11px}
4174
+ .wf-bar-track{flex:1;position:relative;height:14px;min-width:0;overflow:hidden}
4175
+ .wf-bar{position:absolute;top:1px;height:12px;border-radius:3px;opacity:0.8;min-width:3px}
4176
+ .wf-req-row:hover .wf-bar{opacity:1}
4177
+ .wf-req-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-muted);font-size:10px;padding-left:8px}
4178
+
4179
+ /* Sub-event rows \u2014 same layout as request rows: label | bar track | duration */
4180
+ .wf-sub-row{display:flex;align-items:center;gap:0;height:20px;transition:background .1s}
4181
+ .wf-sub-row:hover{background:var(--bg-hover)}
4182
+ .wf-sub-label{width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:10px;padding-left:14px;display:flex;align-items:center;gap:6px}
4183
+ .wf-sub-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
4184
+ .wf-sub-bar-sized{height:8px !important;top:3px !important;opacity:0.65}
4185
+ .wf-sub-row:hover .wf-sub-bar-sized{opacity:0.9}
4186
+ .wf-sub-dur{width:56px;flex-shrink:0;text-align:right;color:var(--text-dim);font-size:9px;padding-left:8px}
4187
+
4188
+ .wf-loading{color:var(--text-muted);padding:12px 0;font-size:11px;font-family:var(--mono)}
4340
4189
  `;
4341
4190
  }
4342
4191
  var init_flows = __esm({
@@ -4842,7 +4691,7 @@ function getTimelineStyles() {
4842
4691
  }
4843
4692
  `;
4844
4693
  }
4845
- var init_timeline2 = __esm({
4694
+ var init_timeline = __esm({
4846
4695
  "src/dashboard/styles/timeline.ts"() {
4847
4696
  "use strict";
4848
4697
  }
@@ -4862,7 +4711,7 @@ var init_styles = __esm({
4862
4711
  init_graph();
4863
4712
  init_overview();
4864
4713
  init_security2();
4865
- init_timeline2();
4714
+ init_timeline();
4866
4715
  }
4867
4716
  });
4868
4717
 
@@ -4957,10 +4806,10 @@ function isTelemetryEnabled() {
4957
4806
  return cachedEnabled;
4958
4807
  }
4959
4808
  var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
4960
- var init_config = __esm({
4809
+ var init_config2 = __esm({
4961
4810
  "src/telemetry/config.ts"() {
4962
4811
  "use strict";
4963
- init_network();
4812
+ init_features();
4964
4813
  CONFIG_DIR = join3(homedir2(), ".brakit");
4965
4814
  CONFIG_PATH = join3(CONFIG_DIR, "config.json");
4966
4815
  cachedEnabled = null;
@@ -5001,12 +4850,12 @@ function speedBucket(ms) {
5001
4850
  }
5002
4851
  return `>${t[t.length - 1]}ms`;
5003
4852
  }
5004
- function trackSession(registry) {
4853
+ function trackSession(services) {
5005
4854
  if (!isTelemetryEnabled()) return;
5006
4855
  const isFirstSession = readConfig() === null;
5007
4856
  const config = getOrCreateConfig();
5008
- const metricsStore = registry.get("metrics-store");
5009
- const analysisEngine = registry.get("analysis-engine");
4857
+ const metricsStore = services.metricsStore;
4858
+ const analysisEngine = services.analysisEngine;
5010
4859
  const live = metricsStore.getLiveEndpoints();
5011
4860
  const insights = analysisEngine.getInsights();
5012
4861
  const findings = analysisEngine.getFindings();
@@ -5034,9 +4883,9 @@ function trackSession(registry) {
5034
4883
  first_session: isFirstSession,
5035
4884
  adapters_detected: session.adapters,
5036
4885
  request_count: session.requestCount,
5037
- error_count: registry.get("error-store").getAll().length,
5038
- query_count: registry.get("query-store").getAll().length,
5039
- fetch_count: registry.get("fetch-store").getAll().length,
4886
+ error_count: services.errorStore.getAll().length,
4887
+ query_count: services.queryStore.getAll().length,
4888
+ fetch_count: services.fetchStore.getAll().length,
5040
4889
  insight_count: insights.length,
5041
4890
  finding_count: findings.length,
5042
4891
  insight_types: [...session.insightTypes],
@@ -5069,13 +4918,13 @@ function trackSession(registry) {
5069
4918
  }
5070
4919
  }
5071
4920
  var POSTHOG_KEY, session;
5072
- var init_telemetry2 = __esm({
4921
+ var init_telemetry = __esm({
5073
4922
  "src/telemetry/index.ts"() {
5074
4923
  "use strict";
5075
4924
  init_src();
5076
- init_config();
5077
- init_telemetry();
5078
- init_config();
4925
+ init_config2();
4926
+ init_labels();
4927
+ init_config2();
5079
4928
  POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
5080
4929
  session = {
5081
4930
  startTime: 0,
@@ -5097,32 +4946,30 @@ var init_telemetry2 = __esm({
5097
4946
  function isDashboardRequest(url) {
5098
4947
  return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
5099
4948
  }
5100
- function createDashboardHandler(registry) {
5101
- const metricsStore = registry.get("metrics-store");
4949
+ function createDashboardHandler(services) {
4950
+ const metricsStore = services.metricsStore;
5102
4951
  const routes = {
5103
- [DASHBOARD_API_REQUESTS]: createRequestsHandler(registry),
5104
- [DASHBOARD_API_EVENTS]: createSSEHandler(registry),
5105
- [DASHBOARD_API_FLOWS]: createFlowsHandler(registry),
5106
- [DASHBOARD_API_CLEAR]: createClearHandler(registry),
5107
- [DASHBOARD_API_LOGS]: createLogsHandler(registry),
5108
- [DASHBOARD_API_FETCHES]: createFetchesHandler(registry),
5109
- [DASHBOARD_API_ERRORS]: createErrorsHandler(registry),
5110
- [DASHBOARD_API_QUERIES]: createQueriesHandler(registry),
4952
+ [DASHBOARD_API_REQUESTS]: createRequestsHandler(services),
4953
+ [DASHBOARD_API_EVENTS]: createSSEHandler(services),
4954
+ [DASHBOARD_API_FLOWS]: createFlowsHandler(services),
4955
+ [DASHBOARD_API_CLEAR]: createClearHandler(services),
4956
+ [DASHBOARD_API_LOGS]: createLogsHandler(services),
4957
+ [DASHBOARD_API_FETCHES]: createFetchesHandler(services),
4958
+ [DASHBOARD_API_ERRORS]: createErrorsHandler(services),
4959
+ [DASHBOARD_API_QUERIES]: createQueriesHandler(services),
5111
4960
  [DASHBOARD_API_METRICS]: createMetricsHandler(metricsStore),
5112
4961
  [DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(metricsStore),
5113
- [DASHBOARD_API_INGEST]: createIngestHandler(registry),
5114
- [DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
4962
+ [DASHBOARD_API_INGEST]: createIngestHandler(services),
4963
+ [DASHBOARD_API_ACTIVITY]: createActivityHandler(services)
5115
4964
  };
5116
- if (registry.has("issue-store")) {
5117
- const issueStore = registry.get("issue-store");
5118
- routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
5119
- routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
5120
- routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
5121
- routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
5122
- issueStore,
5123
- registry.get("event-bus")
5124
- );
5125
- }
4965
+ const issueStore = services.issueStore;
4966
+ routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
4967
+ routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
4968
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
4969
+ routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
4970
+ issueStore,
4971
+ services.bus
4972
+ );
5126
4973
  routes[DASHBOARD_API_TAB] = (req, res) => {
5127
4974
  const raw = (req.url ?? "").split("tab=")[1];
5128
4975
  if (raw) {
@@ -5133,7 +4980,7 @@ function createDashboardHandler(registry) {
5133
4980
  res.end();
5134
4981
  };
5135
4982
  return (req, res, config) => {
5136
- const path = (req.url ?? "/").split("?")[0];
4983
+ const path = stripQueryString(req.url ?? "/");
5137
4984
  const handler = routes[path];
5138
4985
  if (handler) {
5139
4986
  handler(req, res);
@@ -5151,13 +4998,14 @@ function createDashboardHandler(registry) {
5151
4998
  var init_router = __esm({
5152
4999
  "src/dashboard/router.ts"() {
5153
5000
  "use strict";
5001
+ init_endpoint();
5154
5002
  init_constants();
5155
- init_http();
5003
+ init_labels();
5156
5004
  init_api();
5157
5005
  init_issues();
5158
5006
  init_sse();
5159
5007
  init_page();
5160
- init_telemetry2();
5008
+ init_telemetry();
5161
5009
  }
5162
5010
  });
5163
5011
 
@@ -5198,51 +5046,6 @@ var init_event_bus = __esm({
5198
5046
  }
5199
5047
  });
5200
5048
 
5201
- // src/core/service-registry.ts
5202
- var ServiceRegistry;
5203
- var init_service_registry = __esm({
5204
- "src/core/service-registry.ts"() {
5205
- "use strict";
5206
- ServiceRegistry = class {
5207
- constructor() {
5208
- this.services = /* @__PURE__ */ new Map();
5209
- }
5210
- register(name, service) {
5211
- this.services.set(name, service);
5212
- }
5213
- get(name) {
5214
- const service = this.services.get(name);
5215
- if (!service) throw new Error(`Service "${name}" not registered`);
5216
- return service;
5217
- }
5218
- has(name) {
5219
- return this.services.has(name);
5220
- }
5221
- };
5222
- }
5223
- });
5224
-
5225
- // src/utils/static-patterns.ts
5226
- function isStaticPath(urlPath) {
5227
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
5228
- }
5229
- var STATIC_PATTERNS;
5230
- var init_static_patterns = __esm({
5231
- "src/utils/static-patterns.ts"() {
5232
- "use strict";
5233
- STATIC_PATTERNS = [
5234
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
5235
- /^\/favicon/,
5236
- /^\/node_modules\//,
5237
- // Framework-specific static/internal paths
5238
- /^\/_next\//,
5239
- /^\/__nextjs/,
5240
- /^\/@vite\//,
5241
- /^\/__vite/
5242
- ];
5243
- }
5244
- });
5245
-
5246
5049
  // src/store/request-store.ts
5247
5050
  function flattenHeaders(headers2) {
5248
5051
  const flat = {};
@@ -5258,6 +5061,7 @@ var init_request_store = __esm({
5258
5061
  "use strict";
5259
5062
  init_constants();
5260
5063
  init_static_patterns();
5064
+ init_endpoint();
5261
5065
  RequestStore = class {
5262
5066
  constructor(maxEntries = MAX_REQUEST_ENTRIES) {
5263
5067
  this.maxEntries = maxEntries;
@@ -5266,7 +5070,7 @@ var init_request_store = __esm({
5266
5070
  }
5267
5071
  capture(input) {
5268
5072
  const url = input.url;
5269
- const path = url.split("?")[0];
5073
+ const path = stripQueryString(url);
5270
5074
  let requestBodyStr = null;
5271
5075
  if (input.requestBody && input.requestBody.length > 0) {
5272
5076
  requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
@@ -5291,7 +5095,8 @@ var init_request_store = __esm({
5291
5095
  startedAt: input.startTime,
5292
5096
  durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
5293
5097
  responseSize: input.responseBody?.length ?? 0,
5294
- isStatic: isStaticPath(path)
5098
+ isStatic: isStaticPath(path),
5099
+ isHealthCheck: isHealthCheckPath(path)
5295
5100
  };
5296
5101
  this.requests.push(entry);
5297
5102
  if (this.requests.length > this.maxEntries) {
@@ -5368,50 +5173,6 @@ var init_telemetry_store = __esm({
5368
5173
  }
5369
5174
  });
5370
5175
 
5371
- // src/store/fetch-store.ts
5372
- var FetchStore;
5373
- var init_fetch_store = __esm({
5374
- "src/store/fetch-store.ts"() {
5375
- "use strict";
5376
- init_telemetry_store();
5377
- FetchStore = class extends TelemetryStore {
5378
- };
5379
- }
5380
- });
5381
-
5382
- // src/store/log-store.ts
5383
- var LogStore;
5384
- var init_log_store = __esm({
5385
- "src/store/log-store.ts"() {
5386
- "use strict";
5387
- init_telemetry_store();
5388
- LogStore = class extends TelemetryStore {
5389
- };
5390
- }
5391
- });
5392
-
5393
- // src/store/error-store.ts
5394
- var ErrorStore;
5395
- var init_error_store = __esm({
5396
- "src/store/error-store.ts"() {
5397
- "use strict";
5398
- init_telemetry_store();
5399
- ErrorStore = class extends TelemetryStore {
5400
- };
5401
- }
5402
- });
5403
-
5404
- // src/store/query-store.ts
5405
- var QueryStore;
5406
- var init_query_store = __esm({
5407
- "src/store/query-store.ts"() {
5408
- "use strict";
5409
- init_telemetry_store();
5410
- QueryStore = class extends TelemetryStore {
5411
- };
5412
- }
5413
- });
5414
-
5415
5176
  // src/utils/math.ts
5416
5177
  function percentile(values, p) {
5417
5178
  if (values.length === 0) return 0;
@@ -5482,7 +5243,7 @@ var init_metrics_store = __esm({
5482
5243
  this.flush(true);
5483
5244
  }
5484
5245
  recordRequest(req, metrics) {
5485
- if (req.isStatic) return;
5246
+ if (req.isStatic || req.isHealthCheck) return;
5486
5247
  this.dirty = true;
5487
5248
  const key = getEndpointKey(req.method, req.path);
5488
5249
  let acc = this.accumulators.get(key);
@@ -5710,10 +5471,6 @@ var init_store = __esm({
5710
5471
  "use strict";
5711
5472
  init_request_store();
5712
5473
  init_telemetry_store();
5713
- init_fetch_store();
5714
- init_log_store();
5715
- init_error_store();
5716
- init_query_store();
5717
5474
  init_metrics_store();
5718
5475
  init_persistence();
5719
5476
  }
@@ -5745,9 +5502,9 @@ function formatConsoleLine(issue, suffix) {
5745
5502
  }
5746
5503
  return line;
5747
5504
  }
5748
- function startTerminalInsights(registry, proxyPort) {
5749
- const bus = registry.get("event-bus");
5750
- const metricsStore = registry.get("metrics-store");
5505
+ function startTerminalInsights(services, proxyPort) {
5506
+ const bus = services.bus;
5507
+ const metricsStore = services.metricsStore;
5751
5508
  const printedKeys = /* @__PURE__ */ new Set();
5752
5509
  const resolvedKeys = /* @__PURE__ */ new Set();
5753
5510
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
@@ -5818,8 +5575,8 @@ var init_terminal = __esm({
5818
5575
  "use strict";
5819
5576
  init_src();
5820
5577
  init_constants();
5821
- init_limits();
5822
- init_severity();
5578
+ init_config();
5579
+ init_labels();
5823
5580
  SEVERITY_COLOR = {
5824
5581
  critical: pc.red,
5825
5582
  warning: pc.yellow,
@@ -5914,8 +5671,14 @@ function outgoingToIncoming(headers2) {
5914
5671
  }
5915
5672
  return result;
5916
5673
  }
5674
+ function getDecompressor(encoding) {
5675
+ if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
5676
+ if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
5677
+ if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
5678
+ return null;
5679
+ }
5917
5680
  function decompressAsync(body, encoding) {
5918
- const decompressor = encoding === CONTENT_ENCODING_GZIP ? gunzip : encoding === CONTENT_ENCODING_BR ? brotliDecompress : encoding === CONTENT_ENCODING_DEFLATE ? inflate : null;
5681
+ const decompressor = getDecompressor(encoding);
5919
5682
  if (!decompressor) return Promise.resolve(body);
5920
5683
  return new Promise((resolve6) => {
5921
5684
  decompressor(body, (err, result) => {
@@ -5929,7 +5692,7 @@ function toBuffer(chunk) {
5929
5692
  if (typeof chunk === "string") return Buffer.from(chunk);
5930
5693
  return null;
5931
5694
  }
5932
- function captureInProcess(req, res, requestId, requestStore) {
5695
+ function captureInProcess(req, res, requestId, requestStore, isChild = false) {
5933
5696
  const startTime = performance.now();
5934
5697
  const method = req.method ?? "GET";
5935
5698
  const resChunks = [];
@@ -5975,30 +5738,32 @@ function captureInProcess(req, res, requestId, requestStore) {
5975
5738
  const responseHeaders = outgoingToIncoming(res.getHeaders());
5976
5739
  const responseContentType = String(res.getHeader("content-type") ?? "");
5977
5740
  const capturedChunks = resChunks.slice();
5978
- void (async () => {
5979
- try {
5980
- let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
5981
- if (body && encoding && !truncated) {
5982
- body = await decompressAsync(body, encoding);
5741
+ if (!isChild) {
5742
+ void (async () => {
5743
+ try {
5744
+ let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
5745
+ if (body && encoding && !truncated) {
5746
+ body = await decompressAsync(body, encoding);
5747
+ }
5748
+ requestStore.capture({
5749
+ requestId,
5750
+ method,
5751
+ url: req.url ?? "/",
5752
+ requestHeaders: req.headers,
5753
+ requestBody: null,
5754
+ statusCode,
5755
+ responseHeaders,
5756
+ responseBody: body,
5757
+ responseContentType,
5758
+ startTime,
5759
+ endTime,
5760
+ config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
5761
+ });
5762
+ } catch (e) {
5763
+ brakitDebug(`capture store: ${getErrorMessage(e)}`);
5983
5764
  }
5984
- requestStore.capture({
5985
- requestId,
5986
- method,
5987
- url: req.url ?? "/",
5988
- requestHeaders: req.headers,
5989
- requestBody: null,
5990
- statusCode,
5991
- responseHeaders,
5992
- responseBody: body,
5993
- responseContentType,
5994
- startTime,
5995
- endTime,
5996
- config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
5997
- });
5998
- } catch (e) {
5999
- brakitDebug(`capture store: ${getErrorMessage(e)}`);
6000
- }
6001
- })();
5765
+ })();
5766
+ }
6002
5767
  return result;
6003
5768
  };
6004
5769
  }
@@ -6046,13 +5811,15 @@ function installInterceptor(deps) {
6046
5811
  deps.handleDashboard(req, res, deps.config);
6047
5812
  return true;
6048
5813
  }
6049
- const requestId = randomUUID8();
5814
+ const propagated = req.headers[BRAKIT_REQUEST_ID_HEADER];
5815
+ const requestId = propagated ?? randomUUID8();
5816
+ const isChild = propagated !== void 0;
6050
5817
  const ctx = {
6051
5818
  requestId,
6052
5819
  url,
6053
5820
  method: req.method ?? "GET"
6054
5821
  };
6055
- captureInProcess(req, res, requestId, deps.requestStore);
5822
+ captureInProcess(req, res, requestId, deps.requestStore, isChild);
6056
5823
  return storage.run(
6057
5824
  ctx,
6058
5825
  () => original.apply(this, [event, ...args])
@@ -6075,7 +5842,8 @@ var init_interceptor = __esm({
6075
5842
  init_safe_wrap();
6076
5843
  init_guard();
6077
5844
  init_capture();
6078
- init_http();
5845
+ init_labels();
5846
+ init_constants();
6079
5847
  originalEmit = null;
6080
5848
  }
6081
5849
  });
@@ -6093,18 +5861,12 @@ function setup() {
6093
5861
  initPromise = doSetup();
6094
5862
  return initPromise;
6095
5863
  }
6096
- function createStores(bus, registry) {
5864
+ function createStores(bus) {
6097
5865
  const requestStore = new RequestStore();
6098
- const fetchStore = new FetchStore();
6099
- const logStore = new LogStore();
6100
- const errorStore = new ErrorStore();
6101
- const queryStore = new QueryStore();
6102
- registry.register("event-bus", bus);
6103
- registry.register("request-store", requestStore);
6104
- registry.register("fetch-store", fetchStore);
6105
- registry.register("log-store", logStore);
6106
- registry.register("error-store", errorStore);
6107
- registry.register("query-store", queryStore);
5866
+ const fetchStore = new TelemetryStore();
5867
+ const logStore = new TelemetryStore();
5868
+ const errorStore = new TelemetryStore();
5869
+ const queryStore = new TelemetryStore();
6108
5870
  bus.on("telemetry:fetch", (data) => fetchStore.add(data));
6109
5871
  bus.on("telemetry:query", (data) => queryStore.add(data));
6110
5872
  bus.on("telemetry:log", (data) => logStore.add(data));
@@ -6138,17 +5900,16 @@ function installHooks(bus) {
6138
5900
  adapterNames: adapterRegistry.getActive().map((a) => a.name)
6139
5901
  };
6140
5902
  }
6141
- function startAnalysis(registry, stores, dataDir) {
6142
- const bus = registry.get("event-bus");
5903
+ function startAnalysis(bus, stores, dataDir, services) {
6143
5904
  const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
6144
5905
  metricsStore.start();
6145
- registry.register("metrics-store", metricsStore);
6146
5906
  const issueStore = new IssueStore(dataDir);
6147
5907
  issueStore.start();
6148
- registry.register("issue-store", issueStore);
6149
- const analysisEngine = new AnalysisEngine(registry);
5908
+ services.metricsStore = metricsStore;
5909
+ services.issueStore = issueStore;
5910
+ const analysisEngine = new AnalysisEngine(services);
6150
5911
  analysisEngine.start();
6151
- registry.register("analysis-engine", analysisEngine);
5912
+ services.analysisEngine = analysisEngine;
6152
5913
  bus.on("request:completed", (req) => {
6153
5914
  const queries = stores.queryStore.getByRequest(req.id);
6154
5915
  const fetches = stores.fetchStore.getByRequest(req.id);
@@ -6160,7 +5921,7 @@ function startAnalysis(registry, stores, dataDir) {
6160
5921
  });
6161
5922
  return { analysisEngine, metricsStore, issueStore };
6162
5923
  }
6163
- function registerLifecycle(registry, stores, services, cwd) {
5924
+ function registerLifecycle(allServices, stores, services, cwd) {
6164
5925
  let telemetrySent = false;
6165
5926
  const sendTelemetry = () => {
6166
5927
  if (telemetrySent) return;
@@ -6172,7 +5933,7 @@ function registerLifecycle(registry, stores, services, cwd) {
6172
5933
  recordRulesTriggered(
6173
5934
  services.analysisEngine.getFindings().map((f) => f.rule)
6174
5935
  );
6175
- trackSession(registry);
5936
+ trackSession(allServices);
6176
5937
  };
6177
5938
  let teardownCalled = false;
6178
5939
  const runTeardown = () => {
@@ -6201,20 +5962,23 @@ function registerLifecycle(registry, stores, services, cwd) {
6201
5962
  async function doSetup() {
6202
5963
  brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
6203
5964
  const bus = new EventBus();
6204
- const registry = new ServiceRegistry();
6205
5965
  const cwd = process.cwd();
6206
- const stores = createStores(bus, registry);
5966
+ const stores = createStores(bus);
5967
+ const services = {
5968
+ bus,
5969
+ ...stores
5970
+ };
6207
5971
  const { framework, adapterNames } = installHooks(bus);
6208
5972
  initSession(framework, detectPackageManagerSync(cwd), false, adapterNames);
6209
5973
  const dataDir = getProjectDataDir(cwd);
6210
- const services = startAnalysis(registry, stores, dataDir);
5974
+ const analysisServices = startAnalysis(bus, stores, dataDir, services);
6211
5975
  const config = {
6212
5976
  proxyPort: 0,
6213
5977
  targetPort: 0,
6214
5978
  showStatic: false,
6215
5979
  maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
6216
5980
  };
6217
- const handleDashboard = createDashboardHandler(registry);
5981
+ const handleDashboard = createDashboardHandler(services);
6218
5982
  installInterceptor({
6219
5983
  handleDashboard,
6220
5984
  config,
@@ -6247,14 +6011,14 @@ async function doSetup() {
6247
6011
  brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
6248
6012
  }
6249
6013
  })();
6250
- startTerminalInsights(registry, port);
6014
+ startTerminalInsights(services, port);
6251
6015
  process.stdout.write(
6252
6016
  ` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
6253
6017
  `
6254
6018
  );
6255
6019
  }
6256
6020
  });
6257
- registerLifecycle(registry, stores, services, cwd);
6021
+ registerLifecycle(services, stores, analysisServices, cwd);
6258
6022
  }
6259
6023
  var initPromise;
6260
6024
  var init_setup = __esm({
@@ -6266,12 +6030,8 @@ var init_setup = __esm({
6266
6030
  init_adapters();
6267
6031
  init_router();
6268
6032
  init_event_bus();
6269
- init_service_registry();
6270
6033
  init_request_store();
6271
- init_fetch_store();
6272
- init_log_store();
6273
- init_error_store();
6274
- init_query_store();
6034
+ init_telemetry_store();
6275
6035
  init_store();
6276
6036
  init_issue_store();
6277
6037
  init_engine();
@@ -6284,7 +6044,7 @@ var init_setup = __esm({
6284
6044
  init_type_guards();
6285
6045
  init_fs();
6286
6046
  init_project();
6287
- init_telemetry2();
6047
+ init_telemetry();
6288
6048
  initPromise = null;
6289
6049
  }
6290
6050
  });