brakit 0.8.4 → 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,28 +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
- };
224
213
  }
225
214
  });
226
215
 
227
216
  // src/constants/telemetry.ts
228
- var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, POSTHOG_SPAWN_TIMEOUT_MS, SIGNAL_EXIT_SIGINT, SIGNAL_EXIT_SIGTERM;
217
+ var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS;
229
218
  var init_telemetry = __esm({
230
219
  "src/constants/telemetry.ts"() {
231
220
  "use strict";
232
221
  POSTHOG_HOST = "https://us.i.posthog.com";
233
222
  POSTHOG_CAPTURE_PATH = "/i/v0/e/";
234
223
  POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
235
- POSTHOG_SPAWN_TIMEOUT_MS = 5e3;
236
- SIGNAL_EXIT_SIGINT = 130;
237
- SIGNAL_EXIT_SIGTERM = 143;
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"]);
238
234
  }
239
235
  });
240
236
 
@@ -253,6 +249,7 @@ var init_constants = __esm({
253
249
  init_encoding();
254
250
  init_severity();
255
251
  init_telemetry();
252
+ init_lifecycle();
256
253
  }
257
254
  });
258
255
 
@@ -419,11 +416,12 @@ function createCaptureError(emit) {
419
416
  }
420
417
  function setupErrorHook(emit) {
421
418
  const captureError = createCaptureError(emit);
422
- process.on("uncaughtException", (err) => {
419
+ const brakitExceptionHandler = (err) => {
423
420
  captureError(err);
424
- process.removeAllListeners("uncaughtException");
421
+ process.removeListener("uncaughtException", brakitExceptionHandler);
425
422
  throw err;
426
- });
423
+ };
424
+ process.on("uncaughtException", brakitExceptionHandler);
427
425
  process.on("unhandledRejection", (reason) => {
428
426
  captureError(reason);
429
427
  });
@@ -610,7 +608,10 @@ var init_pg = __esm({
610
608
  const result = saved.apply(this, args);
611
609
  if (result && typeof result.then === "function") {
612
610
  return result.then((res) => {
613
- emitQuery(res?.rowCount ?? void 0);
611
+ try {
612
+ emitQuery(res?.rowCount ?? void 0);
613
+ } catch {
614
+ }
614
615
  return res;
615
616
  });
616
617
  }
@@ -688,7 +689,10 @@ var init_mysql2 = __esm({
688
689
  const result = orig.apply(this, args);
689
690
  if (result && typeof result.then === "function") {
690
691
  return result.then((res) => {
691
- emitQuery();
692
+ try {
693
+ emitQuery();
694
+ } catch {
695
+ }
692
696
  return res;
693
697
  });
694
698
  }
@@ -803,6 +807,27 @@ var init_adapters = __esm({
803
807
  }
804
808
  });
805
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
+
806
831
  // src/analysis/categorize.ts
807
832
  function detectCategory(req) {
808
833
  const { method, url, statusCode, responseHeaders } = req;
@@ -1184,12 +1209,12 @@ var init_group = __esm({
1184
1209
  });
1185
1210
 
1186
1211
  // src/dashboard/api/shared.ts
1187
- function maskSensitiveHeaders(headers) {
1212
+ function maskSensitiveHeaders(headers2) {
1188
1213
  const masked = {};
1189
- for (const [key, value] of Object.entries(headers)) {
1214
+ for (const [key, value] of Object.entries(headers2)) {
1190
1215
  if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1191
1216
  const s = String(value);
1192
- masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? "****" : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
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);
1193
1218
  } else {
1194
1219
  masked[key] = value;
1195
1220
  }
@@ -1209,14 +1234,14 @@ function getCorsOrigin(req) {
1209
1234
  }
1210
1235
  function getJsonHeaders(req) {
1211
1236
  const corsOrigin = getCorsOrigin(req);
1212
- const headers = {
1237
+ const headers2 = {
1213
1238
  "content-type": "application/json",
1214
1239
  "cache-control": "no-cache"
1215
1240
  };
1216
1241
  if (corsOrigin) {
1217
- headers["access-control-allow-origin"] = corsOrigin;
1242
+ headers2["access-control-allow-origin"] = corsOrigin;
1218
1243
  }
1219
- return headers;
1244
+ return headers2;
1220
1245
  }
1221
1246
  function sendJson(req, res, status, data) {
1222
1247
  res.writeHead(status, getJsonHeaders(req));
@@ -1224,23 +1249,58 @@ function sendJson(req, res, status, data) {
1224
1249
  }
1225
1250
  function requireGet(req, res) {
1226
1251
  if (req.method !== "GET") {
1227
- sendJson(req, res, 405, { error: "Method not allowed" });
1252
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1228
1253
  return false;
1229
1254
  }
1230
1255
  return true;
1231
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
+ }
1232
1291
  function handleTelemetryGet(req, res, store) {
1233
1292
  if (!requireGet(req, res)) return;
1234
- const url = new URL(req.url ?? "/", "http://localhost");
1293
+ const url = parseRequestUrl(req);
1235
1294
  const requestId = url.searchParams.get("requestId");
1236
1295
  const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1237
- sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1296
+ sendJson(req, res, HTTP_OK, { total: entries.length, entries: entries.reverse() });
1238
1297
  }
1239
1298
  var init_shared2 = __esm({
1240
1299
  "src/dashboard/api/shared.ts"() {
1241
1300
  "use strict";
1242
1301
  init_constants();
1243
1302
  init_limits();
1303
+ init_http();
1244
1304
  }
1245
1305
  });
1246
1306
 
@@ -1255,7 +1315,7 @@ function sanitizeRequest(r) {
1255
1315
  function createRequestsHandler(registry) {
1256
1316
  return (req, res) => {
1257
1317
  if (!requireGet(req, res)) return;
1258
- const url = new URL(req.url ?? "/", "http://localhost");
1318
+ const url = parseRequestUrl(req);
1259
1319
  const method = url.searchParams.get("method");
1260
1320
  const status = url.searchParams.get("status");
1261
1321
  const search = url.searchParams.get("search");
@@ -1288,7 +1348,7 @@ function createRequestsHandler(registry) {
1288
1348
  const total = results.length;
1289
1349
  results = results.slice(offset, offset + limit);
1290
1350
  const sanitized = results.map(sanitizeRequest);
1291
- sendJson(req, res, 200, { total, requests: sanitized });
1351
+ sendJson(req, res, HTTP_OK, { total, requests: sanitized });
1292
1352
  };
1293
1353
  }
1294
1354
  function createFlowsHandler(registry) {
@@ -1298,13 +1358,13 @@ function createFlowsHandler(registry) {
1298
1358
  ...flow,
1299
1359
  requests: flow.requests.map(sanitizeRequest)
1300
1360
  }));
1301
- sendJson(req, res, 200, { total: flows.length, flows });
1361
+ sendJson(req, res, HTTP_OK, { total: flows.length, flows });
1302
1362
  };
1303
1363
  }
1304
1364
  function createClearHandler(registry) {
1305
1365
  return (req, res) => {
1306
1366
  if (req.method !== "POST") {
1307
- sendJson(req, res, 405, { error: "Method not allowed" });
1367
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1308
1368
  return;
1309
1369
  }
1310
1370
  registry.get("request-store").clear();
@@ -1315,7 +1375,7 @@ function createClearHandler(registry) {
1315
1375
  registry.get("metrics-store").reset();
1316
1376
  if (registry.has("finding-store")) registry.get("finding-store").clear();
1317
1377
  registry.get("event-bus").emit("store:cleared", void 0);
1318
- sendJson(req, res, 200, { cleared: true });
1378
+ sendJson(req, res, HTTP_OK, { cleared: true });
1319
1379
  };
1320
1380
  }
1321
1381
  function createFetchesHandler(registry) {
@@ -1335,10 +1395,158 @@ var init_handlers = __esm({
1335
1395
  "use strict";
1336
1396
  init_group();
1337
1397
  init_constants();
1398
+ init_http();
1338
1399
  init_shared2();
1339
1400
  }
1340
1401
  });
1341
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
+
1342
1550
  // src/dashboard/api/ingest.ts
1343
1551
  function isBrakitBatch(msg) {
1344
1552
  return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
@@ -1363,65 +1571,21 @@ function createIngestHandler(registry) {
1363
1571
  break;
1364
1572
  }
1365
1573
  };
1366
- const routeSDKEvent = (event) => {
1367
- const ts = event.timestamp || Date.now();
1368
- const parentRequestId = event.requestId ?? null;
1369
- switch (event.type) {
1370
- case "db.query":
1371
- registry.get("query-store").add({
1372
- driver: event.data.source ?? "sdk",
1373
- source: event.data.source ?? "sdk",
1374
- sql: event.data.sql,
1375
- model: event.data.model,
1376
- operation: event.data.operation,
1377
- normalizedOp: event.data.normalizedOp ?? event.data.operation ?? "OTHER",
1378
- table: event.data.table ?? "",
1379
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1380
- rowCount: event.data.rowCount,
1381
- parentRequestId,
1382
- timestamp: ts
1383
- });
1384
- break;
1385
- case "fetch":
1386
- registry.get("fetch-store").add({
1387
- url: event.data.url ?? "",
1388
- method: event.data.method ?? "GET",
1389
- statusCode: event.data.statusCode ?? 0,
1390
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1391
- parentRequestId,
1392
- timestamp: ts
1393
- });
1394
- break;
1395
- case "log":
1396
- registry.get("log-store").add({
1397
- level: event.data.level ?? "log",
1398
- message: event.data.message ?? "",
1399
- parentRequestId,
1400
- timestamp: ts
1401
- });
1402
- break;
1403
- case "error":
1404
- registry.get("error-store").add({
1405
- name: event.data.name ?? "Error",
1406
- message: event.data.message ?? "",
1407
- stack: event.data.stack ?? "",
1408
- parentRequestId,
1409
- timestamp: ts
1410
- });
1411
- break;
1412
- case "auth.check":
1413
- registry.get("log-store").add({
1414
- level: "info",
1415
- message: `[auth] ${event.data.provider ?? "unknown"}: ${event.data.result ?? "check"}`,
1416
- parentRequestId,
1417
- timestamp: ts
1418
- });
1419
- break;
1420
- }
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)
1421
1585
  };
1422
1586
  return (req, res) => {
1423
1587
  if (req.method !== "POST") {
1424
- sendJson(req, res, 405, { error: "Method not allowed" });
1588
+ sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
1425
1589
  return;
1426
1590
  }
1427
1591
  const chunks = [];
@@ -1429,7 +1593,7 @@ function createIngestHandler(registry) {
1429
1593
  req.on("data", (chunk) => {
1430
1594
  totalSize += chunk.length;
1431
1595
  if (totalSize > MAX_INGEST_BYTES) {
1432
- sendJson(req, res, 413, { error: "Payload too large" });
1596
+ sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
1433
1597
  req.destroy();
1434
1598
  return;
1435
1599
  }
@@ -1441,9 +1605,9 @@ function createIngestHandler(registry) {
1441
1605
  const body = JSON.parse(Buffer.concat(chunks).toString());
1442
1606
  if (isSDKPayload(body)) {
1443
1607
  for (const event of body.events) {
1444
- routeSDKEvent(event);
1608
+ routeSDKEvent(event, stores);
1445
1609
  }
1446
- res.writeHead(204);
1610
+ res.writeHead(HTTP_NO_CONTENT);
1447
1611
  res.end();
1448
1612
  return;
1449
1613
  }
@@ -1451,13 +1615,19 @@ function createIngestHandler(registry) {
1451
1615
  for (const event of body.events) {
1452
1616
  routeEvent(event);
1453
1617
  }
1454
- res.writeHead(204);
1618
+ res.writeHead(HTTP_NO_CONTENT);
1455
1619
  res.end();
1456
1620
  return;
1457
1621
  }
1458
- sendJson(req, res, 400, { error: "Invalid batch" });
1622
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
1459
1623
  } catch {
1460
- 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();
1461
1631
  }
1462
1632
  });
1463
1633
  };
@@ -1466,7 +1636,9 @@ var init_ingest = __esm({
1466
1636
  "src/dashboard/api/ingest.ts"() {
1467
1637
  "use strict";
1468
1638
  init_limits();
1639
+ init_http();
1469
1640
  init_shared2();
1641
+ init_sdk_event_parser();
1470
1642
  }
1471
1643
  });
1472
1644
 
@@ -1474,20 +1646,21 @@ var init_ingest = __esm({
1474
1646
  function createMetricsHandler(metricsStore) {
1475
1647
  return (req, res) => {
1476
1648
  if (!requireGet(req, res)) return;
1477
- const url = new URL(req.url ?? "/", "http://localhost");
1649
+ const url = parseRequestUrl(req);
1478
1650
  const endpoint = url.searchParams.get("endpoint");
1479
1651
  if (endpoint) {
1480
1652
  const ep = metricsStore.getEndpoint(endpoint);
1481
- sendJson(req, res, 200, { endpoints: ep ? [ep] : [] });
1653
+ sendJson(req, res, HTTP_OK, { endpoints: ep ? [ep] : [] });
1482
1654
  return;
1483
1655
  }
1484
- sendJson(req, res, 200, { endpoints: metricsStore.getAll() });
1656
+ sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
1485
1657
  };
1486
1658
  }
1487
1659
  var init_metrics2 = __esm({
1488
1660
  "src/dashboard/api/metrics.ts"() {
1489
1661
  "use strict";
1490
1662
  init_shared2();
1663
+ init_http();
1491
1664
  }
1492
1665
  });
1493
1666
 
@@ -1505,15 +1678,34 @@ var init_metrics_live = __esm({
1505
1678
  }
1506
1679
  });
1507
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
+
1508
1700
  // src/dashboard/api/activity.ts
1509
1701
  function createActivityHandler(registry) {
1510
1702
  return (req, res) => {
1511
1703
  if (!requireGet(req, res)) return;
1512
1704
  try {
1513
- const url = new URL(req.url ?? "/", "http://localhost");
1705
+ const url = parseRequestUrl(req);
1514
1706
  const requestId = url.searchParams.get("requestId");
1515
1707
  if (!requestId) {
1516
- sendJson(req, res, 400, { error: "requestId parameter required" });
1708
+ sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId parameter required" });
1517
1709
  return;
1518
1710
  }
1519
1711
  const fetches = registry.get("fetch-store").getByRequest(requestId);
@@ -1530,7 +1722,7 @@ function createActivityHandler(registry) {
1530
1722
  for (const q of queries)
1531
1723
  timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
1532
1724
  timeline.sort((a, b) => a.timestamp - b.timestamp);
1533
- sendJson(req, res, 200, {
1725
+ sendJson(req, res, HTTP_OK, {
1534
1726
  requestId,
1535
1727
  total: timeline.length,
1536
1728
  timeline,
@@ -1542,9 +1734,9 @@ function createActivityHandler(registry) {
1542
1734
  }
1543
1735
  });
1544
1736
  } catch (err) {
1545
- console.error("[brakit] activity handler error:", err);
1737
+ brakitDebug(`activity handler error: ${err}`);
1546
1738
  if (!res.headersSent) {
1547
- sendJson(req, res, 500, { error: "Internal error" });
1739
+ sendJson(req, res, HTTP_INTERNAL_ERROR, { error: "Internal error" });
1548
1740
  }
1549
1741
  }
1550
1742
  };
@@ -1553,6 +1745,8 @@ var init_activity = __esm({
1553
1745
  "src/dashboard/api/activity.ts"() {
1554
1746
  "use strict";
1555
1747
  init_shared2();
1748
+ init_http();
1749
+ init_log();
1556
1750
  }
1557
1751
  });
1558
1752
 
@@ -1572,19 +1766,20 @@ var init_api = __esm({
1572
1766
  function createInsightsHandler(engine) {
1573
1767
  return (req, res) => {
1574
1768
  if (!requireGet(req, res)) return;
1575
- sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
1769
+ sendJson(req, res, HTTP_OK, { insights: engine.getStatefulInsights() });
1576
1770
  };
1577
1771
  }
1578
1772
  function createSecurityHandler(engine) {
1579
1773
  return (req, res) => {
1580
1774
  if (!requireGet(req, res)) return;
1581
- sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
1775
+ sendJson(req, res, HTTP_OK, { findings: engine.getStatefulFindings() });
1582
1776
  };
1583
1777
  }
1584
1778
  var init_insights = __esm({
1585
1779
  "src/dashboard/api/insights.ts"() {
1586
1780
  "use strict";
1587
1781
  init_shared2();
1782
+ init_http();
1588
1783
  }
1589
1784
  });
1590
1785
 
@@ -1592,99 +1787,160 @@ var init_insights = __esm({
1592
1787
  function createFindingsHandler(findingStore) {
1593
1788
  return (req, res) => {
1594
1789
  if (!requireGet(req, res)) return;
1595
- const url = new URL(req.url ?? "/", "http://localhost");
1790
+ const url = parseRequestUrl(req);
1596
1791
  const stateParam = url.searchParams.get("state");
1597
1792
  let findings;
1598
- if (stateParam && VALID_STATES.has(stateParam)) {
1793
+ if (stateParam && isValidFindingState(stateParam)) {
1599
1794
  findings = findingStore.getByState(stateParam);
1600
1795
  } else {
1601
1796
  findings = findingStore.getAll();
1602
1797
  }
1603
- sendJson(req, res, 200, {
1798
+ sendJson(req, res, HTTP_OK, {
1604
1799
  total: findings.length,
1605
1800
  findings
1606
1801
  });
1607
1802
  };
1608
1803
  }
1609
- 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
+ }
1610
1844
  var init_findings = __esm({
1611
1845
  "src/dashboard/api/findings.ts"() {
1612
1846
  "use strict";
1613
1847
  init_shared2();
1614
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
1848
+ init_type_guards();
1849
+ init_http();
1615
1850
  }
1616
1851
  });
1617
1852
 
1618
- // src/core/disposable.ts
1619
- var SubscriptionBag;
1620
- var init_disposable = __esm({
1621
- "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"() {
1622
1857
  "use strict";
1623
- SubscriptionBag = class {
1624
- items = [];
1625
- add(teardown) {
1626
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
1627
- }
1628
- dispose() {
1629
- for (const d of this.items) d.dispose();
1630
- this.items.length = 0;
1631
- }
1632
- };
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";
1633
1864
  }
1634
1865
  });
1635
1866
 
1636
1867
  // src/dashboard/sse.ts
1637
1868
  function createSSEHandler(registry) {
1638
- return (req, res) => {
1639
- res.writeHead(200, {
1640
- "content-type": "text/event-stream",
1641
- "cache-control": "no-cache",
1642
- connection: "keep-alive",
1643
- "access-control-allow-origin": "*"
1644
- });
1645
- res.write(":ok\n\n");
1646
- const writeEvent = (eventType, data) => {
1647
- if (res.destroyed) return;
1648
- if (eventType) {
1649
- res.write(`event: ${eventType}
1869
+ const clients = /* @__PURE__ */ new Set();
1870
+ function broadcast(eventType, data) {
1871
+ if (clients.size === 0) return;
1872
+ const frame = eventType ? `event: ${eventType}
1650
1873
  data: ${data}
1651
1874
 
1652
- `);
1653
- } else {
1654
- res.write(`data: ${data}
1875
+ ` : `data: ${data}
1655
1876
 
1656
- `);
1877
+ `;
1878
+ for (const client of clients) {
1879
+ if (client.res.destroyed) {
1880
+ clients.delete(client);
1881
+ continue;
1657
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"
1658
1908
  };
1659
- const bus = registry.get("event-bus");
1660
- const subs = new SubscriptionBag();
1661
- subs.add(bus.on("request:completed", (r) => writeEvent(null, JSON.stringify(r))));
1662
- subs.add(bus.on("telemetry:fetch", (e) => writeEvent("fetch", JSON.stringify(e))));
1663
- subs.add(bus.on("telemetry:log", (e) => writeEvent("log", JSON.stringify(e))));
1664
- subs.add(bus.on("telemetry:error", (e) => writeEvent("error_event", JSON.stringify(e))));
1665
- subs.add(bus.on("telemetry:query", (e) => writeEvent("query", JSON.stringify(e))));
1666
- subs.add(bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
1667
- writeEvent("insights", JSON.stringify(statefulInsights));
1668
- writeEvent("security", JSON.stringify(statefulFindings));
1669
- }));
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");
1670
1915
  const heartbeat = setInterval(() => {
1671
1916
  if (res.destroyed) {
1672
1917
  clearInterval(heartbeat);
1918
+ clients.delete(client);
1673
1919
  return;
1674
1920
  }
1675
- res.write(":heartbeat\n\n");
1921
+ try {
1922
+ res.write(":heartbeat\n\n");
1923
+ } catch {
1924
+ clearInterval(heartbeat);
1925
+ clients.delete(client);
1926
+ }
1676
1927
  }, SSE_HEARTBEAT_INTERVAL_MS);
1928
+ heartbeat.unref();
1929
+ const client = { res, heartbeat };
1930
+ clients.add(client);
1677
1931
  req.on("close", () => {
1678
1932
  clearInterval(heartbeat);
1679
- subs.dispose();
1933
+ clients.delete(client);
1680
1934
  });
1681
1935
  };
1682
1936
  }
1683
1937
  var init_sse = __esm({
1684
1938
  "src/dashboard/sse.ts"() {
1685
1939
  "use strict";
1686
- init_disposable();
1687
1940
  init_constants();
1941
+ init_http();
1942
+ init_events();
1943
+ init_shared2();
1688
1944
  }
1689
1945
  });
1690
1946
 
@@ -2203,6 +2459,13 @@ function getSecurityStyles() {
2203
2459
  .sec-item-resolved{color:var(--text-muted)}
2204
2460
  .sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
2205
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}
2206
2469
  `;
2207
2470
  }
2208
2471
  var init_security = __esm({
@@ -2266,9 +2529,17 @@ var init_styles = __esm({
2266
2529
  });
2267
2530
 
2268
2531
  // src/utils/fs.ts
2269
- import { access } from "fs/promises";
2532
+ import { access, readFile, writeFile } from "fs/promises";
2270
2533
  import { existsSync, readFileSync, writeFileSync } from "fs";
2271
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
+ }
2272
2543
  function ensureGitignore(dir, entry) {
2273
2544
  try {
2274
2545
  const gitignorePath = resolve(dir, "../.gitignore");
@@ -2282,28 +2553,22 @@ function ensureGitignore(dir, entry) {
2282
2553
  } catch {
2283
2554
  }
2284
2555
  }
2285
- var init_fs = __esm({
2286
- "src/utils/fs.ts"() {
2287
- "use strict";
2288
- }
2289
- });
2290
-
2291
- // src/utils/log.ts
2292
- function brakitWarn(message) {
2293
- process.stderr.write(`${PREFIX} ${message}
2294
- `);
2295
- }
2296
- function brakitDebug(message) {
2297
- if (process.env.DEBUG_BRAKIT) {
2298
- process.stderr.write(`${PREFIX}:debug ${message}
2299
- `);
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 {
2300
2567
  }
2301
2568
  }
2302
- var PREFIX;
2303
- var init_log = __esm({
2304
- "src/utils/log.ts"() {
2569
+ var init_fs = __esm({
2570
+ "src/utils/fs.ts"() {
2305
2571
  "use strict";
2306
- PREFIX = "[brakit]";
2307
2572
  }
2308
2573
  });
2309
2574
 
@@ -2321,6 +2586,7 @@ var init_atomic_writer = __esm({
2321
2586
  "use strict";
2322
2587
  init_fs();
2323
2588
  init_log();
2589
+ init_type_guards();
2324
2590
  AtomicWriter = class {
2325
2591
  constructor(opts) {
2326
2592
  this.opts = opts;
@@ -2335,7 +2601,7 @@ var init_atomic_writer = __esm({
2335
2601
  writeFileSync2(this.tmpPath, content);
2336
2602
  renameSync(this.tmpPath, this.opts.filePath);
2337
2603
  } catch (err) {
2338
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2604
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2339
2605
  }
2340
2606
  }
2341
2607
  async writeAsync(content) {
@@ -2349,13 +2615,14 @@ var init_atomic_writer = __esm({
2349
2615
  await writeFile2(this.tmpPath, content);
2350
2616
  await rename(this.tmpPath, this.opts.filePath);
2351
2617
  } catch (err) {
2352
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2618
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
2353
2619
  } finally {
2354
2620
  this.writing = false;
2355
2621
  if (this.pendingContent !== null) {
2356
2622
  const next = this.pendingContent;
2357
2623
  this.pendingContent = null;
2358
- this.writeAsync(next);
2624
+ this.writeAsync(next).catch(() => {
2625
+ });
2359
2626
  }
2360
2627
  }
2361
2628
  }
@@ -2368,10 +2635,10 @@ var init_atomic_writer = __esm({
2368
2635
  }
2369
2636
  }
2370
2637
  async ensureDirAsync() {
2371
- if (!existsSync2(this.opts.dir)) {
2638
+ if (!await fileExists(this.opts.dir)) {
2372
2639
  await mkdir(this.opts.dir, { recursive: true });
2373
2640
  if (this.opts.gitignoreEntry) {
2374
- ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2641
+ await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
2375
2642
  }
2376
2643
  }
2377
2644
  }
@@ -2383,23 +2650,32 @@ var init_atomic_writer = __esm({
2383
2650
  import { createHash } from "crypto";
2384
2651
  function computeFindingId(finding) {
2385
2652
  const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
2386
- 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);
2387
2658
  }
2388
2659
  var init_finding_id = __esm({
2389
2660
  "src/store/finding-id.ts"() {
2390
2661
  "use strict";
2662
+ init_limits();
2391
2663
  }
2392
2664
  });
2393
2665
 
2394
2666
  // src/store/finding-store.ts
2667
+ import { readFile as readFile2 } from "fs/promises";
2395
2668
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
2396
2669
  import { resolve as resolve2 } from "path";
2397
2670
  var FindingStore;
2398
2671
  var init_finding_store = __esm({
2399
2672
  "src/store/finding-store.ts"() {
2400
2673
  "use strict";
2674
+ init_fs();
2401
2675
  init_constants();
2676
+ init_limits();
2402
2677
  init_atomic_writer();
2678
+ init_log();
2403
2679
  init_finding_id();
2404
2680
  FindingStore = class {
2405
2681
  constructor(rootDir) {
@@ -2412,7 +2688,6 @@ var init_finding_store = __esm({
2412
2688
  gitignoreEntry: METRICS_DIR,
2413
2689
  label: "findings"
2414
2690
  });
2415
- this.load();
2416
2691
  }
2417
2692
  findings = /* @__PURE__ */ new Map();
2418
2693
  flushTimer = null;
@@ -2420,6 +2695,8 @@ var init_finding_store = __esm({
2420
2695
  writer;
2421
2696
  findingsPath;
2422
2697
  start() {
2698
+ this.loadAsync().catch(() => {
2699
+ });
2423
2700
  this.flushTimer = setInterval(
2424
2701
  () => this.flush(),
2425
2702
  FINDINGS_FLUSH_INTERVAL_MS
@@ -2456,7 +2733,9 @@ var init_finding_store = __esm({
2456
2733
  firstSeenAt: now,
2457
2734
  lastSeenAt: now,
2458
2735
  resolvedAt: null,
2459
- occurrences: 1
2736
+ occurrences: 1,
2737
+ aiStatus: null,
2738
+ aiNotes: null
2460
2739
  };
2461
2740
  this.findings.set(id, stateful);
2462
2741
  this.dirty = true;
@@ -2472,6 +2751,17 @@ var init_finding_store = __esm({
2472
2751
  this.dirty = true;
2473
2752
  return true;
2474
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
+ }
2475
2765
  /**
2476
2766
  * Reconcile passive findings against the current analysis results.
2477
2767
  *
@@ -2484,7 +2774,7 @@ var init_finding_store = __esm({
2484
2774
  reconcilePassive(currentFindings) {
2485
2775
  const currentIds = new Set(currentFindings.map(computeFindingId));
2486
2776
  for (const [id, stateful] of this.findings) {
2487
- if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
2777
+ if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
2488
2778
  stateful.state = "resolved";
2489
2779
  stateful.resolvedAt = Date.now();
2490
2780
  this.dirty = true;
@@ -2504,18 +2794,35 @@ var init_finding_store = __esm({
2504
2794
  this.findings.clear();
2505
2795
  this.dirty = true;
2506
2796
  }
2507
- 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() {
2508
2814
  try {
2509
2815
  if (existsSync3(this.findingsPath)) {
2510
2816
  const raw = readFileSync2(this.findingsPath, "utf-8");
2511
2817
  const parsed = JSON.parse(raw);
2512
- if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
2818
+ if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
2513
2819
  for (const f of parsed.findings) {
2514
2820
  this.findings.set(f.findingId, f);
2515
2821
  }
2516
2822
  }
2517
2823
  }
2518
- } catch {
2824
+ } catch (err) {
2825
+ brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
2519
2826
  }
2520
2827
  }
2521
2828
  flush() {
@@ -2530,7 +2837,7 @@ var init_finding_store = __esm({
2530
2837
  }
2531
2838
  serialize() {
2532
2839
  const data = {
2533
- version: 1,
2840
+ version: FINDINGS_DATA_VERSION,
2534
2841
  findings: [...this.findings.values()]
2535
2842
  };
2536
2843
  return JSON.stringify(data);
@@ -2540,9 +2847,9 @@ var init_finding_store = __esm({
2540
2847
  });
2541
2848
 
2542
2849
  // src/detect/project.ts
2543
- import { readFile as readFile2 } from "fs/promises";
2850
+ import { readFile as readFile3, readdir } from "fs/promises";
2544
2851
  import { existsSync as existsSync4 } from "fs";
2545
- import { join } from "path";
2852
+ import { join, relative } from "path";
2546
2853
  function detectFrameworkFromDeps(allDeps) {
2547
2854
  for (const f of FRAMEWORKS) {
2548
2855
  if (allDeps[f.dep]) return f.name;
@@ -2579,11 +2886,11 @@ var init_patterns = __esm({
2579
2886
  SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
2580
2887
  TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
2581
2888
  SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
2582
- STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
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+/;
2583
2890
  DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
2584
2891
  SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
2585
- SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
2586
- LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
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;
2587
2894
  MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
2588
2895
  EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
2589
2896
  INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
@@ -2595,9 +2902,9 @@ var init_patterns = __esm({
2595
2902
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
2596
2903
  "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
2597
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.",
2598
2906
  "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
2599
2907
  "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
2600
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
2601
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."
2602
2909
  };
2603
2910
  }
@@ -3021,48 +3328,47 @@ function hasInternalIds(obj) {
3021
3328
  }
3022
3329
  return false;
3023
3330
  }
3024
- function detectPII(method, reqBody, resBody) {
3025
- const target = unwrapResponse(resBody);
3026
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
3027
- const reqEmails = findEmails(reqBody);
3028
- if (reqEmails.length > 0) {
3029
- const resEmails = findEmails(target);
3030
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
3031
- if (echoed.length > 0) {
3032
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
3033
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
3034
- return { reason: "echo", emailCount: echoed.length };
3035
- }
3036
- }
3037
- }
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 };
3038
3341
  }
3039
- if (target && typeof target === "object" && !Array.isArray(target)) {
3040
- const fields = topLevelFieldCount(target);
3041
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
3042
- const emails = findEmails(target);
3043
- if (emails.length > 0) {
3044
- return { reason: "full-record", emailCount: emails.length };
3045
- }
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++;
3046
3359
  }
3047
3360
  }
3048
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
3049
- let itemsWithEmail = 0;
3050
- for (let i = 0; i < Math.min(target.length, 10); i++) {
3051
- const item = target[i];
3052
- if (item && typeof item === "object") {
3053
- const emails = findEmails(item);
3054
- if (emails.length > 0) itemsWithEmail++;
3055
- }
3056
- }
3057
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
3058
- const first = target[0];
3059
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
3060
- return { reason: "list-pii", emailCount: itemsWithEmail };
3061
- }
3062
- }
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 };
3063
3365
  }
3064
3366
  return null;
3065
3367
  }
3368
+ function detectPII(method, reqBody, resBody) {
3369
+ const target = unwrapResponse(resBody);
3370
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
3371
+ }
3066
3372
  var WRITE_METHODS, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, REASON_LABELS, responsePiiLeakRule;
3067
3373
  var init_response_pii_leak = __esm({
3068
3374
  "src/analysis/rules/response-pii-leak.ts"() {
@@ -3180,6 +3486,24 @@ var init_rules = __esm({
3180
3486
  }
3181
3487
  });
3182
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
+
3183
3507
  // src/utils/collections.ts
3184
3508
  function groupBy(items, keyFn) {
3185
3509
  const map = /* @__PURE__ */ new Map();
@@ -4021,14 +4345,19 @@ function computeInsightKey(insight) {
4021
4345
  const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
4022
4346
  return `${insight.type}:${identifier}`;
4023
4347
  }
4348
+ function enrichedIdFromInsight(insight) {
4349
+ return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
4350
+ }
4024
4351
  var InsightTracker;
4025
4352
  var init_insight_tracker = __esm({
4026
4353
  "src/analysis/insight-tracker.ts"() {
4027
4354
  "use strict";
4028
4355
  init_endpoint();
4356
+ init_finding_id();
4029
4357
  init_thresholds();
4030
4358
  InsightTracker = class {
4031
4359
  tracked = /* @__PURE__ */ new Map();
4360
+ enrichedIndex = /* @__PURE__ */ new Map();
4032
4361
  reconcile(current) {
4033
4362
  const currentKeys = /* @__PURE__ */ new Set();
4034
4363
  const now = Date.now();
@@ -4036,6 +4365,7 @@ var init_insight_tracker = __esm({
4036
4365
  const key = computeInsightKey(insight);
4037
4366
  currentKeys.add(key);
4038
4367
  const existing = this.tracked.get(key);
4368
+ this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
4039
4369
  if (existing) {
4040
4370
  existing.insight = insight;
4041
4371
  existing.lastSeenAt = now;
@@ -4052,28 +4382,44 @@ var init_insight_tracker = __esm({
4052
4382
  firstSeenAt: now,
4053
4383
  lastSeenAt: now,
4054
4384
  resolvedAt: null,
4055
- consecutiveAbsences: 0
4385
+ consecutiveAbsences: 0,
4386
+ aiStatus: null,
4387
+ aiNotes: null
4056
4388
  });
4057
4389
  }
4058
4390
  }
4059
- for (const [key, stateful] of this.tracked) {
4060
- 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)) {
4061
4393
  stateful.consecutiveAbsences++;
4062
4394
  if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
4063
4395
  stateful.state = "resolved";
4064
4396
  stateful.resolvedAt = now;
4065
4397
  }
4066
4398
  } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
4067
- this.tracked.delete(key);
4399
+ this.tracked.delete(stateful.key);
4400
+ this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
4068
4401
  }
4069
4402
  }
4070
4403
  return [...this.tracked.values()];
4071
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
+ }
4072
4417
  getAll() {
4073
4418
  return [...this.tracked.values()];
4074
4419
  }
4075
4420
  clear() {
4076
4421
  this.tracked.clear();
4422
+ this.enrichedIndex.clear();
4077
4423
  }
4078
4424
  };
4079
4425
  }
@@ -4084,13 +4430,14 @@ var AnalysisEngine;
4084
4430
  var init_engine = __esm({
4085
4431
  "src/analysis/engine.ts"() {
4086
4432
  "use strict";
4433
+ init_limits();
4087
4434
  init_disposable();
4088
4435
  init_group();
4089
4436
  init_rules();
4090
4437
  init_insights3();
4091
4438
  init_insight_tracker();
4092
4439
  AnalysisEngine = class {
4093
- constructor(registry, debounceMs = 300) {
4440
+ constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4094
4441
  this.registry = registry;
4095
4442
  this.debounceMs = debounceMs;
4096
4443
  this.scanner = createDefaultScanner();
@@ -4126,7 +4473,10 @@ var init_engine = __esm({
4126
4473
  return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
4127
4474
  }
4128
4475
  getStatefulInsights() {
4129
- return this.cachedStatefulInsights;
4476
+ return this.insightTracker.getAll();
4477
+ }
4478
+ reportInsightFix(enrichedId, status, notes) {
4479
+ return this.insightTracker.reportFix(enrichedId, status, notes);
4130
4480
  }
4131
4481
  scheduleRecompute() {
4132
4482
  if (this.debounceTimer) return;
@@ -4184,7 +4534,7 @@ var init_src = __esm({
4184
4534
  init_engine();
4185
4535
  init_insights3();
4186
4536
  init_insights2();
4187
- VERSION = "0.8.4";
4537
+ VERSION = "0.8.5";
4188
4538
  }
4189
4539
  });
4190
4540
 
@@ -4873,7 +5223,7 @@ function getFlowDetail() {
4873
5223
  h += '<span>' + req.durationMs + 'ms</span>';
4874
5224
  if (req.responseSize) h += '<span>' + formatSize(req.responseSize) + '</span>';
4875
5225
  h += '</div>';
4876
- 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>';
4877
5227
  h += '<div class="detail-grid">';
4878
5228
  h += '<div class="detail-section"><h4>Request Headers</h4><pre>' + formatHeaders(req.headers) + '</pre></div>';
4879
5229
  h += '<div class="detail-section"><h4>Response Headers</h4><pre>' + formatHeaders(req.responseHeaders) + '</pre></div>';
@@ -6131,7 +6481,7 @@ function getOverviewRender() {
6131
6481
  container.appendChild(summary);
6132
6482
 
6133
6483
  var all = state.insights || [];
6134
- var open = all.filter(function(si) { return si.state === 'open'; });
6484
+ var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing'; });
6135
6485
  var resolved = all.filter(function(si) { return si.state === 'resolved'; });
6136
6486
 
6137
6487
  if (open.length === 0 && resolved.length === 0) {
@@ -6176,10 +6526,17 @@ function getOverviewRender() {
6176
6526
  if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6177
6527
  expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6178
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
+
6179
6536
  card.innerHTML =
6180
6537
  '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6181
6538
  '<div class="ov-card-body">' +
6182
- '<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
6539
+ '<div class="ov-card-title">' + escHtml(insight.title) + aiBadge + '</div>' +
6183
6540
  '<div class="ov-card-desc">' + insight.desc + '</div>' +
6184
6541
  '<div class="ov-card-expand">' + expandHtml + '</div>' +
6185
6542
  '</div>' +
@@ -6271,7 +6628,26 @@ function getSecurityView() {
6271
6628
  container.innerHTML = '';
6272
6629
  var SEV = ${SEVERITY_MAP};
6273
6630
 
6274
- 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
+ }
6275
6651
  var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
6276
6652
  var resolved = all.filter(function(f) { return f.state === 'resolved'; });
6277
6653
 
@@ -6319,12 +6695,13 @@ function getSecurityView() {
6319
6695
  var groups = {};
6320
6696
  var groupOrder = [];
6321
6697
  for (var gi = 0; gi < open.length; gi++) {
6322
- var f = open[gi].finding;
6698
+ var sf = open[gi];
6699
+ var f = sf.finding;
6323
6700
  if (!groups[f.rule]) {
6324
6701
  groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6325
6702
  groupOrder.push(f.rule);
6326
6703
  }
6327
- groups[f.rule].items.push(f);
6704
+ groups[f.rule].items.push(sf);
6328
6705
  }
6329
6706
 
6330
6707
  groupOrder.sort(function(a, b) {
@@ -6361,12 +6738,21 @@ function getSecurityView() {
6361
6738
  var list = document.createElement('div');
6362
6739
  list.className = 'sec-items';
6363
6740
  for (var ii = 0; ii < group.items.length; ii++) {
6364
- var item = group.items[ii];
6741
+ var sf2 = group.items[ii];
6742
+ var item = sf2.finding;
6365
6743
  var row = document.createElement('div');
6366
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>' : '';
6367
6752
  row.innerHTML =
6368
6753
  '<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
6369
- (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;
6370
6756
  list.appendChild(row);
6371
6757
  }
6372
6758
  section.appendChild(list);
@@ -6385,12 +6771,16 @@ function getSecurityView() {
6385
6771
  var resolvedItems = document.createElement('div');
6386
6772
  resolvedItems.className = 'sec-items';
6387
6773
  for (var ri = 0; ri < resolved.length; ri++) {
6388
- var rf = resolved[ri].finding;
6774
+ var rsf = resolved[ri];
6775
+ var rf = rsf.finding;
6389
6776
  var rRow = document.createElement('div');
6390
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>' : '';
6391
6780
  rRow.innerHTML =
6392
6781
  '<span class="sec-resolved-item-icon">\\u2713</span>' +
6393
- '<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;
6394
6784
  resolvedItems.appendChild(rRow);
6395
6785
  }
6396
6786
  resolvedGroup.appendChild(resolvedItems);
@@ -6481,6 +6871,7 @@ function getApp() {
6481
6871
  events.addEventListener('insights', function(e) {
6482
6872
  state.insights = JSON.parse(e.data);
6483
6873
  if (state.activeView === 'overview') renderOverview();
6874
+ if (state.activeView === 'security') renderSecurity();
6484
6875
  updateStats();
6485
6876
  });
6486
6877
 
@@ -6684,7 +7075,7 @@ var init_page = __esm({
6684
7075
  import { homedir } from "os";
6685
7076
  import { join as join2 } from "path";
6686
7077
  import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6687
- import { randomUUID as randomUUID3 } from "crypto";
7078
+ import { randomUUID as randomUUID4 } from "crypto";
6688
7079
  function readConfig() {
6689
7080
  try {
6690
7081
  if (!existsSync5(CONFIG_PATH)) return null;
@@ -6708,27 +7099,33 @@ function getOrCreateConfig() {
6708
7099
  if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
6709
7100
  return existing;
6710
7101
  }
6711
- const config = { telemetry: true, anonymousId: randomUUID3() };
7102
+ const config = { telemetry: true, anonymousId: randomUUID4() };
6712
7103
  writeConfig(config);
6713
7104
  return config;
6714
7105
  }
6715
7106
  function isTelemetryEnabled() {
7107
+ if (cachedEnabled !== null) return cachedEnabled;
6716
7108
  const env = process.env.BRAKIT_TELEMETRY;
6717
- if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
6718
- 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;
6719
7115
  }
6720
- var CONFIG_DIR, CONFIG_PATH;
7116
+ var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
6721
7117
  var init_config = __esm({
6722
7118
  "src/telemetry/config.ts"() {
6723
7119
  "use strict";
6724
7120
  CONFIG_DIR = join2(homedir(), ".brakit");
6725
7121
  CONFIG_PATH = join2(CONFIG_DIR, "config.json");
7122
+ cachedEnabled = null;
6726
7123
  }
6727
7124
  });
6728
7125
 
6729
7126
  // src/telemetry/index.ts
6730
7127
  import { platform, release, arch } from "os";
6731
- import { spawnSync } from "child_process";
7128
+ import { spawn } from "child_process";
6732
7129
  function initSession(framework, packageManager, isCustomCommand, adapters) {
6733
7130
  session.startTime = Date.now();
6734
7131
  session.framework = framework;
@@ -6815,14 +7212,15 @@ function trackSession(registry) {
6815
7212
  try {
6816
7213
  const body = JSON.stringify(payload);
6817
7214
  const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
6818
- spawnSync(
7215
+ const child = spawn(
6819
7216
  process.execPath,
6820
7217
  [
6821
7218
  "-e",
6822
7219
  `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
6823
7220
  ],
6824
- { timeout: POSTHOG_SPAWN_TIMEOUT_MS, stdio: "ignore" }
7221
+ { detached: true, stdio: "ignore" }
6825
7222
  );
7223
+ child.unref();
6826
7224
  } catch {
6827
7225
  }
6828
7226
  }
@@ -6877,7 +7275,13 @@ function createDashboardHandler(registry) {
6877
7275
  routes[DASHBOARD_API_SECURITY] = createSecurityHandler(analysisEngine);
6878
7276
  }
6879
7277
  if (registry.has("finding-store")) {
6880
- 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
+ );
6881
7285
  }
6882
7286
  routes[DASHBOARD_API_TAB] = (req, res) => {
6883
7287
  const raw = (req.url ?? "").split("tab=")[1];
@@ -6885,7 +7289,7 @@ function createDashboardHandler(registry) {
6885
7289
  const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
6886
7290
  if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
6887
7291
  }
6888
- res.writeHead(204);
7292
+ res.writeHead(HTTP_NO_CONTENT);
6889
7293
  res.end();
6890
7294
  };
6891
7295
  return (req, res, config) => {
@@ -6896,7 +7300,7 @@ function createDashboardHandler(registry) {
6896
7300
  return;
6897
7301
  }
6898
7302
  if (isTelemetryEnabled()) recordDashboardOpened();
6899
- res.writeHead(200, {
7303
+ res.writeHead(HTTP_OK, {
6900
7304
  "content-type": "text/html; charset=utf-8",
6901
7305
  "cache-control": "no-cache",
6902
7306
  ...SECURITY_HEADERS
@@ -6904,23 +7308,17 @@ function createDashboardHandler(registry) {
6904
7308
  res.end(getDashboardHtml(config));
6905
7309
  };
6906
7310
  }
6907
- var SECURITY_HEADERS;
6908
7311
  var init_router = __esm({
6909
7312
  "src/dashboard/router.ts"() {
6910
7313
  "use strict";
6911
7314
  init_constants();
7315
+ init_http();
6912
7316
  init_api();
6913
7317
  init_insights();
6914
7318
  init_findings();
6915
7319
  init_sse();
6916
7320
  init_page();
6917
7321
  init_telemetry2();
6918
- SECURITY_HEADERS = {
6919
- "x-content-type-options": "nosniff",
6920
- "x-frame-options": "DENY",
6921
- "referrer-policy": "no-referrer",
6922
- "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
6923
- };
6924
7322
  }
6925
7323
  });
6926
7324
 
@@ -6929,6 +7327,7 @@ var EventBus;
6929
7327
  var init_event_bus = __esm({
6930
7328
  "src/core/event-bus.ts"() {
6931
7329
  "use strict";
7330
+ init_log();
6932
7331
  EventBus = class {
6933
7332
  listeners = /* @__PURE__ */ new Map();
6934
7333
  emit(channel, data) {
@@ -6937,7 +7336,8 @@ var init_event_bus = __esm({
6937
7336
  for (const fn of set) {
6938
7337
  try {
6939
7338
  fn(data);
6940
- } catch {
7339
+ } catch (err) {
7340
+ brakitDebug(`EventBus listener threw on channel "${channel}": ${err}`);
6941
7341
  }
6942
7342
  }
6943
7343
  }
@@ -7001,9 +7401,9 @@ var init_static_patterns = __esm({
7001
7401
  });
7002
7402
 
7003
7403
  // src/store/request-store.ts
7004
- function flattenHeaders(headers) {
7404
+ function flattenHeaders(headers2) {
7005
7405
  const flat = {};
7006
- for (const [key, value] of Object.entries(headers)) {
7406
+ for (const [key, value] of Object.entries(headers2)) {
7007
7407
  if (value === void 0) continue;
7008
7408
  flat[key] = Array.isArray(value) ? value.join(", ") : value;
7009
7409
  }
@@ -7059,6 +7459,15 @@ var init_request_store = __esm({
7059
7459
  }
7060
7460
  return entry;
7061
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
+ }
7062
7471
  getAll() {
7063
7472
  return this.requests;
7064
7473
  }
@@ -7077,7 +7486,7 @@ var init_request_store = __esm({
7077
7486
  });
7078
7487
 
7079
7488
  // src/store/telemetry-store.ts
7080
- import { randomUUID as randomUUID4 } from "crypto";
7489
+ import { randomUUID as randomUUID5 } from "crypto";
7081
7490
  var TelemetryStore;
7082
7491
  var init_telemetry_store = __esm({
7083
7492
  "src/store/telemetry-store.ts"() {
@@ -7090,7 +7499,7 @@ var init_telemetry_store = __esm({
7090
7499
  entries = [];
7091
7500
  listeners = [];
7092
7501
  add(data) {
7093
- const entry = { id: randomUUID4(), ...data };
7502
+ const entry = { id: randomUUID5(), ...data };
7094
7503
  this.entries.push(entry);
7095
7504
  if (this.entries.length > this.maxEntries) this.entries.shift();
7096
7505
  for (const fn of this.listeners) fn(entry);
@@ -7174,7 +7583,7 @@ var init_math = __esm({
7174
7583
  });
7175
7584
 
7176
7585
  // src/store/metrics/metrics-store.ts
7177
- import { randomUUID as randomUUID5 } from "crypto";
7586
+ import { randomUUID as randomUUID6 } from "crypto";
7178
7587
  function createAccumulator() {
7179
7588
  return {
7180
7589
  durations: [],
@@ -7198,19 +7607,23 @@ var init_metrics_store = __esm({
7198
7607
  MetricsStore = class {
7199
7608
  constructor(persistence) {
7200
7609
  this.persistence = persistence;
7201
- this.data = persistence.load();
7202
- for (const ep of this.data.endpoints) {
7203
- this.endpointIndex.set(ep.endpoint, ep);
7204
- }
7610
+ this.data = { version: 1, endpoints: [] };
7205
7611
  }
7206
7612
  data;
7207
7613
  endpointIndex = /* @__PURE__ */ new Map();
7208
- sessionId = randomUUID5();
7614
+ sessionId = randomUUID6();
7209
7615
  sessionStart = Date.now();
7210
7616
  flushTimer = null;
7211
7617
  accumulators = /* @__PURE__ */ new Map();
7212
7618
  pendingPoints = /* @__PURE__ */ new Map();
7213
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
+ });
7214
7627
  this.flushTimer = setInterval(
7215
7628
  () => this.flush(),
7216
7629
  METRICS_FLUSH_INTERVAL_MS
@@ -7370,6 +7783,7 @@ var init_metrics_store = __esm({
7370
7783
  });
7371
7784
 
7372
7785
  // src/store/metrics/persistence.ts
7786
+ import { readFile as readFile4 } from "fs/promises";
7373
7787
  import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync } from "fs";
7374
7788
  import { resolve as resolve3 } from "path";
7375
7789
  var FileMetricsPersistence;
@@ -7378,7 +7792,9 @@ var init_persistence = __esm({
7378
7792
  "use strict";
7379
7793
  init_constants();
7380
7794
  init_atomic_writer();
7795
+ init_fs();
7381
7796
  init_log();
7797
+ init_type_guards();
7382
7798
  FileMetricsPersistence = class {
7383
7799
  metricsPath;
7384
7800
  writer;
@@ -7401,7 +7817,21 @@ var init_persistence = __esm({
7401
7817
  }
7402
7818
  }
7403
7819
  } catch (err) {
7404
- 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)}`);
7405
7835
  }
7406
7836
  return { version: 1, endpoints: [] };
7407
7837
  }
@@ -7540,17 +7970,28 @@ var init_health2 = __esm({
7540
7970
  BrakitHealth = class {
7541
7971
  errorCount = 0;
7542
7972
  disabled = false;
7973
+ disabledAt = 0;
7543
7974
  teardownFn = null;
7544
7975
  reportError() {
7545
7976
  this.errorCount++;
7546
7977
  if (this.errorCount >= MAX_HEALTH_ERRORS && !this.disabled) {
7547
7978
  this.disabled = true;
7548
- 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
+ }
7549
7984
  this.teardownFn?.();
7550
7985
  }
7551
7986
  }
7552
7987
  isActive() {
7553
- 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;
7554
7995
  }
7555
7996
  setTeardown(fn) {
7556
7997
  this.teardownFn = fn;
@@ -7591,10 +8032,10 @@ var init_guard = __esm({
7591
8032
  });
7592
8033
 
7593
8034
  // src/runtime/capture.ts
7594
- import { gunzipSync, brotliDecompressSync, inflateSync } from "zlib";
7595
- function outgoingToIncoming(headers) {
8035
+ import { gunzip, brotliDecompress, inflate } from "zlib";
8036
+ function outgoingToIncoming(headers2) {
7596
8037
  const result = {};
7597
- for (const [key, value] of Object.entries(headers)) {
8038
+ for (const [key, value] of Object.entries(headers2)) {
7598
8039
  if (value === void 0) continue;
7599
8040
  if (Array.isArray(value)) {
7600
8041
  result[key] = value.map(String);
@@ -7604,15 +8045,19 @@ function outgoingToIncoming(headers) {
7604
8045
  }
7605
8046
  return result;
7606
8047
  }
7607
- function decompress(body, encoding) {
7608
- try {
7609
- if (encoding === CONTENT_ENCODING_GZIP) return gunzipSync(body);
7610
- if (encoding === CONTENT_ENCODING_BR) return brotliDecompressSync(body);
7611
- if (encoding === CONTENT_ENCODING_DEFLATE) return inflateSync(body);
7612
- } catch (e) {
7613
- brakitDebug(`decompress failed: ${e.message}`);
7614
- }
7615
- return body;
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
+ });
7616
8061
  }
7617
8062
  function toBuffer(chunk) {
7618
8063
  if (Buffer.isBuffer(chunk)) return chunk;
@@ -7656,29 +8101,35 @@ function captureInProcess(req, res, requestId, requestStore) {
7656
8101
  }
7657
8102
  const result = originalEnd.apply(this, args);
7658
8103
  const endTime = performance.now();
7659
- try {
7660
- const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
7661
- let body = resChunks.length > 0 ? Buffer.concat(resChunks) : null;
7662
- if (body && encoding) {
7663
- body = decompress(body, encoding);
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}`);
7664
8131
  }
7665
- requestStore.capture({
7666
- requestId,
7667
- method,
7668
- url: req.url ?? "/",
7669
- requestHeaders: req.headers,
7670
- requestBody: null,
7671
- statusCode: res.statusCode,
7672
- responseHeaders: outgoingToIncoming(res.getHeaders()),
7673
- responseBody: body,
7674
- responseContentType: String(res.getHeader("content-type") ?? ""),
7675
- startTime,
7676
- endTime,
7677
- config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
7678
- });
7679
- } catch (e) {
7680
- brakitDebug(`capture store: ${e.message}`);
7681
- }
8132
+ })();
7682
8133
  return result;
7683
8134
  };
7684
8135
  }
@@ -7692,7 +8143,7 @@ var init_capture = __esm({
7692
8143
 
7693
8144
  // src/runtime/interceptor.ts
7694
8145
  import http from "http";
7695
- import { randomUUID as randomUUID6 } from "crypto";
8146
+ import { randomUUID as randomUUID7 } from "crypto";
7696
8147
  function installInterceptor(deps) {
7697
8148
  originalEmit = http.Server.prototype.emit;
7698
8149
  const saved = originalEmit;
@@ -7718,14 +8169,14 @@ function installInterceptor(deps) {
7718
8169
  }
7719
8170
  if (isDashboardRequest(url)) {
7720
8171
  if (!isLocalRequest(req)) {
7721
- res.writeHead(404);
8172
+ res.writeHead(HTTP_NOT_FOUND);
7722
8173
  res.end("Not Found");
7723
8174
  return true;
7724
8175
  }
7725
8176
  deps.handleDashboard(req, res, deps.config);
7726
8177
  return true;
7727
8178
  }
7728
- const requestId = randomUUID6();
8179
+ const requestId = randomUUID7();
7729
8180
  const ctx = {
7730
8181
  requestId,
7731
8182
  url,
@@ -7754,6 +8205,7 @@ var init_interceptor = __esm({
7754
8205
  init_safe_wrap();
7755
8206
  init_guard();
7756
8207
  init_capture();
8208
+ init_http();
7757
8209
  originalEmit = null;
7758
8210
  }
7759
8211
  });
@@ -7763,11 +8215,15 @@ var setup_exports = {};
7763
8215
  __export(setup_exports, {
7764
8216
  setup: () => setup
7765
8217
  });
7766
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync7, 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";
7767
8220
  import { resolve as resolve4 } from "path";
7768
8221
  function setup() {
7769
- if (initialized) return;
7770
- initialized = true;
8222
+ if (initPromise) return initPromise;
8223
+ initPromise = doSetup();
8224
+ return initPromise;
8225
+ }
8226
+ async function doSetup() {
7771
8227
  const bus = new EventBus();
7772
8228
  const registry = new ServiceRegistry();
7773
8229
  const requestStore = new RequestStore();
@@ -7798,7 +8254,7 @@ function setup() {
7798
8254
  const cwd = process.cwd();
7799
8255
  let framework = "unknown";
7800
8256
  try {
7801
- const pkg = JSON.parse(readFileSync5(resolve4(cwd, "package.json"), "utf-8"));
8257
+ const pkg = JSON.parse(await readFile5(resolve4(cwd, "package.json"), "utf-8"));
7802
8258
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7803
8259
  framework = detectFrameworkFromDeps(allDeps);
7804
8260
  } catch {
@@ -7841,29 +8297,42 @@ function setup() {
7841
8297
  requestStore,
7842
8298
  onFirstRequest(port) {
7843
8299
  setBrakitPort(port);
7844
- const dir = resolve4(cwd, METRICS_DIR);
7845
- if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
7846
- const portPath = resolve4(cwd, PORT_FILE);
7847
- if (existsSync7(portPath)) {
7848
- const old = readFileSync5(portPath, "utf-8").trim();
7849
- if (old && old !== String(port)) {
7850
- brakitDebug(`Overwriting stale port file (was ${old}, now ${port})`);
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}`);
7851
8315
  }
7852
- }
7853
- writeFileSync4(portPath, String(port));
8316
+ })();
7854
8317
  terminalDispose = startTerminalInsights(registry, port);
7855
8318
  process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
7856
8319
  `);
7857
8320
  }
7858
8321
  });
7859
- let teardownCalled = false;
7860
- const runTeardown = () => {
7861
- if (teardownCalled) return;
7862
- teardownCalled = true;
8322
+ let telemetrySent = false;
8323
+ const sendTelemetry = () => {
8324
+ if (telemetrySent) return;
8325
+ telemetrySent = true;
7863
8326
  recordRequestCount(requestStore.getAll().length);
7864
8327
  recordInsightTypes(analysisEngine.getInsights().map((i) => i.type));
7865
8328
  recordRulesTriggered(analysisEngine.getFindings().map((f) => f.rule));
7866
8329
  trackSession(registry);
8330
+ };
8331
+ let teardownCalled = false;
8332
+ const runTeardown = () => {
8333
+ if (teardownCalled) return;
8334
+ teardownCalled = true;
8335
+ sendTelemetry();
7867
8336
  uninstallInterceptor();
7868
8337
  terminalDispose?.();
7869
8338
  analysisEngine.stop();
@@ -7876,16 +8345,14 @@ function setup() {
7876
8345
  }
7877
8346
  };
7878
8347
  health.setTeardown(runTeardown);
7879
- process.once("SIGINT", () => {
7880
- runTeardown();
7881
- process.exit(SIGNAL_EXIT_SIGINT);
8348
+ process.on("beforeExit", () => {
8349
+ sendTelemetry();
7882
8350
  });
7883
- process.once("SIGTERM", () => {
8351
+ process.on("exit", () => {
7884
8352
  runTeardown();
7885
- process.exit(SIGNAL_EXIT_SIGTERM);
7886
8353
  });
7887
8354
  }
7888
- var initialized;
8355
+ var initPromise;
7889
8356
  var init_setup = __esm({
7890
8357
  "src/runtime/setup.ts"() {
7891
8358
  "use strict";
@@ -7907,13 +8374,12 @@ var init_setup = __esm({
7907
8374
  init_terminal();
7908
8375
  init_src();
7909
8376
  init_constants();
7910
- init_telemetry();
7911
8377
  init_health2();
7912
8378
  init_interceptor();
7913
8379
  init_log();
7914
8380
  init_project();
7915
8381
  init_telemetry2();
7916
- initialized = false;
8382
+ initPromise = null;
7917
8383
  }
7918
8384
  });
7919
8385
 
@@ -7932,7 +8398,7 @@ function shouldActivate() {
7932
8398
  if (shouldActivate()) {
7933
8399
  try {
7934
8400
  const { setup: setup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
7935
- setup2();
8401
+ await setup2();
7936
8402
  } catch (err) {
7937
8403
  console.warn("brakit: failed to start \u2014", err?.message);
7938
8404
  }