brakit 0.8.6 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p align="center">
4
4
  <b>AI writes your code. Brakit watches what it does.</b> <br />
5
- Every request, query, and security issue, caught before you ship. <br />
5
+ Every request, query, and API call mapped to the action that triggered it. See your entire backend at a glance. <br />
6
6
  <b>Open source · Local first · Zero config · AI-native via MCP</b>
7
7
  </p>
8
8
 
@@ -32,6 +32,12 @@
32
32
 
33
33
  ---
34
34
 
35
+ <p align="center">
36
+ <img width="700" src="docs/images/actions.png" alt="Brakit Actions — endpoints grouped by user action" />
37
+ <br />
38
+ <sub>Every endpoint grouped by the action that triggered it — see "Sign Up" and "Load Dashboard", not 47 raw requests</sub>
39
+ </p>
40
+
35
41
  <p align="center">
36
42
  <img width="700" src="docs/images/dashboard.png" alt="Brakit Dashboard — issues surfaced automatically" />
37
43
  <br />
package/dist/api.d.ts CHANGED
@@ -63,6 +63,7 @@ interface TelemetryEntry {
63
63
  timestamp: number;
64
64
  }
65
65
  interface TracedFetch extends TelemetryEntry {
66
+ fetchId?: string;
66
67
  url: string;
67
68
  method: string;
68
69
  statusCode: number;
@@ -75,11 +76,11 @@ interface TracedLog extends TelemetryEntry {
75
76
  interface TracedError extends TelemetryEntry {
76
77
  name: string;
77
78
  message: string;
78
- stack: string;
79
+ stack?: string;
79
80
  }
80
81
  type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
81
82
  interface TracedQuery extends TelemetryEntry {
82
- driver: "pg" | "mysql2" | "prisma" | "sdk";
83
+ driver: "pg" | "mysql2" | "prisma" | "asyncpg" | "sqlalchemy" | "sdk";
83
84
  sql?: string;
84
85
  model?: string;
85
86
  operation?: string;
@@ -88,6 +89,7 @@ interface TracedQuery extends TelemetryEntry {
88
89
  normalizedOp?: NormalizedOp;
89
90
  table?: string;
90
91
  source?: string;
92
+ parentFetchId?: string;
91
93
  }
92
94
  type TelemetryEvent = {
93
95
  type: "fetch";
package/dist/api.js CHANGED
@@ -17,7 +17,7 @@ var ISSUES_DATA_VERSION = 2;
17
17
  var SECRET_SCAN_ARRAY_LIMIT = 5;
18
18
  var PII_SCAN_ARRAY_LIMIT = 10;
19
19
  var MIN_SECRET_VALUE_LENGTH = 8;
20
- var FULL_RECORD_MIN_FIELDS = 5;
20
+ var FULL_RECORD_MIN_FIELDS = 8;
21
21
  var LIST_PII_MIN_ITEMS = 2;
22
22
  var MAX_OBJECT_SCAN_DEPTH = 5;
23
23
  var ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
@@ -132,11 +132,10 @@ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
132
132
  var AtomicWriter = class {
133
133
  constructor(opts) {
134
134
  this.opts = opts;
135
+ this.writing = false;
136
+ this.pendingContent = null;
135
137
  this.tmpPath = opts.filePath + ".tmp";
136
138
  }
137
- tmpPath;
138
- writing = false;
139
- pendingContent = null;
140
139
  writeSync(content) {
141
140
  try {
142
141
  this.ensureDir();
@@ -198,6 +197,9 @@ function computeIssueId(issue) {
198
197
  var IssueStore = class {
199
198
  constructor(dataDir) {
200
199
  this.dataDir = dataDir;
200
+ this.issues = /* @__PURE__ */ new Map();
201
+ this.flushTimer = null;
202
+ this.dirty = false;
201
203
  this.issuesPath = resolve2(dataDir, ISSUES_FILE);
202
204
  this.writer = new AtomicWriter({
203
205
  dir: dataDir,
@@ -205,11 +207,6 @@ var IssueStore = class {
205
207
  label: "issues"
206
208
  });
207
209
  }
208
- issues = /* @__PURE__ */ new Map();
209
- flushTimer = null;
210
- dirty = false;
211
- writer;
212
- issuesPath;
213
210
  start() {
214
211
  this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
215
212
  this.flushTimer = setInterval(
@@ -429,8 +426,10 @@ function detectFrameworkFromDeps(allDeps) {
429
426
 
430
427
  // src/instrument/adapter-registry.ts
431
428
  var AdapterRegistry = class {
432
- adapters = [];
433
- active = [];
429
+ constructor() {
430
+ this.adapters = [];
431
+ this.active = [];
432
+ }
434
433
  register(adapter) {
435
434
  this.adapters.push(adapter);
436
435
  }
@@ -504,6 +503,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
504
503
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
505
504
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
506
505
  var INTERNAL_ID_SUFFIX = /Id$|_id$/;
506
+ var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
507
+ var SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
507
508
  var SELECT_STAR_RE = /^SELECT\s+\*/i;
508
509
  var SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
509
510
  var RULE_HINTS = {
@@ -854,6 +855,15 @@ function hasInternalIds(obj) {
854
855
  }
855
856
  return false;
856
857
  }
858
+ function hasSensitiveFieldNames(obj, depth = 0) {
859
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
860
+ if (!obj || typeof obj !== "object") return false;
861
+ if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
862
+ for (const key of Object.keys(obj)) {
863
+ if (SENSITIVE_FIELD_NAMES.test(key)) return true;
864
+ }
865
+ return false;
866
+ }
857
867
  function detectEchoPII(method, reqBody, target) {
858
868
  if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
859
869
  const reqEmails = findEmails(reqBody);
@@ -875,6 +885,13 @@ function detectFullRecordPII(target) {
875
885
  if (emails.length === 0) return null;
876
886
  return { reason: "full-record", emailCount: emails.length };
877
887
  }
888
+ function detectSensitiveFieldPII(target) {
889
+ const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
890
+ if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
891
+ if (!hasSensitiveFieldNames(inspect)) return null;
892
+ if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
893
+ return { reason: "sensitive-fields", emailCount: 0 };
894
+ }
878
895
  function detectListPII(target) {
879
896
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
880
897
  let itemsWithEmail = 0;
@@ -893,12 +910,13 @@ function detectListPII(target) {
893
910
  }
894
911
  function detectPII(method, reqBody, resBody) {
895
912
  const target = unwrapResponse(resBody);
896
- return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
913
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
897
914
  }
898
915
  var REASON_LABELS = {
899
916
  echo: "echoes back PII from the request body",
900
917
  "full-record": "returns a full record with email and internal IDs",
901
- "list-pii": "returns a list of records containing email addresses"
918
+ "list-pii": "returns a list of records containing email addresses",
919
+ "sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
902
920
  };
903
921
  var responsePiiLeakRule = {
904
922
  id: "response-pii-leak",
@@ -910,14 +928,14 @@ var responsePiiLeakRule = {
910
928
  const seen = /* @__PURE__ */ new Map();
911
929
  for (const r of ctx.requests) {
912
930
  if (isErrorStatus(r.statusCode)) continue;
931
+ if (SELF_SERVICE_PATH.test(r.path)) continue;
913
932
  const resJson = ctx.parsedBodies.response.get(r.id);
914
933
  if (!resJson) continue;
915
934
  const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
916
935
  const detection = detectPII(r.method, reqJson, resJson);
917
936
  if (!detection) continue;
918
937
  const ep = `${r.method} ${r.path}`;
919
- const dedupKey = `${ep}:${detection.reason}`;
920
- const existing = seen.get(dedupKey);
938
+ const existing = seen.get(ep);
921
939
  if (existing) {
922
940
  existing.count++;
923
941
  continue;
@@ -926,12 +944,12 @@ var responsePiiLeakRule = {
926
944
  severity: "warning",
927
945
  rule: "response-pii-leak",
928
946
  title: "PII Leak in Response",
929
- desc: `${ep} \u2014 ${REASON_LABELS[detection.reason]}`,
930
- hint: this.hint,
947
+ desc: `${ep} \u2014 exposes PII in response`,
948
+ hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
931
949
  endpoint: ep,
932
950
  count: 1
933
951
  };
934
- seen.set(dedupKey, finding);
952
+ seen.set(ep, finding);
935
953
  findings.push(finding);
936
954
  }
937
955
  return findings;
@@ -955,7 +973,9 @@ function buildBodyCache(requests) {
955
973
  return { response, request };
956
974
  }
957
975
  var SecurityScanner = class {
958
- rules = [];
976
+ constructor() {
977
+ this.rules = [];
978
+ }
959
979
  register(rule) {
960
980
  this.rules.push(rule);
961
981
  }
@@ -992,7 +1012,9 @@ function createDefaultScanner() {
992
1012
 
993
1013
  // src/core/disposable.ts
994
1014
  var SubscriptionBag = class {
995
- items = [];
1015
+ constructor() {
1016
+ this.items = [];
1017
+ }
996
1018
  add(teardown) {
997
1019
  this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
998
1020
  }
@@ -1557,7 +1579,9 @@ function prepareContext(ctx) {
1557
1579
  // src/analysis/insights/runner.ts
1558
1580
  var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
1559
1581
  var InsightRunner = class {
1560
- rules = [];
1582
+ constructor() {
1583
+ this.rules = [];
1584
+ }
1561
1585
  register(rule) {
1562
1586
  this.rules.push(rule);
1563
1587
  }
@@ -2123,13 +2147,12 @@ var AnalysisEngine = class {
2123
2147
  constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
2124
2148
  this.registry = registry;
2125
2149
  this.debounceMs = debounceMs;
2150
+ this.cachedInsights = [];
2151
+ this.cachedFindings = [];
2152
+ this.debounceTimer = null;
2153
+ this.subs = new SubscriptionBag();
2126
2154
  this.scanner = createDefaultScanner();
2127
2155
  }
2128
- scanner;
2129
- cachedInsights = [];
2130
- cachedFindings = [];
2131
- debounceTimer = null;
2132
- subs = new SubscriptionBag();
2133
2156
  start() {
2134
2157
  const bus = this.registry.get("event-bus");
2135
2158
  this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
@@ -2203,7 +2226,7 @@ var AnalysisEngine = class {
2203
2226
  };
2204
2227
 
2205
2228
  // src/index.ts
2206
- var VERSION = "0.8.6";
2229
+ var VERSION = "0.8.7";
2207
2230
  export {
2208
2231
  AdapterRegistry,
2209
2232
  AnalysisEngine,
@@ -18,7 +18,7 @@ var init_limits = __esm({
18
18
  SECRET_SCAN_ARRAY_LIMIT = 5;
19
19
  PII_SCAN_ARRAY_LIMIT = 10;
20
20
  MIN_SECRET_VALUE_LENGTH = 8;
21
- FULL_RECORD_MIN_FIELDS = 5;
21
+ FULL_RECORD_MIN_FIELDS = 8;
22
22
  LIST_PII_MIN_ITEMS = 2;
23
23
  MAX_OBJECT_SCAN_DEPTH = 5;
24
24
  ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
@@ -172,7 +172,7 @@ var init_mcp = __esm({
172
172
  MAX_TIMELINE_EVENTS = 20;
173
173
  MAX_RESOLVED_DISPLAY = 5;
174
174
  ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
175
- MCP_SERVER_VERSION = "0.8.6";
175
+ MCP_SERVER_VERSION = "0.8.7";
176
176
  }
177
177
  });
178
178
 
@@ -215,6 +215,20 @@ var init_cli = __esm({
215
215
  }
216
216
  });
217
217
 
218
+ // src/constants/timeline.ts
219
+ var init_timeline = __esm({
220
+ "src/constants/timeline.ts"() {
221
+ "use strict";
222
+ }
223
+ });
224
+
225
+ // src/constants/sdk-events.ts
226
+ var init_sdk_events = __esm({
227
+ "src/constants/sdk-events.ts"() {
228
+ "use strict";
229
+ }
230
+ });
231
+
218
232
  // src/constants/index.ts
219
233
  var init_constants = __esm({
220
234
  "src/constants/index.ts"() {
@@ -232,6 +246,8 @@ var init_constants = __esm({
232
246
  init_telemetry();
233
247
  init_lifecycle();
234
248
  init_cli();
249
+ init_timeline();
250
+ init_sdk_events();
235
251
  }
236
252
  });
237
253
 
@@ -1399,6 +1415,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
1399
1415
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
1400
1416
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
1401
1417
  var INTERNAL_ID_SUFFIX = /Id$|_id$/;
1418
+ var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
1419
+ var SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
1402
1420
  var RULE_HINTS = {
1403
1421
  "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
1404
1422
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
@@ -1748,6 +1766,15 @@ function hasInternalIds(obj) {
1748
1766
  }
1749
1767
  return false;
1750
1768
  }
1769
+ function hasSensitiveFieldNames(obj, depth = 0) {
1770
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
1771
+ if (!obj || typeof obj !== "object") return false;
1772
+ if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
1773
+ for (const key of Object.keys(obj)) {
1774
+ if (SENSITIVE_FIELD_NAMES.test(key)) return true;
1775
+ }
1776
+ return false;
1777
+ }
1751
1778
  function detectEchoPII(method, reqBody, target) {
1752
1779
  if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
1753
1780
  const reqEmails = findEmails(reqBody);
@@ -1769,6 +1796,13 @@ function detectFullRecordPII(target) {
1769
1796
  if (emails.length === 0) return null;
1770
1797
  return { reason: "full-record", emailCount: emails.length };
1771
1798
  }
1799
+ function detectSensitiveFieldPII(target) {
1800
+ const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
1801
+ if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
1802
+ if (!hasSensitiveFieldNames(inspect)) return null;
1803
+ if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
1804
+ return { reason: "sensitive-fields", emailCount: 0 };
1805
+ }
1772
1806
  function detectListPII(target) {
1773
1807
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
1774
1808
  let itemsWithEmail = 0;
@@ -1787,12 +1821,13 @@ function detectListPII(target) {
1787
1821
  }
1788
1822
  function detectPII(method, reqBody, resBody) {
1789
1823
  const target = unwrapResponse(resBody);
1790
- return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
1824
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
1791
1825
  }
1792
1826
  var REASON_LABELS = {
1793
1827
  echo: "echoes back PII from the request body",
1794
1828
  "full-record": "returns a full record with email and internal IDs",
1795
- "list-pii": "returns a list of records containing email addresses"
1829
+ "list-pii": "returns a list of records containing email addresses",
1830
+ "sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
1796
1831
  };
1797
1832
  var responsePiiLeakRule = {
1798
1833
  id: "response-pii-leak",
@@ -1804,14 +1839,14 @@ var responsePiiLeakRule = {
1804
1839
  const seen = /* @__PURE__ */ new Map();
1805
1840
  for (const r of ctx.requests) {
1806
1841
  if (isErrorStatus(r.statusCode)) continue;
1842
+ if (SELF_SERVICE_PATH.test(r.path)) continue;
1807
1843
  const resJson = ctx.parsedBodies.response.get(r.id);
1808
1844
  if (!resJson) continue;
1809
1845
  const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
1810
1846
  const detection = detectPII(r.method, reqJson, resJson);
1811
1847
  if (!detection) continue;
1812
1848
  const ep = `${r.method} ${r.path}`;
1813
- const dedupKey = `${ep}:${detection.reason}`;
1814
- const existing = seen.get(dedupKey);
1849
+ const existing = seen.get(ep);
1815
1850
  if (existing) {
1816
1851
  existing.count++;
1817
1852
  continue;
@@ -1820,12 +1855,12 @@ var responsePiiLeakRule = {
1820
1855
  severity: "warning",
1821
1856
  rule: "response-pii-leak",
1822
1857
  title: "PII Leak in Response",
1823
- desc: `${ep} \u2014 ${REASON_LABELS[detection.reason]}`,
1824
- hint: this.hint,
1858
+ desc: `${ep} \u2014 exposes PII in response`,
1859
+ hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
1825
1860
  endpoint: ep,
1826
1861
  count: 1
1827
1862
  };
1828
- seen.set(dedupKey, finding);
1863
+ seen.set(ep, finding);
1829
1864
  findings.push(finding);
1830
1865
  }
1831
1866
  return findings;
@@ -1894,7 +1929,7 @@ init_constants();
1894
1929
  init_endpoint();
1895
1930
 
1896
1931
  // src/index.ts
1897
- var VERSION = "0.8.6";
1932
+ var VERSION = "0.8.7";
1898
1933
 
1899
1934
  // src/cli/commands/install.ts
1900
1935
  init_constants();