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