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/bin/brakit.js
CHANGED
|
@@ -9,46 +9,68 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/constants/
|
|
13
|
-
var
|
|
14
|
-
var
|
|
15
|
-
"src/constants/
|
|
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"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
DASHBOARD_API_SECURITY = "/__brakit/api/security";
|
|
26
|
-
DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
|
|
17
|
+
PROJECT_HASH_LENGTH = 8;
|
|
18
|
+
SECRET_SCAN_ARRAY_LIMIT = 5;
|
|
19
|
+
PII_SCAN_ARRAY_LIMIT = 10;
|
|
20
|
+
MIN_SECRET_VALUE_LENGTH = 8;
|
|
21
|
+
FULL_RECORD_MIN_FIELDS = 5;
|
|
22
|
+
LIST_PII_MIN_ITEMS = 2;
|
|
23
|
+
MAX_OBJECT_SCAN_DEPTH = 5;
|
|
24
|
+
ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
|
|
27
25
|
}
|
|
28
26
|
});
|
|
29
27
|
|
|
30
|
-
// src/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
// src/utils/log.ts
|
|
29
|
+
function brakitDebug(message) {
|
|
30
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
31
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var PREFIX;
|
|
36
|
+
var init_log = __esm({
|
|
37
|
+
"src/utils/log.ts"() {
|
|
34
38
|
"use strict";
|
|
35
|
-
|
|
39
|
+
PREFIX = "[brakit]";
|
|
36
40
|
}
|
|
37
41
|
});
|
|
38
42
|
|
|
39
|
-
// src/constants/
|
|
40
|
-
var
|
|
41
|
-
var
|
|
42
|
-
"src/constants/
|
|
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"() {
|
|
43
47
|
"use strict";
|
|
44
|
-
|
|
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"]);
|
|
45
51
|
}
|
|
46
52
|
});
|
|
47
53
|
|
|
48
|
-
// src/
|
|
49
|
-
|
|
50
|
-
"
|
|
54
|
+
// src/utils/type-guards.ts
|
|
55
|
+
function isNonEmptyString(val) {
|
|
56
|
+
return typeof val === "string" && val.trim().length > 0;
|
|
57
|
+
}
|
|
58
|
+
function getErrorMessage(err) {
|
|
59
|
+
if (err instanceof Error) return err.message;
|
|
60
|
+
if (typeof err === "string") return err;
|
|
61
|
+
return String(err);
|
|
62
|
+
}
|
|
63
|
+
function isValidIssueState(val) {
|
|
64
|
+
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
65
|
+
}
|
|
66
|
+
function isValidAiFixStatus(val) {
|
|
67
|
+
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
68
|
+
}
|
|
69
|
+
var init_type_guards = __esm({
|
|
70
|
+
"src/utils/type-guards.ts"() {
|
|
51
71
|
"use strict";
|
|
72
|
+
init_lifecycle();
|
|
73
|
+
init_limits();
|
|
52
74
|
}
|
|
53
75
|
});
|
|
54
76
|
|
|
@@ -62,6 +84,61 @@ var init_metrics = __esm({
|
|
|
62
84
|
}
|
|
63
85
|
});
|
|
64
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;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
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"() {
|
|
101
|
+
"use strict";
|
|
102
|
+
DASHBOARD_PREFIX = "/__brakit";
|
|
103
|
+
DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
104
|
+
DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
105
|
+
DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
106
|
+
DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
107
|
+
DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
108
|
+
DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
109
|
+
DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
110
|
+
DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
111
|
+
DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
112
|
+
DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
113
|
+
DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
114
|
+
DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
115
|
+
DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
116
|
+
DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
117
|
+
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
118
|
+
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
119
|
+
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
120
|
+
VALID_TABS_TUPLE = [
|
|
121
|
+
"overview",
|
|
122
|
+
"actions",
|
|
123
|
+
"requests",
|
|
124
|
+
"fetches",
|
|
125
|
+
"queries",
|
|
126
|
+
"errors",
|
|
127
|
+
"logs",
|
|
128
|
+
"performance",
|
|
129
|
+
"security"
|
|
130
|
+
];
|
|
131
|
+
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/constants/transport.ts
|
|
136
|
+
var init_transport = __esm({
|
|
137
|
+
"src/constants/transport.ts"() {
|
|
138
|
+
"use strict";
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
65
142
|
// src/constants/headers.ts
|
|
66
143
|
var init_headers = __esm({
|
|
67
144
|
"src/constants/headers.ts"() {
|
|
@@ -70,19 +147,22 @@ var init_headers = __esm({
|
|
|
70
147
|
});
|
|
71
148
|
|
|
72
149
|
// src/constants/network.ts
|
|
150
|
+
var RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
|
|
73
151
|
var init_network = __esm({
|
|
74
152
|
"src/constants/network.ts"() {
|
|
75
153
|
"use strict";
|
|
154
|
+
RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
155
|
+
PORT_MIN = 1;
|
|
156
|
+
PORT_MAX = 65535;
|
|
76
157
|
}
|
|
77
158
|
});
|
|
78
159
|
|
|
79
160
|
// src/constants/mcp.ts
|
|
80
|
-
var MCP_SERVER_NAME,
|
|
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;
|
|
81
162
|
var init_mcp = __esm({
|
|
82
163
|
"src/constants/mcp.ts"() {
|
|
83
164
|
"use strict";
|
|
84
165
|
MCP_SERVER_NAME = "brakit";
|
|
85
|
-
MCP_SERVER_VERSION = "0.8.4";
|
|
86
166
|
INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
87
167
|
LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
88
168
|
CLIENT_FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -92,6 +172,7 @@ var init_mcp = __esm({
|
|
|
92
172
|
MAX_TIMELINE_EVENTS = 20;
|
|
93
173
|
MAX_RESOLVED_DISPLAY = 5;
|
|
94
174
|
ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
175
|
+
MCP_SERVER_VERSION = "0.8.6";
|
|
95
176
|
}
|
|
96
177
|
});
|
|
97
178
|
|
|
@@ -103,18 +184,9 @@ var init_encoding = __esm({
|
|
|
103
184
|
});
|
|
104
185
|
|
|
105
186
|
// src/constants/severity.ts
|
|
106
|
-
var SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
|
|
107
187
|
var init_severity = __esm({
|
|
108
188
|
"src/constants/severity.ts"() {
|
|
109
189
|
"use strict";
|
|
110
|
-
SEVERITY_CRITICAL = "critical";
|
|
111
|
-
SEVERITY_WARNING = "warning";
|
|
112
|
-
SEVERITY_INFO = "info";
|
|
113
|
-
SEVERITY_ICON_MAP = {
|
|
114
|
-
[SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
|
|
115
|
-
[SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
|
|
116
|
-
[SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
|
|
117
|
-
};
|
|
118
190
|
}
|
|
119
191
|
});
|
|
120
192
|
|
|
@@ -125,6 +197,24 @@ var init_telemetry = __esm({
|
|
|
125
197
|
}
|
|
126
198
|
});
|
|
127
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"() {
|
|
204
|
+
"use strict";
|
|
205
|
+
SUPPORTED_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
206
|
+
".ts",
|
|
207
|
+
".tsx",
|
|
208
|
+
".js",
|
|
209
|
+
".jsx",
|
|
210
|
+
".mjs",
|
|
211
|
+
".mts"
|
|
212
|
+
]);
|
|
213
|
+
BUILD_CACHE_DIRS = [".next", ".nuxt", ".output"];
|
|
214
|
+
FALLBACK_SCAN_DIRS = ["src", "."];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
128
218
|
// src/constants/index.ts
|
|
129
219
|
var init_constants = __esm({
|
|
130
220
|
"src/constants/index.ts"() {
|
|
@@ -140,18 +230,8 @@ var init_constants = __esm({
|
|
|
140
230
|
init_encoding();
|
|
141
231
|
init_severity();
|
|
142
232
|
init_telemetry();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// src/store/finding-id.ts
|
|
147
|
-
import { createHash } from "crypto";
|
|
148
|
-
function computeFindingId(finding) {
|
|
149
|
-
const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
|
|
150
|
-
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
151
|
-
}
|
|
152
|
-
var init_finding_id = __esm({
|
|
153
|
-
"src/store/finding-id.ts"() {
|
|
154
|
-
"use strict";
|
|
233
|
+
init_lifecycle();
|
|
234
|
+
init_cli();
|
|
155
235
|
}
|
|
156
236
|
});
|
|
157
237
|
|
|
@@ -189,11 +269,11 @@ var init_client = __esm({
|
|
|
189
269
|
if (params?.offset) url.searchParams.set("offset", String(params.offset));
|
|
190
270
|
return this.fetchJson(url);
|
|
191
271
|
}
|
|
192
|
-
async
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return this.fetchJson(
|
|
272
|
+
async getIssues(params) {
|
|
273
|
+
const url = new URL(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
|
|
274
|
+
if (params?.state) url.searchParams.set("state", params.state);
|
|
275
|
+
if (params?.category) url.searchParams.set("category", params.category);
|
|
276
|
+
return this.fetchJson(url);
|
|
197
277
|
}
|
|
198
278
|
async getQueries(requestId) {
|
|
199
279
|
const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
|
|
@@ -221,6 +301,19 @@ var init_client = __esm({
|
|
|
221
301
|
if (state) url.searchParams.set("state", state);
|
|
222
302
|
return this.fetchJson(url);
|
|
223
303
|
}
|
|
304
|
+
async reportFix(findingId, status, notes) {
|
|
305
|
+
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_FINDINGS_REPORT}`, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: { "Content-Type": "application/json" },
|
|
308
|
+
body: JSON.stringify({ findingId, status, notes }),
|
|
309
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
310
|
+
});
|
|
311
|
+
if (!res.ok) return false;
|
|
312
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
313
|
+
if (!contentType.includes("application/json")) return false;
|
|
314
|
+
const body = await res.json();
|
|
315
|
+
return body.ok === true;
|
|
316
|
+
}
|
|
224
317
|
async clearAll() {
|
|
225
318
|
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
|
|
226
319
|
method: "POST",
|
|
@@ -252,50 +345,56 @@ var init_client = __esm({
|
|
|
252
345
|
});
|
|
253
346
|
|
|
254
347
|
// src/mcp/discovery.ts
|
|
255
|
-
import {
|
|
256
|
-
import { resolve as resolve5, dirname } from "path";
|
|
257
|
-
function readPort(portPath) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
348
|
+
import { readFile as readFile6, readdir as readdir3, stat } from "fs/promises";
|
|
349
|
+
import { resolve as resolve5, dirname as dirname2 } from "path";
|
|
350
|
+
async function readPort(portPath) {
|
|
351
|
+
try {
|
|
352
|
+
const raw = (await readFile6(portPath, "utf-8")).trim();
|
|
353
|
+
const port = parseInt(raw, 10);
|
|
354
|
+
return isNaN(port) || port < PORT_MIN || port > PORT_MAX ? null : port;
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function portInDir(dir) {
|
|
264
360
|
return readPort(resolve5(dir, PORT_FILE));
|
|
265
361
|
}
|
|
266
|
-
function portInChildren(dir) {
|
|
362
|
+
async function portInChildren(dir) {
|
|
267
363
|
try {
|
|
268
|
-
|
|
364
|
+
const entries = await readdir3(dir);
|
|
365
|
+
for (const entry of entries) {
|
|
269
366
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
270
367
|
const child = resolve5(dir, entry);
|
|
271
368
|
try {
|
|
272
|
-
if (!
|
|
273
|
-
} catch {
|
|
369
|
+
if (!(await stat(child)).isDirectory()) continue;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
brakitDebug(`discovery: stat failed for ${child}: ${err}`);
|
|
274
372
|
continue;
|
|
275
373
|
}
|
|
276
|
-
const port = portInDir(child);
|
|
374
|
+
const port = await portInDir(child);
|
|
277
375
|
if (port) return port;
|
|
278
376
|
}
|
|
279
|
-
} catch {
|
|
377
|
+
} catch (err) {
|
|
378
|
+
brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
|
|
280
379
|
}
|
|
281
380
|
return null;
|
|
282
381
|
}
|
|
283
|
-
function searchForPort(startDir) {
|
|
382
|
+
async function searchForPort(startDir) {
|
|
284
383
|
const start = resolve5(startDir);
|
|
285
|
-
const initial = portInDir(start) ?? portInChildren(start);
|
|
384
|
+
const initial = await portInDir(start) ?? await portInChildren(start);
|
|
286
385
|
if (initial) return initial;
|
|
287
|
-
let dir =
|
|
386
|
+
let dir = dirname2(start);
|
|
288
387
|
for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
|
|
289
|
-
const port = portInDir(dir);
|
|
388
|
+
const port = await portInDir(dir) ?? await portInChildren(dir);
|
|
290
389
|
if (port) return port;
|
|
291
|
-
const parent =
|
|
390
|
+
const parent = dirname2(dir);
|
|
292
391
|
if (parent === dir) break;
|
|
293
392
|
dir = parent;
|
|
294
393
|
}
|
|
295
394
|
return null;
|
|
296
395
|
}
|
|
297
|
-
function discoverBrakitPort(cwd) {
|
|
298
|
-
const port = searchForPort(cwd ?? process.cwd());
|
|
396
|
+
async function discoverBrakitPort(cwd) {
|
|
397
|
+
const port = await searchForPort(cwd ?? process.cwd());
|
|
299
398
|
if (!port) {
|
|
300
399
|
throw new Error(
|
|
301
400
|
"Brakit is not running. Start your app with brakit enabled first."
|
|
@@ -307,7 +406,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
|
|
|
307
406
|
const deadline = Date.now() + timeoutMs;
|
|
308
407
|
while (Date.now() < deadline) {
|
|
309
408
|
try {
|
|
310
|
-
const result = discoverBrakitPort(cwd);
|
|
409
|
+
const result = await discoverBrakitPort(cwd);
|
|
311
410
|
const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
|
|
312
411
|
if (res.ok) return result;
|
|
313
412
|
} catch {
|
|
@@ -322,64 +421,54 @@ var init_discovery = __esm({
|
|
|
322
421
|
"src/mcp/discovery.ts"() {
|
|
323
422
|
"use strict";
|
|
324
423
|
init_constants();
|
|
424
|
+
init_log();
|
|
325
425
|
init_mcp();
|
|
326
426
|
}
|
|
327
427
|
});
|
|
328
428
|
|
|
329
429
|
// src/mcp/enrichment.ts
|
|
330
|
-
import { createHash as createHash2 } from "crypto";
|
|
331
|
-
function computeInsightId(type, endpoint, desc) {
|
|
332
|
-
const key = `${type}:${endpoint}:${desc}`;
|
|
333
|
-
return createHash2("sha256").update(key).digest("hex").slice(0, 16);
|
|
334
|
-
}
|
|
335
430
|
async function enrichFindings(client) {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
431
|
+
const issuesData = await client.getIssues();
|
|
432
|
+
const issues = issuesData.issues.filter(
|
|
433
|
+
(si) => si.state !== "resolved" && si.state !== "stale"
|
|
434
|
+
);
|
|
435
|
+
const contexts = await Promise.all(
|
|
436
|
+
issues.map(async (si) => {
|
|
437
|
+
const endpoint = si.issue.endpoint;
|
|
438
|
+
if (!endpoint) return si.issue.detail ?? "";
|
|
439
|
+
try {
|
|
440
|
+
const { path } = parseEndpointKey(endpoint);
|
|
441
|
+
const reqData = await client.getRequests({ search: path, limit: 1 });
|
|
442
|
+
if (reqData.requests.length > 0) {
|
|
443
|
+
const req = reqData.requests[0];
|
|
444
|
+
if (req.id) {
|
|
445
|
+
const activity = await client.getActivity(req.id);
|
|
446
|
+
const queryCount = activity.counts?.queries ?? 0;
|
|
447
|
+
const fetchCount = activity.counts?.fetches ?? 0;
|
|
448
|
+
return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
|
|
449
|
+
}
|
|
353
450
|
}
|
|
451
|
+
} catch {
|
|
452
|
+
return "(context unavailable)";
|
|
354
453
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
endpoint: f.endpoint,
|
|
363
|
-
description: f.desc,
|
|
364
|
-
hint: f.hint,
|
|
365
|
-
occurrences: f.count,
|
|
366
|
-
context
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
for (const si of insightsData.insights) {
|
|
370
|
-
if (si.state === "resolved") continue;
|
|
371
|
-
const i = si.insight;
|
|
372
|
-
if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
|
|
373
|
-
const endpoint = i.nav ?? "global";
|
|
454
|
+
return si.issue.detail ?? "";
|
|
455
|
+
})
|
|
456
|
+
);
|
|
457
|
+
const enriched = [];
|
|
458
|
+
for (let i = 0; i < issues.length; i++) {
|
|
459
|
+
const si = issues[i];
|
|
460
|
+
if (!ENRICHMENT_SEVERITY_FILTER.includes(si.issue.severity)) continue;
|
|
374
461
|
enriched.push({
|
|
375
|
-
findingId:
|
|
376
|
-
severity:
|
|
377
|
-
title:
|
|
378
|
-
endpoint,
|
|
379
|
-
description:
|
|
380
|
-
hint:
|
|
381
|
-
occurrences:
|
|
382
|
-
context: i
|
|
462
|
+
findingId: si.issueId,
|
|
463
|
+
severity: si.issue.severity,
|
|
464
|
+
title: si.issue.title,
|
|
465
|
+
endpoint: si.issue.endpoint ?? "global",
|
|
466
|
+
description: si.issue.desc,
|
|
467
|
+
hint: si.issue.hint,
|
|
468
|
+
occurrences: si.occurrences,
|
|
469
|
+
context: contexts[i],
|
|
470
|
+
aiStatus: si.aiStatus,
|
|
471
|
+
aiNotes: si.aiNotes
|
|
383
472
|
});
|
|
384
473
|
}
|
|
385
474
|
return enriched;
|
|
@@ -435,19 +524,18 @@ var init_enrichment = __esm({
|
|
|
435
524
|
"src/mcp/enrichment.ts"() {
|
|
436
525
|
"use strict";
|
|
437
526
|
init_mcp();
|
|
438
|
-
init_finding_id();
|
|
439
527
|
init_endpoint();
|
|
440
528
|
}
|
|
441
529
|
});
|
|
442
530
|
|
|
443
531
|
// src/mcp/tools/get-findings.ts
|
|
444
|
-
var
|
|
532
|
+
var getFindings;
|
|
445
533
|
var init_get_findings = __esm({
|
|
446
534
|
"src/mcp/tools/get-findings.ts"() {
|
|
447
535
|
"use strict";
|
|
448
536
|
init_enrichment();
|
|
449
|
-
|
|
450
|
-
|
|
537
|
+
init_lifecycle();
|
|
538
|
+
init_type_guards();
|
|
451
539
|
getFindings = {
|
|
452
540
|
name: "get_findings",
|
|
453
541
|
description: "Get all security findings and performance insights from the running app. Returns enriched findings with actionable fix hints, endpoint context, and evidence. Use this to understand what issues exist in the running application.",
|
|
@@ -461,28 +549,28 @@ var init_get_findings = __esm({
|
|
|
461
549
|
},
|
|
462
550
|
state: {
|
|
463
551
|
type: "string",
|
|
464
|
-
enum: ["open", "fixing", "resolved"],
|
|
465
|
-
description: "Filter by
|
|
552
|
+
enum: ["open", "fixing", "resolved", "stale", "regressed"],
|
|
553
|
+
description: "Filter by issue state"
|
|
466
554
|
}
|
|
467
555
|
}
|
|
468
556
|
},
|
|
469
557
|
async handler(client, args) {
|
|
470
558
|
const severity = args.severity;
|
|
471
559
|
const state = args.state;
|
|
472
|
-
if (severity && !
|
|
560
|
+
if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
|
|
473
561
|
return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
|
|
474
562
|
}
|
|
475
|
-
if (state && !
|
|
476
|
-
return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
|
|
563
|
+
if (state && !isValidIssueState(state)) {
|
|
564
|
+
return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved, stale, regressed.` }], isError: true };
|
|
477
565
|
}
|
|
478
566
|
let findings = await enrichFindings(client);
|
|
479
567
|
if (severity) {
|
|
480
568
|
findings = findings.filter((f) => f.severity === severity);
|
|
481
569
|
}
|
|
482
570
|
if (state) {
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
findings = findings.filter((f) =>
|
|
571
|
+
const issuesData = await client.getIssues({ state });
|
|
572
|
+
const issueIds = new Set(issuesData.issues.map((i) => i.issueId));
|
|
573
|
+
findings = findings.filter((f) => issueIds.has(f.findingId));
|
|
486
574
|
}
|
|
487
575
|
if (findings.length === 0) {
|
|
488
576
|
return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
|
|
@@ -491,10 +579,18 @@ var init_get_findings = __esm({
|
|
|
491
579
|
`];
|
|
492
580
|
for (const f of findings) {
|
|
493
581
|
lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
|
|
582
|
+
lines.push(` ID: ${f.findingId}`);
|
|
494
583
|
lines.push(` Endpoint: ${f.endpoint}`);
|
|
495
584
|
lines.push(` Issue: ${f.description}`);
|
|
496
585
|
if (f.context) lines.push(` Context: ${f.context}`);
|
|
497
586
|
lines.push(` Fix: ${f.hint}`);
|
|
587
|
+
if (f.aiStatus === "fixed") {
|
|
588
|
+
lines.push(` AI Status: fixed (awaiting verification)`);
|
|
589
|
+
if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
|
|
590
|
+
} else if (f.aiStatus === "wont_fix") {
|
|
591
|
+
lines.push(` AI Status: won't fix`);
|
|
592
|
+
if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
|
|
593
|
+
}
|
|
498
594
|
lines.push("");
|
|
499
595
|
}
|
|
500
596
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
@@ -653,20 +749,21 @@ var init_verify_fix = __esm({
|
|
|
653
749
|
}
|
|
654
750
|
if (findingId) {
|
|
655
751
|
const data = await client.getFindings();
|
|
656
|
-
const finding = data.findings.find((f) => f.
|
|
752
|
+
const finding = data.findings.find((f) => f.issueId === findingId);
|
|
657
753
|
if (!finding) {
|
|
658
754
|
return {
|
|
659
755
|
content: [{
|
|
660
756
|
type: "text",
|
|
661
757
|
text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
|
|
662
|
-
}]
|
|
758
|
+
}],
|
|
759
|
+
isError: true
|
|
663
760
|
};
|
|
664
761
|
}
|
|
665
762
|
if (finding.state === "resolved") {
|
|
666
763
|
return {
|
|
667
764
|
content: [{
|
|
668
765
|
type: "text",
|
|
669
|
-
text: `RESOLVED: "${finding.
|
|
766
|
+
text: `RESOLVED: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"} is no longer detected. The fix worked.`
|
|
670
767
|
}]
|
|
671
768
|
};
|
|
672
769
|
}
|
|
@@ -674,12 +771,12 @@ var init_verify_fix = __esm({
|
|
|
674
771
|
content: [{
|
|
675
772
|
type: "text",
|
|
676
773
|
text: [
|
|
677
|
-
`STILL PRESENT: "${finding.
|
|
774
|
+
`STILL PRESENT: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"}`,
|
|
678
775
|
` State: ${finding.state}`,
|
|
679
776
|
` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
|
|
680
777
|
` Occurrences: ${finding.occurrences}`,
|
|
681
|
-
` Issue: ${finding.
|
|
682
|
-
` Hint: ${finding.
|
|
778
|
+
` Issue: ${finding.issue.desc}`,
|
|
779
|
+
` Hint: ${finding.issue.hint}`,
|
|
683
780
|
"",
|
|
684
781
|
"Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
|
|
685
782
|
].join("\n")
|
|
@@ -689,7 +786,7 @@ var init_verify_fix = __esm({
|
|
|
689
786
|
if (endpoint) {
|
|
690
787
|
const data = await client.getFindings();
|
|
691
788
|
const endpointFindings = data.findings.filter(
|
|
692
|
-
(f) => f.
|
|
789
|
+
(f) => f.issue.endpoint === endpoint || f.issue.endpoint && f.issue.endpoint.endsWith(` ${endpoint}`)
|
|
693
790
|
);
|
|
694
791
|
if (endpointFindings.length === 0) {
|
|
695
792
|
return {
|
|
@@ -699,7 +796,7 @@ var init_verify_fix = __esm({
|
|
|
699
796
|
}]
|
|
700
797
|
};
|
|
701
798
|
}
|
|
702
|
-
const open = endpointFindings.filter((f) => f.state === "open");
|
|
799
|
+
const open = endpointFindings.filter((f) => f.state === "open" || f.state === "regressed");
|
|
703
800
|
const resolved = endpointFindings.filter((f) => f.state === "resolved");
|
|
704
801
|
const lines = [
|
|
705
802
|
`Endpoint: ${endpoint}`,
|
|
@@ -708,10 +805,10 @@ var init_verify_fix = __esm({
|
|
|
708
805
|
""
|
|
709
806
|
];
|
|
710
807
|
for (const f of open) {
|
|
711
|
-
lines.push(` [${f.
|
|
808
|
+
lines.push(` [${f.issue.severity}] ${f.issue.title}: ${f.issue.desc}`);
|
|
712
809
|
}
|
|
713
810
|
for (const f of resolved) {
|
|
714
|
-
lines.push(` [resolved] ${f.
|
|
811
|
+
lines.push(` [resolved] ${f.issue.title}`);
|
|
715
812
|
}
|
|
716
813
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
717
814
|
}
|
|
@@ -719,7 +816,8 @@ var init_verify_fix = __esm({
|
|
|
719
816
|
content: [{
|
|
720
817
|
type: "text",
|
|
721
818
|
text: "Please provide either a finding_id or an endpoint to verify."
|
|
722
|
-
}]
|
|
819
|
+
}],
|
|
820
|
+
isError: true
|
|
723
821
|
};
|
|
724
822
|
}
|
|
725
823
|
};
|
|
@@ -740,51 +838,52 @@ var init_get_report = __esm({
|
|
|
740
838
|
properties: {}
|
|
741
839
|
},
|
|
742
840
|
async handler(client, _args) {
|
|
743
|
-
const [
|
|
744
|
-
client.
|
|
745
|
-
client.getSecurityFindings(),
|
|
746
|
-
client.getInsights(),
|
|
841
|
+
const [issuesData, metricsData] = await Promise.all([
|
|
842
|
+
client.getIssues(),
|
|
747
843
|
client.getLiveMetrics()
|
|
748
844
|
]);
|
|
749
|
-
const
|
|
750
|
-
const open =
|
|
751
|
-
const resolved =
|
|
752
|
-
const fixing =
|
|
753
|
-
const
|
|
754
|
-
const
|
|
845
|
+
const issues = issuesData.issues;
|
|
846
|
+
const open = issues.filter((f) => f.state === "open" || f.state === "regressed");
|
|
847
|
+
const resolved = issues.filter((f) => f.state === "resolved");
|
|
848
|
+
const fixing = issues.filter((f) => f.state === "fixing");
|
|
849
|
+
const stale = issues.filter((f) => f.state === "stale");
|
|
850
|
+
const criticalOpen = open.filter((f) => f.issue.severity === "critical");
|
|
851
|
+
const warningOpen = open.filter((f) => f.issue.severity === "warning");
|
|
852
|
+
const securityIssues = issues.filter((f) => f.category === "security");
|
|
853
|
+
const perfIssues = issues.filter((f) => f.category === "performance");
|
|
755
854
|
const totalRequests = metricsData.endpoints.reduce(
|
|
756
855
|
(s, ep) => s + ep.summary.totalRequests,
|
|
757
856
|
0
|
|
758
857
|
);
|
|
759
|
-
const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
|
|
760
858
|
const lines = [
|
|
761
859
|
"=== Brakit Report ===",
|
|
762
860
|
"",
|
|
763
861
|
`Endpoints observed: ${metricsData.endpoints.length}`,
|
|
764
862
|
`Total requests captured: ${totalRequests}`,
|
|
765
|
-
`
|
|
766
|
-
`Performance
|
|
863
|
+
`Security issues: ${securityIssues.length}`,
|
|
864
|
+
`Performance issues: ${perfIssues.length}`,
|
|
767
865
|
"",
|
|
768
|
-
"---
|
|
769
|
-
`Total: ${
|
|
866
|
+
"--- Issue Summary ---",
|
|
867
|
+
`Total: ${issues.length}`,
|
|
770
868
|
` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
|
|
771
869
|
` In progress: ${fixing.length}`,
|
|
772
|
-
` Resolved: ${resolved.length}
|
|
870
|
+
` Resolved: ${resolved.length}`,
|
|
871
|
+
` Stale: ${stale.length}`
|
|
773
872
|
];
|
|
774
873
|
if (criticalOpen.length > 0) {
|
|
775
874
|
lines.push("");
|
|
776
875
|
lines.push("--- Critical Issues (fix first) ---");
|
|
777
876
|
for (const f of criticalOpen) {
|
|
778
|
-
lines.push(` [CRITICAL] ${f.
|
|
779
|
-
lines.push(` ${f.
|
|
780
|
-
lines.push(` Fix: ${f.
|
|
877
|
+
lines.push(` [CRITICAL] ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
|
|
878
|
+
lines.push(` ${f.issue.desc}`);
|
|
879
|
+
lines.push(` Fix: ${f.issue.hint}`);
|
|
781
880
|
}
|
|
782
881
|
}
|
|
783
882
|
if (resolved.length > 0) {
|
|
784
883
|
lines.push("");
|
|
785
884
|
lines.push("--- Recently Resolved ---");
|
|
786
885
|
for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
|
|
787
|
-
lines.push(` \u2713 ${f.
|
|
886
|
+
lines.push(` \u2713 ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
|
|
788
887
|
}
|
|
789
888
|
if (resolved.length > MAX_RESOLVED_DISPLAY) {
|
|
790
889
|
lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
|
|
@@ -823,6 +922,61 @@ var init_clear_findings = __esm({
|
|
|
823
922
|
}
|
|
824
923
|
});
|
|
825
924
|
|
|
925
|
+
// src/mcp/tools/report-fix.ts
|
|
926
|
+
var reportFix;
|
|
927
|
+
var init_report_fix = __esm({
|
|
928
|
+
"src/mcp/tools/report-fix.ts"() {
|
|
929
|
+
"use strict";
|
|
930
|
+
init_type_guards();
|
|
931
|
+
reportFix = {
|
|
932
|
+
name: "report_fix",
|
|
933
|
+
description: "Report the result of fixing a brakit finding. Call this after attempting to fix each finding to update the dashboard with the outcome. Use status 'fixed' when you've applied a fix, or 'wont_fix' when the issue can't be resolved (e.g. third-party library, by design).",
|
|
934
|
+
inputSchema: {
|
|
935
|
+
type: "object",
|
|
936
|
+
properties: {
|
|
937
|
+
finding_id: {
|
|
938
|
+
type: "string",
|
|
939
|
+
description: "The finding ID to report on"
|
|
940
|
+
},
|
|
941
|
+
status: {
|
|
942
|
+
type: "string",
|
|
943
|
+
description: "Whether the fix was applied or can't be fixed",
|
|
944
|
+
enum: ["fixed", "wont_fix"]
|
|
945
|
+
},
|
|
946
|
+
summary: {
|
|
947
|
+
type: "string",
|
|
948
|
+
description: "Brief description of what was done or why it can't be fixed"
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
required: ["finding_id", "status", "summary"]
|
|
952
|
+
},
|
|
953
|
+
async handler(client, args) {
|
|
954
|
+
const { finding_id, status, summary } = args;
|
|
955
|
+
if (!isNonEmptyString(finding_id)) {
|
|
956
|
+
return { content: [{ type: "text", text: "finding_id is required." }], isError: true };
|
|
957
|
+
}
|
|
958
|
+
if (!isValidAiFixStatus(status)) {
|
|
959
|
+
return { content: [{ type: "text", text: "status must be 'fixed' or 'wont_fix'." }], isError: true };
|
|
960
|
+
}
|
|
961
|
+
if (!isNonEmptyString(summary)) {
|
|
962
|
+
return { content: [{ type: "text", text: "summary is required." }], isError: true };
|
|
963
|
+
}
|
|
964
|
+
const ok = await client.reportFix(finding_id, status, summary);
|
|
965
|
+
if (!ok) {
|
|
966
|
+
return {
|
|
967
|
+
content: [{ type: "text", text: `Finding ${finding_id} not found. It may have already been resolved.` }],
|
|
968
|
+
isError: true
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const label = status === "fixed" ? "marked as fixed (awaiting verification)" : "marked as won't fix";
|
|
972
|
+
return {
|
|
973
|
+
content: [{ type: "text", text: `Finding ${finding_id} ${label}. Dashboard updated.` }]
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
826
980
|
// src/mcp/tools/index.ts
|
|
827
981
|
function getToolDefinitions() {
|
|
828
982
|
return [...TOOL_MAP.values()].map((t) => ({
|
|
@@ -851,17 +1005,19 @@ var init_tools = __esm({
|
|
|
851
1005
|
init_verify_fix();
|
|
852
1006
|
init_get_report();
|
|
853
1007
|
init_clear_findings();
|
|
1008
|
+
init_report_fix();
|
|
854
1009
|
TOOL_MAP = new Map(
|
|
855
|
-
[getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
|
|
1010
|
+
[getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
|
|
856
1011
|
);
|
|
857
1012
|
}
|
|
858
1013
|
});
|
|
859
1014
|
|
|
860
1015
|
// src/mcp/prompts.ts
|
|
861
|
-
var PROMPTS, PROMPT_MESSAGES;
|
|
1016
|
+
var SERVER_INSTRUCTIONS, PROMPTS, PROMPT_MESSAGES;
|
|
862
1017
|
var init_prompts = __esm({
|
|
863
1018
|
"src/mcp/prompts.ts"() {
|
|
864
1019
|
"use strict";
|
|
1020
|
+
SERVER_INSTRUCTIONS = "Brakit is a dev tool that monitors your running app for security vulnerabilities, performance issues, and query problems. IMPORTANT: After fixing any brakit finding, always call the report_fix tool with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did. This updates the dashboard in real-time so the developer can track AI-driven fixes.";
|
|
865
1021
|
PROMPTS = [
|
|
866
1022
|
{
|
|
867
1023
|
name: "check-app",
|
|
@@ -873,18 +1029,8 @@ var init_prompts = __esm({
|
|
|
873
1029
|
}
|
|
874
1030
|
];
|
|
875
1031
|
PROMPT_MESSAGES = {
|
|
876
|
-
"check-app":
|
|
877
|
-
|
|
878
|
-
"First get all findings, then get the endpoint summary.",
|
|
879
|
-
"For any critical or warning findings, get the request detail to understand the root cause.",
|
|
880
|
-
"Give me a clear report of what's wrong and offer to fix each issue."
|
|
881
|
-
].join(" "),
|
|
882
|
-
"fix-findings": [
|
|
883
|
-
"Get all open brakit findings.",
|
|
884
|
-
"For each finding, get the request detail to understand the exact issue.",
|
|
885
|
-
"Then find the source code responsible and fix it.",
|
|
886
|
-
"After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
|
|
887
|
-
].join(" ")
|
|
1032
|
+
"check-app": "Check my running app for security and performance issues using brakit. First get all findings, then get the endpoint summary. For any critical or warning findings, get the request detail to understand the root cause. Give me a clear report of what's wrong and offer to fix each issue. After fixing any issue, call report_fix with the finding_id, status, and summary.",
|
|
1033
|
+
"fix-findings": "Get all open brakit findings. For each finding:\n1. Get the request detail to understand the exact issue\n2. Find the source code responsible and fix it\n3. Call report_fix with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did\n4. Move to the next finding\n\nAfter all findings are processed, ask me to re-trigger the endpoints so brakit can verify the fixes."
|
|
888
1034
|
};
|
|
889
1035
|
}
|
|
890
1036
|
});
|
|
@@ -912,7 +1058,7 @@ async function startMcpServer() {
|
|
|
912
1058
|
let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
|
|
913
1059
|
const server = new Server(
|
|
914
1060
|
{ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
|
|
915
|
-
{ capabilities: { tools: {}, prompts: {} } }
|
|
1061
|
+
{ capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
916
1062
|
);
|
|
917
1063
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
918
1064
|
prompts: [...PROMPTS]
|
|
@@ -990,29 +1136,31 @@ import { runMain } from "citty";
|
|
|
990
1136
|
|
|
991
1137
|
// src/cli/commands/install.ts
|
|
992
1138
|
import { defineCommand } from "citty";
|
|
993
|
-
import { resolve as resolve3, join as
|
|
994
|
-
import { readFile as
|
|
1139
|
+
import { resolve as resolve3, join as join3, dirname } from "path";
|
|
1140
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
995
1141
|
import { execSync } from "child_process";
|
|
1142
|
+
import { existsSync as existsSync5 } from "fs";
|
|
996
1143
|
import pc from "picocolors";
|
|
997
1144
|
|
|
998
|
-
// src/store/
|
|
999
|
-
|
|
1000
|
-
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
1145
|
+
// src/store/issue-store.ts
|
|
1146
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1147
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
|
|
1001
1148
|
import { resolve as resolve2 } from "path";
|
|
1002
1149
|
|
|
1003
|
-
// src/utils/atomic-writer.ts
|
|
1004
|
-
import {
|
|
1005
|
-
writeFileSync as writeFileSync2,
|
|
1006
|
-
existsSync as existsSync2,
|
|
1007
|
-
mkdirSync as mkdirSync2,
|
|
1008
|
-
renameSync
|
|
1009
|
-
} from "fs";
|
|
1010
|
-
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
1011
|
-
|
|
1012
1150
|
// src/utils/fs.ts
|
|
1013
|
-
|
|
1151
|
+
init_limits();
|
|
1152
|
+
init_log();
|
|
1153
|
+
init_type_guards();
|
|
1154
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
1014
1155
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
1015
|
-
import {
|
|
1156
|
+
import { createHash } from "crypto";
|
|
1157
|
+
import { homedir } from "os";
|
|
1158
|
+
import { resolve, join } from "path";
|
|
1159
|
+
function getProjectDataDir(projectRoot) {
|
|
1160
|
+
const absolute = resolve(projectRoot);
|
|
1161
|
+
const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
|
|
1162
|
+
return join(homedir(), ".brakit", "projects", hash);
|
|
1163
|
+
}
|
|
1016
1164
|
async function fileExists(path) {
|
|
1017
1165
|
try {
|
|
1018
1166
|
await access(path);
|
|
@@ -1022,13 +1170,35 @@ async function fileExists(path) {
|
|
|
1022
1170
|
}
|
|
1023
1171
|
}
|
|
1024
1172
|
|
|
1025
|
-
// src/store/
|
|
1026
|
-
|
|
1173
|
+
// src/store/issue-store.ts
|
|
1174
|
+
init_metrics();
|
|
1175
|
+
init_limits();
|
|
1176
|
+
init_thresholds();
|
|
1177
|
+
init_limits();
|
|
1178
|
+
|
|
1179
|
+
// src/utils/atomic-writer.ts
|
|
1180
|
+
import {
|
|
1181
|
+
writeFileSync as writeFileSync2,
|
|
1182
|
+
existsSync as existsSync2,
|
|
1183
|
+
mkdirSync as mkdirSync2,
|
|
1184
|
+
renameSync
|
|
1185
|
+
} from "fs";
|
|
1186
|
+
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
1187
|
+
init_log();
|
|
1188
|
+
init_type_guards();
|
|
1189
|
+
|
|
1190
|
+
// src/store/issue-store.ts
|
|
1191
|
+
init_log();
|
|
1192
|
+
init_type_guards();
|
|
1193
|
+
|
|
1194
|
+
// src/utils/issue-id.ts
|
|
1195
|
+
init_limits();
|
|
1196
|
+
import { createHash as createHash2 } from "crypto";
|
|
1027
1197
|
|
|
1028
1198
|
// src/detect/project.ts
|
|
1029
|
-
import { readFile as
|
|
1199
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
1030
1200
|
import { existsSync as existsSync4 } from "fs";
|
|
1031
|
-
import { join } from "path";
|
|
1201
|
+
import { join as join2, relative } from "path";
|
|
1032
1202
|
var FRAMEWORKS = [
|
|
1033
1203
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
1034
1204
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -1037,24 +1207,24 @@ var FRAMEWORKS = [
|
|
|
1037
1207
|
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
|
|
1038
1208
|
];
|
|
1039
1209
|
async function detectProject(rootDir) {
|
|
1040
|
-
const pkgPath =
|
|
1041
|
-
const raw = await
|
|
1210
|
+
const pkgPath = join2(rootDir, "package.json");
|
|
1211
|
+
const raw = await readFile3(pkgPath, "utf-8");
|
|
1042
1212
|
const pkg = JSON.parse(raw);
|
|
1043
1213
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1044
1214
|
const framework = detectFrameworkFromDeps(allDeps);
|
|
1045
1215
|
const matched = FRAMEWORKS.find((f) => f.name === framework);
|
|
1046
1216
|
const devCommand = matched?.devCmd ?? "";
|
|
1047
|
-
const devBin = matched ?
|
|
1217
|
+
const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
|
|
1048
1218
|
const defaultPort = matched?.defaultPort ?? 3e3;
|
|
1049
1219
|
const packageManager = await detectPackageManager(rootDir);
|
|
1050
1220
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
1051
1221
|
}
|
|
1052
1222
|
async function detectPackageManager(rootDir) {
|
|
1053
|
-
if (await fileExists(
|
|
1054
|
-
if (await fileExists(
|
|
1055
|
-
if (await fileExists(
|
|
1056
|
-
if (await fileExists(
|
|
1057
|
-
if (await fileExists(
|
|
1223
|
+
if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
|
|
1224
|
+
if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
|
|
1225
|
+
if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1226
|
+
if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
1227
|
+
if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
|
|
1058
1228
|
return "unknown";
|
|
1059
1229
|
}
|
|
1060
1230
|
function detectFrameworkFromDeps(allDeps) {
|
|
@@ -1063,16 +1233,168 @@ function detectFrameworkFromDeps(allDeps) {
|
|
|
1063
1233
|
}
|
|
1064
1234
|
return "unknown";
|
|
1065
1235
|
}
|
|
1236
|
+
var PYTHON_ENTRY_CANDIDATES = [
|
|
1237
|
+
"app.py",
|
|
1238
|
+
"main.py",
|
|
1239
|
+
"wsgi.py",
|
|
1240
|
+
"asgi.py",
|
|
1241
|
+
"server.py",
|
|
1242
|
+
"run.py",
|
|
1243
|
+
"manage.py",
|
|
1244
|
+
"app/__init__.py"
|
|
1245
|
+
];
|
|
1246
|
+
var PYTHON_FRAMEWORK_MAP = {
|
|
1247
|
+
flask: "flask",
|
|
1248
|
+
fastapi: "fastapi",
|
|
1249
|
+
django: "django"
|
|
1250
|
+
};
|
|
1251
|
+
var PYTHON_DEFAULT_PORTS = {
|
|
1252
|
+
flask: 5e3,
|
|
1253
|
+
fastapi: 8e3,
|
|
1254
|
+
django: 8e3,
|
|
1255
|
+
unknown: 8e3
|
|
1256
|
+
};
|
|
1257
|
+
async function detectPythonProject(rootDir) {
|
|
1258
|
+
const hasPyproject = await fileExists(join2(rootDir, "pyproject.toml"));
|
|
1259
|
+
const hasRequirements = await fileExists(join2(rootDir, "requirements.txt"));
|
|
1260
|
+
const hasSetupPy = await fileExists(join2(rootDir, "setup.py"));
|
|
1261
|
+
if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
|
|
1262
|
+
const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
|
|
1263
|
+
const packageManager = await detectPythonPackageManager(rootDir);
|
|
1264
|
+
const entryFile = await detectPythonEntry(rootDir);
|
|
1265
|
+
return {
|
|
1266
|
+
framework,
|
|
1267
|
+
packageManager,
|
|
1268
|
+
entryFile,
|
|
1269
|
+
defaultPort: PYTHON_DEFAULT_PORTS[framework] ?? 8e3
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
|
|
1273
|
+
if (hasPyproject) {
|
|
1274
|
+
try {
|
|
1275
|
+
const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
|
|
1276
|
+
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1277
|
+
if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
|
|
1278
|
+
return fw;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
} catch {
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (hasRequirements) {
|
|
1285
|
+
try {
|
|
1286
|
+
const content = await readFile3(join2(rootDir, "requirements.txt"), "utf-8");
|
|
1287
|
+
const lines = content.toLowerCase().split("\n");
|
|
1288
|
+
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1289
|
+
if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
|
|
1290
|
+
return fw;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
} catch {
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return "unknown";
|
|
1297
|
+
}
|
|
1298
|
+
async function detectPythonPackageManager(rootDir) {
|
|
1299
|
+
if (await fileExists(join2(rootDir, "uv.lock"))) return "uv";
|
|
1300
|
+
if (await fileExists(join2(rootDir, "poetry.lock"))) return "poetry";
|
|
1301
|
+
if (await fileExists(join2(rootDir, "Pipfile.lock"))) return "pipenv";
|
|
1302
|
+
if (await fileExists(join2(rootDir, "Pipfile"))) return "pipenv";
|
|
1303
|
+
if (await fileExists(join2(rootDir, "requirements.txt"))) return "pip";
|
|
1304
|
+
try {
|
|
1305
|
+
const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
|
|
1306
|
+
if (content.includes("[tool.poetry]")) return "poetry";
|
|
1307
|
+
if (content.includes("[tool.uv]")) return "uv";
|
|
1308
|
+
} catch {
|
|
1309
|
+
}
|
|
1310
|
+
return "unknown";
|
|
1311
|
+
}
|
|
1312
|
+
async function detectPythonEntry(rootDir) {
|
|
1313
|
+
for (const candidate of PYTHON_ENTRY_CANDIDATES) {
|
|
1314
|
+
if (await fileExists(join2(rootDir, candidate))) {
|
|
1315
|
+
return candidate;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1321
|
+
"node_modules",
|
|
1322
|
+
".git",
|
|
1323
|
+
".brakit",
|
|
1324
|
+
"dist",
|
|
1325
|
+
"build",
|
|
1326
|
+
"__pycache__",
|
|
1327
|
+
".venv",
|
|
1328
|
+
"venv",
|
|
1329
|
+
".next",
|
|
1330
|
+
".nuxt",
|
|
1331
|
+
".output",
|
|
1332
|
+
".cache",
|
|
1333
|
+
"coverage"
|
|
1334
|
+
]);
|
|
1335
|
+
async function scanForProjects(rootDir) {
|
|
1336
|
+
const projects = [];
|
|
1337
|
+
await detectInDir(rootDir, rootDir, projects);
|
|
1338
|
+
try {
|
|
1339
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
1340
|
+
for (const entry of entries) {
|
|
1341
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
1342
|
+
const childDir = join2(rootDir, entry.name);
|
|
1343
|
+
await detectInDir(childDir, rootDir, projects);
|
|
1344
|
+
}
|
|
1345
|
+
} catch {
|
|
1346
|
+
}
|
|
1347
|
+
return projects;
|
|
1348
|
+
}
|
|
1349
|
+
async function detectInDir(dir, rootDir, projects) {
|
|
1350
|
+
const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
|
|
1351
|
+
if (await fileExists(join2(dir, "package.json"))) {
|
|
1352
|
+
try {
|
|
1353
|
+
const node = await detectProject(dir);
|
|
1354
|
+
projects.push({ dir, relDir: rel, type: "node", node });
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const python = await detectPythonProject(dir);
|
|
1359
|
+
if (python) {
|
|
1360
|
+
projects.push({ dir, relDir: rel, type: "python", python });
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/utils/response.ts
|
|
1365
|
+
init_thresholds();
|
|
1366
|
+
function unwrapResponse(parsed) {
|
|
1367
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
1368
|
+
const obj = parsed;
|
|
1369
|
+
const keys = Object.keys(obj);
|
|
1370
|
+
if (keys.length > 3) return parsed;
|
|
1371
|
+
let best = null;
|
|
1372
|
+
let bestSize = 0;
|
|
1373
|
+
for (const key of keys) {
|
|
1374
|
+
const val = obj[key];
|
|
1375
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
1376
|
+
best = val;
|
|
1377
|
+
bestSize = val.length;
|
|
1378
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1379
|
+
const size = Object.keys(val).length;
|
|
1380
|
+
if (size > bestSize) {
|
|
1381
|
+
best = val;
|
|
1382
|
+
bestSize = size;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
1387
|
+
}
|
|
1066
1388
|
|
|
1067
1389
|
// src/analysis/rules/patterns.ts
|
|
1068
1390
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
1069
1391
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
1070
1392
|
var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
|
|
1071
|
-
var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections
|
|
1393
|
+
var 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+/;
|
|
1072
1394
|
var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
|
|
1073
1395
|
var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
1074
|
-
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
1075
|
-
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
1396
|
+
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
1397
|
+
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
|
|
1076
1398
|
var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
1077
1399
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
1078
1400
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
@@ -1082,37 +1404,41 @@ var RULE_HINTS = {
|
|
|
1082
1404
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
1083
1405
|
"stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
|
|
1084
1406
|
"error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
|
|
1407
|
+
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
1085
1408
|
"sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
|
|
1086
1409
|
"cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
|
|
1087
|
-
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
1088
1410
|
"response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
|
|
1089
1411
|
};
|
|
1090
1412
|
|
|
1091
1413
|
// src/analysis/rules/exposed-secret.ts
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1414
|
+
init_limits();
|
|
1415
|
+
|
|
1416
|
+
// src/utils/http-status.ts
|
|
1417
|
+
function isErrorStatus(code) {
|
|
1418
|
+
return code >= 400;
|
|
1419
|
+
}
|
|
1420
|
+
function isRedirect(code) {
|
|
1421
|
+
return code >= 300 && code < 400;
|
|
1099
1422
|
}
|
|
1100
|
-
|
|
1423
|
+
|
|
1424
|
+
// src/analysis/rules/exposed-secret.ts
|
|
1425
|
+
function findSecretKeys(obj, prefix, depth = 0) {
|
|
1101
1426
|
const found = [];
|
|
1427
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
|
|
1102
1428
|
if (!obj || typeof obj !== "object") return found;
|
|
1103
1429
|
if (Array.isArray(obj)) {
|
|
1104
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
1105
|
-
found.push(...findSecretKeys(obj[i], prefix));
|
|
1430
|
+
for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
|
|
1431
|
+
found.push(...findSecretKeys(obj[i], prefix, depth + 1));
|
|
1106
1432
|
}
|
|
1107
1433
|
return found;
|
|
1108
1434
|
}
|
|
1109
1435
|
for (const k of Object.keys(obj)) {
|
|
1110
1436
|
const val = obj[k];
|
|
1111
|
-
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >=
|
|
1437
|
+
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
|
|
1112
1438
|
found.push(k);
|
|
1113
1439
|
}
|
|
1114
1440
|
if (typeof val === "object" && val !== null) {
|
|
1115
|
-
found.push(...findSecretKeys(val, prefix + k + "."));
|
|
1441
|
+
found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
|
|
1116
1442
|
}
|
|
1117
1443
|
}
|
|
1118
1444
|
return found;
|
|
@@ -1126,8 +1452,8 @@ var exposedSecretRule = {
|
|
|
1126
1452
|
const findings = [];
|
|
1127
1453
|
const seen = /* @__PURE__ */ new Map();
|
|
1128
1454
|
for (const r of ctx.requests) {
|
|
1129
|
-
if (r.statusCode
|
|
1130
|
-
const parsed =
|
|
1455
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
1456
|
+
const parsed = ctx.parsedBodies.response.get(r.id);
|
|
1131
1457
|
if (!parsed) continue;
|
|
1132
1458
|
const keys = findSecretKeys(parsed, "");
|
|
1133
1459
|
if (keys.length === 0) continue;
|
|
@@ -1280,7 +1606,7 @@ var errorInfoLeakRule = {
|
|
|
1280
1606
|
|
|
1281
1607
|
// src/analysis/rules/insecure-cookie.ts
|
|
1282
1608
|
function isFrameworkResponse(r) {
|
|
1283
|
-
if (r.statusCode
|
|
1609
|
+
if (isRedirect(r.statusCode)) return true;
|
|
1284
1610
|
if (r.path?.startsWith("/__")) return true;
|
|
1285
1611
|
if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
1286
1612
|
return false;
|
|
@@ -1386,49 +1712,16 @@ var corsCredentialsRule = {
|
|
|
1386
1712
|
}
|
|
1387
1713
|
};
|
|
1388
1714
|
|
|
1389
|
-
// src/utils/response.ts
|
|
1390
|
-
init_thresholds();
|
|
1391
|
-
function unwrapResponse(parsed) {
|
|
1392
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
1393
|
-
const obj = parsed;
|
|
1394
|
-
const keys = Object.keys(obj);
|
|
1395
|
-
if (keys.length > 3) return parsed;
|
|
1396
|
-
let best = null;
|
|
1397
|
-
let bestSize = 0;
|
|
1398
|
-
for (const key of keys) {
|
|
1399
|
-
const val = obj[key];
|
|
1400
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
1401
|
-
best = val;
|
|
1402
|
-
bestSize = val.length;
|
|
1403
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1404
|
-
const size = Object.keys(val).length;
|
|
1405
|
-
if (size > bestSize) {
|
|
1406
|
-
best = val;
|
|
1407
|
-
bestSize = size;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
1715
|
// src/analysis/rules/response-pii-leak.ts
|
|
1716
|
+
init_limits();
|
|
1415
1717
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
1416
|
-
|
|
1417
|
-
var LIST_PII_MIN_ITEMS = 2;
|
|
1418
|
-
function tryParseJson2(body) {
|
|
1419
|
-
if (!body) return null;
|
|
1420
|
-
try {
|
|
1421
|
-
return JSON.parse(body);
|
|
1422
|
-
} catch {
|
|
1423
|
-
return null;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
function findEmails(obj) {
|
|
1718
|
+
function findEmails(obj, depth = 0) {
|
|
1427
1719
|
const emails = [];
|
|
1720
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
|
|
1428
1721
|
if (!obj || typeof obj !== "object") return emails;
|
|
1429
1722
|
if (Array.isArray(obj)) {
|
|
1430
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
1431
|
-
emails.push(...findEmails(obj[i]));
|
|
1723
|
+
for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
1724
|
+
emails.push(...findEmails(obj[i], depth + 1));
|
|
1432
1725
|
}
|
|
1433
1726
|
return emails;
|
|
1434
1727
|
}
|
|
@@ -1436,7 +1729,7 @@ function findEmails(obj) {
|
|
|
1436
1729
|
if (typeof v === "string" && EMAIL_RE.test(v)) {
|
|
1437
1730
|
emails.push(v);
|
|
1438
1731
|
} else if (typeof v === "object" && v !== null) {
|
|
1439
|
-
emails.push(...findEmails(v));
|
|
1732
|
+
emails.push(...findEmails(v, depth + 1));
|
|
1440
1733
|
}
|
|
1441
1734
|
}
|
|
1442
1735
|
return emails;
|
|
@@ -1455,48 +1748,47 @@ function hasInternalIds(obj) {
|
|
|
1455
1748
|
}
|
|
1456
1749
|
return false;
|
|
1457
1750
|
}
|
|
1458
|
-
function
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
return { reason: "echo", emailCount: echoed.length };
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1751
|
+
function detectEchoPII(method, reqBody, target) {
|
|
1752
|
+
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
1753
|
+
const reqEmails = findEmails(reqBody);
|
|
1754
|
+
if (reqEmails.length === 0) return null;
|
|
1755
|
+
const resEmails = findEmails(target);
|
|
1756
|
+
const echoed = reqEmails.filter((e) => resEmails.includes(e));
|
|
1757
|
+
if (echoed.length === 0) return null;
|
|
1758
|
+
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
1759
|
+
if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
|
|
1760
|
+
return { reason: "echo", emailCount: echoed.length };
|
|
1472
1761
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1762
|
+
return null;
|
|
1763
|
+
}
|
|
1764
|
+
function detectFullRecordPII(target) {
|
|
1765
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
|
|
1766
|
+
const fields = topLevelFieldCount(target);
|
|
1767
|
+
if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
|
|
1768
|
+
const emails = findEmails(target);
|
|
1769
|
+
if (emails.length === 0) return null;
|
|
1770
|
+
return { reason: "full-record", emailCount: emails.length };
|
|
1771
|
+
}
|
|
1772
|
+
function detectListPII(target) {
|
|
1773
|
+
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
1774
|
+
let itemsWithEmail = 0;
|
|
1775
|
+
for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
1776
|
+
const item = target[i];
|
|
1777
|
+
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
1778
|
+
itemsWithEmail++;
|
|
1480
1779
|
}
|
|
1481
1780
|
}
|
|
1482
|
-
if (
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
if (item && typeof item === "object") {
|
|
1487
|
-
const emails = findEmails(item);
|
|
1488
|
-
if (emails.length > 0) itemsWithEmail++;
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
|
|
1492
|
-
const first = target[0];
|
|
1493
|
-
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
1494
|
-
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1781
|
+
if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
|
|
1782
|
+
const first = target[0];
|
|
1783
|
+
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
1784
|
+
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
1497
1785
|
}
|
|
1498
1786
|
return null;
|
|
1499
1787
|
}
|
|
1788
|
+
function detectPII(method, reqBody, resBody) {
|
|
1789
|
+
const target = unwrapResponse(resBody);
|
|
1790
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
1791
|
+
}
|
|
1500
1792
|
var REASON_LABELS = {
|
|
1501
1793
|
echo: "echoes back PII from the request body",
|
|
1502
1794
|
"full-record": "returns a full record with email and internal IDs",
|
|
@@ -1511,10 +1803,10 @@ var responsePiiLeakRule = {
|
|
|
1511
1803
|
const findings = [];
|
|
1512
1804
|
const seen = /* @__PURE__ */ new Map();
|
|
1513
1805
|
for (const r of ctx.requests) {
|
|
1514
|
-
if (r.statusCode
|
|
1515
|
-
const resJson =
|
|
1806
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
1807
|
+
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
1516
1808
|
if (!resJson) continue;
|
|
1517
|
-
const reqJson =
|
|
1809
|
+
const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
|
|
1518
1810
|
const detection = detectPII(r.method, reqJson, resJson);
|
|
1519
1811
|
if (!detection) continue;
|
|
1520
1812
|
const ep = `${r.method} ${r.path}`;
|
|
@@ -1540,6 +1832,9 @@ var responsePiiLeakRule = {
|
|
|
1540
1832
|
}
|
|
1541
1833
|
};
|
|
1542
1834
|
|
|
1835
|
+
// src/analysis/engine.ts
|
|
1836
|
+
init_limits();
|
|
1837
|
+
|
|
1543
1838
|
// src/analysis/group.ts
|
|
1544
1839
|
init_constants();
|
|
1545
1840
|
import { randomUUID } from "crypto";
|
|
@@ -1595,12 +1890,11 @@ init_constants();
|
|
|
1595
1890
|
// src/analysis/insights/rules/regression.ts
|
|
1596
1891
|
init_constants();
|
|
1597
1892
|
|
|
1598
|
-
// src/analysis/
|
|
1893
|
+
// src/analysis/issue-mappers.ts
|
|
1599
1894
|
init_endpoint();
|
|
1600
|
-
init_thresholds();
|
|
1601
1895
|
|
|
1602
1896
|
// src/index.ts
|
|
1603
|
-
var VERSION = "0.8.
|
|
1897
|
+
var VERSION = "0.8.6";
|
|
1604
1898
|
|
|
1605
1899
|
// src/cli/commands/install.ts
|
|
1606
1900
|
init_constants();
|
|
@@ -1608,10 +1902,28 @@ init_constants();
|
|
|
1608
1902
|
// src/cli/templates.ts
|
|
1609
1903
|
var IMPORT_LINE = `import "brakit";`;
|
|
1610
1904
|
var IMPORT_MARKER = "brakit";
|
|
1905
|
+
var BRAKIT_IMPORT_PATTERNS = [
|
|
1906
|
+
'import("brakit")',
|
|
1907
|
+
'import "brakit"',
|
|
1908
|
+
"import 'brakit'",
|
|
1909
|
+
'require("brakit")',
|
|
1910
|
+
"require('brakit')"
|
|
1911
|
+
];
|
|
1912
|
+
function containsBrakitImport(content) {
|
|
1913
|
+
return BRAKIT_IMPORT_PATTERNS.some((p) => content.includes(p));
|
|
1914
|
+
}
|
|
1915
|
+
function removeBrakitImportLines(lines) {
|
|
1916
|
+
return lines.filter(
|
|
1917
|
+
(line) => !BRAKIT_IMPORT_PATTERNS.some((p) => line.includes(p))
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1611
1920
|
var CREATED_FILES = [
|
|
1612
1921
|
"src/instrumentation.ts",
|
|
1922
|
+
"src/instrumentation.js",
|
|
1613
1923
|
"instrumentation.ts",
|
|
1614
|
-
"
|
|
1924
|
+
"instrumentation.js",
|
|
1925
|
+
"server/plugins/brakit.ts",
|
|
1926
|
+
"server/plugins/brakit.js"
|
|
1615
1927
|
];
|
|
1616
1928
|
var ENTRY_CANDIDATES = [
|
|
1617
1929
|
"src/index.ts",
|
|
@@ -1628,7 +1940,6 @@ var ENTRY_CANDIDATES = [
|
|
|
1628
1940
|
"index.js"
|
|
1629
1941
|
];
|
|
1630
1942
|
var BRAKIT_TEMPLATES = {
|
|
1631
|
-
/** Next.js instrumentation.ts — standalone file created by install */
|
|
1632
1943
|
nextjs: [
|
|
1633
1944
|
`export async function register() {`,
|
|
1634
1945
|
` if (process.env.NODE_ENV !== "production") {`,
|
|
@@ -1636,7 +1947,6 @@ var BRAKIT_TEMPLATES = {
|
|
|
1636
1947
|
` }`,
|
|
1637
1948
|
`}`
|
|
1638
1949
|
].join("\n"),
|
|
1639
|
-
/** Nuxt server/plugins/brakit.ts — standalone file created by install */
|
|
1640
1950
|
nuxt: `import "brakit";`
|
|
1641
1951
|
};
|
|
1642
1952
|
var ALL_TEMPLATES = Object.values(BRAKIT_TEMPLATES);
|
|
@@ -1668,53 +1978,45 @@ var install_default = defineCommand({
|
|
|
1668
1978
|
},
|
|
1669
1979
|
async run({ args }) {
|
|
1670
1980
|
const rootDir = resolve3(args.dir);
|
|
1671
|
-
const pkgPath = join2(rootDir, "package.json");
|
|
1672
|
-
if (!await fileExists(pkgPath)) {
|
|
1673
|
-
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1674
|
-
process.exit(1);
|
|
1675
|
-
}
|
|
1676
|
-
let pkg;
|
|
1677
|
-
try {
|
|
1678
|
-
pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
|
|
1679
|
-
} catch {
|
|
1680
|
-
console.error(pc.red(" Failed to read package.json."));
|
|
1681
|
-
process.exit(1);
|
|
1682
|
-
}
|
|
1683
|
-
if (!pkg.name || typeof pkg.name !== "string") {
|
|
1684
|
-
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1685
|
-
process.exit(1);
|
|
1686
|
-
}
|
|
1687
|
-
let project;
|
|
1688
|
-
try {
|
|
1689
|
-
project = await detectProject(rootDir);
|
|
1690
|
-
} catch {
|
|
1691
|
-
console.error(pc.red(" Failed to read package.json."));
|
|
1692
|
-
process.exit(1);
|
|
1693
|
-
}
|
|
1694
1981
|
console.log();
|
|
1695
1982
|
console.log(pc.bold(" \u25C6 brakit install"));
|
|
1696
1983
|
console.log();
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
const result = await setupInstrumentation(rootDir, project.framework);
|
|
1704
|
-
if (result.action === "created") {
|
|
1705
|
-
console.log(pc.green(` \u2713 Created ${result.file}`));
|
|
1706
|
-
if (result.content) {
|
|
1984
|
+
const projects = await scanForProjects(rootDir);
|
|
1985
|
+
const nodeProjects = projects.filter((p) => p.type === "node");
|
|
1986
|
+
const pythonProjects = projects.filter((p) => p.type === "python");
|
|
1987
|
+
if (nodeProjects.length === 0) {
|
|
1988
|
+
if (pythonProjects.length > 0) {
|
|
1989
|
+
console.log(pc.dim(" Python project detected. To add brakit:"));
|
|
1707
1990
|
console.log();
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1991
|
+
console.log(pc.bold(" pip install brakit"));
|
|
1992
|
+
console.log(pc.dim(" Then add to the top of your entry file:"));
|
|
1993
|
+
console.log(pc.bold(" import brakit # noqa: F401"));
|
|
1994
|
+
console.log();
|
|
1995
|
+
} else {
|
|
1996
|
+
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1997
|
+
}
|
|
1998
|
+
process.exit(1);
|
|
1999
|
+
}
|
|
2000
|
+
for (const p of nodeProjects) {
|
|
2001
|
+
const node = p.node;
|
|
2002
|
+
const suffix = p.relDir === "." ? "" : ` in ${p.relDir}`;
|
|
2003
|
+
const installed = await installPackage(p.dir, node.packageManager);
|
|
2004
|
+
if (installed) {
|
|
2005
|
+
console.log(pc.green(` \u2713 Added brakit to devDependencies${suffix}`));
|
|
2006
|
+
} else {
|
|
2007
|
+
console.log(pc.dim(` \u2713 brakit already in dependencies${suffix}`));
|
|
2008
|
+
}
|
|
2009
|
+
const result = await setupInstrumentation(p.dir, node.framework);
|
|
2010
|
+
const prefix = p.relDir === "." ? "" : `${p.relDir}/`;
|
|
2011
|
+
if (result.action === "created") {
|
|
2012
|
+
console.log(pc.green(` \u2713 Created ${prefix}${result.file}`));
|
|
2013
|
+
} else if (result.action === "prepended") {
|
|
2014
|
+
console.log(pc.green(` \u2713 Added import to ${prefix}${result.file}`));
|
|
2015
|
+
} else if (result.action === "exists") {
|
|
2016
|
+
console.log(pc.dim(` \u2713 ${prefix}${result.file} already has brakit import`));
|
|
2017
|
+
} else {
|
|
2018
|
+
printManualInstructions(node.framework);
|
|
1711
2019
|
}
|
|
1712
|
-
} else if (result.action === "prepended") {
|
|
1713
|
-
console.log(pc.green(` \u2713 Added import to ${result.file}`));
|
|
1714
|
-
} else if (result.action === "exists") {
|
|
1715
|
-
console.log(pc.dim(` \u2713 ${result.file} already has brakit import`));
|
|
1716
|
-
} else {
|
|
1717
|
-
printManualInstructions(project.framework);
|
|
1718
2020
|
}
|
|
1719
2021
|
await ensureGitignoreEntry(rootDir, METRICS_DIR);
|
|
1720
2022
|
const mcpResult = await setupMcp(rootDir);
|
|
@@ -1723,14 +2025,30 @@ var install_default = defineCommand({
|
|
|
1723
2025
|
} else if (mcpResult === "exists") {
|
|
1724
2026
|
console.log(pc.dim(" \u2713 MCP already configured"));
|
|
1725
2027
|
}
|
|
2028
|
+
const gitRoot = findGitRoot(rootDir);
|
|
2029
|
+
if (gitRoot && gitRoot !== rootDir) {
|
|
2030
|
+
const parentMcpResult = await setupMcp(gitRoot);
|
|
2031
|
+
if (parentMcpResult === "created" || parentMcpResult === "updated") {
|
|
2032
|
+
console.log(pc.green(" \u2713 Configured MCP at project root"));
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1726
2035
|
console.log();
|
|
2036
|
+
const port = nodeProjects[0].node?.defaultPort ?? 3e3;
|
|
1727
2037
|
console.log(pc.dim(" Start your app and visit:"));
|
|
1728
|
-
console.log(pc.bold(
|
|
2038
|
+
console.log(pc.bold(` http://localhost:${port}/__brakit`));
|
|
2039
|
+
if (pythonProjects.length > 0) {
|
|
2040
|
+
const pyLabel = pythonProjects.map((p) => p.relDir).join(", ");
|
|
2041
|
+
console.log();
|
|
2042
|
+
console.log(pc.dim(` Python backend detected (${pyLabel}). To capture telemetry:`));
|
|
2043
|
+
console.log(pc.bold(" pip install brakit"));
|
|
2044
|
+
console.log(pc.dim(" Then add to the top of your entry file:"));
|
|
2045
|
+
console.log(pc.bold(" import brakit # noqa: F401"));
|
|
2046
|
+
}
|
|
1729
2047
|
console.log();
|
|
1730
2048
|
}
|
|
1731
2049
|
});
|
|
1732
2050
|
async function installPackage(rootDir, pm) {
|
|
1733
|
-
const pkgRaw = await
|
|
2051
|
+
const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
|
|
1734
2052
|
const pkg = JSON.parse(pkgRaw);
|
|
1735
2053
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1736
2054
|
if (allDeps["brakit"]) return false;
|
|
@@ -1762,11 +2080,11 @@ async function setupInstrumentation(rootDir, framework) {
|
|
|
1762
2080
|
}
|
|
1763
2081
|
}
|
|
1764
2082
|
async function setupNextjs(rootDir) {
|
|
1765
|
-
const hasSrc = await fileExists(
|
|
2083
|
+
const hasSrc = await fileExists(join3(rootDir, "src"));
|
|
1766
2084
|
const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
|
|
1767
|
-
const absPath =
|
|
2085
|
+
const absPath = join3(rootDir, relPath);
|
|
1768
2086
|
if (await fileExists(absPath)) {
|
|
1769
|
-
const content2 = await
|
|
2087
|
+
const content2 = await readFile4(absPath, "utf-8");
|
|
1770
2088
|
if (content2.includes(IMPORT_MARKER)) {
|
|
1771
2089
|
return { action: "exists", file: relPath };
|
|
1772
2090
|
}
|
|
@@ -1778,16 +2096,16 @@ async function setupNextjs(rootDir) {
|
|
|
1778
2096
|
}
|
|
1779
2097
|
async function setupNuxt(rootDir) {
|
|
1780
2098
|
const relPath = "server/plugins/brakit.ts";
|
|
1781
|
-
const absPath =
|
|
2099
|
+
const absPath = join3(rootDir, relPath);
|
|
1782
2100
|
if (await fileExists(absPath)) {
|
|
1783
|
-
const content2 = await
|
|
2101
|
+
const content2 = await readFile4(absPath, "utf-8");
|
|
1784
2102
|
if (content2.includes(IMPORT_MARKER)) {
|
|
1785
2103
|
return { action: "exists", file: relPath };
|
|
1786
2104
|
}
|
|
1787
2105
|
return { action: "manual", file: relPath };
|
|
1788
2106
|
}
|
|
1789
2107
|
const content = BRAKIT_TEMPLATES.nuxt + "\n";
|
|
1790
|
-
const dir =
|
|
2108
|
+
const dir = join3(rootDir, "server/plugins");
|
|
1791
2109
|
const { mkdirSync: mkdirSync3 } = await import("fs");
|
|
1792
2110
|
mkdirSync3(dir, { recursive: true });
|
|
1793
2111
|
await writeFile3(absPath, content);
|
|
@@ -1795,9 +2113,9 @@ async function setupNuxt(rootDir) {
|
|
|
1795
2113
|
}
|
|
1796
2114
|
async function setupPrepend(rootDir, ...candidates) {
|
|
1797
2115
|
for (const relPath of candidates) {
|
|
1798
|
-
const absPath =
|
|
2116
|
+
const absPath = join3(rootDir, relPath);
|
|
1799
2117
|
if (!await fileExists(absPath)) continue;
|
|
1800
|
-
const content = await
|
|
2118
|
+
const content = await readFile4(absPath, "utf-8");
|
|
1801
2119
|
if (content.includes(IMPORT_MARKER)) {
|
|
1802
2120
|
return { action: "exists", file: relPath };
|
|
1803
2121
|
}
|
|
@@ -1809,7 +2127,7 @@ ${content}`);
|
|
|
1809
2127
|
}
|
|
1810
2128
|
async function setupGeneric(rootDir) {
|
|
1811
2129
|
try {
|
|
1812
|
-
const pkgRaw = await
|
|
2130
|
+
const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
|
|
1813
2131
|
const pkg = JSON.parse(pkgRaw);
|
|
1814
2132
|
if (pkg.main && typeof pkg.main === "string") {
|
|
1815
2133
|
const result2 = await setupPrepend(rootDir, pkg.main);
|
|
@@ -1829,29 +2147,29 @@ var MCP_CONFIG = {
|
|
|
1829
2147
|
}
|
|
1830
2148
|
}
|
|
1831
2149
|
};
|
|
1832
|
-
async function setupMcp(rootDir) {
|
|
1833
|
-
const mcpPath =
|
|
2150
|
+
async function setupMcp(rootDir, config = MCP_CONFIG) {
|
|
2151
|
+
const mcpPath = join3(rootDir, ".mcp.json");
|
|
1834
2152
|
if (await fileExists(mcpPath)) {
|
|
1835
|
-
const raw = await
|
|
2153
|
+
const raw = await readFile4(mcpPath, "utf-8");
|
|
1836
2154
|
try {
|
|
1837
|
-
const
|
|
1838
|
-
if (
|
|
1839
|
-
|
|
1840
|
-
await writeFile3(mcpPath, JSON.stringify(
|
|
2155
|
+
const existing = JSON.parse(raw);
|
|
2156
|
+
if (existing?.mcpServers?.brakit) return "exists";
|
|
2157
|
+
existing.mcpServers = { ...existing.mcpServers, ...config.mcpServers };
|
|
2158
|
+
await writeFile3(mcpPath, JSON.stringify(existing, null, 2) + "\n");
|
|
1841
2159
|
await ensureGitignoreEntry(rootDir, ".mcp.json");
|
|
1842
2160
|
return "updated";
|
|
1843
2161
|
} catch {
|
|
1844
2162
|
}
|
|
1845
2163
|
}
|
|
1846
|
-
await writeFile3(mcpPath, JSON.stringify(
|
|
2164
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1847
2165
|
await ensureGitignoreEntry(rootDir, ".mcp.json");
|
|
1848
2166
|
return "created";
|
|
1849
2167
|
}
|
|
1850
2168
|
async function ensureGitignoreEntry(rootDir, entry) {
|
|
1851
|
-
const gitignorePath =
|
|
2169
|
+
const gitignorePath = join3(rootDir, ".gitignore");
|
|
1852
2170
|
try {
|
|
1853
2171
|
if (await fileExists(gitignorePath)) {
|
|
1854
|
-
const content = await
|
|
2172
|
+
const content = await readFile4(gitignorePath, "utf-8");
|
|
1855
2173
|
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
1856
2174
|
await writeFile3(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
1857
2175
|
} else {
|
|
@@ -1860,6 +2178,15 @@ async function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1860
2178
|
} catch {
|
|
1861
2179
|
}
|
|
1862
2180
|
}
|
|
2181
|
+
function findGitRoot(startDir) {
|
|
2182
|
+
let dir = resolve3(startDir);
|
|
2183
|
+
while (true) {
|
|
2184
|
+
if (existsSync5(join3(dir, ".git"))) return dir;
|
|
2185
|
+
const parent = dirname(dir);
|
|
2186
|
+
if (parent === dir) return null;
|
|
2187
|
+
dir = parent;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
1863
2190
|
function printManualInstructions(framework) {
|
|
1864
2191
|
console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
|
|
1865
2192
|
console.log();
|
|
@@ -1879,11 +2206,13 @@ function printManualInstructions(framework) {
|
|
|
1879
2206
|
|
|
1880
2207
|
// src/cli/commands/uninstall.ts
|
|
1881
2208
|
import { defineCommand as defineCommand2 } from "citty";
|
|
1882
|
-
import { resolve as resolve4, join as
|
|
1883
|
-
import { readFile as
|
|
2209
|
+
import { resolve as resolve4, join as join4, relative as relative2 } from "path";
|
|
2210
|
+
import { readFile as readFile5, writeFile as writeFile4, unlink, rm, readdir as readdir2 } from "fs/promises";
|
|
1884
2211
|
import { execSync as execSync2 } from "child_process";
|
|
1885
2212
|
import pc2 from "picocolors";
|
|
1886
2213
|
init_constants();
|
|
2214
|
+
init_log();
|
|
2215
|
+
init_type_guards();
|
|
1887
2216
|
var PREPENDED_FILES = [
|
|
1888
2217
|
"app/entry.server.tsx",
|
|
1889
2218
|
"app/entry.server.ts",
|
|
@@ -1905,85 +2234,142 @@ var uninstall_default = defineCommand2({
|
|
|
1905
2234
|
},
|
|
1906
2235
|
async run({ args }) {
|
|
1907
2236
|
const rootDir = resolve4(args.dir);
|
|
1908
|
-
let
|
|
2237
|
+
let projects = [];
|
|
1909
2238
|
try {
|
|
1910
|
-
|
|
1911
|
-
|
|
2239
|
+
const scanned = await scanForProjects(rootDir);
|
|
2240
|
+
projects = scanned.filter((p) => p.type === "node" && p.node).map((p) => ({ dir: p.dir, pm: p.node.packageManager }));
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
brakitDebug(`uninstall: project scan failed: ${getErrorMessage(err)}`);
|
|
2243
|
+
}
|
|
2244
|
+
if (projects.length === 0) {
|
|
2245
|
+
projects = [{ dir: rootDir, pm: "npm" }];
|
|
1912
2246
|
}
|
|
1913
2247
|
console.log();
|
|
1914
2248
|
console.log(pc2.bold(" \u25C6 brakit uninstall"));
|
|
1915
2249
|
console.log();
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
const
|
|
1919
|
-
if (
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
await unlink(absPath);
|
|
1924
|
-
console.log(pc2.green(` \u2713 Removed ${relPath}`));
|
|
1925
|
-
removed = true;
|
|
1926
|
-
break;
|
|
1927
|
-
}
|
|
1928
|
-
const lines = content.split("\n");
|
|
1929
|
-
const cleaned = lines.filter(
|
|
1930
|
-
(line) => !line.includes('import("brakit")') && !line.includes('import "brakit"')
|
|
1931
|
-
);
|
|
1932
|
-
if (cleaned.length < lines.length) {
|
|
1933
|
-
await writeFile4(absPath, cleaned.join("\n"));
|
|
1934
|
-
console.log(pc2.green(` \u2713 Removed brakit lines from ${relPath}`));
|
|
1935
|
-
removed = true;
|
|
1936
|
-
break;
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
if (!removed) {
|
|
1940
|
-
const candidates = [...PREPENDED_FILES];
|
|
1941
|
-
try {
|
|
1942
|
-
const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
|
|
1943
|
-
const pkg = JSON.parse(pkgRaw);
|
|
1944
|
-
if (pkg.main) candidates.unshift(pkg.main);
|
|
1945
|
-
} catch {
|
|
2250
|
+
for (const project of projects) {
|
|
2251
|
+
const suffix = projects.length > 1 ? ` in ${relative2(rootDir, project.dir) || "."}` : "";
|
|
2252
|
+
const removed = await removeInstrumentation(project.dir);
|
|
2253
|
+
if (removed) {
|
|
2254
|
+
console.log(pc2.green(` \u2713 ${removed}${suffix}`));
|
|
2255
|
+
} else {
|
|
2256
|
+
console.log(pc2.dim(` No brakit instrumentation files found${suffix}.`));
|
|
1946
2257
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (!content.includes(IMPORT_LINE)) continue;
|
|
1952
|
-
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
|
|
1953
|
-
await writeFile4(absPath, updated);
|
|
1954
|
-
console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
|
|
1955
|
-
removed = true;
|
|
1956
|
-
break;
|
|
2258
|
+
const uninstalled = await uninstallPackage(project.dir, project.pm);
|
|
2259
|
+
if (uninstalled === true) {
|
|
2260
|
+
console.log(pc2.green(` \u2713 Removed brakit from devDependencies${suffix}`));
|
|
2261
|
+
} else if (uninstalled === "failed") {
|
|
1957
2262
|
}
|
|
1958
2263
|
}
|
|
1959
|
-
if (!removed) {
|
|
1960
|
-
console.log(pc2.dim(" No brakit instrumentation files found."));
|
|
1961
|
-
}
|
|
1962
2264
|
const mcpRemoved = await removeMcpConfig(rootDir);
|
|
1963
2265
|
if (mcpRemoved) {
|
|
1964
2266
|
console.log(pc2.green(" \u2713 Removed brakit MCP configuration"));
|
|
1965
2267
|
}
|
|
1966
2268
|
const dataRemoved = await removeBrakitData(rootDir);
|
|
1967
2269
|
if (dataRemoved) {
|
|
1968
|
-
console.log(pc2.green(" \u2713 Removed .brakit
|
|
2270
|
+
console.log(pc2.green(" \u2713 Removed .brakit data"));
|
|
1969
2271
|
}
|
|
1970
2272
|
const gitignoreCleaned = await cleanGitignore(rootDir);
|
|
1971
2273
|
if (gitignoreCleaned) {
|
|
1972
2274
|
console.log(pc2.green(" \u2713 Removed .brakit from .gitignore"));
|
|
1973
2275
|
}
|
|
1974
|
-
const
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
console.log(pc2.green(" \u2713 Removed brakit from devDependencies"));
|
|
2276
|
+
const cacheCleared = await clearBuildCaches(rootDir);
|
|
2277
|
+
if (cacheCleared) {
|
|
2278
|
+
console.log(pc2.green(" \u2713 Cleared build cache"));
|
|
1978
2279
|
}
|
|
1979
2280
|
console.log();
|
|
1980
2281
|
}
|
|
1981
2282
|
});
|
|
2283
|
+
async function removeInstrumentation(projectDir) {
|
|
2284
|
+
for (const relPath of CREATED_FILES) {
|
|
2285
|
+
const result2 = await tryRemoveBrakitFromFile(projectDir, relPath);
|
|
2286
|
+
if (result2) return result2;
|
|
2287
|
+
}
|
|
2288
|
+
const candidates = [...PREPENDED_FILES];
|
|
2289
|
+
try {
|
|
2290
|
+
const pkgRaw = await readFile5(join4(projectDir, "package.json"), "utf-8");
|
|
2291
|
+
const pkg = JSON.parse(pkgRaw);
|
|
2292
|
+
if (pkg.main) candidates.unshift(pkg.main);
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
brakitDebug(`uninstall: no package.json main: ${getErrorMessage(err)}`);
|
|
2295
|
+
}
|
|
2296
|
+
for (const relPath of candidates) {
|
|
2297
|
+
const result2 = await tryRemoveImportLine(projectDir, relPath);
|
|
2298
|
+
if (result2) return result2;
|
|
2299
|
+
}
|
|
2300
|
+
const result = await fallbackSearchAndRemove(projectDir);
|
|
2301
|
+
if (result) return result;
|
|
2302
|
+
return null;
|
|
2303
|
+
}
|
|
2304
|
+
async function tryRemoveBrakitFromFile(projectDir, relPath) {
|
|
2305
|
+
const absPath = join4(projectDir, relPath);
|
|
2306
|
+
if (!await fileExists(absPath)) return null;
|
|
2307
|
+
const content = await readFile5(absPath, "utf-8");
|
|
2308
|
+
if (!content.includes("brakit")) return null;
|
|
2309
|
+
if (isExactBrakitTemplate(content)) {
|
|
2310
|
+
await unlink(absPath);
|
|
2311
|
+
return `Removed ${relPath}`;
|
|
2312
|
+
}
|
|
2313
|
+
const lines = content.split("\n");
|
|
2314
|
+
const cleaned = removeBrakitImportLines(lines);
|
|
2315
|
+
if (cleaned.length < lines.length) {
|
|
2316
|
+
await writeFile4(absPath, cleaned.join("\n"));
|
|
2317
|
+
return `Removed brakit lines from ${relPath}`;
|
|
2318
|
+
}
|
|
2319
|
+
return null;
|
|
2320
|
+
}
|
|
2321
|
+
async function tryRemoveImportLine(projectDir, relPath) {
|
|
2322
|
+
const absPath = join4(projectDir, relPath);
|
|
2323
|
+
if (!await fileExists(absPath)) return null;
|
|
2324
|
+
const content = await readFile5(absPath, "utf-8");
|
|
2325
|
+
if (!content.includes(IMPORT_LINE)) return null;
|
|
2326
|
+
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
|
|
2327
|
+
await writeFile4(absPath, updated);
|
|
2328
|
+
return `Removed brakit import from ${relPath}`;
|
|
2329
|
+
}
|
|
2330
|
+
async function fallbackSearchAndRemove(projectDir) {
|
|
2331
|
+
const dirsToScan = FALLBACK_SCAN_DIRS;
|
|
2332
|
+
for (const dir of dirsToScan) {
|
|
2333
|
+
const absDir = join4(projectDir, dir);
|
|
2334
|
+
if (!await fileExists(absDir)) continue;
|
|
2335
|
+
let entries;
|
|
2336
|
+
try {
|
|
2337
|
+
entries = await readdir2(absDir);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
brakitDebug(`uninstall: could not read ${absDir}: ${getErrorMessage(err)}`);
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
for (const entry of entries) {
|
|
2343
|
+
const ext = entry.slice(entry.lastIndexOf("."));
|
|
2344
|
+
if (!SUPPORTED_SOURCE_EXTENSIONS.has(ext)) continue;
|
|
2345
|
+
const relPath = dir === "." ? entry : `${dir}/${entry}`;
|
|
2346
|
+
const absPath = join4(projectDir, relPath);
|
|
2347
|
+
try {
|
|
2348
|
+
const content = await readFile5(absPath, "utf-8");
|
|
2349
|
+
if (!containsBrakitImport(content)) continue;
|
|
2350
|
+
if (isExactBrakitTemplate(content)) {
|
|
2351
|
+
await unlink(absPath);
|
|
2352
|
+
return `Removed ${relPath}`;
|
|
2353
|
+
}
|
|
2354
|
+
const lines = content.split("\n");
|
|
2355
|
+
const cleaned = removeBrakitImportLines(lines);
|
|
2356
|
+
if (cleaned.length < lines.length) {
|
|
2357
|
+
await writeFile4(absPath, cleaned.join("\n"));
|
|
2358
|
+
return `Removed brakit import from ${relPath}`;
|
|
2359
|
+
}
|
|
2360
|
+
} catch (err) {
|
|
2361
|
+
brakitDebug(`uninstall: fallback scan failed for ${relPath}: ${getErrorMessage(err)}`);
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
1982
2368
|
async function removeMcpConfig(rootDir) {
|
|
1983
|
-
const mcpPath =
|
|
2369
|
+
const mcpPath = join4(rootDir, ".mcp.json");
|
|
1984
2370
|
if (!await fileExists(mcpPath)) return false;
|
|
1985
2371
|
try {
|
|
1986
|
-
const raw = await
|
|
2372
|
+
const raw = await readFile5(mcpPath, "utf-8");
|
|
1987
2373
|
const config = JSON.parse(raw);
|
|
1988
2374
|
if (!config?.mcpServers?.brakit) return false;
|
|
1989
2375
|
delete config.mcpServers.brakit;
|
|
@@ -1993,16 +2379,18 @@ async function removeMcpConfig(rootDir) {
|
|
|
1993
2379
|
await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1994
2380
|
}
|
|
1995
2381
|
return true;
|
|
1996
|
-
} catch {
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
brakitDebug(`uninstall: MCP config cleanup failed: ${getErrorMessage(err)}`);
|
|
1997
2384
|
return false;
|
|
1998
2385
|
}
|
|
1999
2386
|
}
|
|
2000
2387
|
async function uninstallPackage(rootDir, pm) {
|
|
2001
2388
|
try {
|
|
2002
|
-
const pkgRaw = await
|
|
2389
|
+
const pkgRaw = await readFile5(join4(rootDir, "package.json"), "utf-8");
|
|
2003
2390
|
const pkg = JSON.parse(pkgRaw);
|
|
2004
2391
|
if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
|
|
2005
|
-
} catch {
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
brakitDebug(`uninstall: could not read package.json: ${getErrorMessage(err)}`);
|
|
2006
2394
|
return false;
|
|
2007
2395
|
}
|
|
2008
2396
|
const cmds = {
|
|
@@ -2014,35 +2402,63 @@ async function uninstallPackage(rootDir, pm) {
|
|
|
2014
2402
|
const cmd = cmds[pm] ?? cmds.npm;
|
|
2015
2403
|
try {
|
|
2016
2404
|
execSync2(cmd, { cwd: rootDir, stdio: "pipe" });
|
|
2405
|
+
return true;
|
|
2017
2406
|
} catch {
|
|
2018
2407
|
console.warn(pc2.yellow(` \u26A0 Failed to run "${cmd}". Remove brakit manually.`));
|
|
2408
|
+
return "failed";
|
|
2019
2409
|
}
|
|
2020
|
-
return true;
|
|
2021
2410
|
}
|
|
2022
2411
|
async function removeBrakitData(rootDir) {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2412
|
+
let removed = false;
|
|
2413
|
+
const projectDir = join4(rootDir, METRICS_DIR);
|
|
2414
|
+
if (await fileExists(projectDir)) {
|
|
2415
|
+
try {
|
|
2416
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
2417
|
+
removed = true;
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
brakitDebug(`uninstall: could not remove ${projectDir}: ${getErrorMessage(err)}`);
|
|
2420
|
+
}
|
|
2030
2421
|
}
|
|
2422
|
+
const homeDataDir = getProjectDataDir(rootDir);
|
|
2423
|
+
if (await fileExists(homeDataDir)) {
|
|
2424
|
+
try {
|
|
2425
|
+
await rm(homeDataDir, { recursive: true, force: true });
|
|
2426
|
+
removed = true;
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
brakitDebug(`uninstall: could not remove ${homeDataDir}: ${getErrorMessage(err)}`);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return removed;
|
|
2031
2432
|
}
|
|
2032
2433
|
async function cleanGitignore(rootDir) {
|
|
2033
|
-
const gitignorePath =
|
|
2434
|
+
const gitignorePath = join4(rootDir, ".gitignore");
|
|
2034
2435
|
if (!await fileExists(gitignorePath)) return false;
|
|
2035
2436
|
try {
|
|
2036
|
-
const content = await
|
|
2437
|
+
const content = await readFile5(gitignorePath, "utf-8");
|
|
2037
2438
|
const lines = content.split("\n");
|
|
2038
2439
|
const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
|
|
2039
2440
|
if (filtered.length === lines.length) return false;
|
|
2040
2441
|
await writeFile4(gitignorePath, filtered.join("\n"));
|
|
2041
2442
|
return true;
|
|
2042
|
-
} catch {
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
brakitDebug(`uninstall: gitignore cleanup failed: ${getErrorMessage(err)}`);
|
|
2043
2445
|
return false;
|
|
2044
2446
|
}
|
|
2045
2447
|
}
|
|
2448
|
+
async function clearBuildCaches(rootDir) {
|
|
2449
|
+
let cleared = false;
|
|
2450
|
+
for (const dir of BUILD_CACHE_DIRS) {
|
|
2451
|
+
const absDir = join4(rootDir, dir);
|
|
2452
|
+
if (!await fileExists(absDir)) continue;
|
|
2453
|
+
try {
|
|
2454
|
+
await rm(absDir, { recursive: true, force: true });
|
|
2455
|
+
cleared = true;
|
|
2456
|
+
} catch (err) {
|
|
2457
|
+
brakitDebug(`uninstall: could not clear cache ${absDir}: ${getErrorMessage(err)}`);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
return cleared;
|
|
2461
|
+
}
|
|
2046
2462
|
|
|
2047
2463
|
// bin/brakit.ts
|
|
2048
2464
|
var sub = process.argv[2];
|