brakit 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,28 +9,29 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/constants/routes.ts
12
- 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, VALID_TABS;
12
+ 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;
13
13
  var init_routes = __esm({
14
14
  "src/constants/routes.ts"() {
15
15
  "use strict";
16
16
  DASHBOARD_PREFIX = "/__brakit";
17
- DASHBOARD_API_REQUESTS = "/__brakit/api/requests";
18
- DASHBOARD_API_EVENTS = "/__brakit/api/events";
19
- DASHBOARD_API_FLOWS = "/__brakit/api/flows";
20
- DASHBOARD_API_CLEAR = "/__brakit/api/clear";
21
- DASHBOARD_API_LOGS = "/__brakit/api/logs";
22
- DASHBOARD_API_FETCHES = "/__brakit/api/fetches";
23
- DASHBOARD_API_ERRORS = "/__brakit/api/errors";
24
- DASHBOARD_API_QUERIES = "/__brakit/api/queries";
25
- DASHBOARD_API_INGEST = "/__brakit/api/ingest";
26
- DASHBOARD_API_METRICS = "/__brakit/api/metrics";
27
- DASHBOARD_API_ACTIVITY = "/__brakit/api/activity";
28
- DASHBOARD_API_METRICS_LIVE = "/__brakit/api/metrics/live";
29
- DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
30
- DASHBOARD_API_SECURITY = "/__brakit/api/security";
31
- DASHBOARD_API_TAB = "/__brakit/api/tab";
32
- DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
33
- VALID_TABS = /* @__PURE__ */ new Set([
17
+ DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
18
+ DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
19
+ DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
20
+ DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
21
+ DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
22
+ DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
23
+ DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
24
+ DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
25
+ DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
26
+ DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
27
+ DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
28
+ DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
29
+ DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
30
+ DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
31
+ DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
32
+ DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
33
+ DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
34
+ VALID_TABS_TUPLE = [
34
35
  "overview",
35
36
  "actions",
36
37
  "requests",
@@ -40,12 +41,13 @@ var init_routes = __esm({
40
41
  "logs",
41
42
  "performance",
42
43
  "security"
43
- ]);
44
+ ];
45
+ VALID_TABS = new Set(VALID_TABS_TUPLE);
44
46
  }
45
47
  });
46
48
 
47
49
  // src/constants/limits.ts
48
- 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;
50
+ 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;
49
51
  var init_limits = __esm({
50
52
  "src/constants/limits.ts"() {
51
53
  "use strict";
@@ -54,15 +56,31 @@ var init_limits = __esm({
54
56
  DEFAULT_API_LIMIT = 500;
55
57
  MAX_TELEMETRY_ENTRIES = 1e3;
56
58
  MAX_TAB_NAME_LENGTH = 32;
57
- MAX_INGEST_BYTES = 10 * 1024 * 1024;
59
+ MAX_INGEST_BYTES = 10485760;
58
60
  TERMINAL_TRUNCATE_LENGTH = 80;
59
61
  SENSITIVE_MASK_MIN_LENGTH = 8;
60
62
  SENSITIVE_MASK_VISIBLE_CHARS = 4;
63
+ MAX_JSON_BODY_BYTES = 65536;
64
+ ANALYSIS_DEBOUNCE_MS = 300;
65
+ ISSUE_ID_HASH_LENGTH = 16;
66
+ ISSUES_DATA_VERSION = 2;
67
+ SENSITIVE_MASK_PLACEHOLDER = "****";
68
+ PROJECT_HASH_LENGTH = 8;
69
+ SECRET_SCAN_ARRAY_LIMIT = 5;
70
+ PII_SCAN_ARRAY_LIMIT = 10;
71
+ MIN_SECRET_VALUE_LENGTH = 8;
72
+ FULL_RECORD_MIN_FIELDS = 5;
73
+ LIST_PII_MIN_ITEMS = 2;
74
+ MAX_API_LIMIT = 500;
75
+ MAX_OBJECT_SCAN_DEPTH = 5;
76
+ MAX_UNIQUE_ENDPOINTS = 500;
77
+ MAX_ACCUMULATOR_ENTRIES = 1e3;
78
+ ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
61
79
  }
62
80
  });
63
81
 
64
82
  // src/constants/thresholds.ts
65
- 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, RESOLVE_AFTER_ABSENCES, RESOLVED_INSIGHT_TTL_MS;
83
+ 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;
66
84
  var init_thresholds = __esm({
67
85
  "src/constants/thresholds.ts"() {
68
86
  "use strict";
@@ -92,9 +110,9 @@ var init_thresholds = __esm({
92
110
  OVERFETCH_MANY_FIELDS = 12;
93
111
  OVERFETCH_UNWRAP_MIN_SIZE = 3;
94
112
  MAX_DUPLICATE_INSIGHTS = 3;
95
- INSIGHT_WINDOW_PER_ENDPOINT = 2;
96
- RESOLVE_AFTER_ABSENCES = 3;
97
- RESOLVED_INSIGHT_TTL_MS = 18e5;
113
+ INSIGHT_WINDOW_PER_ENDPOINT = 20;
114
+ CLEAN_HITS_FOR_RESOLUTION = 5;
115
+ STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
98
116
  }
99
117
  });
100
118
 
@@ -104,32 +122,24 @@ var init_transport = __esm({
104
122
  "src/constants/transport.ts"() {
105
123
  "use strict";
106
124
  SSE_HEARTBEAT_INTERVAL_MS = 3e4;
107
- NOISE_HOSTS = [
108
- "registry.npmjs.org",
109
- "telemetry.nextjs.org",
110
- "vitejs.dev"
111
- ];
112
- NOISE_PATH_PATTERNS = [
113
- ".hot-update.",
114
- "__webpack",
115
- "__vite"
116
- ];
125
+ NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
126
+ NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
117
127
  }
118
128
  });
119
129
 
120
130
  // src/constants/metrics.ts
121
- var METRICS_DIR, METRICS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, PORT_FILE, FINDINGS_FILE, FINDINGS_FLUSH_INTERVAL_MS;
131
+ var METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS;
122
132
  var init_metrics = __esm({
123
133
  "src/constants/metrics.ts"() {
124
134
  "use strict";
125
135
  METRICS_DIR = ".brakit";
126
- METRICS_FILE = ".brakit/metrics.json";
136
+ METRICS_FILE = "metrics.json";
137
+ PORT_FILE = ".brakit/port";
138
+ ISSUES_FILE = "issues.json";
127
139
  METRICS_FLUSH_INTERVAL_MS = 3e4;
128
140
  METRICS_MAX_SESSIONS = 50;
129
141
  METRICS_MAX_DATA_POINTS = 200;
130
- PORT_FILE = ".brakit/port";
131
- FINDINGS_FILE = ".brakit/findings.json";
132
- FINDINGS_FLUSH_INTERVAL_MS = 1e4;
142
+ ISSUES_FLUSH_INTERVAL_MS = 1e4;
133
143
  }
134
144
  });
135
145
 
@@ -150,38 +160,38 @@ var init_headers = __esm({
150
160
  });
151
161
 
152
162
  // src/constants/network.ts
153
- var LOCALHOST_IPS, LOCALHOST_HOSTNAMES, CLOUD_SIGNALS, MAX_HEALTH_ERRORS;
163
+ var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE, DIR_MODE_OWNER_ONLY, FILE_MODE_OWNER_ONLY;
154
164
  var init_network = __esm({
155
165
  "src/constants/network.ts"() {
156
166
  "use strict";
157
- LOCALHOST_IPS = /* @__PURE__ */ new Set([
158
- "127.0.0.1",
159
- "::1",
160
- "::ffff:127.0.0.1"
161
- ]);
162
- LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set([
163
- "localhost",
164
- "127.0.0.1",
165
- "::1"
166
- ]);
167
167
  CLOUD_SIGNALS = [
168
168
  "VERCEL",
169
169
  "VERCEL_ENV",
170
170
  "NETLIFY",
171
171
  "AWS_LAMBDA_FUNCTION_NAME",
172
172
  "AWS_EXECUTION_ENV",
173
+ "ECS_CONTAINER_METADATA_URI",
173
174
  "GOOGLE_CLOUD_PROJECT",
174
175
  "GCP_PROJECT",
176
+ "K_SERVICE",
175
177
  "AZURE_FUNCTIONS_ENVIRONMENT",
178
+ "WEBSITE_SITE_NAME",
176
179
  "FLY_APP_NAME",
177
180
  "RAILWAY_ENVIRONMENT",
178
181
  "RENDER",
179
- "HEROKU",
182
+ "HEROKU_APP_NAME",
183
+ "DYNO",
184
+ "CF_INSTANCE_GUID",
180
185
  "CF_PAGES",
181
- "KUBERNETES_SERVICE_HOST",
182
- "ECS_CONTAINER_METADATA_URI"
186
+ "KUBERNETES_SERVICE_HOST"
183
187
  ];
184
188
  MAX_HEALTH_ERRORS = 10;
189
+ RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
190
+ LOCALHOST_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
191
+ LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
192
+ URL_PARSE_BASE = "http://localhost";
193
+ DIR_MODE_OWNER_ONLY = 448;
194
+ FILE_MODE_OWNER_ONLY = 384;
185
195
  }
186
196
  });
187
197
 
@@ -204,7 +214,7 @@ var init_encoding = __esm({
204
214
  });
205
215
 
206
216
  // src/constants/severity.ts
207
- var SEVERITY_ICON, SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
217
+ var SEVERITY_ICON;
208
218
  var init_severity = __esm({
209
219
  "src/constants/severity.ts"() {
210
220
  "use strict";
@@ -213,28 +223,36 @@ var init_severity = __esm({
213
223
  warning: "\u26A0",
214
224
  info: "\u2139"
215
225
  };
216
- SEVERITY_CRITICAL = "critical";
217
- SEVERITY_WARNING = "warning";
218
- SEVERITY_INFO = "info";
219
- SEVERITY_ICON_MAP = {
220
- [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
221
- [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
222
- [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
223
- };
224
226
  }
225
227
  });
226
228
 
227
229
  // src/constants/telemetry.ts
228
- var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, POSTHOG_SPAWN_TIMEOUT_MS, SIGNAL_EXIT_SIGINT, SIGNAL_EXIT_SIGTERM;
230
+ var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS;
229
231
  var init_telemetry = __esm({
230
232
  "src/constants/telemetry.ts"() {
231
233
  "use strict";
232
234
  POSTHOG_HOST = "https://us.i.posthog.com";
233
235
  POSTHOG_CAPTURE_PATH = "/i/v0/e/";
234
236
  POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
235
- POSTHOG_SPAWN_TIMEOUT_MS = 5e3;
236
- SIGNAL_EXIT_SIGINT = 130;
237
- SIGNAL_EXIT_SIGTERM = 143;
237
+ SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
238
+ }
239
+ });
240
+
241
+ // src/constants/lifecycle.ts
242
+ var VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES;
243
+ var init_lifecycle = __esm({
244
+ "src/constants/lifecycle.ts"() {
245
+ "use strict";
246
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
247
+ VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
248
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
249
+ }
250
+ });
251
+
252
+ // src/constants/cli.ts
253
+ var init_cli = __esm({
254
+ "src/constants/cli.ts"() {
255
+ "use strict";
238
256
  }
239
257
  });
240
258
 
@@ -253,6 +271,8 @@ var init_constants = __esm({
253
271
  init_encoding();
254
272
  init_severity();
255
273
  init_telemetry();
274
+ init_lifecycle();
275
+ init_cli();
256
276
  }
257
277
  });
258
278
 
@@ -419,11 +439,12 @@ function createCaptureError(emit) {
419
439
  }
420
440
  function setupErrorHook(emit) {
421
441
  const captureError = createCaptureError(emit);
422
- process.on("uncaughtException", (err) => {
442
+ const brakitExceptionHandler = (err) => {
423
443
  captureError(err);
424
- process.removeAllListeners("uncaughtException");
444
+ process.removeListener("uncaughtException", brakitExceptionHandler);
425
445
  throw err;
426
- });
446
+ };
447
+ process.on("uncaughtException", brakitExceptionHandler);
427
448
  process.on("unhandledRejection", (reason) => {
428
449
  captureError(reason);
429
450
  });
@@ -610,7 +631,10 @@ var init_pg = __esm({
610
631
  const result = saved.apply(this, args);
611
632
  if (result && typeof result.then === "function") {
612
633
  return result.then((res) => {
613
- emitQuery(res?.rowCount ?? void 0);
634
+ try {
635
+ emitQuery(res?.rowCount ?? void 0);
636
+ } catch {
637
+ }
614
638
  return res;
615
639
  });
616
640
  }
@@ -688,7 +712,10 @@ var init_mysql2 = __esm({
688
712
  const result = orig.apply(this, args);
689
713
  if (result && typeof result.then === "function") {
690
714
  return result.then((res) => {
691
- emitQuery();
715
+ try {
716
+ emitQuery();
717
+ } catch {
718
+ }
692
719
  return res;
693
720
  });
694
721
  }
@@ -803,6 +830,43 @@ var init_adapters = __esm({
803
830
  }
804
831
  });
805
832
 
833
+ // src/constants/http.ts
834
+ 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;
835
+ var init_http = __esm({
836
+ "src/constants/http.ts"() {
837
+ "use strict";
838
+ HTTP_OK = 200;
839
+ HTTP_NO_CONTENT = 204;
840
+ HTTP_BAD_REQUEST = 400;
841
+ HTTP_NOT_FOUND = 404;
842
+ HTTP_METHOD_NOT_ALLOWED = 405;
843
+ HTTP_PAYLOAD_TOO_LARGE = 413;
844
+ HTTP_INTERNAL_ERROR = 500;
845
+ SECURITY_HEADERS = {
846
+ "x-content-type-options": "nosniff",
847
+ "x-frame-options": "DENY",
848
+ "referrer-policy": "no-referrer",
849
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
850
+ };
851
+ }
852
+ });
853
+
854
+ // src/utils/http-status.ts
855
+ function isErrorStatus(code) {
856
+ return code >= 400;
857
+ }
858
+ function isServerError(code) {
859
+ return code >= 500;
860
+ }
861
+ function isRedirect(code) {
862
+ return code >= 300 && code < 400;
863
+ }
864
+ var init_http_status = __esm({
865
+ "src/utils/http-status.ts"() {
866
+ "use strict";
867
+ }
868
+ });
869
+
806
870
  // src/analysis/categorize.ts
807
871
  function detectCategory(req) {
808
872
  const { method, url, statusCode, responseHeaders } = req;
@@ -871,7 +935,7 @@ function labelRequest(req) {
871
935
  function generateHumanLabel(req, category) {
872
936
  const effectivePath = getEffectivePath(req);
873
937
  const endpointName = getEndpointName(effectivePath);
874
- const failed = req.statusCode >= 400;
938
+ const failed = isErrorStatus(req.statusCode);
875
939
  switch (category) {
876
940
  case "auth-handshake":
877
941
  return "Auth handshake";
@@ -966,6 +1030,7 @@ var init_label = __esm({
966
1030
  "use strict";
967
1031
  init_constants();
968
1032
  init_categorize();
1033
+ init_http_status();
969
1034
  }
970
1035
  });
971
1036
 
@@ -1058,7 +1123,7 @@ function detectWarnings(requests) {
1058
1123
  for (const req of slowRequests) {
1059
1124
  warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
1060
1125
  }
1061
- const errors = requests.filter((r) => r.statusCode >= 500);
1126
+ const errors = requests.filter((r) => isServerError(r.statusCode));
1062
1127
  for (const req of errors) {
1063
1128
  warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
1064
1129
  }
@@ -1070,6 +1135,7 @@ var init_transforms = __esm({
1070
1135
  init_constants();
1071
1136
  init_categorize();
1072
1137
  init_label();
1138
+ init_http_status();
1073
1139
  }
1074
1140
  });
1075
1141
 
@@ -1123,7 +1189,7 @@ function buildFlow(rawRequests) {
1123
1189
  requests,
1124
1190
  startTime,
1125
1191
  totalDurationMs: Math.round(endTime - startTime),
1126
- hasErrors: requests.some((r) => r.statusCode >= 400),
1192
+ hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
1127
1193
  warnings: detectWarnings(rawRequests),
1128
1194
  sourcePage,
1129
1195
  redundancyPct
@@ -1177,6 +1243,7 @@ var init_group = __esm({
1177
1243
  "src/analysis/group.ts"() {
1178
1244
  "use strict";
1179
1245
  init_constants();
1246
+ init_http_status();
1180
1247
  init_label();
1181
1248
  init_categorize();
1182
1249
  init_transforms();
@@ -1184,12 +1251,12 @@ var init_group = __esm({
1184
1251
  });
1185
1252
 
1186
1253
  // src/dashboard/api/shared.ts
1187
- function maskSensitiveHeaders(headers) {
1254
+ function maskSensitiveHeaders(headers2) {
1188
1255
  const masked = {};
1189
- for (const [key, value] of Object.entries(headers)) {
1256
+ for (const [key, value] of Object.entries(headers2)) {
1190
1257
  if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1191
1258
  const s = String(value);
1192
- masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? "****" : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
1259
+ masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? SENSITIVE_MASK_PLACEHOLDER : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
1193
1260
  } else {
1194
1261
  masked[key] = value;
1195
1262
  }
@@ -1209,14 +1276,14 @@ function getCorsOrigin(req) {
1209
1276
  }
1210
1277
  function getJsonHeaders(req) {
1211
1278
  const corsOrigin = getCorsOrigin(req);
1212
- const headers = {
1279
+ const headers2 = {
1213
1280
  "content-type": "application/json",
1214
1281
  "cache-control": "no-cache"
1215
1282
  };
1216
1283
  if (corsOrigin) {
1217
- headers["access-control-allow-origin"] = corsOrigin;
1284
+ headers2["access-control-allow-origin"] = corsOrigin;
1218
1285
  }
1219
- return headers;
1286
+ return headers2;
1220
1287
  }
1221
1288
  function sendJson(req, res, status, data) {
1222
1289
  res.writeHead(status, getJsonHeaders(req));
@@ -1224,23 +1291,58 @@ function sendJson(req, res, status, data) {
1224
1291
  }
1225
1292
  function requireGet(req, res) {
1226
1293
  if (req.method !== "GET") {
1227
- sendJson(req, res, 405, { error: "Method not allowed" });
1294
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1228
1295
  return false;
1229
1296
  }
1230
1297
  return true;
1231
1298
  }
1299
+ function parseRequestUrl(req) {
1300
+ return new URL(req.url ?? "/", URL_PARSE_BASE);
1301
+ }
1302
+ function readJsonBody(req, res, maxBytes = MAX_JSON_BODY_BYTES) {
1303
+ return new Promise((resolve5) => {
1304
+ const chunks = [];
1305
+ let size = 0;
1306
+ req.on("data", (chunk) => {
1307
+ size += chunk.length;
1308
+ if (size > maxBytes) {
1309
+ sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
1310
+ req.destroy();
1311
+ resolve5(null);
1312
+ return;
1313
+ }
1314
+ chunks.push(chunk);
1315
+ });
1316
+ req.on("end", () => {
1317
+ if (size > maxBytes) {
1318
+ resolve5(null);
1319
+ return;
1320
+ }
1321
+ try {
1322
+ resolve5(JSON.parse(Buffer.concat(chunks).toString()));
1323
+ } catch {
1324
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON body" });
1325
+ resolve5(null);
1326
+ }
1327
+ });
1328
+ req.on("error", () => {
1329
+ resolve5(null);
1330
+ });
1331
+ });
1332
+ }
1232
1333
  function handleTelemetryGet(req, res, store) {
1233
1334
  if (!requireGet(req, res)) return;
1234
- const url = new URL(req.url ?? "/", "http://localhost");
1335
+ const url = parseRequestUrl(req);
1235
1336
  const requestId = url.searchParams.get("requestId");
1236
1337
  const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1237
- sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1338
+ sendJson(req, res, HTTP_OK, { total: entries.length, entries: entries.reverse() });
1238
1339
  }
1239
1340
  var init_shared2 = __esm({
1240
1341
  "src/dashboard/api/shared.ts"() {
1241
1342
  "use strict";
1242
1343
  init_constants();
1243
1344
  init_limits();
1345
+ init_http();
1244
1346
  }
1245
1347
  });
1246
1348
 
@@ -1255,15 +1357,13 @@ function sanitizeRequest(r) {
1255
1357
  function createRequestsHandler(registry) {
1256
1358
  return (req, res) => {
1257
1359
  if (!requireGet(req, res)) return;
1258
- const url = new URL(req.url ?? "/", "http://localhost");
1360
+ const url = parseRequestUrl(req);
1259
1361
  const method = url.searchParams.get("method");
1260
1362
  const status = url.searchParams.get("status");
1261
1363
  const search = url.searchParams.get("search");
1262
- const limit = parseInt(
1263
- url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT),
1264
- 10
1265
- );
1266
- const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
1364
+ const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
1365
+ const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
1366
+ const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
1267
1367
  let results = [...registry.get("request-store").getAll()].reverse();
1268
1368
  if (method) {
1269
1369
  results = results.filter((r) => r.method === method.toUpperCase());
@@ -1288,7 +1388,7 @@ function createRequestsHandler(registry) {
1288
1388
  const total = results.length;
1289
1389
  results = results.slice(offset, offset + limit);
1290
1390
  const sanitized = results.map(sanitizeRequest);
1291
- sendJson(req, res, 200, { total, requests: sanitized });
1391
+ sendJson(req, res, HTTP_OK, { total, requests: sanitized });
1292
1392
  };
1293
1393
  }
1294
1394
  function createFlowsHandler(registry) {
@@ -1298,13 +1398,13 @@ function createFlowsHandler(registry) {
1298
1398
  ...flow,
1299
1399
  requests: flow.requests.map(sanitizeRequest)
1300
1400
  }));
1301
- sendJson(req, res, 200, { total: flows.length, flows });
1401
+ sendJson(req, res, HTTP_OK, { total: flows.length, flows });
1302
1402
  };
1303
1403
  }
1304
1404
  function createClearHandler(registry) {
1305
1405
  return (req, res) => {
1306
1406
  if (req.method !== "POST") {
1307
- sendJson(req, res, 405, { error: "Method not allowed" });
1407
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1308
1408
  return;
1309
1409
  }
1310
1410
  registry.get("request-store").clear();
@@ -1313,9 +1413,9 @@ function createClearHandler(registry) {
1313
1413
  registry.get("error-store").clear();
1314
1414
  registry.get("query-store").clear();
1315
1415
  registry.get("metrics-store").reset();
1316
- if (registry.has("finding-store")) registry.get("finding-store").clear();
1416
+ if (registry.has("issue-store")) registry.get("issue-store").clear();
1317
1417
  registry.get("event-bus").emit("store:cleared", void 0);
1318
- sendJson(req, res, 200, { cleared: true });
1418
+ sendJson(req, res, HTTP_OK, { cleared: true });
1319
1419
  };
1320
1420
  }
1321
1421
  function createFetchesHandler(registry) {
@@ -1335,10 +1435,174 @@ var init_handlers = __esm({
1335
1435
  "use strict";
1336
1436
  init_group();
1337
1437
  init_constants();
1438
+ init_http();
1338
1439
  init_shared2();
1339
1440
  }
1340
1441
  });
1341
1442
 
1443
+ // src/utils/type-guards.ts
1444
+ function isString(val) {
1445
+ return typeof val === "string";
1446
+ }
1447
+ function isNumber(val) {
1448
+ return typeof val === "number" && !isNaN(val);
1449
+ }
1450
+ function isBoolean(val) {
1451
+ return typeof val === "boolean";
1452
+ }
1453
+ function getErrorMessage(err) {
1454
+ if (err instanceof Error) return err.message;
1455
+ if (typeof err === "string") return err;
1456
+ return String(err);
1457
+ }
1458
+ function isValidIssueState(val) {
1459
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
1460
+ }
1461
+ function isValidIssueCategory(val) {
1462
+ return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
1463
+ }
1464
+ function isValidAiFixStatus(val) {
1465
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
1466
+ }
1467
+ function validateIssuesData(parsed) {
1468
+ if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
1469
+ return parsed;
1470
+ }
1471
+ return null;
1472
+ }
1473
+ function validateMetricsData(parsed) {
1474
+ if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === 1 && Array.isArray(parsed.endpoints)) {
1475
+ return parsed;
1476
+ }
1477
+ return null;
1478
+ }
1479
+ var init_type_guards = __esm({
1480
+ "src/utils/type-guards.ts"() {
1481
+ "use strict";
1482
+ init_lifecycle();
1483
+ init_limits();
1484
+ }
1485
+ });
1486
+
1487
+ // src/dashboard/api/sdk-event-parser.ts
1488
+ import { randomUUID as randomUUID3 } from "crypto";
1489
+ function str(val, fallback) {
1490
+ return isString(val) ? val : fallback;
1491
+ }
1492
+ function strOrUndef(val) {
1493
+ return isString(val) ? val : void 0;
1494
+ }
1495
+ function num(val, fallback) {
1496
+ return isNumber(val) ? val : fallback;
1497
+ }
1498
+ function numOrUndef(val) {
1499
+ return isNumber(val) ? val : void 0;
1500
+ }
1501
+ function headers(val) {
1502
+ if (val && typeof val === "object" && !Array.isArray(val)) {
1503
+ return val;
1504
+ }
1505
+ return {};
1506
+ }
1507
+ function parseQueryEvent(data, ts, parentRequestId) {
1508
+ return {
1509
+ driver: str(data.source, "sdk"),
1510
+ source: strOrUndef(data.source),
1511
+ sql: strOrUndef(data.sql),
1512
+ model: strOrUndef(data.model),
1513
+ operation: strOrUndef(data.operation),
1514
+ normalizedOp: strOrUndef(data.normalizedOp) ?? strOrUndef(data.operation) ?? "OTHER",
1515
+ table: str(data.table, ""),
1516
+ durationMs: num(data.duration, num(data.durationMs, 0)),
1517
+ rowCount: numOrUndef(data.rowCount),
1518
+ parentRequestId,
1519
+ timestamp: ts
1520
+ };
1521
+ }
1522
+ function parseFetchEvent(data, ts, parentRequestId) {
1523
+ return {
1524
+ url: str(data.url, ""),
1525
+ method: str(data.method, "GET"),
1526
+ statusCode: num(data.statusCode, 0),
1527
+ durationMs: num(data.duration, num(data.durationMs, 0)),
1528
+ parentRequestId,
1529
+ timestamp: ts
1530
+ };
1531
+ }
1532
+ function parseLogEvent(data, ts, parentRequestId) {
1533
+ return {
1534
+ level: str(data.level, "log"),
1535
+ message: str(data.message, ""),
1536
+ parentRequestId,
1537
+ timestamp: ts
1538
+ };
1539
+ }
1540
+ function parseErrorEvent(data, ts, parentRequestId) {
1541
+ return {
1542
+ name: str(data.name, "Error"),
1543
+ message: str(data.message, ""),
1544
+ stack: str(data.stack, ""),
1545
+ parentRequestId,
1546
+ timestamp: ts
1547
+ };
1548
+ }
1549
+ function parseAuthEvent(data, ts, parentRequestId) {
1550
+ return {
1551
+ level: "info",
1552
+ message: `[auth] ${str(data.provider, "unknown")}: ${str(data.result, "check")}`,
1553
+ parentRequestId,
1554
+ timestamp: ts
1555
+ };
1556
+ }
1557
+ function parseRequestEvent(data, ts) {
1558
+ const url = str(data.url, "");
1559
+ return {
1560
+ id: str(data.id, randomUUID3()),
1561
+ method: str(data.method, "GET"),
1562
+ url,
1563
+ path: url.split("?")[0],
1564
+ headers: headers(data.headers),
1565
+ requestBody: isString(data.requestBody) ? data.requestBody : null,
1566
+ statusCode: num(data.statusCode, 200),
1567
+ responseHeaders: headers(data.responseHeaders),
1568
+ responseBody: isString(data.responseBody) ? data.responseBody : null,
1569
+ startedAt: ts,
1570
+ durationMs: num(data.durationMs, 0),
1571
+ responseSize: num(data.responseSize, 0),
1572
+ isStatic: isBoolean(data.isStatic) ? data.isStatic : false
1573
+ };
1574
+ }
1575
+ function routeSDKEvent(event, stores) {
1576
+ const ts = event.timestamp || Date.now();
1577
+ const parentRequestId = event.requestId ?? null;
1578
+ switch (event.type) {
1579
+ case "db.query":
1580
+ stores.addQuery(parseQueryEvent(event.data, ts, parentRequestId));
1581
+ break;
1582
+ case "fetch":
1583
+ stores.addFetch(parseFetchEvent(event.data, ts, parentRequestId));
1584
+ break;
1585
+ case "log":
1586
+ stores.addLog(parseLogEvent(event.data, ts, parentRequestId));
1587
+ break;
1588
+ case "error":
1589
+ stores.addError(parseErrorEvent(event.data, ts, parentRequestId));
1590
+ break;
1591
+ case "auth.check":
1592
+ stores.addLog(parseAuthEvent(event.data, ts, parentRequestId));
1593
+ break;
1594
+ case "request":
1595
+ stores.addRequest(parseRequestEvent(event.data, ts));
1596
+ break;
1597
+ }
1598
+ }
1599
+ var init_sdk_event_parser = __esm({
1600
+ "src/dashboard/api/sdk-event-parser.ts"() {
1601
+ "use strict";
1602
+ init_type_guards();
1603
+ }
1604
+ });
1605
+
1342
1606
  // src/dashboard/api/ingest.ts
1343
1607
  function isBrakitBatch(msg) {
1344
1608
  return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
@@ -1363,65 +1627,21 @@ function createIngestHandler(registry) {
1363
1627
  break;
1364
1628
  }
1365
1629
  };
1366
- const routeSDKEvent = (event) => {
1367
- const ts = event.timestamp || Date.now();
1368
- const parentRequestId = event.requestId ?? null;
1369
- switch (event.type) {
1370
- case "db.query":
1371
- registry.get("query-store").add({
1372
- driver: event.data.source ?? "sdk",
1373
- source: event.data.source ?? "sdk",
1374
- sql: event.data.sql,
1375
- model: event.data.model,
1376
- operation: event.data.operation,
1377
- normalizedOp: event.data.normalizedOp ?? event.data.operation ?? "OTHER",
1378
- table: event.data.table ?? "",
1379
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1380
- rowCount: event.data.rowCount,
1381
- parentRequestId,
1382
- timestamp: ts
1383
- });
1384
- break;
1385
- case "fetch":
1386
- registry.get("fetch-store").add({
1387
- url: event.data.url ?? "",
1388
- method: event.data.method ?? "GET",
1389
- statusCode: event.data.statusCode ?? 0,
1390
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1391
- parentRequestId,
1392
- timestamp: ts
1393
- });
1394
- break;
1395
- case "log":
1396
- registry.get("log-store").add({
1397
- level: event.data.level ?? "log",
1398
- message: event.data.message ?? "",
1399
- parentRequestId,
1400
- timestamp: ts
1401
- });
1402
- break;
1403
- case "error":
1404
- registry.get("error-store").add({
1405
- name: event.data.name ?? "Error",
1406
- message: event.data.message ?? "",
1407
- stack: event.data.stack ?? "",
1408
- parentRequestId,
1409
- timestamp: ts
1410
- });
1411
- break;
1412
- case "auth.check":
1413
- registry.get("log-store").add({
1414
- level: "info",
1415
- message: `[auth] ${event.data.provider ?? "unknown"}: ${event.data.result ?? "check"}`,
1416
- parentRequestId,
1417
- timestamp: ts
1418
- });
1419
- break;
1420
- }
1630
+ const queryStore = registry.get("query-store");
1631
+ const fetchStore = registry.get("fetch-store");
1632
+ const logStore = registry.get("log-store");
1633
+ const errorStore = registry.get("error-store");
1634
+ const requestStore = registry.get("request-store");
1635
+ const stores = {
1636
+ addQuery: (data) => queryStore.add(data),
1637
+ addFetch: (data) => fetchStore.add(data),
1638
+ addLog: (data) => logStore.add(data),
1639
+ addError: (data) => errorStore.add(data),
1640
+ addRequest: (data) => requestStore.add(data)
1421
1641
  };
1422
1642
  return (req, res) => {
1423
1643
  if (req.method !== "POST") {
1424
- sendJson(req, res, 405, { error: "Method not allowed" });
1644
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1425
1645
  return;
1426
1646
  }
1427
1647
  const chunks = [];
@@ -1429,7 +1649,7 @@ function createIngestHandler(registry) {
1429
1649
  req.on("data", (chunk) => {
1430
1650
  totalSize += chunk.length;
1431
1651
  if (totalSize > MAX_INGEST_BYTES) {
1432
- sendJson(req, res, 413, { error: "Payload too large" });
1652
+ sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
1433
1653
  req.destroy();
1434
1654
  return;
1435
1655
  }
@@ -1441,9 +1661,9 @@ function createIngestHandler(registry) {
1441
1661
  const body = JSON.parse(Buffer.concat(chunks).toString());
1442
1662
  if (isSDKPayload(body)) {
1443
1663
  for (const event of body.events) {
1444
- routeSDKEvent(event);
1664
+ routeSDKEvent(event, stores);
1445
1665
  }
1446
- res.writeHead(204);
1666
+ res.writeHead(HTTP_NO_CONTENT);
1447
1667
  res.end();
1448
1668
  return;
1449
1669
  }
@@ -1451,13 +1671,19 @@ function createIngestHandler(registry) {
1451
1671
  for (const event of body.events) {
1452
1672
  routeEvent(event);
1453
1673
  }
1454
- res.writeHead(204);
1674
+ res.writeHead(HTTP_NO_CONTENT);
1455
1675
  res.end();
1456
1676
  return;
1457
1677
  }
1458
- sendJson(req, res, 400, { error: "Invalid batch" });
1678
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
1459
1679
  } catch {
1460
- sendJson(req, res, 400, { error: "Invalid JSON" });
1680
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON" });
1681
+ }
1682
+ });
1683
+ req.on("error", () => {
1684
+ if (!res.headersSent) {
1685
+ res.writeHead(HTTP_BAD_REQUEST);
1686
+ res.end();
1461
1687
  }
1462
1688
  });
1463
1689
  };
@@ -1466,7 +1692,9 @@ var init_ingest = __esm({
1466
1692
  "src/dashboard/api/ingest.ts"() {
1467
1693
  "use strict";
1468
1694
  init_limits();
1695
+ init_http();
1469
1696
  init_shared2();
1697
+ init_sdk_event_parser();
1470
1698
  }
1471
1699
  });
1472
1700
 
@@ -1474,20 +1702,21 @@ var init_ingest = __esm({
1474
1702
  function createMetricsHandler(metricsStore) {
1475
1703
  return (req, res) => {
1476
1704
  if (!requireGet(req, res)) return;
1477
- const url = new URL(req.url ?? "/", "http://localhost");
1705
+ const url = parseRequestUrl(req);
1478
1706
  const endpoint = url.searchParams.get("endpoint");
1479
1707
  if (endpoint) {
1480
1708
  const ep = metricsStore.getEndpoint(endpoint);
1481
- sendJson(req, res, 200, { endpoints: ep ? [ep] : [] });
1709
+ sendJson(req, res, HTTP_OK, { endpoints: ep ? [ep] : [] });
1482
1710
  return;
1483
1711
  }
1484
- sendJson(req, res, 200, { endpoints: metricsStore.getAll() });
1712
+ sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
1485
1713
  };
1486
1714
  }
1487
1715
  var init_metrics2 = __esm({
1488
1716
  "src/dashboard/api/metrics.ts"() {
1489
1717
  "use strict";
1490
1718
  init_shared2();
1719
+ init_http();
1491
1720
  }
1492
1721
  });
1493
1722
 
@@ -1505,15 +1734,34 @@ var init_metrics_live = __esm({
1505
1734
  }
1506
1735
  });
1507
1736
 
1737
+ // src/utils/log.ts
1738
+ function brakitWarn(message) {
1739
+ process.stderr.write(`${PREFIX} ${message}
1740
+ `);
1741
+ }
1742
+ function brakitDebug(message) {
1743
+ if (process.env.DEBUG_BRAKIT) {
1744
+ process.stderr.write(`${PREFIX}:debug ${message}
1745
+ `);
1746
+ }
1747
+ }
1748
+ var PREFIX;
1749
+ var init_log = __esm({
1750
+ "src/utils/log.ts"() {
1751
+ "use strict";
1752
+ PREFIX = "[brakit]";
1753
+ }
1754
+ });
1755
+
1508
1756
  // src/dashboard/api/activity.ts
1509
1757
  function createActivityHandler(registry) {
1510
1758
  return (req, res) => {
1511
1759
  if (!requireGet(req, res)) return;
1512
1760
  try {
1513
- const url = new URL(req.url ?? "/", "http://localhost");
1761
+ const url = parseRequestUrl(req);
1514
1762
  const requestId = url.searchParams.get("requestId");
1515
1763
  if (!requestId) {
1516
- sendJson(req, res, 400, { error: "requestId parameter required" });
1764
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId parameter required" });
1517
1765
  return;
1518
1766
  }
1519
1767
  const fetches = registry.get("fetch-store").getByRequest(requestId);
@@ -1530,7 +1778,7 @@ function createActivityHandler(registry) {
1530
1778
  for (const q of queries)
1531
1779
  timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
1532
1780
  timeline.sort((a, b) => a.timestamp - b.timestamp);
1533
- sendJson(req, res, 200, {
1781
+ sendJson(req, res, HTTP_OK, {
1534
1782
  requestId,
1535
1783
  total: timeline.length,
1536
1784
  timeline,
@@ -1542,9 +1790,9 @@ function createActivityHandler(registry) {
1542
1790
  }
1543
1791
  });
1544
1792
  } catch (err) {
1545
- console.error("[brakit] activity handler error:", err);
1793
+ brakitDebug(`activity handler error: ${err}`);
1546
1794
  if (!res.headersSent) {
1547
- sendJson(req, res, 500, { error: "Internal error" });
1795
+ sendJson(req, res, HTTP_INTERNAL_ERROR, { error: "Internal error" });
1548
1796
  }
1549
1797
  }
1550
1798
  };
@@ -1553,6 +1801,8 @@ var init_activity = __esm({
1553
1801
  "src/dashboard/api/activity.ts"() {
1554
1802
  "use strict";
1555
1803
  init_shared2();
1804
+ init_http();
1805
+ init_log();
1556
1806
  }
1557
1807
  });
1558
1808
 
@@ -1568,123 +1818,168 @@ var init_api = __esm({
1568
1818
  }
1569
1819
  });
1570
1820
 
1571
- // src/dashboard/api/insights.ts
1572
- function createInsightsHandler(engine) {
1821
+ // src/dashboard/api/issues.ts
1822
+ function createIssuesHandler(issueStore) {
1573
1823
  return (req, res) => {
1574
1824
  if (!requireGet(req, res)) return;
1575
- sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
1576
- };
1577
- }
1578
- function createSecurityHandler(engine) {
1579
- return (req, res) => {
1580
- if (!requireGet(req, res)) return;
1581
- sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
1825
+ const url = parseRequestUrl(req);
1826
+ const stateParam = url.searchParams.get("state");
1827
+ const categoryParam = url.searchParams.get("category");
1828
+ let issues;
1829
+ if (stateParam && isValidIssueState(stateParam)) {
1830
+ issues = issueStore.getByState(stateParam);
1831
+ } else if (categoryParam && isValidIssueCategory(categoryParam)) {
1832
+ issues = issueStore.getByCategory(categoryParam);
1833
+ } else {
1834
+ issues = issueStore.getAll();
1835
+ }
1836
+ sendJson(req, res, HTTP_OK, { issues });
1582
1837
  };
1583
1838
  }
1584
- var init_insights = __esm({
1585
- "src/dashboard/api/insights.ts"() {
1586
- "use strict";
1587
- init_shared2();
1588
- }
1589
- });
1590
-
1591
- // src/dashboard/api/findings.ts
1592
- function createFindingsHandler(findingStore) {
1839
+ function createFindingsHandler(issueStore) {
1593
1840
  return (req, res) => {
1594
1841
  if (!requireGet(req, res)) return;
1595
- const url = new URL(req.url ?? "/", "http://localhost");
1842
+ const url = parseRequestUrl(req);
1596
1843
  const stateParam = url.searchParams.get("state");
1597
- let findings;
1598
- if (stateParam && VALID_STATES.has(stateParam)) {
1599
- findings = findingStore.getByState(stateParam);
1844
+ let issues;
1845
+ if (stateParam && isValidIssueState(stateParam)) {
1846
+ issues = issueStore.getByState(stateParam);
1600
1847
  } else {
1601
- findings = findingStore.getAll();
1848
+ issues = issueStore.getAll();
1602
1849
  }
1603
- sendJson(req, res, 200, {
1604
- total: findings.length,
1605
- findings
1850
+ sendJson(req, res, HTTP_OK, {
1851
+ total: issues.length,
1852
+ findings: issues
1606
1853
  });
1607
1854
  };
1608
1855
  }
1609
- var VALID_STATES;
1610
- var init_findings = __esm({
1611
- "src/dashboard/api/findings.ts"() {
1856
+ function createIssuesReportHandler(issueStore, eventBus) {
1857
+ return async (req, res) => {
1858
+ if (req.method !== "POST") {
1859
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1860
+ return;
1861
+ }
1862
+ const body = await readJsonBody(req, res);
1863
+ if (!body) return;
1864
+ const { findingId, status, notes } = body;
1865
+ if (!findingId || typeof findingId !== "string") {
1866
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "findingId is required" });
1867
+ return;
1868
+ }
1869
+ if (!isValidAiFixStatus(status)) {
1870
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "status must be 'fixed' or 'wont_fix'" });
1871
+ return;
1872
+ }
1873
+ if (!notes || typeof notes !== "string") {
1874
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
1875
+ return;
1876
+ }
1877
+ if (issueStore.reportFix(findingId, status, notes)) {
1878
+ eventBus.emit("issues:changed", issueStore.getAll());
1879
+ sendJson(req, res, HTTP_OK, { ok: true });
1880
+ return;
1881
+ }
1882
+ sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
1883
+ };
1884
+ }
1885
+ var init_issues = __esm({
1886
+ "src/dashboard/api/issues.ts"() {
1612
1887
  "use strict";
1613
1888
  init_shared2();
1614
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
1889
+ init_type_guards();
1890
+ init_http();
1615
1891
  }
1616
1892
  });
1617
1893
 
1618
- // src/core/disposable.ts
1619
- var SubscriptionBag;
1620
- var init_disposable = __esm({
1621
- "src/core/disposable.ts"() {
1894
+ // src/constants/events.ts
1895
+ var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES;
1896
+ var init_events = __esm({
1897
+ "src/constants/events.ts"() {
1622
1898
  "use strict";
1623
- SubscriptionBag = class {
1624
- items = [];
1625
- add(teardown) {
1626
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
1627
- }
1628
- dispose() {
1629
- for (const d of this.items) d.dispose();
1630
- this.items.length = 0;
1631
- }
1632
- };
1899
+ SSE_EVENT_FETCH = "fetch";
1900
+ SSE_EVENT_LOG = "log";
1901
+ SSE_EVENT_ERROR = "error_event";
1902
+ SSE_EVENT_QUERY = "query";
1903
+ SSE_EVENT_ISSUES = "issues";
1633
1904
  }
1634
1905
  });
1635
1906
 
1636
1907
  // src/dashboard/sse.ts
1637
1908
  function createSSEHandler(registry) {
1638
- return (req, res) => {
1639
- res.writeHead(200, {
1640
- "content-type": "text/event-stream",
1641
- "cache-control": "no-cache",
1642
- connection: "keep-alive",
1643
- "access-control-allow-origin": "*"
1644
- });
1645
- res.write(":ok\n\n");
1646
- const writeEvent = (eventType, data) => {
1647
- if (res.destroyed) return;
1648
- if (eventType) {
1649
- res.write(`event: ${eventType}
1909
+ const clients = /* @__PURE__ */ new Set();
1910
+ function broadcast(eventType, data) {
1911
+ if (clients.size === 0) return;
1912
+ const frame = eventType ? `event: ${eventType}
1650
1913
  data: ${data}
1651
1914
 
1652
- `);
1653
- } else {
1654
- res.write(`data: ${data}
1915
+ ` : `data: ${data}
1655
1916
 
1656
- `);
1917
+ `;
1918
+ for (const client of clients) {
1919
+ if (client.res.destroyed) {
1920
+ clients.delete(client);
1921
+ continue;
1657
1922
  }
1923
+ try {
1924
+ client.res.write(frame);
1925
+ } catch {
1926
+ clients.delete(client);
1927
+ }
1928
+ }
1929
+ }
1930
+ const bus = registry.get("event-bus");
1931
+ bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
1932
+ bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
1933
+ bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
1934
+ bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
1935
+ bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
1936
+ bus.on("analysis:updated", ({ issues }) => {
1937
+ broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
1938
+ });
1939
+ bus.on("issues:changed", (issues) => {
1940
+ broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
1941
+ });
1942
+ return (req, res) => {
1943
+ const headers2 = {
1944
+ "content-type": "text/event-stream",
1945
+ "cache-control": "no-cache",
1946
+ connection: "keep-alive"
1658
1947
  };
1659
- const bus = registry.get("event-bus");
1660
- const subs = new SubscriptionBag();
1661
- subs.add(bus.on("request:completed", (r) => writeEvent(null, JSON.stringify(r))));
1662
- subs.add(bus.on("telemetry:fetch", (e) => writeEvent("fetch", JSON.stringify(e))));
1663
- subs.add(bus.on("telemetry:log", (e) => writeEvent("log", JSON.stringify(e))));
1664
- subs.add(bus.on("telemetry:error", (e) => writeEvent("error_event", JSON.stringify(e))));
1665
- subs.add(bus.on("telemetry:query", (e) => writeEvent("query", JSON.stringify(e))));
1666
- subs.add(bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
1667
- writeEvent("insights", JSON.stringify(statefulInsights));
1668
- writeEvent("security", JSON.stringify(statefulFindings));
1669
- }));
1948
+ const corsOrigin = getCorsOrigin(req);
1949
+ if (corsOrigin) {
1950
+ headers2["access-control-allow-origin"] = corsOrigin;
1951
+ }
1952
+ res.writeHead(HTTP_OK, headers2);
1953
+ res.write(":ok\n\n");
1670
1954
  const heartbeat = setInterval(() => {
1671
1955
  if (res.destroyed) {
1672
1956
  clearInterval(heartbeat);
1957
+ clients.delete(client);
1673
1958
  return;
1674
1959
  }
1675
- res.write(":heartbeat\n\n");
1960
+ try {
1961
+ res.write(":heartbeat\n\n");
1962
+ } catch {
1963
+ clearInterval(heartbeat);
1964
+ clients.delete(client);
1965
+ }
1676
1966
  }, SSE_HEARTBEAT_INTERVAL_MS);
1967
+ heartbeat.unref();
1968
+ const client = { res, heartbeat };
1969
+ clients.add(client);
1677
1970
  req.on("close", () => {
1678
1971
  clearInterval(heartbeat);
1679
- subs.dispose();
1972
+ clients.delete(client);
1680
1973
  });
1681
1974
  };
1682
1975
  }
1683
1976
  var init_sse = __esm({
1684
1977
  "src/dashboard/sse.ts"() {
1685
1978
  "use strict";
1686
- init_disposable();
1687
1979
  init_constants();
1980
+ init_http();
1981
+ init_events();
1982
+ init_shared2();
1688
1983
  }
1689
1984
  });
1690
1985
 
@@ -2203,6 +2498,13 @@ function getSecurityStyles() {
2203
2498
  .sec-item-resolved{color:var(--text-muted)}
2204
2499
  .sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
2205
2500
  .sec-resolved-item-icon{color:var(--green);font-size:12px;flex-shrink:0;margin-right:8px}
2501
+
2502
+ /* AI status badges */
2503
+ .sec-ai-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:8px;margin-left:8px;white-space:nowrap}
2504
+ .sec-ai-fixing{background:rgba(217,119,6,.1);color:var(--amber)}
2505
+ .sec-ai-wontfix{background:rgba(107,114,128,.1);color:var(--text-muted)}
2506
+ .sec-ai-verified{background:rgba(22,163,74,.1);color:var(--green)}
2507
+ .sec-ai-notes{font-size:11px;color:var(--text-muted);font-style:italic;margin-top:2px;padding-left:0}
2206
2508
  `;
2207
2509
  }
2208
2510
  var init_security = __esm({
@@ -2266,9 +2568,24 @@ var init_styles = __esm({
2266
2568
  });
2267
2569
 
2268
2570
  // src/utils/fs.ts
2269
- import { access } from "fs/promises";
2571
+ import { access, readFile, writeFile } from "fs/promises";
2270
2572
  import { existsSync, readFileSync, writeFileSync } from "fs";
2271
- import { resolve } from "path";
2573
+ import { createHash } from "crypto";
2574
+ import { homedir } from "os";
2575
+ import { resolve, join } from "path";
2576
+ function getProjectDataDir(projectRoot) {
2577
+ const absolute = resolve(projectRoot);
2578
+ const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
2579
+ return join(homedir(), ".brakit", "projects", hash);
2580
+ }
2581
+ async function fileExists(path) {
2582
+ try {
2583
+ await access(path);
2584
+ return true;
2585
+ } catch {
2586
+ return false;
2587
+ }
2588
+ }
2272
2589
  function ensureGitignore(dir, entry) {
2273
2590
  try {
2274
2591
  const gitignorePath = resolve(dir, "../.gitignore");
@@ -2279,31 +2596,30 @@ function ensureGitignore(dir, entry) {
2279
2596
  } else {
2280
2597
  writeFileSync(gitignorePath, entry + "\n");
2281
2598
  }
2282
- } catch {
2283
- }
2284
- }
2285
- var init_fs = __esm({
2286
- "src/utils/fs.ts"() {
2287
- "use strict";
2599
+ } catch (err) {
2600
+ brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
2288
2601
  }
2289
- });
2290
-
2291
- // src/utils/log.ts
2292
- function brakitWarn(message) {
2293
- process.stderr.write(`${PREFIX} ${message}
2294
- `);
2295
2602
  }
2296
- function brakitDebug(message) {
2297
- if (process.env.DEBUG_BRAKIT) {
2298
- process.stderr.write(`${PREFIX}:debug ${message}
2299
- `);
2603
+ async function ensureGitignoreAsync(dir, entry) {
2604
+ try {
2605
+ const gitignorePath = resolve(dir, "../.gitignore");
2606
+ if (await fileExists(gitignorePath)) {
2607
+ const content = await readFile(gitignorePath, "utf-8");
2608
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
2609
+ await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2610
+ } else {
2611
+ await writeFile(gitignorePath, entry + "\n");
2612
+ }
2613
+ } catch (err) {
2614
+ brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
2300
2615
  }
2301
2616
  }
2302
- var PREFIX;
2303
- var init_log = __esm({
2304
- "src/utils/log.ts"() {
2617
+ var init_fs = __esm({
2618
+ "src/utils/fs.ts"() {
2305
2619
  "use strict";
2306
- PREFIX = "[brakit]";
2620
+ init_limits();
2621
+ init_log();
2622
+ init_type_guards();
2307
2623
  }
2308
2624
  });
2309
2625
 
@@ -2321,6 +2637,7 @@ var init_atomic_writer = __esm({
2321
2637
  "use strict";
2322
2638
  init_fs();
2323
2639
  init_log();
2640
+ init_type_guards();
2324
2641
  AtomicWriter = class {
2325
2642
  constructor(opts) {
2326
2643
  this.opts = opts;
@@ -2335,7 +2652,7 @@ var init_atomic_writer = __esm({
2335
2652
  writeFileSync2(this.tmpPath, content);
2336
2653
  renameSync(this.tmpPath, this.opts.filePath);
2337
2654
  } catch (err) {
2338
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2655
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2339
2656
  }
2340
2657
  }
2341
2658
  async writeAsync(content) {
@@ -2349,13 +2666,14 @@ var init_atomic_writer = __esm({
2349
2666
  await writeFile2(this.tmpPath, content);
2350
2667
  await rename(this.tmpPath, this.opts.filePath);
2351
2668
  } catch (err) {
2352
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2669
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2353
2670
  } finally {
2354
2671
  this.writing = false;
2355
2672
  if (this.pendingContent !== null) {
2356
2673
  const next = this.pendingContent;
2357
2674
  this.pendingContent = null;
2358
- this.writeAsync(next);
2675
+ this.writeAsync(next).catch(() => {
2676
+ });
2359
2677
  }
2360
2678
  }
2361
2679
  }
@@ -2368,10 +2686,10 @@ var init_atomic_writer = __esm({
2368
2686
  }
2369
2687
  }
2370
2688
  async ensureDirAsync() {
2371
- if (!existsSync2(this.opts.dir)) {
2689
+ if (!await fileExists(this.opts.dir)) {
2372
2690
  await mkdir(this.opts.dir, { recursive: true });
2373
2691
  if (this.opts.gitignoreEntry) {
2374
- ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2692
+ await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
2375
2693
  }
2376
2694
  }
2377
2695
  }
@@ -2379,50 +2697,57 @@ var init_atomic_writer = __esm({
2379
2697
  }
2380
2698
  });
2381
2699
 
2382
- // src/store/finding-id.ts
2383
- import { createHash } from "crypto";
2384
- function computeFindingId(finding) {
2385
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
2386
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
2700
+ // src/utils/issue-id.ts
2701
+ import { createHash as createHash2 } from "crypto";
2702
+ function computeIssueId(issue) {
2703
+ const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
2704
+ const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
2705
+ return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
2387
2706
  }
2388
- var init_finding_id = __esm({
2389
- "src/store/finding-id.ts"() {
2707
+ var init_issue_id = __esm({
2708
+ "src/utils/issue-id.ts"() {
2390
2709
  "use strict";
2710
+ init_limits();
2391
2711
  }
2392
2712
  });
2393
2713
 
2394
- // src/store/finding-store.ts
2395
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
2714
+ // src/store/issue-store.ts
2715
+ import { readFile as readFile2 } from "fs/promises";
2716
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
2396
2717
  import { resolve as resolve2 } from "path";
2397
- var FindingStore;
2398
- var init_finding_store = __esm({
2399
- "src/store/finding-store.ts"() {
2718
+ var IssueStore;
2719
+ var init_issue_store = __esm({
2720
+ "src/store/issue-store.ts"() {
2400
2721
  "use strict";
2401
- init_constants();
2722
+ init_fs();
2723
+ init_metrics();
2724
+ init_limits();
2725
+ init_thresholds();
2726
+ init_limits();
2402
2727
  init_atomic_writer();
2403
- init_finding_id();
2404
- FindingStore = class {
2405
- constructor(rootDir) {
2406
- this.rootDir = rootDir;
2407
- const metricsDir = resolve2(rootDir, METRICS_DIR);
2408
- this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
2728
+ init_log();
2729
+ init_type_guards();
2730
+ init_issue_id();
2731
+ IssueStore = class {
2732
+ constructor(dataDir) {
2733
+ this.dataDir = dataDir;
2734
+ this.issuesPath = resolve2(dataDir, ISSUES_FILE);
2409
2735
  this.writer = new AtomicWriter({
2410
- dir: metricsDir,
2411
- filePath: this.findingsPath,
2412
- gitignoreEntry: METRICS_DIR,
2413
- label: "findings"
2736
+ dir: dataDir,
2737
+ filePath: this.issuesPath,
2738
+ label: "issues"
2414
2739
  });
2415
- this.load();
2416
2740
  }
2417
- findings = /* @__PURE__ */ new Map();
2741
+ issues = /* @__PURE__ */ new Map();
2418
2742
  flushTimer = null;
2419
2743
  dirty = false;
2420
2744
  writer;
2421
- findingsPath;
2745
+ issuesPath;
2422
2746
  start() {
2747
+ this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
2423
2748
  this.flushTimer = setInterval(
2424
2749
  () => this.flush(),
2425
- FINDINGS_FLUSH_INTERVAL_MS
2750
+ ISSUES_FLUSH_INTERVAL_MS
2426
2751
  );
2427
2752
  this.flushTimer.unref();
2428
2753
  }
@@ -2433,91 +2758,150 @@ var init_finding_store = __esm({
2433
2758
  }
2434
2759
  this.flushSync();
2435
2760
  }
2436
- upsert(finding, source) {
2437
- const id = computeFindingId(finding);
2438
- const existing = this.findings.get(id);
2761
+ upsert(issue, source) {
2762
+ const id = computeIssueId(issue);
2763
+ const existing = this.issues.get(id);
2439
2764
  const now = Date.now();
2440
2765
  if (existing) {
2441
2766
  existing.lastSeenAt = now;
2442
2767
  existing.occurrences++;
2443
- existing.finding = finding;
2444
- if (existing.state === "resolved") {
2445
- existing.state = "open";
2768
+ existing.issue = issue;
2769
+ existing.cleanHitsSinceLastSeen = 0;
2770
+ if (existing.state === "resolved" || existing.state === "stale") {
2771
+ existing.state = "regressed";
2446
2772
  existing.resolvedAt = null;
2447
2773
  }
2448
2774
  this.dirty = true;
2449
2775
  return existing;
2450
2776
  }
2451
2777
  const stateful = {
2452
- findingId: id,
2778
+ issueId: id,
2453
2779
  state: "open",
2454
2780
  source,
2455
- finding,
2781
+ category: issue.category,
2782
+ issue,
2456
2783
  firstSeenAt: now,
2457
2784
  lastSeenAt: now,
2458
2785
  resolvedAt: null,
2459
- occurrences: 1
2786
+ occurrences: 1,
2787
+ cleanHitsSinceLastSeen: 0,
2788
+ aiStatus: null,
2789
+ aiNotes: null
2460
2790
  };
2461
- this.findings.set(id, stateful);
2791
+ this.issues.set(id, stateful);
2462
2792
  this.dirty = true;
2463
2793
  return stateful;
2464
2794
  }
2465
- transition(findingId, state) {
2466
- const finding = this.findings.get(findingId);
2467
- if (!finding) return false;
2468
- finding.state = state;
2469
- if (state === "resolved") {
2470
- finding.resolvedAt = Date.now();
2471
- }
2472
- this.dirty = true;
2473
- return true;
2474
- }
2475
2795
  /**
2476
- * Reconcile passive findings against the current analysis results.
2796
+ * Reconcile issues against the current analysis results using evidence-based resolution.
2477
2797
  *
2478
- * Passive findings are detected by continuous scanning (not user-triggered).
2479
- * When a previously-seen finding is absent from the current results, it means
2480
- * the issue has been fixed — transition it to "resolved" automatically.
2481
- * Active findings (from MCP verify-fix) are not auto-resolved because they
2482
- * require explicit verification.
2798
+ * @param currentIssueIds - IDs of issues detected in the current analysis cycle
2799
+ * @param activeEndpoints - Endpoints that had requests in the current cycle
2483
2800
  */
2484
- reconcilePassive(currentFindings) {
2485
- const currentIds = new Set(currentFindings.map(computeFindingId));
2486
- for (const [id, stateful] of this.findings) {
2487
- if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
2488
- stateful.state = "resolved";
2489
- stateful.resolvedAt = Date.now();
2801
+ reconcile(currentIssueIds, activeEndpoints) {
2802
+ const now = Date.now();
2803
+ for (const [, stateful] of this.issues) {
2804
+ const isActive = stateful.state === "open" || stateful.state === "fixing" || stateful.state === "regressed";
2805
+ if (!isActive) continue;
2806
+ if (currentIssueIds.has(stateful.issueId)) continue;
2807
+ const endpoint = stateful.issue.endpoint;
2808
+ if (endpoint && activeEndpoints.has(endpoint)) {
2809
+ stateful.cleanHitsSinceLastSeen++;
2810
+ if (stateful.cleanHitsSinceLastSeen >= CLEAN_HITS_FOR_RESOLUTION) {
2811
+ stateful.state = "resolved";
2812
+ stateful.resolvedAt = now;
2813
+ }
2814
+ this.dirty = true;
2815
+ } else if (now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS) {
2816
+ stateful.state = "stale";
2817
+ this.dirty = true;
2818
+ }
2819
+ }
2820
+ for (const [id, stateful] of this.issues) {
2821
+ if (stateful.state === "resolved" && stateful.resolvedAt && now - stateful.resolvedAt > ISSUE_PRUNE_TTL_MS) {
2822
+ this.issues.delete(id);
2823
+ this.dirty = true;
2824
+ } else if (stateful.state === "stale" && now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS + ISSUE_PRUNE_TTL_MS) {
2825
+ this.issues.delete(id);
2490
2826
  this.dirty = true;
2491
2827
  }
2492
2828
  }
2493
2829
  }
2830
+ transition(issueId, state) {
2831
+ const issue = this.issues.get(issueId);
2832
+ if (!issue) return false;
2833
+ issue.state = state;
2834
+ if (state === "resolved") {
2835
+ issue.resolvedAt = Date.now();
2836
+ }
2837
+ this.dirty = true;
2838
+ return true;
2839
+ }
2840
+ reportFix(issueId, status, notes) {
2841
+ const issue = this.issues.get(issueId);
2842
+ if (!issue) return false;
2843
+ issue.aiStatus = status;
2844
+ issue.aiNotes = notes;
2845
+ if (status === "fixed") {
2846
+ issue.state = "fixing";
2847
+ }
2848
+ this.dirty = true;
2849
+ return true;
2850
+ }
2494
2851
  getAll() {
2495
- return [...this.findings.values()];
2852
+ return [...this.issues.values()];
2496
2853
  }
2497
2854
  getByState(state) {
2498
- return [...this.findings.values()].filter((f) => f.state === state);
2855
+ return [...this.issues.values()].filter((i) => i.state === state);
2499
2856
  }
2500
- get(findingId) {
2501
- return this.findings.get(findingId);
2857
+ getByCategory(category) {
2858
+ return [...this.issues.values()].filter((i) => i.category === category);
2502
2859
  }
2503
- clear() {
2504
- this.findings.clear();
2505
- this.dirty = true;
2860
+ get(issueId) {
2861
+ return this.issues.get(issueId);
2506
2862
  }
2507
- load() {
2863
+ clear() {
2864
+ this.issues.clear();
2865
+ this.dirty = false;
2508
2866
  try {
2509
- if (existsSync3(this.findingsPath)) {
2510
- const raw = readFileSync2(this.findingsPath, "utf-8");
2511
- const parsed = JSON.parse(raw);
2512
- if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
2513
- for (const f of parsed.findings) {
2514
- this.findings.set(f.findingId, f);
2515
- }
2516
- }
2867
+ if (existsSync3(this.issuesPath)) {
2868
+ unlinkSync(this.issuesPath);
2517
2869
  }
2518
2870
  } catch {
2519
2871
  }
2520
2872
  }
2873
+ isDirty() {
2874
+ return this.dirty;
2875
+ }
2876
+ async loadAsync() {
2877
+ try {
2878
+ if (await fileExists(this.issuesPath)) {
2879
+ const raw = await readFile2(this.issuesPath, "utf-8");
2880
+ this.hydrate(raw);
2881
+ }
2882
+ } catch (err) {
2883
+ brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
2884
+ }
2885
+ }
2886
+ /** Sync load for tests only — not used in production paths. */
2887
+ loadSync() {
2888
+ try {
2889
+ if (existsSync3(this.issuesPath)) {
2890
+ const raw = readFileSync2(this.issuesPath, "utf-8");
2891
+ this.hydrate(raw);
2892
+ }
2893
+ } catch (err) {
2894
+ brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
2895
+ }
2896
+ }
2897
+ /** Parse and populate issues from a raw JSON string. */
2898
+ hydrate(raw) {
2899
+ const validated = validateIssuesData(JSON.parse(raw));
2900
+ if (!validated) return;
2901
+ for (const issue of validated.issues) {
2902
+ this.issues.set(issue.issueId, issue);
2903
+ }
2904
+ }
2521
2905
  flush() {
2522
2906
  if (!this.dirty) return;
2523
2907
  this.writer.writeAsync(this.serialize());
@@ -2530,8 +2914,8 @@ var init_finding_store = __esm({
2530
2914
  }
2531
2915
  serialize() {
2532
2916
  const data = {
2533
- version: 1,
2534
- findings: [...this.findings.values()]
2917
+ version: ISSUES_DATA_VERSION,
2918
+ issues: [...this.issues.values()]
2535
2919
  };
2536
2920
  return JSON.stringify(data);
2537
2921
  }
@@ -2540,9 +2924,9 @@ var init_finding_store = __esm({
2540
2924
  });
2541
2925
 
2542
2926
  // src/detect/project.ts
2543
- import { readFile as readFile2 } from "fs/promises";
2927
+ import { readFile as readFile3, readdir } from "fs/promises";
2544
2928
  import { existsSync as existsSync4 } from "fs";
2545
- import { join } from "path";
2929
+ import { join as join2, relative } from "path";
2546
2930
  function detectFrameworkFromDeps(allDeps) {
2547
2931
  for (const f of FRAMEWORKS) {
2548
2932
  if (allDeps[f.dep]) return f.name;
@@ -2550,10 +2934,10 @@ function detectFrameworkFromDeps(allDeps) {
2550
2934
  return "unknown";
2551
2935
  }
2552
2936
  function detectPackageManagerSync(rootDir) {
2553
- if (existsSync4(join(rootDir, "bun.lockb")) || existsSync4(join(rootDir, "bun.lock"))) return "bun";
2554
- if (existsSync4(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2555
- if (existsSync4(join(rootDir, "yarn.lock"))) return "yarn";
2556
- if (existsSync4(join(rootDir, "package-lock.json"))) return "npm";
2937
+ if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
2938
+ if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2939
+ if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
2940
+ if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
2557
2941
  return "unknown";
2558
2942
  }
2559
2943
  var FRAMEWORKS;
@@ -2571,6 +2955,44 @@ var init_project = __esm({
2571
2955
  }
2572
2956
  });
2573
2957
 
2958
+ // src/utils/response.ts
2959
+ function tryParseJson(body) {
2960
+ if (!body) return null;
2961
+ try {
2962
+ return JSON.parse(body);
2963
+ } catch {
2964
+ return null;
2965
+ }
2966
+ }
2967
+ function unwrapResponse(parsed) {
2968
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
2969
+ const obj = parsed;
2970
+ const keys = Object.keys(obj);
2971
+ if (keys.length > 3) return parsed;
2972
+ let best = null;
2973
+ let bestSize = 0;
2974
+ for (const key of keys) {
2975
+ const val = obj[key];
2976
+ if (Array.isArray(val) && val.length > bestSize) {
2977
+ best = val;
2978
+ bestSize = val.length;
2979
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
2980
+ const size = Object.keys(val).length;
2981
+ if (size > bestSize) {
2982
+ best = val;
2983
+ bestSize = size;
2984
+ }
2985
+ }
2986
+ }
2987
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
2988
+ }
2989
+ var init_response = __esm({
2990
+ "src/utils/response.ts"() {
2991
+ "use strict";
2992
+ init_thresholds();
2993
+ }
2994
+ });
2995
+
2574
2996
  // src/analysis/rules/patterns.ts
2575
2997
  var SECRET_KEYS, TOKEN_PARAMS, SAFE_PARAMS, STACK_TRACE_RE, DB_CONN_RE, SQL_FRAGMENT_RE, SECRET_VAL_RE, LOG_SECRET_RE, MASKED_RE, EMAIL_RE, INTERNAL_ID_KEYS, INTERNAL_ID_SUFFIX, SELECT_STAR_RE, SELECT_DOT_STAR_RE, RULE_HINTS;
2576
2998
  var init_patterns = __esm({
@@ -2579,11 +3001,11 @@ var init_patterns = __esm({
2579
3001
  SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
2580
3002
  TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
2581
3003
  SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
2582
- STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
3004
+ STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
2583
3005
  DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
2584
3006
  SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
2585
- SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
2586
- LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
3007
+ SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
3008
+ LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
2587
3009
  MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
2588
3010
  EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
2589
3011
  INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
@@ -2595,39 +3017,32 @@ var init_patterns = __esm({
2595
3017
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
2596
3018
  "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
2597
3019
  "error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
3020
+ "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
2598
3021
  "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
2599
3022
  "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
2600
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
2601
3023
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
2602
3024
  };
2603
3025
  }
2604
3026
  });
2605
3027
 
2606
3028
  // src/analysis/rules/exposed-secret.ts
2607
- function tryParseJson(body) {
2608
- if (!body) return null;
2609
- try {
2610
- return JSON.parse(body);
2611
- } catch {
2612
- return null;
2613
- }
2614
- }
2615
- function findSecretKeys(obj, prefix) {
3029
+ function findSecretKeys(obj, prefix, depth = 0) {
2616
3030
  const found = [];
3031
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
2617
3032
  if (!obj || typeof obj !== "object") return found;
2618
3033
  if (Array.isArray(obj)) {
2619
- for (let i = 0; i < Math.min(obj.length, 5); i++) {
2620
- found.push(...findSecretKeys(obj[i], prefix));
3034
+ for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
3035
+ found.push(...findSecretKeys(obj[i], prefix, depth + 1));
2621
3036
  }
2622
3037
  return found;
2623
3038
  }
2624
3039
  for (const k of Object.keys(obj)) {
2625
3040
  const val = obj[k];
2626
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= 8 && !MASKED_RE.test(val)) {
3041
+ if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
2627
3042
  found.push(k);
2628
3043
  }
2629
3044
  if (typeof val === "object" && val !== null) {
2630
- found.push(...findSecretKeys(val, prefix + k + "."));
3045
+ found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
2631
3046
  }
2632
3047
  }
2633
3048
  return found;
@@ -2637,6 +3052,8 @@ var init_exposed_secret = __esm({
2637
3052
  "src/analysis/rules/exposed-secret.ts"() {
2638
3053
  "use strict";
2639
3054
  init_patterns();
3055
+ init_limits();
3056
+ init_http_status();
2640
3057
  exposedSecretRule = {
2641
3058
  id: "exposed-secret",
2642
3059
  severity: "critical",
@@ -2646,8 +3063,8 @@ var init_exposed_secret = __esm({
2646
3063
  const findings = [];
2647
3064
  const seen = /* @__PURE__ */ new Map();
2648
3065
  for (const r of ctx.requests) {
2649
- if (r.statusCode >= 400) continue;
2650
- const parsed = tryParseJson(r.responseBody);
3066
+ if (isErrorStatus(r.statusCode)) continue;
3067
+ const parsed = ctx.parsedBodies.response.get(r.id);
2651
3068
  if (!parsed) continue;
2652
3069
  const keys = findSecretKeys(parsed, "");
2653
3070
  if (keys.length === 0) continue;
@@ -2823,7 +3240,7 @@ var init_error_info_leak = __esm({
2823
3240
 
2824
3241
  // src/analysis/rules/insecure-cookie.ts
2825
3242
  function isFrameworkResponse(r) {
2826
- if (r.statusCode >= 300 && r.statusCode < 400) return true;
3243
+ if (isRedirect(r.statusCode)) return true;
2827
3244
  if (r.path?.startsWith("/__")) return true;
2828
3245
  if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
2829
3246
  return false;
@@ -2833,6 +3250,7 @@ var init_insecure_cookie = __esm({
2833
3250
  "src/analysis/rules/insecure-cookie.ts"() {
2834
3251
  "use strict";
2835
3252
  init_patterns();
3253
+ init_http_status();
2836
3254
  insecureCookieRule = {
2837
3255
  id: "insecure-cookie",
2838
3256
  severity: "warning",
@@ -2927,74 +3345,37 @@ var init_cors_credentials = __esm({
2927
3345
  const findings = [];
2928
3346
  const seen = /* @__PURE__ */ new Set();
2929
3347
  for (const r of ctx.requests) {
2930
- if (!r.responseHeaders) continue;
2931
- const origin = r.responseHeaders["access-control-allow-origin"];
2932
- const creds = r.responseHeaders["access-control-allow-credentials"];
2933
- if (origin !== "*" || creds !== "true") continue;
2934
- const ep = `${r.method} ${r.path}`;
2935
- if (seen.has(ep)) continue;
2936
- seen.add(ep);
2937
- findings.push({
2938
- severity: "warning",
2939
- rule: "cors-credentials",
2940
- title: "CORS Credentials with Wildcard",
2941
- desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
2942
- hint: this.hint,
2943
- endpoint: ep,
2944
- count: 1
2945
- });
2946
- }
2947
- return findings;
2948
- }
2949
- };
2950
- }
2951
- });
2952
-
2953
- // src/utils/response.ts
2954
- function unwrapResponse(parsed) {
2955
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
2956
- const obj = parsed;
2957
- const keys = Object.keys(obj);
2958
- if (keys.length > 3) return parsed;
2959
- let best = null;
2960
- let bestSize = 0;
2961
- for (const key of keys) {
2962
- const val = obj[key];
2963
- if (Array.isArray(val) && val.length > bestSize) {
2964
- best = val;
2965
- bestSize = val.length;
2966
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
2967
- const size = Object.keys(val).length;
2968
- if (size > bestSize) {
2969
- best = val;
2970
- bestSize = size;
3348
+ if (!r.responseHeaders) continue;
3349
+ const origin = r.responseHeaders["access-control-allow-origin"];
3350
+ const creds = r.responseHeaders["access-control-allow-credentials"];
3351
+ if (origin !== "*" || creds !== "true") continue;
3352
+ const ep = `${r.method} ${r.path}`;
3353
+ if (seen.has(ep)) continue;
3354
+ seen.add(ep);
3355
+ findings.push({
3356
+ severity: "warning",
3357
+ rule: "cors-credentials",
3358
+ title: "CORS Credentials with Wildcard",
3359
+ desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
3360
+ hint: this.hint,
3361
+ endpoint: ep,
3362
+ count: 1
3363
+ });
3364
+ }
3365
+ return findings;
2971
3366
  }
2972
- }
2973
- }
2974
- return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
2975
- }
2976
- var init_response = __esm({
2977
- "src/utils/response.ts"() {
2978
- "use strict";
2979
- init_thresholds();
3367
+ };
2980
3368
  }
2981
3369
  });
2982
3370
 
2983
3371
  // src/analysis/rules/response-pii-leak.ts
2984
- function tryParseJson2(body) {
2985
- if (!body) return null;
2986
- try {
2987
- return JSON.parse(body);
2988
- } catch {
2989
- return null;
2990
- }
2991
- }
2992
- function findEmails(obj) {
3372
+ function findEmails(obj, depth = 0) {
2993
3373
  const emails = [];
3374
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
2994
3375
  if (!obj || typeof obj !== "object") return emails;
2995
3376
  if (Array.isArray(obj)) {
2996
- for (let i = 0; i < Math.min(obj.length, 10); i++) {
2997
- emails.push(...findEmails(obj[i]));
3377
+ for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
3378
+ emails.push(...findEmails(obj[i], depth + 1));
2998
3379
  }
2999
3380
  return emails;
3000
3381
  }
@@ -3002,7 +3383,7 @@ function findEmails(obj) {
3002
3383
  if (typeof v === "string" && EMAIL_RE.test(v)) {
3003
3384
  emails.push(v);
3004
3385
  } else if (typeof v === "object" && v !== null) {
3005
- emails.push(...findEmails(v));
3386
+ emails.push(...findEmails(v, depth + 1));
3006
3387
  }
3007
3388
  }
3008
3389
  return emails;
@@ -3021,57 +3402,56 @@ function hasInternalIds(obj) {
3021
3402
  }
3022
3403
  return false;
3023
3404
  }
3024
- function detectPII(method, reqBody, resBody) {
3025
- const target = unwrapResponse(resBody);
3026
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
3027
- const reqEmails = findEmails(reqBody);
3028
- if (reqEmails.length > 0) {
3029
- const resEmails = findEmails(target);
3030
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
3031
- if (echoed.length > 0) {
3032
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
3033
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
3034
- return { reason: "echo", emailCount: echoed.length };
3035
- }
3036
- }
3037
- }
3405
+ function detectEchoPII(method, reqBody, target) {
3406
+ if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
3407
+ const reqEmails = findEmails(reqBody);
3408
+ if (reqEmails.length === 0) return null;
3409
+ const resEmails = findEmails(target);
3410
+ const echoed = reqEmails.filter((e) => resEmails.includes(e));
3411
+ if (echoed.length === 0) return null;
3412
+ const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
3413
+ if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
3414
+ return { reason: "echo", emailCount: echoed.length };
3038
3415
  }
3039
- if (target && typeof target === "object" && !Array.isArray(target)) {
3040
- const fields = topLevelFieldCount(target);
3041
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
3042
- const emails = findEmails(target);
3043
- if (emails.length > 0) {
3044
- return { reason: "full-record", emailCount: emails.length };
3045
- }
3416
+ return null;
3417
+ }
3418
+ function detectFullRecordPII(target) {
3419
+ if (!target || typeof target !== "object" || Array.isArray(target)) return null;
3420
+ const fields = topLevelFieldCount(target);
3421
+ if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
3422
+ const emails = findEmails(target);
3423
+ if (emails.length === 0) return null;
3424
+ return { reason: "full-record", emailCount: emails.length };
3425
+ }
3426
+ function detectListPII(target) {
3427
+ if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
3428
+ let itemsWithEmail = 0;
3429
+ for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
3430
+ const item = target[i];
3431
+ if (item && typeof item === "object" && findEmails(item).length > 0) {
3432
+ itemsWithEmail++;
3046
3433
  }
3047
3434
  }
3048
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
3049
- let itemsWithEmail = 0;
3050
- for (let i = 0; i < Math.min(target.length, 10); i++) {
3051
- const item = target[i];
3052
- if (item && typeof item === "object") {
3053
- const emails = findEmails(item);
3054
- if (emails.length > 0) itemsWithEmail++;
3055
- }
3056
- }
3057
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
3058
- const first = target[0];
3059
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
3060
- return { reason: "list-pii", emailCount: itemsWithEmail };
3061
- }
3062
- }
3435
+ if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
3436
+ const first = target[0];
3437
+ if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
3438
+ return { reason: "list-pii", emailCount: itemsWithEmail };
3063
3439
  }
3064
3440
  return null;
3065
3441
  }
3066
- var WRITE_METHODS, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, REASON_LABELS, responsePiiLeakRule;
3442
+ function detectPII(method, reqBody, resBody) {
3443
+ const target = unwrapResponse(resBody);
3444
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
3445
+ }
3446
+ var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
3067
3447
  var init_response_pii_leak = __esm({
3068
3448
  "src/analysis/rules/response-pii-leak.ts"() {
3069
3449
  "use strict";
3070
3450
  init_patterns();
3071
3451
  init_response();
3452
+ init_limits();
3453
+ init_http_status();
3072
3454
  WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
3073
- FULL_RECORD_MIN_FIELDS = 5;
3074
- LIST_PII_MIN_ITEMS = 2;
3075
3455
  REASON_LABELS = {
3076
3456
  echo: "echoes back PII from the request body",
3077
3457
  "full-record": "returns a full record with email and internal IDs",
@@ -3086,10 +3466,10 @@ var init_response_pii_leak = __esm({
3086
3466
  const findings = [];
3087
3467
  const seen = /* @__PURE__ */ new Map();
3088
3468
  for (const r of ctx.requests) {
3089
- if (r.statusCode >= 400) continue;
3090
- const resJson = tryParseJson2(r.responseBody);
3469
+ if (isErrorStatus(r.statusCode)) continue;
3470
+ const resJson = ctx.parsedBodies.response.get(r.id);
3091
3471
  if (!resJson) continue;
3092
- const reqJson = tryParseJson2(r.requestBody);
3472
+ const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
3093
3473
  const detection = detectPII(r.method, reqJson, resJson);
3094
3474
  if (!detection) continue;
3095
3475
  const ep = `${r.method} ${r.path}`;
@@ -3118,6 +3498,21 @@ var init_response_pii_leak = __esm({
3118
3498
  });
3119
3499
 
3120
3500
  // src/analysis/rules/scanner.ts
3501
+ function buildBodyCache(requests) {
3502
+ const response = /* @__PURE__ */ new Map();
3503
+ const request = /* @__PURE__ */ new Map();
3504
+ for (const r of requests) {
3505
+ if (r.responseBody) {
3506
+ const parsed = tryParseJson(r.responseBody);
3507
+ if (parsed != null) response.set(r.id, parsed);
3508
+ }
3509
+ if (r.requestBody) {
3510
+ const parsed = tryParseJson(r.requestBody);
3511
+ if (parsed != null) request.set(r.id, parsed);
3512
+ }
3513
+ }
3514
+ return { response, request };
3515
+ }
3121
3516
  function createDefaultScanner() {
3122
3517
  const scanner = new SecurityScanner();
3123
3518
  scanner.register(exposedSecretRule);
@@ -3134,6 +3529,7 @@ var SecurityScanner;
3134
3529
  var init_scanner = __esm({
3135
3530
  "src/analysis/rules/scanner.ts"() {
3136
3531
  "use strict";
3532
+ init_response();
3137
3533
  init_exposed_secret();
3138
3534
  init_token_in_url();
3139
3535
  init_stack_trace_leak();
@@ -3147,7 +3543,11 @@ var init_scanner = __esm({
3147
3543
  register(rule) {
3148
3544
  this.rules.push(rule);
3149
3545
  }
3150
- scan(ctx) {
3546
+ scan(input) {
3547
+ const ctx = {
3548
+ ...input,
3549
+ parsedBodies: buildBodyCache(input.requests)
3550
+ };
3151
3551
  const findings = [];
3152
3552
  for (const rule of this.rules) {
3153
3553
  try {
@@ -3180,6 +3580,24 @@ var init_rules = __esm({
3180
3580
  }
3181
3581
  });
3182
3582
 
3583
+ // src/core/disposable.ts
3584
+ var SubscriptionBag;
3585
+ var init_disposable = __esm({
3586
+ "src/core/disposable.ts"() {
3587
+ "use strict";
3588
+ SubscriptionBag = class {
3589
+ items = [];
3590
+ add(teardown) {
3591
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3592
+ }
3593
+ dispose() {
3594
+ for (const d of this.items) d.dispose();
3595
+ this.items.length = 0;
3596
+ }
3597
+ };
3598
+ }
3599
+ });
3600
+
3183
3601
  // src/utils/collections.ts
3184
3602
  function groupBy(items, keyFn) {
3185
3603
  const map = /* @__PURE__ */ new Map();
@@ -3202,16 +3620,22 @@ var init_collections = __esm({
3202
3620
  });
3203
3621
 
3204
3622
  // src/utils/endpoint.ts
3623
+ function normalizePath(path) {
3624
+ const qIdx = path.indexOf("?");
3625
+ const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
3626
+ return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
3627
+ }
3205
3628
  function getEndpointKey(method, path) {
3206
- return `${method} ${path}`;
3629
+ return `${method} ${normalizePath(path)}`;
3207
3630
  }
3208
3631
  function extractEndpointFromDesc(desc) {
3209
3632
  return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
3210
3633
  }
3211
- var ENDPOINT_PREFIX_RE;
3634
+ var DYNAMIC_SEGMENT_RE, ENDPOINT_PREFIX_RE;
3212
3635
  var init_endpoint = __esm({
3213
3636
  "src/utils/endpoint.ts"() {
3214
3637
  "use strict";
3638
+ 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;
3215
3639
  ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
3216
3640
  }
3217
3641
  });
@@ -3265,6 +3689,15 @@ function windowByEndpoint(requests) {
3265
3689
  }
3266
3690
  return windowed;
3267
3691
  }
3692
+ function extractActiveEndpoints(requests) {
3693
+ const endpoints = /* @__PURE__ */ new Set();
3694
+ for (const r of requests) {
3695
+ if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
3696
+ endpoints.add(getEndpointKey(r.method, r.path));
3697
+ }
3698
+ }
3699
+ return endpoints;
3700
+ }
3268
3701
  function prepareContext(ctx) {
3269
3702
  const nonStatic = ctx.requests.filter(
3270
3703
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
@@ -3282,7 +3715,7 @@ function prepareContext(ctx) {
3282
3715
  endpointGroups.set(ep, g);
3283
3716
  }
3284
3717
  g.total++;
3285
- if (r.statusCode >= 400) g.errors++;
3718
+ if (isErrorStatus(r.statusCode)) g.errors++;
3286
3719
  g.totalDuration += r.durationMs;
3287
3720
  g.totalSize += r.responseSize ?? 0;
3288
3721
  const reqQueries = queriesByReq.get(r.id) ?? [];
@@ -3319,6 +3752,7 @@ var init_prepare = __esm({
3319
3752
  init_collections();
3320
3753
  init_endpoint();
3321
3754
  init_constants();
3755
+ init_http_status();
3322
3756
  init_thresholds();
3323
3757
  init_query_helpers();
3324
3758
  }
@@ -3803,6 +4237,7 @@ var init_response_overfetch = __esm({
3803
4237
  "use strict";
3804
4238
  init_endpoint();
3805
4239
  init_response();
4240
+ init_http_status();
3806
4241
  init_patterns();
3807
4242
  init_constants();
3808
4243
  responseOverfetchRule = {
@@ -3811,7 +4246,7 @@ var init_response_overfetch = __esm({
3811
4246
  const insights = [];
3812
4247
  const seen = /* @__PURE__ */ new Set();
3813
4248
  for (const r of ctx.nonStatic) {
3814
- if (r.statusCode >= 400 || !r.responseBody) continue;
4249
+ if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
3815
4250
  const ep = getEndpointKey(r.method, r.path);
3816
4251
  if (seen.has(ep)) continue;
3817
4252
  let parsed;
@@ -3999,7 +4434,7 @@ function createDefaultInsightRunner() {
3999
4434
  function computeInsights(ctx) {
4000
4435
  return createDefaultInsightRunner().run(ctx);
4001
4436
  }
4002
- var init_insights2 = __esm({
4437
+ var init_insights = __esm({
4003
4438
  "src/analysis/insights/index.ts"() {
4004
4439
  "use strict";
4005
4440
  init_runner();
@@ -4009,73 +4444,48 @@ var init_insights2 = __esm({
4009
4444
  });
4010
4445
 
4011
4446
  // src/analysis/insights.ts
4012
- var init_insights3 = __esm({
4447
+ var init_insights2 = __esm({
4013
4448
  "src/analysis/insights.ts"() {
4014
4449
  "use strict";
4015
- init_insights2();
4450
+ init_insights();
4016
4451
  }
4017
4452
  });
4018
4453
 
4019
- // src/analysis/insight-tracker.ts
4020
- function computeInsightKey(insight) {
4021
- const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
4022
- return `${insight.type}:${identifier}`;
4454
+ // src/analysis/issue-mappers.ts
4455
+ function categorizeInsight(type) {
4456
+ if (type === "security") return "security";
4457
+ if (type === "error" || type === "error-hotspot") return "reliability";
4458
+ return "performance";
4023
4459
  }
4024
- var InsightTracker;
4025
- var init_insight_tracker = __esm({
4026
- "src/analysis/insight-tracker.ts"() {
4460
+ function insightToIssue(insight) {
4461
+ return {
4462
+ category: categorizeInsight(insight.type),
4463
+ rule: insight.type,
4464
+ severity: insight.severity,
4465
+ title: insight.title,
4466
+ desc: insight.desc,
4467
+ hint: insight.hint,
4468
+ detail: insight.detail,
4469
+ endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
4470
+ nav: insight.nav
4471
+ };
4472
+ }
4473
+ function securityFindingToIssue(finding) {
4474
+ return {
4475
+ category: "security",
4476
+ rule: finding.rule,
4477
+ severity: finding.severity,
4478
+ title: finding.title,
4479
+ desc: finding.desc,
4480
+ hint: finding.hint,
4481
+ endpoint: finding.endpoint,
4482
+ nav: "security"
4483
+ };
4484
+ }
4485
+ var init_issue_mappers = __esm({
4486
+ "src/analysis/issue-mappers.ts"() {
4027
4487
  "use strict";
4028
4488
  init_endpoint();
4029
- init_thresholds();
4030
- InsightTracker = class {
4031
- tracked = /* @__PURE__ */ new Map();
4032
- reconcile(current) {
4033
- const currentKeys = /* @__PURE__ */ new Set();
4034
- const now = Date.now();
4035
- for (const insight of current) {
4036
- const key = computeInsightKey(insight);
4037
- currentKeys.add(key);
4038
- const existing = this.tracked.get(key);
4039
- if (existing) {
4040
- existing.insight = insight;
4041
- existing.lastSeenAt = now;
4042
- existing.consecutiveAbsences = 0;
4043
- if (existing.state === "resolved") {
4044
- existing.state = "open";
4045
- existing.resolvedAt = null;
4046
- }
4047
- } else {
4048
- this.tracked.set(key, {
4049
- key,
4050
- state: "open",
4051
- insight,
4052
- firstSeenAt: now,
4053
- lastSeenAt: now,
4054
- resolvedAt: null,
4055
- consecutiveAbsences: 0
4056
- });
4057
- }
4058
- }
4059
- for (const [key, stateful] of this.tracked) {
4060
- if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
4061
- stateful.consecutiveAbsences++;
4062
- if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
4063
- stateful.state = "resolved";
4064
- stateful.resolvedAt = now;
4065
- }
4066
- } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
4067
- this.tracked.delete(key);
4068
- }
4069
- }
4070
- return [...this.tracked.values()];
4071
- }
4072
- getAll() {
4073
- return [...this.tracked.values()];
4074
- }
4075
- clear() {
4076
- this.tracked.clear();
4077
- }
4078
- };
4079
4489
  }
4080
4490
  });
4081
4491
 
@@ -4084,22 +4494,23 @@ var AnalysisEngine;
4084
4494
  var init_engine = __esm({
4085
4495
  "src/analysis/engine.ts"() {
4086
4496
  "use strict";
4497
+ init_limits();
4087
4498
  init_disposable();
4088
4499
  init_group();
4089
4500
  init_rules();
4090
- init_insights3();
4091
- init_insight_tracker();
4501
+ init_insights2();
4502
+ init_issue_mappers();
4503
+ init_issue_id();
4504
+ init_prepare();
4092
4505
  AnalysisEngine = class {
4093
- constructor(registry, debounceMs = 300) {
4506
+ constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4094
4507
  this.registry = registry;
4095
4508
  this.debounceMs = debounceMs;
4096
4509
  this.scanner = createDefaultScanner();
4097
4510
  }
4098
4511
  scanner;
4099
- insightTracker = new InsightTracker();
4100
4512
  cachedInsights = [];
4101
4513
  cachedFindings = [];
4102
- cachedStatefulInsights = [];
4103
4514
  debounceTimer = null;
4104
4515
  subs = new SubscriptionBag();
4105
4516
  start() {
@@ -4122,12 +4533,6 @@ var init_engine = __esm({
4122
4533
  getFindings() {
4123
4534
  return this.cachedFindings;
4124
4535
  }
4125
- getStatefulFindings() {
4126
- return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
4127
- }
4128
- getStatefulInsights() {
4129
- return this.cachedStatefulInsights;
4130
- }
4131
4536
  scheduleRecompute() {
4132
4537
  if (this.debounceTimer) return;
4133
4538
  this.debounceTimer = setTimeout(() => {
@@ -4136,20 +4541,14 @@ var init_engine = __esm({
4136
4541
  }, this.debounceMs);
4137
4542
  }
4138
4543
  recompute() {
4139
- const requests = this.registry.get("request-store").getAll();
4544
+ const allRequests = this.registry.get("request-store").getAll();
4140
4545
  const queries = this.registry.get("query-store").getAll();
4141
4546
  const errors = this.registry.get("error-store").getAll();
4142
4547
  const logs = this.registry.get("log-store").getAll();
4143
4548
  const fetches = this.registry.get("fetch-store").getAll();
4549
+ const requests = windowByEndpoint(allRequests);
4144
4550
  const flows = groupRequestsIntoFlows(requests);
4145
4551
  this.cachedFindings = this.scanner.scan({ requests, logs });
4146
- if (this.registry.has("finding-store")) {
4147
- const findingStore = this.registry.get("finding-store");
4148
- for (const finding of this.cachedFindings) {
4149
- findingStore.upsert(finding, "passive");
4150
- }
4151
- findingStore.reconcilePassive(this.cachedFindings);
4152
- }
4153
4552
  this.cachedInsights = computeInsights({
4154
4553
  requests,
4155
4554
  queries,
@@ -4159,14 +4558,30 @@ var init_engine = __esm({
4159
4558
  previousMetrics: this.registry.get("metrics-store").getAll(),
4160
4559
  securityFindings: this.cachedFindings
4161
4560
  });
4162
- this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
4163
- const update = {
4164
- insights: this.cachedInsights,
4165
- findings: this.cachedFindings,
4166
- statefulFindings: this.getStatefulFindings(),
4167
- statefulInsights: this.cachedStatefulInsights
4168
- };
4169
- this.registry.get("event-bus").emit("analysis:updated", update);
4561
+ if (this.registry.has("issue-store")) {
4562
+ const issueStore = this.registry.get("issue-store");
4563
+ for (const finding of this.cachedFindings) {
4564
+ issueStore.upsert(securityFindingToIssue(finding), "passive");
4565
+ }
4566
+ for (const insight of this.cachedInsights) {
4567
+ issueStore.upsert(insightToIssue(insight), "passive");
4568
+ }
4569
+ const currentIssueIds = /* @__PURE__ */ new Set();
4570
+ for (const finding of this.cachedFindings) {
4571
+ currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
4572
+ }
4573
+ for (const insight of this.cachedInsights) {
4574
+ currentIssueIds.add(computeIssueId(insightToIssue(insight)));
4575
+ }
4576
+ const activeEndpoints = extractActiveEndpoints(allRequests);
4577
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
4578
+ const update = {
4579
+ insights: this.cachedInsights,
4580
+ findings: this.cachedFindings,
4581
+ issues: issueStore.getAll()
4582
+ };
4583
+ this.registry.get("event-bus").emit("analysis:updated", update);
4584
+ }
4170
4585
  }
4171
4586
  };
4172
4587
  }
@@ -4177,14 +4592,14 @@ var VERSION;
4177
4592
  var init_src = __esm({
4178
4593
  "src/index.ts"() {
4179
4594
  "use strict";
4180
- init_finding_store();
4595
+ init_issue_store();
4181
4596
  init_project();
4182
4597
  init_adapter_registry();
4183
4598
  init_rules();
4184
4599
  init_engine();
4185
- init_insights3();
4186
4600
  init_insights2();
4187
- VERSION = "0.8.4";
4601
+ init_insights();
4602
+ VERSION = "0.8.6";
4188
4603
  }
4189
4604
  });
4190
4605
 
@@ -4798,7 +5213,7 @@ function getFlowInsights() {
4798
5213
  }
4799
5214
  `;
4800
5215
  }
4801
- var init_insights4 = __esm({
5216
+ var init_insights3 = __esm({
4802
5217
  "src/dashboard/client/views/flows/insights.ts"() {
4803
5218
  "use strict";
4804
5219
  init_constants();
@@ -4873,7 +5288,7 @@ function getFlowDetail() {
4873
5288
  h += '<span>' + req.durationMs + 'ms</span>';
4874
5289
  if (req.responseSize) h += '<span>' + formatSize(req.responseSize) + '</span>';
4875
5290
  h += '</div>';
4876
- h += '<div class="request-timeline tl-hidden" data-request-id="' + req.id + '" data-request-started="' + req.startedAt + '"></div>';
5291
+ h += '<div class="request-timeline tl-hidden" data-request-id="' + escHtml(req.id) + '" data-request-started="' + escHtml(String(req.startedAt)) + '"></div>';
4877
5292
  h += '<div class="detail-grid">';
4878
5293
  h += '<div class="detail-section"><h4>Request Headers</h4><pre>' + formatHeaders(req.headers) + '</pre></div>';
4879
5294
  h += '<div class="detail-section"><h4>Response Headers</h4><pre>' + formatHeaders(req.responseHeaders) + '</pre></div>';
@@ -4990,7 +5405,7 @@ function getFlowsView() {
4990
5405
  var init_flows2 = __esm({
4991
5406
  "src/dashboard/client/views/flows.ts"() {
4992
5407
  "use strict";
4993
- init_insights4();
5408
+ init_insights3();
4994
5409
  init_detail();
4995
5410
  }
4996
5411
  });
@@ -6130,8 +6545,8 @@ function getOverviewRender() {
6130
6545
  '<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
6131
6546
  container.appendChild(summary);
6132
6547
 
6133
- var all = state.insights || [];
6134
- var open = all.filter(function(si) { return si.state === 'open'; });
6548
+ var all = state.issues || [];
6549
+ var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing' || si.state === 'regressed'; });
6135
6550
  var resolved = all.filter(function(si) { return si.state === 'resolved'; });
6136
6551
 
6137
6552
  if (open.length === 0 && resolved.length === 0) {
@@ -6163,24 +6578,35 @@ function getOverviewRender() {
6163
6578
 
6164
6579
  for (var i = 0; i < open.length; i++) {
6165
6580
  (function(si) {
6166
- var insight = si.insight;
6581
+ var issue = si.issue;
6167
6582
  var card = document.createElement('div');
6168
6583
  card.className = 'ov-card';
6169
6584
 
6170
- var sevCfg = SEV[insight.severity];
6585
+ var sevCfg = SEV[issue.severity];
6171
6586
  var iconCls = sevCfg.cls;
6172
6587
  var iconChar = sevCfg.icon;
6173
6588
 
6174
6589
  var expandHtml = '';
6175
- if (insight.detail) expandHtml += insight.detail;
6176
- if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6177
- expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6590
+ if (issue.detail) expandHtml += issue.detail;
6591
+ if (issue.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(issue.hint) + '</div>';
6592
+ if (issue.nav) expandHtml += '<span class="ov-card-link" data-nav="' + issue.nav + '">View in ' + (NAV_LABELS[issue.nav] || issue.nav) + ' \\u2192</span>';
6593
+
6594
+ var aiBadge = '';
6595
+ if (si.state === 'fixing' && si.aiStatus === 'fixed') {
6596
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
6597
+ } else if (si.aiStatus === 'wont_fix') {
6598
+ aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
6599
+ } else if (si.state === 'regressed') {
6600
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
6601
+ }
6602
+
6603
+ var occBadge = si.occurrences > 1 ? ' <span class="sec-item-count">' + si.occurrences + 'x</span>' : '';
6178
6604
 
6179
6605
  card.innerHTML =
6180
6606
  '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6181
6607
  '<div class="ov-card-body">' +
6182
- '<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
6183
- '<div class="ov-card-desc">' + insight.desc + '</div>' +
6608
+ '<div class="ov-card-title">' + escHtml(issue.title) + occBadge + aiBadge + '</div>' +
6609
+ '<div class="ov-card-desc">' + issue.desc + '</div>' +
6184
6610
  '<div class="ov-card-expand">' + expandHtml + '</div>' +
6185
6611
  '</div>' +
6186
6612
  '<span class="ov-card-arrow">\\u2192</span>';
@@ -6226,14 +6652,14 @@ function getOverviewRender() {
6226
6652
  resolvedCards.className = 'ov-cards';
6227
6653
 
6228
6654
  for (var ri = 0; ri < resolved.length; ri++) {
6229
- var rInsight = resolved[ri].insight;
6655
+ var rIssue = resolved[ri].issue;
6230
6656
  var rCard = document.createElement('div');
6231
6657
  rCard.className = 'ov-card ov-card-resolved';
6232
6658
  rCard.innerHTML =
6233
6659
  '<span class="ov-card-icon resolved">\\u2713</span>' +
6234
6660
  '<div class="ov-card-body">' +
6235
- '<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rInsight.title) + '</div>' +
6236
- '<div class="ov-card-desc">' + rInsight.desc + '</div>' +
6661
+ '<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rIssue.title) + '</div>' +
6662
+ '<div class="ov-card-desc">' + rIssue.desc + '</div>' +
6237
6663
  '</div>';
6238
6664
  resolvedCards.appendChild(rCard);
6239
6665
  }
@@ -6271,11 +6697,12 @@ function getSecurityView() {
6271
6697
  container.innerHTML = '';
6272
6698
  var SEV = ${SEVERITY_MAP};
6273
6699
 
6274
- var all = state.findings || [];
6275
- var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
6700
+ var all = (state.issues || []).slice();
6701
+ var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing' || f.state === 'regressed'; });
6276
6702
  var resolved = all.filter(function(f) { return f.state === 'resolved'; });
6703
+ var stale = all.filter(function(f) { return f.state === 'stale'; });
6277
6704
 
6278
- if (open.length === 0 && resolved.length === 0) {
6705
+ if (open.length === 0 && resolved.length === 0 && stale.length === 0) {
6279
6706
  var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
6280
6707
  if (!hasData) {
6281
6708
  container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
@@ -6287,7 +6714,7 @@ function getSecurityView() {
6287
6714
 
6288
6715
  var critCount = 0, warnCount = 0, infoCount = 0;
6289
6716
  for (var ci = 0; ci < open.length; ci++) {
6290
- var sev = open[ci].finding.severity;
6717
+ var sev = open[ci].issue.severity;
6291
6718
  if (sev === 'critical') critCount++;
6292
6719
  else if (sev === 'info') infoCount++;
6293
6720
  else warnCount++;
@@ -6319,12 +6746,13 @@ function getSecurityView() {
6319
6746
  var groups = {};
6320
6747
  var groupOrder = [];
6321
6748
  for (var gi = 0; gi < open.length; gi++) {
6322
- var f = open[gi].finding;
6749
+ var sf = open[gi];
6750
+ var f = sf.issue;
6323
6751
  if (!groups[f.rule]) {
6324
6752
  groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6325
6753
  groupOrder.push(f.rule);
6326
6754
  }
6327
- groups[f.rule].items.push(f);
6755
+ groups[f.rule].items.push(sf);
6328
6756
  }
6329
6757
 
6330
6758
  groupOrder.sort(function(a, b) {
@@ -6361,12 +6789,24 @@ function getSecurityView() {
6361
6789
  var list = document.createElement('div');
6362
6790
  list.className = 'sec-items';
6363
6791
  for (var ii = 0; ii < group.items.length; ii++) {
6364
- var item = group.items[ii];
6792
+ var sf2 = group.items[ii];
6793
+ var item = sf2.issue;
6365
6794
  var row = document.createElement('div');
6366
6795
  row.className = 'sec-item';
6796
+ var aiBadge = '';
6797
+ if (sf2.state === 'fixing' && sf2.aiStatus === 'fixed') {
6798
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
6799
+ } else if (sf2.aiStatus === 'wont_fix') {
6800
+ aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
6801
+ } else if (sf2.state === 'regressed') {
6802
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
6803
+ }
6804
+ var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
6805
+ var occBadge = sf2.occurrences > 1 ? '<span class="sec-item-count">' + sf2.occurrences + 'x</span>' : '';
6367
6806
  row.innerHTML =
6368
6807
  '<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
6369
- (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
6808
+ occBadge +
6809
+ aiBadge + aiNotes;
6370
6810
  list.appendChild(row);
6371
6811
  }
6372
6812
  section.appendChild(list);
@@ -6385,17 +6825,45 @@ function getSecurityView() {
6385
6825
  var resolvedItems = document.createElement('div');
6386
6826
  resolvedItems.className = 'sec-items';
6387
6827
  for (var ri = 0; ri < resolved.length; ri++) {
6388
- var rf = resolved[ri].finding;
6828
+ var rsf = resolved[ri];
6829
+ var rf = rsf.issue;
6389
6830
  var rRow = document.createElement('div');
6390
6831
  rRow.className = 'sec-item sec-item-resolved';
6832
+ var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
6833
+ var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
6391
6834
  rRow.innerHTML =
6392
6835
  '<span class="sec-resolved-item-icon">\\u2713</span>' +
6393
- '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>';
6836
+ '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint || 'global') + '</div>' +
6837
+ verifiedBadge + rNotes;
6394
6838
  resolvedItems.appendChild(rRow);
6395
6839
  }
6396
6840
  resolvedGroup.appendChild(resolvedItems);
6397
6841
  container.appendChild(resolvedGroup);
6398
6842
  }
6843
+
6844
+ if (stale.length > 0) {
6845
+ var staleTitle = document.createElement('div');
6846
+ staleTitle.className = 'sec-resolved-title';
6847
+ staleTitle.innerHTML = '<span style="color:var(--text-muted)">\\u23F8</span> Stale <span class="sec-resolved-count">' + stale.length + '</span>';
6848
+ container.appendChild(staleTitle);
6849
+
6850
+ var staleGroup = document.createElement('div');
6851
+ staleGroup.className = 'sec-group sec-group-resolved';
6852
+ var staleItems = document.createElement('div');
6853
+ staleItems.className = 'sec-items';
6854
+ for (var sti = 0; sti < stale.length; sti++) {
6855
+ var ssf = stale[sti];
6856
+ var sf3 = ssf.issue;
6857
+ var sRow = document.createElement('div');
6858
+ sRow.className = 'sec-item sec-item-resolved';
6859
+ sRow.innerHTML =
6860
+ '<span style="color:var(--text-muted)">\\u23F8</span>' +
6861
+ '<div class="sec-item-desc" style="color:var(--text-muted)">' + escHtml(sf3.title) + ' \\u2014 endpoint inactive</div>';
6862
+ staleItems.appendChild(sRow);
6863
+ }
6864
+ staleGroup.appendChild(staleItems);
6865
+ container.appendChild(staleGroup);
6866
+ }
6399
6867
  }
6400
6868
  `;
6401
6869
  }
@@ -6433,13 +6901,7 @@ function getApp() {
6433
6901
  try {
6434
6902
  var res3 = await fetch('${DASHBOARD_API_INSIGHTS}');
6435
6903
  var data3 = await res3.json();
6436
- state.insights = data3.insights || [];
6437
- } catch(e) { console.warn('[brakit]', e); }
6438
-
6439
- try {
6440
- var res4 = await fetch('${DASHBOARD_API_SECURITY}');
6441
- var data4 = await res4.json();
6442
- state.findings = data4.findings || [];
6904
+ state.issues = data3.issues || [];
6443
6905
  } catch(e) { console.warn('[brakit]', e); }
6444
6906
 
6445
6907
  updateStats();
@@ -6478,14 +6940,9 @@ function getApp() {
6478
6940
  registerTelemetryListener('error_event', 'errors', prependErrorRow);
6479
6941
  registerTelemetryListener('query', 'queries', prependQueryRow);
6480
6942
 
6481
- events.addEventListener('insights', function(e) {
6482
- state.insights = JSON.parse(e.data);
6943
+ events.addEventListener('issues', function(e) {
6944
+ state.issues = JSON.parse(e.data);
6483
6945
  if (state.activeView === 'overview') renderOverview();
6484
- updateStats();
6485
- });
6486
-
6487
- events.addEventListener('security', function(e) {
6488
- state.findings = JSON.parse(e.data);
6489
6946
  if (state.activeView === 'security') renderSecurity();
6490
6947
  updateStats();
6491
6948
  });
@@ -6568,9 +7025,9 @@ function getApp() {
6568
7025
  if (queryCount) queryCount.textContent = state.queries.length;
6569
7026
  var secCount = document.getElementById('sidebar-count-security');
6570
7027
  if (secCount) {
6571
- var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
6572
- secCount.textContent = numFindings;
6573
- secCount.style.display = numFindings > 0 ? '' : 'none';
7028
+ var numIssues = (state.issues || []).filter(function(f) { return f.state !== 'resolved' && f.state !== 'stale'; }).length;
7029
+ secCount.textContent = numIssues;
7030
+ secCount.style.display = numIssues > 0 ? '' : 'none';
6574
7031
  }
6575
7032
  }
6576
7033
 
@@ -6588,7 +7045,7 @@ function getApp() {
6588
7045
  if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
6589
7046
  await fetch('${DASHBOARD_API_CLEAR}', {method: 'POST'});
6590
7047
  state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
6591
- state.insights = []; state.findings = [];
7048
+ state.issues = [];
6592
7049
  graphData = []; selectedEndpoint = ${ALL_ENDPOINTS_SELECTOR}; timelineCache = {};
6593
7050
  renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
6594
7051
  showToast('Cleared');
@@ -6681,10 +7138,10 @@ var init_page = __esm({
6681
7138
  });
6682
7139
 
6683
7140
  // src/telemetry/config.ts
6684
- import { homedir } from "os";
6685
- import { join as join2 } from "path";
7141
+ import { homedir as homedir2 } from "os";
7142
+ import { join as join3 } from "path";
6686
7143
  import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6687
- import { randomUUID as randomUUID3 } from "crypto";
7144
+ import { randomUUID as randomUUID4 } from "crypto";
6688
7145
  function readConfig() {
6689
7146
  try {
6690
7147
  if (!existsSync5(CONFIG_PATH)) return null;
@@ -6696,9 +7153,9 @@ function readConfig() {
6696
7153
  function writeConfig(config) {
6697
7154
  try {
6698
7155
  if (!existsSync5(CONFIG_DIR))
6699
- mkdirSync3(CONFIG_DIR, { recursive: true, mode: 448 });
7156
+ mkdirSync3(CONFIG_DIR, { recursive: true, mode: DIR_MODE_OWNER_ONLY });
6700
7157
  writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
6701
- mode: 384
7158
+ mode: FILE_MODE_OWNER_ONLY
6702
7159
  });
6703
7160
  } catch {
6704
7161
  }
@@ -6708,27 +7165,34 @@ function getOrCreateConfig() {
6708
7165
  if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
6709
7166
  return existing;
6710
7167
  }
6711
- const config = { telemetry: true, anonymousId: randomUUID3() };
7168
+ const config = { telemetry: true, anonymousId: randomUUID4() };
6712
7169
  writeConfig(config);
6713
7170
  return config;
6714
7171
  }
6715
7172
  function isTelemetryEnabled() {
7173
+ if (cachedEnabled !== null) return cachedEnabled;
6716
7174
  const env = process.env.BRAKIT_TELEMETRY;
6717
- if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
6718
- return readConfig()?.telemetry ?? true;
7175
+ if (env !== void 0) {
7176
+ cachedEnabled = env !== "false" && env !== "0" && env !== "off";
7177
+ return cachedEnabled;
7178
+ }
7179
+ cachedEnabled = readConfig()?.telemetry ?? true;
7180
+ return cachedEnabled;
6719
7181
  }
6720
- var CONFIG_DIR, CONFIG_PATH;
7182
+ var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
6721
7183
  var init_config = __esm({
6722
7184
  "src/telemetry/config.ts"() {
6723
7185
  "use strict";
6724
- CONFIG_DIR = join2(homedir(), ".brakit");
6725
- CONFIG_PATH = join2(CONFIG_DIR, "config.json");
7186
+ init_network();
7187
+ CONFIG_DIR = join3(homedir2(), ".brakit");
7188
+ CONFIG_PATH = join3(CONFIG_DIR, "config.json");
7189
+ cachedEnabled = null;
6726
7190
  }
6727
7191
  });
6728
7192
 
6729
7193
  // src/telemetry/index.ts
6730
7194
  import { platform, release, arch } from "os";
6731
- import { spawnSync } from "child_process";
7195
+ import { spawn } from "child_process";
6732
7196
  function initSession(framework, packageManager, isCustomCommand, adapters) {
6733
7197
  session.startTime = Date.now();
6734
7198
  session.framework = framework;
@@ -6753,12 +7217,12 @@ function recordDashboardOpened() {
6753
7217
  }
6754
7218
  function speedBucket(ms) {
6755
7219
  if (ms === 0) return "none";
6756
- if (ms < 200) return "<200ms";
6757
- if (ms < 500) return "200-500ms";
6758
- if (ms < 1e3) return "500-1000ms";
6759
- if (ms < 2e3) return "1000-2000ms";
6760
- if (ms < 5e3) return "2000-5000ms";
6761
- return ">5000ms";
7220
+ const t = SPEED_BUCKET_THRESHOLDS;
7221
+ if (ms < t[0]) return `<${t[0]}ms`;
7222
+ for (let i = 1; i < t.length; i++) {
7223
+ if (ms < t[i]) return `${t[i - 1]}-${t[i]}ms`;
7224
+ }
7225
+ return `>${t[t.length - 1]}ms`;
6762
7226
  }
6763
7227
  function trackSession(registry) {
6764
7228
  if (!isTelemetryEnabled()) return;
@@ -6815,14 +7279,15 @@ function trackSession(registry) {
6815
7279
  try {
6816
7280
  const body = JSON.stringify(payload);
6817
7281
  const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
6818
- spawnSync(
7282
+ const child = spawn(
6819
7283
  process.execPath,
6820
7284
  [
6821
7285
  "-e",
6822
7286
  `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
6823
7287
  ],
6824
- { timeout: POSTHOG_SPAWN_TIMEOUT_MS, stdio: "ignore" }
7288
+ { detached: true, stdio: "ignore" }
6825
7289
  );
7290
+ child.unref();
6826
7291
  } catch {
6827
7292
  }
6828
7293
  }
@@ -6857,7 +7322,6 @@ function isDashboardRequest(url) {
6857
7322
  }
6858
7323
  function createDashboardHandler(registry) {
6859
7324
  const metricsStore = registry.get("metrics-store");
6860
- const analysisEngine = registry.has("analysis-engine") ? registry.get("analysis-engine") : void 0;
6861
7325
  const routes = {
6862
7326
  [DASHBOARD_API_REQUESTS]: createRequestsHandler(registry),
6863
7327
  [DASHBOARD_API_EVENTS]: createSSEHandler(registry),
@@ -6872,12 +7336,15 @@ function createDashboardHandler(registry) {
6872
7336
  [DASHBOARD_API_INGEST]: createIngestHandler(registry),
6873
7337
  [DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
6874
7338
  };
6875
- if (analysisEngine) {
6876
- routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(analysisEngine);
6877
- routes[DASHBOARD_API_SECURITY] = createSecurityHandler(analysisEngine);
6878
- }
6879
- if (registry.has("finding-store")) {
6880
- routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(registry.get("finding-store"));
7339
+ if (registry.has("issue-store")) {
7340
+ const issueStore = registry.get("issue-store");
7341
+ routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
7342
+ routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
7343
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
7344
+ routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
7345
+ issueStore,
7346
+ registry.get("event-bus")
7347
+ );
6881
7348
  }
6882
7349
  routes[DASHBOARD_API_TAB] = (req, res) => {
6883
7350
  const raw = (req.url ?? "").split("tab=")[1];
@@ -6885,7 +7352,7 @@ function createDashboardHandler(registry) {
6885
7352
  const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
6886
7353
  if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
6887
7354
  }
6888
- res.writeHead(204);
7355
+ res.writeHead(HTTP_NO_CONTENT);
6889
7356
  res.end();
6890
7357
  };
6891
7358
  return (req, res, config) => {
@@ -6896,7 +7363,7 @@ function createDashboardHandler(registry) {
6896
7363
  return;
6897
7364
  }
6898
7365
  if (isTelemetryEnabled()) recordDashboardOpened();
6899
- res.writeHead(200, {
7366
+ res.writeHead(HTTP_OK, {
6900
7367
  "content-type": "text/html; charset=utf-8",
6901
7368
  "cache-control": "no-cache",
6902
7369
  ...SECURITY_HEADERS
@@ -6904,23 +7371,16 @@ function createDashboardHandler(registry) {
6904
7371
  res.end(getDashboardHtml(config));
6905
7372
  };
6906
7373
  }
6907
- var SECURITY_HEADERS;
6908
7374
  var init_router = __esm({
6909
7375
  "src/dashboard/router.ts"() {
6910
7376
  "use strict";
6911
7377
  init_constants();
7378
+ init_http();
6912
7379
  init_api();
6913
- init_insights();
6914
- init_findings();
7380
+ init_issues();
6915
7381
  init_sse();
6916
7382
  init_page();
6917
7383
  init_telemetry2();
6918
- SECURITY_HEADERS = {
6919
- "x-content-type-options": "nosniff",
6920
- "x-frame-options": "DENY",
6921
- "referrer-policy": "no-referrer",
6922
- "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
6923
- };
6924
7384
  }
6925
7385
  });
6926
7386
 
@@ -6929,6 +7389,7 @@ var EventBus;
6929
7389
  var init_event_bus = __esm({
6930
7390
  "src/core/event-bus.ts"() {
6931
7391
  "use strict";
7392
+ init_log();
6932
7393
  EventBus = class {
6933
7394
  listeners = /* @__PURE__ */ new Map();
6934
7395
  emit(channel, data) {
@@ -6937,7 +7398,8 @@ var init_event_bus = __esm({
6937
7398
  for (const fn of set) {
6938
7399
  try {
6939
7400
  fn(data);
6940
- } catch {
7401
+ } catch (err) {
7402
+ brakitDebug(`EventBus listener threw on channel "${channel}": ${err}`);
6941
7403
  }
6942
7404
  }
6943
7405
  }
@@ -7001,9 +7463,9 @@ var init_static_patterns = __esm({
7001
7463
  });
7002
7464
 
7003
7465
  // src/store/request-store.ts
7004
- function flattenHeaders(headers) {
7466
+ function flattenHeaders(headers2) {
7005
7467
  const flat = {};
7006
- for (const [key, value] of Object.entries(headers)) {
7468
+ for (const [key, value] of Object.entries(headers2)) {
7007
7469
  if (value === void 0) continue;
7008
7470
  flat[key] = Array.isArray(value) ? value.join(", ") : value;
7009
7471
  }
@@ -7059,6 +7521,15 @@ var init_request_store = __esm({
7059
7521
  }
7060
7522
  return entry;
7061
7523
  }
7524
+ add(entry) {
7525
+ this.requests.push(entry);
7526
+ if (this.requests.length > this.maxEntries) {
7527
+ this.requests.shift();
7528
+ }
7529
+ for (const fn of this.listeners) {
7530
+ fn(entry);
7531
+ }
7532
+ }
7062
7533
  getAll() {
7063
7534
  return this.requests;
7064
7535
  }
@@ -7077,7 +7548,7 @@ var init_request_store = __esm({
7077
7548
  });
7078
7549
 
7079
7550
  // src/store/telemetry-store.ts
7080
- import { randomUUID as randomUUID4 } from "crypto";
7551
+ import { randomUUID as randomUUID5 } from "crypto";
7081
7552
  var TelemetryStore;
7082
7553
  var init_telemetry_store = __esm({
7083
7554
  "src/store/telemetry-store.ts"() {
@@ -7090,7 +7561,7 @@ var init_telemetry_store = __esm({
7090
7561
  entries = [];
7091
7562
  listeners = [];
7092
7563
  add(data) {
7093
- const entry = { id: randomUUID4(), ...data };
7564
+ const entry = { id: randomUUID5(), ...data };
7094
7565
  this.entries.push(entry);
7095
7566
  if (this.entries.length > this.maxEntries) this.entries.shift();
7096
7567
  for (const fn of this.listeners) fn(entry);
@@ -7174,7 +7645,7 @@ var init_math = __esm({
7174
7645
  });
7175
7646
 
7176
7647
  // src/store/metrics/metrics-store.ts
7177
- import { randomUUID as randomUUID5 } from "crypto";
7648
+ import { randomUUID as randomUUID6 } from "crypto";
7178
7649
  function createAccumulator() {
7179
7650
  return {
7180
7651
  durations: [],
@@ -7194,23 +7665,29 @@ var init_metrics_store = __esm({
7194
7665
  "use strict";
7195
7666
  init_constants();
7196
7667
  init_math();
7668
+ init_http_status();
7197
7669
  init_endpoint();
7198
7670
  MetricsStore = class {
7199
7671
  constructor(persistence) {
7200
7672
  this.persistence = persistence;
7201
- this.data = persistence.load();
7202
- for (const ep of this.data.endpoints) {
7203
- this.endpointIndex.set(ep.endpoint, ep);
7204
- }
7673
+ this.data = { version: 1, endpoints: [] };
7205
7674
  }
7206
7675
  data;
7207
7676
  endpointIndex = /* @__PURE__ */ new Map();
7208
- sessionId = randomUUID5();
7677
+ sessionId = randomUUID6();
7209
7678
  sessionStart = Date.now();
7210
7679
  flushTimer = null;
7680
+ dirty = false;
7211
7681
  accumulators = /* @__PURE__ */ new Map();
7212
7682
  pendingPoints = /* @__PURE__ */ new Map();
7213
7683
  start() {
7684
+ this.persistence.loadAsync().then((data) => {
7685
+ this.data = data;
7686
+ for (const ep of this.data.endpoints) {
7687
+ this.endpointIndex.set(ep.endpoint, ep);
7688
+ }
7689
+ }).catch(() => {
7690
+ });
7214
7691
  this.flushTimer = setInterval(
7215
7692
  () => this.flush(),
7216
7693
  METRICS_FLUSH_INTERVAL_MS
@@ -7226,21 +7703,27 @@ var init_metrics_store = __esm({
7226
7703
  }
7227
7704
  recordRequest(req, metrics) {
7228
7705
  if (req.isStatic) return;
7706
+ this.dirty = true;
7229
7707
  const key = getEndpointKey(req.method, req.path);
7230
7708
  let acc = this.accumulators.get(key);
7231
7709
  if (!acc) {
7710
+ if (this.accumulators.size >= MAX_UNIQUE_ENDPOINTS) return;
7232
7711
  acc = createAccumulator();
7233
7712
  this.accumulators.set(key, acc);
7234
7713
  }
7235
- acc.durations.push(req.durationMs);
7236
- acc.queryCounts.push(metrics.queryCount);
7237
- if (req.statusCode >= 400) acc.errorCount++;
7714
+ if (acc.durations.length < MAX_ACCUMULATOR_ENTRIES) {
7715
+ acc.durations.push(req.durationMs);
7716
+ }
7717
+ if (acc.queryCounts.length < MAX_ACCUMULATOR_ENTRIES) {
7718
+ acc.queryCounts.push(metrics.queryCount);
7719
+ }
7720
+ if (isErrorStatus(req.statusCode)) acc.errorCount++;
7238
7721
  acc.totalDurationSum += req.durationMs;
7239
7722
  acc.totalRequestCount++;
7240
7723
  acc.totalQuerySum += metrics.queryCount;
7241
7724
  acc.totalQueryTimeMs += metrics.queryTimeMs;
7242
7725
  acc.totalFetchTimeMs += metrics.fetchTimeMs;
7243
- if (req.statusCode >= 400) acc.totalErrorCount++;
7726
+ if (isErrorStatus(req.statusCode)) acc.totalErrorCount++;
7244
7727
  const timestamp = Math.round(
7245
7728
  Date.now() - (performance.now() - req.startedAt)
7246
7729
  );
@@ -7254,10 +7737,13 @@ var init_metrics_store = __esm({
7254
7737
  };
7255
7738
  let pending2 = this.pendingPoints.get(key);
7256
7739
  if (!pending2) {
7740
+ if (this.pendingPoints.size >= MAX_UNIQUE_ENDPOINTS) return;
7257
7741
  pending2 = [];
7258
7742
  this.pendingPoints.set(key, pending2);
7259
7743
  }
7260
- pending2.push(point);
7744
+ if (pending2.length < MAX_ACCUMULATOR_ENTRIES) {
7745
+ pending2.push(point);
7746
+ }
7261
7747
  }
7262
7748
  getAll() {
7263
7749
  return this.data.endpoints;
@@ -7280,7 +7766,7 @@ var init_metrics_store = __esm({
7280
7766
  for (const [endpoint, requests] of merged) {
7281
7767
  if (requests.length === 0) continue;
7282
7768
  const durations = requests.map((r) => r.durationMs);
7283
- const errors = requests.filter((r) => r.statusCode >= 400).length;
7769
+ const errors = requests.filter((r) => isErrorStatus(r.statusCode)).length;
7284
7770
  const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
7285
7771
  const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
7286
7772
  const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
@@ -7310,6 +7796,7 @@ var init_metrics_store = __esm({
7310
7796
  this.endpointIndex.clear();
7311
7797
  this.accumulators.clear();
7312
7798
  this.pendingPoints.clear();
7799
+ this.dirty = false;
7313
7800
  this.persistence.remove();
7314
7801
  }
7315
7802
  flush(sync = false) {
@@ -7350,11 +7837,13 @@ var init_metrics_store = __esm({
7350
7837
  epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
7351
7838
  }
7352
7839
  this.pendingPoints.clear();
7840
+ if (!this.dirty) return;
7353
7841
  if (sync) {
7354
7842
  this.persistence.saveSync(this.data);
7355
7843
  } else {
7356
7844
  this.persistence.save(this.data);
7357
7845
  }
7846
+ this.dirty = false;
7358
7847
  }
7359
7848
  getOrCreateEndpoint(endpoint) {
7360
7849
  let ep = this.endpointIndex.get(endpoint);
@@ -7370,40 +7859,53 @@ var init_metrics_store = __esm({
7370
7859
  });
7371
7860
 
7372
7861
  // src/store/metrics/persistence.ts
7373
- import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync } from "fs";
7862
+ import { readFile as readFile4 } from "fs/promises";
7863
+ import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
7374
7864
  import { resolve as resolve3 } from "path";
7375
- var FileMetricsPersistence;
7865
+ var DEFAULT_METRICS, FileMetricsPersistence;
7376
7866
  var init_persistence = __esm({
7377
7867
  "src/store/metrics/persistence.ts"() {
7378
7868
  "use strict";
7379
7869
  init_constants();
7380
7870
  init_atomic_writer();
7871
+ init_fs();
7381
7872
  init_log();
7873
+ init_type_guards();
7874
+ DEFAULT_METRICS = { version: 1, endpoints: [] };
7382
7875
  FileMetricsPersistence = class {
7383
7876
  metricsPath;
7384
7877
  writer;
7385
- constructor(rootDir) {
7386
- this.metricsPath = resolve3(rootDir, METRICS_FILE);
7878
+ constructor(dataDir) {
7879
+ this.metricsPath = resolve3(dataDir, METRICS_FILE);
7387
7880
  this.writer = new AtomicWriter({
7388
- dir: resolve3(rootDir, METRICS_DIR),
7881
+ dir: dataDir,
7389
7882
  filePath: this.metricsPath,
7390
- gitignoreEntry: METRICS_DIR,
7391
7883
  label: "metrics"
7392
7884
  });
7393
7885
  }
7394
7886
  load() {
7395
7887
  try {
7396
7888
  if (existsSync6(this.metricsPath)) {
7397
- const raw = readFileSync4(this.metricsPath, "utf-8");
7398
- const parsed = JSON.parse(raw);
7399
- if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
7400
- return parsed;
7401
- }
7889
+ return this.parseMetrics(readFileSync4(this.metricsPath, "utf-8"));
7890
+ }
7891
+ } catch (err) {
7892
+ brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
7893
+ }
7894
+ return { ...DEFAULT_METRICS };
7895
+ }
7896
+ async loadAsync() {
7897
+ try {
7898
+ if (await fileExists(this.metricsPath)) {
7899
+ return this.parseMetrics(await readFile4(this.metricsPath, "utf-8"));
7402
7900
  }
7403
7901
  } catch (err) {
7404
- brakitWarn(`failed to load metrics: ${err.message}`);
7902
+ brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
7405
7903
  }
7406
- return { version: 1, endpoints: [] };
7904
+ return { ...DEFAULT_METRICS };
7905
+ }
7906
+ /** Parse and validate metrics JSON, returning default empty data on invalid input. */
7907
+ parseMetrics(raw) {
7908
+ return validateMetricsData(JSON.parse(raw)) ?? { ...DEFAULT_METRICS };
7407
7909
  }
7408
7910
  save(data) {
7409
7911
  this.writer.writeAsync(JSON.stringify(data));
@@ -7414,9 +7916,10 @@ var init_persistence = __esm({
7414
7916
  remove() {
7415
7917
  try {
7416
7918
  if (existsSync6(this.metricsPath)) {
7417
- unlinkSync(this.metricsPath);
7919
+ unlinkSync2(this.metricsPath);
7418
7920
  }
7419
- } catch {
7921
+ } catch (err) {
7922
+ brakitDebug(`failed to remove metrics file: ${getErrorMessage(err)}`);
7420
7923
  }
7421
7924
  }
7422
7925
  };
@@ -7453,14 +7956,14 @@ function colorTitle(severity, text) {
7453
7956
  function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
7454
7957
  return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
7455
7958
  }
7456
- function formatConsoleLine(insight, suffix) {
7457
- const icon = severityIcon(insight.severity);
7458
- const title = colorTitle(insight.severity, insight.title);
7459
- const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
7959
+ function formatConsoleLine(issue, suffix) {
7960
+ const icon = severityIcon(issue.severity);
7961
+ const title = colorTitle(issue.severity, issue.title);
7962
+ const desc = pc.dim(truncate(issue.desc) + (suffix ?? ""));
7460
7963
  let line = ` ${icon} ${title} \u2014 ${desc}`;
7461
- if (insight.detail) {
7964
+ if (issue.detail) {
7462
7965
  line += `
7463
- ${pc.dim("\u2514 " + insight.detail)}`;
7966
+ ${pc.dim("\u2514 " + issue.detail)}`;
7464
7967
  }
7465
7968
  return line;
7466
7969
  }
@@ -7470,26 +7973,37 @@ function startTerminalInsights(registry, proxyPort) {
7470
7973
  const printedKeys = /* @__PURE__ */ new Set();
7471
7974
  const resolvedKeys = /* @__PURE__ */ new Set();
7472
7975
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
7473
- return bus.on("analysis:updated", ({ statefulInsights }) => {
7976
+ return bus.on("analysis:updated", ({ issues }) => {
7474
7977
  const newLines = [];
7475
7978
  const resolvedLines = [];
7476
- for (const si of statefulInsights) {
7979
+ const regressedLines = [];
7980
+ for (const si of issues) {
7477
7981
  if (si.state === "resolved") {
7478
- if (resolvedKeys.has(si.key)) continue;
7479
- resolvedKeys.add(si.key);
7480
- printedKeys.delete(si.key);
7481
- const title = pc.green(pc.bold(`\u2713 ${si.insight.title}`));
7482
- const desc = pc.dim(truncate(si.insight.desc));
7982
+ if (resolvedKeys.has(si.issueId)) continue;
7983
+ resolvedKeys.add(si.issueId);
7984
+ printedKeys.delete(si.issueId);
7985
+ const title = pc.green(pc.bold(`\u2713 ${si.issue.title}`));
7986
+ const desc = pc.dim(truncate(si.issue.desc));
7483
7987
  resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
7484
7988
  continue;
7485
7989
  }
7486
- resolvedKeys.delete(si.key);
7487
- if (si.insight.severity === "info") continue;
7488
- if (printedKeys.has(si.key)) continue;
7489
- printedKeys.add(si.key);
7990
+ if (si.state === "regressed") {
7991
+ if (!printedKeys.has(si.issueId)) {
7992
+ printedKeys.add(si.issueId);
7993
+ resolvedKeys.delete(si.issueId);
7994
+ const title = pc.red(pc.bold(`\u26A0 ${si.issue.title}`));
7995
+ const desc = pc.dim(truncate(si.issue.desc));
7996
+ regressedLines.push(` ${title} \u2014 ${desc} ${pc.red("regressed")}`);
7997
+ }
7998
+ continue;
7999
+ }
8000
+ resolvedKeys.delete(si.issueId);
8001
+ if (si.issue.severity === "info") continue;
8002
+ if (printedKeys.has(si.issueId)) continue;
8003
+ printedKeys.add(si.issueId);
7490
8004
  let suffix;
7491
- if (si.insight.type === "slow") {
7492
- const endpoint = extractEndpointFromDesc(si.insight.desc);
8005
+ if (si.issue.rule === "slow") {
8006
+ const endpoint = si.issue.endpoint;
7493
8007
  if (endpoint) {
7494
8008
  const ep = metricsStore.getEndpoint(endpoint);
7495
8009
  if (ep && ep.sessions.length > 1) {
@@ -7498,7 +8012,7 @@ function startTerminalInsights(registry, proxyPort) {
7498
8012
  }
7499
8013
  }
7500
8014
  }
7501
- newLines.push(formatConsoleLine(si.insight, suffix));
8015
+ newLines.push(formatConsoleLine(si.issue, suffix));
7502
8016
  }
7503
8017
  if (newLines.length > 0) {
7504
8018
  print("");
@@ -7506,6 +8020,12 @@ function startTerminalInsights(registry, proxyPort) {
7506
8020
  print("");
7507
8021
  print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.dim("Dashboard:")} ${pc.underline(`http://${dashUrl}`)} ${pc.dim("or ask your AI:")} ${pc.bold('"Fix brakit findings"')}`);
7508
8022
  }
8023
+ if (regressedLines.length > 0) {
8024
+ print("");
8025
+ for (const line of regressedLines) print(line);
8026
+ print("");
8027
+ print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.red("Issues came back after being resolved!")}`);
8028
+ }
7509
8029
  if (resolvedLines.length > 0) {
7510
8030
  print("");
7511
8031
  for (const line of resolvedLines) print(line);
@@ -7522,7 +8042,6 @@ var init_terminal = __esm({
7522
8042
  init_constants();
7523
8043
  init_limits();
7524
8044
  init_severity();
7525
- init_endpoint();
7526
8045
  SEVERITY_COLOR = {
7527
8046
  critical: pc.red,
7528
8047
  warning: pc.yellow,
@@ -7540,17 +8059,28 @@ var init_health2 = __esm({
7540
8059
  BrakitHealth = class {
7541
8060
  errorCount = 0;
7542
8061
  disabled = false;
8062
+ disabledAt = 0;
7543
8063
  teardownFn = null;
7544
8064
  reportError() {
7545
8065
  this.errorCount++;
7546
8066
  if (this.errorCount >= MAX_HEALTH_ERRORS && !this.disabled) {
7547
8067
  this.disabled = true;
7548
- console.warn("brakit: too many errors, disabling for this session.");
8068
+ this.disabledAt = Date.now();
8069
+ try {
8070
+ process.stderr.write("brakit: too many errors, disabling temporarily.\n");
8071
+ } catch {
8072
+ }
7549
8073
  this.teardownFn?.();
7550
8074
  }
7551
8075
  }
7552
8076
  isActive() {
7553
- return !this.disabled;
8077
+ if (!this.disabled) return true;
8078
+ if (Date.now() - this.disabledAt > RECOVERY_WINDOW_MS) {
8079
+ this.disabled = false;
8080
+ this.errorCount = 0;
8081
+ return true;
8082
+ }
8083
+ return false;
7554
8084
  }
7555
8085
  setTeardown(fn) {
7556
8086
  this.teardownFn = fn;
@@ -7591,10 +8121,10 @@ var init_guard = __esm({
7591
8121
  });
7592
8122
 
7593
8123
  // src/runtime/capture.ts
7594
- import { gunzipSync, brotliDecompressSync, inflateSync } from "zlib";
7595
- function outgoingToIncoming(headers) {
8124
+ import { gunzip, brotliDecompress, inflate } from "zlib";
8125
+ function outgoingToIncoming(headers2) {
7596
8126
  const result = {};
7597
- for (const [key, value] of Object.entries(headers)) {
8127
+ for (const [key, value] of Object.entries(headers2)) {
7598
8128
  if (value === void 0) continue;
7599
8129
  if (Array.isArray(value)) {
7600
8130
  result[key] = value.map(String);
@@ -7604,15 +8134,14 @@ function outgoingToIncoming(headers) {
7604
8134
  }
7605
8135
  return result;
7606
8136
  }
7607
- function decompress(body, encoding) {
7608
- try {
7609
- if (encoding === CONTENT_ENCODING_GZIP) return gunzipSync(body);
7610
- if (encoding === CONTENT_ENCODING_BR) return brotliDecompressSync(body);
7611
- if (encoding === CONTENT_ENCODING_DEFLATE) return inflateSync(body);
7612
- } catch (e) {
7613
- brakitDebug(`decompress failed: ${e.message}`);
7614
- }
7615
- return body;
8137
+ function decompressAsync(body, encoding) {
8138
+ const decompressor = encoding === CONTENT_ENCODING_GZIP ? gunzip : encoding === CONTENT_ENCODING_BR ? brotliDecompress : encoding === CONTENT_ENCODING_DEFLATE ? inflate : null;
8139
+ if (!decompressor) return Promise.resolve(body);
8140
+ return new Promise((resolve5) => {
8141
+ decompressor(body, (err, result) => {
8142
+ resolve5(err ? body : result);
8143
+ });
8144
+ });
7616
8145
  }
7617
8146
  function toBuffer(chunk) {
7618
8147
  if (Buffer.isBuffer(chunk)) return chunk;
@@ -7627,18 +8156,23 @@ function captureInProcess(req, res, requestId, requestStore) {
7627
8156
  let resSize = 0;
7628
8157
  const originalWrite = res.write;
7629
8158
  const originalEnd = res.end;
8159
+ let truncated = false;
7630
8160
  res.write = function(...args) {
7631
8161
  try {
7632
8162
  const chunk = args[0];
7633
- if (chunk != null && typeof chunk !== "function" && resSize < DEFAULT_MAX_BODY_CAPTURE) {
7634
- const buf = toBuffer(chunk);
7635
- if (buf) {
7636
- resChunks.push(buf);
7637
- resSize += buf.length;
8163
+ if (chunk != null && typeof chunk !== "function") {
8164
+ if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
8165
+ const buf = toBuffer(chunk);
8166
+ if (buf) {
8167
+ resChunks.push(buf);
8168
+ resSize += buf.length;
8169
+ }
8170
+ } else {
8171
+ truncated = true;
7638
8172
  }
7639
8173
  }
7640
8174
  } catch (e) {
7641
- brakitDebug(`capture write: ${e.message}`);
8175
+ brakitDebug(`capture write: ${getErrorMessage(e)}`);
7642
8176
  }
7643
8177
  return originalWrite.apply(this, args);
7644
8178
  };
@@ -7652,33 +8186,39 @@ function captureInProcess(req, res, requestId, requestStore) {
7652
8186
  }
7653
8187
  }
7654
8188
  } catch (e) {
7655
- brakitDebug(`capture end: ${e.message}`);
8189
+ brakitDebug(`capture end: ${getErrorMessage(e)}`);
7656
8190
  }
7657
8191
  const result = originalEnd.apply(this, args);
7658
8192
  const endTime = performance.now();
7659
- try {
7660
- const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
7661
- let body = resChunks.length > 0 ? Buffer.concat(resChunks) : null;
7662
- if (body && encoding) {
7663
- body = decompress(body, encoding);
8193
+ const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
8194
+ const statusCode = res.statusCode;
8195
+ const responseHeaders = outgoingToIncoming(res.getHeaders());
8196
+ const responseContentType = String(res.getHeader("content-type") ?? "");
8197
+ const capturedChunks = resChunks.slice();
8198
+ void (async () => {
8199
+ try {
8200
+ let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
8201
+ if (body && encoding && !truncated) {
8202
+ body = await decompressAsync(body, encoding);
8203
+ }
8204
+ requestStore.capture({
8205
+ requestId,
8206
+ method,
8207
+ url: req.url ?? "/",
8208
+ requestHeaders: req.headers,
8209
+ requestBody: null,
8210
+ statusCode,
8211
+ responseHeaders,
8212
+ responseBody: body,
8213
+ responseContentType,
8214
+ startTime,
8215
+ endTime,
8216
+ config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
8217
+ });
8218
+ } catch (e) {
8219
+ brakitDebug(`capture store: ${getErrorMessage(e)}`);
7664
8220
  }
7665
- requestStore.capture({
7666
- requestId,
7667
- method,
7668
- url: req.url ?? "/",
7669
- requestHeaders: req.headers,
7670
- requestBody: null,
7671
- statusCode: res.statusCode,
7672
- responseHeaders: outgoingToIncoming(res.getHeaders()),
7673
- responseBody: body,
7674
- responseContentType: String(res.getHeader("content-type") ?? ""),
7675
- startTime,
7676
- endTime,
7677
- config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
7678
- });
7679
- } catch (e) {
7680
- brakitDebug(`capture store: ${e.message}`);
7681
- }
8221
+ })();
7682
8222
  return result;
7683
8223
  };
7684
8224
  }
@@ -7687,12 +8227,13 @@ var init_capture = __esm({
7687
8227
  "use strict";
7688
8228
  init_constants();
7689
8229
  init_log();
8230
+ init_type_guards();
7690
8231
  }
7691
8232
  });
7692
8233
 
7693
8234
  // src/runtime/interceptor.ts
7694
8235
  import http from "http";
7695
- import { randomUUID as randomUUID6 } from "crypto";
8236
+ import { randomUUID as randomUUID7 } from "crypto";
7696
8237
  function installInterceptor(deps) {
7697
8238
  originalEmit = http.Server.prototype.emit;
7698
8239
  const saved = originalEmit;
@@ -7718,14 +8259,14 @@ function installInterceptor(deps) {
7718
8259
  }
7719
8260
  if (isDashboardRequest(url)) {
7720
8261
  if (!isLocalRequest(req)) {
7721
- res.writeHead(404);
8262
+ res.writeHead(HTTP_NOT_FOUND);
7722
8263
  res.end("Not Found");
7723
8264
  return true;
7724
8265
  }
7725
8266
  deps.handleDashboard(req, res, deps.config);
7726
8267
  return true;
7727
8268
  }
7728
- const requestId = randomUUID6();
8269
+ const requestId = randomUUID7();
7729
8270
  const ctx = {
7730
8271
  requestId,
7731
8272
  url,
@@ -7754,6 +8295,7 @@ var init_interceptor = __esm({
7754
8295
  init_safe_wrap();
7755
8296
  init_guard();
7756
8297
  init_capture();
8298
+ init_http();
7757
8299
  originalEmit = null;
7758
8300
  }
7759
8301
  });
@@ -7763,11 +8305,16 @@ var setup_exports = {};
7763
8305
  __export(setup_exports, {
7764
8306
  setup: () => setup
7765
8307
  });
7766
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
8308
+ import { readFile as readFile5, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
8309
+ import { existsSync as existsSync7, unlinkSync as unlinkSync3 } from "fs";
7767
8310
  import { resolve as resolve4 } from "path";
7768
8311
  function setup() {
7769
- if (initialized) return;
7770
- initialized = true;
8312
+ if (initPromise) return initPromise;
8313
+ initPromise = doSetup();
8314
+ return initPromise;
8315
+ }
8316
+ async function doSetup() {
8317
+ brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
7771
8318
  const bus = new EventBus();
7772
8319
  const registry = new ServiceRegistry();
7773
8320
  const requestStore = new RequestStore();
@@ -7798,7 +8345,9 @@ function setup() {
7798
8345
  const cwd = process.cwd();
7799
8346
  let framework = "unknown";
7800
8347
  try {
7801
- const pkg = JSON.parse(readFileSync5(resolve4(cwd, "package.json"), "utf-8"));
8348
+ const pkg = JSON.parse(
8349
+ await readFile5(resolve4(cwd, "package.json"), "utf-8")
8350
+ );
7802
8351
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7803
8352
  framework = detectFrameworkFromDeps(allDeps);
7804
8353
  } catch {
@@ -7809,12 +8358,13 @@ function setup() {
7809
8358
  false,
7810
8359
  adapterRegistry.getActive().map((a) => a.name)
7811
8360
  );
7812
- const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
8361
+ const dataDir = getProjectDataDir(cwd);
8362
+ const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
7813
8363
  metricsStore.start();
7814
8364
  registry.register("metrics-store", metricsStore);
7815
- const findingStore = new FindingStore(cwd);
7816
- findingStore.start();
7817
- registry.register("finding-store", findingStore);
8365
+ const issueStore = new IssueStore(dataDir);
8366
+ issueStore.start();
8367
+ registry.register("issue-store", issueStore);
7818
8368
  const analysisEngine = new AnalysisEngine(registry);
7819
8369
  analysisEngine.start();
7820
8370
  registry.register("analysis-engine", analysisEngine);
@@ -7841,51 +8391,74 @@ function setup() {
7841
8391
  requestStore,
7842
8392
  onFirstRequest(port) {
7843
8393
  setBrakitPort(port);
7844
- const dir = resolve4(cwd, METRICS_DIR);
7845
- if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
7846
- const portPath = resolve4(cwd, PORT_FILE);
7847
- if (existsSync7(portPath)) {
7848
- const old = readFileSync5(portPath, "utf-8").trim();
7849
- if (old && old !== String(port)) {
7850
- brakitDebug(`Overwriting stale port file (was ${old}, now ${port})`);
8394
+ brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
8395
+ void (async () => {
8396
+ try {
8397
+ const dir = resolve4(cwd, METRICS_DIR);
8398
+ await mkdir2(dir, { recursive: true });
8399
+ const portPath = resolve4(cwd, PORT_FILE);
8400
+ try {
8401
+ const old = await readFile5(portPath, "utf-8");
8402
+ if (old.trim() === String(port)) {
8403
+ brakitDebug(`[setup] port file already correct, skipping write`);
8404
+ return;
8405
+ }
8406
+ if (old.trim()) {
8407
+ brakitDebug(
8408
+ `Overwriting stale port file (was ${old.trim()}, now ${port})`
8409
+ );
8410
+ }
8411
+ } catch {
8412
+ brakitDebug(`[setup] no existing port file, will create`);
8413
+ }
8414
+ await writeFile3(portPath, String(port));
8415
+ brakitDebug(`[setup] wrote port file: ${portPath}`);
8416
+ } catch (err) {
8417
+ brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
7851
8418
  }
7852
- }
7853
- writeFileSync4(portPath, String(port));
8419
+ })();
7854
8420
  terminalDispose = startTerminalInsights(registry, port);
7855
- process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
7856
- `);
8421
+ process.stdout.write(
8422
+ ` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
8423
+ `
8424
+ );
7857
8425
  }
7858
8426
  });
7859
- let teardownCalled = false;
7860
- const runTeardown = () => {
7861
- if (teardownCalled) return;
7862
- teardownCalled = true;
8427
+ let telemetrySent = false;
8428
+ const sendTelemetry = () => {
8429
+ if (telemetrySent) return;
8430
+ telemetrySent = true;
7863
8431
  recordRequestCount(requestStore.getAll().length);
7864
8432
  recordInsightTypes(analysisEngine.getInsights().map((i) => i.type));
7865
8433
  recordRulesTriggered(analysisEngine.getFindings().map((f) => f.rule));
7866
8434
  trackSession(registry);
8435
+ };
8436
+ let teardownCalled = false;
8437
+ const runTeardown = () => {
8438
+ if (teardownCalled) return;
8439
+ teardownCalled = true;
8440
+ sendTelemetry();
7867
8441
  uninstallInterceptor();
7868
8442
  terminalDispose?.();
7869
8443
  analysisEngine.stop();
7870
- findingStore.stop();
8444
+ issueStore.stop();
7871
8445
  metricsStore.stop();
7872
8446
  try {
7873
8447
  const portPath = resolve4(cwd, PORT_FILE);
7874
- if (existsSync7(portPath)) unlinkSync2(portPath);
7875
- } catch {
8448
+ if (existsSync7(portPath)) unlinkSync3(portPath);
8449
+ } catch (err) {
8450
+ brakitDebug(`[setup] port file cleanup failed: ${getErrorMessage(err)}`);
7876
8451
  }
7877
8452
  };
7878
8453
  health.setTeardown(runTeardown);
7879
- process.once("SIGINT", () => {
7880
- runTeardown();
7881
- process.exit(SIGNAL_EXIT_SIGINT);
8454
+ process.on("beforeExit", () => {
8455
+ sendTelemetry();
7882
8456
  });
7883
- process.once("SIGTERM", () => {
8457
+ process.on("exit", () => {
7884
8458
  runTeardown();
7885
- process.exit(SIGNAL_EXIT_SIGTERM);
7886
8459
  });
7887
8460
  }
7888
- var initialized;
8461
+ var initPromise;
7889
8462
  var init_setup = __esm({
7890
8463
  "src/runtime/setup.ts"() {
7891
8464
  "use strict";
@@ -7902,18 +8475,19 @@ var init_setup = __esm({
7902
8475
  init_error_store();
7903
8476
  init_query_store();
7904
8477
  init_store();
7905
- init_finding_store();
8478
+ init_issue_store();
7906
8479
  init_engine();
7907
8480
  init_terminal();
7908
8481
  init_src();
7909
8482
  init_constants();
7910
- init_telemetry();
7911
8483
  init_health2();
7912
8484
  init_interceptor();
7913
8485
  init_log();
8486
+ init_type_guards();
8487
+ init_fs();
7914
8488
  init_project();
7915
8489
  init_telemetry2();
7916
- initialized = false;
8490
+ initPromise = null;
7917
8491
  }
7918
8492
  });
7919
8493
 
@@ -7932,7 +8506,7 @@ function shouldActivate() {
7932
8506
  if (shouldActivate()) {
7933
8507
  try {
7934
8508
  const { setup: setup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
7935
- setup2();
8509
+ await setup2();
7936
8510
  } catch (err) {
7937
8511
  console.warn("brakit: failed to start \u2014", err?.message);
7938
8512
  }