brakit 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +47 -19
- package/dist/api.js +127 -31
- package/dist/bin/brakit.js +425 -60
- package/dist/dashboard-client.global.js +524 -465
- package/dist/dashboard.html +623 -513
- package/dist/mcp/server.js +175 -22
- package/dist/runtime/index.js +1758 -1458
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import { IncomingHttpHeaders } from 'node:http';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Canonical type definitions shared between server and browser client.
|
|
5
|
+
*
|
|
6
|
+
* Both `src/types/telemetry.ts` (server) and
|
|
7
|
+
* `src/dashboard/client/store/types.ts` (browser) import from here.
|
|
8
|
+
* This is the single source of truth for union types used across the
|
|
9
|
+
* bundling boundary.
|
|
10
|
+
*/
|
|
11
|
+
type DbDriver = "pg" | "mysql2" | "prisma" | "asyncpg" | "sqlalchemy" | "sdk";
|
|
12
|
+
type LogLevel = "log" | "warn" | "error" | "info" | "debug";
|
|
13
|
+
type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
|
|
14
|
+
type Severity = "critical" | "warning" | "info";
|
|
15
|
+
/**
|
|
16
|
+
* State machine for issue lifecycle:
|
|
17
|
+
*
|
|
18
|
+
* open ──→ fixing ──→ resolved
|
|
19
|
+
* │ │ │
|
|
20
|
+
* │ ▼ ▼
|
|
21
|
+
* │ regressed (pruned after PRUNE_ISSUE_TTL_MS)
|
|
22
|
+
* │
|
|
23
|
+
* └──→ stale (no traffic for STALE_ISSUE_TTL_MS)
|
|
24
|
+
*/
|
|
25
|
+
type IssueState = "open" | "fixing" | "resolved" | "stale" | "regressed";
|
|
26
|
+
type IssueSource = "passive";
|
|
27
|
+
type IssueCategory = "security" | "performance" | "reliability";
|
|
28
|
+
type AiFixStatus = "fixed" | "wont_fix";
|
|
29
|
+
interface SourceLocation {
|
|
30
|
+
file: string;
|
|
31
|
+
line: number;
|
|
32
|
+
column?: number;
|
|
33
|
+
fn?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
3
36
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
4
37
|
type FlatHeaders = Record<string, string>;
|
|
5
38
|
interface TracedRequest {
|
|
@@ -20,7 +53,7 @@ interface TracedRequest {
|
|
|
20
53
|
}
|
|
21
54
|
type RequestListener = (req: TracedRequest) => void;
|
|
22
55
|
|
|
23
|
-
type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "flask" | "fastapi" | "django" | "custom" | "unknown";
|
|
56
|
+
type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "express" | "fastify" | "koa" | "hono" | "nestjs" | "hapi" | "adonis" | "sails" | "flask" | "fastapi" | "django" | "custom" | "unknown";
|
|
24
57
|
interface DetectedProject {
|
|
25
58
|
framework: Framework;
|
|
26
59
|
devCommand: string;
|
|
@@ -69,9 +102,10 @@ interface TracedFetch extends TelemetryEntry {
|
|
|
69
102
|
method: string;
|
|
70
103
|
statusCode: number;
|
|
71
104
|
durationMs: number;
|
|
105
|
+
callSite?: SourceLocation;
|
|
72
106
|
}
|
|
73
107
|
interface TracedLog extends TelemetryEntry {
|
|
74
|
-
level:
|
|
108
|
+
level: LogLevel;
|
|
75
109
|
message: string;
|
|
76
110
|
}
|
|
77
111
|
interface TracedError extends TelemetryEntry {
|
|
@@ -79,9 +113,8 @@ interface TracedError extends TelemetryEntry {
|
|
|
79
113
|
message: string;
|
|
80
114
|
stack?: string;
|
|
81
115
|
}
|
|
82
|
-
type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
|
|
83
116
|
interface TracedQuery extends TelemetryEntry {
|
|
84
|
-
driver:
|
|
117
|
+
driver: DbDriver;
|
|
85
118
|
sql?: string;
|
|
86
119
|
model?: string;
|
|
87
120
|
operation?: string;
|
|
@@ -91,6 +124,7 @@ interface TracedQuery extends TelemetryEntry {
|
|
|
91
124
|
table?: string;
|
|
92
125
|
source?: string;
|
|
93
126
|
parentFetchId?: string;
|
|
127
|
+
callSite?: SourceLocation;
|
|
94
128
|
}
|
|
95
129
|
type TelemetryEvent = {
|
|
96
130
|
type: "fetch";
|
|
@@ -157,8 +191,6 @@ interface RequestMetrics {
|
|
|
157
191
|
fetchTimeMs: number;
|
|
158
192
|
}
|
|
159
193
|
|
|
160
|
-
/** Shared severity levels used by both security findings and insights. */
|
|
161
|
-
type Severity = "critical" | "warning" | "info";
|
|
162
194
|
type SecuritySeverity = Severity;
|
|
163
195
|
interface SecurityFinding {
|
|
164
196
|
severity: SecuritySeverity;
|
|
@@ -171,10 +203,6 @@ interface SecurityFinding {
|
|
|
171
203
|
count: number;
|
|
172
204
|
}
|
|
173
205
|
|
|
174
|
-
type IssueState = "open" | "fixing" | "resolved" | "stale" | "regressed";
|
|
175
|
-
type IssueSource = "passive";
|
|
176
|
-
type IssueCategory = "security" | "performance" | "reliability";
|
|
177
|
-
type AiFixStatus = "fixed" | "wont_fix";
|
|
178
206
|
interface Issue {
|
|
179
207
|
category: IssueCategory;
|
|
180
208
|
/** Rule identifier: "slow", "n1", "exposed-secret", etc. */
|
|
@@ -188,6 +216,8 @@ interface Issue {
|
|
|
188
216
|
endpoint?: string;
|
|
189
217
|
/** Dashboard tab to link to (e.g., "requests", "queries", "security"). */
|
|
190
218
|
nav?: string;
|
|
219
|
+
/** Occurrence count for rules that aggregate (e.g., N+1 query count). */
|
|
220
|
+
count?: number;
|
|
191
221
|
}
|
|
192
222
|
interface StatefulIssue {
|
|
193
223
|
/** Stable ID derived from rule + endpoint + description hash. */
|
|
@@ -202,12 +232,11 @@ interface StatefulIssue {
|
|
|
202
232
|
occurrences: number;
|
|
203
233
|
/**
|
|
204
234
|
* Number of requests to this endpoint that did NOT reproduce the issue
|
|
205
|
-
* since it was last seen. Used for evidence-based resolution
|
|
235
|
+
* since it was last seen. Used for evidence-based resolution:
|
|
236
|
+
* after CLEAN_HITS_FOR_RESOLUTION clean hits the issue auto-resolves.
|
|
206
237
|
*/
|
|
207
238
|
cleanHitsSinceLastSeen: number;
|
|
208
|
-
/** What AI reported after attempting a fix. */
|
|
209
239
|
aiStatus: AiFixStatus | null;
|
|
210
|
-
/** AI's summary of what was done or why it can't be fixed. */
|
|
211
240
|
aiNotes: string | null;
|
|
212
241
|
}
|
|
213
242
|
interface IssuesData {
|
|
@@ -227,10 +256,11 @@ declare class IssueStore {
|
|
|
227
256
|
stop(): void;
|
|
228
257
|
upsert(issue: Issue, source: IssueSource): StatefulIssue;
|
|
229
258
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
259
|
+
* Evidence-based reconciliation: for each active issue whose endpoint had
|
|
260
|
+
* traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
|
|
261
|
+
* After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
|
|
262
|
+
* Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
|
|
263
|
+
* Resolved and stale issues are pruned after their respective TTLs expire.
|
|
234
264
|
*/
|
|
235
265
|
reconcile(currentIssueIds: Set<string>, activeEndpoints: Set<string>): void;
|
|
236
266
|
transition(issueId: string, state: IssueState): boolean;
|
|
@@ -244,7 +274,6 @@ declare class IssueStore {
|
|
|
244
274
|
private loadAsync;
|
|
245
275
|
/** Sync load for tests only — not used in production paths. */
|
|
246
276
|
loadSync(): void;
|
|
247
|
-
/** Parse and populate issues from a raw JSON string. */
|
|
248
277
|
private hydrate;
|
|
249
278
|
private flush;
|
|
250
279
|
private flushSync;
|
|
@@ -407,7 +436,6 @@ declare class RequestStore {
|
|
|
407
436
|
}
|
|
408
437
|
|
|
409
438
|
type TelemetryListener<T> = (entry: T) => void;
|
|
410
|
-
/** Read-only view of a TelemetryStore — used by API handlers that only query data. */
|
|
411
439
|
interface ReadonlyTelemetryStore {
|
|
412
440
|
getAll(): readonly TelemetryEntry[];
|
|
413
441
|
getByRequest(requestId: string): TelemetryEntry[];
|
package/dist/api.js
CHANGED
|
@@ -232,7 +232,7 @@ var IssueStore = class {
|
|
|
232
232
|
existing.occurrences++;
|
|
233
233
|
existing.issue = issue;
|
|
234
234
|
existing.cleanHitsSinceLastSeen = 0;
|
|
235
|
-
if (existing.state === "resolved" || existing.state === "stale") {
|
|
235
|
+
if (existing.aiStatus !== "wont_fix" && (existing.state === "resolved" || existing.state === "stale")) {
|
|
236
236
|
existing.state = "regressed";
|
|
237
237
|
existing.resolvedAt = null;
|
|
238
238
|
}
|
|
@@ -258,10 +258,11 @@ var IssueStore = class {
|
|
|
258
258
|
return stateful;
|
|
259
259
|
}
|
|
260
260
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
261
|
+
* Evidence-based reconciliation: for each active issue whose endpoint had
|
|
262
|
+
* traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
|
|
263
|
+
* After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
|
|
264
|
+
* Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
|
|
265
|
+
* Resolved and stale issues are pruned after their respective TTLs expire.
|
|
265
266
|
*/
|
|
266
267
|
reconcile(currentIssueIds, activeEndpoints) {
|
|
267
268
|
const now = Date.now();
|
|
@@ -359,9 +360,15 @@ var IssueStore = class {
|
|
|
359
360
|
brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
|
|
360
361
|
}
|
|
361
362
|
}
|
|
362
|
-
/** Parse and populate issues from a raw JSON string. */
|
|
363
363
|
hydrate(raw) {
|
|
364
|
-
|
|
364
|
+
let parsed;
|
|
365
|
+
try {
|
|
366
|
+
parsed = JSON.parse(raw);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
brakitDebug(`IssueStore: corrupt JSON in issues file, starting fresh: ${err}`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const validated = validateIssuesData(parsed);
|
|
365
372
|
if (!validated) return;
|
|
366
373
|
for (const issue of validated.issues) {
|
|
367
374
|
this.issues.set(issue.issueId, issue);
|
|
@@ -374,8 +381,12 @@ var IssueStore = class {
|
|
|
374
381
|
}
|
|
375
382
|
flushSync() {
|
|
376
383
|
if (!this.dirty) return;
|
|
377
|
-
|
|
378
|
-
|
|
384
|
+
try {
|
|
385
|
+
this.writer.writeSync(this.serialize());
|
|
386
|
+
this.dirty = false;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
brakitDebug(`IssueStore: flush failed, will retry: ${err}`);
|
|
389
|
+
}
|
|
379
390
|
}
|
|
380
391
|
serialize() {
|
|
381
392
|
const data = {
|
|
@@ -390,12 +401,102 @@ var IssueStore = class {
|
|
|
390
401
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
391
402
|
import { existsSync as existsSync4 } from "fs";
|
|
392
403
|
import { join as join2, relative } from "path";
|
|
404
|
+
|
|
405
|
+
// src/constants/detection.ts
|
|
406
|
+
var KNOWN_DEPENDENCY_NAMES = [
|
|
407
|
+
// -- Frameworks (meta) --
|
|
408
|
+
"next",
|
|
409
|
+
"@remix-run/dev",
|
|
410
|
+
"nuxt",
|
|
411
|
+
"astro",
|
|
412
|
+
// -- Frameworks (backend) --
|
|
413
|
+
"@nestjs/core",
|
|
414
|
+
"@adonisjs/core",
|
|
415
|
+
"sails",
|
|
416
|
+
"express",
|
|
417
|
+
"fastify",
|
|
418
|
+
"hono",
|
|
419
|
+
"koa",
|
|
420
|
+
"@hapi/hapi",
|
|
421
|
+
"elysia",
|
|
422
|
+
"h3",
|
|
423
|
+
"nitro",
|
|
424
|
+
"@trpc/server",
|
|
425
|
+
// -- Bundlers --
|
|
426
|
+
"vite",
|
|
427
|
+
// -- ORM / query builders --
|
|
428
|
+
"prisma",
|
|
429
|
+
"@prisma/client",
|
|
430
|
+
"drizzle-orm",
|
|
431
|
+
"typeorm",
|
|
432
|
+
"sequelize",
|
|
433
|
+
"mongoose",
|
|
434
|
+
"kysely",
|
|
435
|
+
"knex",
|
|
436
|
+
"@mikro-orm/core",
|
|
437
|
+
"objection",
|
|
438
|
+
// -- DB drivers --
|
|
439
|
+
"pg",
|
|
440
|
+
"mysql2",
|
|
441
|
+
"mongodb",
|
|
442
|
+
"better-sqlite3",
|
|
443
|
+
"@libsql/client",
|
|
444
|
+
"@planetscale/database",
|
|
445
|
+
"ioredis",
|
|
446
|
+
"redis",
|
|
447
|
+
// -- Auth --
|
|
448
|
+
"lucia",
|
|
449
|
+
"next-auth",
|
|
450
|
+
"@auth/core",
|
|
451
|
+
"passport",
|
|
452
|
+
// -- Queues / messaging --
|
|
453
|
+
"bullmq",
|
|
454
|
+
"amqplib",
|
|
455
|
+
"kafkajs",
|
|
456
|
+
// -- Validation --
|
|
457
|
+
"zod",
|
|
458
|
+
"joi",
|
|
459
|
+
"yup",
|
|
460
|
+
"arktype",
|
|
461
|
+
"valibot",
|
|
462
|
+
// -- HTTP clients --
|
|
463
|
+
"axios",
|
|
464
|
+
"got",
|
|
465
|
+
"ky",
|
|
466
|
+
"undici",
|
|
467
|
+
// -- Realtime --
|
|
468
|
+
"socket.io",
|
|
469
|
+
"ws",
|
|
470
|
+
// -- CSS / styling --
|
|
471
|
+
"tailwindcss",
|
|
472
|
+
// -- Testing --
|
|
473
|
+
"vitest",
|
|
474
|
+
"jest",
|
|
475
|
+
"mocha",
|
|
476
|
+
// -- Runtime indicators --
|
|
477
|
+
"bun-types",
|
|
478
|
+
"@types/bun"
|
|
479
|
+
];
|
|
480
|
+
var KNOWN_DEPENDENCY_SET = new Set(KNOWN_DEPENDENCY_NAMES);
|
|
481
|
+
|
|
482
|
+
// src/detect/project.ts
|
|
393
483
|
var FRAMEWORKS = [
|
|
484
|
+
// Meta-frameworks first (they bundle Express/Vite internally)
|
|
394
485
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
395
486
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
396
487
|
{ name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
397
|
-
{ name: "
|
|
398
|
-
{ name: "
|
|
488
|
+
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] },
|
|
489
|
+
{ name: "nestjs", dep: "@nestjs/core", devCmd: "nest start", bin: "nest", defaultPort: 3e3, devArgs: ["--watch"] },
|
|
490
|
+
{ name: "adonis", dep: "@adonisjs/core", devCmd: "node ace serve", bin: "ace", defaultPort: 3333, devArgs: ["serve", "--watch"] },
|
|
491
|
+
{ name: "sails", dep: "sails", devCmd: "sails lift", bin: "sails", defaultPort: 1337, devArgs: ["lift"] },
|
|
492
|
+
// Server frameworks
|
|
493
|
+
{ name: "hono", dep: "hono", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
494
|
+
{ name: "fastify", dep: "fastify", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
495
|
+
{ name: "koa", dep: "koa", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
496
|
+
{ name: "hapi", dep: "@hapi/hapi", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
497
|
+
{ name: "express", dep: "express", devCmd: "node", bin: "node", defaultPort: 3e3 },
|
|
498
|
+
// Bundlers (last — likely used alongside a framework above)
|
|
499
|
+
{ name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] }
|
|
399
500
|
];
|
|
400
501
|
async function detectProject(rootDir) {
|
|
401
502
|
const pkgPath = join2(rootDir, "package.json");
|
|
@@ -407,23 +508,22 @@ async function detectProject(rootDir) {
|
|
|
407
508
|
const devCommand = matched?.devCmd ?? "";
|
|
408
509
|
const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
|
|
409
510
|
const defaultPort = matched?.defaultPort ?? 3e3;
|
|
410
|
-
const packageManager =
|
|
511
|
+
const packageManager = detectPackageManager(rootDir);
|
|
411
512
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
412
513
|
}
|
|
413
|
-
async function detectPackageManager(rootDir) {
|
|
414
|
-
if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
|
|
415
|
-
if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
|
|
416
|
-
if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
417
|
-
if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
418
|
-
if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
|
|
419
|
-
return "unknown";
|
|
420
|
-
}
|
|
421
514
|
function detectFrameworkFromDeps(allDeps) {
|
|
422
515
|
for (const f of FRAMEWORKS) {
|
|
423
516
|
if (allDeps[f.dep]) return f.name;
|
|
424
517
|
}
|
|
425
518
|
return "unknown";
|
|
426
519
|
}
|
|
520
|
+
function detectPackageManager(rootDir) {
|
|
521
|
+
if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
|
|
522
|
+
if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
523
|
+
if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
524
|
+
if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
|
|
525
|
+
return "unknown";
|
|
526
|
+
}
|
|
427
527
|
|
|
428
528
|
// src/instrument/adapter-registry.ts
|
|
429
529
|
var AdapterRegistry = class {
|
|
@@ -1133,14 +1233,14 @@ function createDefaultScanner() {
|
|
|
1133
1233
|
// src/core/disposable.ts
|
|
1134
1234
|
var SubscriptionBag = class {
|
|
1135
1235
|
constructor() {
|
|
1136
|
-
this.items =
|
|
1236
|
+
this.items = /* @__PURE__ */ new Set();
|
|
1137
1237
|
}
|
|
1138
1238
|
add(teardown) {
|
|
1139
|
-
this.items.
|
|
1239
|
+
this.items.add(typeof teardown === "function" ? { dispose: teardown } : teardown);
|
|
1140
1240
|
}
|
|
1141
1241
|
dispose() {
|
|
1142
1242
|
for (const d of this.items) d.dispose();
|
|
1143
|
-
this.items.
|
|
1243
|
+
this.items.clear();
|
|
1144
1244
|
}
|
|
1145
1245
|
};
|
|
1146
1246
|
|
|
@@ -1170,14 +1270,10 @@ var DASHBOARD_API_GRAPH = `${DASHBOARD_PREFIX}/api/graph`;
|
|
|
1170
1270
|
var VALID_TABS_TUPLE = [
|
|
1171
1271
|
"overview",
|
|
1172
1272
|
"actions",
|
|
1173
|
-
"
|
|
1174
|
-
"fetches",
|
|
1175
|
-
"queries",
|
|
1176
|
-
"errors",
|
|
1177
|
-
"logs",
|
|
1273
|
+
"insights",
|
|
1178
1274
|
"performance",
|
|
1179
|
-
"
|
|
1180
|
-
"
|
|
1275
|
+
"graph",
|
|
1276
|
+
"explorer"
|
|
1181
1277
|
];
|
|
1182
1278
|
var VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
1183
1279
|
|
|
@@ -2411,7 +2507,7 @@ var AnalysisEngine = class {
|
|
|
2411
2507
|
};
|
|
2412
2508
|
|
|
2413
2509
|
// src/index.ts
|
|
2414
|
-
var VERSION = "0.10.
|
|
2510
|
+
var VERSION = "0.10.2";
|
|
2415
2511
|
export {
|
|
2416
2512
|
AdapterRegistry,
|
|
2417
2513
|
AnalysisEngine,
|