brakit 0.8.1 → 0.8.3
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.
- package/README.md +5 -5
- package/dist/api.d.ts +103 -44
- package/dist/api.js +153 -266
- package/dist/bin/brakit.js +130 -219
- package/dist/mcp/server.js +65 -23
- package/dist/runtime/index.js +1623 -1539
- package/package.json +1 -1
package/dist/runtime/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
285
|
+
path,
|
|
243
286
|
startTime: performance.now(),
|
|
244
|
-
parentRequestId: ctx
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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/
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
1293
|
-
|
|
1294
|
-
};
|
|
1295
|
-
defaultLogStore = new LogStore();
|
|
1227
|
+
init_constants();
|
|
1228
|
+
init_limits();
|
|
1296
1229
|
}
|
|
1297
1230
|
});
|
|
1298
1231
|
|
|
1299
|
-
// src/
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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/
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
};
|
|
1319
|
-
defaultQueryStore = new QueryStore();
|
|
1453
|
+
init_limits();
|
|
1454
|
+
init_shared2();
|
|
1320
1455
|
}
|
|
1321
1456
|
});
|
|
1322
1457
|
|
|
1323
|
-
// src/
|
|
1324
|
-
function
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
|
1331
|
-
"src/
|
|
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/
|
|
1337
|
-
function
|
|
1338
|
-
return
|
|
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
|
-
|
|
1341
|
-
|
|
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
|
|
1344
|
-
|
|
1345
|
-
"src/utils/endpoint.ts"() {
|
|
1537
|
+
var init_activity = __esm({
|
|
1538
|
+
"src/dashboard/api/activity.ts"() {
|
|
1346
1539
|
"use strict";
|
|
1347
|
-
|
|
1540
|
+
init_shared2();
|
|
1348
1541
|
}
|
|
1349
1542
|
});
|
|
1350
1543
|
|
|
1351
|
-
// src/
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2387
|
+
init_atomic_writer();
|
|
2791
2388
|
init_finding_id();
|
|
2792
2389
|
FindingStore = class {
|
|
2793
2390
|
constructor(rootDir) {
|
|
2794
2391
|
this.rootDir = rootDir;
|
|
2795
|
-
|
|
2796
|
-
this.findingsPath =
|
|
2797
|
-
this.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2904
|
-
|
|
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
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
4463
|
-
this.
|
|
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
|
-
|
|
4479
|
-
boundRequestListener;
|
|
4480
|
-
boundQueryListener;
|
|
4481
|
-
boundErrorListener;
|
|
4482
|
-
boundLogListener;
|
|
4067
|
+
subs = new SubscriptionBag();
|
|
4483
4068
|
start() {
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
4527
|
-
const queries =
|
|
4528
|
-
const errors =
|
|
4529
|
-
const logs =
|
|
4530
|
-
const fetches =
|
|
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.
|
|
4109
|
+
if (this.registry.has("finding-store")) {
|
|
4110
|
+
const findingStore = this.registry.get("finding-store");
|
|
4534
4111
|
for (const finding of this.cachedFindings) {
|
|
4535
|
-
|
|
4112
|
+
findingStore.upsert(finding, "passive");
|
|
4536
4113
|
}
|
|
4537
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
4150
|
+
VERSION = "0.8.3";
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
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('
|
|
6867
|
-
|
|
6868
|
-
state.
|
|
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('
|
|
6876
|
-
|
|
6877
|
-
state.
|
|
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
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
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
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
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
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
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
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
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
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
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
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
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
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
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
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
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
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
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
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
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
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
var
|
|
7007
|
-
"src/
|
|
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
|
-
|
|
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/
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
(
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
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
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
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
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
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/
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
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
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
6985
|
+
init_telemetry_store();
|
|
6986
|
+
ErrorStore = class extends TelemetryStore {
|
|
6987
|
+
};
|
|
7086
6988
|
}
|
|
7087
6989
|
});
|
|
7088
6990
|
|
|
7089
|
-
// src/
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
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
|
-
|
|
7103
|
-
|
|
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/
|
|
7117
|
-
import {
|
|
7118
|
-
function
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
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
|
|
7125
|
-
var
|
|
7126
|
-
"src/
|
|
7030
|
+
var MetricsStore;
|
|
7031
|
+
var init_metrics_store = __esm({
|
|
7032
|
+
"src/store/metrics/metrics-store.ts"() {
|
|
7127
7033
|
"use strict";
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
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/
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
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
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
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/
|
|
7219
|
-
var
|
|
7220
|
-
|
|
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
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
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
|
|
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 ===
|
|
7398
|
-
if (encoding ===
|
|
7399
|
-
if (encoding ===
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
const
|
|
7556
|
-
|
|
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
|
-
|
|
7643
|
+
registry.register("finding-store", findingStore);
|
|
7644
|
+
const analysisEngine = new AnalysisEngine(registry);
|
|
7563
7645
|
analysisEngine.start();
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
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 (!
|
|
7586
|
-
|
|
7587
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
});
|