brakit 0.8.3 → 0.8.5

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,83 @@
1
1
  // src/store/finding-store.ts
2
+ import { readFile as readFile2 } from "fs/promises";
2
3
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
3
4
  import { resolve as resolve2 } from "path";
4
5
 
6
+ // src/utils/fs.ts
7
+ import { access, readFile, writeFile } from "fs/promises";
8
+ import { existsSync, readFileSync, writeFileSync } from "fs";
9
+ import { resolve } from "path";
10
+ async function fileExists(path) {
11
+ try {
12
+ await access(path);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+ function ensureGitignore(dir, entry) {
19
+ try {
20
+ const gitignorePath = resolve(dir, "../.gitignore");
21
+ if (existsSync(gitignorePath)) {
22
+ const content = readFileSync(gitignorePath, "utf-8");
23
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
24
+ writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
25
+ } else {
26
+ writeFileSync(gitignorePath, entry + "\n");
27
+ }
28
+ } catch {
29
+ }
30
+ }
31
+ async function ensureGitignoreAsync(dir, entry) {
32
+ try {
33
+ const gitignorePath = resolve(dir, "../.gitignore");
34
+ if (await fileExists(gitignorePath)) {
35
+ const content = await readFile(gitignorePath, "utf-8");
36
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
37
+ await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
38
+ } else {
39
+ await writeFile(gitignorePath, entry + "\n");
40
+ }
41
+ } catch {
42
+ }
43
+ }
44
+
5
45
  // src/constants/routes.ts
6
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);
7
76
 
8
77
  // src/constants/limits.ts
9
- var MAX_INGEST_BYTES = 10 * 1024 * 1024;
78
+ var ANALYSIS_DEBOUNCE_MS = 300;
79
+ var FINDING_ID_HASH_LENGTH = 16;
80
+ var FINDINGS_DATA_VERSION = 1;
10
81
 
11
82
  // src/constants/thresholds.ts
12
83
  var FLOW_GAP_MS = 5e3;
@@ -44,15 +115,8 @@ var METRICS_DIR = ".brakit";
44
115
  var FINDINGS_FILE = ".brakit/findings.json";
45
116
  var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
46
117
 
47
- // src/constants/severity.ts
48
- var SEVERITY_CRITICAL = "critical";
49
- var SEVERITY_WARNING = "warning";
50
- var SEVERITY_INFO = "info";
51
- var SEVERITY_ICON_MAP = {
52
- [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
53
- [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
54
- [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
55
- };
118
+ // src/constants/network.ts
119
+ var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
56
120
 
57
121
  // src/utils/atomic-writer.ts
58
122
  import {
@@ -63,38 +127,25 @@ import {
63
127
  } from "fs";
64
128
  import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
65
129
 
66
- // src/utils/fs.ts
67
- import { access } from "fs/promises";
68
- import { existsSync, readFileSync, writeFileSync } from "fs";
69
- import { resolve } from "path";
70
- async function fileExists(path) {
71
- try {
72
- await access(path);
73
- return true;
74
- } catch {
75
- return false;
76
- }
77
- }
78
- function ensureGitignore(dir, entry) {
79
- try {
80
- const gitignorePath = resolve(dir, "../.gitignore");
81
- if (existsSync(gitignorePath)) {
82
- const content = readFileSync(gitignorePath, "utf-8");
83
- if (content.split("\n").some((l) => l.trim() === entry)) return;
84
- writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
85
- } else {
86
- writeFileSync(gitignorePath, entry + "\n");
87
- }
88
- } catch {
89
- }
90
- }
91
-
92
130
  // src/utils/log.ts
93
131
  var PREFIX = "[brakit]";
94
132
  function brakitWarn(message) {
95
133
  process.stderr.write(`${PREFIX} ${message}
96
134
  `);
97
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
+ }
98
149
 
99
150
  // src/utils/atomic-writer.ts
100
151
  var AtomicWriter = class {
@@ -111,7 +162,7 @@ var AtomicWriter = class {
111
162
  writeFileSync2(this.tmpPath, content);
112
163
  renameSync(this.tmpPath, this.opts.filePath);
113
164
  } catch (err) {
114
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
165
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
115
166
  }
116
167
  }
117
168
  async writeAsync(content) {
@@ -125,13 +176,14 @@ var AtomicWriter = class {
125
176
  await writeFile2(this.tmpPath, content);
126
177
  await rename(this.tmpPath, this.opts.filePath);
127
178
  } catch (err) {
128
- brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
179
+ brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
129
180
  } finally {
130
181
  this.writing = false;
131
182
  if (this.pendingContent !== null) {
132
183
  const next = this.pendingContent;
133
184
  this.pendingContent = null;
134
- this.writeAsync(next);
185
+ this.writeAsync(next).catch(() => {
186
+ });
135
187
  }
136
188
  }
137
189
  }
@@ -144,10 +196,10 @@ var AtomicWriter = class {
144
196
  }
145
197
  }
146
198
  async ensureDirAsync() {
147
- if (!existsSync2(this.opts.dir)) {
199
+ if (!await fileExists(this.opts.dir)) {
148
200
  await mkdir(this.opts.dir, { recursive: true });
149
201
  if (this.opts.gitignoreEntry) {
150
- ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
202
+ await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
151
203
  }
152
204
  }
153
205
  }
@@ -157,7 +209,11 @@ var AtomicWriter = class {
157
209
  import { createHash } from "crypto";
158
210
  function computeFindingId(finding) {
159
211
  const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
160
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
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);
161
217
  }
162
218
 
163
219
  // src/store/finding-store.ts
@@ -172,7 +228,6 @@ var FindingStore = class {
172
228
  gitignoreEntry: METRICS_DIR,
173
229
  label: "findings"
174
230
  });
175
- this.load();
176
231
  }
177
232
  findings = /* @__PURE__ */ new Map();
178
233
  flushTimer = null;
@@ -180,6 +235,8 @@ var FindingStore = class {
180
235
  writer;
181
236
  findingsPath;
182
237
  start() {
238
+ this.loadAsync().catch(() => {
239
+ });
183
240
  this.flushTimer = setInterval(
184
241
  () => this.flush(),
185
242
  FINDINGS_FLUSH_INTERVAL_MS
@@ -216,7 +273,9 @@ var FindingStore = class {
216
273
  firstSeenAt: now,
217
274
  lastSeenAt: now,
218
275
  resolvedAt: null,
219
- occurrences: 1
276
+ occurrences: 1,
277
+ aiStatus: null,
278
+ aiNotes: null
220
279
  };
221
280
  this.findings.set(id, stateful);
222
281
  this.dirty = true;
@@ -232,6 +291,17 @@ var FindingStore = class {
232
291
  this.dirty = true;
233
292
  return true;
234
293
  }
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;
299
+ if (status === "fixed") {
300
+ finding.state = "fixing";
301
+ }
302
+ this.dirty = true;
303
+ return true;
304
+ }
235
305
  /**
236
306
  * Reconcile passive findings against the current analysis results.
237
307
  *
@@ -244,7 +314,7 @@ var FindingStore = class {
244
314
  reconcilePassive(currentFindings) {
245
315
  const currentIds = new Set(currentFindings.map(computeFindingId));
246
316
  for (const [id, stateful] of this.findings) {
247
- if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
317
+ if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
248
318
  stateful.state = "resolved";
249
319
  stateful.resolvedAt = Date.now();
250
320
  this.dirty = true;
@@ -264,18 +334,35 @@ var FindingStore = class {
264
334
  this.findings.clear();
265
335
  this.dirty = true;
266
336
  }
267
- load() {
337
+ async loadAsync() {
338
+ 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
+ }
347
+ }
348
+ } catch (err) {
349
+ brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
350
+ }
351
+ }
352
+ /** Sync load for tests only — not used in production paths. */
353
+ loadSync() {
268
354
  try {
269
355
  if (existsSync3(this.findingsPath)) {
270
356
  const raw = readFileSync2(this.findingsPath, "utf-8");
271
357
  const parsed = JSON.parse(raw);
272
- if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
358
+ if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
273
359
  for (const f of parsed.findings) {
274
360
  this.findings.set(f.findingId, f);
275
361
  }
276
362
  }
277
363
  }
278
- } catch {
364
+ } catch (err) {
365
+ brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
279
366
  }
280
367
  }
281
368
  flush() {
@@ -290,7 +377,7 @@ var FindingStore = class {
290
377
  }
291
378
  serialize() {
292
379
  const data = {
293
- version: 1,
380
+ version: FINDINGS_DATA_VERSION,
294
381
  findings: [...this.findings.values()]
295
382
  };
296
383
  return JSON.stringify(data);
@@ -298,8 +385,9 @@ var FindingStore = class {
298
385
  };
299
386
 
300
387
  // src/detect/project.ts
301
- import { readFile as readFile2 } from "fs/promises";
302
- import { join } from "path";
388
+ import { readFile as readFile3, readdir } from "fs/promises";
389
+ import { existsSync as existsSync4 } from "fs";
390
+ import { join, relative } from "path";
303
391
  var FRAMEWORKS = [
304
392
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
305
393
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -309,22 +397,14 @@ var FRAMEWORKS = [
309
397
  ];
310
398
  async function detectProject(rootDir) {
311
399
  const pkgPath = join(rootDir, "package.json");
312
- const raw = await readFile2(pkgPath, "utf-8");
400
+ const raw = await readFile3(pkgPath, "utf-8");
313
401
  const pkg = JSON.parse(raw);
314
402
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
315
- let framework = "unknown";
316
- let devCommand = "";
317
- let devBin = "";
318
- let defaultPort = 3e3;
319
- for (const f of FRAMEWORKS) {
320
- if (allDeps[f.dep]) {
321
- framework = f.name;
322
- devCommand = f.devCmd;
323
- devBin = join(rootDir, "node_modules", ".bin", f.bin);
324
- defaultPort = f.defaultPort;
325
- break;
326
- }
327
- }
403
+ const framework = detectFrameworkFromDeps(allDeps);
404
+ const matched = FRAMEWORKS.find((f) => f.name === framework);
405
+ const devCommand = matched?.devCmd ?? "";
406
+ const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
407
+ const defaultPort = matched?.defaultPort ?? 3e3;
328
408
  const packageManager = await detectPackageManager(rootDir);
329
409
  return { framework, devCommand, devBin, defaultPort, packageManager };
330
410
  }
@@ -336,6 +416,12 @@ async function detectPackageManager(rootDir) {
336
416
  if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
337
417
  return "unknown";
338
418
  }
419
+ function detectFrameworkFromDeps(allDeps) {
420
+ for (const f of FRAMEWORKS) {
421
+ if (allDeps[f.dep]) return f.name;
422
+ }
423
+ return "unknown";
424
+ }
339
425
 
340
426
  // src/instrument/adapter-registry.ts
341
427
  var AdapterRegistry = class {
@@ -373,11 +459,11 @@ var AdapterRegistry = class {
373
459
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
374
460
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
375
461
  var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
376
- var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
462
+ 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+/;
377
463
  var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
378
464
  var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
379
- var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
380
- var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
465
+ var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
466
+ var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
381
467
  var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
382
468
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
383
469
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
@@ -389,9 +475,9 @@ var RULE_HINTS = {
389
475
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
390
476
  "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
391
477
  "error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
478
+ "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
392
479
  "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
393
480
  "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
394
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
395
481
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
396
482
  };
397
483
 
@@ -761,48 +847,47 @@ function hasInternalIds(obj) {
761
847
  }
762
848
  return false;
763
849
  }
764
- function detectPII(method, reqBody, resBody) {
765
- const target = unwrapResponse(resBody);
766
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
767
- const reqEmails = findEmails(reqBody);
768
- if (reqEmails.length > 0) {
769
- const resEmails = findEmails(target);
770
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
771
- if (echoed.length > 0) {
772
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
773
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
774
- return { reason: "echo", emailCount: echoed.length };
775
- }
776
- }
777
- }
850
+ function detectEchoPII(method, reqBody, target) {
851
+ if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
852
+ const reqEmails = findEmails(reqBody);
853
+ if (reqEmails.length === 0) return null;
854
+ const resEmails = findEmails(target);
855
+ const echoed = reqEmails.filter((e) => resEmails.includes(e));
856
+ if (echoed.length === 0) return null;
857
+ const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
858
+ if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
859
+ return { reason: "echo", emailCount: echoed.length };
778
860
  }
779
- if (target && typeof target === "object" && !Array.isArray(target)) {
780
- const fields = topLevelFieldCount(target);
781
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
782
- const emails = findEmails(target);
783
- if (emails.length > 0) {
784
- return { reason: "full-record", emailCount: emails.length };
785
- }
861
+ return null;
862
+ }
863
+ function detectFullRecordPII(target) {
864
+ if (!target || typeof target !== "object" || Array.isArray(target)) return null;
865
+ const fields = topLevelFieldCount(target);
866
+ if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
867
+ const emails = findEmails(target);
868
+ if (emails.length === 0) return null;
869
+ return { reason: "full-record", emailCount: emails.length };
870
+ }
871
+ function detectListPII(target) {
872
+ if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
873
+ let itemsWithEmail = 0;
874
+ for (let i = 0; i < Math.min(target.length, 10); i++) {
875
+ const item = target[i];
876
+ if (item && typeof item === "object" && findEmails(item).length > 0) {
877
+ itemsWithEmail++;
786
878
  }
787
879
  }
788
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
789
- let itemsWithEmail = 0;
790
- for (let i = 0; i < Math.min(target.length, 10); i++) {
791
- const item = target[i];
792
- if (item && typeof item === "object") {
793
- const emails = findEmails(item);
794
- if (emails.length > 0) itemsWithEmail++;
795
- }
796
- }
797
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
798
- const first = target[0];
799
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
800
- return { reason: "list-pii", emailCount: itemsWithEmail };
801
- }
802
- }
880
+ if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
881
+ const first = target[0];
882
+ if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
883
+ return { reason: "list-pii", emailCount: itemsWithEmail };
803
884
  }
804
885
  return null;
805
886
  }
887
+ function detectPII(method, reqBody, resBody) {
888
+ const target = unwrapResponse(resBody);
889
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
890
+ }
806
891
  var REASON_LABELS = {
807
892
  echo: "echoes back PII from the request body",
808
893
  "full-record": "returns a full record with email and internal IDs",
@@ -1930,8 +2015,12 @@ function computeInsightKey(insight) {
1930
2015
  const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
1931
2016
  return `${insight.type}:${identifier}`;
1932
2017
  }
2018
+ function enrichedIdFromInsight(insight) {
2019
+ return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
2020
+ }
1933
2021
  var InsightTracker = class {
1934
2022
  tracked = /* @__PURE__ */ new Map();
2023
+ enrichedIndex = /* @__PURE__ */ new Map();
1935
2024
  reconcile(current) {
1936
2025
  const currentKeys = /* @__PURE__ */ new Set();
1937
2026
  const now = Date.now();
@@ -1939,6 +2028,7 @@ var InsightTracker = class {
1939
2028
  const key = computeInsightKey(insight);
1940
2029
  currentKeys.add(key);
1941
2030
  const existing = this.tracked.get(key);
2031
+ this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
1942
2032
  if (existing) {
1943
2033
  existing.insight = insight;
1944
2034
  existing.lastSeenAt = now;
@@ -1955,34 +2045,50 @@ var InsightTracker = class {
1955
2045
  firstSeenAt: now,
1956
2046
  lastSeenAt: now,
1957
2047
  resolvedAt: null,
1958
- consecutiveAbsences: 0
2048
+ consecutiveAbsences: 0,
2049
+ aiStatus: null,
2050
+ aiNotes: null
1959
2051
  });
1960
2052
  }
1961
2053
  }
1962
- for (const [key, stateful] of this.tracked) {
1963
- if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
2054
+ for (const [, stateful] of this.tracked) {
2055
+ if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
1964
2056
  stateful.consecutiveAbsences++;
1965
2057
  if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
1966
2058
  stateful.state = "resolved";
1967
2059
  stateful.resolvedAt = now;
1968
2060
  }
1969
2061
  } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
1970
- this.tracked.delete(key);
2062
+ this.tracked.delete(stateful.key);
2063
+ this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
1971
2064
  }
1972
2065
  }
1973
2066
  return [...this.tracked.values()];
1974
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
+ }
1975
2080
  getAll() {
1976
2081
  return [...this.tracked.values()];
1977
2082
  }
1978
2083
  clear() {
1979
2084
  this.tracked.clear();
2085
+ this.enrichedIndex.clear();
1980
2086
  }
1981
2087
  };
1982
2088
 
1983
2089
  // src/analysis/engine.ts
1984
2090
  var AnalysisEngine = class {
1985
- constructor(registry, debounceMs = 300) {
2091
+ constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
1986
2092
  this.registry = registry;
1987
2093
  this.debounceMs = debounceMs;
1988
2094
  this.scanner = createDefaultScanner();
@@ -2018,7 +2124,10 @@ var AnalysisEngine = class {
2018
2124
  return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
2019
2125
  }
2020
2126
  getStatefulInsights() {
2021
- return this.cachedStatefulInsights;
2127
+ return this.insightTracker.getAll();
2128
+ }
2129
+ reportInsightFix(enrichedId, status, notes) {
2130
+ return this.insightTracker.reportFix(enrichedId, status, notes);
2022
2131
  }
2023
2132
  scheduleRecompute() {
2024
2133
  if (this.debounceTimer) return;
@@ -2063,7 +2172,7 @@ var AnalysisEngine = class {
2063
2172
  };
2064
2173
 
2065
2174
  // src/index.ts
2066
- var VERSION = "0.8.3";
2175
+ var VERSION = "0.8.5";
2067
2176
  export {
2068
2177
  AdapterRegistry,
2069
2178
  AnalysisEngine,