brakit 0.8.3 → 0.8.5

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, FINDING_ID_HASH_LENGTH, FINDINGS_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER;
49
51
  var init_limits = __esm({
50
52
  "src/constants/limits.ts"() {
51
53
  "use strict";
@@ -54,10 +56,15 @@ 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
+ FINDING_ID_HASH_LENGTH = 16;
66
+ FINDINGS_DATA_VERSION = 1;
67
+ SENSITIVE_MASK_PLACEHOLDER = "****";
61
68
  }
62
69
  });
63
70
 
@@ -104,31 +111,23 @@ var init_transport = __esm({
104
111
  "src/constants/transport.ts"() {
105
112
  "use strict";
106
113
  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
- ];
114
+ NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
115
+ NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
117
116
  }
118
117
  });
119
118
 
120
119
  // 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;
120
+ var METRICS_DIR, METRICS_FILE, PORT_FILE, FINDINGS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, FINDINGS_FLUSH_INTERVAL_MS;
122
121
  var init_metrics = __esm({
123
122
  "src/constants/metrics.ts"() {
124
123
  "use strict";
125
124
  METRICS_DIR = ".brakit";
126
125
  METRICS_FILE = ".brakit/metrics.json";
126
+ PORT_FILE = ".brakit/port";
127
+ FINDINGS_FILE = ".brakit/findings.json";
127
128
  METRICS_FLUSH_INTERVAL_MS = 3e4;
128
129
  METRICS_MAX_SESSIONS = 50;
129
130
  METRICS_MAX_DATA_POINTS = 200;
130
- PORT_FILE = ".brakit/port";
131
- FINDINGS_FILE = ".brakit/findings.json";
132
131
  FINDINGS_FLUSH_INTERVAL_MS = 1e4;
133
132
  }
134
133
  });
@@ -150,38 +149,36 @@ var init_headers = __esm({
150
149
  });
151
150
 
152
151
  // src/constants/network.ts
153
- var LOCALHOST_IPS, LOCALHOST_HOSTNAMES, CLOUD_SIGNALS, MAX_HEALTH_ERRORS;
152
+ var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE;
154
153
  var init_network = __esm({
155
154
  "src/constants/network.ts"() {
156
155
  "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
156
  CLOUD_SIGNALS = [
168
157
  "VERCEL",
169
158
  "VERCEL_ENV",
170
159
  "NETLIFY",
171
160
  "AWS_LAMBDA_FUNCTION_NAME",
172
161
  "AWS_EXECUTION_ENV",
162
+ "ECS_CONTAINER_METADATA_URI",
173
163
  "GOOGLE_CLOUD_PROJECT",
174
164
  "GCP_PROJECT",
165
+ "K_SERVICE",
175
166
  "AZURE_FUNCTIONS_ENVIRONMENT",
167
+ "WEBSITE_SITE_NAME",
176
168
  "FLY_APP_NAME",
177
169
  "RAILWAY_ENVIRONMENT",
178
170
  "RENDER",
179
- "HEROKU",
171
+ "HEROKU_APP_NAME",
172
+ "DYNO",
173
+ "CF_INSTANCE_GUID",
180
174
  "CF_PAGES",
181
- "KUBERNETES_SERVICE_HOST",
182
- "ECS_CONTAINER_METADATA_URI"
175
+ "KUBERNETES_SERVICE_HOST"
183
176
  ];
184
177
  MAX_HEALTH_ERRORS = 10;
178
+ RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
179
+ LOCALHOST_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
180
+ LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
181
+ URL_PARSE_BASE = "http://localhost";
185
182
  }
186
183
  });
187
184
 
@@ -204,7 +201,7 @@ var init_encoding = __esm({
204
201
  });
205
202
 
206
203
  // src/constants/severity.ts
207
- var SEVERITY_ICON, SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
204
+ var SEVERITY_ICON;
208
205
  var init_severity = __esm({
209
206
  "src/constants/severity.ts"() {
210
207
  "use strict";
@@ -213,14 +210,27 @@ var init_severity = __esm({
213
210
  warning: "\u26A0",
214
211
  info: "\u2139"
215
212
  };
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
- };
213
+ }
214
+ });
215
+
216
+ // src/constants/telemetry.ts
217
+ var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS;
218
+ var init_telemetry = __esm({
219
+ "src/constants/telemetry.ts"() {
220
+ "use strict";
221
+ POSTHOG_HOST = "https://us.i.posthog.com";
222
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/";
223
+ POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
224
+ }
225
+ });
226
+
227
+ // src/constants/lifecycle.ts
228
+ var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES;
229
+ var init_lifecycle = __esm({
230
+ "src/constants/lifecycle.ts"() {
231
+ "use strict";
232
+ VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
233
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
224
234
  }
225
235
  });
226
236
 
@@ -238,6 +248,8 @@ var init_constants = __esm({
238
248
  init_mcp();
239
249
  init_encoding();
240
250
  init_severity();
251
+ init_telemetry();
252
+ init_lifecycle();
241
253
  }
242
254
  });
243
255
 
@@ -404,11 +416,12 @@ function createCaptureError(emit) {
404
416
  }
405
417
  function setupErrorHook(emit) {
406
418
  const captureError = createCaptureError(emit);
407
- process.on("uncaughtException", (err) => {
419
+ const brakitExceptionHandler = (err) => {
408
420
  captureError(err);
409
- process.removeAllListeners("uncaughtException");
421
+ process.removeListener("uncaughtException", brakitExceptionHandler);
410
422
  throw err;
411
- });
423
+ };
424
+ process.on("uncaughtException", brakitExceptionHandler);
412
425
  process.on("unhandledRejection", (reason) => {
413
426
  captureError(reason);
414
427
  });
@@ -595,7 +608,10 @@ var init_pg = __esm({
595
608
  const result = saved.apply(this, args);
596
609
  if (result && typeof result.then === "function") {
597
610
  return result.then((res) => {
598
- emitQuery(res?.rowCount ?? void 0);
611
+ try {
612
+ emitQuery(res?.rowCount ?? void 0);
613
+ } catch {
614
+ }
599
615
  return res;
600
616
  });
601
617
  }
@@ -673,7 +689,10 @@ var init_mysql2 = __esm({
673
689
  const result = orig.apply(this, args);
674
690
  if (result && typeof result.then === "function") {
675
691
  return result.then((res) => {
676
- emitQuery();
692
+ try {
693
+ emitQuery();
694
+ } catch {
695
+ }
677
696
  return res;
678
697
  });
679
698
  }
@@ -788,6 +807,27 @@ var init_adapters = __esm({
788
807
  }
789
808
  });
790
809
 
810
+ // src/constants/http.ts
811
+ 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;
812
+ var init_http = __esm({
813
+ "src/constants/http.ts"() {
814
+ "use strict";
815
+ HTTP_OK = 200;
816
+ HTTP_NO_CONTENT = 204;
817
+ HTTP_BAD_REQUEST = 400;
818
+ HTTP_NOT_FOUND = 404;
819
+ HTTP_METHOD_NOT_ALLOWED = 405;
820
+ HTTP_PAYLOAD_TOO_LARGE = 413;
821
+ HTTP_INTERNAL_ERROR = 500;
822
+ SECURITY_HEADERS = {
823
+ "x-content-type-options": "nosniff",
824
+ "x-frame-options": "DENY",
825
+ "referrer-policy": "no-referrer",
826
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
827
+ };
828
+ }
829
+ });
830
+
791
831
  // src/analysis/categorize.ts
792
832
  function detectCategory(req) {
793
833
  const { method, url, statusCode, responseHeaders } = req;
@@ -1169,12 +1209,12 @@ var init_group = __esm({
1169
1209
  });
1170
1210
 
1171
1211
  // src/dashboard/api/shared.ts
1172
- function maskSensitiveHeaders(headers) {
1212
+ function maskSensitiveHeaders(headers2) {
1173
1213
  const masked = {};
1174
- for (const [key, value] of Object.entries(headers)) {
1214
+ for (const [key, value] of Object.entries(headers2)) {
1175
1215
  if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1176
1216
  const s = String(value);
1177
- masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? "****" : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
1217
+ masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? SENSITIVE_MASK_PLACEHOLDER : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
1178
1218
  } else {
1179
1219
  masked[key] = value;
1180
1220
  }
@@ -1194,14 +1234,14 @@ function getCorsOrigin(req) {
1194
1234
  }
1195
1235
  function getJsonHeaders(req) {
1196
1236
  const corsOrigin = getCorsOrigin(req);
1197
- const headers = {
1237
+ const headers2 = {
1198
1238
  "content-type": "application/json",
1199
1239
  "cache-control": "no-cache"
1200
1240
  };
1201
1241
  if (corsOrigin) {
1202
- headers["access-control-allow-origin"] = corsOrigin;
1242
+ headers2["access-control-allow-origin"] = corsOrigin;
1203
1243
  }
1204
- return headers;
1244
+ return headers2;
1205
1245
  }
1206
1246
  function sendJson(req, res, status, data) {
1207
1247
  res.writeHead(status, getJsonHeaders(req));
@@ -1209,23 +1249,58 @@ function sendJson(req, res, status, data) {
1209
1249
  }
1210
1250
  function requireGet(req, res) {
1211
1251
  if (req.method !== "GET") {
1212
- sendJson(req, res, 405, { error: "Method not allowed" });
1252
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1213
1253
  return false;
1214
1254
  }
1215
1255
  return true;
1216
1256
  }
1257
+ function parseRequestUrl(req) {
1258
+ return new URL(req.url ?? "/", URL_PARSE_BASE);
1259
+ }
1260
+ function readJsonBody(req, res, maxBytes = MAX_JSON_BODY_BYTES) {
1261
+ return new Promise((resolve5) => {
1262
+ const chunks = [];
1263
+ let size = 0;
1264
+ req.on("data", (chunk) => {
1265
+ size += chunk.length;
1266
+ if (size > maxBytes) {
1267
+ sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
1268
+ req.destroy();
1269
+ resolve5(null);
1270
+ return;
1271
+ }
1272
+ chunks.push(chunk);
1273
+ });
1274
+ req.on("end", () => {
1275
+ if (size > maxBytes) {
1276
+ resolve5(null);
1277
+ return;
1278
+ }
1279
+ try {
1280
+ resolve5(JSON.parse(Buffer.concat(chunks).toString()));
1281
+ } catch {
1282
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON body" });
1283
+ resolve5(null);
1284
+ }
1285
+ });
1286
+ req.on("error", () => {
1287
+ resolve5(null);
1288
+ });
1289
+ });
1290
+ }
1217
1291
  function handleTelemetryGet(req, res, store) {
1218
1292
  if (!requireGet(req, res)) return;
1219
- const url = new URL(req.url ?? "/", "http://localhost");
1293
+ const url = parseRequestUrl(req);
1220
1294
  const requestId = url.searchParams.get("requestId");
1221
1295
  const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1222
- sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1296
+ sendJson(req, res, HTTP_OK, { total: entries.length, entries: entries.reverse() });
1223
1297
  }
1224
1298
  var init_shared2 = __esm({
1225
1299
  "src/dashboard/api/shared.ts"() {
1226
1300
  "use strict";
1227
1301
  init_constants();
1228
1302
  init_limits();
1303
+ init_http();
1229
1304
  }
1230
1305
  });
1231
1306
 
@@ -1240,7 +1315,7 @@ function sanitizeRequest(r) {
1240
1315
  function createRequestsHandler(registry) {
1241
1316
  return (req, res) => {
1242
1317
  if (!requireGet(req, res)) return;
1243
- const url = new URL(req.url ?? "/", "http://localhost");
1318
+ const url = parseRequestUrl(req);
1244
1319
  const method = url.searchParams.get("method");
1245
1320
  const status = url.searchParams.get("status");
1246
1321
  const search = url.searchParams.get("search");
@@ -1273,7 +1348,7 @@ function createRequestsHandler(registry) {
1273
1348
  const total = results.length;
1274
1349
  results = results.slice(offset, offset + limit);
1275
1350
  const sanitized = results.map(sanitizeRequest);
1276
- sendJson(req, res, 200, { total, requests: sanitized });
1351
+ sendJson(req, res, HTTP_OK, { total, requests: sanitized });
1277
1352
  };
1278
1353
  }
1279
1354
  function createFlowsHandler(registry) {
@@ -1283,13 +1358,13 @@ function createFlowsHandler(registry) {
1283
1358
  ...flow,
1284
1359
  requests: flow.requests.map(sanitizeRequest)
1285
1360
  }));
1286
- sendJson(req, res, 200, { total: flows.length, flows });
1361
+ sendJson(req, res, HTTP_OK, { total: flows.length, flows });
1287
1362
  };
1288
1363
  }
1289
1364
  function createClearHandler(registry) {
1290
1365
  return (req, res) => {
1291
1366
  if (req.method !== "POST") {
1292
- sendJson(req, res, 405, { error: "Method not allowed" });
1367
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1293
1368
  return;
1294
1369
  }
1295
1370
  registry.get("request-store").clear();
@@ -1300,7 +1375,7 @@ function createClearHandler(registry) {
1300
1375
  registry.get("metrics-store").reset();
1301
1376
  if (registry.has("finding-store")) registry.get("finding-store").clear();
1302
1377
  registry.get("event-bus").emit("store:cleared", void 0);
1303
- sendJson(req, res, 200, { cleared: true });
1378
+ sendJson(req, res, HTTP_OK, { cleared: true });
1304
1379
  };
1305
1380
  }
1306
1381
  function createFetchesHandler(registry) {
@@ -1320,10 +1395,158 @@ var init_handlers = __esm({
1320
1395
  "use strict";
1321
1396
  init_group();
1322
1397
  init_constants();
1398
+ init_http();
1323
1399
  init_shared2();
1324
1400
  }
1325
1401
  });
1326
1402
 
1403
+ // src/utils/type-guards.ts
1404
+ function isString(val) {
1405
+ return typeof val === "string";
1406
+ }
1407
+ function isNumber(val) {
1408
+ return typeof val === "number" && !isNaN(val);
1409
+ }
1410
+ function isBoolean(val) {
1411
+ return typeof val === "boolean";
1412
+ }
1413
+ function getErrorMessage(err) {
1414
+ if (err instanceof Error) return err.message;
1415
+ if (typeof err === "string") return err;
1416
+ return String(err);
1417
+ }
1418
+ function isValidFindingState(val) {
1419
+ return typeof val === "string" && VALID_FINDING_STATES.has(val);
1420
+ }
1421
+ function isValidAiFixStatus(val) {
1422
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
1423
+ }
1424
+ var init_type_guards = __esm({
1425
+ "src/utils/type-guards.ts"() {
1426
+ "use strict";
1427
+ init_lifecycle();
1428
+ }
1429
+ });
1430
+
1431
+ // src/dashboard/api/sdk-event-parser.ts
1432
+ import { randomUUID as randomUUID3 } from "crypto";
1433
+ function str(val, fallback) {
1434
+ return isString(val) ? val : fallback;
1435
+ }
1436
+ function strOrUndef(val) {
1437
+ return isString(val) ? val : void 0;
1438
+ }
1439
+ function num(val, fallback) {
1440
+ return isNumber(val) ? val : fallback;
1441
+ }
1442
+ function numOrUndef(val) {
1443
+ return isNumber(val) ? val : void 0;
1444
+ }
1445
+ function headers(val) {
1446
+ if (val && typeof val === "object" && !Array.isArray(val)) {
1447
+ return val;
1448
+ }
1449
+ return {};
1450
+ }
1451
+ function parseQueryEvent(data, ts, parentRequestId) {
1452
+ return {
1453
+ driver: str(data.source, "sdk"),
1454
+ source: strOrUndef(data.source),
1455
+ sql: strOrUndef(data.sql),
1456
+ model: strOrUndef(data.model),
1457
+ operation: strOrUndef(data.operation),
1458
+ normalizedOp: strOrUndef(data.normalizedOp) ?? strOrUndef(data.operation) ?? "OTHER",
1459
+ table: str(data.table, ""),
1460
+ durationMs: num(data.duration, num(data.durationMs, 0)),
1461
+ rowCount: numOrUndef(data.rowCount),
1462
+ parentRequestId,
1463
+ timestamp: ts
1464
+ };
1465
+ }
1466
+ function parseFetchEvent(data, ts, parentRequestId) {
1467
+ return {
1468
+ url: str(data.url, ""),
1469
+ method: str(data.method, "GET"),
1470
+ statusCode: num(data.statusCode, 0),
1471
+ durationMs: num(data.duration, num(data.durationMs, 0)),
1472
+ parentRequestId,
1473
+ timestamp: ts
1474
+ };
1475
+ }
1476
+ function parseLogEvent(data, ts, parentRequestId) {
1477
+ return {
1478
+ level: str(data.level, "log"),
1479
+ message: str(data.message, ""),
1480
+ parentRequestId,
1481
+ timestamp: ts
1482
+ };
1483
+ }
1484
+ function parseErrorEvent(data, ts, parentRequestId) {
1485
+ return {
1486
+ name: str(data.name, "Error"),
1487
+ message: str(data.message, ""),
1488
+ stack: str(data.stack, ""),
1489
+ parentRequestId,
1490
+ timestamp: ts
1491
+ };
1492
+ }
1493
+ function parseAuthEvent(data, ts, parentRequestId) {
1494
+ return {
1495
+ level: "info",
1496
+ message: `[auth] ${str(data.provider, "unknown")}: ${str(data.result, "check")}`,
1497
+ parentRequestId,
1498
+ timestamp: ts
1499
+ };
1500
+ }
1501
+ function parseRequestEvent(data, ts) {
1502
+ const url = str(data.url, "");
1503
+ return {
1504
+ id: str(data.id, randomUUID3()),
1505
+ method: str(data.method, "GET"),
1506
+ url,
1507
+ path: url.split("?")[0],
1508
+ headers: headers(data.headers),
1509
+ requestBody: isString(data.requestBody) ? data.requestBody : null,
1510
+ statusCode: num(data.statusCode, 200),
1511
+ responseHeaders: headers(data.responseHeaders),
1512
+ responseBody: isString(data.responseBody) ? data.responseBody : null,
1513
+ startedAt: ts,
1514
+ durationMs: num(data.durationMs, 0),
1515
+ responseSize: num(data.responseSize, 0),
1516
+ isStatic: isBoolean(data.isStatic) ? data.isStatic : false
1517
+ };
1518
+ }
1519
+ function routeSDKEvent(event, stores) {
1520
+ const ts = event.timestamp || Date.now();
1521
+ const parentRequestId = event.requestId ?? null;
1522
+ switch (event.type) {
1523
+ case "db.query":
1524
+ stores.addQuery(parseQueryEvent(event.data, ts, parentRequestId));
1525
+ break;
1526
+ case "fetch":
1527
+ stores.addFetch(parseFetchEvent(event.data, ts, parentRequestId));
1528
+ break;
1529
+ case "log":
1530
+ stores.addLog(parseLogEvent(event.data, ts, parentRequestId));
1531
+ break;
1532
+ case "error":
1533
+ stores.addError(parseErrorEvent(event.data, ts, parentRequestId));
1534
+ break;
1535
+ case "auth.check":
1536
+ stores.addLog(parseAuthEvent(event.data, ts, parentRequestId));
1537
+ break;
1538
+ case "request":
1539
+ stores.addRequest(parseRequestEvent(event.data, ts));
1540
+ break;
1541
+ }
1542
+ }
1543
+ var init_sdk_event_parser = __esm({
1544
+ "src/dashboard/api/sdk-event-parser.ts"() {
1545
+ "use strict";
1546
+ init_type_guards();
1547
+ }
1548
+ });
1549
+
1327
1550
  // src/dashboard/api/ingest.ts
1328
1551
  function isBrakitBatch(msg) {
1329
1552
  return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
@@ -1348,65 +1571,21 @@ function createIngestHandler(registry) {
1348
1571
  break;
1349
1572
  }
1350
1573
  };
1351
- const routeSDKEvent = (event) => {
1352
- const ts = event.timestamp || Date.now();
1353
- const parentRequestId = event.requestId ?? null;
1354
- switch (event.type) {
1355
- case "db.query":
1356
- registry.get("query-store").add({
1357
- driver: event.data.source ?? "sdk",
1358
- source: event.data.source ?? "sdk",
1359
- sql: event.data.sql,
1360
- model: event.data.model,
1361
- operation: event.data.operation,
1362
- normalizedOp: event.data.normalizedOp ?? event.data.operation ?? "OTHER",
1363
- table: event.data.table ?? "",
1364
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1365
- rowCount: event.data.rowCount,
1366
- parentRequestId,
1367
- timestamp: ts
1368
- });
1369
- break;
1370
- case "fetch":
1371
- registry.get("fetch-store").add({
1372
- url: event.data.url ?? "",
1373
- method: event.data.method ?? "GET",
1374
- statusCode: event.data.statusCode ?? 0,
1375
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1376
- parentRequestId,
1377
- timestamp: ts
1378
- });
1379
- break;
1380
- case "log":
1381
- registry.get("log-store").add({
1382
- level: event.data.level ?? "log",
1383
- message: event.data.message ?? "",
1384
- parentRequestId,
1385
- timestamp: ts
1386
- });
1387
- break;
1388
- case "error":
1389
- registry.get("error-store").add({
1390
- name: event.data.name ?? "Error",
1391
- message: event.data.message ?? "",
1392
- stack: event.data.stack ?? "",
1393
- parentRequestId,
1394
- timestamp: ts
1395
- });
1396
- break;
1397
- case "auth.check":
1398
- registry.get("log-store").add({
1399
- level: "info",
1400
- message: `[auth] ${event.data.provider ?? "unknown"}: ${event.data.result ?? "check"}`,
1401
- parentRequestId,
1402
- timestamp: ts
1403
- });
1404
- break;
1405
- }
1574
+ const queryStore = registry.get("query-store");
1575
+ const fetchStore = registry.get("fetch-store");
1576
+ const logStore = registry.get("log-store");
1577
+ const errorStore = registry.get("error-store");
1578
+ const requestStore = registry.get("request-store");
1579
+ const stores = {
1580
+ addQuery: (data) => queryStore.add(data),
1581
+ addFetch: (data) => fetchStore.add(data),
1582
+ addLog: (data) => logStore.add(data),
1583
+ addError: (data) => errorStore.add(data),
1584
+ addRequest: (data) => requestStore.add(data)
1406
1585
  };
1407
1586
  return (req, res) => {
1408
1587
  if (req.method !== "POST") {
1409
- sendJson(req, res, 405, { error: "Method not allowed" });
1588
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1410
1589
  return;
1411
1590
  }
1412
1591
  const chunks = [];
@@ -1414,7 +1593,7 @@ function createIngestHandler(registry) {
1414
1593
  req.on("data", (chunk) => {
1415
1594
  totalSize += chunk.length;
1416
1595
  if (totalSize > MAX_INGEST_BYTES) {
1417
- sendJson(req, res, 413, { error: "Payload too large" });
1596
+ sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
1418
1597
  req.destroy();
1419
1598
  return;
1420
1599
  }
@@ -1426,9 +1605,9 @@ function createIngestHandler(registry) {
1426
1605
  const body = JSON.parse(Buffer.concat(chunks).toString());
1427
1606
  if (isSDKPayload(body)) {
1428
1607
  for (const event of body.events) {
1429
- routeSDKEvent(event);
1608
+ routeSDKEvent(event, stores);
1430
1609
  }
1431
- res.writeHead(204);
1610
+ res.writeHead(HTTP_NO_CONTENT);
1432
1611
  res.end();
1433
1612
  return;
1434
1613
  }
@@ -1436,13 +1615,19 @@ function createIngestHandler(registry) {
1436
1615
  for (const event of body.events) {
1437
1616
  routeEvent(event);
1438
1617
  }
1439
- res.writeHead(204);
1618
+ res.writeHead(HTTP_NO_CONTENT);
1440
1619
  res.end();
1441
1620
  return;
1442
1621
  }
1443
- sendJson(req, res, 400, { error: "Invalid batch" });
1622
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
1444
1623
  } catch {
1445
- sendJson(req, res, 400, { error: "Invalid JSON" });
1624
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON" });
1625
+ }
1626
+ });
1627
+ req.on("error", () => {
1628
+ if (!res.headersSent) {
1629
+ res.writeHead(HTTP_BAD_REQUEST);
1630
+ res.end();
1446
1631
  }
1447
1632
  });
1448
1633
  };
@@ -1451,7 +1636,9 @@ var init_ingest = __esm({
1451
1636
  "src/dashboard/api/ingest.ts"() {
1452
1637
  "use strict";
1453
1638
  init_limits();
1639
+ init_http();
1454
1640
  init_shared2();
1641
+ init_sdk_event_parser();
1455
1642
  }
1456
1643
  });
1457
1644
 
@@ -1459,20 +1646,21 @@ var init_ingest = __esm({
1459
1646
  function createMetricsHandler(metricsStore) {
1460
1647
  return (req, res) => {
1461
1648
  if (!requireGet(req, res)) return;
1462
- const url = new URL(req.url ?? "/", "http://localhost");
1649
+ const url = parseRequestUrl(req);
1463
1650
  const endpoint = url.searchParams.get("endpoint");
1464
1651
  if (endpoint) {
1465
1652
  const ep = metricsStore.getEndpoint(endpoint);
1466
- sendJson(req, res, 200, { endpoints: ep ? [ep] : [] });
1653
+ sendJson(req, res, HTTP_OK, { endpoints: ep ? [ep] : [] });
1467
1654
  return;
1468
1655
  }
1469
- sendJson(req, res, 200, { endpoints: metricsStore.getAll() });
1656
+ sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
1470
1657
  };
1471
1658
  }
1472
1659
  var init_metrics2 = __esm({
1473
1660
  "src/dashboard/api/metrics.ts"() {
1474
1661
  "use strict";
1475
1662
  init_shared2();
1663
+ init_http();
1476
1664
  }
1477
1665
  });
1478
1666
 
@@ -1490,15 +1678,34 @@ var init_metrics_live = __esm({
1490
1678
  }
1491
1679
  });
1492
1680
 
1681
+ // src/utils/log.ts
1682
+ function brakitWarn(message) {
1683
+ process.stderr.write(`${PREFIX} ${message}
1684
+ `);
1685
+ }
1686
+ function brakitDebug(message) {
1687
+ if (process.env.DEBUG_BRAKIT) {
1688
+ process.stderr.write(`${PREFIX}:debug ${message}
1689
+ `);
1690
+ }
1691
+ }
1692
+ var PREFIX;
1693
+ var init_log = __esm({
1694
+ "src/utils/log.ts"() {
1695
+ "use strict";
1696
+ PREFIX = "[brakit]";
1697
+ }
1698
+ });
1699
+
1493
1700
  // src/dashboard/api/activity.ts
1494
1701
  function createActivityHandler(registry) {
1495
1702
  return (req, res) => {
1496
1703
  if (!requireGet(req, res)) return;
1497
1704
  try {
1498
- const url = new URL(req.url ?? "/", "http://localhost");
1705
+ const url = parseRequestUrl(req);
1499
1706
  const requestId = url.searchParams.get("requestId");
1500
1707
  if (!requestId) {
1501
- sendJson(req, res, 400, { error: "requestId parameter required" });
1708
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId parameter required" });
1502
1709
  return;
1503
1710
  }
1504
1711
  const fetches = registry.get("fetch-store").getByRequest(requestId);
@@ -1515,7 +1722,7 @@ function createActivityHandler(registry) {
1515
1722
  for (const q of queries)
1516
1723
  timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
1517
1724
  timeline.sort((a, b) => a.timestamp - b.timestamp);
1518
- sendJson(req, res, 200, {
1725
+ sendJson(req, res, HTTP_OK, {
1519
1726
  requestId,
1520
1727
  total: timeline.length,
1521
1728
  timeline,
@@ -1527,9 +1734,9 @@ function createActivityHandler(registry) {
1527
1734
  }
1528
1735
  });
1529
1736
  } catch (err) {
1530
- console.error("[brakit] activity handler error:", err);
1737
+ brakitDebug(`activity handler error: ${err}`);
1531
1738
  if (!res.headersSent) {
1532
- sendJson(req, res, 500, { error: "Internal error" });
1739
+ sendJson(req, res, HTTP_INTERNAL_ERROR, { error: "Internal error" });
1533
1740
  }
1534
1741
  }
1535
1742
  };
@@ -1538,6 +1745,8 @@ var init_activity = __esm({
1538
1745
  "src/dashboard/api/activity.ts"() {
1539
1746
  "use strict";
1540
1747
  init_shared2();
1748
+ init_http();
1749
+ init_log();
1541
1750
  }
1542
1751
  });
1543
1752
 
@@ -1557,19 +1766,20 @@ var init_api = __esm({
1557
1766
  function createInsightsHandler(engine) {
1558
1767
  return (req, res) => {
1559
1768
  if (!requireGet(req, res)) return;
1560
- sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
1769
+ sendJson(req, res, HTTP_OK, { insights: engine.getStatefulInsights() });
1561
1770
  };
1562
1771
  }
1563
1772
  function createSecurityHandler(engine) {
1564
1773
  return (req, res) => {
1565
1774
  if (!requireGet(req, res)) return;
1566
- sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
1775
+ sendJson(req, res, HTTP_OK, { findings: engine.getStatefulFindings() });
1567
1776
  };
1568
1777
  }
1569
1778
  var init_insights = __esm({
1570
1779
  "src/dashboard/api/insights.ts"() {
1571
1780
  "use strict";
1572
1781
  init_shared2();
1782
+ init_http();
1573
1783
  }
1574
1784
  });
1575
1785
 
@@ -1577,99 +1787,160 @@ var init_insights = __esm({
1577
1787
  function createFindingsHandler(findingStore) {
1578
1788
  return (req, res) => {
1579
1789
  if (!requireGet(req, res)) return;
1580
- const url = new URL(req.url ?? "/", "http://localhost");
1790
+ const url = parseRequestUrl(req);
1581
1791
  const stateParam = url.searchParams.get("state");
1582
1792
  let findings;
1583
- if (stateParam && VALID_STATES.has(stateParam)) {
1793
+ if (stateParam && isValidFindingState(stateParam)) {
1584
1794
  findings = findingStore.getByState(stateParam);
1585
1795
  } else {
1586
1796
  findings = findingStore.getAll();
1587
1797
  }
1588
- sendJson(req, res, 200, {
1798
+ sendJson(req, res, HTTP_OK, {
1589
1799
  total: findings.length,
1590
1800
  findings
1591
1801
  });
1592
1802
  };
1593
1803
  }
1594
- var VALID_STATES;
1804
+ function createFindingsReportHandler(findingStore, eventBus, analysisEngine) {
1805
+ return async (req, res) => {
1806
+ if (req.method !== "POST") {
1807
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1808
+ return;
1809
+ }
1810
+ const body = await readJsonBody(req, res);
1811
+ if (!body) return;
1812
+ const { findingId, status, notes } = body;
1813
+ if (!findingId || typeof findingId !== "string") {
1814
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "findingId is required" });
1815
+ return;
1816
+ }
1817
+ if (!isValidAiFixStatus(status)) {
1818
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "status must be 'fixed' or 'wont_fix'" });
1819
+ return;
1820
+ }
1821
+ if (!notes || typeof notes !== "string") {
1822
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
1823
+ return;
1824
+ }
1825
+ const findingOk = findingStore.reportFix(findingId, status, notes);
1826
+ if (findingOk) {
1827
+ eventBus.emit("findings:changed", findingStore.getAll());
1828
+ sendJson(req, res, HTTP_OK, { ok: true });
1829
+ return;
1830
+ }
1831
+ if (analysisEngine?.reportInsightFix(findingId, status, notes)) {
1832
+ eventBus.emit("analysis:updated", {
1833
+ insights: analysisEngine.getInsights(),
1834
+ findings: analysisEngine.getFindings(),
1835
+ statefulFindings: analysisEngine.getStatefulFindings(),
1836
+ statefulInsights: analysisEngine.getStatefulInsights()
1837
+ });
1838
+ sendJson(req, res, HTTP_OK, { ok: true });
1839
+ return;
1840
+ }
1841
+ sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
1842
+ };
1843
+ }
1595
1844
  var init_findings = __esm({
1596
1845
  "src/dashboard/api/findings.ts"() {
1597
1846
  "use strict";
1598
1847
  init_shared2();
1599
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
1848
+ init_type_guards();
1849
+ init_http();
1600
1850
  }
1601
1851
  });
1602
1852
 
1603
- // src/core/disposable.ts
1604
- var SubscriptionBag;
1605
- var init_disposable = __esm({
1606
- "src/core/disposable.ts"() {
1853
+ // src/constants/events.ts
1854
+ var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_INSIGHTS, SSE_EVENT_SECURITY;
1855
+ var init_events = __esm({
1856
+ "src/constants/events.ts"() {
1607
1857
  "use strict";
1608
- SubscriptionBag = class {
1609
- items = [];
1610
- add(teardown) {
1611
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
1612
- }
1613
- dispose() {
1614
- for (const d of this.items) d.dispose();
1615
- this.items.length = 0;
1616
- }
1617
- };
1858
+ SSE_EVENT_FETCH = "fetch";
1859
+ SSE_EVENT_LOG = "log";
1860
+ SSE_EVENT_ERROR = "error_event";
1861
+ SSE_EVENT_QUERY = "query";
1862
+ SSE_EVENT_INSIGHTS = "insights";
1863
+ SSE_EVENT_SECURITY = "security";
1618
1864
  }
1619
1865
  });
1620
1866
 
1621
1867
  // src/dashboard/sse.ts
1622
1868
  function createSSEHandler(registry) {
1623
- return (req, res) => {
1624
- res.writeHead(200, {
1625
- "content-type": "text/event-stream",
1626
- "cache-control": "no-cache",
1627
- connection: "keep-alive",
1628
- "access-control-allow-origin": "*"
1629
- });
1630
- res.write(":ok\n\n");
1631
- const writeEvent = (eventType, data) => {
1632
- if (res.destroyed) return;
1633
- if (eventType) {
1634
- res.write(`event: ${eventType}
1869
+ const clients = /* @__PURE__ */ new Set();
1870
+ function broadcast(eventType, data) {
1871
+ if (clients.size === 0) return;
1872
+ const frame = eventType ? `event: ${eventType}
1635
1873
  data: ${data}
1636
1874
 
1637
- `);
1638
- } else {
1639
- res.write(`data: ${data}
1875
+ ` : `data: ${data}
1640
1876
 
1641
- `);
1877
+ `;
1878
+ for (const client of clients) {
1879
+ if (client.res.destroyed) {
1880
+ clients.delete(client);
1881
+ continue;
1642
1882
  }
1883
+ try {
1884
+ client.res.write(frame);
1885
+ } catch {
1886
+ clients.delete(client);
1887
+ }
1888
+ }
1889
+ }
1890
+ const bus = registry.get("event-bus");
1891
+ bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
1892
+ bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
1893
+ bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
1894
+ bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
1895
+ bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
1896
+ bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
1897
+ broadcast(SSE_EVENT_INSIGHTS, JSON.stringify(statefulInsights));
1898
+ broadcast(SSE_EVENT_SECURITY, JSON.stringify(statefulFindings));
1899
+ });
1900
+ bus.on("findings:changed", (findings) => {
1901
+ broadcast(SSE_EVENT_SECURITY, JSON.stringify(findings));
1902
+ });
1903
+ return (req, res) => {
1904
+ const headers2 = {
1905
+ "content-type": "text/event-stream",
1906
+ "cache-control": "no-cache",
1907
+ connection: "keep-alive"
1643
1908
  };
1644
- const bus = registry.get("event-bus");
1645
- const subs = new SubscriptionBag();
1646
- subs.add(bus.on("request:completed", (r) => writeEvent(null, JSON.stringify(r))));
1647
- subs.add(bus.on("telemetry:fetch", (e) => writeEvent("fetch", JSON.stringify(e))));
1648
- subs.add(bus.on("telemetry:log", (e) => writeEvent("log", JSON.stringify(e))));
1649
- subs.add(bus.on("telemetry:error", (e) => writeEvent("error_event", JSON.stringify(e))));
1650
- subs.add(bus.on("telemetry:query", (e) => writeEvent("query", JSON.stringify(e))));
1651
- subs.add(bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
1652
- writeEvent("insights", JSON.stringify(statefulInsights));
1653
- writeEvent("security", JSON.stringify(statefulFindings));
1654
- }));
1909
+ const corsOrigin = getCorsOrigin(req);
1910
+ if (corsOrigin) {
1911
+ headers2["access-control-allow-origin"] = corsOrigin;
1912
+ }
1913
+ res.writeHead(HTTP_OK, headers2);
1914
+ res.write(":ok\n\n");
1655
1915
  const heartbeat = setInterval(() => {
1656
1916
  if (res.destroyed) {
1657
1917
  clearInterval(heartbeat);
1918
+ clients.delete(client);
1658
1919
  return;
1659
1920
  }
1660
- res.write(":heartbeat\n\n");
1921
+ try {
1922
+ res.write(":heartbeat\n\n");
1923
+ } catch {
1924
+ clearInterval(heartbeat);
1925
+ clients.delete(client);
1926
+ }
1661
1927
  }, SSE_HEARTBEAT_INTERVAL_MS);
1928
+ heartbeat.unref();
1929
+ const client = { res, heartbeat };
1930
+ clients.add(client);
1662
1931
  req.on("close", () => {
1663
1932
  clearInterval(heartbeat);
1664
- subs.dispose();
1933
+ clients.delete(client);
1665
1934
  });
1666
1935
  };
1667
1936
  }
1668
1937
  var init_sse = __esm({
1669
1938
  "src/dashboard/sse.ts"() {
1670
1939
  "use strict";
1671
- init_disposable();
1672
1940
  init_constants();
1941
+ init_http();
1942
+ init_events();
1943
+ init_shared2();
1673
1944
  }
1674
1945
  });
1675
1946
 
@@ -2188,6 +2459,13 @@ function getSecurityStyles() {
2188
2459
  .sec-item-resolved{color:var(--text-muted)}
2189
2460
  .sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
2190
2461
  .sec-resolved-item-icon{color:var(--green);font-size:12px;flex-shrink:0;margin-right:8px}
2462
+
2463
+ /* AI status badges */
2464
+ .sec-ai-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:8px;margin-left:8px;white-space:nowrap}
2465
+ .sec-ai-fixing{background:rgba(217,119,6,.1);color:var(--amber)}
2466
+ .sec-ai-wontfix{background:rgba(107,114,128,.1);color:var(--text-muted)}
2467
+ .sec-ai-verified{background:rgba(22,163,74,.1);color:var(--green)}
2468
+ .sec-ai-notes{font-size:11px;color:var(--text-muted);font-style:italic;margin-top:2px;padding-left:0}
2191
2469
  `;
2192
2470
  }
2193
2471
  var init_security = __esm({
@@ -2251,9 +2529,17 @@ var init_styles = __esm({
2251
2529
  });
2252
2530
 
2253
2531
  // src/utils/fs.ts
2254
- import { access } from "fs/promises";
2532
+ import { access, readFile, writeFile } from "fs/promises";
2255
2533
  import { existsSync, readFileSync, writeFileSync } from "fs";
2256
2534
  import { resolve } from "path";
2535
+ async function fileExists(path) {
2536
+ try {
2537
+ await access(path);
2538
+ return true;
2539
+ } catch {
2540
+ return false;
2541
+ }
2542
+ }
2257
2543
  function ensureGitignore(dir, entry) {
2258
2544
  try {
2259
2545
  const gitignorePath = resolve(dir, "../.gitignore");
@@ -2267,28 +2553,22 @@ function ensureGitignore(dir, entry) {
2267
2553
  } catch {
2268
2554
  }
2269
2555
  }
2270
- var init_fs = __esm({
2271
- "src/utils/fs.ts"() {
2272
- "use strict";
2273
- }
2274
- });
2275
-
2276
- // src/utils/log.ts
2277
- function brakitWarn(message) {
2278
- process.stderr.write(`${PREFIX} ${message}
2279
- `);
2280
- }
2281
- function brakitDebug(message) {
2282
- if (process.env.DEBUG_BRAKIT) {
2283
- process.stderr.write(`${PREFIX}:debug ${message}
2284
- `);
2556
+ async function ensureGitignoreAsync(dir, entry) {
2557
+ try {
2558
+ const gitignorePath = resolve(dir, "../.gitignore");
2559
+ if (await fileExists(gitignorePath)) {
2560
+ const content = await readFile(gitignorePath, "utf-8");
2561
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
2562
+ await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2563
+ } else {
2564
+ await writeFile(gitignorePath, entry + "\n");
2565
+ }
2566
+ } catch {
2285
2567
  }
2286
2568
  }
2287
- var PREFIX;
2288
- var init_log = __esm({
2289
- "src/utils/log.ts"() {
2569
+ var init_fs = __esm({
2570
+ "src/utils/fs.ts"() {
2290
2571
  "use strict";
2291
- PREFIX = "[brakit]";
2292
2572
  }
2293
2573
  });
2294
2574
 
@@ -2306,6 +2586,7 @@ var init_atomic_writer = __esm({
2306
2586
  "use strict";
2307
2587
  init_fs();
2308
2588
  init_log();
2589
+ init_type_guards();
2309
2590
  AtomicWriter = class {
2310
2591
  constructor(opts) {
2311
2592
  this.opts = opts;
@@ -2320,7 +2601,7 @@ var init_atomic_writer = __esm({
2320
2601
  writeFileSync2(this.tmpPath, content);
2321
2602
  renameSync(this.tmpPath, this.opts.filePath);
2322
2603
  } catch (err) {
2323
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2604
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2324
2605
  }
2325
2606
  }
2326
2607
  async writeAsync(content) {
@@ -2334,13 +2615,14 @@ var init_atomic_writer = __esm({
2334
2615
  await writeFile2(this.tmpPath, content);
2335
2616
  await rename(this.tmpPath, this.opts.filePath);
2336
2617
  } catch (err) {
2337
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2618
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2338
2619
  } finally {
2339
2620
  this.writing = false;
2340
2621
  if (this.pendingContent !== null) {
2341
2622
  const next = this.pendingContent;
2342
2623
  this.pendingContent = null;
2343
- this.writeAsync(next);
2624
+ this.writeAsync(next).catch(() => {
2625
+ });
2344
2626
  }
2345
2627
  }
2346
2628
  }
@@ -2353,10 +2635,10 @@ var init_atomic_writer = __esm({
2353
2635
  }
2354
2636
  }
2355
2637
  async ensureDirAsync() {
2356
- if (!existsSync2(this.opts.dir)) {
2638
+ if (!await fileExists(this.opts.dir)) {
2357
2639
  await mkdir(this.opts.dir, { recursive: true });
2358
2640
  if (this.opts.gitignoreEntry) {
2359
- ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2641
+ await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
2360
2642
  }
2361
2643
  }
2362
2644
  }
@@ -2368,23 +2650,32 @@ var init_atomic_writer = __esm({
2368
2650
  import { createHash } from "crypto";
2369
2651
  function computeFindingId(finding) {
2370
2652
  const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
2371
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
2653
+ return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
2654
+ }
2655
+ function computeInsightId(type, endpoint, desc) {
2656
+ const key = `${type}:${endpoint}:${desc}`;
2657
+ return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
2372
2658
  }
2373
2659
  var init_finding_id = __esm({
2374
2660
  "src/store/finding-id.ts"() {
2375
2661
  "use strict";
2662
+ init_limits();
2376
2663
  }
2377
2664
  });
2378
2665
 
2379
2666
  // src/store/finding-store.ts
2667
+ import { readFile as readFile2 } from "fs/promises";
2380
2668
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
2381
2669
  import { resolve as resolve2 } from "path";
2382
2670
  var FindingStore;
2383
2671
  var init_finding_store = __esm({
2384
2672
  "src/store/finding-store.ts"() {
2385
2673
  "use strict";
2674
+ init_fs();
2386
2675
  init_constants();
2676
+ init_limits();
2387
2677
  init_atomic_writer();
2678
+ init_log();
2388
2679
  init_finding_id();
2389
2680
  FindingStore = class {
2390
2681
  constructor(rootDir) {
@@ -2397,7 +2688,6 @@ var init_finding_store = __esm({
2397
2688
  gitignoreEntry: METRICS_DIR,
2398
2689
  label: "findings"
2399
2690
  });
2400
- this.load();
2401
2691
  }
2402
2692
  findings = /* @__PURE__ */ new Map();
2403
2693
  flushTimer = null;
@@ -2405,6 +2695,8 @@ var init_finding_store = __esm({
2405
2695
  writer;
2406
2696
  findingsPath;
2407
2697
  start() {
2698
+ this.loadAsync().catch(() => {
2699
+ });
2408
2700
  this.flushTimer = setInterval(
2409
2701
  () => this.flush(),
2410
2702
  FINDINGS_FLUSH_INTERVAL_MS
@@ -2441,7 +2733,9 @@ var init_finding_store = __esm({
2441
2733
  firstSeenAt: now,
2442
2734
  lastSeenAt: now,
2443
2735
  resolvedAt: null,
2444
- occurrences: 1
2736
+ occurrences: 1,
2737
+ aiStatus: null,
2738
+ aiNotes: null
2445
2739
  };
2446
2740
  this.findings.set(id, stateful);
2447
2741
  this.dirty = true;
@@ -2457,6 +2751,17 @@ var init_finding_store = __esm({
2457
2751
  this.dirty = true;
2458
2752
  return true;
2459
2753
  }
2754
+ reportFix(findingId, status, notes) {
2755
+ const finding = this.findings.get(findingId);
2756
+ if (!finding) return false;
2757
+ finding.aiStatus = status;
2758
+ finding.aiNotes = notes;
2759
+ if (status === "fixed") {
2760
+ finding.state = "fixing";
2761
+ }
2762
+ this.dirty = true;
2763
+ return true;
2764
+ }
2460
2765
  /**
2461
2766
  * Reconcile passive findings against the current analysis results.
2462
2767
  *
@@ -2469,7 +2774,7 @@ var init_finding_store = __esm({
2469
2774
  reconcilePassive(currentFindings) {
2470
2775
  const currentIds = new Set(currentFindings.map(computeFindingId));
2471
2776
  for (const [id, stateful] of this.findings) {
2472
- if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
2777
+ if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
2473
2778
  stateful.state = "resolved";
2474
2779
  stateful.resolvedAt = Date.now();
2475
2780
  this.dirty = true;
@@ -2489,18 +2794,35 @@ var init_finding_store = __esm({
2489
2794
  this.findings.clear();
2490
2795
  this.dirty = true;
2491
2796
  }
2492
- load() {
2797
+ async loadAsync() {
2798
+ try {
2799
+ if (await fileExists(this.findingsPath)) {
2800
+ const raw = await readFile2(this.findingsPath, "utf-8");
2801
+ const parsed = JSON.parse(raw);
2802
+ if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
2803
+ for (const f of parsed.findings) {
2804
+ this.findings.set(f.findingId, f);
2805
+ }
2806
+ }
2807
+ }
2808
+ } catch (err) {
2809
+ brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
2810
+ }
2811
+ }
2812
+ /** Sync load for tests only — not used in production paths. */
2813
+ loadSync() {
2493
2814
  try {
2494
2815
  if (existsSync3(this.findingsPath)) {
2495
2816
  const raw = readFileSync2(this.findingsPath, "utf-8");
2496
2817
  const parsed = JSON.parse(raw);
2497
- if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
2818
+ if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
2498
2819
  for (const f of parsed.findings) {
2499
2820
  this.findings.set(f.findingId, f);
2500
2821
  }
2501
2822
  }
2502
2823
  }
2503
- } catch {
2824
+ } catch (err) {
2825
+ brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
2504
2826
  }
2505
2827
  }
2506
2828
  flush() {
@@ -2515,7 +2837,7 @@ var init_finding_store = __esm({
2515
2837
  }
2516
2838
  serialize() {
2517
2839
  const data = {
2518
- version: 1,
2840
+ version: FINDINGS_DATA_VERSION,
2519
2841
  findings: [...this.findings.values()]
2520
2842
  };
2521
2843
  return JSON.stringify(data);
@@ -2525,12 +2847,34 @@ var init_finding_store = __esm({
2525
2847
  });
2526
2848
 
2527
2849
  // src/detect/project.ts
2528
- import { readFile as readFile2 } from "fs/promises";
2529
- import { join } from "path";
2850
+ import { readFile as readFile3, readdir } from "fs/promises";
2851
+ import { existsSync as existsSync4 } from "fs";
2852
+ import { join, relative } from "path";
2853
+ function detectFrameworkFromDeps(allDeps) {
2854
+ for (const f of FRAMEWORKS) {
2855
+ if (allDeps[f.dep]) return f.name;
2856
+ }
2857
+ return "unknown";
2858
+ }
2859
+ function detectPackageManagerSync(rootDir) {
2860
+ if (existsSync4(join(rootDir, "bun.lockb")) || existsSync4(join(rootDir, "bun.lock"))) return "bun";
2861
+ if (existsSync4(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2862
+ if (existsSync4(join(rootDir, "yarn.lock"))) return "yarn";
2863
+ if (existsSync4(join(rootDir, "package-lock.json"))) return "npm";
2864
+ return "unknown";
2865
+ }
2866
+ var FRAMEWORKS;
2530
2867
  var init_project = __esm({
2531
2868
  "src/detect/project.ts"() {
2532
2869
  "use strict";
2533
2870
  init_fs();
2871
+ FRAMEWORKS = [
2872
+ { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
2873
+ { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
2874
+ { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
2875
+ { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
2876
+ { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
2877
+ ];
2534
2878
  }
2535
2879
  });
2536
2880
 
@@ -2542,11 +2886,11 @@ var init_patterns = __esm({
2542
2886
  SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
2543
2887
  TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
2544
2888
  SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
2545
- STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
2889
+ 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+/;
2546
2890
  DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
2547
2891
  SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
2548
- SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
2549
- LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
2892
+ SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
2893
+ LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
2550
2894
  MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
2551
2895
  EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
2552
2896
  INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
@@ -2558,9 +2902,9 @@ var init_patterns = __esm({
2558
2902
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
2559
2903
  "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
2560
2904
  "error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
2905
+ "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
2561
2906
  "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
2562
2907
  "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
2563
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
2564
2908
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
2565
2909
  };
2566
2910
  }
@@ -2984,48 +3328,47 @@ function hasInternalIds(obj) {
2984
3328
  }
2985
3329
  return false;
2986
3330
  }
2987
- function detectPII(method, reqBody, resBody) {
2988
- const target = unwrapResponse(resBody);
2989
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
2990
- const reqEmails = findEmails(reqBody);
2991
- if (reqEmails.length > 0) {
2992
- const resEmails = findEmails(target);
2993
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
2994
- if (echoed.length > 0) {
2995
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
2996
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
2997
- return { reason: "echo", emailCount: echoed.length };
2998
- }
2999
- }
3000
- }
3331
+ function detectEchoPII(method, reqBody, target) {
3332
+ if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
3333
+ const reqEmails = findEmails(reqBody);
3334
+ if (reqEmails.length === 0) return null;
3335
+ const resEmails = findEmails(target);
3336
+ const echoed = reqEmails.filter((e) => resEmails.includes(e));
3337
+ if (echoed.length === 0) return null;
3338
+ const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
3339
+ if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
3340
+ return { reason: "echo", emailCount: echoed.length };
3001
3341
  }
3002
- if (target && typeof target === "object" && !Array.isArray(target)) {
3003
- const fields = topLevelFieldCount(target);
3004
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
3005
- const emails = findEmails(target);
3006
- if (emails.length > 0) {
3007
- return { reason: "full-record", emailCount: emails.length };
3008
- }
3342
+ return null;
3343
+ }
3344
+ function detectFullRecordPII(target) {
3345
+ if (!target || typeof target !== "object" || Array.isArray(target)) return null;
3346
+ const fields = topLevelFieldCount(target);
3347
+ if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
3348
+ const emails = findEmails(target);
3349
+ if (emails.length === 0) return null;
3350
+ return { reason: "full-record", emailCount: emails.length };
3351
+ }
3352
+ function detectListPII(target) {
3353
+ if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
3354
+ let itemsWithEmail = 0;
3355
+ for (let i = 0; i < Math.min(target.length, 10); i++) {
3356
+ const item = target[i];
3357
+ if (item && typeof item === "object" && findEmails(item).length > 0) {
3358
+ itemsWithEmail++;
3009
3359
  }
3010
3360
  }
3011
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
3012
- let itemsWithEmail = 0;
3013
- for (let i = 0; i < Math.min(target.length, 10); i++) {
3014
- const item = target[i];
3015
- if (item && typeof item === "object") {
3016
- const emails = findEmails(item);
3017
- if (emails.length > 0) itemsWithEmail++;
3018
- }
3019
- }
3020
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
3021
- const first = target[0];
3022
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
3023
- return { reason: "list-pii", emailCount: itemsWithEmail };
3024
- }
3025
- }
3361
+ if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
3362
+ const first = target[0];
3363
+ if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
3364
+ return { reason: "list-pii", emailCount: itemsWithEmail };
3026
3365
  }
3027
3366
  return null;
3028
3367
  }
3368
+ function detectPII(method, reqBody, resBody) {
3369
+ const target = unwrapResponse(resBody);
3370
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
3371
+ }
3029
3372
  var WRITE_METHODS, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, REASON_LABELS, responsePiiLeakRule;
3030
3373
  var init_response_pii_leak = __esm({
3031
3374
  "src/analysis/rules/response-pii-leak.ts"() {
@@ -3143,6 +3486,24 @@ var init_rules = __esm({
3143
3486
  }
3144
3487
  });
3145
3488
 
3489
+ // src/core/disposable.ts
3490
+ var SubscriptionBag;
3491
+ var init_disposable = __esm({
3492
+ "src/core/disposable.ts"() {
3493
+ "use strict";
3494
+ SubscriptionBag = class {
3495
+ items = [];
3496
+ add(teardown) {
3497
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
3498
+ }
3499
+ dispose() {
3500
+ for (const d of this.items) d.dispose();
3501
+ this.items.length = 0;
3502
+ }
3503
+ };
3504
+ }
3505
+ });
3506
+
3146
3507
  // src/utils/collections.ts
3147
3508
  function groupBy(items, keyFn) {
3148
3509
  const map = /* @__PURE__ */ new Map();
@@ -3984,14 +4345,19 @@ function computeInsightKey(insight) {
3984
4345
  const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
3985
4346
  return `${insight.type}:${identifier}`;
3986
4347
  }
4348
+ function enrichedIdFromInsight(insight) {
4349
+ return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
4350
+ }
3987
4351
  var InsightTracker;
3988
4352
  var init_insight_tracker = __esm({
3989
4353
  "src/analysis/insight-tracker.ts"() {
3990
4354
  "use strict";
3991
4355
  init_endpoint();
4356
+ init_finding_id();
3992
4357
  init_thresholds();
3993
4358
  InsightTracker = class {
3994
4359
  tracked = /* @__PURE__ */ new Map();
4360
+ enrichedIndex = /* @__PURE__ */ new Map();
3995
4361
  reconcile(current) {
3996
4362
  const currentKeys = /* @__PURE__ */ new Set();
3997
4363
  const now = Date.now();
@@ -3999,6 +4365,7 @@ var init_insight_tracker = __esm({
3999
4365
  const key = computeInsightKey(insight);
4000
4366
  currentKeys.add(key);
4001
4367
  const existing = this.tracked.get(key);
4368
+ this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
4002
4369
  if (existing) {
4003
4370
  existing.insight = insight;
4004
4371
  existing.lastSeenAt = now;
@@ -4015,28 +4382,44 @@ var init_insight_tracker = __esm({
4015
4382
  firstSeenAt: now,
4016
4383
  lastSeenAt: now,
4017
4384
  resolvedAt: null,
4018
- consecutiveAbsences: 0
4385
+ consecutiveAbsences: 0,
4386
+ aiStatus: null,
4387
+ aiNotes: null
4019
4388
  });
4020
4389
  }
4021
4390
  }
4022
- for (const [key, stateful] of this.tracked) {
4023
- if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
4391
+ for (const [, stateful] of this.tracked) {
4392
+ if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
4024
4393
  stateful.consecutiveAbsences++;
4025
4394
  if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
4026
4395
  stateful.state = "resolved";
4027
4396
  stateful.resolvedAt = now;
4028
4397
  }
4029
4398
  } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
4030
- this.tracked.delete(key);
4399
+ this.tracked.delete(stateful.key);
4400
+ this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
4031
4401
  }
4032
4402
  }
4033
4403
  return [...this.tracked.values()];
4034
4404
  }
4405
+ reportFix(enrichedId, status, notes) {
4406
+ const key = this.enrichedIndex.get(enrichedId);
4407
+ if (!key) return false;
4408
+ const stateful = this.tracked.get(key);
4409
+ if (!stateful) return false;
4410
+ stateful.aiStatus = status;
4411
+ stateful.aiNotes = notes;
4412
+ if (status === "fixed") {
4413
+ stateful.state = "fixing";
4414
+ }
4415
+ return true;
4416
+ }
4035
4417
  getAll() {
4036
4418
  return [...this.tracked.values()];
4037
4419
  }
4038
4420
  clear() {
4039
4421
  this.tracked.clear();
4422
+ this.enrichedIndex.clear();
4040
4423
  }
4041
4424
  };
4042
4425
  }
@@ -4047,13 +4430,14 @@ var AnalysisEngine;
4047
4430
  var init_engine = __esm({
4048
4431
  "src/analysis/engine.ts"() {
4049
4432
  "use strict";
4433
+ init_limits();
4050
4434
  init_disposable();
4051
4435
  init_group();
4052
4436
  init_rules();
4053
4437
  init_insights3();
4054
4438
  init_insight_tracker();
4055
4439
  AnalysisEngine = class {
4056
- constructor(registry, debounceMs = 300) {
4440
+ constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4057
4441
  this.registry = registry;
4058
4442
  this.debounceMs = debounceMs;
4059
4443
  this.scanner = createDefaultScanner();
@@ -4089,7 +4473,10 @@ var init_engine = __esm({
4089
4473
  return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
4090
4474
  }
4091
4475
  getStatefulInsights() {
4092
- return this.cachedStatefulInsights;
4476
+ return this.insightTracker.getAll();
4477
+ }
4478
+ reportInsightFix(enrichedId, status, notes) {
4479
+ return this.insightTracker.reportFix(enrichedId, status, notes);
4093
4480
  }
4094
4481
  scheduleRecompute() {
4095
4482
  if (this.debounceTimer) return;
@@ -4147,7 +4534,7 @@ var init_src = __esm({
4147
4534
  init_engine();
4148
4535
  init_insights3();
4149
4536
  init_insights2();
4150
- VERSION = "0.8.3";
4537
+ VERSION = "0.8.5";
4151
4538
  }
4152
4539
  });
4153
4540
 
@@ -4836,7 +5223,7 @@ function getFlowDetail() {
4836
5223
  h += '<span>' + req.durationMs + 'ms</span>';
4837
5224
  if (req.responseSize) h += '<span>' + formatSize(req.responseSize) + '</span>';
4838
5225
  h += '</div>';
4839
- h += '<div class="request-timeline tl-hidden" data-request-id="' + req.id + '" data-request-started="' + req.startedAt + '"></div>';
5226
+ h += '<div class="request-timeline tl-hidden" data-request-id="' + escHtml(req.id) + '" data-request-started="' + escHtml(String(req.startedAt)) + '"></div>';
4840
5227
  h += '<div class="detail-grid">';
4841
5228
  h += '<div class="detail-section"><h4>Request Headers</h4><pre>' + formatHeaders(req.headers) + '</pre></div>';
4842
5229
  h += '<div class="detail-section"><h4>Response Headers</h4><pre>' + formatHeaders(req.responseHeaders) + '</pre></div>';
@@ -6094,7 +6481,7 @@ function getOverviewRender() {
6094
6481
  container.appendChild(summary);
6095
6482
 
6096
6483
  var all = state.insights || [];
6097
- var open = all.filter(function(si) { return si.state === 'open'; });
6484
+ var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing'; });
6098
6485
  var resolved = all.filter(function(si) { return si.state === 'resolved'; });
6099
6486
 
6100
6487
  if (open.length === 0 && resolved.length === 0) {
@@ -6139,10 +6526,17 @@ function getOverviewRender() {
6139
6526
  if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6140
6527
  expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6141
6528
 
6529
+ var aiBadge = '';
6530
+ if (si.state === 'fixing' && si.aiStatus === 'fixed') {
6531
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
6532
+ } else if (si.aiStatus === 'wont_fix') {
6533
+ aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
6534
+ }
6535
+
6142
6536
  card.innerHTML =
6143
6537
  '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6144
6538
  '<div class="ov-card-body">' +
6145
- '<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
6539
+ '<div class="ov-card-title">' + escHtml(insight.title) + aiBadge + '</div>' +
6146
6540
  '<div class="ov-card-desc">' + insight.desc + '</div>' +
6147
6541
  '<div class="ov-card-expand">' + expandHtml + '</div>' +
6148
6542
  '</div>' +
@@ -6234,7 +6628,26 @@ function getSecurityView() {
6234
6628
  container.innerHTML = '';
6235
6629
  var SEV = ${SEVERITY_MAP};
6236
6630
 
6237
- var all = state.findings || [];
6631
+ var all = (state.findings || []).slice();
6632
+ var insightsList = state.insights || [];
6633
+ for (var ix = 0; ix < insightsList.length; ix++) {
6634
+ var si = insightsList[ix];
6635
+ all.push({
6636
+ findingId: si.key,
6637
+ state: si.state,
6638
+ aiStatus: si.aiStatus,
6639
+ aiNotes: si.aiNotes,
6640
+ finding: {
6641
+ severity: si.insight.severity,
6642
+ rule: 'insight-' + si.insight.type,
6643
+ title: si.insight.title,
6644
+ desc: si.insight.desc,
6645
+ hint: si.insight.hint,
6646
+ endpoint: si.insight.nav || 'global',
6647
+ count: 1
6648
+ }
6649
+ });
6650
+ }
6238
6651
  var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
6239
6652
  var resolved = all.filter(function(f) { return f.state === 'resolved'; });
6240
6653
 
@@ -6282,12 +6695,13 @@ function getSecurityView() {
6282
6695
  var groups = {};
6283
6696
  var groupOrder = [];
6284
6697
  for (var gi = 0; gi < open.length; gi++) {
6285
- var f = open[gi].finding;
6698
+ var sf = open[gi];
6699
+ var f = sf.finding;
6286
6700
  if (!groups[f.rule]) {
6287
6701
  groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6288
6702
  groupOrder.push(f.rule);
6289
6703
  }
6290
- groups[f.rule].items.push(f);
6704
+ groups[f.rule].items.push(sf);
6291
6705
  }
6292
6706
 
6293
6707
  groupOrder.sort(function(a, b) {
@@ -6324,12 +6738,21 @@ function getSecurityView() {
6324
6738
  var list = document.createElement('div');
6325
6739
  list.className = 'sec-items';
6326
6740
  for (var ii = 0; ii < group.items.length; ii++) {
6327
- var item = group.items[ii];
6741
+ var sf2 = group.items[ii];
6742
+ var item = sf2.finding;
6328
6743
  var row = document.createElement('div');
6329
6744
  row.className = 'sec-item';
6745
+ var aiBadge = '';
6746
+ if (sf2.state === 'fixing' && sf2.aiStatus === 'fixed') {
6747
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
6748
+ } else if (sf2.aiStatus === 'wont_fix') {
6749
+ aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
6750
+ }
6751
+ var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
6330
6752
  row.innerHTML =
6331
6753
  '<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
6332
- (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
6754
+ (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '') +
6755
+ aiBadge + aiNotes;
6333
6756
  list.appendChild(row);
6334
6757
  }
6335
6758
  section.appendChild(list);
@@ -6348,12 +6771,16 @@ function getSecurityView() {
6348
6771
  var resolvedItems = document.createElement('div');
6349
6772
  resolvedItems.className = 'sec-items';
6350
6773
  for (var ri = 0; ri < resolved.length; ri++) {
6351
- var rf = resolved[ri].finding;
6774
+ var rsf = resolved[ri];
6775
+ var rf = rsf.finding;
6352
6776
  var rRow = document.createElement('div');
6353
6777
  rRow.className = 'sec-item sec-item-resolved';
6778
+ var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
6779
+ var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
6354
6780
  rRow.innerHTML =
6355
6781
  '<span class="sec-resolved-item-icon">\\u2713</span>' +
6356
- '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>';
6782
+ '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>' +
6783
+ verifiedBadge + rNotes;
6357
6784
  resolvedItems.appendChild(rRow);
6358
6785
  }
6359
6786
  resolvedGroup.appendChild(resolvedItems);
@@ -6444,6 +6871,7 @@ function getApp() {
6444
6871
  events.addEventListener('insights', function(e) {
6445
6872
  state.insights = JSON.parse(e.data);
6446
6873
  if (state.activeView === 'overview') renderOverview();
6874
+ if (state.activeView === 'security') renderSecurity();
6447
6875
  updateStats();
6448
6876
  });
6449
6877
 
@@ -6646,47 +7074,178 @@ var init_page = __esm({
6646
7074
  // src/telemetry/config.ts
6647
7075
  import { homedir } from "os";
6648
7076
  import { join as join2 } from "path";
6649
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6650
- import { randomUUID as randomUUID3 } from "crypto";
7077
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
7078
+ import { randomUUID as randomUUID4 } from "crypto";
6651
7079
  function readConfig() {
6652
7080
  try {
6653
- if (!existsSync4(CONFIG_PATH)) return null;
7081
+ if (!existsSync5(CONFIG_PATH)) return null;
6654
7082
  return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
6655
7083
  } catch {
6656
7084
  return null;
6657
7085
  }
6658
7086
  }
7087
+ function writeConfig(config) {
7088
+ try {
7089
+ if (!existsSync5(CONFIG_DIR))
7090
+ mkdirSync3(CONFIG_DIR, { recursive: true, mode: 448 });
7091
+ writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
7092
+ mode: 384
7093
+ });
7094
+ } catch {
7095
+ }
7096
+ }
7097
+ function getOrCreateConfig() {
7098
+ const existing = readConfig();
7099
+ if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
7100
+ return existing;
7101
+ }
7102
+ const config = { telemetry: true, anonymousId: randomUUID4() };
7103
+ writeConfig(config);
7104
+ return config;
7105
+ }
6659
7106
  function isTelemetryEnabled() {
7107
+ if (cachedEnabled !== null) return cachedEnabled;
6660
7108
  const env = process.env.BRAKIT_TELEMETRY;
6661
- if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
6662
- return readConfig()?.telemetry ?? true;
7109
+ if (env !== void 0) {
7110
+ cachedEnabled = env !== "false" && env !== "0" && env !== "off";
7111
+ return cachedEnabled;
7112
+ }
7113
+ cachedEnabled = readConfig()?.telemetry ?? true;
7114
+ return cachedEnabled;
6663
7115
  }
6664
- var CONFIG_DIR, CONFIG_PATH;
7116
+ var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
6665
7117
  var init_config = __esm({
6666
7118
  "src/telemetry/config.ts"() {
6667
7119
  "use strict";
6668
7120
  CONFIG_DIR = join2(homedir(), ".brakit");
6669
7121
  CONFIG_PATH = join2(CONFIG_DIR, "config.json");
7122
+ cachedEnabled = null;
6670
7123
  }
6671
7124
  });
6672
7125
 
6673
7126
  // src/telemetry/index.ts
6674
7127
  import { platform, release, arch } from "os";
7128
+ import { spawn } from "child_process";
7129
+ function initSession(framework, packageManager, isCustomCommand, adapters) {
7130
+ session.startTime = Date.now();
7131
+ session.framework = framework;
7132
+ session.packageManager = packageManager;
7133
+ session.isCustomCommand = isCustomCommand;
7134
+ session.adapters = adapters;
7135
+ }
7136
+ function recordRequestCount(count) {
7137
+ session.requestCount = count;
7138
+ }
7139
+ function recordInsightTypes(types) {
7140
+ for (const t of types) session.insightTypes.add(t);
7141
+ }
7142
+ function recordRulesTriggered(rules) {
7143
+ for (const r of rules) session.rulesTriggered.add(r);
7144
+ }
6675
7145
  function recordTabViewed(tab) {
6676
- tabsViewed.add(tab);
7146
+ session.tabsViewed.add(tab);
6677
7147
  }
6678
7148
  function recordDashboardOpened() {
6679
- dashboardOpened = true;
7149
+ session.dashboardOpened = true;
7150
+ }
7151
+ function speedBucket(ms) {
7152
+ if (ms === 0) return "none";
7153
+ if (ms < 200) return "<200ms";
7154
+ if (ms < 500) return "200-500ms";
7155
+ if (ms < 1e3) return "500-1000ms";
7156
+ if (ms < 2e3) return "1000-2000ms";
7157
+ if (ms < 5e3) return "2000-5000ms";
7158
+ return ">5000ms";
7159
+ }
7160
+ function trackSession(registry) {
7161
+ if (!isTelemetryEnabled()) return;
7162
+ const isFirstSession = readConfig() === null;
7163
+ const config = getOrCreateConfig();
7164
+ const metricsStore = registry.get("metrics-store");
7165
+ const analysisEngine = registry.get("analysis-engine");
7166
+ const live = metricsStore.getLiveEndpoints();
7167
+ const insights = analysisEngine.getInsights();
7168
+ const findings = analysisEngine.getFindings();
7169
+ let totalRequests = 0;
7170
+ let totalDuration = 0;
7171
+ let slowestP95 = 0;
7172
+ for (const ep of live) {
7173
+ totalRequests += ep.summary.totalRequests;
7174
+ totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
7175
+ if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
7176
+ }
7177
+ const payload = {
7178
+ api_key: POSTHOG_KEY,
7179
+ event: "session",
7180
+ distinct_id: config.anonymousId,
7181
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7182
+ properties: {
7183
+ brakit_version: VERSION,
7184
+ node_version: process.version,
7185
+ os: `${platform()}-${release()}`,
7186
+ arch: arch(),
7187
+ framework: session.framework,
7188
+ package_manager: session.packageManager,
7189
+ is_custom_command: session.isCustomCommand,
7190
+ first_session: isFirstSession,
7191
+ adapters_detected: session.adapters,
7192
+ request_count: session.requestCount,
7193
+ error_count: registry.get("error-store").getAll().length,
7194
+ query_count: registry.get("query-store").getAll().length,
7195
+ fetch_count: registry.get("fetch-store").getAll().length,
7196
+ insight_count: insights.length,
7197
+ finding_count: findings.length,
7198
+ insight_types: [...session.insightTypes],
7199
+ rules_triggered: [...session.rulesTriggered],
7200
+ endpoint_count: live.length,
7201
+ avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
7202
+ slowest_endpoint_bucket: speedBucket(slowestP95),
7203
+ tabs_viewed: [...session.tabsViewed],
7204
+ dashboard_opened: session.dashboardOpened,
7205
+ explain_used: session.explainUsed,
7206
+ session_duration_s: Math.round((Date.now() - session.startTime) / 1e3),
7207
+ $lib: "brakit",
7208
+ $process_person_profile: false,
7209
+ $geoip_disable: true
7210
+ }
7211
+ };
7212
+ try {
7213
+ const body = JSON.stringify(payload);
7214
+ const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
7215
+ const child = spawn(
7216
+ process.execPath,
7217
+ [
7218
+ "-e",
7219
+ `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
7220
+ ],
7221
+ { detached: true, stdio: "ignore" }
7222
+ );
7223
+ child.unref();
7224
+ } catch {
7225
+ }
6680
7226
  }
6681
- var tabsViewed, dashboardOpened;
6682
- var init_telemetry = __esm({
7227
+ var POSTHOG_KEY, session;
7228
+ var init_telemetry2 = __esm({
6683
7229
  "src/telemetry/index.ts"() {
6684
7230
  "use strict";
6685
7231
  init_src();
6686
7232
  init_config();
7233
+ init_telemetry();
6687
7234
  init_config();
6688
- tabsViewed = /* @__PURE__ */ new Set();
6689
- dashboardOpened = false;
7235
+ POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
7236
+ session = {
7237
+ startTime: 0,
7238
+ framework: "",
7239
+ packageManager: "",
7240
+ isCustomCommand: false,
7241
+ adapters: [],
7242
+ requestCount: 0,
7243
+ insightTypes: /* @__PURE__ */ new Set(),
7244
+ rulesTriggered: /* @__PURE__ */ new Set(),
7245
+ tabsViewed: /* @__PURE__ */ new Set(),
7246
+ dashboardOpened: false,
7247
+ explainUsed: false
7248
+ };
6690
7249
  }
6691
7250
  });
6692
7251
 
@@ -6716,7 +7275,13 @@ function createDashboardHandler(registry) {
6716
7275
  routes[DASHBOARD_API_SECURITY] = createSecurityHandler(analysisEngine);
6717
7276
  }
6718
7277
  if (registry.has("finding-store")) {
6719
- routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(registry.get("finding-store"));
7278
+ const findingStore = registry.get("finding-store");
7279
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(findingStore);
7280
+ routes[DASHBOARD_API_FINDINGS_REPORT] = createFindingsReportHandler(
7281
+ findingStore,
7282
+ registry.get("event-bus"),
7283
+ analysisEngine
7284
+ );
6720
7285
  }
6721
7286
  routes[DASHBOARD_API_TAB] = (req, res) => {
6722
7287
  const raw = (req.url ?? "").split("tab=")[1];
@@ -6724,7 +7289,7 @@ function createDashboardHandler(registry) {
6724
7289
  const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
6725
7290
  if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
6726
7291
  }
6727
- res.writeHead(204);
7292
+ res.writeHead(HTTP_NO_CONTENT);
6728
7293
  res.end();
6729
7294
  };
6730
7295
  return (req, res, config) => {
@@ -6735,7 +7300,7 @@ function createDashboardHandler(registry) {
6735
7300
  return;
6736
7301
  }
6737
7302
  if (isTelemetryEnabled()) recordDashboardOpened();
6738
- res.writeHead(200, {
7303
+ res.writeHead(HTTP_OK, {
6739
7304
  "content-type": "text/html; charset=utf-8",
6740
7305
  "cache-control": "no-cache",
6741
7306
  ...SECURITY_HEADERS
@@ -6743,23 +7308,17 @@ function createDashboardHandler(registry) {
6743
7308
  res.end(getDashboardHtml(config));
6744
7309
  };
6745
7310
  }
6746
- var SECURITY_HEADERS;
6747
7311
  var init_router = __esm({
6748
7312
  "src/dashboard/router.ts"() {
6749
7313
  "use strict";
6750
7314
  init_constants();
7315
+ init_http();
6751
7316
  init_api();
6752
7317
  init_insights();
6753
7318
  init_findings();
6754
7319
  init_sse();
6755
7320
  init_page();
6756
- init_telemetry();
6757
- SECURITY_HEADERS = {
6758
- "x-content-type-options": "nosniff",
6759
- "x-frame-options": "DENY",
6760
- "referrer-policy": "no-referrer",
6761
- "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
6762
- };
7321
+ init_telemetry2();
6763
7322
  }
6764
7323
  });
6765
7324
 
@@ -6768,6 +7327,7 @@ var EventBus;
6768
7327
  var init_event_bus = __esm({
6769
7328
  "src/core/event-bus.ts"() {
6770
7329
  "use strict";
7330
+ init_log();
6771
7331
  EventBus = class {
6772
7332
  listeners = /* @__PURE__ */ new Map();
6773
7333
  emit(channel, data) {
@@ -6776,7 +7336,8 @@ var init_event_bus = __esm({
6776
7336
  for (const fn of set) {
6777
7337
  try {
6778
7338
  fn(data);
6779
- } catch {
7339
+ } catch (err) {
7340
+ brakitDebug(`EventBus listener threw on channel "${channel}": ${err}`);
6780
7341
  }
6781
7342
  }
6782
7343
  }
@@ -6840,9 +7401,9 @@ var init_static_patterns = __esm({
6840
7401
  });
6841
7402
 
6842
7403
  // src/store/request-store.ts
6843
- function flattenHeaders(headers) {
7404
+ function flattenHeaders(headers2) {
6844
7405
  const flat = {};
6845
- for (const [key, value] of Object.entries(headers)) {
7406
+ for (const [key, value] of Object.entries(headers2)) {
6846
7407
  if (value === void 0) continue;
6847
7408
  flat[key] = Array.isArray(value) ? value.join(", ") : value;
6848
7409
  }
@@ -6898,6 +7459,15 @@ var init_request_store = __esm({
6898
7459
  }
6899
7460
  return entry;
6900
7461
  }
7462
+ add(entry) {
7463
+ this.requests.push(entry);
7464
+ if (this.requests.length > this.maxEntries) {
7465
+ this.requests.shift();
7466
+ }
7467
+ for (const fn of this.listeners) {
7468
+ fn(entry);
7469
+ }
7470
+ }
6901
7471
  getAll() {
6902
7472
  return this.requests;
6903
7473
  }
@@ -6916,7 +7486,7 @@ var init_request_store = __esm({
6916
7486
  });
6917
7487
 
6918
7488
  // src/store/telemetry-store.ts
6919
- import { randomUUID as randomUUID4 } from "crypto";
7489
+ import { randomUUID as randomUUID5 } from "crypto";
6920
7490
  var TelemetryStore;
6921
7491
  var init_telemetry_store = __esm({
6922
7492
  "src/store/telemetry-store.ts"() {
@@ -6929,7 +7499,7 @@ var init_telemetry_store = __esm({
6929
7499
  entries = [];
6930
7500
  listeners = [];
6931
7501
  add(data) {
6932
- const entry = { id: randomUUID4(), ...data };
7502
+ const entry = { id: randomUUID5(), ...data };
6933
7503
  this.entries.push(entry);
6934
7504
  if (this.entries.length > this.maxEntries) this.entries.shift();
6935
7505
  for (const fn of this.listeners) fn(entry);
@@ -7013,7 +7583,7 @@ var init_math = __esm({
7013
7583
  });
7014
7584
 
7015
7585
  // src/store/metrics/metrics-store.ts
7016
- import { randomUUID as randomUUID5 } from "crypto";
7586
+ import { randomUUID as randomUUID6 } from "crypto";
7017
7587
  function createAccumulator() {
7018
7588
  return {
7019
7589
  durations: [],
@@ -7037,19 +7607,23 @@ var init_metrics_store = __esm({
7037
7607
  MetricsStore = class {
7038
7608
  constructor(persistence) {
7039
7609
  this.persistence = persistence;
7040
- this.data = persistence.load();
7041
- for (const ep of this.data.endpoints) {
7042
- this.endpointIndex.set(ep.endpoint, ep);
7043
- }
7610
+ this.data = { version: 1, endpoints: [] };
7044
7611
  }
7045
7612
  data;
7046
7613
  endpointIndex = /* @__PURE__ */ new Map();
7047
- sessionId = randomUUID5();
7614
+ sessionId = randomUUID6();
7048
7615
  sessionStart = Date.now();
7049
7616
  flushTimer = null;
7050
7617
  accumulators = /* @__PURE__ */ new Map();
7051
7618
  pendingPoints = /* @__PURE__ */ new Map();
7052
7619
  start() {
7620
+ this.persistence.loadAsync().then((data) => {
7621
+ this.data = data;
7622
+ for (const ep of this.data.endpoints) {
7623
+ this.endpointIndex.set(ep.endpoint, ep);
7624
+ }
7625
+ }).catch(() => {
7626
+ });
7053
7627
  this.flushTimer = setInterval(
7054
7628
  () => this.flush(),
7055
7629
  METRICS_FLUSH_INTERVAL_MS
@@ -7155,7 +7729,7 @@ var init_metrics_store = __esm({
7155
7729
  for (const [endpoint, acc] of this.accumulators) {
7156
7730
  if (acc.durations.length === 0) continue;
7157
7731
  const n = acc.totalRequestCount;
7158
- const session = {
7732
+ const session2 = {
7159
7733
  sessionId: this.sessionId,
7160
7734
  startedAt: this.sessionStart,
7161
7735
  avgDurationMs: Math.round(acc.totalDurationSum / n),
@@ -7171,9 +7745,9 @@ var init_metrics_store = __esm({
7171
7745
  (s) => s.sessionId === this.sessionId
7172
7746
  );
7173
7747
  if (existingIdx !== -1) {
7174
- epMetrics.sessions[existingIdx] = session;
7748
+ epMetrics.sessions[existingIdx] = session2;
7175
7749
  } else {
7176
- epMetrics.sessions.push(session);
7750
+ epMetrics.sessions.push(session2);
7177
7751
  }
7178
7752
  if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
7179
7753
  epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
@@ -7209,7 +7783,8 @@ var init_metrics_store = __esm({
7209
7783
  });
7210
7784
 
7211
7785
  // src/store/metrics/persistence.ts
7212
- import { readFileSync as readFileSync4, existsSync as existsSync5, unlinkSync } from "fs";
7786
+ import { readFile as readFile4 } from "fs/promises";
7787
+ import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync } from "fs";
7213
7788
  import { resolve as resolve3 } from "path";
7214
7789
  var FileMetricsPersistence;
7215
7790
  var init_persistence = __esm({
@@ -7217,7 +7792,9 @@ var init_persistence = __esm({
7217
7792
  "use strict";
7218
7793
  init_constants();
7219
7794
  init_atomic_writer();
7795
+ init_fs();
7220
7796
  init_log();
7797
+ init_type_guards();
7221
7798
  FileMetricsPersistence = class {
7222
7799
  metricsPath;
7223
7800
  writer;
@@ -7232,7 +7809,7 @@ var init_persistence = __esm({
7232
7809
  }
7233
7810
  load() {
7234
7811
  try {
7235
- if (existsSync5(this.metricsPath)) {
7812
+ if (existsSync6(this.metricsPath)) {
7236
7813
  const raw = readFileSync4(this.metricsPath, "utf-8");
7237
7814
  const parsed = JSON.parse(raw);
7238
7815
  if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
@@ -7240,7 +7817,21 @@ var init_persistence = __esm({
7240
7817
  }
7241
7818
  }
7242
7819
  } catch (err) {
7243
- brakitWarn(`failed to load metrics: ${err.message}`);
7820
+ brakitWarn(`failed to load metrics: ${getErrorMessage(err)}`);
7821
+ }
7822
+ return { version: 1, endpoints: [] };
7823
+ }
7824
+ async loadAsync() {
7825
+ try {
7826
+ if (await fileExists(this.metricsPath)) {
7827
+ const raw = await readFile4(this.metricsPath, "utf-8");
7828
+ const parsed = JSON.parse(raw);
7829
+ if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
7830
+ return parsed;
7831
+ }
7832
+ }
7833
+ } catch (err) {
7834
+ brakitWarn(`failed to load metrics: ${getErrorMessage(err)}`);
7244
7835
  }
7245
7836
  return { version: 1, endpoints: [] };
7246
7837
  }
@@ -7252,7 +7843,7 @@ var init_persistence = __esm({
7252
7843
  }
7253
7844
  remove() {
7254
7845
  try {
7255
- if (existsSync5(this.metricsPath)) {
7846
+ if (existsSync6(this.metricsPath)) {
7256
7847
  unlinkSync(this.metricsPath);
7257
7848
  }
7258
7849
  } catch {
@@ -7379,17 +7970,28 @@ var init_health2 = __esm({
7379
7970
  BrakitHealth = class {
7380
7971
  errorCount = 0;
7381
7972
  disabled = false;
7973
+ disabledAt = 0;
7382
7974
  teardownFn = null;
7383
7975
  reportError() {
7384
7976
  this.errorCount++;
7385
7977
  if (this.errorCount >= MAX_HEALTH_ERRORS && !this.disabled) {
7386
7978
  this.disabled = true;
7387
- console.warn("brakit: too many errors, disabling for this session.");
7979
+ this.disabledAt = Date.now();
7980
+ try {
7981
+ process.stderr.write("brakit: too many errors, disabling temporarily.\n");
7982
+ } catch {
7983
+ }
7388
7984
  this.teardownFn?.();
7389
7985
  }
7390
7986
  }
7391
7987
  isActive() {
7392
- return !this.disabled;
7988
+ if (!this.disabled) return true;
7989
+ if (Date.now() - this.disabledAt > RECOVERY_WINDOW_MS) {
7990
+ this.disabled = false;
7991
+ this.errorCount = 0;
7992
+ return true;
7993
+ }
7994
+ return false;
7393
7995
  }
7394
7996
  setTeardown(fn) {
7395
7997
  this.teardownFn = fn;
@@ -7430,10 +8032,10 @@ var init_guard = __esm({
7430
8032
  });
7431
8033
 
7432
8034
  // src/runtime/capture.ts
7433
- import { gunzipSync, brotliDecompressSync, inflateSync } from "zlib";
7434
- function outgoingToIncoming(headers) {
8035
+ import { gunzip, brotliDecompress, inflate } from "zlib";
8036
+ function outgoingToIncoming(headers2) {
7435
8037
  const result = {};
7436
- for (const [key, value] of Object.entries(headers)) {
8038
+ for (const [key, value] of Object.entries(headers2)) {
7437
8039
  if (value === void 0) continue;
7438
8040
  if (Array.isArray(value)) {
7439
8041
  result[key] = value.map(String);
@@ -7443,15 +8045,19 @@ function outgoingToIncoming(headers) {
7443
8045
  }
7444
8046
  return result;
7445
8047
  }
7446
- function decompress(body, encoding) {
7447
- try {
7448
- if (encoding === CONTENT_ENCODING_GZIP) return gunzipSync(body);
7449
- if (encoding === CONTENT_ENCODING_BR) return brotliDecompressSync(body);
7450
- if (encoding === CONTENT_ENCODING_DEFLATE) return inflateSync(body);
7451
- } catch (e) {
7452
- brakitDebug(`decompress failed: ${e.message}`);
7453
- }
7454
- return body;
8048
+ function decompressAsync(body, encoding) {
8049
+ const decompressor = encoding === CONTENT_ENCODING_GZIP ? gunzip : encoding === CONTENT_ENCODING_BR ? brotliDecompress : encoding === CONTENT_ENCODING_DEFLATE ? inflate : null;
8050
+ if (!decompressor) return Promise.resolve(body);
8051
+ return new Promise((resolve5) => {
8052
+ decompressor(body, (err, result) => {
8053
+ if (err) {
8054
+ brakitDebug(`decompress failed: ${err.message}`);
8055
+ resolve5(body);
8056
+ } else {
8057
+ resolve5(result);
8058
+ }
8059
+ });
8060
+ });
7455
8061
  }
7456
8062
  function toBuffer(chunk) {
7457
8063
  if (Buffer.isBuffer(chunk)) return chunk;
@@ -7495,29 +8101,35 @@ function captureInProcess(req, res, requestId, requestStore) {
7495
8101
  }
7496
8102
  const result = originalEnd.apply(this, args);
7497
8103
  const endTime = performance.now();
7498
- try {
7499
- const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
7500
- let body = resChunks.length > 0 ? Buffer.concat(resChunks) : null;
7501
- if (body && encoding) {
7502
- body = decompress(body, encoding);
8104
+ const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
8105
+ const statusCode = res.statusCode;
8106
+ const responseHeaders = outgoingToIncoming(res.getHeaders());
8107
+ const responseContentType = String(res.getHeader("content-type") ?? "");
8108
+ const capturedChunks = resChunks.slice();
8109
+ void (async () => {
8110
+ try {
8111
+ let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
8112
+ if (body && encoding) {
8113
+ body = await decompressAsync(body, encoding);
8114
+ }
8115
+ requestStore.capture({
8116
+ requestId,
8117
+ method,
8118
+ url: req.url ?? "/",
8119
+ requestHeaders: req.headers,
8120
+ requestBody: null,
8121
+ statusCode,
8122
+ responseHeaders,
8123
+ responseBody: body,
8124
+ responseContentType,
8125
+ startTime,
8126
+ endTime,
8127
+ config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
8128
+ });
8129
+ } catch (e) {
8130
+ brakitDebug(`capture store: ${e.message}`);
7503
8131
  }
7504
- requestStore.capture({
7505
- requestId,
7506
- method,
7507
- url: req.url ?? "/",
7508
- requestHeaders: req.headers,
7509
- requestBody: null,
7510
- statusCode: res.statusCode,
7511
- responseHeaders: outgoingToIncoming(res.getHeaders()),
7512
- responseBody: body,
7513
- responseContentType: String(res.getHeader("content-type") ?? ""),
7514
- startTime,
7515
- endTime,
7516
- config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
7517
- });
7518
- } catch (e) {
7519
- brakitDebug(`capture store: ${e.message}`);
7520
- }
8132
+ })();
7521
8133
  return result;
7522
8134
  };
7523
8135
  }
@@ -7531,7 +8143,7 @@ var init_capture = __esm({
7531
8143
 
7532
8144
  // src/runtime/interceptor.ts
7533
8145
  import http from "http";
7534
- import { randomUUID as randomUUID6 } from "crypto";
8146
+ import { randomUUID as randomUUID7 } from "crypto";
7535
8147
  function installInterceptor(deps) {
7536
8148
  originalEmit = http.Server.prototype.emit;
7537
8149
  const saved = originalEmit;
@@ -7557,14 +8169,14 @@ function installInterceptor(deps) {
7557
8169
  }
7558
8170
  if (isDashboardRequest(url)) {
7559
8171
  if (!isLocalRequest(req)) {
7560
- res.writeHead(404);
8172
+ res.writeHead(HTTP_NOT_FOUND);
7561
8173
  res.end("Not Found");
7562
8174
  return true;
7563
8175
  }
7564
8176
  deps.handleDashboard(req, res, deps.config);
7565
8177
  return true;
7566
8178
  }
7567
- const requestId = randomUUID6();
8179
+ const requestId = randomUUID7();
7568
8180
  const ctx = {
7569
8181
  requestId,
7570
8182
  url,
@@ -7593,6 +8205,7 @@ var init_interceptor = __esm({
7593
8205
  init_safe_wrap();
7594
8206
  init_guard();
7595
8207
  init_capture();
8208
+ init_http();
7596
8209
  originalEmit = null;
7597
8210
  }
7598
8211
  });
@@ -7602,11 +8215,15 @@ var setup_exports = {};
7602
8215
  __export(setup_exports, {
7603
8216
  setup: () => setup
7604
8217
  });
7605
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
8218
+ import { readFile as readFile5, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
8219
+ import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
7606
8220
  import { resolve as resolve4 } from "path";
7607
8221
  function setup() {
7608
- if (initialized) return;
7609
- initialized = true;
8222
+ if (initPromise) return initPromise;
8223
+ initPromise = doSetup();
8224
+ return initPromise;
8225
+ }
8226
+ async function doSetup() {
7610
8227
  const bus = new EventBus();
7611
8228
  const registry = new ServiceRegistry();
7612
8229
  const requestStore = new RequestStore();
@@ -7635,6 +8252,19 @@ function setup() {
7635
8252
  const adapterRegistry = createDefaultRegistry();
7636
8253
  adapterRegistry.patchAll(telemetryEmit);
7637
8254
  const cwd = process.cwd();
8255
+ let framework = "unknown";
8256
+ try {
8257
+ const pkg = JSON.parse(await readFile5(resolve4(cwd, "package.json"), "utf-8"));
8258
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
8259
+ framework = detectFrameworkFromDeps(allDeps);
8260
+ } catch {
8261
+ }
8262
+ initSession(
8263
+ framework,
8264
+ detectPackageManagerSync(cwd),
8265
+ false,
8266
+ adapterRegistry.getActive().map((a) => a.name)
8267
+ );
7638
8268
  const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
7639
8269
  metricsStore.start();
7640
8270
  registry.register("metrics-store", metricsStore);
@@ -7667,22 +8297,42 @@ function setup() {
7667
8297
  requestStore,
7668
8298
  onFirstRequest(port) {
7669
8299
  setBrakitPort(port);
7670
- const dir = resolve4(cwd, METRICS_DIR);
7671
- if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
7672
- const portPath = resolve4(cwd, PORT_FILE);
7673
- if (existsSync6(portPath)) {
7674
- const old = readFileSync5(portPath, "utf-8").trim();
7675
- if (old && old !== String(port)) {
7676
- brakitDebug(`Overwriting stale port file (was ${old}, now ${port})`);
8300
+ void (async () => {
8301
+ try {
8302
+ const dir = resolve4(cwd, METRICS_DIR);
8303
+ await mkdir2(dir, { recursive: true });
8304
+ const portPath = resolve4(cwd, PORT_FILE);
8305
+ try {
8306
+ const old = await readFile5(portPath, "utf-8");
8307
+ if (old.trim() && old.trim() !== String(port)) {
8308
+ brakitDebug(`Overwriting stale port file (was ${old.trim()}, now ${port})`);
8309
+ }
8310
+ } catch {
8311
+ }
8312
+ await writeFile3(portPath, String(port));
8313
+ } catch (err) {
8314
+ brakitDebug(`port file write failed: ${err.message}`);
7677
8315
  }
7678
- }
7679
- writeFileSync4(portPath, String(port));
8316
+ })();
7680
8317
  terminalDispose = startTerminalInsights(registry, port);
7681
8318
  process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
7682
8319
  `);
7683
8320
  }
7684
8321
  });
7685
- health.setTeardown(() => {
8322
+ let telemetrySent = false;
8323
+ const sendTelemetry = () => {
8324
+ if (telemetrySent) return;
8325
+ telemetrySent = true;
8326
+ recordRequestCount(requestStore.getAll().length);
8327
+ recordInsightTypes(analysisEngine.getInsights().map((i) => i.type));
8328
+ recordRulesTriggered(analysisEngine.getFindings().map((f) => f.rule));
8329
+ trackSession(registry);
8330
+ };
8331
+ let teardownCalled = false;
8332
+ const runTeardown = () => {
8333
+ if (teardownCalled) return;
8334
+ teardownCalled = true;
8335
+ sendTelemetry();
7686
8336
  uninstallInterceptor();
7687
8337
  terminalDispose?.();
7688
8338
  analysisEngine.stop();
@@ -7690,12 +8340,19 @@ function setup() {
7690
8340
  metricsStore.stop();
7691
8341
  try {
7692
8342
  const portPath = resolve4(cwd, PORT_FILE);
7693
- if (existsSync6(portPath)) unlinkSync2(portPath);
8343
+ if (existsSync7(portPath)) unlinkSync2(portPath);
7694
8344
  } catch {
7695
8345
  }
8346
+ };
8347
+ health.setTeardown(runTeardown);
8348
+ process.on("beforeExit", () => {
8349
+ sendTelemetry();
8350
+ });
8351
+ process.on("exit", () => {
8352
+ runTeardown();
7696
8353
  });
7697
8354
  }
7698
- var initialized;
8355
+ var initPromise;
7699
8356
  var init_setup = __esm({
7700
8357
  "src/runtime/setup.ts"() {
7701
8358
  "use strict";
@@ -7720,7 +8377,9 @@ var init_setup = __esm({
7720
8377
  init_health2();
7721
8378
  init_interceptor();
7722
8379
  init_log();
7723
- initialized = false;
8380
+ init_project();
8381
+ init_telemetry2();
8382
+ initPromise = null;
7724
8383
  }
7725
8384
  });
7726
8385
 
@@ -7739,7 +8398,7 @@ function shouldActivate() {
7739
8398
  if (shouldActivate()) {
7740
8399
  try {
7741
8400
  const { setup: setup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
7742
- setup2();
8401
+ await setup2();
7743
8402
  } catch (err) {
7744
8403
  console.warn("brakit: failed to start \u2014", err?.message);
7745
8404
  }