brakit 0.8.1 → 0.8.2

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,7 +9,7 @@ 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;
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;
13
13
  var init_routes = __esm({
14
14
  "src/constants/routes.ts"() {
15
15
  "use strict";
@@ -30,6 +30,17 @@ var init_routes = __esm({
30
30
  DASHBOARD_API_SECURITY = "/__brakit/api/security";
31
31
  DASHBOARD_API_TAB = "/__brakit/api/tab";
32
32
  DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
33
+ VALID_TABS = /* @__PURE__ */ new Set([
34
+ "overview",
35
+ "actions",
36
+ "requests",
37
+ "fetches",
38
+ "queries",
39
+ "errors",
40
+ "logs",
41
+ "performance",
42
+ "security"
43
+ ]);
33
44
  }
34
45
  });
35
46
 
@@ -88,14 +99,20 @@ var init_thresholds = __esm({
88
99
  });
89
100
 
90
101
  // src/constants/transport.ts
91
- var SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS;
102
+ var SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS;
92
103
  var init_transport = __esm({
93
104
  "src/constants/transport.ts"() {
94
105
  "use strict";
95
106
  SSE_HEARTBEAT_INTERVAL_MS = 3e4;
96
107
  NOISE_HOSTS = [
97
108
  "registry.npmjs.org",
98
- "telemetry.nextjs.org"
109
+ "telemetry.nextjs.org",
110
+ "vitejs.dev"
111
+ ];
112
+ NOISE_PATH_PATTERNS = [
113
+ ".hot-update.",
114
+ "__webpack",
115
+ "__vite"
99
116
  ];
100
117
  }
101
118
  });
@@ -175,6 +192,38 @@ var init_mcp = __esm({
175
192
  }
176
193
  });
177
194
 
195
+ // src/constants/encoding.ts
196
+ var CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE;
197
+ var init_encoding = __esm({
198
+ "src/constants/encoding.ts"() {
199
+ "use strict";
200
+ CONTENT_ENCODING_GZIP = "gzip";
201
+ CONTENT_ENCODING_BR = "br";
202
+ CONTENT_ENCODING_DEFLATE = "deflate";
203
+ }
204
+ });
205
+
206
+ // src/constants/severity.ts
207
+ var SEVERITY_ICON, SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
208
+ var init_severity = __esm({
209
+ "src/constants/severity.ts"() {
210
+ "use strict";
211
+ SEVERITY_ICON = {
212
+ critical: "\u2717",
213
+ warning: "\u26A0",
214
+ info: "\u2139"
215
+ };
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
+ }
225
+ });
226
+
178
227
  // src/constants/index.ts
179
228
  var init_constants = __esm({
180
229
  "src/constants/index.ts"() {
@@ -187,21 +236,8 @@ var init_constants = __esm({
187
236
  init_headers();
188
237
  init_network();
189
238
  init_mcp();
190
- }
191
- });
192
-
193
- // src/instrument/transport.ts
194
- function setEmitter(fn) {
195
- emitter = fn;
196
- }
197
- function send(event) {
198
- emitter?.(event);
199
- }
200
- var emitter;
201
- var init_transport2 = __esm({
202
- "src/instrument/transport.ts"() {
203
- "use strict";
204
- emitter = null;
239
+ init_encoding();
240
+ init_severity();
205
241
  }
206
242
  });
207
243
 
@@ -221,7 +257,11 @@ var init_context = __esm({
221
257
 
222
258
  // src/instrument/hooks/fetch.ts
223
259
  import { subscribe } from "diagnostics_channel";
224
- function isNoise(origin) {
260
+ function setBrakitPort(port) {
261
+ brakitPort = port;
262
+ }
263
+ function isNoise(origin, path) {
264
+ if (NOISE_PATH_PATTERNS.some((p) => path.includes(p))) return true;
225
265
  try {
226
266
  const host = new URL(origin).hostname;
227
267
  return NOISE_HOSTS.some((h) => host === h || host.endsWith("." + h));
@@ -229,19 +269,22 @@ function isNoise(origin) {
229
269
  return false;
230
270
  }
231
271
  }
232
- function setupFetchHook() {
272
+ function setupFetchHook(emit) {
233
273
  subscribe("undici:request:create", (message) => {
234
274
  const msg = message;
235
275
  const req = msg.request;
236
276
  const origin = req.origin ?? "";
237
- if (isNoise(origin)) return;
277
+ const path = req.path ?? "/";
278
+ if (isNoise(origin, path)) return;
279
+ if (brakitPort && origin.includes(`localhost:${brakitPort}`)) return;
238
280
  const ctx = getRequestContext();
281
+ if (!ctx) return;
239
282
  pending.set(msg.request, {
240
283
  origin,
241
284
  method: req.method ?? "GET",
242
- path: req.path ?? "/",
285
+ path,
243
286
  startTime: performance.now(),
244
- parentRequestId: ctx?.requestId ?? null
287
+ parentRequestId: ctx.requestId
245
288
  });
246
289
  });
247
290
  subscribe("undici:request:headers", (message) => {
@@ -249,7 +292,7 @@ function setupFetchHook() {
249
292
  const info = pending.get(msg.request);
250
293
  if (!info) return;
251
294
  pending.delete(msg.request);
252
- send({
295
+ emit({
253
296
  type: "fetch",
254
297
  data: {
255
298
  url: info.origin + info.path,
@@ -266,32 +309,33 @@ function setupFetchHook() {
266
309
  pending.delete(msg.request);
267
310
  });
268
311
  }
269
- var pending;
312
+ var brakitPort, pending;
270
313
  var init_fetch = __esm({
271
314
  "src/instrument/hooks/fetch.ts"() {
272
315
  "use strict";
273
- init_transport2();
274
316
  init_context();
275
317
  init_constants();
318
+ brakitPort = 0;
276
319
  pending = /* @__PURE__ */ new WeakMap();
277
320
  }
278
321
  });
279
322
 
280
323
  // src/instrument/hooks/console.ts
281
324
  import { format } from "util";
282
- function setupConsoleHook() {
325
+ function setupConsoleHook(emit) {
283
326
  for (const level of LEVELS) {
284
327
  const original = originals[level];
285
328
  console[level] = (...args) => {
286
329
  original.apply(console, args);
287
330
  const ctx = getRequestContext();
331
+ if (!ctx) return;
288
332
  const message = format(...args);
289
333
  const timestamp = Date.now();
290
- const parentRequestId = ctx?.requestId ?? null;
334
+ const parentRequestId = ctx.requestId;
291
335
  if (level === "error") {
292
336
  const errorArg = args.find((a) => a instanceof Error);
293
337
  if (errorArg) {
294
- send({
338
+ emit({
295
339
  type: "error",
296
340
  data: {
297
341
  name: errorArg.name,
@@ -305,7 +349,7 @@ function setupConsoleHook() {
305
349
  }
306
350
  const match = message.match(/(\w*Error):\s+(.+)/s);
307
351
  if (match) {
308
- send({
352
+ emit({
309
353
  type: "error",
310
354
  data: {
311
355
  name: match[1],
@@ -318,7 +362,7 @@ function setupConsoleHook() {
318
362
  return;
319
363
  }
320
364
  }
321
- send({
365
+ emit({
322
366
  type: "log",
323
367
  data: { level, message, parentRequestId, timestamp }
324
368
  });
@@ -329,7 +373,6 @@ var LEVELS, originals;
329
373
  var init_console = __esm({
330
374
  "src/instrument/hooks/console.ts"() {
331
375
  "use strict";
332
- init_transport2();
333
376
  init_context();
334
377
  LEVELS = ["log", "warn", "error", "info", "debug"];
335
378
  originals = {
@@ -343,21 +386,24 @@ var init_console = __esm({
343
386
  });
344
387
 
345
388
  // src/instrument/hooks/errors.ts
346
- function captureError(err) {
347
- const error = err instanceof Error ? err : new Error(String(err));
348
- const ctx = getRequestContext();
349
- send({
350
- type: "error",
351
- data: {
352
- name: error.name,
353
- message: error.message,
354
- stack: error.stack ?? "",
355
- parentRequestId: ctx?.requestId ?? null,
356
- timestamp: Date.now()
357
- }
358
- });
389
+ function createCaptureError(emit) {
390
+ return (err) => {
391
+ const error = err instanceof Error ? err : new Error(String(err));
392
+ const ctx = getRequestContext();
393
+ emit({
394
+ type: "error",
395
+ data: {
396
+ name: error.name,
397
+ message: error.message,
398
+ stack: error.stack ?? "",
399
+ parentRequestId: ctx?.requestId ?? null,
400
+ timestamp: Date.now()
401
+ }
402
+ });
403
+ };
359
404
  }
360
- function setupErrorHook() {
405
+ function setupErrorHook(emit) {
406
+ const captureError = createCaptureError(emit);
361
407
  process.on("uncaughtException", (err) => {
362
408
  captureError(err);
363
409
  process.removeAllListeners("uncaughtException");
@@ -370,7 +416,6 @@ function setupErrorHook() {
370
416
  var init_errors = __esm({
371
417
  "src/instrument/hooks/errors.ts"() {
372
418
  "use strict";
373
- init_transport2();
374
419
  init_context();
375
420
  }
376
421
  });
@@ -743,115 +788,6 @@ var init_adapters = __esm({
743
788
  }
744
789
  });
745
790
 
746
- // src/utils/static-patterns.ts
747
- function isStaticPath(urlPath) {
748
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
749
- }
750
- var STATIC_PATTERNS;
751
- var init_static_patterns = __esm({
752
- "src/utils/static-patterns.ts"() {
753
- "use strict";
754
- STATIC_PATTERNS = [
755
- /^\/_next\//,
756
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
757
- /^\/favicon/,
758
- /^\/__nextjs/
759
- ];
760
- }
761
- });
762
-
763
- // src/store/request-store.ts
764
- function flattenHeaders(headers) {
765
- const flat = {};
766
- for (const [key, value] of Object.entries(headers)) {
767
- if (value === void 0) continue;
768
- flat[key] = Array.isArray(value) ? value.join(", ") : value;
769
- }
770
- return flat;
771
- }
772
- var RequestStore;
773
- var init_request_store = __esm({
774
- "src/store/request-store.ts"() {
775
- "use strict";
776
- init_constants();
777
- init_static_patterns();
778
- RequestStore = class {
779
- constructor(maxEntries = MAX_REQUEST_ENTRIES) {
780
- this.maxEntries = maxEntries;
781
- }
782
- requests = [];
783
- listeners = [];
784
- capture(input) {
785
- const url = input.url;
786
- const path = url.split("?")[0];
787
- let requestBodyStr = null;
788
- if (input.requestBody && input.requestBody.length > 0) {
789
- requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
790
- }
791
- let responseBodyStr = null;
792
- if (input.responseBody && input.responseBody.length > 0) {
793
- const ct = input.responseContentType;
794
- if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
795
- responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
796
- }
797
- }
798
- const entry = {
799
- id: input.requestId,
800
- method: input.method,
801
- url,
802
- path,
803
- headers: flattenHeaders(input.requestHeaders),
804
- requestBody: requestBodyStr,
805
- statusCode: input.statusCode,
806
- responseHeaders: flattenHeaders(input.responseHeaders),
807
- responseBody: responseBodyStr,
808
- startedAt: input.startTime,
809
- durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
810
- responseSize: input.responseBody?.length ?? 0,
811
- isStatic: isStaticPath(path)
812
- };
813
- this.requests.push(entry);
814
- if (this.requests.length > this.maxEntries) {
815
- this.requests.shift();
816
- }
817
- for (const fn of this.listeners) {
818
- fn(entry);
819
- }
820
- return entry;
821
- }
822
- getAll() {
823
- return this.requests;
824
- }
825
- clear() {
826
- this.requests.length = 0;
827
- }
828
- onRequest(fn) {
829
- this.listeners.push(fn);
830
- }
831
- offRequest(fn) {
832
- const idx = this.listeners.indexOf(fn);
833
- if (idx !== -1) this.listeners.splice(idx, 1);
834
- }
835
- };
836
- }
837
- });
838
-
839
- // src/store/request-log.ts
840
- var defaultStore, getRequests, clearRequests, onRequest, offRequest;
841
- var init_request_log = __esm({
842
- "src/store/request-log.ts"() {
843
- "use strict";
844
- init_request_store();
845
- init_static_patterns();
846
- init_request_store();
847
- defaultStore = new RequestStore();
848
- getRequests = () => defaultStore.getAll();
849
- clearRequests = () => defaultStore.clear();
850
- onRequest = (fn) => defaultStore.onRequest(fn);
851
- offRequest = (fn) => defaultStore.offRequest(fn);
852
- }
853
- });
854
-
855
791
  // src/analysis/categorize.ts
856
792
  function detectCategory(req) {
857
793
  const { method, url, statusCode, responseHeaders } = req;
@@ -1232,884 +1168,458 @@ var init_group = __esm({
1232
1168
  }
1233
1169
  });
1234
1170
 
1235
- // src/store/telemetry-store.ts
1236
- import { randomUUID as randomUUID3 } from "crypto";
1237
- var TelemetryStore;
1238
- var init_telemetry_store = __esm({
1239
- "src/store/telemetry-store.ts"() {
1240
- "use strict";
1241
- init_constants();
1242
- TelemetryStore = class {
1243
- constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
1244
- this.maxEntries = maxEntries;
1245
- }
1246
- entries = [];
1247
- listeners = [];
1248
- add(data) {
1249
- const entry = { id: randomUUID3(), ...data };
1250
- this.entries.push(entry);
1251
- if (this.entries.length > this.maxEntries) this.entries.shift();
1252
- for (const fn of this.listeners) fn(entry);
1253
- return entry;
1254
- }
1255
- getAll() {
1256
- return this.entries;
1257
- }
1258
- getByRequest(requestId) {
1259
- return this.entries.filter((e) => e.parentRequestId === requestId);
1260
- }
1261
- clear() {
1262
- this.entries.length = 0;
1263
- }
1264
- onEntry(fn) {
1265
- this.listeners.push(fn);
1266
- }
1267
- offEntry(fn) {
1268
- const idx = this.listeners.indexOf(fn);
1269
- if (idx !== -1) this.listeners.splice(idx, 1);
1270
- }
1271
- };
1171
+ // src/dashboard/api/shared.ts
1172
+ function maskSensitiveHeaders(headers) {
1173
+ const masked = {};
1174
+ for (const [key, value] of Object.entries(headers)) {
1175
+ if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1176
+ 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);
1178
+ } else {
1179
+ masked[key] = value;
1180
+ }
1272
1181
  }
1273
- });
1274
-
1275
- // src/store/fetch-store.ts
1276
- var FetchStore, defaultFetchStore;
1277
- var init_fetch_store = __esm({
1278
- "src/store/fetch-store.ts"() {
1279
- "use strict";
1280
- init_telemetry_store();
1281
- FetchStore = class extends TelemetryStore {
1282
- };
1283
- defaultFetchStore = new FetchStore();
1182
+ return masked;
1183
+ }
1184
+ function getCorsOrigin(req) {
1185
+ const origin = req.headers.origin ?? "";
1186
+ try {
1187
+ const url = new URL(origin);
1188
+ if (LOCALHOST_HOSTNAMES.has(url.hostname)) {
1189
+ return origin;
1190
+ }
1191
+ } catch {
1284
1192
  }
1285
- });
1286
-
1287
- // src/store/log-store.ts
1288
- var LogStore, defaultLogStore;
1289
- var init_log_store = __esm({
1290
- "src/store/log-store.ts"() {
1193
+ return "";
1194
+ }
1195
+ function getJsonHeaders(req) {
1196
+ const corsOrigin = getCorsOrigin(req);
1197
+ const headers = {
1198
+ "content-type": "application/json",
1199
+ "cache-control": "no-cache"
1200
+ };
1201
+ if (corsOrigin) {
1202
+ headers["access-control-allow-origin"] = corsOrigin;
1203
+ }
1204
+ return headers;
1205
+ }
1206
+ function sendJson(req, res, status, data) {
1207
+ res.writeHead(status, getJsonHeaders(req));
1208
+ res.end(JSON.stringify(data));
1209
+ }
1210
+ function requireGet(req, res) {
1211
+ if (req.method !== "GET") {
1212
+ sendJson(req, res, 405, { error: "Method not allowed" });
1213
+ return false;
1214
+ }
1215
+ return true;
1216
+ }
1217
+ function handleTelemetryGet(req, res, store) {
1218
+ if (!requireGet(req, res)) return;
1219
+ const url = new URL(req.url ?? "/", "http://localhost");
1220
+ const requestId = url.searchParams.get("requestId");
1221
+ const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1222
+ sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1223
+ }
1224
+ var init_shared2 = __esm({
1225
+ "src/dashboard/api/shared.ts"() {
1291
1226
  "use strict";
1292
- init_telemetry_store();
1293
- LogStore = class extends TelemetryStore {
1294
- };
1295
- defaultLogStore = new LogStore();
1227
+ init_constants();
1228
+ init_limits();
1296
1229
  }
1297
1230
  });
1298
1231
 
1299
- // src/store/error-store.ts
1300
- var ErrorStore, defaultErrorStore;
1301
- var init_error_store = __esm({
1302
- "src/store/error-store.ts"() {
1303
- "use strict";
1304
- init_telemetry_store();
1305
- ErrorStore = class extends TelemetryStore {
1306
- };
1307
- defaultErrorStore = new ErrorStore();
1232
+ // src/dashboard/api/handlers.ts
1233
+ function sanitizeRequest(r) {
1234
+ return {
1235
+ ...r,
1236
+ headers: maskSensitiveHeaders(r.headers),
1237
+ responseHeaders: maskSensitiveHeaders(r.responseHeaders)
1238
+ };
1239
+ }
1240
+ function createRequestsHandler(registry) {
1241
+ return (req, res) => {
1242
+ if (!requireGet(req, res)) return;
1243
+ const url = new URL(req.url ?? "/", "http://localhost");
1244
+ const method = url.searchParams.get("method");
1245
+ const status = url.searchParams.get("status");
1246
+ const search = url.searchParams.get("search");
1247
+ const limit = parseInt(
1248
+ url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT),
1249
+ 10
1250
+ );
1251
+ const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
1252
+ let results = [...registry.get("request-store").getAll()].reverse();
1253
+ if (method) {
1254
+ results = results.filter((r) => r.method === method.toUpperCase());
1255
+ }
1256
+ if (status) {
1257
+ if (status.endsWith("xx")) {
1258
+ const prefix = parseInt(status[0], 10);
1259
+ results = results.filter(
1260
+ (r) => Math.floor(r.statusCode / 100) === prefix
1261
+ );
1262
+ } else {
1263
+ const code = parseInt(status, 10);
1264
+ results = results.filter((r) => r.statusCode === code);
1265
+ }
1266
+ }
1267
+ if (search) {
1268
+ const lower = search.toLowerCase();
1269
+ results = results.filter(
1270
+ (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1271
+ );
1272
+ }
1273
+ const total = results.length;
1274
+ results = results.slice(offset, offset + limit);
1275
+ const sanitized = results.map(sanitizeRequest);
1276
+ sendJson(req, res, 200, { total, requests: sanitized });
1277
+ };
1278
+ }
1279
+ function createFlowsHandler(registry) {
1280
+ return (req, res) => {
1281
+ if (!requireGet(req, res)) return;
1282
+ const flows = groupRequestsIntoFlows(registry.get("request-store").getAll()).reverse().map((flow) => ({
1283
+ ...flow,
1284
+ requests: flow.requests.map(sanitizeRequest)
1285
+ }));
1286
+ sendJson(req, res, 200, { total: flows.length, flows });
1287
+ };
1288
+ }
1289
+ function createClearHandler(registry) {
1290
+ return (req, res) => {
1291
+ if (req.method !== "POST") {
1292
+ sendJson(req, res, 405, { error: "Method not allowed" });
1293
+ return;
1294
+ }
1295
+ registry.get("request-store").clear();
1296
+ registry.get("fetch-store").clear();
1297
+ registry.get("log-store").clear();
1298
+ registry.get("error-store").clear();
1299
+ registry.get("query-store").clear();
1300
+ registry.get("metrics-store").reset();
1301
+ if (registry.has("finding-store")) registry.get("finding-store").clear();
1302
+ registry.get("event-bus").emit("store:cleared", void 0);
1303
+ sendJson(req, res, 200, { cleared: true });
1304
+ };
1305
+ }
1306
+ function createFetchesHandler(registry) {
1307
+ return (req, res) => handleTelemetryGet(req, res, registry.get("fetch-store"));
1308
+ }
1309
+ function createLogsHandler(registry) {
1310
+ return (req, res) => handleTelemetryGet(req, res, registry.get("log-store"));
1311
+ }
1312
+ function createErrorsHandler(registry) {
1313
+ return (req, res) => handleTelemetryGet(req, res, registry.get("error-store"));
1314
+ }
1315
+ function createQueriesHandler(registry) {
1316
+ return (req, res) => handleTelemetryGet(req, res, registry.get("query-store"));
1317
+ }
1318
+ var init_handlers = __esm({
1319
+ "src/dashboard/api/handlers.ts"() {
1320
+ "use strict";
1321
+ init_group();
1322
+ init_constants();
1323
+ init_shared2();
1308
1324
  }
1309
1325
  });
1310
1326
 
1311
- // src/store/query-store.ts
1312
- var QueryStore, defaultQueryStore;
1313
- var init_query_store = __esm({
1314
- "src/store/query-store.ts"() {
1327
+ // src/dashboard/api/ingest.ts
1328
+ function isBrakitBatch(msg) {
1329
+ return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
1330
+ }
1331
+ function isSDKPayload(msg) {
1332
+ return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
1333
+ }
1334
+ function createIngestHandler(registry) {
1335
+ const routeEvent = (event) => {
1336
+ switch (event.type) {
1337
+ case "fetch":
1338
+ registry.get("fetch-store").add(event.data);
1339
+ break;
1340
+ case "log":
1341
+ registry.get("log-store").add(event.data);
1342
+ break;
1343
+ case "error":
1344
+ registry.get("error-store").add(event.data);
1345
+ break;
1346
+ case "query":
1347
+ registry.get("query-store").add(event.data);
1348
+ break;
1349
+ }
1350
+ };
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
+ }
1406
+ };
1407
+ return (req, res) => {
1408
+ if (req.method !== "POST") {
1409
+ sendJson(req, res, 405, { error: "Method not allowed" });
1410
+ return;
1411
+ }
1412
+ const chunks = [];
1413
+ let totalSize = 0;
1414
+ req.on("data", (chunk) => {
1415
+ totalSize += chunk.length;
1416
+ if (totalSize > MAX_INGEST_BYTES) {
1417
+ sendJson(req, res, 413, { error: "Payload too large" });
1418
+ req.destroy();
1419
+ return;
1420
+ }
1421
+ chunks.push(chunk);
1422
+ });
1423
+ req.on("end", () => {
1424
+ if (totalSize > MAX_INGEST_BYTES) return;
1425
+ try {
1426
+ const body = JSON.parse(Buffer.concat(chunks).toString());
1427
+ if (isSDKPayload(body)) {
1428
+ for (const event of body.events) {
1429
+ routeSDKEvent(event);
1430
+ }
1431
+ res.writeHead(204);
1432
+ res.end();
1433
+ return;
1434
+ }
1435
+ if (isBrakitBatch(body)) {
1436
+ for (const event of body.events) {
1437
+ routeEvent(event);
1438
+ }
1439
+ res.writeHead(204);
1440
+ res.end();
1441
+ return;
1442
+ }
1443
+ sendJson(req, res, 400, { error: "Invalid batch" });
1444
+ } catch {
1445
+ sendJson(req, res, 400, { error: "Invalid JSON" });
1446
+ }
1447
+ });
1448
+ };
1449
+ }
1450
+ var init_ingest = __esm({
1451
+ "src/dashboard/api/ingest.ts"() {
1315
1452
  "use strict";
1316
- init_telemetry_store();
1317
- QueryStore = class extends TelemetryStore {
1318
- };
1319
- defaultQueryStore = new QueryStore();
1453
+ init_limits();
1454
+ init_shared2();
1320
1455
  }
1321
1456
  });
1322
1457
 
1323
- // src/utils/math.ts
1324
- function percentile(values, p) {
1325
- if (values.length === 0) return 0;
1326
- const sorted = [...values].sort((a, b) => a - b);
1327
- const idx = Math.ceil(p * sorted.length) - 1;
1328
- return Math.round(sorted[Math.max(0, idx)]);
1458
+ // src/dashboard/api/metrics.ts
1459
+ function createMetricsHandler(metricsStore) {
1460
+ return (req, res) => {
1461
+ if (!requireGet(req, res)) return;
1462
+ const url = new URL(req.url ?? "/", "http://localhost");
1463
+ const endpoint = url.searchParams.get("endpoint");
1464
+ if (endpoint) {
1465
+ const ep = metricsStore.getEndpoint(endpoint);
1466
+ sendJson(req, res, 200, { endpoints: ep ? [ep] : [] });
1467
+ return;
1468
+ }
1469
+ sendJson(req, res, 200, { endpoints: metricsStore.getAll() });
1470
+ };
1329
1471
  }
1330
- var init_math = __esm({
1331
- "src/utils/math.ts"() {
1472
+ var init_metrics2 = __esm({
1473
+ "src/dashboard/api/metrics.ts"() {
1332
1474
  "use strict";
1475
+ init_shared2();
1333
1476
  }
1334
1477
  });
1335
1478
 
1336
- // src/utils/endpoint.ts
1337
- function getEndpointKey(method, path) {
1338
- return `${method} ${path}`;
1479
+ // src/dashboard/api/metrics-live.ts
1480
+ function createLiveMetricsHandler(metricsStore) {
1481
+ return (req, res) => {
1482
+ if (!requireGet(req, res)) return;
1483
+ sendJson(req, res, 200, { endpoints: metricsStore.getLiveEndpoints() });
1484
+ };
1339
1485
  }
1340
- function extractEndpointFromDesc(desc) {
1341
- return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1486
+ var init_metrics_live = __esm({
1487
+ "src/dashboard/api/metrics-live.ts"() {
1488
+ "use strict";
1489
+ init_shared2();
1490
+ }
1491
+ });
1492
+
1493
+ // src/dashboard/api/activity.ts
1494
+ function createActivityHandler(registry) {
1495
+ return (req, res) => {
1496
+ if (!requireGet(req, res)) return;
1497
+ try {
1498
+ const url = new URL(req.url ?? "/", "http://localhost");
1499
+ const requestId = url.searchParams.get("requestId");
1500
+ if (!requestId) {
1501
+ sendJson(req, res, 400, { error: "requestId parameter required" });
1502
+ return;
1503
+ }
1504
+ const fetches = registry.get("fetch-store").getByRequest(requestId);
1505
+ const logs = registry.get("log-store").getByRequest(requestId);
1506
+ const errors = registry.get("error-store").getByRequest(requestId);
1507
+ const queries = registry.get("query-store").getByRequest(requestId);
1508
+ const timeline = [];
1509
+ for (const f of fetches)
1510
+ timeline.push({ type: "fetch", timestamp: f.timestamp, data: { ...f } });
1511
+ for (const l of logs)
1512
+ timeline.push({ type: "log", timestamp: l.timestamp, data: { ...l } });
1513
+ for (const e of errors)
1514
+ timeline.push({ type: "error", timestamp: e.timestamp, data: { ...e } });
1515
+ for (const q of queries)
1516
+ timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
1517
+ timeline.sort((a, b) => a.timestamp - b.timestamp);
1518
+ sendJson(req, res, 200, {
1519
+ requestId,
1520
+ total: timeline.length,
1521
+ timeline,
1522
+ counts: {
1523
+ fetches: fetches.length,
1524
+ logs: logs.length,
1525
+ errors: errors.length,
1526
+ queries: queries.length
1527
+ }
1528
+ });
1529
+ } catch (err) {
1530
+ console.error("[brakit] activity handler error:", err);
1531
+ if (!res.headersSent) {
1532
+ sendJson(req, res, 500, { error: "Internal error" });
1533
+ }
1534
+ }
1535
+ };
1342
1536
  }
1343
- var ENDPOINT_PREFIX_RE;
1344
- var init_endpoint = __esm({
1345
- "src/utils/endpoint.ts"() {
1537
+ var init_activity = __esm({
1538
+ "src/dashboard/api/activity.ts"() {
1346
1539
  "use strict";
1347
- ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1540
+ init_shared2();
1348
1541
  }
1349
1542
  });
1350
1543
 
1351
- // src/store/metrics/metrics-store.ts
1352
- import { randomUUID as randomUUID4 } from "crypto";
1353
- function createAccumulator() {
1354
- return {
1355
- durations: [],
1356
- queryCounts: [],
1357
- errorCount: 0,
1358
- totalDurationSum: 0,
1359
- totalRequestCount: 0,
1360
- totalErrorCount: 0,
1361
- totalQuerySum: 0,
1362
- totalQueryTimeMs: 0,
1363
- totalFetchTimeMs: 0
1544
+ // src/dashboard/api/index.ts
1545
+ var init_api = __esm({
1546
+ "src/dashboard/api/index.ts"() {
1547
+ "use strict";
1548
+ init_handlers();
1549
+ init_ingest();
1550
+ init_metrics2();
1551
+ init_metrics_live();
1552
+ init_activity();
1553
+ }
1554
+ });
1555
+
1556
+ // src/dashboard/api/insights.ts
1557
+ function createInsightsHandler(engine) {
1558
+ return (req, res) => {
1559
+ if (!requireGet(req, res)) return;
1560
+ sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
1364
1561
  };
1365
1562
  }
1366
- var MetricsStore;
1367
- var init_metrics_store = __esm({
1368
- "src/store/metrics/metrics-store.ts"() {
1563
+ function createSecurityHandler(engine) {
1564
+ return (req, res) => {
1565
+ if (!requireGet(req, res)) return;
1566
+ sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
1567
+ };
1568
+ }
1569
+ var init_insights = __esm({
1570
+ "src/dashboard/api/insights.ts"() {
1369
1571
  "use strict";
1370
- init_constants();
1371
- init_math();
1372
- init_endpoint();
1373
- MetricsStore = class {
1374
- constructor(persistence) {
1375
- this.persistence = persistence;
1376
- this.data = persistence.load();
1377
- for (const ep of this.data.endpoints) {
1378
- this.endpointIndex.set(ep.endpoint, ep);
1379
- }
1380
- }
1381
- data;
1382
- endpointIndex = /* @__PURE__ */ new Map();
1383
- sessionId = randomUUID4();
1384
- sessionStart = Date.now();
1385
- flushTimer = null;
1386
- accumulators = /* @__PURE__ */ new Map();
1387
- pendingPoints = /* @__PURE__ */ new Map();
1388
- start() {
1389
- this.flushTimer = setInterval(
1390
- () => this.flush(),
1391
- METRICS_FLUSH_INTERVAL_MS
1392
- );
1393
- this.flushTimer.unref();
1572
+ init_shared2();
1573
+ }
1574
+ });
1575
+
1576
+ // src/dashboard/api/findings.ts
1577
+ function createFindingsHandler(findingStore) {
1578
+ return (req, res) => {
1579
+ if (!requireGet(req, res)) return;
1580
+ const url = new URL(req.url ?? "/", "http://localhost");
1581
+ const stateParam = url.searchParams.get("state");
1582
+ let findings;
1583
+ if (stateParam && VALID_STATES.has(stateParam)) {
1584
+ findings = findingStore.getByState(stateParam);
1585
+ } else {
1586
+ findings = findingStore.getAll();
1587
+ }
1588
+ sendJson(req, res, 200, {
1589
+ total: findings.length,
1590
+ findings
1591
+ });
1592
+ };
1593
+ }
1594
+ var VALID_STATES;
1595
+ var init_findings = __esm({
1596
+ "src/dashboard/api/findings.ts"() {
1597
+ "use strict";
1598
+ init_shared2();
1599
+ VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
1600
+ }
1601
+ });
1602
+
1603
+ // src/core/disposable.ts
1604
+ var SubscriptionBag;
1605
+ var init_disposable = __esm({
1606
+ "src/core/disposable.ts"() {
1607
+ "use strict";
1608
+ SubscriptionBag = class {
1609
+ items = [];
1610
+ add(teardown) {
1611
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
1394
1612
  }
1395
- stop() {
1396
- if (this.flushTimer) {
1397
- clearInterval(this.flushTimer);
1398
- this.flushTimer = null;
1399
- }
1400
- this.flush(true);
1613
+ dispose() {
1614
+ for (const d of this.items) d.dispose();
1615
+ this.items.length = 0;
1401
1616
  }
1402
- recordRequest(req, metrics) {
1403
- if (req.isStatic) return;
1404
- const key = getEndpointKey(req.method, req.path);
1405
- let acc = this.accumulators.get(key);
1406
- if (!acc) {
1407
- acc = createAccumulator();
1408
- this.accumulators.set(key, acc);
1409
- }
1410
- acc.durations.push(req.durationMs);
1411
- acc.queryCounts.push(metrics.queryCount);
1412
- if (req.statusCode >= 400) acc.errorCount++;
1413
- acc.totalDurationSum += req.durationMs;
1414
- acc.totalRequestCount++;
1415
- acc.totalQuerySum += metrics.queryCount;
1416
- acc.totalQueryTimeMs += metrics.queryTimeMs;
1417
- acc.totalFetchTimeMs += metrics.fetchTimeMs;
1418
- if (req.statusCode >= 400) acc.totalErrorCount++;
1419
- const timestamp = Math.round(
1420
- Date.now() - (performance.now() - req.startedAt)
1421
- );
1422
- const point = {
1423
- timestamp,
1424
- durationMs: req.durationMs,
1425
- statusCode: req.statusCode,
1426
- queryCount: metrics.queryCount,
1427
- queryTimeMs: metrics.queryTimeMs,
1428
- fetchTimeMs: metrics.fetchTimeMs
1429
- };
1430
- let pending2 = this.pendingPoints.get(key);
1431
- if (!pending2) {
1432
- pending2 = [];
1433
- this.pendingPoints.set(key, pending2);
1434
- }
1435
- pending2.push(point);
1436
- }
1437
- getAll() {
1438
- return this.data.endpoints;
1439
- }
1440
- getEndpoint(endpoint) {
1441
- return this.endpointIndex.get(endpoint);
1442
- }
1443
- getLiveEndpoints() {
1444
- const merged = /* @__PURE__ */ new Map();
1445
- for (const ep of this.data.endpoints) {
1446
- if (ep.dataPoints && ep.dataPoints.length > 0) {
1447
- merged.set(ep.endpoint, ep.dataPoints);
1448
- }
1449
- }
1450
- for (const [endpoint, points] of this.pendingPoints) {
1451
- const existing = merged.get(endpoint);
1452
- merged.set(endpoint, existing ? existing.concat(points) : points);
1453
- }
1454
- const endpoints = [];
1455
- for (const [endpoint, requests] of merged) {
1456
- if (requests.length === 0) continue;
1457
- const durations = requests.map((r) => r.durationMs);
1458
- const errors = requests.filter((r) => r.statusCode >= 400).length;
1459
- const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
1460
- const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
1461
- const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
1462
- const n = requests.length;
1463
- const avgDurationMs = Math.round(durations.reduce((s, d) => s + d, 0) / n);
1464
- const avgQueryTimeMs = Math.round(totalQueryTime / n);
1465
- const avgFetchTimeMs = Math.round(totalFetchTime / n);
1466
- endpoints.push({
1467
- endpoint,
1468
- requests,
1469
- summary: {
1470
- p95Ms: percentile(durations, 0.95),
1471
- errorRate: errors / n,
1472
- avgQueryCount: Math.round(totalQueries / n),
1473
- totalRequests: n,
1474
- avgQueryTimeMs,
1475
- avgFetchTimeMs,
1476
- avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
1477
- }
1478
- });
1479
- }
1480
- endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
1481
- return endpoints;
1482
- }
1483
- reset() {
1484
- this.data = { version: 1, endpoints: [] };
1485
- this.endpointIndex.clear();
1486
- this.accumulators.clear();
1487
- this.pendingPoints.clear();
1488
- this.persistence.remove();
1489
- }
1490
- flush(sync = false) {
1491
- for (const [endpoint, acc] of this.accumulators) {
1492
- if (acc.durations.length === 0) continue;
1493
- const n = acc.totalRequestCount;
1494
- const session = {
1495
- sessionId: this.sessionId,
1496
- startedAt: this.sessionStart,
1497
- avgDurationMs: Math.round(acc.totalDurationSum / n),
1498
- p95DurationMs: percentile(acc.durations, 0.95),
1499
- requestCount: n,
1500
- errorCount: acc.totalErrorCount,
1501
- avgQueryCount: n > 0 ? Math.round(acc.totalQuerySum / n) : 0,
1502
- avgQueryTimeMs: n > 0 ? Math.round(acc.totalQueryTimeMs / n) : 0,
1503
- avgFetchTimeMs: n > 0 ? Math.round(acc.totalFetchTimeMs / n) : 0
1504
- };
1505
- const epMetrics = this.getOrCreateEndpoint(endpoint);
1506
- const existingIdx = epMetrics.sessions.findIndex(
1507
- (s) => s.sessionId === this.sessionId
1508
- );
1509
- if (existingIdx !== -1) {
1510
- epMetrics.sessions[existingIdx] = session;
1511
- } else {
1512
- epMetrics.sessions.push(session);
1513
- }
1514
- if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
1515
- epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
1516
- }
1517
- acc.durations.length = 0;
1518
- acc.queryCounts.length = 0;
1519
- acc.errorCount = 0;
1520
- }
1521
- for (const [endpoint, points] of this.pendingPoints) {
1522
- if (points.length === 0) continue;
1523
- const epMetrics = this.getOrCreateEndpoint(endpoint);
1524
- const existing = epMetrics.dataPoints ?? [];
1525
- epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
1526
- }
1527
- this.pendingPoints.clear();
1528
- if (sync) {
1529
- this.persistence.saveSync(this.data);
1530
- } else {
1531
- this.persistence.save(this.data);
1532
- }
1533
- }
1534
- getOrCreateEndpoint(endpoint) {
1535
- let ep = this.endpointIndex.get(endpoint);
1536
- if (!ep) {
1537
- ep = { endpoint, sessions: [] };
1538
- this.data.endpoints.push(ep);
1539
- this.endpointIndex.set(endpoint, ep);
1540
- }
1541
- return ep;
1542
- }
1543
- };
1544
- }
1545
- });
1546
-
1547
- // src/utils/fs.ts
1548
- import { access } from "fs/promises";
1549
- import { existsSync, readFileSync, writeFileSync } from "fs";
1550
- import { resolve } from "path";
1551
- function ensureGitignore(dir, entry) {
1552
- try {
1553
- const gitignorePath = resolve(dir, "../.gitignore");
1554
- if (existsSync(gitignorePath)) {
1555
- const content = readFileSync(gitignorePath, "utf-8");
1556
- if (content.split("\n").some((l) => l.trim() === entry)) return;
1557
- writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
1558
- } else {
1559
- writeFileSync(gitignorePath, entry + "\n");
1560
- }
1561
- } catch {
1562
- }
1563
- }
1564
- var init_fs = __esm({
1565
- "src/utils/fs.ts"() {
1566
- "use strict";
1567
- }
1568
- });
1569
-
1570
- // src/store/metrics/persistence.ts
1571
- import {
1572
- readFileSync as readFileSync2,
1573
- writeFileSync as writeFileSync2,
1574
- mkdirSync as mkdirSync2,
1575
- existsSync as existsSync2,
1576
- unlinkSync,
1577
- renameSync
1578
- } from "fs";
1579
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1580
- import { resolve as resolve2 } from "path";
1581
- var FileMetricsPersistence;
1582
- var init_persistence = __esm({
1583
- "src/store/metrics/persistence.ts"() {
1584
- "use strict";
1585
- init_constants();
1586
- init_fs();
1587
- FileMetricsPersistence = class {
1588
- metricsDir;
1589
- metricsPath;
1590
- tmpPath;
1591
- writing = false;
1592
- pendingData = null;
1593
- constructor(rootDir) {
1594
- this.metricsDir = resolve2(rootDir, METRICS_DIR);
1595
- this.metricsPath = resolve2(rootDir, METRICS_FILE);
1596
- this.tmpPath = this.metricsPath + ".tmp";
1597
- }
1598
- load() {
1599
- try {
1600
- if (existsSync2(this.metricsPath)) {
1601
- const raw = readFileSync2(this.metricsPath, "utf-8");
1602
- const parsed = JSON.parse(raw);
1603
- if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
1604
- return parsed;
1605
- }
1606
- }
1607
- } catch (err) {
1608
- process.stderr.write(`[brakit] failed to load metrics: ${err.message}
1609
- `);
1610
- }
1611
- return { version: 1, endpoints: [] };
1612
- }
1613
- save(data) {
1614
- if (this.writing) {
1615
- this.pendingData = data;
1616
- return;
1617
- }
1618
- this.writeAsync(data);
1619
- }
1620
- saveSync(data) {
1621
- try {
1622
- this.ensureDir();
1623
- writeFileSync2(this.tmpPath, JSON.stringify(data));
1624
- renameSync(this.tmpPath, this.metricsPath);
1625
- } catch (err) {
1626
- process.stderr.write(`[brakit] failed to save metrics: ${err.message}
1627
- `);
1628
- }
1629
- }
1630
- remove() {
1631
- try {
1632
- if (existsSync2(this.metricsPath)) {
1633
- unlinkSync(this.metricsPath);
1634
- }
1635
- } catch {
1636
- }
1637
- }
1638
- async writeAsync(data) {
1639
- this.writing = true;
1640
- try {
1641
- if (!existsSync2(this.metricsDir)) {
1642
- await mkdir(this.metricsDir, { recursive: true });
1643
- ensureGitignore(this.metricsDir, METRICS_DIR);
1644
- }
1645
- await writeFile2(this.tmpPath, JSON.stringify(data));
1646
- await rename(this.tmpPath, this.metricsPath);
1647
- } catch (err) {
1648
- process.stderr.write(`[brakit] failed to save metrics: ${err.message}
1649
- `);
1650
- } finally {
1651
- this.writing = false;
1652
- if (this.pendingData) {
1653
- const next = this.pendingData;
1654
- this.pendingData = null;
1655
- this.writeAsync(next);
1656
- }
1657
- }
1658
- }
1659
- ensureDir() {
1660
- if (!existsSync2(this.metricsDir)) {
1661
- mkdirSync2(this.metricsDir, { recursive: true });
1662
- ensureGitignore(this.metricsDir, METRICS_DIR);
1663
- }
1664
- }
1665
- };
1666
- }
1667
- });
1668
-
1669
- // src/store/index.ts
1670
- var init_store = __esm({
1671
- "src/store/index.ts"() {
1672
- "use strict";
1673
- init_request_store();
1674
- init_telemetry_store();
1675
- init_fetch_store();
1676
- init_log_store();
1677
- init_error_store();
1678
- init_query_store();
1679
- init_metrics_store();
1680
- init_persistence();
1681
- }
1682
- });
1683
-
1684
- // src/dashboard/api/shared.ts
1685
- function maskSensitiveHeaders(headers) {
1686
- const masked = {};
1687
- for (const [key, value] of Object.entries(headers)) {
1688
- if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
1689
- const s = String(value);
1690
- masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? "****" : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
1691
- } else {
1692
- masked[key] = value;
1693
- }
1694
- }
1695
- return masked;
1696
- }
1697
- function getCorsOrigin(req) {
1698
- const origin = req.headers.origin ?? "";
1699
- try {
1700
- const url = new URL(origin);
1701
- if (LOCALHOST_HOSTNAMES.has(url.hostname)) {
1702
- return origin;
1703
- }
1704
- } catch {
1705
- }
1706
- return "";
1707
- }
1708
- function getJsonHeaders(req) {
1709
- const corsOrigin = getCorsOrigin(req);
1710
- const headers = {
1711
- "content-type": "application/json",
1712
- "cache-control": "no-cache"
1713
- };
1714
- if (corsOrigin) {
1715
- headers["access-control-allow-origin"] = corsOrigin;
1716
- }
1717
- return headers;
1718
- }
1719
- function sendJson(req, res, status, data) {
1720
- res.writeHead(status, getJsonHeaders(req));
1721
- res.end(JSON.stringify(data));
1722
- }
1723
- function requireGet(req, res) {
1724
- if (req.method !== "GET") {
1725
- sendJson(req, res, 405, { error: "Method not allowed" });
1726
- return false;
1727
- }
1728
- return true;
1729
- }
1730
- function handleTelemetryGet(req, res, store) {
1731
- if (!requireGet(req, res)) return;
1732
- const url = new URL(req.url ?? "/", "http://localhost");
1733
- const requestId = url.searchParams.get("requestId");
1734
- const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
1735
- sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
1736
- }
1737
- var init_shared2 = __esm({
1738
- "src/dashboard/api/shared.ts"() {
1739
- "use strict";
1740
- init_constants();
1741
- init_limits();
1742
- }
1743
- });
1744
-
1745
- // src/dashboard/api/handlers.ts
1746
- function handleApiRequests(req, res) {
1747
- if (!requireGet(req, res)) return;
1748
- const url = new URL(req.url ?? "/", "http://localhost");
1749
- const method = url.searchParams.get("method");
1750
- const status = url.searchParams.get("status");
1751
- const search = url.searchParams.get("search");
1752
- const limit = parseInt(
1753
- url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT),
1754
- 10
1755
- );
1756
- const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
1757
- let results = [...getRequests()].reverse();
1758
- if (method) {
1759
- results = results.filter((r) => r.method === method.toUpperCase());
1760
- }
1761
- if (status) {
1762
- if (status.endsWith("xx")) {
1763
- const prefix = parseInt(status[0], 10);
1764
- results = results.filter(
1765
- (r) => Math.floor(r.statusCode / 100) === prefix
1766
- );
1767
- } else {
1768
- const code = parseInt(status, 10);
1769
- results = results.filter((r) => r.statusCode === code);
1770
- }
1771
- }
1772
- if (search) {
1773
- const lower = search.toLowerCase();
1774
- results = results.filter(
1775
- (r) => r.url.toLowerCase().includes(lower) || r.requestBody?.toLowerCase().includes(lower) || r.responseBody?.toLowerCase().includes(lower)
1776
- );
1777
- }
1778
- const total = results.length;
1779
- results = results.slice(offset, offset + limit);
1780
- const sanitized = results.map(sanitizeRequest);
1781
- sendJson(req, res, 200, { total, requests: sanitized });
1782
- }
1783
- function sanitizeRequest(r) {
1784
- return {
1785
- ...r,
1786
- headers: maskSensitiveHeaders(r.headers),
1787
- responseHeaders: maskSensitiveHeaders(r.responseHeaders)
1788
- };
1789
- }
1790
- function handleApiFlows(req, res) {
1791
- if (!requireGet(req, res)) return;
1792
- const flows = groupRequestsIntoFlows(getRequests()).reverse().map((flow) => ({
1793
- ...flow,
1794
- requests: flow.requests.map(sanitizeRequest)
1795
- }));
1796
- sendJson(req, res, 200, { total: flows.length, flows });
1797
- }
1798
- function createClearHandler(metricsStore, findingStore) {
1799
- return (req, res) => {
1800
- if (req.method !== "POST") {
1801
- sendJson(req, res, 405, { error: "Method not allowed" });
1802
- return;
1803
- }
1804
- clearRequests();
1805
- defaultFetchStore.clear();
1806
- defaultLogStore.clear();
1807
- defaultErrorStore.clear();
1808
- defaultQueryStore.clear();
1809
- metricsStore.reset();
1810
- findingStore?.clear();
1811
- sendJson(req, res, 200, { cleared: true });
1812
- };
1813
- }
1814
- function handleApiFetches(req, res) {
1815
- handleTelemetryGet(req, res, defaultFetchStore);
1816
- }
1817
- function handleApiLogs(req, res) {
1818
- handleTelemetryGet(req, res, defaultLogStore);
1819
- }
1820
- function handleApiErrors(req, res) {
1821
- handleTelemetryGet(req, res, defaultErrorStore);
1822
- }
1823
- function handleApiQueries(req, res) {
1824
- handleTelemetryGet(req, res, defaultQueryStore);
1825
- }
1826
- var init_handlers = __esm({
1827
- "src/dashboard/api/handlers.ts"() {
1828
- "use strict";
1829
- init_request_log();
1830
- init_group();
1831
- init_store();
1832
- init_constants();
1833
- init_shared2();
1834
- }
1835
- });
1836
-
1837
- // src/dashboard/api/ingest.ts
1838
- function isBrakitBatch(msg) {
1839
- return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
1840
- }
1841
- function routeEvent(event) {
1842
- switch (event.type) {
1843
- case "fetch":
1844
- defaultFetchStore.add(event.data);
1845
- break;
1846
- case "log":
1847
- defaultLogStore.add(event.data);
1848
- break;
1849
- case "error":
1850
- defaultErrorStore.add(event.data);
1851
- break;
1852
- case "query":
1853
- defaultQueryStore.add(event.data);
1854
- break;
1855
- }
1856
- }
1857
- function isSDKPayload(msg) {
1858
- return typeof msg === "object" && msg !== null && "_brakit" in msg && "version" in msg && typeof msg.version === "number";
1859
- }
1860
- function routeSDKEvent(event) {
1861
- const ts = event.timestamp || Date.now();
1862
- const parentRequestId = event.requestId ?? null;
1863
- switch (event.type) {
1864
- case "db.query":
1865
- defaultQueryStore.add({
1866
- driver: event.data.source ?? "sdk",
1867
- source: event.data.source ?? "sdk",
1868
- sql: event.data.sql,
1869
- model: event.data.model,
1870
- operation: event.data.operation,
1871
- normalizedOp: event.data.normalizedOp ?? event.data.operation ?? "OTHER",
1872
- table: event.data.table ?? "",
1873
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1874
- rowCount: event.data.rowCount,
1875
- parentRequestId,
1876
- timestamp: ts
1877
- });
1878
- break;
1879
- case "fetch":
1880
- defaultFetchStore.add({
1881
- url: event.data.url ?? "",
1882
- method: event.data.method ?? "GET",
1883
- statusCode: event.data.statusCode ?? 0,
1884
- durationMs: event.data.duration ?? event.data.durationMs ?? 0,
1885
- parentRequestId,
1886
- timestamp: ts
1887
- });
1888
- break;
1889
- case "log":
1890
- defaultLogStore.add({
1891
- level: event.data.level ?? "log",
1892
- message: event.data.message ?? "",
1893
- parentRequestId,
1894
- timestamp: ts
1895
- });
1896
- break;
1897
- case "error":
1898
- defaultErrorStore.add({
1899
- name: event.data.name ?? "Error",
1900
- message: event.data.message ?? "",
1901
- stack: event.data.stack ?? "",
1902
- parentRequestId,
1903
- timestamp: ts
1904
- });
1905
- break;
1906
- case "auth.check":
1907
- defaultLogStore.add({
1908
- level: "info",
1909
- message: `[auth] ${event.data.provider ?? "unknown"}: ${event.data.result ?? "check"}`,
1910
- parentRequestId,
1911
- timestamp: ts
1912
- });
1913
- break;
1914
- }
1915
- }
1916
- function handleApiIngest(req, res) {
1917
- if (req.method !== "POST") {
1918
- sendJson(req, res, 405, { error: "Method not allowed" });
1919
- return;
1920
- }
1921
- const chunks = [];
1922
- let totalSize = 0;
1923
- req.on("data", (chunk) => {
1924
- totalSize += chunk.length;
1925
- if (totalSize > MAX_INGEST_BYTES) {
1926
- sendJson(req, res, 413, { error: "Payload too large" });
1927
- req.destroy();
1928
- return;
1929
- }
1930
- chunks.push(chunk);
1931
- });
1932
- req.on("end", () => {
1933
- if (totalSize > MAX_INGEST_BYTES) return;
1934
- try {
1935
- const body = JSON.parse(Buffer.concat(chunks).toString());
1936
- if (isSDKPayload(body)) {
1937
- for (const event of body.events) {
1938
- routeSDKEvent(event);
1939
- }
1940
- res.writeHead(204);
1941
- res.end();
1942
- return;
1943
- }
1944
- if (isBrakitBatch(body)) {
1945
- for (const event of body.events) {
1946
- routeEvent(event);
1947
- }
1948
- res.writeHead(204);
1949
- res.end();
1950
- return;
1951
- }
1952
- sendJson(req, res, 400, { error: "Invalid batch" });
1953
- } catch {
1954
- sendJson(req, res, 400, { error: "Invalid JSON" });
1955
- }
1956
- });
1957
- }
1958
- var init_ingest = __esm({
1959
- "src/dashboard/api/ingest.ts"() {
1960
- "use strict";
1961
- init_store();
1962
- init_limits();
1963
- init_shared2();
1964
- }
1965
- });
1966
-
1967
- // src/dashboard/api/metrics.ts
1968
- function createMetricsHandler(metricsStore) {
1969
- return (req, res) => {
1970
- if (!requireGet(req, res)) return;
1971
- const url = new URL(req.url ?? "/", "http://localhost");
1972
- const endpoint = url.searchParams.get("endpoint");
1973
- if (endpoint) {
1974
- const ep = metricsStore.getEndpoint(endpoint);
1975
- sendJson(req, res, 200, { endpoints: ep ? [ep] : [] });
1976
- return;
1977
- }
1978
- sendJson(req, res, 200, { endpoints: metricsStore.getAll() });
1979
- };
1980
- }
1981
- var init_metrics2 = __esm({
1982
- "src/dashboard/api/metrics.ts"() {
1983
- "use strict";
1984
- init_shared2();
1985
- }
1986
- });
1987
-
1988
- // src/dashboard/api/metrics-live.ts
1989
- function createLiveMetricsHandler(metricsStore) {
1990
- return (req, res) => {
1991
- if (!requireGet(req, res)) return;
1992
- sendJson(req, res, 200, { endpoints: metricsStore.getLiveEndpoints() });
1993
- };
1994
- }
1995
- var init_metrics_live = __esm({
1996
- "src/dashboard/api/metrics-live.ts"() {
1997
- "use strict";
1998
- init_shared2();
1999
- }
2000
- });
2001
-
2002
- // src/dashboard/api/activity.ts
2003
- function handleApiActivity(req, res) {
2004
- if (!requireGet(req, res)) return;
2005
- try {
2006
- const url = new URL(req.url ?? "/", "http://localhost");
2007
- const requestId = url.searchParams.get("requestId");
2008
- if (!requestId) {
2009
- sendJson(req, res, 400, { error: "requestId parameter required" });
2010
- return;
2011
- }
2012
- const fetches = defaultFetchStore.getByRequest(requestId);
2013
- const logs = defaultLogStore.getByRequest(requestId);
2014
- const errors = defaultErrorStore.getByRequest(requestId);
2015
- const queries = defaultQueryStore.getByRequest(requestId);
2016
- const timeline = [];
2017
- for (const f of fetches)
2018
- timeline.push({ type: "fetch", timestamp: f.timestamp, data: { ...f } });
2019
- for (const l of logs)
2020
- timeline.push({ type: "log", timestamp: l.timestamp, data: { ...l } });
2021
- for (const e of errors)
2022
- timeline.push({ type: "error", timestamp: e.timestamp, data: { ...e } });
2023
- for (const q of queries)
2024
- timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
2025
- timeline.sort((a, b) => a.timestamp - b.timestamp);
2026
- sendJson(req, res, 200, {
2027
- requestId,
2028
- total: timeline.length,
2029
- timeline,
2030
- counts: {
2031
- fetches: fetches.length,
2032
- logs: logs.length,
2033
- errors: errors.length,
2034
- queries: queries.length
2035
- }
2036
- });
2037
- } catch (err) {
2038
- console.error("[brakit] activity handler error:", err);
2039
- if (!res.headersSent) {
2040
- sendJson(req, res, 500, { error: "Internal error" });
2041
- }
2042
- }
2043
- }
2044
- var init_activity = __esm({
2045
- "src/dashboard/api/activity.ts"() {
2046
- "use strict";
2047
- init_store();
2048
- init_shared2();
2049
- }
2050
- });
2051
-
2052
- // src/dashboard/api/index.ts
2053
- var init_api = __esm({
2054
- "src/dashboard/api/index.ts"() {
2055
- "use strict";
2056
- init_handlers();
2057
- init_ingest();
2058
- init_metrics2();
2059
- init_metrics_live();
2060
- init_activity();
2061
- }
2062
- });
2063
-
2064
- // src/dashboard/api/insights.ts
2065
- function createInsightsHandler(engine) {
2066
- return (req, res) => {
2067
- if (!requireGet(req, res)) return;
2068
- sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
2069
- };
2070
- }
2071
- function createSecurityHandler(engine) {
2072
- return (req, res) => {
2073
- if (!requireGet(req, res)) return;
2074
- sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
2075
- };
2076
- }
2077
- var init_insights = __esm({
2078
- "src/dashboard/api/insights.ts"() {
2079
- "use strict";
2080
- init_shared2();
2081
- }
2082
- });
2083
-
2084
- // src/dashboard/api/findings.ts
2085
- function createFindingsHandler(findingStore) {
2086
- return (req, res) => {
2087
- if (!requireGet(req, res)) return;
2088
- const url = new URL(req.url ?? "/", "http://localhost");
2089
- const stateParam = url.searchParams.get("state");
2090
- let findings;
2091
- if (stateParam && VALID_STATES.has(stateParam)) {
2092
- findings = findingStore.getByState(stateParam);
2093
- } else {
2094
- findings = findingStore.getAll();
2095
- }
2096
- sendJson(req, res, 200, {
2097
- total: findings.length,
2098
- findings
2099
- });
2100
- };
2101
- }
2102
- var VALID_STATES;
2103
- var init_findings = __esm({
2104
- "src/dashboard/api/findings.ts"() {
2105
- "use strict";
2106
- init_shared2();
2107
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
1617
+ };
2108
1618
  }
2109
1619
  });
2110
1620
 
2111
1621
  // src/dashboard/sse.ts
2112
- function createSSEHandler(engine) {
1622
+ function createSSEHandler(registry) {
2113
1623
  return (req, res) => {
2114
1624
  res.writeHead(200, {
2115
1625
  "content-type": "text/event-stream",
@@ -2131,31 +1641,17 @@ data: ${data}
2131
1641
  `);
2132
1642
  }
2133
1643
  };
2134
- const requestListener = (traced) => {
2135
- writeEvent(null, JSON.stringify(traced));
2136
- };
2137
- const fetchListener = (entry) => {
2138
- writeEvent("fetch", JSON.stringify(entry));
2139
- };
2140
- const logListener = (entry) => {
2141
- writeEvent("log", JSON.stringify(entry));
2142
- };
2143
- const errorListener = (entry) => {
2144
- writeEvent("error_event", JSON.stringify(entry));
2145
- };
2146
- const queryListener = (entry) => {
2147
- writeEvent("query", JSON.stringify(entry));
2148
- };
2149
- const analysisListener = engine ? ({ statefulInsights, statefulFindings }) => {
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 }) => {
2150
1652
  writeEvent("insights", JSON.stringify(statefulInsights));
2151
1653
  writeEvent("security", JSON.stringify(statefulFindings));
2152
- } : void 0;
2153
- onRequest(requestListener);
2154
- defaultFetchStore.onEntry(fetchListener);
2155
- defaultLogStore.onEntry(logListener);
2156
- defaultErrorStore.onEntry(errorListener);
2157
- defaultQueryStore.onEntry(queryListener);
2158
- if (engine && analysisListener) engine.onUpdate(analysisListener);
1654
+ }));
2159
1655
  const heartbeat = setInterval(() => {
2160
1656
  if (res.destroyed) {
2161
1657
  clearInterval(heartbeat);
@@ -2165,20 +1661,14 @@ data: ${data}
2165
1661
  }, SSE_HEARTBEAT_INTERVAL_MS);
2166
1662
  req.on("close", () => {
2167
1663
  clearInterval(heartbeat);
2168
- offRequest(requestListener);
2169
- defaultFetchStore.offEntry(fetchListener);
2170
- defaultLogStore.offEntry(logListener);
2171
- defaultErrorStore.offEntry(errorListener);
2172
- defaultQueryStore.offEntry(queryListener);
2173
- if (engine && analysisListener) engine.offUpdate(analysisListener);
1664
+ subs.dispose();
2174
1665
  });
2175
1666
  };
2176
1667
  }
2177
1668
  var init_sse = __esm({
2178
1669
  "src/dashboard/sse.ts"() {
2179
1670
  "use strict";
2180
- init_request_log();
2181
- init_store();
1671
+ init_disposable();
2182
1672
  init_constants();
2183
1673
  }
2184
1674
  });
@@ -2760,6 +2250,120 @@ var init_styles = __esm({
2760
2250
  }
2761
2251
  });
2762
2252
 
2253
+ // src/utils/fs.ts
2254
+ import { access } from "fs/promises";
2255
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2256
+ import { resolve } from "path";
2257
+ function ensureGitignore(dir, entry) {
2258
+ try {
2259
+ const gitignorePath = resolve(dir, "../.gitignore");
2260
+ if (existsSync(gitignorePath)) {
2261
+ const content = readFileSync(gitignorePath, "utf-8");
2262
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
2263
+ writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
2264
+ } else {
2265
+ writeFileSync(gitignorePath, entry + "\n");
2266
+ }
2267
+ } catch {
2268
+ }
2269
+ }
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
+ `);
2285
+ }
2286
+ }
2287
+ var PREFIX;
2288
+ var init_log = __esm({
2289
+ "src/utils/log.ts"() {
2290
+ "use strict";
2291
+ PREFIX = "[brakit]";
2292
+ }
2293
+ });
2294
+
2295
+ // src/utils/atomic-writer.ts
2296
+ import {
2297
+ writeFileSync as writeFileSync2,
2298
+ existsSync as existsSync2,
2299
+ mkdirSync as mkdirSync2,
2300
+ renameSync
2301
+ } from "fs";
2302
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
2303
+ var AtomicWriter;
2304
+ var init_atomic_writer = __esm({
2305
+ "src/utils/atomic-writer.ts"() {
2306
+ "use strict";
2307
+ init_fs();
2308
+ init_log();
2309
+ AtomicWriter = class {
2310
+ constructor(opts) {
2311
+ this.opts = opts;
2312
+ this.tmpPath = opts.filePath + ".tmp";
2313
+ }
2314
+ tmpPath;
2315
+ writing = false;
2316
+ pendingContent = null;
2317
+ writeSync(content) {
2318
+ try {
2319
+ this.ensureDir();
2320
+ writeFileSync2(this.tmpPath, content);
2321
+ renameSync(this.tmpPath, this.opts.filePath);
2322
+ } catch (err) {
2323
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2324
+ }
2325
+ }
2326
+ async writeAsync(content) {
2327
+ if (this.writing) {
2328
+ this.pendingContent = content;
2329
+ return;
2330
+ }
2331
+ this.writing = true;
2332
+ try {
2333
+ await this.ensureDirAsync();
2334
+ await writeFile2(this.tmpPath, content);
2335
+ await rename(this.tmpPath, this.opts.filePath);
2336
+ } catch (err) {
2337
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
2338
+ } finally {
2339
+ this.writing = false;
2340
+ if (this.pendingContent !== null) {
2341
+ const next = this.pendingContent;
2342
+ this.pendingContent = null;
2343
+ this.writeAsync(next);
2344
+ }
2345
+ }
2346
+ }
2347
+ ensureDir() {
2348
+ if (!existsSync2(this.opts.dir)) {
2349
+ mkdirSync2(this.opts.dir, { recursive: true });
2350
+ if (this.opts.gitignoreEntry) {
2351
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2352
+ }
2353
+ }
2354
+ }
2355
+ async ensureDirAsync() {
2356
+ if (!existsSync2(this.opts.dir)) {
2357
+ await mkdir(this.opts.dir, { recursive: true });
2358
+ if (this.opts.gitignoreEntry) {
2359
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
2360
+ }
2361
+ }
2362
+ }
2363
+ };
2364
+ }
2365
+ });
2366
+
2763
2367
  // src/store/finding-id.ts
2764
2368
  import { createHash } from "crypto";
2765
2369
  function computeFindingId(finding) {
@@ -2773,37 +2377,33 @@ var init_finding_id = __esm({
2773
2377
  });
2774
2378
 
2775
2379
  // src/store/finding-store.ts
2776
- import {
2777
- readFileSync as readFileSync3,
2778
- writeFileSync as writeFileSync3,
2779
- existsSync as existsSync3,
2780
- mkdirSync as mkdirSync3,
2781
- renameSync as renameSync2
2782
- } from "fs";
2783
- import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
2784
- import { resolve as resolve3 } from "path";
2380
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
2381
+ import { resolve as resolve2 } from "path";
2785
2382
  var FindingStore;
2786
2383
  var init_finding_store = __esm({
2787
2384
  "src/store/finding-store.ts"() {
2788
2385
  "use strict";
2789
2386
  init_constants();
2790
- init_fs();
2387
+ init_atomic_writer();
2791
2388
  init_finding_id();
2792
2389
  FindingStore = class {
2793
2390
  constructor(rootDir) {
2794
2391
  this.rootDir = rootDir;
2795
- this.metricsDir = resolve3(rootDir, METRICS_DIR);
2796
- this.findingsPath = resolve3(rootDir, FINDINGS_FILE);
2797
- this.tmpPath = this.findingsPath + ".tmp";
2392
+ const metricsDir = resolve2(rootDir, METRICS_DIR);
2393
+ this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
2394
+ this.writer = new AtomicWriter({
2395
+ dir: metricsDir,
2396
+ filePath: this.findingsPath,
2397
+ gitignoreEntry: METRICS_DIR,
2398
+ label: "findings"
2399
+ });
2798
2400
  this.load();
2799
2401
  }
2800
2402
  findings = /* @__PURE__ */ new Map();
2801
2403
  flushTimer = null;
2802
2404
  dirty = false;
2803
- writing = false;
2405
+ writer;
2804
2406
  findingsPath;
2805
- tmpPath;
2806
- metricsDir;
2807
2407
  start() {
2808
2408
  this.flushTimer = setInterval(
2809
2409
  () => this.flush(),
@@ -2857,6 +2457,15 @@ var init_finding_store = __esm({
2857
2457
  this.dirty = true;
2858
2458
  return true;
2859
2459
  }
2460
+ /**
2461
+ * Reconcile passive findings against the current analysis results.
2462
+ *
2463
+ * Passive findings are detected by continuous scanning (not user-triggered).
2464
+ * When a previously-seen finding is absent from the current results, it means
2465
+ * the issue has been fixed — transition it to "resolved" automatically.
2466
+ * Active findings (from MCP verify-fix) are not auto-resolved because they
2467
+ * require explicit verification.
2468
+ */
2860
2469
  reconcilePassive(currentFindings) {
2861
2470
  const currentIds = new Set(currentFindings.map(computeFindingId));
2862
2471
  for (const [id, stateful] of this.findings) {
@@ -2883,7 +2492,7 @@ var init_finding_store = __esm({
2883
2492
  load() {
2884
2493
  try {
2885
2494
  if (existsSync3(this.findingsPath)) {
2886
- const raw = readFileSync3(this.findingsPath, "utf-8");
2495
+ const raw = readFileSync2(this.findingsPath, "utf-8");
2887
2496
  const parsed = JSON.parse(raw);
2888
2497
  if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
2889
2498
  for (const f of parsed.findings) {
@@ -2896,56 +2505,20 @@ var init_finding_store = __esm({
2896
2505
  }
2897
2506
  flush() {
2898
2507
  if (!this.dirty) return;
2899
- this.writeAsync();
2508
+ this.writer.writeAsync(this.serialize());
2509
+ this.dirty = false;
2900
2510
  }
2901
2511
  flushSync() {
2902
2512
  if (!this.dirty) return;
2903
- try {
2904
- this.ensureDir();
2905
- const data = {
2906
- version: 1,
2907
- findings: [...this.findings.values()]
2908
- };
2909
- writeFileSync3(this.tmpPath, JSON.stringify(data));
2910
- renameSync2(this.tmpPath, this.findingsPath);
2911
- this.dirty = false;
2912
- } catch (err) {
2913
- process.stderr.write(
2914
- `[brakit] failed to save findings: ${err.message}
2915
- `
2916
- );
2917
- }
2918
- }
2919
- async writeAsync() {
2920
- if (this.writing) return;
2921
- this.writing = true;
2922
- try {
2923
- if (!existsSync3(this.metricsDir)) {
2924
- await mkdir2(this.metricsDir, { recursive: true });
2925
- ensureGitignore(this.metricsDir, METRICS_DIR);
2926
- }
2927
- const data = {
2928
- version: 1,
2929
- findings: [...this.findings.values()]
2930
- };
2931
- await writeFile3(this.tmpPath, JSON.stringify(data));
2932
- await rename2(this.tmpPath, this.findingsPath);
2933
- this.dirty = false;
2934
- } catch (err) {
2935
- process.stderr.write(
2936
- `[brakit] failed to save findings: ${err.message}
2937
- `
2938
- );
2939
- } finally {
2940
- this.writing = false;
2941
- if (this.dirty) this.writeAsync();
2942
- }
2513
+ this.writer.writeSync(this.serialize());
2514
+ this.dirty = false;
2943
2515
  }
2944
- ensureDir() {
2945
- if (!existsSync3(this.metricsDir)) {
2946
- mkdirSync3(this.metricsDir, { recursive: true });
2947
- ensureGitignore(this.metricsDir, METRICS_DIR);
2948
- }
2516
+ serialize() {
2517
+ const data = {
2518
+ version: 1,
2519
+ findings: [...this.findings.values()]
2520
+ };
2521
+ return JSON.stringify(data);
2949
2522
  }
2950
2523
  };
2951
2524
  }
@@ -3591,6 +3164,21 @@ var init_collections = __esm({
3591
3164
  }
3592
3165
  });
3593
3166
 
3167
+ // src/utils/endpoint.ts
3168
+ function getEndpointKey(method, path) {
3169
+ return `${method} ${path}`;
3170
+ }
3171
+ function extractEndpointFromDesc(desc) {
3172
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
3173
+ }
3174
+ var ENDPOINT_PREFIX_RE;
3175
+ var init_endpoint = __esm({
3176
+ "src/utils/endpoint.ts"() {
3177
+ "use strict";
3178
+ ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
3179
+ }
3180
+ });
3181
+
3594
3182
  // src/analysis/insights/query-helpers.ts
3595
3183
  function getQueryShape(q) {
3596
3184
  if (q.sql) return normalizeQueryParams(q.sql) ?? "";
@@ -3736,7 +3324,7 @@ var init_n1 = __esm({
3736
3324
  "use strict";
3737
3325
  init_query_helpers();
3738
3326
  init_endpoint();
3739
- init_thresholds();
3327
+ init_constants();
3740
3328
  n1Rule = {
3741
3329
  id: "n1",
3742
3330
  check(ctx) {
@@ -3786,7 +3374,7 @@ var init_cross_endpoint = __esm({
3786
3374
  "use strict";
3787
3375
  init_query_helpers();
3788
3376
  init_endpoint();
3789
- init_thresholds();
3377
+ init_constants();
3790
3378
  crossEndpointRule = {
3791
3379
  id: "cross-endpoint",
3792
3380
  check(ctx) {
@@ -3844,7 +3432,7 @@ var init_redundant_query = __esm({
3844
3432
  "use strict";
3845
3433
  init_query_helpers();
3846
3434
  init_endpoint();
3847
- init_thresholds();
3435
+ init_constants();
3848
3436
  redundantQueryRule = {
3849
3437
  id: "redundant-query",
3850
3438
  check(ctx) {
@@ -3923,7 +3511,7 @@ var errorHotspotRule;
3923
3511
  var init_error_hotspot = __esm({
3924
3512
  "src/analysis/insights/rules/error-hotspot.ts"() {
3925
3513
  "use strict";
3926
- init_thresholds();
3514
+ init_constants();
3927
3515
  errorHotspotRule = {
3928
3516
  id: "error-hotspot",
3929
3517
  check(ctx) {
@@ -3953,7 +3541,7 @@ var duplicateRule;
3953
3541
  var init_duplicate = __esm({
3954
3542
  "src/analysis/insights/rules/duplicate.ts"() {
3955
3543
  "use strict";
3956
- init_thresholds();
3544
+ init_constants();
3957
3545
  duplicateRule = {
3958
3546
  id: "duplicate",
3959
3547
  check(ctx) {
@@ -4016,7 +3604,7 @@ var init_slow = __esm({
4016
3604
  "src/analysis/insights/rules/slow.ts"() {
4017
3605
  "use strict";
4018
3606
  init_format();
4019
- init_thresholds();
3607
+ init_constants();
4020
3608
  slowRule = {
4021
3609
  id: "slow",
4022
3610
  check(ctx) {
@@ -4063,7 +3651,7 @@ var queryHeavyRule;
4063
3651
  var init_query_heavy = __esm({
4064
3652
  "src/analysis/insights/rules/query-heavy.ts"() {
4065
3653
  "use strict";
4066
- init_thresholds();
3654
+ init_constants();
4067
3655
  queryHeavyRule = {
4068
3656
  id: "query-heavy",
4069
3657
  check(ctx) {
@@ -4094,7 +3682,7 @@ var init_select_star = __esm({
4094
3682
  "src/analysis/insights/rules/select-star.ts"() {
4095
3683
  "use strict";
4096
3684
  init_query_helpers();
4097
- init_thresholds();
3685
+ init_constants();
4098
3686
  init_patterns();
4099
3687
  selectStarRule = {
4100
3688
  id: "select-star",
@@ -4134,7 +3722,7 @@ var init_high_rows = __esm({
4134
3722
  "src/analysis/insights/rules/high-rows.ts"() {
4135
3723
  "use strict";
4136
3724
  init_query_helpers();
4137
- init_thresholds();
3725
+ init_constants();
4138
3726
  highRowsRule = {
4139
3727
  id: "high-rows",
4140
3728
  check(ctx) {
@@ -4179,7 +3767,7 @@ var init_response_overfetch = __esm({
4179
3767
  init_endpoint();
4180
3768
  init_response();
4181
3769
  init_patterns();
4182
- init_thresholds();
3770
+ init_constants();
4183
3771
  responseOverfetchRule = {
4184
3772
  id: "response-overfetch",
4185
3773
  check(ctx) {
@@ -4238,7 +3826,7 @@ var init_large_response = __esm({
4238
3826
  "src/analysis/insights/rules/large-response.ts"() {
4239
3827
  "use strict";
4240
3828
  init_format();
4241
- init_thresholds();
3829
+ init_constants();
4242
3830
  largeResponseRule = {
4243
3831
  id: "large-response",
4244
3832
  check(ctx) {
@@ -4269,7 +3857,7 @@ var init_regression = __esm({
4269
3857
  "src/analysis/insights/rules/regression.ts"() {
4270
3858
  "use strict";
4271
3859
  init_format();
4272
- init_thresholds();
3860
+ init_constants();
4273
3861
  regressionRule = {
4274
3862
  id: "regression",
4275
3863
  check(ctx) {
@@ -4331,6 +3919,27 @@ var init_security2 = __esm({
4331
3919
  }
4332
3920
  });
4333
3921
 
3922
+ // src/analysis/insights/rules/index.ts
3923
+ var init_rules2 = __esm({
3924
+ "src/analysis/insights/rules/index.ts"() {
3925
+ "use strict";
3926
+ init_n1();
3927
+ init_cross_endpoint();
3928
+ init_redundant_query();
3929
+ init_error();
3930
+ init_error_hotspot();
3931
+ init_duplicate();
3932
+ init_slow();
3933
+ init_query_heavy();
3934
+ init_select_star();
3935
+ init_high_rows();
3936
+ init_response_overfetch();
3937
+ init_large_response();
3938
+ init_regression();
3939
+ init_security2();
3940
+ }
3941
+ });
3942
+
4334
3943
  // src/analysis/insights/index.ts
4335
3944
  function createDefaultInsightRunner() {
4336
3945
  const runner = new InsightRunner();
@@ -4358,20 +3967,7 @@ var init_insights2 = __esm({
4358
3967
  "use strict";
4359
3968
  init_runner();
4360
3969
  init_runner();
4361
- init_n1();
4362
- init_cross_endpoint();
4363
- init_redundant_query();
4364
- init_error();
4365
- init_error_hotspot();
4366
- init_duplicate();
4367
- init_slow();
4368
- init_query_heavy();
4369
- init_select_star();
4370
- init_high_rows();
4371
- init_response_overfetch();
4372
- init_large_response();
4373
- init_regression();
4374
- init_security2();
3970
+ init_rules2();
4375
3971
  }
4376
3972
  });
4377
3973
 
@@ -4451,23 +4047,16 @@ var AnalysisEngine;
4451
4047
  var init_engine = __esm({
4452
4048
  "src/analysis/engine.ts"() {
4453
4049
  "use strict";
4454
- init_request_log();
4455
- init_store();
4456
- init_request_log();
4050
+ init_disposable();
4457
4051
  init_group();
4458
4052
  init_rules();
4459
4053
  init_insights3();
4460
4054
  init_insight_tracker();
4461
4055
  AnalysisEngine = class {
4462
- constructor(metricsStore, findingStore, debounceMs = 300) {
4463
- this.metricsStore = metricsStore;
4464
- this.findingStore = findingStore;
4056
+ constructor(registry, debounceMs = 300) {
4057
+ this.registry = registry;
4465
4058
  this.debounceMs = debounceMs;
4466
4059
  this.scanner = createDefaultScanner();
4467
- this.boundRequestListener = () => this.scheduleRecompute();
4468
- this.boundQueryListener = () => this.scheduleRecompute();
4469
- this.boundErrorListener = () => this.scheduleRecompute();
4470
- this.boundLogListener = () => this.scheduleRecompute();
4471
4060
  }
4472
4061
  scanner;
4473
4062
  insightTracker = new InsightTracker();
@@ -4475,34 +4064,21 @@ var init_engine = __esm({
4475
4064
  cachedFindings = [];
4476
4065
  cachedStatefulInsights = [];
4477
4066
  debounceTimer = null;
4478
- listeners = [];
4479
- boundRequestListener;
4480
- boundQueryListener;
4481
- boundErrorListener;
4482
- boundLogListener;
4067
+ subs = new SubscriptionBag();
4483
4068
  start() {
4484
- onRequest(this.boundRequestListener);
4485
- defaultQueryStore.onEntry(this.boundQueryListener);
4486
- defaultErrorStore.onEntry(this.boundErrorListener);
4487
- defaultLogStore.onEntry(this.boundLogListener);
4069
+ const bus = this.registry.get("event-bus");
4070
+ this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
4071
+ this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
4072
+ this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
4073
+ this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
4488
4074
  }
4489
4075
  stop() {
4490
- offRequest(this.boundRequestListener);
4491
- defaultQueryStore.offEntry(this.boundQueryListener);
4492
- defaultErrorStore.offEntry(this.boundErrorListener);
4493
- defaultLogStore.offEntry(this.boundLogListener);
4076
+ this.subs.dispose();
4494
4077
  if (this.debounceTimer) {
4495
4078
  clearTimeout(this.debounceTimer);
4496
4079
  this.debounceTimer = null;
4497
4080
  }
4498
4081
  }
4499
- onUpdate(fn) {
4500
- this.listeners.push(fn);
4501
- }
4502
- offUpdate(fn) {
4503
- const idx = this.listeners.indexOf(fn);
4504
- if (idx !== -1) this.listeners.splice(idx, 1);
4505
- }
4506
4082
  getInsights() {
4507
4083
  return this.cachedInsights;
4508
4084
  }
@@ -4510,7 +4086,7 @@ var init_engine = __esm({
4510
4086
  return this.cachedFindings;
4511
4087
  }
4512
4088
  getStatefulFindings() {
4513
- return this.findingStore?.getAll() ?? [];
4089
+ return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
4514
4090
  }
4515
4091
  getStatefulInsights() {
4516
4092
  return this.cachedStatefulInsights;
@@ -4523,18 +4099,19 @@ var init_engine = __esm({
4523
4099
  }, this.debounceMs);
4524
4100
  }
4525
4101
  recompute() {
4526
- const requests = getRequests();
4527
- const queries = defaultQueryStore.getAll();
4528
- const errors = defaultErrorStore.getAll();
4529
- const logs = defaultLogStore.getAll();
4530
- const fetches = defaultFetchStore.getAll();
4102
+ const requests = this.registry.get("request-store").getAll();
4103
+ const queries = this.registry.get("query-store").getAll();
4104
+ const errors = this.registry.get("error-store").getAll();
4105
+ const logs = this.registry.get("log-store").getAll();
4106
+ const fetches = this.registry.get("fetch-store").getAll();
4531
4107
  const flows = groupRequestsIntoFlows(requests);
4532
4108
  this.cachedFindings = this.scanner.scan({ requests, logs });
4533
- if (this.findingStore) {
4109
+ if (this.registry.has("finding-store")) {
4110
+ const findingStore = this.registry.get("finding-store");
4534
4111
  for (const finding of this.cachedFindings) {
4535
- this.findingStore.upsert(finding, "passive");
4112
+ findingStore.upsert(finding, "passive");
4536
4113
  }
4537
- this.findingStore.reconcilePassive(this.cachedFindings);
4114
+ findingStore.reconcilePassive(this.cachedFindings);
4538
4115
  }
4539
4116
  this.cachedInsights = computeInsights({
4540
4117
  requests,
@@ -4542,7 +4119,7 @@ var init_engine = __esm({
4542
4119
  errors,
4543
4120
  flows,
4544
4121
  fetches,
4545
- previousMetrics: this.metricsStore.getAll(),
4122
+ previousMetrics: this.registry.get("metrics-store").getAll(),
4546
4123
  securityFindings: this.cachedFindings
4547
4124
  });
4548
4125
  this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
@@ -4552,12 +4129,7 @@ var init_engine = __esm({
4552
4129
  statefulFindings: this.getStatefulFindings(),
4553
4130
  statefulInsights: this.cachedStatefulInsights
4554
4131
  };
4555
- for (const fn of this.listeners) {
4556
- try {
4557
- fn(update);
4558
- } catch {
4559
- }
4560
- }
4132
+ this.registry.get("event-bus").emit("analysis:updated", update);
4561
4133
  }
4562
4134
  };
4563
4135
  }
@@ -4575,7 +4147,7 @@ var init_src = __esm({
4575
4147
  init_engine();
4576
4148
  init_insights3();
4577
4149
  init_insights2();
4578
- VERSION = "0.8.1";
4150
+ VERSION = "0.8.2";
4579
4151
  }
4580
4152
  });
4581
4153
 
@@ -6053,7 +5625,7 @@ function getGraphDetail() {
6053
5625
  function renderEndpointDetail(container) {
6054
5626
  var ep = graphData.find(function(e) { return e.endpoint === selectedEndpoint; });
6055
5627
  if (!ep || !ep.requests || ep.requests.length === 0) {
6056
- container.innerHTML += '<div class="empty" style="height:300px"><span class="empty-sub">No data for this endpoint</span></div>';
5628
+ container.innerHTML += '<div class="empty"><span class="empty-sub">No data for this endpoint</span></div>';
6057
5629
  return;
6058
5630
  }
6059
5631
 
@@ -6501,7 +6073,7 @@ function getOverviewRender() {
6501
6073
  var hasData = nonStatic.length > 0 || state.queries.length > 0 || state.errors.length > 0;
6502
6074
 
6503
6075
  if (!hasData) {
6504
- container.innerHTML = '<div class="empty" style="height:400px"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see insights here</span></div>';
6076
+ container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see insights here</span></div>';
6505
6077
  return;
6506
6078
  }
6507
6079
 
@@ -6669,7 +6241,7 @@ function getSecurityView() {
6669
6241
  if (open.length === 0 && resolved.length === 0) {
6670
6242
  var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
6671
6243
  if (!hasData) {
6672
- container.innerHTML = '<div class="empty" style="height:400px"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
6244
+ container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
6673
6245
  } else {
6674
6246
  container.innerHTML = '<div class="sec-clear"><span class="sec-clear-icon">\\u2713</span><div class="sec-clear-text"><div class="sec-clear-title">All clear</div><div class="sec-clear-sub">No security or quality issues detected this session</div></div></div>';
6675
6247
  }
@@ -6756,7 +6328,7 @@ function getSecurityView() {
6756
6328
  var row = document.createElement('div');
6757
6329
  row.className = 'sec-item';
6758
6330
  row.innerHTML =
6759
- '<div class="sec-item-desc">' + item.desc + '</div>' +
6331
+ '<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
6760
6332
  (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
6761
6333
  list.appendChild(row);
6762
6334
  }
@@ -6854,377 +6426,854 @@ function getApp() {
6854
6426
  }
6855
6427
  };
6856
6428
 
6857
- events.addEventListener('fetch', function(e) {
6858
- var f = JSON.parse(e.data);
6859
- state.fetches.unshift(f);
6860
- if (state.fetches.length > ${MAX_TELEMETRY_ENTRIES}) state.fetches.pop();
6861
- prependFetchRow(f);
6862
- updateStats();
6863
- if (f.parentRequestId) { invalidateTimelineCache(f.parentRequestId); refreshVisibleTimeline(f.parentRequestId); }
6864
- });
6429
+ function registerTelemetryListener(eventName, stateKey, prependFn) {
6430
+ events.addEventListener(eventName, function(e) {
6431
+ var item = JSON.parse(e.data);
6432
+ state[stateKey].unshift(item);
6433
+ if (state[stateKey].length > ${MAX_TELEMETRY_ENTRIES}) state[stateKey].pop();
6434
+ prependFn(item);
6435
+ updateStats();
6436
+ if (item.parentRequestId) { invalidateTimelineCache(item.parentRequestId); refreshVisibleTimeline(item.parentRequestId); }
6437
+ });
6438
+ }
6439
+ registerTelemetryListener('fetch', 'fetches', prependFetchRow);
6440
+ registerTelemetryListener('log', 'logs', prependLogRow);
6441
+ registerTelemetryListener('error_event', 'errors', prependErrorRow);
6442
+ registerTelemetryListener('query', 'queries', prependQueryRow);
6865
6443
 
6866
- events.addEventListener('log', function(e) {
6867
- var l = JSON.parse(e.data);
6868
- state.logs.unshift(l);
6869
- if (state.logs.length > ${MAX_TELEMETRY_ENTRIES}) state.logs.pop();
6870
- prependLogRow(l);
6444
+ events.addEventListener('insights', function(e) {
6445
+ state.insights = JSON.parse(e.data);
6446
+ if (state.activeView === 'overview') renderOverview();
6871
6447
  updateStats();
6872
- if (l.parentRequestId) { invalidateTimelineCache(l.parentRequestId); refreshVisibleTimeline(l.parentRequestId); }
6873
6448
  });
6874
6449
 
6875
- events.addEventListener('error_event', function(e) {
6876
- var err = JSON.parse(e.data);
6877
- state.errors.unshift(err);
6878
- if (state.errors.length > ${MAX_TELEMETRY_ENTRIES}) state.errors.pop();
6879
- prependErrorRow(err);
6450
+ events.addEventListener('security', function(e) {
6451
+ state.findings = JSON.parse(e.data);
6452
+ if (state.activeView === 'security') renderSecurity();
6880
6453
  updateStats();
6881
- if (err.parentRequestId) { invalidateTimelineCache(err.parentRequestId); refreshVisibleTimeline(err.parentRequestId); }
6882
6454
  });
6883
6455
 
6884
- events.addEventListener('query', function(e) {
6885
- var q = JSON.parse(e.data);
6886
- state.queries.unshift(q);
6887
- if (state.queries.length > ${MAX_TELEMETRY_ENTRIES}) state.queries.pop();
6888
- prependQueryRow(q);
6889
- updateStats();
6890
- if (q.parentRequestId) { invalidateTimelineCache(q.parentRequestId); refreshVisibleTimeline(q.parentRequestId); }
6456
+ window.addEventListener('beforeunload', function() {
6457
+ events.close();
6458
+ clearTimeout(reloadTimer);
6459
+ clearTimeout(perfReloadTimer);
6891
6460
  });
6461
+ }
6892
6462
 
6893
- events.addEventListener('insights', function(e) {
6894
- state.insights = JSON.parse(e.data);
6895
- if (state.activeView === 'overview') renderOverview();
6463
+ async function reloadFlows() {
6464
+ try {
6465
+ var res = await fetch('${DASHBOARD_API_FLOWS}');
6466
+ var data = await res.json();
6467
+ state.flows = data.flows;
6468
+ renderFlows();
6896
6469
  updateStats();
6470
+ } catch(e) { console.warn('[brakit]', e); }
6471
+ }
6472
+
6473
+ function switchView(view) {
6474
+ Object.keys(VIEW_CONTAINERS).forEach(function(v) {
6475
+ var el = document.getElementById(VIEW_CONTAINERS[v]);
6476
+ if (el) el.style.display = v === view ? 'block' : 'none';
6897
6477
  });
6478
+ }
6898
6479
 
6899
- events.addEventListener('security', function(e) {
6900
- state.findings = JSON.parse(e.data);
6901
- if (state.activeView === 'security') renderSecurity();
6902
- updateStats();
6480
+ var sidebarItems = document.querySelectorAll('.sidebar-item:not(.disabled)');
6481
+ sidebarItems.forEach(function(item) {
6482
+ item.addEventListener('click', function() {
6483
+ var view = item.getAttribute('data-view');
6484
+ if (!view || view === state.activeView) return;
6485
+ sidebarItems.forEach(function(i) { i.classList.remove('active'); });
6486
+ item.classList.add('active');
6487
+ state.activeView = view;
6488
+ fetch('${DASHBOARD_API_TAB}?tab=' + encodeURIComponent(view)).catch(function(){});
6489
+ document.getElementById('header-title').textContent = VIEW_TITLES[view] || view;
6490
+ document.getElementById('header-sub').textContent = VIEW_SUBTITLES[view] || '';
6491
+ document.getElementById('mode-toggle').style.display = view === 'actions' ? 'flex' : 'none';
6492
+ if (view === 'overview') renderOverview();
6493
+ if (view === 'security') renderSecurity();
6494
+ if (view === 'performance') loadMetrics();
6495
+ switchView(view);
6903
6496
  });
6497
+ });
6498
+
6499
+ document.getElementById('mode-simple').addEventListener('click', function() {
6500
+ state.viewMode = 'simple';
6501
+ document.getElementById('mode-simple').classList.add('active');
6502
+ document.getElementById('mode-detailed').classList.remove('active');
6503
+ collapseAll('.flow-row', '.flow-expand');
6504
+ });
6505
+ document.getElementById('mode-detailed').addEventListener('click', function() {
6506
+ state.viewMode = 'detailed';
6507
+ document.getElementById('mode-detailed').classList.add('active');
6508
+ document.getElementById('mode-simple').classList.remove('active');
6509
+ collapseAll('.flow-row', '.flow-expand');
6510
+ });
6511
+
6512
+ function updateStats() {
6513
+ var reqs = state.requests.filter(function(r) { return !r.path || !r.path.startsWith('${DASHBOARD_PREFIX}'); });
6514
+ var errors = reqs.filter(function(r) { return r.statusCode >= 400; }).length;
6515
+ var avg = reqs.length > 0 ? Math.round(reqs.reduce(function(s,r) { return s + r.durationMs; }, 0) / reqs.length) : 0;
6516
+ document.getElementById('stat-total').textContent = reqs.length + ' request' + (reqs.length !== 1 ? 's' : '');
6517
+ document.getElementById('stat-flows').textContent = state.flows.length + ' action' + (state.flows.length !== 1 ? 's' : '');
6518
+ document.getElementById('stat-errors').textContent = errors + ' error' + (errors !== 1 ? 's' : '');
6519
+ document.getElementById('stat-avg').textContent = 'Avg: ' + avg + 'ms';
6520
+ var actionCount = document.getElementById('sidebar-count-actions');
6521
+ var requestCount = document.getElementById('sidebar-count-requests');
6522
+ var fetchCount = document.getElementById('sidebar-count-fetches');
6523
+ var errorCount = document.getElementById('sidebar-count-errors');
6524
+ var logCount = document.getElementById('sidebar-count-logs');
6525
+ var queryCount = document.getElementById('sidebar-count-queries');
6526
+ if (actionCount) actionCount.textContent = state.flows.length;
6527
+ if (requestCount) requestCount.textContent = reqs.length;
6528
+ if (fetchCount) fetchCount.textContent = state.fetches.length;
6529
+ if (errorCount) errorCount.textContent = state.errors.length;
6530
+ if (logCount) logCount.textContent = state.logs.length;
6531
+ if (queryCount) queryCount.textContent = state.queries.length;
6532
+ var secCount = document.getElementById('sidebar-count-security');
6533
+ if (secCount) {
6534
+ var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
6535
+ secCount.textContent = numFindings;
6536
+ secCount.style.display = numFindings > 0 ? '' : 'none';
6537
+ }
6538
+ }
6539
+
6540
+ function copyAsCurl(req) {
6541
+ var headers = Object.entries(req.headers || {})
6542
+ .filter(function(e) { return ${CURL_SKIP_HEADERS}.indexOf(e[0]) === -1; })
6543
+ .map(function(e) { return "-H '" + e[0] + ": " + e[1] + "'"; })
6544
+ .join(' ');
6545
+ var body = req.requestBody ? " -d '" + req.requestBody.replace(/'/g, "'\\\\''") + "'" : '';
6546
+ var curl = "curl -X " + req.method + " " + headers + body + " 'http://localhost:" + PORT + req.url + "'";
6547
+ navigator.clipboard.writeText(curl).then(function() { showToast('Copied cURL command'); });
6548
+ }
6549
+
6550
+ document.getElementById('clear-btn').addEventListener('click', async function() {
6551
+ if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
6552
+ await fetch('${DASHBOARD_API_CLEAR}', {method: 'POST'});
6553
+ state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
6554
+ state.insights = []; state.findings = [];
6555
+ graphData = []; selectedEndpoint = ${ALL_ENDPOINTS_SELECTOR}; timelineCache = {};
6556
+ renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
6557
+ showToast('Cleared');
6558
+ });
6559
+
6560
+ init();
6561
+ `;
6562
+ }
6563
+ var init_app2 = __esm({
6564
+ "src/dashboard/client/app.ts"() {
6565
+ "use strict";
6566
+ init_constants();
6567
+ init_constants2();
6568
+ }
6569
+ });
6570
+
6571
+ // src/dashboard/client/index.ts
6572
+ function getClientScript(config) {
6573
+ return `
6574
+ (function(){
6575
+ var PORT = ${config.proxyPort};
6576
+ var state = { flows: [], requests: [], fetches: [], errors: [], logs: [], queries: [], insights: [], findings: [], viewMode: 'simple', activeView: 'overview' };
6577
+
6578
+ var appEl = document.getElementById('app');
6579
+ var flowListEl = document.getElementById('flow-list');
6580
+ var reqListEl = document.getElementById('request-list');
6581
+ var emptyFlows = document.getElementById('empty-flows');
6582
+ var toastEl = document.getElementById('toast');
6583
+
6584
+ ${getHelpers()}
6585
+ ${getTelemetryViewHelpers()}
6586
+ ${getSqlUtils()}
6587
+ ${getFlowsView()}
6588
+ ${getRequestsView()}
6589
+ ${getFetchesView()}
6590
+ ${getErrorsView()}
6591
+ ${getLogsView()}
6592
+ ${getQueriesView()}
6593
+ ${getTimelineView()}
6594
+ ${getGraphView()}
6595
+ ${getOverviewView()}
6596
+ ${getSecurityView()}
6597
+ ${getApp()}
6598
+ })();
6599
+ `;
6600
+ }
6601
+ var init_client = __esm({
6602
+ "src/dashboard/client/index.ts"() {
6603
+ "use strict";
6604
+ init_helpers();
6605
+ init_view_helpers();
6606
+ init_sql_utils();
6607
+ init_flows2();
6608
+ init_requests2();
6609
+ init_fetches();
6610
+ init_errors2();
6611
+ init_logs();
6612
+ init_queries();
6613
+ init_timeline2();
6614
+ init_graph2();
6615
+ init_overview3();
6616
+ init_security3();
6617
+ init_app2();
6618
+ }
6619
+ });
6620
+
6621
+ // src/dashboard/page.ts
6622
+ function getDashboardHtml(config) {
6623
+ return `<!DOCTYPE html>
6624
+ <html lang="en">
6625
+ <head>
6626
+ <meta charset="UTF-8">
6627
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6628
+ <title>brakit</title>
6629
+ <style>${getStyles()}</style>
6630
+ </head>
6631
+ <body>
6632
+ ${getLayoutHtml(config)}
6633
+ <script>${getClientScript(config)}</script>
6634
+ </body>
6635
+ </html>`;
6636
+ }
6637
+ var init_page = __esm({
6638
+ "src/dashboard/page.ts"() {
6639
+ "use strict";
6640
+ init_styles();
6641
+ init_layout2();
6642
+ init_client();
6643
+ }
6644
+ });
6645
+
6646
+ // src/telemetry/config.ts
6647
+ import { homedir } from "os";
6648
+ 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";
6651
+ function readConfig() {
6652
+ try {
6653
+ if (!existsSync4(CONFIG_PATH)) return null;
6654
+ return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
6655
+ } catch {
6656
+ return null;
6657
+ }
6658
+ }
6659
+ function isTelemetryEnabled() {
6660
+ const env = process.env.BRAKIT_TELEMETRY;
6661
+ if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
6662
+ return readConfig()?.telemetry ?? true;
6663
+ }
6664
+ var CONFIG_DIR, CONFIG_PATH;
6665
+ var init_config = __esm({
6666
+ "src/telemetry/config.ts"() {
6667
+ "use strict";
6668
+ CONFIG_DIR = join2(homedir(), ".brakit");
6669
+ CONFIG_PATH = join2(CONFIG_DIR, "config.json");
6904
6670
  }
6671
+ });
6905
6672
 
6906
- async function reloadFlows() {
6907
- try {
6908
- var res = await fetch('${DASHBOARD_API_FLOWS}');
6909
- var data = await res.json();
6910
- state.flows = data.flows;
6911
- renderFlows();
6912
- updateStats();
6913
- } catch(e) { console.warn('[brakit]', e); }
6673
+ // src/telemetry/index.ts
6674
+ import { platform, release, arch } from "os";
6675
+ function recordTabViewed(tab) {
6676
+ tabsViewed.add(tab);
6677
+ }
6678
+ function recordDashboardOpened() {
6679
+ dashboardOpened = true;
6680
+ }
6681
+ var tabsViewed, dashboardOpened;
6682
+ var init_telemetry = __esm({
6683
+ "src/telemetry/index.ts"() {
6684
+ "use strict";
6685
+ init_src();
6686
+ init_config();
6687
+ init_config();
6688
+ tabsViewed = /* @__PURE__ */ new Set();
6689
+ dashboardOpened = false;
6914
6690
  }
6691
+ });
6915
6692
 
6916
- function switchView(view) {
6917
- Object.keys(VIEW_CONTAINERS).forEach(function(v) {
6918
- var el = document.getElementById(VIEW_CONTAINERS[v]);
6919
- if (el) el.style.display = v === view ? 'block' : 'none';
6920
- });
6693
+ // src/dashboard/router.ts
6694
+ function isDashboardRequest(url) {
6695
+ return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
6696
+ }
6697
+ function createDashboardHandler(registry) {
6698
+ const metricsStore = registry.get("metrics-store");
6699
+ const analysisEngine = registry.has("analysis-engine") ? registry.get("analysis-engine") : void 0;
6700
+ const routes = {
6701
+ [DASHBOARD_API_REQUESTS]: createRequestsHandler(registry),
6702
+ [DASHBOARD_API_EVENTS]: createSSEHandler(registry),
6703
+ [DASHBOARD_API_FLOWS]: createFlowsHandler(registry),
6704
+ [DASHBOARD_API_CLEAR]: createClearHandler(registry),
6705
+ [DASHBOARD_API_LOGS]: createLogsHandler(registry),
6706
+ [DASHBOARD_API_FETCHES]: createFetchesHandler(registry),
6707
+ [DASHBOARD_API_ERRORS]: createErrorsHandler(registry),
6708
+ [DASHBOARD_API_QUERIES]: createQueriesHandler(registry),
6709
+ [DASHBOARD_API_METRICS]: createMetricsHandler(metricsStore),
6710
+ [DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(metricsStore),
6711
+ [DASHBOARD_API_INGEST]: createIngestHandler(registry),
6712
+ [DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
6713
+ };
6714
+ if (analysisEngine) {
6715
+ routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(analysisEngine);
6716
+ routes[DASHBOARD_API_SECURITY] = createSecurityHandler(analysisEngine);
6921
6717
  }
6922
-
6923
- var sidebarItems = document.querySelectorAll('.sidebar-item:not(.disabled)');
6924
- sidebarItems.forEach(function(item) {
6925
- item.addEventListener('click', function() {
6926
- var view = item.getAttribute('data-view');
6927
- if (!view || view === state.activeView) return;
6928
- sidebarItems.forEach(function(i) { i.classList.remove('active'); });
6929
- item.classList.add('active');
6930
- state.activeView = view;
6931
- fetch('${DASHBOARD_API_TAB}?tab=' + encodeURIComponent(view)).catch(function(){});
6932
- document.getElementById('header-title').textContent = VIEW_TITLES[view] || view;
6933
- document.getElementById('header-sub').textContent = VIEW_SUBTITLES[view] || '';
6934
- document.getElementById('mode-toggle').style.display = view === 'actions' ? 'flex' : 'none';
6935
- if (view === 'overview') renderOverview();
6936
- if (view === 'security') renderSecurity();
6937
- if (view === 'performance') loadMetrics();
6938
- switchView(view);
6718
+ if (registry.has("finding-store")) {
6719
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(registry.get("finding-store"));
6720
+ }
6721
+ routes[DASHBOARD_API_TAB] = (req, res) => {
6722
+ const raw = (req.url ?? "").split("tab=")[1];
6723
+ if (raw) {
6724
+ const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
6725
+ if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
6726
+ }
6727
+ res.writeHead(204);
6728
+ res.end();
6729
+ };
6730
+ return (req, res, config) => {
6731
+ const path = (req.url ?? "/").split("?")[0];
6732
+ const handler = routes[path];
6733
+ if (handler) {
6734
+ handler(req, res);
6735
+ return;
6736
+ }
6737
+ if (isTelemetryEnabled()) recordDashboardOpened();
6738
+ res.writeHead(200, {
6739
+ "content-type": "text/html; charset=utf-8",
6740
+ "cache-control": "no-cache",
6741
+ ...SECURITY_HEADERS
6939
6742
  });
6940
- });
6743
+ res.end(getDashboardHtml(config));
6744
+ };
6745
+ }
6746
+ var SECURITY_HEADERS;
6747
+ var init_router = __esm({
6748
+ "src/dashboard/router.ts"() {
6749
+ "use strict";
6750
+ init_constants();
6751
+ init_api();
6752
+ init_insights();
6753
+ init_findings();
6754
+ init_sse();
6755
+ 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
+ };
6763
+ }
6764
+ });
6941
6765
 
6942
- document.getElementById('mode-simple').addEventListener('click', function() {
6943
- state.viewMode = 'simple';
6944
- document.getElementById('mode-simple').classList.add('active');
6945
- document.getElementById('mode-detailed').classList.remove('active');
6946
- collapseAll('.flow-row', '.flow-expand');
6947
- });
6948
- document.getElementById('mode-detailed').addEventListener('click', function() {
6949
- state.viewMode = 'detailed';
6950
- document.getElementById('mode-detailed').classList.add('active');
6951
- document.getElementById('mode-simple').classList.remove('active');
6952
- collapseAll('.flow-row', '.flow-expand');
6953
- });
6766
+ // src/core/event-bus.ts
6767
+ var EventBus;
6768
+ var init_event_bus = __esm({
6769
+ "src/core/event-bus.ts"() {
6770
+ "use strict";
6771
+ EventBus = class {
6772
+ listeners = /* @__PURE__ */ new Map();
6773
+ emit(channel, data) {
6774
+ const set = this.listeners.get(channel);
6775
+ if (!set) return;
6776
+ for (const fn of set) {
6777
+ try {
6778
+ fn(data);
6779
+ } catch {
6780
+ }
6781
+ }
6782
+ }
6783
+ on(channel, fn) {
6784
+ let set = this.listeners.get(channel);
6785
+ if (!set) {
6786
+ set = /* @__PURE__ */ new Set();
6787
+ this.listeners.set(channel, set);
6788
+ }
6789
+ set.add(fn);
6790
+ return () => set.delete(fn);
6791
+ }
6792
+ off(channel, fn) {
6793
+ this.listeners.get(channel)?.delete(fn);
6794
+ }
6795
+ };
6796
+ }
6797
+ });
6954
6798
 
6955
- function updateStats() {
6956
- var reqs = state.requests.filter(function(r) { return !r.path || !r.path.startsWith('${DASHBOARD_PREFIX}'); });
6957
- var errors = reqs.filter(function(r) { return r.statusCode >= 400; }).length;
6958
- var avg = reqs.length > 0 ? Math.round(reqs.reduce(function(s,r) { return s + r.durationMs; }, 0) / reqs.length) : 0;
6959
- document.getElementById('stat-total').textContent = reqs.length + ' request' + (reqs.length !== 1 ? 's' : '');
6960
- document.getElementById('stat-flows').textContent = state.flows.length + ' action' + (state.flows.length !== 1 ? 's' : '');
6961
- document.getElementById('stat-errors').textContent = errors + ' error' + (errors !== 1 ? 's' : '');
6962
- document.getElementById('stat-avg').textContent = 'Avg: ' + avg + 'ms';
6963
- var actionCount = document.getElementById('sidebar-count-actions');
6964
- var requestCount = document.getElementById('sidebar-count-requests');
6965
- var fetchCount = document.getElementById('sidebar-count-fetches');
6966
- var errorCount = document.getElementById('sidebar-count-errors');
6967
- var logCount = document.getElementById('sidebar-count-logs');
6968
- var queryCount = document.getElementById('sidebar-count-queries');
6969
- if (actionCount) actionCount.textContent = state.flows.length;
6970
- if (requestCount) requestCount.textContent = reqs.length;
6971
- if (fetchCount) fetchCount.textContent = state.fetches.length;
6972
- if (errorCount) errorCount.textContent = state.errors.length;
6973
- if (logCount) logCount.textContent = state.logs.length;
6974
- if (queryCount) queryCount.textContent = state.queries.length;
6975
- var secCount = document.getElementById('sidebar-count-security');
6976
- if (secCount) {
6977
- var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
6978
- secCount.textContent = numFindings;
6979
- secCount.style.display = numFindings > 0 ? '' : 'none';
6980
- }
6799
+ // src/core/service-registry.ts
6800
+ var ServiceRegistry;
6801
+ var init_service_registry = __esm({
6802
+ "src/core/service-registry.ts"() {
6803
+ "use strict";
6804
+ ServiceRegistry = class {
6805
+ services = /* @__PURE__ */ new Map();
6806
+ register(name, service) {
6807
+ this.services.set(name, service);
6808
+ }
6809
+ get(name) {
6810
+ const service = this.services.get(name);
6811
+ if (!service) throw new Error(`Service "${name}" not registered`);
6812
+ return service;
6813
+ }
6814
+ has(name) {
6815
+ return this.services.has(name);
6816
+ }
6817
+ };
6981
6818
  }
6819
+ });
6982
6820
 
6983
- function copyAsCurl(req) {
6984
- var headers = Object.entries(req.headers || {})
6985
- .filter(function(e) { return ${CURL_SKIP_HEADERS}.indexOf(e[0]) === -1; })
6986
- .map(function(e) { return "-H '" + e[0] + ": " + e[1] + "'"; })
6987
- .join(' ');
6988
- var body = req.requestBody ? " -d '" + req.requestBody.replace(/'/g, "'\\\\''") + "'" : '';
6989
- var curl = "curl -X " + req.method + " " + headers + body + " 'http://localhost:" + PORT + req.url + "'";
6990
- navigator.clipboard.writeText(curl).then(function() { showToast('Copied cURL command'); });
6821
+ // src/utils/static-patterns.ts
6822
+ function isStaticPath(urlPath) {
6823
+ return STATIC_PATTERNS.some((p) => p.test(urlPath));
6824
+ }
6825
+ var STATIC_PATTERNS;
6826
+ var init_static_patterns = __esm({
6827
+ "src/utils/static-patterns.ts"() {
6828
+ "use strict";
6829
+ STATIC_PATTERNS = [
6830
+ /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
6831
+ /^\/favicon/,
6832
+ /^\/node_modules\//,
6833
+ // Framework-specific static/internal paths
6834
+ /^\/_next\//,
6835
+ /^\/__nextjs/,
6836
+ /^\/@vite\//,
6837
+ /^\/__vite/
6838
+ ];
6991
6839
  }
6840
+ });
6992
6841
 
6993
- document.getElementById('clear-btn').addEventListener('click', async function() {
6994
- if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
6995
- await fetch('${DASHBOARD_API_CLEAR}', {method: 'POST'});
6996
- state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
6997
- state.insights = []; state.findings = [];
6998
- graphData = []; selectedEndpoint = ${ALL_ENDPOINTS_SELECTOR}; timelineCache = {};
6999
- renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
7000
- showToast('Cleared');
7001
- });
6842
+ // src/store/request-store.ts
6843
+ function flattenHeaders(headers) {
6844
+ const flat = {};
6845
+ for (const [key, value] of Object.entries(headers)) {
6846
+ if (value === void 0) continue;
6847
+ flat[key] = Array.isArray(value) ? value.join(", ") : value;
6848
+ }
6849
+ return flat;
6850
+ }
6851
+ var RequestStore;
6852
+ var init_request_store = __esm({
6853
+ "src/store/request-store.ts"() {
6854
+ "use strict";
6855
+ init_constants();
6856
+ init_static_patterns();
6857
+ RequestStore = class {
6858
+ constructor(maxEntries = MAX_REQUEST_ENTRIES) {
6859
+ this.maxEntries = maxEntries;
6860
+ }
6861
+ requests = [];
6862
+ listeners = [];
6863
+ capture(input) {
6864
+ const url = input.url;
6865
+ const path = url.split("?")[0];
6866
+ let requestBodyStr = null;
6867
+ if (input.requestBody && input.requestBody.length > 0) {
6868
+ requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
6869
+ }
6870
+ let responseBodyStr = null;
6871
+ if (input.responseBody && input.responseBody.length > 0) {
6872
+ const ct = input.responseContentType;
6873
+ if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
6874
+ responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
6875
+ }
6876
+ }
6877
+ const entry = {
6878
+ id: input.requestId,
6879
+ method: input.method,
6880
+ url,
6881
+ path,
6882
+ headers: flattenHeaders(input.requestHeaders),
6883
+ requestBody: requestBodyStr,
6884
+ statusCode: input.statusCode,
6885
+ responseHeaders: flattenHeaders(input.responseHeaders),
6886
+ responseBody: responseBodyStr,
6887
+ startedAt: input.startTime,
6888
+ durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
6889
+ responseSize: input.responseBody?.length ?? 0,
6890
+ isStatic: isStaticPath(path)
6891
+ };
6892
+ this.requests.push(entry);
6893
+ if (this.requests.length > this.maxEntries) {
6894
+ this.requests.shift();
6895
+ }
6896
+ for (const fn of this.listeners) {
6897
+ fn(entry);
6898
+ }
6899
+ return entry;
6900
+ }
6901
+ getAll() {
6902
+ return this.requests;
6903
+ }
6904
+ clear() {
6905
+ this.requests.length = 0;
6906
+ }
6907
+ onRequest(fn) {
6908
+ this.listeners.push(fn);
6909
+ }
6910
+ offRequest(fn) {
6911
+ const idx = this.listeners.indexOf(fn);
6912
+ if (idx !== -1) this.listeners.splice(idx, 1);
6913
+ }
6914
+ };
6915
+ }
6916
+ });
7002
6917
 
7003
- init();
7004
- `;
7005
- }
7006
- var init_app2 = __esm({
7007
- "src/dashboard/client/app.ts"() {
6918
+ // src/store/telemetry-store.ts
6919
+ import { randomUUID as randomUUID4 } from "crypto";
6920
+ var TelemetryStore;
6921
+ var init_telemetry_store = __esm({
6922
+ "src/store/telemetry-store.ts"() {
7008
6923
  "use strict";
7009
6924
  init_constants();
7010
- init_constants2();
6925
+ TelemetryStore = class {
6926
+ constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
6927
+ this.maxEntries = maxEntries;
6928
+ }
6929
+ entries = [];
6930
+ listeners = [];
6931
+ add(data) {
6932
+ const entry = { id: randomUUID4(), ...data };
6933
+ this.entries.push(entry);
6934
+ if (this.entries.length > this.maxEntries) this.entries.shift();
6935
+ for (const fn of this.listeners) fn(entry);
6936
+ return entry;
6937
+ }
6938
+ getAll() {
6939
+ return this.entries;
6940
+ }
6941
+ getByRequest(requestId) {
6942
+ return this.entries.filter((e) => e.parentRequestId === requestId);
6943
+ }
6944
+ clear() {
6945
+ this.entries.length = 0;
6946
+ }
6947
+ onEntry(fn) {
6948
+ this.listeners.push(fn);
6949
+ }
6950
+ offEntry(fn) {
6951
+ const idx = this.listeners.indexOf(fn);
6952
+ if (idx !== -1) this.listeners.splice(idx, 1);
6953
+ }
6954
+ };
7011
6955
  }
7012
6956
  });
7013
6957
 
7014
- // src/dashboard/client/index.ts
7015
- function getClientScript(config) {
7016
- return `
7017
- (function(){
7018
- var PORT = ${config.proxyPort};
7019
- var state = { flows: [], requests: [], fetches: [], errors: [], logs: [], queries: [], insights: [], findings: [], viewMode: 'simple', activeView: 'overview' };
7020
-
7021
- var appEl = document.getElementById('app');
7022
- var flowListEl = document.getElementById('flow-list');
7023
- var reqListEl = document.getElementById('request-list');
7024
- var emptyFlows = document.getElementById('empty-flows');
7025
- var toastEl = document.getElementById('toast');
6958
+ // src/store/fetch-store.ts
6959
+ var FetchStore;
6960
+ var init_fetch_store = __esm({
6961
+ "src/store/fetch-store.ts"() {
6962
+ "use strict";
6963
+ init_telemetry_store();
6964
+ FetchStore = class extends TelemetryStore {
6965
+ };
6966
+ }
6967
+ });
7026
6968
 
7027
- ${getHelpers()}
7028
- ${getTelemetryViewHelpers()}
7029
- ${getSqlUtils()}
7030
- ${getFlowsView()}
7031
- ${getRequestsView()}
7032
- ${getFetchesView()}
7033
- ${getErrorsView()}
7034
- ${getLogsView()}
7035
- ${getQueriesView()}
7036
- ${getTimelineView()}
7037
- ${getGraphView()}
7038
- ${getOverviewView()}
7039
- ${getSecurityView()}
7040
- ${getApp()}
7041
- })();
7042
- `;
7043
- }
7044
- var init_client = __esm({
7045
- "src/dashboard/client/index.ts"() {
6969
+ // src/store/log-store.ts
6970
+ var LogStore;
6971
+ var init_log_store = __esm({
6972
+ "src/store/log-store.ts"() {
7046
6973
  "use strict";
7047
- init_helpers();
7048
- init_view_helpers();
7049
- init_sql_utils();
7050
- init_flows2();
7051
- init_requests2();
7052
- init_fetches();
7053
- init_errors2();
7054
- init_logs();
7055
- init_queries();
7056
- init_timeline2();
7057
- init_graph2();
7058
- init_overview3();
7059
- init_security3();
7060
- init_app2();
6974
+ init_telemetry_store();
6975
+ LogStore = class extends TelemetryStore {
6976
+ };
7061
6977
  }
7062
6978
  });
7063
6979
 
7064
- // src/dashboard/page.ts
7065
- function getDashboardHtml(config) {
7066
- return `<!DOCTYPE html>
7067
- <html lang="en">
7068
- <head>
7069
- <meta charset="UTF-8">
7070
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7071
- <title>brakit</title>
7072
- <style>${getStyles()}</style>
7073
- </head>
7074
- <body>
7075
- ${getLayoutHtml(config)}
7076
- <script>${getClientScript(config)}</script>
7077
- </body>
7078
- </html>`;
7079
- }
7080
- var init_page = __esm({
7081
- "src/dashboard/page.ts"() {
6980
+ // src/store/error-store.ts
6981
+ var ErrorStore;
6982
+ var init_error_store = __esm({
6983
+ "src/store/error-store.ts"() {
7082
6984
  "use strict";
7083
- init_styles();
7084
- init_layout2();
7085
- init_client();
6985
+ init_telemetry_store();
6986
+ ErrorStore = class extends TelemetryStore {
6987
+ };
7086
6988
  }
7087
6989
  });
7088
6990
 
7089
- // src/telemetry/config.ts
7090
- import { homedir } from "os";
7091
- import { join as join2 } from "path";
7092
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
7093
- import { randomUUID as randomUUID5 } from "crypto";
7094
- function readConfig() {
7095
- try {
7096
- if (!existsSync4(CONFIG_PATH)) return null;
7097
- return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
7098
- } catch {
7099
- return null;
6991
+ // src/store/query-store.ts
6992
+ var QueryStore;
6993
+ var init_query_store = __esm({
6994
+ "src/store/query-store.ts"() {
6995
+ "use strict";
6996
+ init_telemetry_store();
6997
+ QueryStore = class extends TelemetryStore {
6998
+ };
7100
6999
  }
7000
+ });
7001
+
7002
+ // src/utils/math.ts
7003
+ function percentile(values, p) {
7004
+ if (values.length === 0) return 0;
7005
+ const sorted = [...values].sort((a, b) => a - b);
7006
+ const idx = Math.ceil(p * sorted.length) - 1;
7007
+ return Math.round(sorted[Math.max(0, idx)]);
7101
7008
  }
7102
- function isTelemetryEnabled() {
7103
- const env = process.env.BRAKIT_TELEMETRY;
7104
- if (env !== void 0) return env !== "false" && env !== "0" && env !== "off";
7105
- return readConfig()?.telemetry ?? true;
7106
- }
7107
- var CONFIG_DIR, CONFIG_PATH;
7108
- var init_config = __esm({
7109
- "src/telemetry/config.ts"() {
7009
+ var init_math = __esm({
7010
+ "src/utils/math.ts"() {
7110
7011
  "use strict";
7111
- CONFIG_DIR = join2(homedir(), ".brakit");
7112
- CONFIG_PATH = join2(CONFIG_DIR, "config.json");
7113
7012
  }
7114
7013
  });
7115
7014
 
7116
- // src/telemetry/index.ts
7117
- import { platform, release, arch } from "os";
7118
- function recordTabViewed(tab) {
7119
- tabsViewed.add(tab);
7120
- }
7121
- function recordDashboardOpened() {
7122
- dashboardOpened = true;
7015
+ // src/store/metrics/metrics-store.ts
7016
+ import { randomUUID as randomUUID5 } from "crypto";
7017
+ function createAccumulator() {
7018
+ return {
7019
+ durations: [],
7020
+ queryCounts: [],
7021
+ errorCount: 0,
7022
+ totalDurationSum: 0,
7023
+ totalRequestCount: 0,
7024
+ totalErrorCount: 0,
7025
+ totalQuerySum: 0,
7026
+ totalQueryTimeMs: 0,
7027
+ totalFetchTimeMs: 0
7028
+ };
7123
7029
  }
7124
- var tabsViewed, dashboardOpened;
7125
- var init_telemetry = __esm({
7126
- "src/telemetry/index.ts"() {
7030
+ var MetricsStore;
7031
+ var init_metrics_store = __esm({
7032
+ "src/store/metrics/metrics-store.ts"() {
7127
7033
  "use strict";
7128
- init_src();
7129
- init_store();
7130
- init_config();
7131
- init_config();
7132
- tabsViewed = /* @__PURE__ */ new Set();
7133
- dashboardOpened = false;
7034
+ init_constants();
7035
+ init_math();
7036
+ init_endpoint();
7037
+ MetricsStore = class {
7038
+ constructor(persistence) {
7039
+ this.persistence = persistence;
7040
+ this.data = persistence.load();
7041
+ for (const ep of this.data.endpoints) {
7042
+ this.endpointIndex.set(ep.endpoint, ep);
7043
+ }
7044
+ }
7045
+ data;
7046
+ endpointIndex = /* @__PURE__ */ new Map();
7047
+ sessionId = randomUUID5();
7048
+ sessionStart = Date.now();
7049
+ flushTimer = null;
7050
+ accumulators = /* @__PURE__ */ new Map();
7051
+ pendingPoints = /* @__PURE__ */ new Map();
7052
+ start() {
7053
+ this.flushTimer = setInterval(
7054
+ () => this.flush(),
7055
+ METRICS_FLUSH_INTERVAL_MS
7056
+ );
7057
+ this.flushTimer.unref();
7058
+ }
7059
+ stop() {
7060
+ if (this.flushTimer) {
7061
+ clearInterval(this.flushTimer);
7062
+ this.flushTimer = null;
7063
+ }
7064
+ this.flush(true);
7065
+ }
7066
+ recordRequest(req, metrics) {
7067
+ if (req.isStatic) return;
7068
+ const key = getEndpointKey(req.method, req.path);
7069
+ let acc = this.accumulators.get(key);
7070
+ if (!acc) {
7071
+ acc = createAccumulator();
7072
+ this.accumulators.set(key, acc);
7073
+ }
7074
+ acc.durations.push(req.durationMs);
7075
+ acc.queryCounts.push(metrics.queryCount);
7076
+ if (req.statusCode >= 400) acc.errorCount++;
7077
+ acc.totalDurationSum += req.durationMs;
7078
+ acc.totalRequestCount++;
7079
+ acc.totalQuerySum += metrics.queryCount;
7080
+ acc.totalQueryTimeMs += metrics.queryTimeMs;
7081
+ acc.totalFetchTimeMs += metrics.fetchTimeMs;
7082
+ if (req.statusCode >= 400) acc.totalErrorCount++;
7083
+ const timestamp = Math.round(
7084
+ Date.now() - (performance.now() - req.startedAt)
7085
+ );
7086
+ const point = {
7087
+ timestamp,
7088
+ durationMs: req.durationMs,
7089
+ statusCode: req.statusCode,
7090
+ queryCount: metrics.queryCount,
7091
+ queryTimeMs: metrics.queryTimeMs,
7092
+ fetchTimeMs: metrics.fetchTimeMs
7093
+ };
7094
+ let pending2 = this.pendingPoints.get(key);
7095
+ if (!pending2) {
7096
+ pending2 = [];
7097
+ this.pendingPoints.set(key, pending2);
7098
+ }
7099
+ pending2.push(point);
7100
+ }
7101
+ getAll() {
7102
+ return this.data.endpoints;
7103
+ }
7104
+ getEndpoint(endpoint) {
7105
+ return this.endpointIndex.get(endpoint);
7106
+ }
7107
+ getLiveEndpoints() {
7108
+ const merged = /* @__PURE__ */ new Map();
7109
+ for (const ep of this.data.endpoints) {
7110
+ if (ep.dataPoints && ep.dataPoints.length > 0) {
7111
+ merged.set(ep.endpoint, ep.dataPoints);
7112
+ }
7113
+ }
7114
+ for (const [endpoint, points] of this.pendingPoints) {
7115
+ const existing = merged.get(endpoint);
7116
+ merged.set(endpoint, existing ? existing.concat(points) : points);
7117
+ }
7118
+ const endpoints = [];
7119
+ for (const [endpoint, requests] of merged) {
7120
+ if (requests.length === 0) continue;
7121
+ const durations = requests.map((r) => r.durationMs);
7122
+ const errors = requests.filter((r) => r.statusCode >= 400).length;
7123
+ const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
7124
+ const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
7125
+ const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
7126
+ const n = requests.length;
7127
+ const avgDurationMs = Math.round(durations.reduce((s, d) => s + d, 0) / n);
7128
+ const avgQueryTimeMs = Math.round(totalQueryTime / n);
7129
+ const avgFetchTimeMs = Math.round(totalFetchTime / n);
7130
+ endpoints.push({
7131
+ endpoint,
7132
+ requests,
7133
+ summary: {
7134
+ p95Ms: percentile(durations, 0.95),
7135
+ errorRate: errors / n,
7136
+ avgQueryCount: Math.round(totalQueries / n),
7137
+ totalRequests: n,
7138
+ avgQueryTimeMs,
7139
+ avgFetchTimeMs,
7140
+ avgAppTimeMs: Math.max(0, avgDurationMs - avgQueryTimeMs - avgFetchTimeMs)
7141
+ }
7142
+ });
7143
+ }
7144
+ endpoints.sort((a, b) => b.summary.p95Ms - a.summary.p95Ms);
7145
+ return endpoints;
7146
+ }
7147
+ reset() {
7148
+ this.data = { version: 1, endpoints: [] };
7149
+ this.endpointIndex.clear();
7150
+ this.accumulators.clear();
7151
+ this.pendingPoints.clear();
7152
+ this.persistence.remove();
7153
+ }
7154
+ flush(sync = false) {
7155
+ for (const [endpoint, acc] of this.accumulators) {
7156
+ if (acc.durations.length === 0) continue;
7157
+ const n = acc.totalRequestCount;
7158
+ const session = {
7159
+ sessionId: this.sessionId,
7160
+ startedAt: this.sessionStart,
7161
+ avgDurationMs: Math.round(acc.totalDurationSum / n),
7162
+ p95DurationMs: percentile(acc.durations, 0.95),
7163
+ requestCount: n,
7164
+ errorCount: acc.totalErrorCount,
7165
+ avgQueryCount: n > 0 ? Math.round(acc.totalQuerySum / n) : 0,
7166
+ avgQueryTimeMs: n > 0 ? Math.round(acc.totalQueryTimeMs / n) : 0,
7167
+ avgFetchTimeMs: n > 0 ? Math.round(acc.totalFetchTimeMs / n) : 0
7168
+ };
7169
+ const epMetrics = this.getOrCreateEndpoint(endpoint);
7170
+ const existingIdx = epMetrics.sessions.findIndex(
7171
+ (s) => s.sessionId === this.sessionId
7172
+ );
7173
+ if (existingIdx !== -1) {
7174
+ epMetrics.sessions[existingIdx] = session;
7175
+ } else {
7176
+ epMetrics.sessions.push(session);
7177
+ }
7178
+ if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
7179
+ epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
7180
+ }
7181
+ acc.durations.length = 0;
7182
+ acc.queryCounts.length = 0;
7183
+ acc.errorCount = 0;
7184
+ }
7185
+ for (const [endpoint, points] of this.pendingPoints) {
7186
+ if (points.length === 0) continue;
7187
+ const epMetrics = this.getOrCreateEndpoint(endpoint);
7188
+ const existing = epMetrics.dataPoints ?? [];
7189
+ epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
7190
+ }
7191
+ this.pendingPoints.clear();
7192
+ if (sync) {
7193
+ this.persistence.saveSync(this.data);
7194
+ } else {
7195
+ this.persistence.save(this.data);
7196
+ }
7197
+ }
7198
+ getOrCreateEndpoint(endpoint) {
7199
+ let ep = this.endpointIndex.get(endpoint);
7200
+ if (!ep) {
7201
+ ep = { endpoint, sessions: [] };
7202
+ this.data.endpoints.push(ep);
7203
+ this.endpointIndex.set(endpoint, ep);
7204
+ }
7205
+ return ep;
7206
+ }
7207
+ };
7134
7208
  }
7135
7209
  });
7136
7210
 
7137
- // src/dashboard/router.ts
7138
- function isDashboardRequest(url) {
7139
- return url === DASHBOARD_PREFIX || url.startsWith(DASHBOARD_PREFIX + "/");
7140
- }
7141
- function createDashboardHandler(deps) {
7142
- const routes = {
7143
- [DASHBOARD_API_REQUESTS]: handleApiRequests,
7144
- [DASHBOARD_API_EVENTS]: createSSEHandler(deps.analysisEngine),
7145
- [DASHBOARD_API_FLOWS]: handleApiFlows,
7146
- [DASHBOARD_API_CLEAR]: createClearHandler(deps.metricsStore, deps.findingStore),
7147
- [DASHBOARD_API_LOGS]: handleApiLogs,
7148
- [DASHBOARD_API_FETCHES]: handleApiFetches,
7149
- [DASHBOARD_API_ERRORS]: handleApiErrors,
7150
- [DASHBOARD_API_QUERIES]: handleApiQueries,
7151
- [DASHBOARD_API_METRICS]: createMetricsHandler(deps.metricsStore),
7152
- [DASHBOARD_API_METRICS_LIVE]: createLiveMetricsHandler(deps.metricsStore),
7153
- [DASHBOARD_API_INGEST]: handleApiIngest,
7154
- [DASHBOARD_API_ACTIVITY]: handleApiActivity
7155
- };
7156
- if (deps.analysisEngine) {
7157
- routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(deps.analysisEngine);
7158
- routes[DASHBOARD_API_SECURITY] = createSecurityHandler(deps.analysisEngine);
7159
- }
7160
- if (deps.findingStore) {
7161
- routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(deps.findingStore);
7162
- }
7163
- routes[DASHBOARD_API_TAB] = (req, res) => {
7164
- const raw = (req.url ?? "").split("tab=")[1];
7165
- if (raw) {
7166
- const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
7167
- if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
7168
- }
7169
- res.writeHead(204);
7170
- res.end();
7171
- };
7172
- return (req, res, config) => {
7173
- const path = (req.url ?? "/").split("?")[0];
7174
- const handler = routes[path];
7175
- if (handler) {
7176
- handler(req, res);
7177
- return;
7178
- }
7179
- if (isTelemetryEnabled()) recordDashboardOpened();
7180
- res.writeHead(200, {
7181
- "content-type": "text/html; charset=utf-8",
7182
- "cache-control": "no-cache",
7183
- ...SECURITY_HEADERS
7184
- });
7185
- res.end(getDashboardHtml(config));
7186
- };
7187
- }
7188
- var VALID_TABS, SECURITY_HEADERS;
7189
- var init_router = __esm({
7190
- "src/dashboard/router.ts"() {
7211
+ // src/store/metrics/persistence.ts
7212
+ import { readFileSync as readFileSync4, existsSync as existsSync5, unlinkSync } from "fs";
7213
+ import { resolve as resolve3 } from "path";
7214
+ var FileMetricsPersistence;
7215
+ var init_persistence = __esm({
7216
+ "src/store/metrics/persistence.ts"() {
7191
7217
  "use strict";
7192
7218
  init_constants();
7193
- init_api();
7194
- init_insights();
7195
- init_findings();
7196
- init_sse();
7197
- init_page();
7198
- init_telemetry();
7199
- VALID_TABS = /* @__PURE__ */ new Set([
7200
- "overview",
7201
- "actions",
7202
- "requests",
7203
- "fetches",
7204
- "queries",
7205
- "errors",
7206
- "logs",
7207
- "performance",
7208
- "security"
7209
- ]);
7210
- SECURITY_HEADERS = {
7211
- "x-content-type-options": "nosniff",
7212
- "x-frame-options": "DENY",
7213
- "referrer-policy": "no-referrer"
7219
+ init_atomic_writer();
7220
+ init_log();
7221
+ FileMetricsPersistence = class {
7222
+ metricsPath;
7223
+ writer;
7224
+ constructor(rootDir) {
7225
+ this.metricsPath = resolve3(rootDir, METRICS_FILE);
7226
+ this.writer = new AtomicWriter({
7227
+ dir: resolve3(rootDir, METRICS_DIR),
7228
+ filePath: this.metricsPath,
7229
+ gitignoreEntry: METRICS_DIR,
7230
+ label: "metrics"
7231
+ });
7232
+ }
7233
+ load() {
7234
+ try {
7235
+ if (existsSync5(this.metricsPath)) {
7236
+ const raw = readFileSync4(this.metricsPath, "utf-8");
7237
+ const parsed = JSON.parse(raw);
7238
+ if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
7239
+ return parsed;
7240
+ }
7241
+ }
7242
+ } catch (err) {
7243
+ brakitWarn(`failed to load metrics: ${err.message}`);
7244
+ }
7245
+ return { version: 1, endpoints: [] };
7246
+ }
7247
+ save(data) {
7248
+ this.writer.writeAsync(JSON.stringify(data));
7249
+ }
7250
+ saveSync(data) {
7251
+ this.writer.writeSync(JSON.stringify(data));
7252
+ }
7253
+ remove() {
7254
+ try {
7255
+ if (existsSync5(this.metricsPath)) {
7256
+ unlinkSync(this.metricsPath);
7257
+ }
7258
+ } catch {
7259
+ }
7260
+ }
7214
7261
  };
7215
7262
  }
7216
7263
  });
7217
7264
 
7218
- // src/constants/severity.ts
7219
- var SEVERITY_ICON;
7220
- var init_severity = __esm({
7221
- "src/constants/severity.ts"() {
7265
+ // src/store/index.ts
7266
+ var init_store = __esm({
7267
+ "src/store/index.ts"() {
7222
7268
  "use strict";
7223
- SEVERITY_ICON = {
7224
- critical: "\u2717",
7225
- warning: "\u26A0",
7226
- info: "\u2139"
7227
- };
7269
+ init_request_store();
7270
+ init_telemetry_store();
7271
+ init_fetch_store();
7272
+ init_log_store();
7273
+ init_error_store();
7274
+ init_query_store();
7275
+ init_metrics_store();
7276
+ init_persistence();
7228
7277
  }
7229
7278
  });
7230
7279
 
@@ -7254,11 +7303,13 @@ function formatConsoleLine(insight, suffix) {
7254
7303
  }
7255
7304
  return line;
7256
7305
  }
7257
- function createConsoleInsightListener(proxyPort, metricsStore) {
7306
+ function startTerminalInsights(registry, proxyPort) {
7307
+ const bus = registry.get("event-bus");
7308
+ const metricsStore = registry.get("metrics-store");
7258
7309
  const printedKeys = /* @__PURE__ */ new Set();
7259
7310
  const resolvedKeys = /* @__PURE__ */ new Set();
7260
7311
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
7261
- return ({ statefulInsights }) => {
7312
+ return bus.on("analysis:updated", ({ statefulInsights }) => {
7262
7313
  const newLines = [];
7263
7314
  const resolvedLines = [];
7264
7315
  for (const si of statefulInsights) {
@@ -7300,7 +7351,7 @@ function createConsoleInsightListener(proxyPort, metricsStore) {
7300
7351
  print("");
7301
7352
  print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.green("Issues fixed!")}`);
7302
7353
  }
7303
- };
7354
+ });
7304
7355
  }
7305
7356
  var SEVERITY_COLOR;
7306
7357
  var init_terminal = __esm({
@@ -7394,10 +7445,11 @@ function outgoingToIncoming(headers) {
7394
7445
  }
7395
7446
  function decompress(body, encoding) {
7396
7447
  try {
7397
- if (encoding === "gzip") return gunzipSync(body);
7398
- if (encoding === "br") return brotliDecompressSync(body);
7399
- if (encoding === "deflate") return inflateSync(body);
7400
- } catch {
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}`);
7401
7453
  }
7402
7454
  return body;
7403
7455
  }
@@ -7407,7 +7459,7 @@ function toBuffer(chunk) {
7407
7459
  if (typeof chunk === "string") return Buffer.from(chunk);
7408
7460
  return null;
7409
7461
  }
7410
- function captureInProcess(req, res, requestId) {
7462
+ function captureInProcess(req, res, requestId, requestStore) {
7411
7463
  const startTime = performance.now();
7412
7464
  const method = req.method ?? "GET";
7413
7465
  const resChunks = [];
@@ -7424,7 +7476,8 @@ function captureInProcess(req, res, requestId) {
7424
7476
  resSize += buf.length;
7425
7477
  }
7426
7478
  }
7427
- } catch {
7479
+ } catch (e) {
7480
+ brakitDebug(`capture write: ${e.message}`);
7428
7481
  }
7429
7482
  return originalWrite.apply(this, args);
7430
7483
  };
@@ -7437,7 +7490,8 @@ function captureInProcess(req, res, requestId) {
7437
7490
  resChunks.push(buf);
7438
7491
  }
7439
7492
  }
7440
- } catch {
7493
+ } catch (e) {
7494
+ brakitDebug(`capture end: ${e.message}`);
7441
7495
  }
7442
7496
  const result = originalEnd.apply(this, args);
7443
7497
  const endTime = performance.now();
@@ -7447,7 +7501,7 @@ function captureInProcess(req, res, requestId) {
7447
7501
  if (body && encoding) {
7448
7502
  body = decompress(body, encoding);
7449
7503
  }
7450
- defaultStore.capture({
7504
+ requestStore.capture({
7451
7505
  requestId,
7452
7506
  method,
7453
7507
  url: req.url ?? "/",
@@ -7461,7 +7515,8 @@ function captureInProcess(req, res, requestId) {
7461
7515
  endTime,
7462
7516
  config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
7463
7517
  });
7464
- } catch {
7518
+ } catch (e) {
7519
+ brakitDebug(`capture store: ${e.message}`);
7465
7520
  }
7466
7521
  return result;
7467
7522
  };
@@ -7469,8 +7524,8 @@ function captureInProcess(req, res, requestId) {
7469
7524
  var init_capture = __esm({
7470
7525
  "src/runtime/capture.ts"() {
7471
7526
  "use strict";
7472
- init_request_log();
7473
7527
  init_constants();
7528
+ init_log();
7474
7529
  }
7475
7530
  });
7476
7531
 
@@ -7496,6 +7551,10 @@ function installInterceptor(deps) {
7496
7551
  deps.onFirstRequest(port);
7497
7552
  }
7498
7553
  }
7554
+ const localPort = req.socket.localPort;
7555
+ if (bannerPrinted && localPort && deps.config.proxyPort && localPort !== deps.config.proxyPort) {
7556
+ return original.apply(this, [event, ...args]);
7557
+ }
7499
7558
  if (isDashboardRequest(url)) {
7500
7559
  if (!isLocalRequest(req)) {
7501
7560
  res.writeHead(404);
@@ -7511,7 +7570,7 @@ function installInterceptor(deps) {
7511
7570
  url,
7512
7571
  method: req.method ?? "GET"
7513
7572
  };
7514
- captureInProcess(req, res, requestId);
7573
+ captureInProcess(req, res, requestId, deps.requestStore);
7515
7574
  return storage.run(
7516
7575
  ctx,
7517
7576
  () => original.apply(this, [event, ...args])
@@ -7543,91 +7602,115 @@ var setup_exports = {};
7543
7602
  __export(setup_exports, {
7544
7603
  setup: () => setup
7545
7604
  });
7546
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
7605
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
7547
7606
  import { resolve as resolve4 } from "path";
7548
7607
  function setup() {
7549
7608
  if (initialized) return;
7550
7609
  initialized = true;
7551
- setEmitter(routeEvent2);
7552
- setupFetchHook();
7553
- setupConsoleHook();
7554
- setupErrorHook();
7555
- const registry = createDefaultRegistry();
7556
- registry.patchAll(routeEvent2);
7610
+ const bus = new EventBus();
7611
+ const registry = new ServiceRegistry();
7612
+ const requestStore = new RequestStore();
7613
+ const fetchStore = new FetchStore();
7614
+ const logStore = new LogStore();
7615
+ const errorStore = new ErrorStore();
7616
+ const queryStore = new QueryStore();
7617
+ registry.register("event-bus", bus);
7618
+ registry.register("request-store", requestStore);
7619
+ registry.register("fetch-store", fetchStore);
7620
+ registry.register("log-store", logStore);
7621
+ registry.register("error-store", errorStore);
7622
+ registry.register("query-store", queryStore);
7623
+ bus.on("telemetry:fetch", (data) => fetchStore.add(data));
7624
+ bus.on("telemetry:query", (data) => queryStore.add(data));
7625
+ bus.on("telemetry:log", (data) => logStore.add(data));
7626
+ bus.on("telemetry:error", (data) => errorStore.add(data));
7627
+ requestStore.onRequest((req) => bus.emit("request:completed", req));
7628
+ const telemetryEmit = (event) => {
7629
+ const channel = `telemetry:${event.type}`;
7630
+ bus.emit(channel, event.data);
7631
+ };
7632
+ setupFetchHook(telemetryEmit);
7633
+ setupConsoleHook(telemetryEmit);
7634
+ setupErrorHook(telemetryEmit);
7635
+ const adapterRegistry = createDefaultRegistry();
7636
+ adapterRegistry.patchAll(telemetryEmit);
7557
7637
  const cwd = process.cwd();
7558
7638
  const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
7559
7639
  metricsStore.start();
7640
+ registry.register("metrics-store", metricsStore);
7560
7641
  const findingStore = new FindingStore(cwd);
7561
7642
  findingStore.start();
7562
- const analysisEngine = new AnalysisEngine(metricsStore, findingStore);
7643
+ registry.register("finding-store", findingStore);
7644
+ const analysisEngine = new AnalysisEngine(registry);
7563
7645
  analysisEngine.start();
7564
- const config = {
7565
- proxyPort: 0,
7566
- targetPort: 0,
7567
- showStatic: false,
7568
- maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
7569
- };
7570
- const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine, findingStore });
7571
- onRequest((req) => {
7572
- const queries = defaultQueryStore.getByRequest(req.id);
7573
- const fetches = defaultFetchStore.getByRequest(req.id);
7646
+ registry.register("analysis-engine", analysisEngine);
7647
+ bus.on("request:completed", (req) => {
7648
+ const queries = queryStore.getByRequest(req.id);
7649
+ const fetches = fetchStore.getByRequest(req.id);
7574
7650
  metricsStore.recordRequest(req, {
7575
7651
  queryCount: queries.length,
7576
7652
  queryTimeMs: queries.reduce((s, q) => s + q.durationMs, 0),
7577
7653
  fetchTimeMs: fetches.reduce((s, f) => s + f.durationMs, 0)
7578
7654
  });
7579
7655
  });
7656
+ const config = {
7657
+ proxyPort: 0,
7658
+ targetPort: 0,
7659
+ showStatic: false,
7660
+ maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
7661
+ };
7662
+ const handleDashboard = createDashboardHandler(registry);
7663
+ let terminalDispose = null;
7580
7664
  installInterceptor({
7581
7665
  handleDashboard,
7582
7666
  config,
7667
+ requestStore,
7583
7668
  onFirstRequest(port) {
7669
+ setBrakitPort(port);
7584
7670
  const dir = resolve4(cwd, METRICS_DIR);
7585
- if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
7586
- writeFileSync5(resolve4(cwd, PORT_FILE), String(port));
7587
- analysisEngine.onUpdate(createConsoleInsightListener(port, metricsStore));
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})`);
7677
+ }
7678
+ }
7679
+ writeFileSync4(portPath, String(port));
7680
+ terminalDispose = startTerminalInsights(registry, port);
7588
7681
  process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
7589
7682
  `);
7590
7683
  }
7591
7684
  });
7592
7685
  health.setTeardown(() => {
7593
7686
  uninstallInterceptor();
7687
+ terminalDispose?.();
7594
7688
  analysisEngine.stop();
7595
7689
  findingStore.stop();
7596
7690
  metricsStore.stop();
7597
7691
  try {
7598
7692
  const portPath = resolve4(cwd, PORT_FILE);
7599
- if (existsSync5(portPath)) unlinkSync2(portPath);
7693
+ if (existsSync6(portPath)) unlinkSync2(portPath);
7600
7694
  } catch {
7601
7695
  }
7602
7696
  });
7603
7697
  }
7604
- function routeEvent2(event) {
7605
- switch (event.type) {
7606
- case "fetch":
7607
- defaultFetchStore.add(event.data);
7608
- break;
7609
- case "log":
7610
- defaultLogStore.add(event.data);
7611
- break;
7612
- case "error":
7613
- defaultErrorStore.add(event.data);
7614
- break;
7615
- case "query":
7616
- defaultQueryStore.add(event.data);
7617
- break;
7618
- }
7619
- }
7620
7698
  var initialized;
7621
7699
  var init_setup = __esm({
7622
7700
  "src/runtime/setup.ts"() {
7623
7701
  "use strict";
7624
- init_transport2();
7625
7702
  init_fetch();
7626
7703
  init_console();
7627
7704
  init_errors();
7628
7705
  init_adapters();
7629
7706
  init_router();
7630
- init_request_log();
7707
+ init_event_bus();
7708
+ init_service_registry();
7709
+ init_request_store();
7710
+ init_fetch_store();
7711
+ init_log_store();
7712
+ init_error_store();
7713
+ init_query_store();
7631
7714
  init_store();
7632
7715
  init_finding_store();
7633
7716
  init_engine();
@@ -7636,6 +7719,7 @@ var init_setup = __esm({
7636
7719
  init_constants();
7637
7720
  init_health2();
7638
7721
  init_interceptor();
7722
+ init_log();
7639
7723
  initialized = false;
7640
7724
  }
7641
7725
  });