brakit 0.8.1 → 0.8.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.js CHANGED
@@ -1,20 +1,11 @@
1
1
  // src/store/finding-store.ts
2
- import {
3
- readFileSync as readFileSync2,
4
- writeFileSync as writeFileSync2,
5
- existsSync as existsSync2,
6
- mkdirSync as mkdirSync2,
7
- renameSync
8
- } from "fs";
9
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
2
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
10
3
  import { resolve as resolve2 } from "path";
11
4
 
12
5
  // src/constants/routes.ts
13
6
  var DASHBOARD_PREFIX = "/__brakit";
14
7
 
15
8
  // src/constants/limits.ts
16
- var MAX_REQUEST_ENTRIES = 1e3;
17
- var MAX_TELEMETRY_ENTRIES = 1e3;
18
9
  var MAX_INGEST_BYTES = 10 * 1024 * 1024;
19
10
 
20
11
  // src/constants/thresholds.ts
@@ -53,6 +44,25 @@ var METRICS_DIR = ".brakit";
53
44
  var FINDINGS_FILE = ".brakit/findings.json";
54
45
  var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
55
46
 
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
+ };
56
+
57
+ // src/utils/atomic-writer.ts
58
+ import {
59
+ writeFileSync as writeFileSync2,
60
+ existsSync as existsSync2,
61
+ mkdirSync as mkdirSync2,
62
+ renameSync
63
+ } from "fs";
64
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
65
+
56
66
  // src/utils/fs.ts
57
67
  import { access } from "fs/promises";
58
68
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -79,6 +89,70 @@ function ensureGitignore(dir, entry) {
79
89
  }
80
90
  }
81
91
 
92
+ // src/utils/log.ts
93
+ var PREFIX = "[brakit]";
94
+ function brakitWarn(message) {
95
+ process.stderr.write(`${PREFIX} ${message}
96
+ `);
97
+ }
98
+
99
+ // src/utils/atomic-writer.ts
100
+ var AtomicWriter = class {
101
+ constructor(opts) {
102
+ this.opts = opts;
103
+ this.tmpPath = opts.filePath + ".tmp";
104
+ }
105
+ tmpPath;
106
+ writing = false;
107
+ pendingContent = null;
108
+ writeSync(content) {
109
+ try {
110
+ this.ensureDir();
111
+ writeFileSync2(this.tmpPath, content);
112
+ renameSync(this.tmpPath, this.opts.filePath);
113
+ } catch (err) {
114
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
115
+ }
116
+ }
117
+ async writeAsync(content) {
118
+ if (this.writing) {
119
+ this.pendingContent = content;
120
+ return;
121
+ }
122
+ this.writing = true;
123
+ try {
124
+ await this.ensureDirAsync();
125
+ await writeFile2(this.tmpPath, content);
126
+ await rename(this.tmpPath, this.opts.filePath);
127
+ } catch (err) {
128
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
129
+ } finally {
130
+ this.writing = false;
131
+ if (this.pendingContent !== null) {
132
+ const next = this.pendingContent;
133
+ this.pendingContent = null;
134
+ this.writeAsync(next);
135
+ }
136
+ }
137
+ }
138
+ ensureDir() {
139
+ if (!existsSync2(this.opts.dir)) {
140
+ mkdirSync2(this.opts.dir, { recursive: true });
141
+ if (this.opts.gitignoreEntry) {
142
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
143
+ }
144
+ }
145
+ }
146
+ async ensureDirAsync() {
147
+ if (!existsSync2(this.opts.dir)) {
148
+ await mkdir(this.opts.dir, { recursive: true });
149
+ if (this.opts.gitignoreEntry) {
150
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
151
+ }
152
+ }
153
+ }
154
+ };
155
+
82
156
  // src/store/finding-id.ts
83
157
  import { createHash } from "crypto";
84
158
  function computeFindingId(finding) {
@@ -90,18 +164,21 @@ function computeFindingId(finding) {
90
164
  var FindingStore = class {
91
165
  constructor(rootDir) {
92
166
  this.rootDir = rootDir;
93
- this.metricsDir = resolve2(rootDir, METRICS_DIR);
167
+ const metricsDir = resolve2(rootDir, METRICS_DIR);
94
168
  this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
95
- this.tmpPath = this.findingsPath + ".tmp";
169
+ this.writer = new AtomicWriter({
170
+ dir: metricsDir,
171
+ filePath: this.findingsPath,
172
+ gitignoreEntry: METRICS_DIR,
173
+ label: "findings"
174
+ });
96
175
  this.load();
97
176
  }
98
177
  findings = /* @__PURE__ */ new Map();
99
178
  flushTimer = null;
100
179
  dirty = false;
101
- writing = false;
180
+ writer;
102
181
  findingsPath;
103
- tmpPath;
104
- metricsDir;
105
182
  start() {
106
183
  this.flushTimer = setInterval(
107
184
  () => this.flush(),
@@ -155,6 +232,15 @@ var FindingStore = class {
155
232
  this.dirty = true;
156
233
  return true;
157
234
  }
235
+ /**
236
+ * Reconcile passive findings against the current analysis results.
237
+ *
238
+ * Passive findings are detected by continuous scanning (not user-triggered).
239
+ * When a previously-seen finding is absent from the current results, it means
240
+ * the issue has been fixed — transition it to "resolved" automatically.
241
+ * Active findings (from MCP verify-fix) are not auto-resolved because they
242
+ * require explicit verification.
243
+ */
158
244
  reconcilePassive(currentFindings) {
159
245
  const currentIds = new Set(currentFindings.map(computeFindingId));
160
246
  for (const [id, stateful] of this.findings) {
@@ -180,7 +266,7 @@ var FindingStore = class {
180
266
  }
181
267
  load() {
182
268
  try {
183
- if (existsSync2(this.findingsPath)) {
269
+ if (existsSync3(this.findingsPath)) {
184
270
  const raw = readFileSync2(this.findingsPath, "utf-8");
185
271
  const parsed = JSON.parse(raw);
186
272
  if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
@@ -194,56 +280,20 @@ var FindingStore = class {
194
280
  }
195
281
  flush() {
196
282
  if (!this.dirty) return;
197
- this.writeAsync();
283
+ this.writer.writeAsync(this.serialize());
284
+ this.dirty = false;
198
285
  }
199
286
  flushSync() {
200
287
  if (!this.dirty) return;
201
- try {
202
- this.ensureDir();
203
- const data = {
204
- version: 1,
205
- findings: [...this.findings.values()]
206
- };
207
- writeFileSync2(this.tmpPath, JSON.stringify(data));
208
- renameSync(this.tmpPath, this.findingsPath);
209
- this.dirty = false;
210
- } catch (err) {
211
- process.stderr.write(
212
- `[brakit] failed to save findings: ${err.message}
213
- `
214
- );
215
- }
216
- }
217
- async writeAsync() {
218
- if (this.writing) return;
219
- this.writing = true;
220
- try {
221
- if (!existsSync2(this.metricsDir)) {
222
- await mkdir(this.metricsDir, { recursive: true });
223
- ensureGitignore(this.metricsDir, METRICS_DIR);
224
- }
225
- const data = {
226
- version: 1,
227
- findings: [...this.findings.values()]
228
- };
229
- await writeFile2(this.tmpPath, JSON.stringify(data));
230
- await rename(this.tmpPath, this.findingsPath);
231
- this.dirty = false;
232
- } catch (err) {
233
- process.stderr.write(
234
- `[brakit] failed to save findings: ${err.message}
235
- `
236
- );
237
- } finally {
238
- this.writing = false;
239
- if (this.dirty) this.writeAsync();
240
- }
288
+ this.writer.writeSync(this.serialize());
289
+ this.dirty = false;
241
290
  }
242
- ensureDir() {
243
- if (!existsSync2(this.metricsDir)) {
244
- mkdirSync2(this.metricsDir, { recursive: true });
245
- ensureGitignore(this.metricsDir, METRICS_DIR);
246
- }
291
+ serialize() {
292
+ const data = {
293
+ version: 1,
294
+ findings: [...this.findings.values()]
295
+ };
296
+ return JSON.stringify(data);
247
297
  }
248
298
  };
249
299
 
@@ -829,170 +879,20 @@ function createDefaultScanner() {
829
879
  return scanner;
830
880
  }
831
881
 
832
- // src/utils/static-patterns.ts
833
- var STATIC_PATTERNS = [
834
- /^\/_next\//,
835
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
836
- /^\/favicon/,
837
- /^\/__nextjs/
838
- ];
839
- function isStaticPath(urlPath) {
840
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
841
- }
842
-
843
- // src/store/request-store.ts
844
- function flattenHeaders(headers) {
845
- const flat = {};
846
- for (const [key, value] of Object.entries(headers)) {
847
- if (value === void 0) continue;
848
- flat[key] = Array.isArray(value) ? value.join(", ") : value;
849
- }
850
- return flat;
851
- }
852
- var RequestStore = class {
853
- constructor(maxEntries = MAX_REQUEST_ENTRIES) {
854
- this.maxEntries = maxEntries;
855
- }
856
- requests = [];
857
- listeners = [];
858
- capture(input) {
859
- const url = input.url;
860
- const path = url.split("?")[0];
861
- let requestBodyStr = null;
862
- if (input.requestBody && input.requestBody.length > 0) {
863
- requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
864
- }
865
- let responseBodyStr = null;
866
- if (input.responseBody && input.responseBody.length > 0) {
867
- const ct = input.responseContentType;
868
- if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
869
- responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
870
- }
871
- }
872
- const entry = {
873
- id: input.requestId,
874
- method: input.method,
875
- url,
876
- path,
877
- headers: flattenHeaders(input.requestHeaders),
878
- requestBody: requestBodyStr,
879
- statusCode: input.statusCode,
880
- responseHeaders: flattenHeaders(input.responseHeaders),
881
- responseBody: responseBodyStr,
882
- startedAt: input.startTime,
883
- durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
884
- responseSize: input.responseBody?.length ?? 0,
885
- isStatic: isStaticPath(path)
886
- };
887
- this.requests.push(entry);
888
- if (this.requests.length > this.maxEntries) {
889
- this.requests.shift();
890
- }
891
- for (const fn of this.listeners) {
892
- fn(entry);
893
- }
894
- return entry;
895
- }
896
- getAll() {
897
- return this.requests;
882
+ // src/core/disposable.ts
883
+ var SubscriptionBag = class {
884
+ items = [];
885
+ add(teardown) {
886
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
898
887
  }
899
- clear() {
900
- this.requests.length = 0;
901
- }
902
- onRequest(fn) {
903
- this.listeners.push(fn);
904
- }
905
- offRequest(fn) {
906
- const idx = this.listeners.indexOf(fn);
907
- if (idx !== -1) this.listeners.splice(idx, 1);
888
+ dispose() {
889
+ for (const d of this.items) d.dispose();
890
+ this.items.length = 0;
908
891
  }
909
892
  };
910
893
 
911
- // src/store/request-log.ts
912
- var defaultStore = new RequestStore();
913
- var getRequests = () => defaultStore.getAll();
914
- var onRequest = (fn) => defaultStore.onRequest(fn);
915
- var offRequest = (fn) => defaultStore.offRequest(fn);
916
-
917
- // src/store/telemetry-store.ts
918
- import { randomUUID } from "crypto";
919
- var TelemetryStore = class {
920
- constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
921
- this.maxEntries = maxEntries;
922
- }
923
- entries = [];
924
- listeners = [];
925
- add(data) {
926
- const entry = { id: randomUUID(), ...data };
927
- this.entries.push(entry);
928
- if (this.entries.length > this.maxEntries) this.entries.shift();
929
- for (const fn of this.listeners) fn(entry);
930
- return entry;
931
- }
932
- getAll() {
933
- return this.entries;
934
- }
935
- getByRequest(requestId) {
936
- return this.entries.filter((e) => e.parentRequestId === requestId);
937
- }
938
- clear() {
939
- this.entries.length = 0;
940
- }
941
- onEntry(fn) {
942
- this.listeners.push(fn);
943
- }
944
- offEntry(fn) {
945
- const idx = this.listeners.indexOf(fn);
946
- if (idx !== -1) this.listeners.splice(idx, 1);
947
- }
948
- };
949
-
950
- // src/store/fetch-store.ts
951
- var FetchStore = class extends TelemetryStore {
952
- };
953
- var defaultFetchStore = new FetchStore();
954
-
955
- // src/store/log-store.ts
956
- var LogStore = class extends TelemetryStore {
957
- };
958
- var defaultLogStore = new LogStore();
959
-
960
- // src/store/error-store.ts
961
- var ErrorStore = class extends TelemetryStore {
962
- };
963
- var defaultErrorStore = new ErrorStore();
964
-
965
- // src/store/query-store.ts
966
- var QueryStore = class extends TelemetryStore {
967
- };
968
- var defaultQueryStore = new QueryStore();
969
-
970
- // src/store/metrics/metrics-store.ts
971
- import { randomUUID as randomUUID2 } from "crypto";
972
-
973
- // src/utils/endpoint.ts
974
- function getEndpointKey(method, path) {
975
- return `${method} ${path}`;
976
- }
977
- var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
978
- function extractEndpointFromDesc(desc) {
979
- return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
980
- }
981
-
982
- // src/store/metrics/persistence.ts
983
- import {
984
- readFileSync as readFileSync3,
985
- writeFileSync as writeFileSync3,
986
- mkdirSync as mkdirSync3,
987
- existsSync as existsSync3,
988
- unlinkSync,
989
- renameSync as renameSync2
990
- } from "fs";
991
- import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
992
- import { resolve as resolve3 } from "path";
993
-
994
894
  // src/analysis/group.ts
995
- import { randomUUID as randomUUID3 } from "crypto";
895
+ import { randomUUID } from "crypto";
996
896
 
997
897
  // src/analysis/categorize.ts
998
898
  function detectCategory(req) {
@@ -1288,7 +1188,7 @@ function buildFlow(rawRequests) {
1288
1188
  const redundancyPct = nonStaticCount > 0 ? Math.round(duplicateCount / nonStaticCount * 100) : 0;
1289
1189
  const sourcePage = getDominantSourcePage(rawRequests);
1290
1190
  return {
1291
- id: randomUUID3(),
1191
+ id: randomUUID(),
1292
1192
  label: deriveFlowLabel(requests, sourcePage),
1293
1193
  requests,
1294
1194
  startTime,
@@ -1360,6 +1260,15 @@ function groupBy(items, keyFn) {
1360
1260
  return map;
1361
1261
  }
1362
1262
 
1263
+ // src/utils/endpoint.ts
1264
+ function getEndpointKey(method, path) {
1265
+ return `${method} ${path}`;
1266
+ }
1267
+ var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1268
+ function extractEndpointFromDesc(desc) {
1269
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1270
+ }
1271
+
1363
1272
  // src/instrument/adapters/normalize.ts
1364
1273
  function normalizeSQL(sql) {
1365
1274
  if (!sql) return { op: "OTHER", table: "" };
@@ -2073,15 +1982,10 @@ var InsightTracker = class {
2073
1982
 
2074
1983
  // src/analysis/engine.ts
2075
1984
  var AnalysisEngine = class {
2076
- constructor(metricsStore, findingStore, debounceMs = 300) {
2077
- this.metricsStore = metricsStore;
2078
- this.findingStore = findingStore;
1985
+ constructor(registry, debounceMs = 300) {
1986
+ this.registry = registry;
2079
1987
  this.debounceMs = debounceMs;
2080
1988
  this.scanner = createDefaultScanner();
2081
- this.boundRequestListener = () => this.scheduleRecompute();
2082
- this.boundQueryListener = () => this.scheduleRecompute();
2083
- this.boundErrorListener = () => this.scheduleRecompute();
2084
- this.boundLogListener = () => this.scheduleRecompute();
2085
1989
  }
2086
1990
  scanner;
2087
1991
  insightTracker = new InsightTracker();
@@ -2089,34 +1993,21 @@ var AnalysisEngine = class {
2089
1993
  cachedFindings = [];
2090
1994
  cachedStatefulInsights = [];
2091
1995
  debounceTimer = null;
2092
- listeners = [];
2093
- boundRequestListener;
2094
- boundQueryListener;
2095
- boundErrorListener;
2096
- boundLogListener;
1996
+ subs = new SubscriptionBag();
2097
1997
  start() {
2098
- onRequest(this.boundRequestListener);
2099
- defaultQueryStore.onEntry(this.boundQueryListener);
2100
- defaultErrorStore.onEntry(this.boundErrorListener);
2101
- defaultLogStore.onEntry(this.boundLogListener);
1998
+ const bus = this.registry.get("event-bus");
1999
+ this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
2000
+ this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
2001
+ this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
2002
+ this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
2102
2003
  }
2103
2004
  stop() {
2104
- offRequest(this.boundRequestListener);
2105
- defaultQueryStore.offEntry(this.boundQueryListener);
2106
- defaultErrorStore.offEntry(this.boundErrorListener);
2107
- defaultLogStore.offEntry(this.boundLogListener);
2005
+ this.subs.dispose();
2108
2006
  if (this.debounceTimer) {
2109
2007
  clearTimeout(this.debounceTimer);
2110
2008
  this.debounceTimer = null;
2111
2009
  }
2112
2010
  }
2113
- onUpdate(fn) {
2114
- this.listeners.push(fn);
2115
- }
2116
- offUpdate(fn) {
2117
- const idx = this.listeners.indexOf(fn);
2118
- if (idx !== -1) this.listeners.splice(idx, 1);
2119
- }
2120
2011
  getInsights() {
2121
2012
  return this.cachedInsights;
2122
2013
  }
@@ -2124,7 +2015,7 @@ var AnalysisEngine = class {
2124
2015
  return this.cachedFindings;
2125
2016
  }
2126
2017
  getStatefulFindings() {
2127
- return this.findingStore?.getAll() ?? [];
2018
+ return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
2128
2019
  }
2129
2020
  getStatefulInsights() {
2130
2021
  return this.cachedStatefulInsights;
@@ -2137,18 +2028,19 @@ var AnalysisEngine = class {
2137
2028
  }, this.debounceMs);
2138
2029
  }
2139
2030
  recompute() {
2140
- const requests = getRequests();
2141
- const queries = defaultQueryStore.getAll();
2142
- const errors = defaultErrorStore.getAll();
2143
- const logs = defaultLogStore.getAll();
2144
- const fetches = defaultFetchStore.getAll();
2031
+ const requests = this.registry.get("request-store").getAll();
2032
+ const queries = this.registry.get("query-store").getAll();
2033
+ const errors = this.registry.get("error-store").getAll();
2034
+ const logs = this.registry.get("log-store").getAll();
2035
+ const fetches = this.registry.get("fetch-store").getAll();
2145
2036
  const flows = groupRequestsIntoFlows(requests);
2146
2037
  this.cachedFindings = this.scanner.scan({ requests, logs });
2147
- if (this.findingStore) {
2038
+ if (this.registry.has("finding-store")) {
2039
+ const findingStore = this.registry.get("finding-store");
2148
2040
  for (const finding of this.cachedFindings) {
2149
- this.findingStore.upsert(finding, "passive");
2041
+ findingStore.upsert(finding, "passive");
2150
2042
  }
2151
- this.findingStore.reconcilePassive(this.cachedFindings);
2043
+ findingStore.reconcilePassive(this.cachedFindings);
2152
2044
  }
2153
2045
  this.cachedInsights = computeInsights({
2154
2046
  requests,
@@ -2156,7 +2048,7 @@ var AnalysisEngine = class {
2156
2048
  errors,
2157
2049
  flows,
2158
2050
  fetches,
2159
- previousMetrics: this.metricsStore.getAll(),
2051
+ previousMetrics: this.registry.get("metrics-store").getAll(),
2160
2052
  securityFindings: this.cachedFindings
2161
2053
  });
2162
2054
  this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
@@ -2166,17 +2058,12 @@ var AnalysisEngine = class {
2166
2058
  statefulFindings: this.getStatefulFindings(),
2167
2059
  statefulInsights: this.cachedStatefulInsights
2168
2060
  };
2169
- for (const fn of this.listeners) {
2170
- try {
2171
- fn(update);
2172
- } catch {
2173
- }
2174
- }
2061
+ this.registry.get("event-bus").emit("analysis:updated", update);
2175
2062
  }
2176
2063
  };
2177
2064
 
2178
2065
  // src/index.ts
2179
- var VERSION = "0.8.1";
2066
+ var VERSION = "0.8.2";
2180
2067
  export {
2181
2068
  AdapterRegistry,
2182
2069
  AnalysisEngine,