brakit 0.8.5 → 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/dist/api.js CHANGED
@@ -1,12 +1,54 @@
1
- // src/store/finding-store.ts
1
+ // src/store/issue-store.ts
2
2
  import { readFile as readFile2 } from "fs/promises";
3
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
3
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
4
4
  import { resolve as resolve2 } from "path";
5
5
 
6
6
  // src/utils/fs.ts
7
7
  import { access, readFile, writeFile } from "fs/promises";
8
8
  import { existsSync, readFileSync, writeFileSync } from "fs";
9
- import { resolve } from "path";
9
+ import { createHash } from "crypto";
10
+ import { homedir } from "os";
11
+ import { resolve, join } from "path";
12
+
13
+ // src/constants/limits.ts
14
+ var ANALYSIS_DEBOUNCE_MS = 300;
15
+ var ISSUE_ID_HASH_LENGTH = 16;
16
+ var ISSUES_DATA_VERSION = 2;
17
+ var SECRET_SCAN_ARRAY_LIMIT = 5;
18
+ var PII_SCAN_ARRAY_LIMIT = 10;
19
+ var MIN_SECRET_VALUE_LENGTH = 8;
20
+ var FULL_RECORD_MIN_FIELDS = 5;
21
+ var LIST_PII_MIN_ITEMS = 2;
22
+ var MAX_OBJECT_SCAN_DEPTH = 5;
23
+ var ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
24
+
25
+ // src/utils/log.ts
26
+ var PREFIX = "[brakit]";
27
+ function brakitWarn(message) {
28
+ process.stderr.write(`${PREFIX} ${message}
29
+ `);
30
+ }
31
+ function brakitDebug(message) {
32
+ if (process.env.DEBUG_BRAKIT) {
33
+ process.stderr.write(`${PREFIX}:debug ${message}
34
+ `);
35
+ }
36
+ }
37
+
38
+ // src/utils/type-guards.ts
39
+ function getErrorMessage(err) {
40
+ if (err instanceof Error) return err.message;
41
+ if (typeof err === "string") return err;
42
+ return String(err);
43
+ }
44
+ function validateIssuesData(parsed) {
45
+ if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
46
+ return parsed;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ // src/utils/fs.ts
10
52
  async function fileExists(path) {
11
53
  try {
12
54
  await access(path);
@@ -25,7 +67,8 @@ function ensureGitignore(dir, entry) {
25
67
  } else {
26
68
  writeFileSync(gitignorePath, entry + "\n");
27
69
  }
28
- } catch {
70
+ } catch (err) {
71
+ brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
29
72
  }
30
73
  }
31
74
  async function ensureGitignoreAsync(dir, entry) {
@@ -38,46 +81,14 @@ async function ensureGitignoreAsync(dir, entry) {
38
81
  } else {
39
82
  await writeFile(gitignorePath, entry + "\n");
40
83
  }
41
- } catch {
84
+ } catch (err) {
85
+ brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
42
86
  }
43
87
  }
44
88
 
45
- // src/constants/routes.ts
46
- var DASHBOARD_PREFIX = "/__brakit";
47
- var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
48
- var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
49
- var DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
50
- var DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
51
- var DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
52
- var DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
53
- var DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
54
- var DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
55
- var DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
56
- var DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
57
- var DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
58
- var DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
59
- var DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
60
- var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
61
- var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
62
- var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
63
- var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
64
- var VALID_TABS_TUPLE = [
65
- "overview",
66
- "actions",
67
- "requests",
68
- "fetches",
69
- "queries",
70
- "errors",
71
- "logs",
72
- "performance",
73
- "security"
74
- ];
75
- var VALID_TABS = new Set(VALID_TABS_TUPLE);
76
-
77
- // src/constants/limits.ts
78
- var ANALYSIS_DEBOUNCE_MS = 300;
79
- var FINDING_ID_HASH_LENGTH = 16;
80
- var FINDINGS_DATA_VERSION = 1;
89
+ // src/constants/metrics.ts
90
+ var ISSUES_FILE = "issues.json";
91
+ var ISSUES_FLUSH_INTERVAL_MS = 1e4;
81
92
 
82
93
  // src/constants/thresholds.ts
83
94
  var FLOW_GAP_MS = 5e3;
@@ -106,17 +117,9 @@ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
106
117
  var OVERFETCH_MANY_FIELDS = 12;
107
118
  var OVERFETCH_UNWRAP_MIN_SIZE = 3;
108
119
  var MAX_DUPLICATE_INSIGHTS = 3;
109
- var INSIGHT_WINDOW_PER_ENDPOINT = 2;
110
- var RESOLVE_AFTER_ABSENCES = 3;
111
- var RESOLVED_INSIGHT_TTL_MS = 18e5;
112
-
113
- // src/constants/metrics.ts
114
- var METRICS_DIR = ".brakit";
115
- var FINDINGS_FILE = ".brakit/findings.json";
116
- var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
117
-
118
- // src/constants/network.ts
119
- var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
120
+ var INSIGHT_WINDOW_PER_ENDPOINT = 20;
121
+ var CLEAN_HITS_FOR_RESOLUTION = 5;
122
+ var STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
120
123
 
121
124
  // src/utils/atomic-writer.ts
122
125
  import {
@@ -126,28 +129,6 @@ import {
126
129
  renameSync
127
130
  } from "fs";
128
131
  import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
129
-
130
- // src/utils/log.ts
131
- var PREFIX = "[brakit]";
132
- function brakitWarn(message) {
133
- process.stderr.write(`${PREFIX} ${message}
134
- `);
135
- }
136
- function brakitDebug(message) {
137
- if (process.env.DEBUG_BRAKIT) {
138
- process.stderr.write(`${PREFIX}:debug ${message}
139
- `);
140
- }
141
- }
142
-
143
- // src/utils/type-guards.ts
144
- function getErrorMessage(err) {
145
- if (err instanceof Error) return err.message;
146
- if (typeof err === "string") return err;
147
- return String(err);
148
- }
149
-
150
- // src/utils/atomic-writer.ts
151
132
  var AtomicWriter = class {
152
133
  constructor(opts) {
153
134
  this.opts = opts;
@@ -205,41 +186,35 @@ var AtomicWriter = class {
205
186
  }
206
187
  };
207
188
 
208
- // src/store/finding-id.ts
209
- import { createHash } from "crypto";
210
- function computeFindingId(finding) {
211
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
212
- return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
213
- }
214
- function computeInsightId(type, endpoint, desc) {
215
- const key = `${type}:${endpoint}:${desc}`;
216
- return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
189
+ // src/utils/issue-id.ts
190
+ import { createHash as createHash2 } from "crypto";
191
+ function computeIssueId(issue) {
192
+ const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
193
+ const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
194
+ return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
217
195
  }
218
196
 
219
- // src/store/finding-store.ts
220
- var FindingStore = class {
221
- constructor(rootDir) {
222
- this.rootDir = rootDir;
223
- const metricsDir = resolve2(rootDir, METRICS_DIR);
224
- this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
197
+ // src/store/issue-store.ts
198
+ var IssueStore = class {
199
+ constructor(dataDir) {
200
+ this.dataDir = dataDir;
201
+ this.issuesPath = resolve2(dataDir, ISSUES_FILE);
225
202
  this.writer = new AtomicWriter({
226
- dir: metricsDir,
227
- filePath: this.findingsPath,
228
- gitignoreEntry: METRICS_DIR,
229
- label: "findings"
203
+ dir: dataDir,
204
+ filePath: this.issuesPath,
205
+ label: "issues"
230
206
  });
231
207
  }
232
- findings = /* @__PURE__ */ new Map();
208
+ issues = /* @__PURE__ */ new Map();
233
209
  flushTimer = null;
234
210
  dirty = false;
235
211
  writer;
236
- findingsPath;
212
+ issuesPath;
237
213
  start() {
238
- this.loadAsync().catch(() => {
239
- });
214
+ this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
240
215
  this.flushTimer = setInterval(
241
216
  () => this.flush(),
242
- FINDINGS_FLUSH_INTERVAL_MS
217
+ ISSUES_FLUSH_INTERVAL_MS
243
218
  );
244
219
  this.flushTimer.unref();
245
220
  }
@@ -250,119 +225,148 @@ var FindingStore = class {
250
225
  }
251
226
  this.flushSync();
252
227
  }
253
- upsert(finding, source) {
254
- const id = computeFindingId(finding);
255
- const existing = this.findings.get(id);
228
+ upsert(issue, source) {
229
+ const id = computeIssueId(issue);
230
+ const existing = this.issues.get(id);
256
231
  const now = Date.now();
257
232
  if (existing) {
258
233
  existing.lastSeenAt = now;
259
234
  existing.occurrences++;
260
- existing.finding = finding;
261
- if (existing.state === "resolved") {
262
- existing.state = "open";
235
+ existing.issue = issue;
236
+ existing.cleanHitsSinceLastSeen = 0;
237
+ if (existing.state === "resolved" || existing.state === "stale") {
238
+ existing.state = "regressed";
263
239
  existing.resolvedAt = null;
264
240
  }
265
241
  this.dirty = true;
266
242
  return existing;
267
243
  }
268
244
  const stateful = {
269
- findingId: id,
245
+ issueId: id,
270
246
  state: "open",
271
247
  source,
272
- finding,
248
+ category: issue.category,
249
+ issue,
273
250
  firstSeenAt: now,
274
251
  lastSeenAt: now,
275
252
  resolvedAt: null,
276
253
  occurrences: 1,
254
+ cleanHitsSinceLastSeen: 0,
277
255
  aiStatus: null,
278
256
  aiNotes: null
279
257
  };
280
- this.findings.set(id, stateful);
258
+ this.issues.set(id, stateful);
281
259
  this.dirty = true;
282
260
  return stateful;
283
261
  }
284
- transition(findingId, state) {
285
- const finding = this.findings.get(findingId);
286
- if (!finding) return false;
287
- finding.state = state;
262
+ /**
263
+ * Reconcile issues against the current analysis results using evidence-based resolution.
264
+ *
265
+ * @param currentIssueIds - IDs of issues detected in the current analysis cycle
266
+ * @param activeEndpoints - Endpoints that had requests in the current cycle
267
+ */
268
+ reconcile(currentIssueIds, activeEndpoints) {
269
+ const now = Date.now();
270
+ for (const [, stateful] of this.issues) {
271
+ const isActive = stateful.state === "open" || stateful.state === "fixing" || stateful.state === "regressed";
272
+ if (!isActive) continue;
273
+ if (currentIssueIds.has(stateful.issueId)) continue;
274
+ const endpoint = stateful.issue.endpoint;
275
+ if (endpoint && activeEndpoints.has(endpoint)) {
276
+ stateful.cleanHitsSinceLastSeen++;
277
+ if (stateful.cleanHitsSinceLastSeen >= CLEAN_HITS_FOR_RESOLUTION) {
278
+ stateful.state = "resolved";
279
+ stateful.resolvedAt = now;
280
+ }
281
+ this.dirty = true;
282
+ } else if (now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS) {
283
+ stateful.state = "stale";
284
+ this.dirty = true;
285
+ }
286
+ }
287
+ for (const [id, stateful] of this.issues) {
288
+ if (stateful.state === "resolved" && stateful.resolvedAt && now - stateful.resolvedAt > ISSUE_PRUNE_TTL_MS) {
289
+ this.issues.delete(id);
290
+ this.dirty = true;
291
+ } else if (stateful.state === "stale" && now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS + ISSUE_PRUNE_TTL_MS) {
292
+ this.issues.delete(id);
293
+ this.dirty = true;
294
+ }
295
+ }
296
+ }
297
+ transition(issueId, state) {
298
+ const issue = this.issues.get(issueId);
299
+ if (!issue) return false;
300
+ issue.state = state;
288
301
  if (state === "resolved") {
289
- finding.resolvedAt = Date.now();
302
+ issue.resolvedAt = Date.now();
290
303
  }
291
304
  this.dirty = true;
292
305
  return true;
293
306
  }
294
- reportFix(findingId, status, notes) {
295
- const finding = this.findings.get(findingId);
296
- if (!finding) return false;
297
- finding.aiStatus = status;
298
- finding.aiNotes = notes;
307
+ reportFix(issueId, status, notes) {
308
+ const issue = this.issues.get(issueId);
309
+ if (!issue) return false;
310
+ issue.aiStatus = status;
311
+ issue.aiNotes = notes;
299
312
  if (status === "fixed") {
300
- finding.state = "fixing";
313
+ issue.state = "fixing";
301
314
  }
302
315
  this.dirty = true;
303
316
  return true;
304
317
  }
305
- /**
306
- * Reconcile passive findings against the current analysis results.
307
- *
308
- * Passive findings are detected by continuous scanning (not user-triggered).
309
- * When a previously-seen finding is absent from the current results, it means
310
- * the issue has been fixed — transition it to "resolved" automatically.
311
- * Active findings (from MCP verify-fix) are not auto-resolved because they
312
- * require explicit verification.
313
- */
314
- reconcilePassive(currentFindings) {
315
- const currentIds = new Set(currentFindings.map(computeFindingId));
316
- for (const [id, stateful] of this.findings) {
317
- if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
318
- stateful.state = "resolved";
319
- stateful.resolvedAt = Date.now();
320
- this.dirty = true;
321
- }
322
- }
323
- }
324
318
  getAll() {
325
- return [...this.findings.values()];
319
+ return [...this.issues.values()];
326
320
  }
327
321
  getByState(state) {
328
- return [...this.findings.values()].filter((f) => f.state === state);
322
+ return [...this.issues.values()].filter((i) => i.state === state);
323
+ }
324
+ getByCategory(category) {
325
+ return [...this.issues.values()].filter((i) => i.category === category);
329
326
  }
330
- get(findingId) {
331
- return this.findings.get(findingId);
327
+ get(issueId) {
328
+ return this.issues.get(issueId);
332
329
  }
333
330
  clear() {
334
- this.findings.clear();
335
- this.dirty = true;
331
+ this.issues.clear();
332
+ this.dirty = false;
333
+ try {
334
+ if (existsSync3(this.issuesPath)) {
335
+ unlinkSync(this.issuesPath);
336
+ }
337
+ } catch {
338
+ }
339
+ }
340
+ isDirty() {
341
+ return this.dirty;
336
342
  }
337
343
  async loadAsync() {
338
344
  try {
339
- if (await fileExists(this.findingsPath)) {
340
- const raw = await readFile2(this.findingsPath, "utf-8");
341
- const parsed = JSON.parse(raw);
342
- if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
343
- for (const f of parsed.findings) {
344
- this.findings.set(f.findingId, f);
345
- }
346
- }
345
+ if (await fileExists(this.issuesPath)) {
346
+ const raw = await readFile2(this.issuesPath, "utf-8");
347
+ this.hydrate(raw);
347
348
  }
348
349
  } catch (err) {
349
- brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
350
+ brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
350
351
  }
351
352
  }
352
353
  /** Sync load for tests only — not used in production paths. */
353
354
  loadSync() {
354
355
  try {
355
- if (existsSync3(this.findingsPath)) {
356
- const raw = readFileSync2(this.findingsPath, "utf-8");
357
- const parsed = JSON.parse(raw);
358
- if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
359
- for (const f of parsed.findings) {
360
- this.findings.set(f.findingId, f);
361
- }
362
- }
356
+ if (existsSync3(this.issuesPath)) {
357
+ const raw = readFileSync2(this.issuesPath, "utf-8");
358
+ this.hydrate(raw);
363
359
  }
364
360
  } catch (err) {
365
- brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
361
+ brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
362
+ }
363
+ }
364
+ /** Parse and populate issues from a raw JSON string. */
365
+ hydrate(raw) {
366
+ const validated = validateIssuesData(JSON.parse(raw));
367
+ if (!validated) return;
368
+ for (const issue of validated.issues) {
369
+ this.issues.set(issue.issueId, issue);
366
370
  }
367
371
  }
368
372
  flush() {
@@ -377,8 +381,8 @@ var FindingStore = class {
377
381
  }
378
382
  serialize() {
379
383
  const data = {
380
- version: FINDINGS_DATA_VERSION,
381
- findings: [...this.findings.values()]
384
+ version: ISSUES_DATA_VERSION,
385
+ issues: [...this.issues.values()]
382
386
  };
383
387
  return JSON.stringify(data);
384
388
  }
@@ -387,7 +391,7 @@ var FindingStore = class {
387
391
  // src/detect/project.ts
388
392
  import { readFile as readFile3, readdir } from "fs/promises";
389
393
  import { existsSync as existsSync4 } from "fs";
390
- import { join, relative } from "path";
394
+ import { join as join2, relative } from "path";
391
395
  var FRAMEWORKS = [
392
396
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
393
397
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -396,24 +400,24 @@ var FRAMEWORKS = [
396
400
  { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
397
401
  ];
398
402
  async function detectProject(rootDir) {
399
- const pkgPath = join(rootDir, "package.json");
403
+ const pkgPath = join2(rootDir, "package.json");
400
404
  const raw = await readFile3(pkgPath, "utf-8");
401
405
  const pkg = JSON.parse(raw);
402
406
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
403
407
  const framework = detectFrameworkFromDeps(allDeps);
404
408
  const matched = FRAMEWORKS.find((f) => f.name === framework);
405
409
  const devCommand = matched?.devCmd ?? "";
406
- const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
410
+ const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
407
411
  const defaultPort = matched?.defaultPort ?? 3e3;
408
412
  const packageManager = await detectPackageManager(rootDir);
409
413
  return { framework, devCommand, devBin, defaultPort, packageManager };
410
414
  }
411
415
  async function detectPackageManager(rootDir) {
412
- if (await fileExists(join(rootDir, "bun.lockb"))) return "bun";
413
- if (await fileExists(join(rootDir, "bun.lock"))) return "bun";
414
- if (await fileExists(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
415
- if (await fileExists(join(rootDir, "yarn.lock"))) return "yarn";
416
- if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
416
+ if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
417
+ if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
418
+ if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
419
+ if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
420
+ if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
417
421
  return "unknown";
418
422
  }
419
423
  function detectFrameworkFromDeps(allDeps) {
@@ -455,6 +459,38 @@ var AdapterRegistry = class {
455
459
  }
456
460
  };
457
461
 
462
+ // src/utils/response.ts
463
+ function tryParseJson(body) {
464
+ if (!body) return null;
465
+ try {
466
+ return JSON.parse(body);
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+ function unwrapResponse(parsed) {
472
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
473
+ const obj = parsed;
474
+ const keys = Object.keys(obj);
475
+ if (keys.length > 3) return parsed;
476
+ let best = null;
477
+ let bestSize = 0;
478
+ for (const key of keys) {
479
+ const val = obj[key];
480
+ if (Array.isArray(val) && val.length > bestSize) {
481
+ best = val;
482
+ bestSize = val.length;
483
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
484
+ const size = Object.keys(val).length;
485
+ if (size > bestSize) {
486
+ best = val;
487
+ bestSize = size;
488
+ }
489
+ }
490
+ }
491
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
492
+ }
493
+
458
494
  // src/analysis/rules/patterns.ts
459
495
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
460
496
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
@@ -481,31 +517,35 @@ var RULE_HINTS = {
481
517
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
482
518
  };
483
519
 
484
- // src/analysis/rules/exposed-secret.ts
485
- function tryParseJson(body) {
486
- if (!body) return null;
487
- try {
488
- return JSON.parse(body);
489
- } catch {
490
- return null;
491
- }
520
+ // src/utils/http-status.ts
521
+ function isErrorStatus(code) {
522
+ return code >= 400;
523
+ }
524
+ function isServerError(code) {
525
+ return code >= 500;
492
526
  }
493
- function findSecretKeys(obj, prefix) {
527
+ function isRedirect(code) {
528
+ return code >= 300 && code < 400;
529
+ }
530
+
531
+ // src/analysis/rules/exposed-secret.ts
532
+ function findSecretKeys(obj, prefix, depth = 0) {
494
533
  const found = [];
534
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
495
535
  if (!obj || typeof obj !== "object") return found;
496
536
  if (Array.isArray(obj)) {
497
- for (let i = 0; i < Math.min(obj.length, 5); i++) {
498
- found.push(...findSecretKeys(obj[i], prefix));
537
+ for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
538
+ found.push(...findSecretKeys(obj[i], prefix, depth + 1));
499
539
  }
500
540
  return found;
501
541
  }
502
542
  for (const k of Object.keys(obj)) {
503
543
  const val = obj[k];
504
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= 8 && !MASKED_RE.test(val)) {
544
+ if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
505
545
  found.push(k);
506
546
  }
507
547
  if (typeof val === "object" && val !== null) {
508
- found.push(...findSecretKeys(val, prefix + k + "."));
548
+ found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
509
549
  }
510
550
  }
511
551
  return found;
@@ -519,8 +559,8 @@ var exposedSecretRule = {
519
559
  const findings = [];
520
560
  const seen = /* @__PURE__ */ new Map();
521
561
  for (const r of ctx.requests) {
522
- if (r.statusCode >= 400) continue;
523
- const parsed = tryParseJson(r.responseBody);
562
+ if (isErrorStatus(r.statusCode)) continue;
563
+ const parsed = ctx.parsedBodies.response.get(r.id);
524
564
  if (!parsed) continue;
525
565
  const keys = findSecretKeys(parsed, "");
526
566
  if (keys.length === 0) continue;
@@ -673,7 +713,7 @@ var errorInfoLeakRule = {
673
713
 
674
714
  // src/analysis/rules/insecure-cookie.ts
675
715
  function isFrameworkResponse(r) {
676
- if (r.statusCode >= 300 && r.statusCode < 400) return true;
716
+ if (isRedirect(r.statusCode)) return true;
677
717
  if (r.path?.startsWith("/__")) return true;
678
718
  if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
679
719
  return false;
@@ -779,48 +819,15 @@ var corsCredentialsRule = {
779
819
  }
780
820
  };
781
821
 
782
- // src/utils/response.ts
783
- function unwrapResponse(parsed) {
784
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
785
- const obj = parsed;
786
- const keys = Object.keys(obj);
787
- if (keys.length > 3) return parsed;
788
- let best = null;
789
- let bestSize = 0;
790
- for (const key of keys) {
791
- const val = obj[key];
792
- if (Array.isArray(val) && val.length > bestSize) {
793
- best = val;
794
- bestSize = val.length;
795
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
796
- const size = Object.keys(val).length;
797
- if (size > bestSize) {
798
- best = val;
799
- bestSize = size;
800
- }
801
- }
802
- }
803
- return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
804
- }
805
-
806
822
  // src/analysis/rules/response-pii-leak.ts
807
823
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
808
- var FULL_RECORD_MIN_FIELDS = 5;
809
- var LIST_PII_MIN_ITEMS = 2;
810
- function tryParseJson2(body) {
811
- if (!body) return null;
812
- try {
813
- return JSON.parse(body);
814
- } catch {
815
- return null;
816
- }
817
- }
818
- function findEmails(obj) {
824
+ function findEmails(obj, depth = 0) {
819
825
  const emails = [];
826
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
820
827
  if (!obj || typeof obj !== "object") return emails;
821
828
  if (Array.isArray(obj)) {
822
- for (let i = 0; i < Math.min(obj.length, 10); i++) {
823
- emails.push(...findEmails(obj[i]));
829
+ for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
830
+ emails.push(...findEmails(obj[i], depth + 1));
824
831
  }
825
832
  return emails;
826
833
  }
@@ -828,7 +835,7 @@ function findEmails(obj) {
828
835
  if (typeof v === "string" && EMAIL_RE.test(v)) {
829
836
  emails.push(v);
830
837
  } else if (typeof v === "object" && v !== null) {
831
- emails.push(...findEmails(v));
838
+ emails.push(...findEmails(v, depth + 1));
832
839
  }
833
840
  }
834
841
  return emails;
@@ -871,7 +878,7 @@ function detectFullRecordPII(target) {
871
878
  function detectListPII(target) {
872
879
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
873
880
  let itemsWithEmail = 0;
874
- for (let i = 0; i < Math.min(target.length, 10); i++) {
881
+ for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
875
882
  const item = target[i];
876
883
  if (item && typeof item === "object" && findEmails(item).length > 0) {
877
884
  itemsWithEmail++;
@@ -902,10 +909,10 @@ var responsePiiLeakRule = {
902
909
  const findings = [];
903
910
  const seen = /* @__PURE__ */ new Map();
904
911
  for (const r of ctx.requests) {
905
- if (r.statusCode >= 400) continue;
906
- const resJson = tryParseJson2(r.responseBody);
912
+ if (isErrorStatus(r.statusCode)) continue;
913
+ const resJson = ctx.parsedBodies.response.get(r.id);
907
914
  if (!resJson) continue;
908
- const reqJson = tryParseJson2(r.requestBody);
915
+ const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
909
916
  const detection = detectPII(r.method, reqJson, resJson);
910
917
  if (!detection) continue;
911
918
  const ep = `${r.method} ${r.path}`;
@@ -932,12 +939,31 @@ var responsePiiLeakRule = {
932
939
  };
933
940
 
934
941
  // src/analysis/rules/scanner.ts
942
+ function buildBodyCache(requests) {
943
+ const response = /* @__PURE__ */ new Map();
944
+ const request = /* @__PURE__ */ new Map();
945
+ for (const r of requests) {
946
+ if (r.responseBody) {
947
+ const parsed = tryParseJson(r.responseBody);
948
+ if (parsed != null) response.set(r.id, parsed);
949
+ }
950
+ if (r.requestBody) {
951
+ const parsed = tryParseJson(r.requestBody);
952
+ if (parsed != null) request.set(r.id, parsed);
953
+ }
954
+ }
955
+ return { response, request };
956
+ }
935
957
  var SecurityScanner = class {
936
958
  rules = [];
937
959
  register(rule) {
938
960
  this.rules.push(rule);
939
961
  }
940
- scan(ctx) {
962
+ scan(input) {
963
+ const ctx = {
964
+ ...input,
965
+ parsedBodies: buildBodyCache(input.requests)
966
+ };
941
967
  const findings = [];
942
968
  for (const rule of this.rules) {
943
969
  try {
@@ -979,6 +1005,41 @@ var SubscriptionBag = class {
979
1005
  // src/analysis/group.ts
980
1006
  import { randomUUID } from "crypto";
981
1007
 
1008
+ // src/constants/routes.ts
1009
+ var DASHBOARD_PREFIX = "/__brakit";
1010
+ var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
1011
+ var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
1012
+ var DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
1013
+ var DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
1014
+ var DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
1015
+ var DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
1016
+ var DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
1017
+ var DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
1018
+ var DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
1019
+ var DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
1020
+ var DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
1021
+ var DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
1022
+ var DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
1023
+ var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
1024
+ var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
1025
+ var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
1026
+ var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
1027
+ var VALID_TABS_TUPLE = [
1028
+ "overview",
1029
+ "actions",
1030
+ "requests",
1031
+ "fetches",
1032
+ "queries",
1033
+ "errors",
1034
+ "logs",
1035
+ "performance",
1036
+ "security"
1037
+ ];
1038
+ var VALID_TABS = new Set(VALID_TABS_TUPLE);
1039
+
1040
+ // src/constants/network.ts
1041
+ var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
1042
+
982
1043
  // src/analysis/categorize.ts
983
1044
  function detectCategory(req) {
984
1045
  const { method, url, statusCode, responseHeaders } = req;
@@ -1042,7 +1103,7 @@ function labelRequest(req) {
1042
1103
  function generateHumanLabel(req, category) {
1043
1104
  const effectivePath = getEffectivePath(req);
1044
1105
  const endpointName = getEndpointName(effectivePath);
1045
- const failed = req.statusCode >= 400;
1106
+ const failed = isErrorStatus(req.statusCode);
1046
1107
  switch (category) {
1047
1108
  case "auth-handshake":
1048
1109
  return "Auth handshake";
@@ -1222,7 +1283,7 @@ function detectWarnings(requests) {
1222
1283
  for (const req of slowRequests) {
1223
1284
  warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
1224
1285
  }
1225
- const errors = requests.filter((r) => r.statusCode >= 500);
1286
+ const errors = requests.filter((r) => isServerError(r.statusCode));
1226
1287
  for (const req of errors) {
1227
1288
  warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
1228
1289
  }
@@ -1278,7 +1339,7 @@ function buildFlow(rawRequests) {
1278
1339
  requests,
1279
1340
  startTime,
1280
1341
  totalDurationMs: Math.round(endTime - startTime),
1281
- hasErrors: requests.some((r) => r.statusCode >= 400),
1342
+ hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
1282
1343
  warnings: detectWarnings(rawRequests),
1283
1344
  sourcePage,
1284
1345
  redundancyPct
@@ -1346,8 +1407,14 @@ function groupBy(items, keyFn) {
1346
1407
  }
1347
1408
 
1348
1409
  // src/utils/endpoint.ts
1410
+ var DYNAMIC_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\d+$|^[0-9a-f]{12,}$|^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/i;
1411
+ function normalizePath(path) {
1412
+ const qIdx = path.indexOf("?");
1413
+ const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
1414
+ return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
1415
+ }
1349
1416
  function getEndpointKey(method, path) {
1350
- return `${method} ${path}`;
1417
+ return `${method} ${normalizePath(path)}`;
1351
1418
  }
1352
1419
  var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1353
1420
  function extractEndpointFromDesc(desc) {
@@ -1429,6 +1496,15 @@ function windowByEndpoint(requests) {
1429
1496
  }
1430
1497
  return windowed;
1431
1498
  }
1499
+ function extractActiveEndpoints(requests) {
1500
+ const endpoints = /* @__PURE__ */ new Set();
1501
+ for (const r of requests) {
1502
+ if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
1503
+ endpoints.add(getEndpointKey(r.method, r.path));
1504
+ }
1505
+ }
1506
+ return endpoints;
1507
+ }
1432
1508
  function prepareContext(ctx) {
1433
1509
  const nonStatic = ctx.requests.filter(
1434
1510
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
@@ -1446,7 +1522,7 @@ function prepareContext(ctx) {
1446
1522
  endpointGroups.set(ep, g);
1447
1523
  }
1448
1524
  g.total++;
1449
- if (r.statusCode >= 400) g.errors++;
1525
+ if (isErrorStatus(r.statusCode)) g.errors++;
1450
1526
  g.totalDuration += r.durationMs;
1451
1527
  g.totalSize += r.responseSize ?? 0;
1452
1528
  const reqQueries = queriesByReq.get(r.id) ?? [];
@@ -1866,7 +1942,7 @@ var responseOverfetchRule = {
1866
1942
  const insights = [];
1867
1943
  const seen = /* @__PURE__ */ new Set();
1868
1944
  for (const r of ctx.nonStatic) {
1869
- if (r.statusCode >= 400 || !r.responseBody) continue;
1945
+ if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
1870
1946
  const ep = getEndpointKey(r.method, r.path);
1871
1947
  if (seen.has(ep)) continue;
1872
1948
  let parsed;
@@ -2010,81 +2086,37 @@ function computeInsights(ctx) {
2010
2086
  return createDefaultInsightRunner().run(ctx);
2011
2087
  }
2012
2088
 
2013
- // src/analysis/insight-tracker.ts
2014
- function computeInsightKey(insight) {
2015
- const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
2016
- return `${insight.type}:${identifier}`;
2017
- }
2018
- function enrichedIdFromInsight(insight) {
2019
- return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
2020
- }
2021
- var InsightTracker = class {
2022
- tracked = /* @__PURE__ */ new Map();
2023
- enrichedIndex = /* @__PURE__ */ new Map();
2024
- reconcile(current) {
2025
- const currentKeys = /* @__PURE__ */ new Set();
2026
- const now = Date.now();
2027
- for (const insight of current) {
2028
- const key = computeInsightKey(insight);
2029
- currentKeys.add(key);
2030
- const existing = this.tracked.get(key);
2031
- this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
2032
- if (existing) {
2033
- existing.insight = insight;
2034
- existing.lastSeenAt = now;
2035
- existing.consecutiveAbsences = 0;
2036
- if (existing.state === "resolved") {
2037
- existing.state = "open";
2038
- existing.resolvedAt = null;
2039
- }
2040
- } else {
2041
- this.tracked.set(key, {
2042
- key,
2043
- state: "open",
2044
- insight,
2045
- firstSeenAt: now,
2046
- lastSeenAt: now,
2047
- resolvedAt: null,
2048
- consecutiveAbsences: 0,
2049
- aiStatus: null,
2050
- aiNotes: null
2051
- });
2052
- }
2053
- }
2054
- for (const [, stateful] of this.tracked) {
2055
- if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
2056
- stateful.consecutiveAbsences++;
2057
- if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
2058
- stateful.state = "resolved";
2059
- stateful.resolvedAt = now;
2060
- }
2061
- } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
2062
- this.tracked.delete(stateful.key);
2063
- this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
2064
- }
2065
- }
2066
- return [...this.tracked.values()];
2067
- }
2068
- reportFix(enrichedId, status, notes) {
2069
- const key = this.enrichedIndex.get(enrichedId);
2070
- if (!key) return false;
2071
- const stateful = this.tracked.get(key);
2072
- if (!stateful) return false;
2073
- stateful.aiStatus = status;
2074
- stateful.aiNotes = notes;
2075
- if (status === "fixed") {
2076
- stateful.state = "fixing";
2077
- }
2078
- return true;
2079
- }
2080
- getAll() {
2081
- return [...this.tracked.values()];
2082
- }
2083
- clear() {
2084
- this.tracked.clear();
2085
- this.enrichedIndex.clear();
2086
- }
2087
- };
2089
+ // src/analysis/issue-mappers.ts
2090
+ function categorizeInsight(type) {
2091
+ if (type === "security") return "security";
2092
+ if (type === "error" || type === "error-hotspot") return "reliability";
2093
+ return "performance";
2094
+ }
2095
+ function insightToIssue(insight) {
2096
+ return {
2097
+ category: categorizeInsight(insight.type),
2098
+ rule: insight.type,
2099
+ severity: insight.severity,
2100
+ title: insight.title,
2101
+ desc: insight.desc,
2102
+ hint: insight.hint,
2103
+ detail: insight.detail,
2104
+ endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
2105
+ nav: insight.nav
2106
+ };
2107
+ }
2108
+ function securityFindingToIssue(finding) {
2109
+ return {
2110
+ category: "security",
2111
+ rule: finding.rule,
2112
+ severity: finding.severity,
2113
+ title: finding.title,
2114
+ desc: finding.desc,
2115
+ hint: finding.hint,
2116
+ endpoint: finding.endpoint,
2117
+ nav: "security"
2118
+ };
2119
+ }
2088
2120
 
2089
2121
  // src/analysis/engine.ts
2090
2122
  var AnalysisEngine = class {
@@ -2094,10 +2126,8 @@ var AnalysisEngine = class {
2094
2126
  this.scanner = createDefaultScanner();
2095
2127
  }
2096
2128
  scanner;
2097
- insightTracker = new InsightTracker();
2098
2129
  cachedInsights = [];
2099
2130
  cachedFindings = [];
2100
- cachedStatefulInsights = [];
2101
2131
  debounceTimer = null;
2102
2132
  subs = new SubscriptionBag();
2103
2133
  start() {
@@ -2120,15 +2150,6 @@ var AnalysisEngine = class {
2120
2150
  getFindings() {
2121
2151
  return this.cachedFindings;
2122
2152
  }
2123
- getStatefulFindings() {
2124
- return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
2125
- }
2126
- getStatefulInsights() {
2127
- return this.insightTracker.getAll();
2128
- }
2129
- reportInsightFix(enrichedId, status, notes) {
2130
- return this.insightTracker.reportFix(enrichedId, status, notes);
2131
- }
2132
2153
  scheduleRecompute() {
2133
2154
  if (this.debounceTimer) return;
2134
2155
  this.debounceTimer = setTimeout(() => {
@@ -2137,20 +2158,14 @@ var AnalysisEngine = class {
2137
2158
  }, this.debounceMs);
2138
2159
  }
2139
2160
  recompute() {
2140
- const requests = this.registry.get("request-store").getAll();
2161
+ const allRequests = this.registry.get("request-store").getAll();
2141
2162
  const queries = this.registry.get("query-store").getAll();
2142
2163
  const errors = this.registry.get("error-store").getAll();
2143
2164
  const logs = this.registry.get("log-store").getAll();
2144
2165
  const fetches = this.registry.get("fetch-store").getAll();
2166
+ const requests = windowByEndpoint(allRequests);
2145
2167
  const flows = groupRequestsIntoFlows(requests);
2146
2168
  this.cachedFindings = this.scanner.scan({ requests, logs });
2147
- if (this.registry.has("finding-store")) {
2148
- const findingStore = this.registry.get("finding-store");
2149
- for (const finding of this.cachedFindings) {
2150
- findingStore.upsert(finding, "passive");
2151
- }
2152
- findingStore.reconcilePassive(this.cachedFindings);
2153
- }
2154
2169
  this.cachedInsights = computeInsights({
2155
2170
  requests,
2156
2171
  queries,
@@ -2160,24 +2175,40 @@ var AnalysisEngine = class {
2160
2175
  previousMetrics: this.registry.get("metrics-store").getAll(),
2161
2176
  securityFindings: this.cachedFindings
2162
2177
  });
2163
- this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
2164
- const update = {
2165
- insights: this.cachedInsights,
2166
- findings: this.cachedFindings,
2167
- statefulFindings: this.getStatefulFindings(),
2168
- statefulInsights: this.cachedStatefulInsights
2169
- };
2170
- this.registry.get("event-bus").emit("analysis:updated", update);
2178
+ if (this.registry.has("issue-store")) {
2179
+ const issueStore = this.registry.get("issue-store");
2180
+ for (const finding of this.cachedFindings) {
2181
+ issueStore.upsert(securityFindingToIssue(finding), "passive");
2182
+ }
2183
+ for (const insight of this.cachedInsights) {
2184
+ issueStore.upsert(insightToIssue(insight), "passive");
2185
+ }
2186
+ const currentIssueIds = /* @__PURE__ */ new Set();
2187
+ for (const finding of this.cachedFindings) {
2188
+ currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
2189
+ }
2190
+ for (const insight of this.cachedInsights) {
2191
+ currentIssueIds.add(computeIssueId(insightToIssue(insight)));
2192
+ }
2193
+ const activeEndpoints = extractActiveEndpoints(allRequests);
2194
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
2195
+ const update = {
2196
+ insights: this.cachedInsights,
2197
+ findings: this.cachedFindings,
2198
+ issues: issueStore.getAll()
2199
+ };
2200
+ this.registry.get("event-bus").emit("analysis:updated", update);
2201
+ }
2171
2202
  }
2172
2203
  };
2173
2204
 
2174
2205
  // src/index.ts
2175
- var VERSION = "0.8.5";
2206
+ var VERSION = "0.8.6";
2176
2207
  export {
2177
2208
  AdapterRegistry,
2178
2209
  AnalysisEngine,
2179
- FindingStore,
2180
2210
  InsightRunner,
2211
+ IssueStore,
2181
2212
  SecurityScanner,
2182
2213
  VERSION,
2183
2214
  computeInsights,