brakit 0.8.7 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,10 +9,10 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/constants/limits.ts
13
- var PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_OBJECT_SCAN_DEPTH, ISSUE_PRUNE_TTL_MS;
14
- var init_limits = __esm({
15
- "src/constants/limits.ts"() {
12
+ // src/constants/config.ts
13
+ var PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_OBJECT_SCAN_DEPTH, ISSUE_PRUNE_TTL_MS, OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS, METRICS_DIR, PORT_FILE, VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES, TELEMETRY_EVENT_CLI_INVOKED, TELEMETRY_EVENT_CLI_UNINSTALL, DETAIL_PREVIEW_LENGTH;
14
+ var init_config = __esm({
15
+ "src/constants/config.ts"() {
16
16
  "use strict";
17
17
  PROJECT_HASH_LENGTH = 8;
18
18
  SECRET_SCAN_ARRAY_LIMIT = 5;
@@ -22,6 +22,16 @@ var init_limits = __esm({
22
22
  LIST_PII_MIN_ITEMS = 2;
23
23
  MAX_OBJECT_SCAN_DEPTH = 5;
24
24
  ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
25
+ OVERFETCH_UNWRAP_MIN_SIZE = 3;
26
+ STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
27
+ METRICS_DIR = ".brakit";
28
+ PORT_FILE = ".brakit/port";
29
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
30
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
31
+ VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
32
+ TELEMETRY_EVENT_CLI_INVOKED = "cli_invoked";
33
+ TELEMETRY_EVENT_CLI_UNINSTALL = "cli_uninstall";
34
+ DETAIL_PREVIEW_LENGTH = 120;
25
35
  }
26
36
  });
27
37
 
@@ -40,17 +50,6 @@ var init_log = __esm({
40
50
  }
41
51
  });
42
52
 
43
- // src/constants/lifecycle.ts
44
- var VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
45
- var init_lifecycle = __esm({
46
- "src/constants/lifecycle.ts"() {
47
- "use strict";
48
- VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
49
- VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
50
- VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
51
- }
52
- });
53
-
54
53
  // src/utils/type-guards.ts
55
54
  function isNonEmptyString(val) {
56
55
  return typeof val === "string" && val.trim().length > 0;
@@ -69,35 +68,14 @@ function isValidAiFixStatus(val) {
69
68
  var init_type_guards = __esm({
70
69
  "src/utils/type-guards.ts"() {
71
70
  "use strict";
72
- init_lifecycle();
73
- init_limits();
74
- }
75
- });
76
-
77
- // src/constants/metrics.ts
78
- var METRICS_DIR, PORT_FILE;
79
- var init_metrics = __esm({
80
- "src/constants/metrics.ts"() {
81
- "use strict";
82
- METRICS_DIR = ".brakit";
83
- PORT_FILE = ".brakit/port";
84
- }
85
- });
86
-
87
- // src/constants/thresholds.ts
88
- var OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS;
89
- var init_thresholds = __esm({
90
- "src/constants/thresholds.ts"() {
91
- "use strict";
92
- OVERFETCH_UNWRAP_MIN_SIZE = 3;
93
- STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
71
+ init_config();
94
72
  }
95
73
  });
96
74
 
97
- // src/constants/routes.ts
98
- 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;
99
- var init_routes = __esm({
100
- "src/constants/routes.ts"() {
75
+ // src/constants/labels.ts
76
+ 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, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS;
77
+ var init_labels = __esm({
78
+ "src/constants/labels.ts"() {
101
79
  "use strict";
102
80
  DASHBOARD_PREFIX = "/__brakit";
103
81
  DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
@@ -129,78 +107,16 @@ var init_routes = __esm({
129
107
  "security"
130
108
  ];
131
109
  VALID_TABS = new Set(VALID_TABS_TUPLE);
110
+ POSTHOG_HOST = "https://us.i.posthog.com";
111
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/";
112
+ POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
132
113
  }
133
114
  });
134
115
 
135
- // src/constants/transport.ts
136
- var init_transport = __esm({
137
- "src/constants/transport.ts"() {
138
- "use strict";
139
- }
140
- });
141
-
142
- // src/constants/headers.ts
143
- var init_headers = __esm({
144
- "src/constants/headers.ts"() {
145
- "use strict";
146
- }
147
- });
148
-
149
- // src/constants/network.ts
150
- var RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
151
- var init_network = __esm({
152
- "src/constants/network.ts"() {
153
- "use strict";
154
- RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
155
- PORT_MIN = 1;
156
- PORT_MAX = 65535;
157
- }
158
- });
159
-
160
- // src/constants/mcp.ts
161
- var MCP_SERVER_NAME, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_DISCOVERY_DEPTH, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER, MCP_SERVER_VERSION;
162
- var init_mcp = __esm({
163
- "src/constants/mcp.ts"() {
164
- "use strict";
165
- MCP_SERVER_NAME = "brakit";
166
- INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
167
- LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
168
- CLIENT_FETCH_TIMEOUT_MS = 1e4;
169
- HEALTH_CHECK_TIMEOUT_MS = 3e3;
170
- DISCOVERY_POLL_INTERVAL_MS = 500;
171
- MAX_DISCOVERY_DEPTH = 5;
172
- MAX_TIMELINE_EVENTS = 20;
173
- MAX_RESOLVED_DISPLAY = 5;
174
- ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
175
- MCP_SERVER_VERSION = "0.8.7";
176
- }
177
- });
178
-
179
- // src/constants/encoding.ts
180
- var init_encoding = __esm({
181
- "src/constants/encoding.ts"() {
182
- "use strict";
183
- }
184
- });
185
-
186
- // src/constants/severity.ts
187
- var init_severity = __esm({
188
- "src/constants/severity.ts"() {
189
- "use strict";
190
- }
191
- });
192
-
193
- // src/constants/telemetry.ts
194
- var init_telemetry = __esm({
195
- "src/constants/telemetry.ts"() {
196
- "use strict";
197
- }
198
- });
199
-
200
- // src/constants/cli.ts
201
- var SUPPORTED_SOURCE_EXTENSIONS, BUILD_CACHE_DIRS, FALLBACK_SCAN_DIRS;
202
- var init_cli = __esm({
203
- "src/constants/cli.ts"() {
116
+ // src/constants/features.ts
117
+ var SUPPORTED_SOURCE_EXTENSIONS, BUILD_CACHE_DIRS, FALLBACK_SCAN_DIRS, MCP_SERVER_NAME, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_DISCOVERY_DEPTH, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER, MCP_SERVER_VERSION, RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX, DIR_MODE_OWNER_ONLY, FILE_MODE_OWNER_ONLY;
118
+ var init_features = __esm({
119
+ "src/constants/features.ts"() {
204
120
  "use strict";
205
121
  SUPPORTED_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
206
122
  ".ts",
@@ -212,20 +128,22 @@ var init_cli = __esm({
212
128
  ]);
213
129
  BUILD_CACHE_DIRS = [".next", ".nuxt", ".output"];
214
130
  FALLBACK_SCAN_DIRS = ["src", "."];
215
- }
216
- });
217
-
218
- // src/constants/timeline.ts
219
- var init_timeline = __esm({
220
- "src/constants/timeline.ts"() {
221
- "use strict";
222
- }
223
- });
224
-
225
- // src/constants/sdk-events.ts
226
- var init_sdk_events = __esm({
227
- "src/constants/sdk-events.ts"() {
228
- "use strict";
131
+ MCP_SERVER_NAME = "brakit";
132
+ INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
133
+ LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
134
+ CLIENT_FETCH_TIMEOUT_MS = 1e4;
135
+ HEALTH_CHECK_TIMEOUT_MS = 3e3;
136
+ DISCOVERY_POLL_INTERVAL_MS = 500;
137
+ MAX_DISCOVERY_DEPTH = 5;
138
+ MAX_TIMELINE_EVENTS = 20;
139
+ MAX_RESOLVED_DISPLAY = 5;
140
+ ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
141
+ MCP_SERVER_VERSION = "0.9.1";
142
+ RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
143
+ PORT_MIN = 1;
144
+ PORT_MAX = 65535;
145
+ DIR_MODE_OWNER_ONLY = 448;
146
+ FILE_MODE_OWNER_ONLY = 384;
229
147
  }
230
148
  });
231
149
 
@@ -233,21 +151,9 @@ var init_sdk_events = __esm({
233
151
  var init_constants = __esm({
234
152
  "src/constants/index.ts"() {
235
153
  "use strict";
236
- init_routes();
237
- init_limits();
238
- init_thresholds();
239
- init_transport();
240
- init_metrics();
241
- init_headers();
242
- init_network();
243
- init_mcp();
244
- init_encoding();
245
- init_severity();
246
- init_telemetry();
247
- init_lifecycle();
248
- init_cli();
249
- init_timeline();
250
- init_sdk_events();
154
+ init_config();
155
+ init_labels();
156
+ init_features();
251
157
  }
252
158
  });
253
159
 
@@ -270,8 +176,8 @@ var BrakitClient;
270
176
  var init_client = __esm({
271
177
  "src/mcp/client.ts"() {
272
178
  "use strict";
273
- init_routes();
274
- init_mcp();
179
+ init_labels();
180
+ init_features();
275
181
  BrakitClient = class {
276
182
  constructor(baseUrl) {
277
183
  this.baseUrl = baseUrl;
@@ -409,8 +315,8 @@ async function searchForPort(startDir) {
409
315
  }
410
316
  return null;
411
317
  }
412
- async function discoverBrakitPort(cwd) {
413
- const port = await searchForPort(cwd ?? process.cwd());
318
+ async function discoverBrakitPort(cwd2) {
319
+ const port = await searchForPort(cwd2 ?? process.cwd());
414
320
  if (!port) {
415
321
  throw new Error(
416
322
  "Brakit is not running. Start your app with brakit enabled first."
@@ -418,11 +324,11 @@ async function discoverBrakitPort(cwd) {
418
324
  }
419
325
  return { port, baseUrl: `http://localhost:${port}` };
420
326
  }
421
- async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTERVAL_MS) {
327
+ async function waitForBrakit(cwd2, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTERVAL_MS) {
422
328
  const deadline = Date.now() + timeoutMs;
423
329
  while (Date.now() < deadline) {
424
330
  try {
425
- const result = await discoverBrakitPort(cwd);
331
+ const result = await discoverBrakitPort(cwd2);
426
332
  const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
427
333
  if (res.ok) return result;
428
334
  } catch {
@@ -438,7 +344,7 @@ var init_discovery = __esm({
438
344
  "use strict";
439
345
  init_constants();
440
346
  init_log();
441
- init_mcp();
347
+ init_features();
442
348
  }
443
349
  });
444
350
 
@@ -539,7 +445,7 @@ async function buildRequestDetail(client, req) {
539
445
  var init_enrichment = __esm({
540
446
  "src/mcp/enrichment.ts"() {
541
447
  "use strict";
542
- init_mcp();
448
+ init_features();
543
449
  init_endpoint();
544
450
  }
545
451
  });
@@ -550,7 +456,7 @@ var init_get_findings = __esm({
550
456
  "src/mcp/tools/get-findings.ts"() {
551
457
  "use strict";
552
458
  init_enrichment();
553
- init_lifecycle();
459
+ init_config();
554
460
  init_type_guards();
555
461
  getFindings = {
556
462
  name: "get_findings",
@@ -665,7 +571,7 @@ var getRequestDetail;
665
571
  var init_get_request_detail = __esm({
666
572
  "src/mcp/tools/get-request-detail.ts"() {
667
573
  "use strict";
668
- init_mcp();
574
+ init_features();
669
575
  init_enrichment();
670
576
  getRequestDetail = {
671
577
  name: "get_request_detail",
@@ -845,7 +751,7 @@ var getReport;
845
751
  var init_get_report = __esm({
846
752
  "src/mcp/tools/get-report.ts"() {
847
753
  "use strict";
848
- init_mcp();
754
+ init_features();
849
755
  getReport = {
850
756
  name: "get_report",
851
757
  description: "Generate a summary report of all findings: total found, open, resolved. Use this to get a high-level overview of the application's health.",
@@ -1142,13 +1048,15 @@ var init_server = __esm({
1142
1048
  init_client();
1143
1049
  init_discovery();
1144
1050
  init_tools();
1145
- init_mcp();
1051
+ init_features();
1146
1052
  init_prompts();
1147
1053
  }
1148
1054
  });
1149
1055
 
1150
1056
  // bin/brakit.ts
1151
1057
  import { runMain } from "citty";
1058
+ import { existsSync as existsSync7 } from "fs";
1059
+ import { resolve as resolve6 } from "path";
1152
1060
 
1153
1061
  // src/cli/commands/install.ts
1154
1062
  import { defineCommand } from "citty";
@@ -1164,7 +1072,7 @@ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync }
1164
1072
  import { resolve as resolve2 } from "path";
1165
1073
 
1166
1074
  // src/utils/fs.ts
1167
- init_limits();
1075
+ init_config();
1168
1076
  init_log();
1169
1077
  init_type_guards();
1170
1078
  import { access, readFile, writeFile } from "fs/promises";
@@ -1187,10 +1095,7 @@ async function fileExists(path) {
1187
1095
  }
1188
1096
 
1189
1097
  // src/store/issue-store.ts
1190
- init_metrics();
1191
- init_limits();
1192
- init_thresholds();
1193
- init_limits();
1098
+ init_config();
1194
1099
 
1195
1100
  // src/utils/atomic-writer.ts
1196
1101
  import {
@@ -1208,7 +1113,7 @@ init_log();
1208
1113
  init_type_guards();
1209
1114
 
1210
1115
  // src/utils/issue-id.ts
1211
- init_limits();
1116
+ init_config();
1212
1117
  import { createHash as createHash2 } from "crypto";
1213
1118
 
1214
1119
  // src/detect/project.ts
@@ -1378,12 +1283,13 @@ async function detectInDir(dir, rootDir, projects) {
1378
1283
  }
1379
1284
 
1380
1285
  // src/utils/response.ts
1381
- init_thresholds();
1286
+ init_config();
1287
+ var MAX_WRAPPER_KEYS = 3;
1382
1288
  function unwrapResponse(parsed) {
1383
1289
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1384
1290
  const obj = parsed;
1385
1291
  const keys = Object.keys(obj);
1386
- if (keys.length > 3) return parsed;
1292
+ if (keys.length > MAX_WRAPPER_KEYS) return parsed;
1387
1293
  let best = null;
1388
1294
  let bestSize = 0;
1389
1295
  for (const key of keys) {
@@ -1402,6 +1308,10 @@ function unwrapResponse(parsed) {
1402
1308
  return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1403
1309
  }
1404
1310
 
1311
+ // src/analysis/rules/scanner.ts
1312
+ init_log();
1313
+ init_type_guards();
1314
+
1405
1315
  // src/analysis/rules/patterns.ts
1406
1316
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1407
1317
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
@@ -1428,8 +1338,8 @@ var RULE_HINTS = {
1428
1338
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
1429
1339
  };
1430
1340
 
1431
- // src/analysis/rules/exposed-secret.ts
1432
- init_limits();
1341
+ // src/analysis/rules/auth-rules.ts
1342
+ init_config();
1433
1343
 
1434
1344
  // src/utils/http-status.ts
1435
1345
  function isErrorStatus(code) {
@@ -1439,27 +1349,66 @@ function isRedirect(code) {
1439
1349
  return code >= 300 && code < 400;
1440
1350
  }
1441
1351
 
1442
- // src/analysis/rules/exposed-secret.ts
1443
- function findSecretKeys(obj, prefix, depth = 0) {
1444
- const found = [];
1445
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
1446
- if (!obj || typeof obj !== "object") return found;
1447
- if (Array.isArray(obj)) {
1448
- for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
1449
- found.push(...findSecretKeys(obj[i], prefix, depth + 1));
1352
+ // src/utils/collections.ts
1353
+ function deduplicateFindings(items, extract) {
1354
+ const seen = /* @__PURE__ */ new Map();
1355
+ const findings = [];
1356
+ for (const item of items) {
1357
+ const result = extract(item);
1358
+ if (!result) continue;
1359
+ const existing = seen.get(result.key);
1360
+ if (existing) {
1361
+ existing.count++;
1362
+ continue;
1450
1363
  }
1451
- return found;
1364
+ seen.set(result.key, result.finding);
1365
+ findings.push(result.finding);
1452
1366
  }
1453
- for (const k of Object.keys(obj)) {
1454
- const val = obj[k];
1455
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
1456
- found.push(k);
1367
+ return findings;
1368
+ }
1369
+
1370
+ // src/utils/object-scan.ts
1371
+ init_config();
1372
+ var DEFAULTS = {
1373
+ maxDepth: MAX_OBJECT_SCAN_DEPTH,
1374
+ arrayLimit: SECRET_SCAN_ARRAY_LIMIT
1375
+ };
1376
+ function walkObject(obj, visitor, options) {
1377
+ const opts = { ...DEFAULTS, ...options };
1378
+ walk(obj, visitor, opts, 0);
1379
+ }
1380
+ function walk(obj, visitor, opts, depth) {
1381
+ if (depth >= opts.maxDepth) return;
1382
+ if (!obj || typeof obj !== "object") return;
1383
+ if (Array.isArray(obj)) {
1384
+ for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
1385
+ walk(obj[i], visitor, opts, depth + 1);
1457
1386
  }
1387
+ return;
1388
+ }
1389
+ for (const key of Object.keys(obj)) {
1390
+ const val = obj[key];
1391
+ visitor(key, val, depth);
1458
1392
  if (typeof val === "object" && val !== null) {
1459
- found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
1393
+ walk(val, visitor, opts, depth + 1);
1460
1394
  }
1461
1395
  }
1462
- return found;
1396
+ }
1397
+ function collectFromObject(obj, match, options) {
1398
+ const results = [];
1399
+ walkObject(obj, (key, value) => {
1400
+ const result = match(key, value);
1401
+ if (result !== null) results.push(result);
1402
+ }, options);
1403
+ return results;
1404
+ }
1405
+
1406
+ // src/analysis/rules/auth-rules.ts
1407
+ function findSecretKeys(obj) {
1408
+ return collectFromObject(
1409
+ obj,
1410
+ (key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
1411
+ );
1463
1412
  }
1464
1413
  var exposedSecretRule = {
1465
1414
  id: "exposed-secret",
@@ -1467,50 +1416,39 @@ var exposedSecretRule = {
1467
1416
  name: "Exposed Secret in Response",
1468
1417
  hint: RULE_HINTS["exposed-secret"],
1469
1418
  check(ctx) {
1470
- const findings = [];
1471
- const seen = /* @__PURE__ */ new Map();
1472
- for (const r of ctx.requests) {
1473
- if (isErrorStatus(r.statusCode)) continue;
1474
- const parsed = ctx.parsedBodies.response.get(r.id);
1475
- if (!parsed) continue;
1476
- const keys = findSecretKeys(parsed, "");
1477
- if (keys.length === 0) continue;
1478
- const ep = `${r.method} ${r.path}`;
1479
- const dedupKey = `${ep}:${keys.sort().join(",")}`;
1480
- const existing = seen.get(dedupKey);
1481
- if (existing) {
1482
- existing.count++;
1483
- continue;
1484
- }
1485
- const finding = {
1486
- severity: "critical",
1487
- rule: "exposed-secret",
1488
- title: "Exposed Secret in Response",
1489
- desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
1490
- hint: this.hint,
1491
- endpoint: ep,
1492
- count: 1
1419
+ return deduplicateFindings(ctx.requests, (request) => {
1420
+ if (isErrorStatus(request.statusCode)) return null;
1421
+ const parsed = ctx.parsedBodies.response.get(request.id);
1422
+ if (!parsed) return null;
1423
+ const keys = findSecretKeys(parsed);
1424
+ if (keys.length === 0) return null;
1425
+ const ep = `${request.method} ${request.path}`;
1426
+ return {
1427
+ key: `${ep}:${keys.sort().join(",")}`,
1428
+ finding: {
1429
+ severity: "critical",
1430
+ rule: "exposed-secret",
1431
+ title: "Exposed Secret in Response",
1432
+ desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
1433
+ hint: this.hint,
1434
+ detail: `Exposed fields: ${keys.join(", ")}. ${keys.length} unmasked secret value${keys.length !== 1 ? "s" : ""} in response body.`,
1435
+ endpoint: ep,
1436
+ count: 1
1437
+ }
1493
1438
  };
1494
- seen.set(dedupKey, finding);
1495
- findings.push(finding);
1496
- }
1497
- return findings;
1439
+ });
1498
1440
  }
1499
1441
  };
1500
-
1501
- // src/analysis/rules/token-in-url.ts
1502
1442
  var tokenInUrlRule = {
1503
1443
  id: "token-in-url",
1504
1444
  severity: "critical",
1505
1445
  name: "Auth Token in URL",
1506
1446
  hint: RULE_HINTS["token-in-url"],
1507
1447
  check(ctx) {
1508
- const findings = [];
1509
- const seen = /* @__PURE__ */ new Map();
1510
- for (const r of ctx.requests) {
1511
- const qIdx = r.url.indexOf("?");
1512
- if (qIdx === -1) continue;
1513
- const params = r.url.substring(qIdx + 1).split("&");
1448
+ return deduplicateFindings(ctx.requests, (request) => {
1449
+ const qIdx = request.url.indexOf("?");
1450
+ if (qIdx === -1) return null;
1451
+ const params = request.url.substring(qIdx + 1).split("&");
1514
1452
  const flagged = [];
1515
1453
  for (const param of params) {
1516
1454
  const [name, ...rest] = param.split("=");
@@ -1520,65 +1458,129 @@ var tokenInUrlRule = {
1520
1458
  flagged.push(name);
1521
1459
  }
1522
1460
  }
1523
- if (flagged.length === 0) continue;
1524
- const ep = `${r.method} ${r.path}`;
1525
- const dedupKey = `${ep}:${flagged.sort().join(",")}`;
1526
- const existing = seen.get(dedupKey);
1527
- if (existing) {
1528
- existing.count++;
1529
- continue;
1461
+ if (flagged.length === 0) return null;
1462
+ const ep = `${request.method} ${request.path}`;
1463
+ return {
1464
+ key: `${ep}:${flagged.sort().join(",")}`,
1465
+ finding: {
1466
+ severity: "critical",
1467
+ rule: "token-in-url",
1468
+ title: "Auth Token in URL",
1469
+ desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
1470
+ hint: this.hint,
1471
+ detail: `Parameters in URL: ${flagged.join(", ")}. Auth tokens in URLs are logged by proxies, browsers, and CDNs.`,
1472
+ endpoint: ep,
1473
+ count: 1
1474
+ }
1475
+ };
1476
+ });
1477
+ }
1478
+ };
1479
+ function isFrameworkResponse(request) {
1480
+ if (isRedirect(request.statusCode)) return true;
1481
+ if (request.path?.startsWith("/__")) return true;
1482
+ if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
1483
+ return false;
1484
+ }
1485
+ var insecureCookieRule = {
1486
+ id: "insecure-cookie",
1487
+ severity: "warning",
1488
+ name: "Insecure Cookie",
1489
+ hint: RULE_HINTS["insecure-cookie"],
1490
+ check(ctx) {
1491
+ const cookieEntries = [];
1492
+ for (const request of ctx.requests) {
1493
+ if (!request.responseHeaders) continue;
1494
+ if (isFrameworkResponse(request)) continue;
1495
+ const setCookie = request.responseHeaders["set-cookie"];
1496
+ if (!setCookie) continue;
1497
+ const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
1498
+ for (const cookie of cookies) {
1499
+ cookieEntries.push({ cookie });
1530
1500
  }
1531
- const finding = {
1532
- severity: "critical",
1533
- rule: "token-in-url",
1534
- title: "Auth Token in URL",
1535
- desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
1501
+ }
1502
+ return deduplicateFindings(cookieEntries, ({ cookie }) => {
1503
+ const cookieName = cookie.trim().split("=")[0].trim();
1504
+ const lower = cookie.toLowerCase();
1505
+ const issues = [];
1506
+ if (!lower.includes("httponly")) issues.push("HttpOnly");
1507
+ if (!lower.includes("samesite")) issues.push("SameSite");
1508
+ if (issues.length === 0) return null;
1509
+ return {
1510
+ key: `${cookieName}:${issues.join(",")}`,
1511
+ finding: {
1512
+ severity: "warning",
1513
+ rule: "insecure-cookie",
1514
+ title: "Insecure Cookie",
1515
+ desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
1516
+ hint: this.hint,
1517
+ detail: `Missing: ${issues.join(", ")}. ${issues.includes("HttpOnly") ? "Cookie accessible via JavaScript (XSS risk). " : ""}${issues.includes("SameSite") ? "Cookie sent on cross-site requests (CSRF risk)." : ""}`,
1518
+ endpoint: cookieName,
1519
+ count: 1
1520
+ }
1521
+ };
1522
+ });
1523
+ }
1524
+ };
1525
+ var corsCredentialsRule = {
1526
+ id: "cors-credentials",
1527
+ severity: "warning",
1528
+ name: "CORS Credentials with Wildcard",
1529
+ hint: RULE_HINTS["cors-credentials"],
1530
+ check(ctx) {
1531
+ const findings = [];
1532
+ const seen = /* @__PURE__ */ new Set();
1533
+ for (const request of ctx.requests) {
1534
+ if (!request.responseHeaders) continue;
1535
+ const origin = request.responseHeaders["access-control-allow-origin"];
1536
+ const creds = request.responseHeaders["access-control-allow-credentials"];
1537
+ if (origin !== "*" || creds !== "true") continue;
1538
+ const ep = `${request.method} ${request.path}`;
1539
+ if (seen.has(ep)) continue;
1540
+ seen.add(ep);
1541
+ findings.push({
1542
+ severity: "warning",
1543
+ rule: "cors-credentials",
1544
+ title: "CORS Credentials with Wildcard",
1545
+ desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
1536
1546
  hint: this.hint,
1537
1547
  endpoint: ep,
1538
1548
  count: 1
1539
- };
1540
- seen.set(dedupKey, finding);
1541
- findings.push(finding);
1549
+ });
1542
1550
  }
1543
1551
  return findings;
1544
1552
  }
1545
1553
  };
1546
1554
 
1547
- // src/analysis/rules/stack-trace-leak.ts
1555
+ // src/analysis/rules/data-rules.ts
1556
+ init_config();
1548
1557
  var stackTraceLeakRule = {
1549
1558
  id: "stack-trace-leak",
1550
1559
  severity: "critical",
1551
1560
  name: "Stack Trace Leaked to Client",
1552
1561
  hint: RULE_HINTS["stack-trace-leak"],
1553
1562
  check(ctx) {
1554
- const findings = [];
1555
- const seen = /* @__PURE__ */ new Map();
1556
- for (const r of ctx.requests) {
1557
- if (!r.responseBody) continue;
1558
- if (!STACK_TRACE_RE.test(r.responseBody)) continue;
1559
- const ep = `${r.method} ${r.path}`;
1560
- const existing = seen.get(ep);
1561
- if (existing) {
1562
- existing.count++;
1563
- continue;
1564
- }
1565
- const finding = {
1566
- severity: "critical",
1567
- rule: "stack-trace-leak",
1568
- title: "Stack Trace Leaked to Client",
1569
- desc: `${ep} \u2014 response exposes internal stack trace`,
1570
- hint: this.hint,
1571
- endpoint: ep,
1572
- count: 1
1563
+ return deduplicateFindings(ctx.requests, (request) => {
1564
+ if (!request.responseBody) return null;
1565
+ if (!STACK_TRACE_RE.test(request.responseBody)) return null;
1566
+ const ep = `${request.method} ${request.path}`;
1567
+ const firstLine = request.responseBody.split("\n").find((l) => STACK_TRACE_RE.test(l))?.trim() ?? "";
1568
+ return {
1569
+ key: ep,
1570
+ finding: {
1571
+ severity: "critical",
1572
+ rule: "stack-trace-leak",
1573
+ title: "Stack Trace Leaked to Client",
1574
+ desc: `${ep} \u2014 response exposes internal stack trace`,
1575
+ hint: this.hint,
1576
+ detail: firstLine ? `Stack trace: ${firstLine.slice(0, DETAIL_PREVIEW_LENGTH)}` : void 0,
1577
+ endpoint: ep,
1578
+ count: 1
1579
+ }
1573
1580
  };
1574
- seen.set(ep, finding);
1575
- findings.push(finding);
1576
- }
1577
- return findings;
1581
+ });
1578
1582
  }
1579
1583
  };
1580
-
1581
- // src/analysis/rules/error-info-leak.ts
1582
1584
  var CRITICAL_PATTERNS = [
1583
1585
  { re: DB_CONN_RE, label: "database connection string" },
1584
1586
  { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
@@ -1590,90 +1592,35 @@ var errorInfoLeakRule = {
1590
1592
  name: "Sensitive Data in Error Response",
1591
1593
  hint: RULE_HINTS["error-info-leak"],
1592
1594
  check(ctx) {
1593
- const findings = [];
1594
- const seen = /* @__PURE__ */ new Map();
1595
- for (const r of ctx.requests) {
1596
- if (r.statusCode < 400) continue;
1597
- if (!r.responseBody) continue;
1598
- if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
1599
- const ep = `${r.method} ${r.path}`;
1600
- for (const p of CRITICAL_PATTERNS) {
1601
- if (!p.re.test(r.responseBody)) continue;
1602
- const dedupKey = `${ep}:${p.label}`;
1603
- const existing = seen.get(dedupKey);
1604
- if (existing) {
1605
- existing.count++;
1606
- continue;
1595
+ const entries = [];
1596
+ for (const request of ctx.requests) {
1597
+ if (request.statusCode < 400) continue;
1598
+ if (!request.responseBody) continue;
1599
+ if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
1600
+ const ep = `${request.method} ${request.path}`;
1601
+ for (const pattern of CRITICAL_PATTERNS) {
1602
+ if (pattern.re.test(request.responseBody)) {
1603
+ entries.push({ ep, pattern, body: request.responseBody });
1607
1604
  }
1608
- const finding = {
1605
+ }
1606
+ }
1607
+ return deduplicateFindings(entries, ({ ep, pattern }) => {
1608
+ return {
1609
+ key: `${ep}:${pattern.label}`,
1610
+ finding: {
1609
1611
  severity: "critical",
1610
1612
  rule: "error-info-leak",
1611
1613
  title: "Sensitive Data in Error Response",
1612
- desc: `${ep} \u2014 error response exposes ${p.label}`,
1614
+ desc: `${ep} \u2014 error response exposes ${pattern.label}`,
1613
1615
  hint: this.hint,
1616
+ detail: `Detected: ${pattern.label} in error response body`,
1614
1617
  endpoint: ep,
1615
1618
  count: 1
1616
- };
1617
- seen.set(dedupKey, finding);
1618
- findings.push(finding);
1619
- }
1620
- }
1621
- return findings;
1622
- }
1623
- };
1624
-
1625
- // src/analysis/rules/insecure-cookie.ts
1626
- function isFrameworkResponse(r) {
1627
- if (isRedirect(r.statusCode)) return true;
1628
- if (r.path?.startsWith("/__")) return true;
1629
- if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
1630
- return false;
1631
- }
1632
- var insecureCookieRule = {
1633
- id: "insecure-cookie",
1634
- severity: "warning",
1635
- name: "Insecure Cookie",
1636
- hint: RULE_HINTS["insecure-cookie"],
1637
- check(ctx) {
1638
- const findings = [];
1639
- const seen = /* @__PURE__ */ new Map();
1640
- for (const r of ctx.requests) {
1641
- if (!r.responseHeaders) continue;
1642
- if (isFrameworkResponse(r)) continue;
1643
- const setCookie = r.responseHeaders["set-cookie"];
1644
- if (!setCookie) continue;
1645
- const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
1646
- for (const cookie of cookies) {
1647
- const cookieName = cookie.trim().split("=")[0].trim();
1648
- const lower = cookie.toLowerCase();
1649
- const issues = [];
1650
- if (!lower.includes("httponly")) issues.push("HttpOnly");
1651
- if (!lower.includes("samesite")) issues.push("SameSite");
1652
- if (issues.length === 0) continue;
1653
- const dedupKey = `${cookieName}:${issues.join(",")}`;
1654
- const existing = seen.get(dedupKey);
1655
- if (existing) {
1656
- existing.count++;
1657
- continue;
1658
1619
  }
1659
- const finding = {
1660
- severity: "warning",
1661
- rule: "insecure-cookie",
1662
- title: "Insecure Cookie",
1663
- desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
1664
- hint: this.hint,
1665
- endpoint: cookieName,
1666
- count: 1
1667
- };
1668
- seen.set(dedupKey, finding);
1669
- findings.push(finding);
1670
- }
1671
- }
1672
- return findings;
1620
+ };
1621
+ });
1673
1622
  }
1674
1623
  };
1675
-
1676
- // src/analysis/rules/sensitive-logs.ts
1677
1624
  var sensitiveLogsRule = {
1678
1625
  id: "sensitive-logs",
1679
1626
  severity: "warning",
@@ -1698,59 +1645,13 @@ var sensitiveLogsRule = {
1698
1645
  }];
1699
1646
  }
1700
1647
  };
1701
-
1702
- // src/analysis/rules/cors-credentials.ts
1703
- var corsCredentialsRule = {
1704
- id: "cors-credentials",
1705
- severity: "warning",
1706
- name: "CORS Credentials with Wildcard",
1707
- hint: RULE_HINTS["cors-credentials"],
1708
- check(ctx) {
1709
- const findings = [];
1710
- const seen = /* @__PURE__ */ new Set();
1711
- for (const r of ctx.requests) {
1712
- if (!r.responseHeaders) continue;
1713
- const origin = r.responseHeaders["access-control-allow-origin"];
1714
- const creds = r.responseHeaders["access-control-allow-credentials"];
1715
- if (origin !== "*" || creds !== "true") continue;
1716
- const ep = `${r.method} ${r.path}`;
1717
- if (seen.has(ep)) continue;
1718
- seen.add(ep);
1719
- findings.push({
1720
- severity: "warning",
1721
- rule: "cors-credentials",
1722
- title: "CORS Credentials with Wildcard",
1723
- desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
1724
- hint: this.hint,
1725
- endpoint: ep,
1726
- count: 1
1727
- });
1728
- }
1729
- return findings;
1730
- }
1731
- };
1732
-
1733
- // src/analysis/rules/response-pii-leak.ts
1734
- init_limits();
1735
1648
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1736
- function findEmails(obj, depth = 0) {
1737
- const emails = [];
1738
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
1739
- if (!obj || typeof obj !== "object") return emails;
1740
- if (Array.isArray(obj)) {
1741
- for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
1742
- emails.push(...findEmails(obj[i], depth + 1));
1743
- }
1744
- return emails;
1745
- }
1746
- for (const v of Object.values(obj)) {
1747
- if (typeof v === "string" && EMAIL_RE.test(v)) {
1748
- emails.push(v);
1749
- } else if (typeof v === "object" && v !== null) {
1750
- emails.push(...findEmails(v, depth + 1));
1751
- }
1752
- }
1753
- return emails;
1649
+ function findEmails(obj) {
1650
+ return collectFromObject(
1651
+ obj,
1652
+ (_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
1653
+ { arrayLimit: PII_SCAN_ARRAY_LIMIT }
1654
+ );
1754
1655
  }
1755
1656
  function topLevelFieldCount(obj) {
1756
1657
  if (Array.isArray(obj)) {
@@ -1835,101 +1736,83 @@ var responsePiiLeakRule = {
1835
1736
  name: "PII Leak in Response",
1836
1737
  hint: RULE_HINTS["response-pii-leak"],
1837
1738
  check(ctx) {
1838
- const findings = [];
1839
- const seen = /* @__PURE__ */ new Map();
1840
- for (const r of ctx.requests) {
1841
- if (isErrorStatus(r.statusCode)) continue;
1842
- if (SELF_SERVICE_PATH.test(r.path)) continue;
1843
- const resJson = ctx.parsedBodies.response.get(r.id);
1844
- if (!resJson) continue;
1845
- const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
1846
- const detection = detectPII(r.method, reqJson, resJson);
1847
- if (!detection) continue;
1848
- const ep = `${r.method} ${r.path}`;
1849
- const existing = seen.get(ep);
1850
- if (existing) {
1851
- existing.count++;
1852
- continue;
1853
- }
1854
- const finding = {
1855
- severity: "warning",
1856
- rule: "response-pii-leak",
1857
- title: "PII Leak in Response",
1858
- desc: `${ep} \u2014 exposes PII in response`,
1859
- hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
1860
- endpoint: ep,
1861
- count: 1
1739
+ return deduplicateFindings(ctx.requests, (request) => {
1740
+ if (isErrorStatus(request.statusCode)) return null;
1741
+ if (SELF_SERVICE_PATH.test(request.path)) return null;
1742
+ const resJson = ctx.parsedBodies.response.get(request.id);
1743
+ if (!resJson) return null;
1744
+ const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
1745
+ const detection = detectPII(request.method, reqJson, resJson);
1746
+ if (!detection) return null;
1747
+ const ep = `${request.method} ${request.path}`;
1748
+ const fieldCount = topLevelFieldCount(resJson);
1749
+ const detailParts = [`Pattern: ${REASON_LABELS[detection.reason]}`];
1750
+ if (detection.emailCount > 0) detailParts.push(`${detection.emailCount} email${detection.emailCount !== 1 ? "s" : ""} detected`);
1751
+ if (fieldCount > 0) detailParts.push(`${fieldCount} fields per record`);
1752
+ return {
1753
+ key: ep,
1754
+ finding: {
1755
+ severity: "warning",
1756
+ rule: "response-pii-leak",
1757
+ title: "PII Leak in Response",
1758
+ desc: `${ep} \u2014 exposes PII in response`,
1759
+ hint: this.hint,
1760
+ detail: detailParts.join(". "),
1761
+ endpoint: ep,
1762
+ count: 1
1763
+ }
1862
1764
  };
1863
- seen.set(ep, finding);
1864
- findings.push(finding);
1865
- }
1866
- return findings;
1765
+ });
1867
1766
  }
1868
1767
  };
1869
1768
 
1870
1769
  // src/analysis/engine.ts
1871
- init_limits();
1770
+ init_config();
1872
1771
 
1873
1772
  // src/analysis/group.ts
1874
1773
  init_constants();
1875
1774
  import { randomUUID } from "crypto";
1775
+ init_endpoint();
1876
1776
 
1877
1777
  // src/analysis/label.ts
1878
1778
  init_constants();
1879
1779
 
1880
1780
  // src/analysis/transforms.ts
1881
1781
  init_constants();
1782
+ init_config();
1783
+ init_endpoint();
1882
1784
 
1883
1785
  // src/analysis/insights/prepare.ts
1884
1786
  init_endpoint();
1885
1787
  init_constants();
1886
- init_thresholds();
1788
+ init_config();
1887
1789
 
1888
- // src/analysis/insights/rules/n1.ts
1889
- init_endpoint();
1890
- init_constants();
1790
+ // src/analysis/insights/runner.ts
1791
+ init_log();
1792
+ init_type_guards();
1891
1793
 
1892
- // src/analysis/insights/rules/cross-endpoint.ts
1794
+ // src/analysis/insights/rules/query-rules.ts
1893
1795
  init_endpoint();
1894
1796
  init_constants();
1895
1797
 
1896
- // src/analysis/insights/rules/redundant-query.ts
1798
+ // src/analysis/insights/rules/response-rules.ts
1897
1799
  init_endpoint();
1800
+ init_log();
1801
+ init_type_guards();
1898
1802
  init_constants();
1899
1803
 
1900
- // src/analysis/insights/rules/error-hotspot.ts
1901
- init_constants();
1902
-
1903
- // src/analysis/insights/rules/duplicate.ts
1904
- init_constants();
1905
-
1906
- // src/analysis/insights/rules/slow.ts
1907
- init_constants();
1908
-
1909
- // src/analysis/insights/rules/query-heavy.ts
1910
- init_constants();
1911
-
1912
- // src/analysis/insights/rules/select-star.ts
1913
- init_constants();
1914
-
1915
- // src/analysis/insights/rules/high-rows.ts
1804
+ // src/analysis/insights/rules/reliability-rules.ts
1916
1805
  init_constants();
1917
1806
 
1918
- // src/analysis/insights/rules/response-overfetch.ts
1807
+ // src/analysis/insights/rules/pattern-rules.ts
1919
1808
  init_endpoint();
1920
1809
  init_constants();
1921
1810
 
1922
- // src/analysis/insights/rules/large-response.ts
1923
- init_constants();
1924
-
1925
- // src/analysis/insights/rules/regression.ts
1926
- init_constants();
1927
-
1928
1811
  // src/analysis/issue-mappers.ts
1929
1812
  init_endpoint();
1930
1813
 
1931
1814
  // src/index.ts
1932
- var VERSION = "0.8.7";
1815
+ var VERSION = "0.9.1";
1933
1816
 
1934
1817
  // src/cli/commands/install.ts
1935
1818
  init_constants();
@@ -2141,8 +2024,8 @@ async function setupNuxt(rootDir) {
2141
2024
  }
2142
2025
  const content = BRAKIT_TEMPLATES.nuxt + "\n";
2143
2026
  const dir = join3(rootDir, "server/plugins");
2144
- const { mkdirSync: mkdirSync3 } = await import("fs");
2145
- mkdirSync3(dir, { recursive: true });
2027
+ const { mkdirSync: mkdirSync4 } = await import("fs");
2028
+ mkdirSync4(dir, { recursive: true });
2146
2029
  await writeFile3(absPath, content);
2147
2030
  return { action: "created", file: relPath, content };
2148
2031
  }
@@ -2241,13 +2124,118 @@ function printManualInstructions(framework) {
2241
2124
 
2242
2125
  // src/cli/commands/uninstall.ts
2243
2126
  import { defineCommand as defineCommand2 } from "citty";
2244
- import { resolve as resolve4, join as join4, relative as relative2 } from "path";
2127
+ import { resolve as resolve4, join as join5, relative as relative2 } from "path";
2245
2128
  import { readFile as readFile5, writeFile as writeFile4, unlink, rm, readdir as readdir2 } from "fs/promises";
2246
2129
  import { execSync as execSync2 } from "child_process";
2247
2130
  import pc2 from "picocolors";
2248
2131
  init_constants();
2249
2132
  init_log();
2250
2133
  init_type_guards();
2134
+
2135
+ // src/telemetry/index.ts
2136
+ import { platform as platform2, release, arch } from "os";
2137
+ import { spawn } from "child_process";
2138
+
2139
+ // src/telemetry/config.ts
2140
+ init_features();
2141
+ import { homedir as homedir2, platform } from "os";
2142
+ import { join as join4 } from "path";
2143
+ import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
2144
+ import { randomUUID as randomUUID2 } from "crypto";
2145
+ var IS_WINDOWS = platform() === "win32";
2146
+ var CONFIG_DIR = join4(homedir2(), ".brakit");
2147
+ var CONFIG_PATH = join4(CONFIG_DIR, "config.json");
2148
+ function readConfig() {
2149
+ try {
2150
+ if (!existsSync6(CONFIG_PATH)) return null;
2151
+ return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
2152
+ } catch {
2153
+ return null;
2154
+ }
2155
+ }
2156
+ function writeConfig(config) {
2157
+ try {
2158
+ if (!existsSync6(CONFIG_DIR))
2159
+ mkdirSync3(CONFIG_DIR, { recursive: true, ...IS_WINDOWS ? {} : { mode: DIR_MODE_OWNER_ONLY } });
2160
+ writeFileSync3(
2161
+ CONFIG_PATH,
2162
+ JSON.stringify(config, null, 2) + "\n",
2163
+ IS_WINDOWS ? {} : { mode: FILE_MODE_OWNER_ONLY }
2164
+ );
2165
+ } catch (err) {
2166
+ if (process.env.BRAKIT_DEBUG) {
2167
+ process.stderr.write(`[brakit] config write failed: ${err?.message ?? err}
2168
+ `);
2169
+ }
2170
+ }
2171
+ }
2172
+ function getOrCreateConfig() {
2173
+ const existing = readConfig();
2174
+ if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
2175
+ return existing;
2176
+ }
2177
+ const config = { telemetry: true, anonymousId: randomUUID2() };
2178
+ writeConfig(config);
2179
+ return config;
2180
+ }
2181
+ var cachedEnabled = null;
2182
+ function isTelemetryEnabled() {
2183
+ if (cachedEnabled !== null) return cachedEnabled;
2184
+ const env = process.env.BRAKIT_TELEMETRY;
2185
+ if (env !== void 0) {
2186
+ cachedEnabled = env !== "false" && env !== "0" && env !== "off";
2187
+ return cachedEnabled;
2188
+ }
2189
+ cachedEnabled = readConfig()?.telemetry ?? true;
2190
+ return cachedEnabled;
2191
+ }
2192
+
2193
+ // src/telemetry/index.ts
2194
+ init_labels();
2195
+ init_config();
2196
+ var POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
2197
+ function commonProperties() {
2198
+ return {
2199
+ brakit_version: VERSION,
2200
+ node_version: process.version,
2201
+ os: `${platform2()}-${release()}`,
2202
+ arch: arch(),
2203
+ $lib: "brakit",
2204
+ $process_person_profile: false,
2205
+ $geoip_disable: true
2206
+ };
2207
+ }
2208
+ function sendToPosthog(event, properties) {
2209
+ if (!isTelemetryEnabled()) return;
2210
+ const config = getOrCreateConfig();
2211
+ const payload = {
2212
+ api_key: POSTHOG_KEY,
2213
+ event,
2214
+ distinct_id: config.anonymousId,
2215
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2216
+ properties: { ...commonProperties(), ...properties }
2217
+ };
2218
+ try {
2219
+ const body = JSON.stringify(payload);
2220
+ const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
2221
+ const child = spawn(
2222
+ process.execPath,
2223
+ [
2224
+ "-e",
2225
+ `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
2226
+ ],
2227
+ { detached: true, stdio: "ignore" }
2228
+ );
2229
+ child.unref();
2230
+ } catch {
2231
+ }
2232
+ }
2233
+ function trackEvent(event, properties) {
2234
+ sendToPosthog(event, { sdk: "node", ...properties });
2235
+ }
2236
+
2237
+ // src/cli/commands/uninstall.ts
2238
+ init_config();
2251
2239
  var PREPENDED_FILES = [
2252
2240
  "app/entry.server.tsx",
2253
2241
  "app/entry.server.ts",
@@ -2282,16 +2270,20 @@ var uninstall_default = defineCommand2({
2282
2270
  console.log();
2283
2271
  console.log(pc2.bold(" \u25C6 brakit uninstall"));
2284
2272
  console.log();
2273
+ let anyInstrumentationRemoved = false;
2274
+ let anyPackageRemoved = false;
2285
2275
  for (const project of projects) {
2286
2276
  const suffix = projects.length > 1 ? ` in ${relative2(rootDir, project.dir) || "."}` : "";
2287
2277
  const removed = await removeInstrumentation(project.dir);
2288
2278
  if (removed) {
2279
+ anyInstrumentationRemoved = true;
2289
2280
  console.log(pc2.green(` \u2713 ${removed}${suffix}`));
2290
2281
  } else {
2291
2282
  console.log(pc2.dim(` No brakit instrumentation files found${suffix}.`));
2292
2283
  }
2293
2284
  const uninstalled = await uninstallPackage(project.dir, project.pm);
2294
2285
  if (uninstalled === true) {
2286
+ anyPackageRemoved = true;
2295
2287
  console.log(pc2.green(` \u2713 Removed brakit from devDependencies${suffix}`));
2296
2288
  } else if (uninstalled === "failed") {
2297
2289
  }
@@ -2312,6 +2304,12 @@ var uninstall_default = defineCommand2({
2312
2304
  if (cacheCleared) {
2313
2305
  console.log(pc2.green(" \u2713 Cleared build cache"));
2314
2306
  }
2307
+ trackEvent(TELEMETRY_EVENT_CLI_UNINSTALL, {
2308
+ instrumentation_removed: anyInstrumentationRemoved,
2309
+ package_removed: anyPackageRemoved,
2310
+ mcp_removed: mcpRemoved,
2311
+ data_removed: dataRemoved
2312
+ });
2315
2313
  console.log();
2316
2314
  }
2317
2315
  });
@@ -2322,7 +2320,7 @@ async function removeInstrumentation(projectDir) {
2322
2320
  }
2323
2321
  const candidates = [...PREPENDED_FILES];
2324
2322
  try {
2325
- const pkgRaw = await readFile5(join4(projectDir, "package.json"), "utf-8");
2323
+ const pkgRaw = await readFile5(join5(projectDir, "package.json"), "utf-8");
2326
2324
  const pkg = JSON.parse(pkgRaw);
2327
2325
  if (pkg.main) candidates.unshift(pkg.main);
2328
2326
  } catch (err) {
@@ -2337,7 +2335,7 @@ async function removeInstrumentation(projectDir) {
2337
2335
  return null;
2338
2336
  }
2339
2337
  async function tryRemoveBrakitFromFile(projectDir, relPath) {
2340
- const absPath = join4(projectDir, relPath);
2338
+ const absPath = join5(projectDir, relPath);
2341
2339
  if (!await fileExists(absPath)) return null;
2342
2340
  const content = await readFile5(absPath, "utf-8");
2343
2341
  if (!content.includes("brakit")) return null;
@@ -2354,7 +2352,7 @@ async function tryRemoveBrakitFromFile(projectDir, relPath) {
2354
2352
  return null;
2355
2353
  }
2356
2354
  async function tryRemoveImportLine(projectDir, relPath) {
2357
- const absPath = join4(projectDir, relPath);
2355
+ const absPath = join5(projectDir, relPath);
2358
2356
  if (!await fileExists(absPath)) return null;
2359
2357
  const content = await readFile5(absPath, "utf-8");
2360
2358
  if (!content.includes(IMPORT_LINE)) return null;
@@ -2365,7 +2363,7 @@ async function tryRemoveImportLine(projectDir, relPath) {
2365
2363
  async function fallbackSearchAndRemove(projectDir) {
2366
2364
  const dirsToScan = FALLBACK_SCAN_DIRS;
2367
2365
  for (const dir of dirsToScan) {
2368
- const absDir = join4(projectDir, dir);
2366
+ const absDir = join5(projectDir, dir);
2369
2367
  if (!await fileExists(absDir)) continue;
2370
2368
  let entries;
2371
2369
  try {
@@ -2378,7 +2376,7 @@ async function fallbackSearchAndRemove(projectDir) {
2378
2376
  const ext = entry.slice(entry.lastIndexOf("."));
2379
2377
  if (!SUPPORTED_SOURCE_EXTENSIONS.has(ext)) continue;
2380
2378
  const relPath = dir === "." ? entry : `${dir}/${entry}`;
2381
- const absPath = join4(projectDir, relPath);
2379
+ const absPath = join5(projectDir, relPath);
2382
2380
  try {
2383
2381
  const content = await readFile5(absPath, "utf-8");
2384
2382
  if (!containsBrakitImport(content)) continue;
@@ -2401,7 +2399,7 @@ async function fallbackSearchAndRemove(projectDir) {
2401
2399
  return null;
2402
2400
  }
2403
2401
  async function removeMcpConfig(rootDir) {
2404
- const mcpPath = join4(rootDir, ".mcp.json");
2402
+ const mcpPath = join5(rootDir, ".mcp.json");
2405
2403
  if (!await fileExists(mcpPath)) return false;
2406
2404
  try {
2407
2405
  const raw = await readFile5(mcpPath, "utf-8");
@@ -2421,7 +2419,7 @@ async function removeMcpConfig(rootDir) {
2421
2419
  }
2422
2420
  async function uninstallPackage(rootDir, pm) {
2423
2421
  try {
2424
- const pkgRaw = await readFile5(join4(rootDir, "package.json"), "utf-8");
2422
+ const pkgRaw = await readFile5(join5(rootDir, "package.json"), "utf-8");
2425
2423
  const pkg = JSON.parse(pkgRaw);
2426
2424
  if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
2427
2425
  } catch (err) {
@@ -2445,7 +2443,7 @@ async function uninstallPackage(rootDir, pm) {
2445
2443
  }
2446
2444
  async function removeBrakitData(rootDir) {
2447
2445
  let removed = false;
2448
- const projectDir = join4(rootDir, METRICS_DIR);
2446
+ const projectDir = join5(rootDir, METRICS_DIR);
2449
2447
  if (await fileExists(projectDir)) {
2450
2448
  try {
2451
2449
  await rm(projectDir, { recursive: true, force: true });
@@ -2466,7 +2464,7 @@ async function removeBrakitData(rootDir) {
2466
2464
  return removed;
2467
2465
  }
2468
2466
  async function cleanGitignore(rootDir) {
2469
- const gitignorePath = join4(rootDir, ".gitignore");
2467
+ const gitignorePath = join5(rootDir, ".gitignore");
2470
2468
  if (!await fileExists(gitignorePath)) return false;
2471
2469
  try {
2472
2470
  const content = await readFile5(gitignorePath, "utf-8");
@@ -2483,7 +2481,7 @@ async function cleanGitignore(rootDir) {
2483
2481
  async function clearBuildCaches(rootDir) {
2484
2482
  let cleared = false;
2485
2483
  for (const dir of BUILD_CACHE_DIRS) {
2486
- const absDir = join4(rootDir, dir);
2484
+ const absDir = join5(rootDir, dir);
2487
2485
  if (!await fileExists(absDir)) continue;
2488
2486
  try {
2489
2487
  await rm(absDir, { recursive: true, force: true });
@@ -2496,7 +2494,15 @@ async function clearBuildCaches(rootDir) {
2496
2494
  }
2497
2495
 
2498
2496
  // bin/brakit.ts
2497
+ init_config();
2499
2498
  var sub = process.argv[2];
2499
+ var command = sub === "uninstall" ? "uninstall" : sub === "mcp" ? "mcp" : "install";
2500
+ var cwd = process.cwd();
2501
+ trackEvent(TELEMETRY_EVENT_CLI_INVOKED, {
2502
+ command,
2503
+ has_package_json: existsSync7(resolve6(cwd, "package.json")),
2504
+ cwd_has_node_modules: existsSync7(resolve6(cwd, "node_modules"))
2505
+ });
2500
2506
  if (sub === "uninstall") {
2501
2507
  process.argv.splice(2, 1);
2502
2508
  runMain(uninstall_default);