brakit 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -20,7 +20,7 @@ interface TracedRequest {
20
20
  }
21
21
  type RequestListener = (req: TracedRequest) => void;
22
22
 
23
- type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "flask" | "fastapi" | "django" | "custom" | "unknown";
23
+ type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "express" | "fastify" | "koa" | "hono" | "nestjs" | "hapi" | "adonis" | "sails" | "flask" | "fastapi" | "django" | "custom" | "unknown";
24
24
  interface DetectedProject {
25
25
  framework: Framework;
26
26
  devCommand: string;
@@ -227,10 +227,11 @@ declare class IssueStore {
227
227
  stop(): void;
228
228
  upsert(issue: Issue, source: IssueSource): StatefulIssue;
229
229
  /**
230
- * Reconcile issues against the current analysis results using evidence-based resolution.
231
- *
232
- * @param currentIssueIds - IDs of issues detected in the current analysis cycle
233
- * @param activeEndpoints - Endpoints that had requests in the current cycle
230
+ * Evidence-based reconciliation: for each active issue whose endpoint had
231
+ * traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
232
+ * After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
233
+ * Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
234
+ * Resolved and stale issues are pruned after their respective TTLs expire.
234
235
  */
235
236
  reconcile(currentIssueIds: Set<string>, activeEndpoints: Set<string>): void;
236
237
  transition(issueId: string, state: IssueState): boolean;
@@ -468,6 +469,147 @@ declare class MetricsStore {
468
469
  private getOrCreateEndpoint;
469
470
  }
470
471
 
472
+ /** LiveGraph data model — runtime dependency graph types. */
473
+ type NodeType = "endpoint" | "cluster" | "table" | "external";
474
+ type EdgeType = "reads" | "writes" | "fetches" | "calls";
475
+ interface GraphNodeStats {
476
+ requestCount: number;
477
+ avgLatencyMs: number;
478
+ errorRate: number;
479
+ avgQueryCount: number;
480
+ lastSeenAt: number;
481
+ firstSeenAt: number;
482
+ }
483
+ interface SecurityFindingSummary {
484
+ rule: string;
485
+ severity: string;
486
+ title: string;
487
+ count: number;
488
+ }
489
+ interface InsightSummary {
490
+ type: string;
491
+ severity: string;
492
+ title: string;
493
+ }
494
+ interface NodeAnnotations {
495
+ categories?: string[];
496
+ hasAuth?: boolean;
497
+ isMiddleware?: boolean;
498
+ securityFindings?: SecurityFindingSummary[];
499
+ insights?: InsightSummary[];
500
+ openIssueCount?: number;
501
+ p95Ms?: number;
502
+ }
503
+ interface EdgeAnnotations {
504
+ hasIssue?: boolean;
505
+ }
506
+ interface GraphNode {
507
+ id: string;
508
+ type: NodeType;
509
+ label: string;
510
+ children?: string[];
511
+ stats: GraphNodeStats;
512
+ annotations?: NodeAnnotations;
513
+ }
514
+ interface GraphEdgeStats {
515
+ frequency: number;
516
+ avgLatencyMs: number;
517
+ lastSeenAt: number;
518
+ firstSeenAt: number;
519
+ }
520
+ interface GraphEdge {
521
+ id: string;
522
+ source: string;
523
+ target: string;
524
+ type: EdgeType;
525
+ stats: GraphEdgeStats;
526
+ patterns?: string[];
527
+ annotations?: EdgeAnnotations;
528
+ }
529
+ interface LiveGraph {
530
+ nodes: Map<string, GraphNode>;
531
+ edges: Map<string, GraphEdge>;
532
+ metadata: {
533
+ totalObservations: number;
534
+ lastUpdatedAt: number;
535
+ };
536
+ }
537
+ interface ClusterInfo {
538
+ id: string;
539
+ label: string;
540
+ children: string[];
541
+ stats: GraphNodeStats;
542
+ }
543
+ interface GraphApiResponse {
544
+ nodes: GraphNode[];
545
+ edges: GraphEdge[];
546
+ clusters: ClusterInfo[];
547
+ metadata: {
548
+ totalObservations: number;
549
+ lastUpdatedAt: number;
550
+ };
551
+ }
552
+
553
+ type GroupingStrategy = "path" | "auth-boundary" | "data-domain";
554
+
555
+ /**
556
+ * GraphBuilder — listens to EventBus events and incrementally builds
557
+ * a runtime dependency graph of the application.
558
+ *
559
+ * Timing: queries and fetches fire DURING request processing, but
560
+ * request:completed fires AFTER the response is sent. We buffer
561
+ * telemetry events by parentRequestId and process them when the
562
+ * request completes (so we know the endpoint key).
563
+ */
564
+
565
+ declare class GraphBuilder {
566
+ private bus;
567
+ private requestStore;
568
+ private graph;
569
+ /**
570
+ * Buffered telemetry events waiting for their parent request to complete.
571
+ * Key = parentRequestId, value = pending queries/fetches.
572
+ */
573
+ private pending;
574
+ /** Accumulated request categories per endpoint node. */
575
+ private nodeCategories;
576
+ /** Latest analysis snapshot — refreshed on every analysis:updated event. */
577
+ private latestAnalysis;
578
+ private cleanupUnsubs;
579
+ constructor(bus: EventBus, requestStore: RequestStore);
580
+ start(): void;
581
+ stop(): void;
582
+ getGraph(): LiveGraph;
583
+ /**
584
+ * Enrich endpoint nodes with p95 from an external metrics source.
585
+ * Called by the API handler which has access to MetricsStore.
586
+ */
587
+ enrichWithMetrics(getP95: (endpointKey: string) => number | undefined): void;
588
+ getApiResponse(options?: {
589
+ cluster?: string;
590
+ node?: string;
591
+ level?: string;
592
+ grouping?: string;
593
+ }): GraphApiResponse;
594
+ clear(): void;
595
+ private handleAnalysisUpdate;
596
+ private enrichNodesFromAnalysis;
597
+ private handleRequest;
598
+ private handleQuery;
599
+ private handleFetch;
600
+ private processQuery;
601
+ private processFetch;
602
+ computeClusters(strategy?: GroupingStrategy): Map<string, ClusterInfo>;
603
+ private clusterByPath;
604
+ private clusterByAuthBoundary;
605
+ private clusterByDataDomain;
606
+ private buildCluster;
607
+ private getEndpointView;
608
+ private getClusterView;
609
+ private getClusterExpanded;
610
+ private getNodeNeighborhood;
611
+ }
612
+
471
613
  interface Services {
472
614
  bus: EventBus;
473
615
  requestStore: RequestStore;
@@ -478,6 +620,7 @@ interface Services {
478
620
  metricsStore: MetricsStore;
479
621
  issueStore: IssueStore;
480
622
  analysisEngine: AnalysisEngine;
623
+ graphBuilder: GraphBuilder;
481
624
  }
482
625
 
483
626
  declare class AnalysisEngine {
package/dist/api.js CHANGED
@@ -232,7 +232,7 @@ var IssueStore = class {
232
232
  existing.occurrences++;
233
233
  existing.issue = issue;
234
234
  existing.cleanHitsSinceLastSeen = 0;
235
- if (existing.state === "resolved" || existing.state === "stale") {
235
+ if (existing.aiStatus !== "wont_fix" && (existing.state === "resolved" || existing.state === "stale")) {
236
236
  existing.state = "regressed";
237
237
  existing.resolvedAt = null;
238
238
  }
@@ -258,10 +258,11 @@ var IssueStore = class {
258
258
  return stateful;
259
259
  }
260
260
  /**
261
- * Reconcile issues against the current analysis results using evidence-based resolution.
262
- *
263
- * @param currentIssueIds - IDs of issues detected in the current analysis cycle
264
- * @param activeEndpoints - Endpoints that had requests in the current cycle
261
+ * Evidence-based reconciliation: for each active issue whose endpoint had
262
+ * traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
263
+ * After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
264
+ * Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
265
+ * Resolved and stale issues are pruned after their respective TTLs expire.
265
266
  */
266
267
  reconcile(currentIssueIds, activeEndpoints) {
267
268
  const now = Date.now();
@@ -391,11 +392,22 @@ import { readFile as readFile3, readdir } from "fs/promises";
391
392
  import { existsSync as existsSync4 } from "fs";
392
393
  import { join as join2, relative } from "path";
393
394
  var FRAMEWORKS = [
395
+ // Meta-frameworks first (they bundle Express/Vite internally)
394
396
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
395
397
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
396
398
  { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
397
- { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
398
- { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
399
+ { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] },
400
+ { name: "nestjs", dep: "@nestjs/core", devCmd: "nest start", bin: "nest", defaultPort: 3e3, devArgs: ["--watch"] },
401
+ { name: "adonis", dep: "@adonisjs/core", devCmd: "node ace serve", bin: "ace", defaultPort: 3333, devArgs: ["serve", "--watch"] },
402
+ { name: "sails", dep: "sails", devCmd: "sails lift", bin: "sails", defaultPort: 1337, devArgs: ["lift"] },
403
+ // Server frameworks
404
+ { name: "hono", dep: "hono", devCmd: "node", bin: "node", defaultPort: 3e3 },
405
+ { name: "fastify", dep: "fastify", devCmd: "node", bin: "node", defaultPort: 3e3 },
406
+ { name: "koa", dep: "koa", devCmd: "node", bin: "node", defaultPort: 3e3 },
407
+ { name: "hapi", dep: "@hapi/hapi", devCmd: "node", bin: "node", defaultPort: 3e3 },
408
+ { name: "express", dep: "express", devCmd: "node", bin: "node", defaultPort: 3e3 },
409
+ // Bundlers (last — likely used alongside a framework above)
410
+ { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] }
399
411
  ];
400
412
  async function detectProject(rootDir) {
401
413
  const pkgPath = join2(rootDir, "package.json");
@@ -498,22 +510,124 @@ function unwrapResponse(parsed) {
498
510
  }
499
511
 
500
512
  // src/analysis/rules/patterns.ts
501
- var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
502
- var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
503
- var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
513
+ var SECRET_KEY_SET = /* @__PURE__ */ new Set([
514
+ "password",
515
+ "passwd",
516
+ "secret",
517
+ "api_key",
518
+ "apiKey",
519
+ "api_secret",
520
+ "apiSecret",
521
+ "private_key",
522
+ "privateKey",
523
+ "client_secret",
524
+ "clientSecret"
525
+ ]);
526
+ var SECRET_KEYS = { test: (s) => SECRET_KEY_SET.has(s) };
527
+ var TOKEN_PARAM_SET = /* @__PURE__ */ new Set([
528
+ "token",
529
+ "api_key",
530
+ "apiKey",
531
+ "secret",
532
+ "password",
533
+ "access_token",
534
+ "session_id",
535
+ "sessionId"
536
+ ]);
537
+ var TOKEN_PARAMS = { test: (s) => TOKEN_PARAM_SET.has(s) };
538
+ var SAFE_PARAM_SET = /* @__PURE__ */ new Set([
539
+ "_rsc",
540
+ "__clerk_handshake",
541
+ "__clerk_db_jwt",
542
+ "callback",
543
+ "code",
544
+ "state",
545
+ "nonce",
546
+ "redirect_uri",
547
+ "utm_",
548
+ "fbclid",
549
+ "gclid"
550
+ ]);
551
+ var SAFE_PARAMS = { test: (s) => SAFE_PARAM_SET.has(s) };
552
+ var INTERNAL_ID_KEY_SET = /* @__PURE__ */ new Set([
553
+ "id",
554
+ "_id",
555
+ "userId",
556
+ "user_id",
557
+ "createdBy",
558
+ "updatedBy",
559
+ "organizationId",
560
+ "org_id",
561
+ "tenantId",
562
+ "tenant_id"
563
+ ]);
564
+ var INTERNAL_ID_KEYS = { test: (s) => INTERNAL_ID_KEY_SET.has(s) };
565
+ var INTERNAL_ID_SUFFIX = {
566
+ test: (s) => s.endsWith("Id") || s.endsWith("_id")
567
+ };
568
+ var SENSITIVE_FIELD_SET = /* @__PURE__ */ new Set([
569
+ "phone",
570
+ "phonenumber",
571
+ "phone_number",
572
+ "ssn",
573
+ "socialsecuritynumber",
574
+ "social_security_number",
575
+ "dateofbirth",
576
+ "date_of_birth",
577
+ "dob",
578
+ "address",
579
+ "streetaddress",
580
+ "street_address",
581
+ "creditcard",
582
+ "credit_card",
583
+ "cardnumber",
584
+ "card_number",
585
+ "bankaccount",
586
+ "bank_account",
587
+ "passport",
588
+ "passportnumber",
589
+ "passport_number",
590
+ "nationalid",
591
+ "national_id"
592
+ ]);
593
+ var SENSITIVE_FIELD_NAMES = {
594
+ test: (s) => SENSITIVE_FIELD_SET.has(s.toLowerCase())
595
+ };
596
+ var SELF_SERVICE_SEGMENTS = /* @__PURE__ */ new Set(["me", "account", "profile", "settings", "self"]);
597
+ var SELF_SERVICE_PATH = {
598
+ test: (path) => {
599
+ const segments = path.toLowerCase().split(/[/?#]/);
600
+ return segments.some((seg) => SELF_SERVICE_SEGMENTS.has(seg));
601
+ }
602
+ };
603
+ var MASKED_LITERALS = ["[REDACTED]", "[FILTERED]", "CHANGE_ME"];
604
+ var MASKED_RE = {
605
+ test: (s) => {
606
+ const upper = s.toUpperCase();
607
+ if (MASKED_LITERALS.some((m) => upper.includes(m))) return true;
608
+ if (s.length > 0 && s.split("").every((c) => c === "*")) return true;
609
+ if (s.length >= 3 && s.split("").every((c) => c === "x" || c === "X")) return true;
610
+ return false;
611
+ }
612
+ };
613
+ var DB_PROTOCOLS = ["postgres://", "mysql://", "mongodb://", "redis://"];
614
+ var DB_CONN_RE = {
615
+ test: (s) => DB_PROTOCOLS.some((p) => s.includes(p))
616
+ };
617
+ var SELECT_STAR_RE = {
618
+ test: (s) => {
619
+ const t = s.trimStart().toUpperCase();
620
+ return t.startsWith("SELECT *") || t.startsWith("SELECT *");
621
+ }
622
+ };
623
+ var SELECT_DOT_STAR_RE = {
624
+ test: (s) => s.toUpperCase().includes(".* FROM")
625
+ };
504
626
  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+/;
505
- var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
506
627
  var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
507
628
  var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
508
629
  var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
509
- var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
510
630
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
511
- var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
512
- var INTERNAL_ID_SUFFIX = /Id$|_id$/;
513
- var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
514
- 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;
515
- var SELECT_STAR_RE = /^SELECT\s+\*/i;
516
- var SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
517
631
  var RULE_HINTS = {
518
632
  "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
519
633
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
@@ -1064,16 +1178,14 @@ var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
1064
1178
  var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
1065
1179
  var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
1066
1180
  var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
1181
+ var DASHBOARD_API_GRAPH = `${DASHBOARD_PREFIX}/api/graph`;
1067
1182
  var VALID_TABS_TUPLE = [
1068
1183
  "overview",
1069
1184
  "actions",
1070
- "requests",
1071
- "fetches",
1072
- "queries",
1073
- "errors",
1074
- "logs",
1185
+ "insights",
1075
1186
  "performance",
1076
- "security"
1187
+ "graph",
1188
+ "explorer"
1077
1189
  ];
1078
1190
  var VALID_TABS = new Set(VALID_TABS_TUPLE);
1079
1191
 
@@ -1081,12 +1193,54 @@ var VALID_TABS = new Set(VALID_TABS_TUPLE);
1081
1193
  var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
1082
1194
 
1083
1195
  // src/utils/endpoint.ts
1084
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1085
- var NUMERIC_ID_RE = /^\d+$/;
1086
- var HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
1087
- var ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
1196
+ var UUID_LEN = 36;
1197
+ var MIN_HEX_LEN = 12;
1198
+ var MIN_TOKEN_LEN = 8;
1199
+ function isUUID(s) {
1200
+ if (s.length !== UUID_LEN) return false;
1201
+ for (let i = 0; i < s.length; i++) {
1202
+ const c = s[i];
1203
+ if (i === 8 || i === 13 || i === 18 || i === 23) {
1204
+ if (c !== "-") return false;
1205
+ } else {
1206
+ if (!isHexChar(c)) return false;
1207
+ }
1208
+ }
1209
+ return true;
1210
+ }
1211
+ function isHexChar(c) {
1212
+ const code = c.charCodeAt(0);
1213
+ return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
1214
+ }
1215
+ function isNumericId(s) {
1216
+ if (s.length === 0) return false;
1217
+ for (let i = 0; i < s.length; i++) {
1218
+ const code = s.charCodeAt(i);
1219
+ if (code < 48 || code > 57) return false;
1220
+ }
1221
+ return true;
1222
+ }
1223
+ function isHexHash(s) {
1224
+ if (s.length < MIN_HEX_LEN) return false;
1225
+ for (let i = 0; i < s.length; i++) {
1226
+ if (!isHexChar(s[i])) return false;
1227
+ }
1228
+ return true;
1229
+ }
1230
+ function isAlphanumericToken(s) {
1231
+ if (s.length < MIN_TOKEN_LEN) return false;
1232
+ let hasLetter = false;
1233
+ let hasDigit = false;
1234
+ for (let i = 0; i < s.length; i++) {
1235
+ const code = s.charCodeAt(i);
1236
+ if (code >= 65 && code <= 90 || code >= 97 && code <= 122) hasLetter = true;
1237
+ else if (code >= 48 && code <= 57) hasDigit = true;
1238
+ else if (code !== 95 && code !== 45) return false;
1239
+ }
1240
+ return hasLetter && hasDigit;
1241
+ }
1088
1242
  function isDynamicSegment(segment) {
1089
- return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
1243
+ return isUUID(segment) || isNumericId(segment) || isHexHash(segment) || isAlphanumericToken(segment);
1090
1244
  }
1091
1245
  var DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
1092
1246
  function normalizePath(path) {
@@ -1097,9 +1251,12 @@ function normalizePath(path) {
1097
1251
  function getEndpointKey(method, path) {
1098
1252
  return `${method} ${normalizePath(path)}`;
1099
1253
  }
1100
- var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1101
1254
  function extractEndpointFromDesc(desc) {
1102
- return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1255
+ const spaceIdx = desc.indexOf(" ");
1256
+ if (spaceIdx <= 0) return null;
1257
+ const secondSpace = desc.indexOf(" ", spaceIdx + 1);
1258
+ if (secondSpace === -1) return desc;
1259
+ return desc.slice(0, secondSpace);
1103
1260
  }
1104
1261
  function stripQueryString(path) {
1105
1262
  const i = path.indexOf("?");
@@ -1107,6 +1264,10 @@ function stripQueryString(path) {
1107
1264
  }
1108
1265
 
1109
1266
  // src/analysis/categorize.ts
1267
+ function isAuthPath(path) {
1268
+ const lower = path.toLowerCase();
1269
+ return lower.startsWith("/api/auth") || lower.startsWith("/clerk") || lower.startsWith("/api/clerk");
1270
+ }
1110
1271
  function detectCategory(req) {
1111
1272
  const { method, url, statusCode, responseHeaders } = req;
1112
1273
  if (req.isStatic) return "static";
@@ -1115,7 +1276,7 @@ function detectCategory(req) {
1115
1276
  return "auth-handshake";
1116
1277
  }
1117
1278
  const effectivePath = getEffectivePath(req);
1118
- if (/^\/api\/auth/i.test(effectivePath) || /^\/(api\/)?clerk/i.test(effectivePath)) {
1279
+ if (isAuthPath(effectivePath)) {
1119
1280
  return "auth-check";
1120
1281
  }
1121
1282
  if (method === "POST" && !effectivePath.startsWith("/api/")) {
@@ -1205,8 +1366,11 @@ function generateHumanLabel(req, category) {
1205
1366
  return failed ? `${req.method} ${req.path} failed` : `${req.method} ${req.path}`;
1206
1367
  }
1207
1368
  }
1369
+ function stripApiPrefix(s) {
1370
+ return s.startsWith("/api/") ? s.slice(5) : s;
1371
+ }
1208
1372
  function prettifyEndpoint(name) {
1209
- const cleaned = name.replace(/^\/api\//, "").replace(/\//g, " ").replace(/\.\.\./g, "").trim();
1373
+ const cleaned = stripApiPrefix(name).split("/").join(" ").split("...").join("").trim();
1210
1374
  if (!cleaned) return "data";
1211
1375
  return cleaned.split(" ").map((word) => {
1212
1376
  if (word.endsWith("ses") || word.endsWith("us") || word.endsWith("ss"))
@@ -1216,24 +1380,28 @@ function prettifyEndpoint(name) {
1216
1380
  return word;
1217
1381
  }).join(" ");
1218
1382
  }
1383
+ var VERB_MAP = [
1384
+ ["enhance", "Enhanced"],
1385
+ ["generate", "Generated"],
1386
+ ["create", "Created"],
1387
+ ["update", "Updated"],
1388
+ ["delete", "Deleted"],
1389
+ ["remove", "Deleted"],
1390
+ ["send", "Sent"],
1391
+ ["upload", "Uploaded"],
1392
+ ["save", "Saved"],
1393
+ ["submit", "Submitted"],
1394
+ ["login", "Logged in"],
1395
+ ["signin", "Logged in"],
1396
+ ["logout", "Logged out"],
1397
+ ["signout", "Logged out"],
1398
+ ["register", "Registered"],
1399
+ ["signup", "Registered"]
1400
+ ];
1219
1401
  function deriveActionVerb(method, endpointName) {
1220
1402
  const lower = endpointName.toLowerCase();
1221
- const VERB_PATTERNS = [
1222
- [/enhance/, "Enhanced"],
1223
- [/generate/, "Generated"],
1224
- [/create/, "Created"],
1225
- [/update/, "Updated"],
1226
- [/delete|remove/, "Deleted"],
1227
- [/send/, "Sent"],
1228
- [/upload/, "Uploaded"],
1229
- [/save/, "Saved"],
1230
- [/submit/, "Submitted"],
1231
- [/login|signin/, "Logged in"],
1232
- [/logout|signout/, "Logged out"],
1233
- [/register|signup/, "Registered"]
1234
- ];
1235
- for (const [pattern, verb] of VERB_PATTERNS) {
1236
- if (pattern.test(lower)) return verb;
1403
+ for (const [keyword, verb] of VERB_MAP) {
1404
+ if (lower.includes(keyword)) return verb;
1237
1405
  }
1238
1406
  switch (method) {
1239
1407
  case "POST":
@@ -1248,14 +1416,16 @@ function deriveActionVerb(method, endpointName) {
1248
1416
  }
1249
1417
  }
1250
1418
  function getEndpointName(path) {
1251
- const parts = path.replace(/^\/api\//, "").split("/");
1419
+ const parts = stripApiPrefix(path).split("/");
1252
1420
  if (parts.length <= 2) return parts.join("/");
1253
1421
  return parts.map((p) => p.length > ENDPOINT_TRUNCATE_LENGTH ? "..." : p).join("/");
1254
1422
  }
1255
1423
  function prettifyPageName(path) {
1256
- const clean = path.replace(/^\//, "").replace(/\/$/, "");
1424
+ let clean = path;
1425
+ if (clean.startsWith("/")) clean = clean.slice(1);
1426
+ if (clean.endsWith("/")) clean = clean.slice(0, -1);
1257
1427
  if (!clean) return "Home";
1258
- return clean.split("/").map((s) => capitalize(s.replace(/[-_]/g, " "))).join(" ");
1428
+ return clean.split("/").map((s) => capitalize(s.split("-").join(" ").split("_").join(" "))).join(" ");
1259
1429
  }
1260
1430
  function capitalize(s) {
1261
1431
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -1488,7 +1658,15 @@ var TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
1488
1658
  function normalizeSQL(sql) {
1489
1659
  if (!sql) return { op: "OTHER", table: "" };
1490
1660
  const trimmed = sql.trim();
1491
- const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
1661
+ let spaceIdx = -1;
1662
+ for (let i = 0; i < trimmed.length; i++) {
1663
+ const c = trimmed[i];
1664
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
1665
+ spaceIdx = i;
1666
+ break;
1667
+ }
1668
+ }
1669
+ const keyword = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toUpperCase();
1492
1670
  const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
1493
1671
  const table = trimmed.match(TABLE_RE)?.[1] ?? "";
1494
1672
  return { op, table };
@@ -2241,7 +2419,7 @@ var AnalysisEngine = class {
2241
2419
  };
2242
2420
 
2243
2421
  // src/index.ts
2244
- var VERSION = "0.9.2";
2422
+ var VERSION = "0.10.1";
2245
2423
  export {
2246
2424
  AdapterRegistry,
2247
2425
  AnalysisEngine,