brakit 0.9.1 → 0.10.0
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 +142 -0
- package/dist/api.js +212 -42
- package/dist/bin/brakit.js +138 -19
- package/dist/dashboard-client.global.js +587 -276
- package/dist/dashboard.html +691 -276
- package/dist/mcp/server.js +4 -2
- package/dist/runtime/index.js +1103 -74
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -468,6 +468,147 @@ declare class MetricsStore {
|
|
|
468
468
|
private getOrCreateEndpoint;
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
+
/** LiveGraph data model — runtime dependency graph types. */
|
|
472
|
+
type NodeType = "endpoint" | "cluster" | "table" | "external";
|
|
473
|
+
type EdgeType = "reads" | "writes" | "fetches" | "calls";
|
|
474
|
+
interface GraphNodeStats {
|
|
475
|
+
requestCount: number;
|
|
476
|
+
avgLatencyMs: number;
|
|
477
|
+
errorRate: number;
|
|
478
|
+
avgQueryCount: number;
|
|
479
|
+
lastSeenAt: number;
|
|
480
|
+
firstSeenAt: number;
|
|
481
|
+
}
|
|
482
|
+
interface SecurityFindingSummary {
|
|
483
|
+
rule: string;
|
|
484
|
+
severity: string;
|
|
485
|
+
title: string;
|
|
486
|
+
count: number;
|
|
487
|
+
}
|
|
488
|
+
interface InsightSummary {
|
|
489
|
+
type: string;
|
|
490
|
+
severity: string;
|
|
491
|
+
title: string;
|
|
492
|
+
}
|
|
493
|
+
interface NodeAnnotations {
|
|
494
|
+
categories?: string[];
|
|
495
|
+
hasAuth?: boolean;
|
|
496
|
+
isMiddleware?: boolean;
|
|
497
|
+
securityFindings?: SecurityFindingSummary[];
|
|
498
|
+
insights?: InsightSummary[];
|
|
499
|
+
openIssueCount?: number;
|
|
500
|
+
p95Ms?: number;
|
|
501
|
+
}
|
|
502
|
+
interface EdgeAnnotations {
|
|
503
|
+
hasIssue?: boolean;
|
|
504
|
+
}
|
|
505
|
+
interface GraphNode {
|
|
506
|
+
id: string;
|
|
507
|
+
type: NodeType;
|
|
508
|
+
label: string;
|
|
509
|
+
children?: string[];
|
|
510
|
+
stats: GraphNodeStats;
|
|
511
|
+
annotations?: NodeAnnotations;
|
|
512
|
+
}
|
|
513
|
+
interface GraphEdgeStats {
|
|
514
|
+
frequency: number;
|
|
515
|
+
avgLatencyMs: number;
|
|
516
|
+
lastSeenAt: number;
|
|
517
|
+
firstSeenAt: number;
|
|
518
|
+
}
|
|
519
|
+
interface GraphEdge {
|
|
520
|
+
id: string;
|
|
521
|
+
source: string;
|
|
522
|
+
target: string;
|
|
523
|
+
type: EdgeType;
|
|
524
|
+
stats: GraphEdgeStats;
|
|
525
|
+
patterns?: string[];
|
|
526
|
+
annotations?: EdgeAnnotations;
|
|
527
|
+
}
|
|
528
|
+
interface LiveGraph {
|
|
529
|
+
nodes: Map<string, GraphNode>;
|
|
530
|
+
edges: Map<string, GraphEdge>;
|
|
531
|
+
metadata: {
|
|
532
|
+
totalObservations: number;
|
|
533
|
+
lastUpdatedAt: number;
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
interface ClusterInfo {
|
|
537
|
+
id: string;
|
|
538
|
+
label: string;
|
|
539
|
+
children: string[];
|
|
540
|
+
stats: GraphNodeStats;
|
|
541
|
+
}
|
|
542
|
+
interface GraphApiResponse {
|
|
543
|
+
nodes: GraphNode[];
|
|
544
|
+
edges: GraphEdge[];
|
|
545
|
+
clusters: ClusterInfo[];
|
|
546
|
+
metadata: {
|
|
547
|
+
totalObservations: number;
|
|
548
|
+
lastUpdatedAt: number;
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
type GroupingStrategy = "path" | "auth-boundary" | "data-domain";
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* GraphBuilder — listens to EventBus events and incrementally builds
|
|
556
|
+
* a runtime dependency graph of the application.
|
|
557
|
+
*
|
|
558
|
+
* Timing: queries and fetches fire DURING request processing, but
|
|
559
|
+
* request:completed fires AFTER the response is sent. We buffer
|
|
560
|
+
* telemetry events by parentRequestId and process them when the
|
|
561
|
+
* request completes (so we know the endpoint key).
|
|
562
|
+
*/
|
|
563
|
+
|
|
564
|
+
declare class GraphBuilder {
|
|
565
|
+
private bus;
|
|
566
|
+
private requestStore;
|
|
567
|
+
private graph;
|
|
568
|
+
/**
|
|
569
|
+
* Buffered telemetry events waiting for their parent request to complete.
|
|
570
|
+
* Key = parentRequestId, value = pending queries/fetches.
|
|
571
|
+
*/
|
|
572
|
+
private pending;
|
|
573
|
+
/** Accumulated request categories per endpoint node. */
|
|
574
|
+
private nodeCategories;
|
|
575
|
+
/** Latest analysis snapshot — refreshed on every analysis:updated event. */
|
|
576
|
+
private latestAnalysis;
|
|
577
|
+
private cleanupUnsubs;
|
|
578
|
+
constructor(bus: EventBus, requestStore: RequestStore);
|
|
579
|
+
start(): void;
|
|
580
|
+
stop(): void;
|
|
581
|
+
getGraph(): LiveGraph;
|
|
582
|
+
/**
|
|
583
|
+
* Enrich endpoint nodes with p95 from an external metrics source.
|
|
584
|
+
* Called by the API handler which has access to MetricsStore.
|
|
585
|
+
*/
|
|
586
|
+
enrichWithMetrics(getP95: (endpointKey: string) => number | undefined): void;
|
|
587
|
+
getApiResponse(options?: {
|
|
588
|
+
cluster?: string;
|
|
589
|
+
node?: string;
|
|
590
|
+
level?: string;
|
|
591
|
+
grouping?: string;
|
|
592
|
+
}): GraphApiResponse;
|
|
593
|
+
clear(): void;
|
|
594
|
+
private handleAnalysisUpdate;
|
|
595
|
+
private enrichNodesFromAnalysis;
|
|
596
|
+
private handleRequest;
|
|
597
|
+
private handleQuery;
|
|
598
|
+
private handleFetch;
|
|
599
|
+
private processQuery;
|
|
600
|
+
private processFetch;
|
|
601
|
+
computeClusters(strategy?: GroupingStrategy): Map<string, ClusterInfo>;
|
|
602
|
+
private clusterByPath;
|
|
603
|
+
private clusterByAuthBoundary;
|
|
604
|
+
private clusterByDataDomain;
|
|
605
|
+
private buildCluster;
|
|
606
|
+
private getEndpointView;
|
|
607
|
+
private getClusterView;
|
|
608
|
+
private getClusterExpanded;
|
|
609
|
+
private getNodeNeighborhood;
|
|
610
|
+
}
|
|
611
|
+
|
|
471
612
|
interface Services {
|
|
472
613
|
bus: EventBus;
|
|
473
614
|
requestStore: RequestStore;
|
|
@@ -478,6 +619,7 @@ interface Services {
|
|
|
478
619
|
metricsStore: MetricsStore;
|
|
479
620
|
issueStore: IssueStore;
|
|
480
621
|
analysisEngine: AnalysisEngine;
|
|
622
|
+
graphBuilder: GraphBuilder;
|
|
481
623
|
}
|
|
482
624
|
|
|
483
625
|
declare class AnalysisEngine {
|
package/dist/api.js
CHANGED
|
@@ -498,22 +498,124 @@ function unwrapResponse(parsed) {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
// src/analysis/rules/patterns.ts
|
|
501
|
-
var
|
|
502
|
-
|
|
503
|
-
|
|
501
|
+
var SECRET_KEY_SET = /* @__PURE__ */ new Set([
|
|
502
|
+
"password",
|
|
503
|
+
"passwd",
|
|
504
|
+
"secret",
|
|
505
|
+
"api_key",
|
|
506
|
+
"apiKey",
|
|
507
|
+
"api_secret",
|
|
508
|
+
"apiSecret",
|
|
509
|
+
"private_key",
|
|
510
|
+
"privateKey",
|
|
511
|
+
"client_secret",
|
|
512
|
+
"clientSecret"
|
|
513
|
+
]);
|
|
514
|
+
var SECRET_KEYS = { test: (s) => SECRET_KEY_SET.has(s) };
|
|
515
|
+
var TOKEN_PARAM_SET = /* @__PURE__ */ new Set([
|
|
516
|
+
"token",
|
|
517
|
+
"api_key",
|
|
518
|
+
"apiKey",
|
|
519
|
+
"secret",
|
|
520
|
+
"password",
|
|
521
|
+
"access_token",
|
|
522
|
+
"session_id",
|
|
523
|
+
"sessionId"
|
|
524
|
+
]);
|
|
525
|
+
var TOKEN_PARAMS = { test: (s) => TOKEN_PARAM_SET.has(s) };
|
|
526
|
+
var SAFE_PARAM_SET = /* @__PURE__ */ new Set([
|
|
527
|
+
"_rsc",
|
|
528
|
+
"__clerk_handshake",
|
|
529
|
+
"__clerk_db_jwt",
|
|
530
|
+
"callback",
|
|
531
|
+
"code",
|
|
532
|
+
"state",
|
|
533
|
+
"nonce",
|
|
534
|
+
"redirect_uri",
|
|
535
|
+
"utm_",
|
|
536
|
+
"fbclid",
|
|
537
|
+
"gclid"
|
|
538
|
+
]);
|
|
539
|
+
var SAFE_PARAMS = { test: (s) => SAFE_PARAM_SET.has(s) };
|
|
540
|
+
var INTERNAL_ID_KEY_SET = /* @__PURE__ */ new Set([
|
|
541
|
+
"id",
|
|
542
|
+
"_id",
|
|
543
|
+
"userId",
|
|
544
|
+
"user_id",
|
|
545
|
+
"createdBy",
|
|
546
|
+
"updatedBy",
|
|
547
|
+
"organizationId",
|
|
548
|
+
"org_id",
|
|
549
|
+
"tenantId",
|
|
550
|
+
"tenant_id"
|
|
551
|
+
]);
|
|
552
|
+
var INTERNAL_ID_KEYS = { test: (s) => INTERNAL_ID_KEY_SET.has(s) };
|
|
553
|
+
var INTERNAL_ID_SUFFIX = {
|
|
554
|
+
test: (s) => s.endsWith("Id") || s.endsWith("_id")
|
|
555
|
+
};
|
|
556
|
+
var SENSITIVE_FIELD_SET = /* @__PURE__ */ new Set([
|
|
557
|
+
"phone",
|
|
558
|
+
"phonenumber",
|
|
559
|
+
"phone_number",
|
|
560
|
+
"ssn",
|
|
561
|
+
"socialsecuritynumber",
|
|
562
|
+
"social_security_number",
|
|
563
|
+
"dateofbirth",
|
|
564
|
+
"date_of_birth",
|
|
565
|
+
"dob",
|
|
566
|
+
"address",
|
|
567
|
+
"streetaddress",
|
|
568
|
+
"street_address",
|
|
569
|
+
"creditcard",
|
|
570
|
+
"credit_card",
|
|
571
|
+
"cardnumber",
|
|
572
|
+
"card_number",
|
|
573
|
+
"bankaccount",
|
|
574
|
+
"bank_account",
|
|
575
|
+
"passport",
|
|
576
|
+
"passportnumber",
|
|
577
|
+
"passport_number",
|
|
578
|
+
"nationalid",
|
|
579
|
+
"national_id"
|
|
580
|
+
]);
|
|
581
|
+
var SENSITIVE_FIELD_NAMES = {
|
|
582
|
+
test: (s) => SENSITIVE_FIELD_SET.has(s.toLowerCase())
|
|
583
|
+
};
|
|
584
|
+
var SELF_SERVICE_SEGMENTS = /* @__PURE__ */ new Set(["me", "account", "profile", "settings", "self"]);
|
|
585
|
+
var SELF_SERVICE_PATH = {
|
|
586
|
+
test: (path) => {
|
|
587
|
+
const segments = path.toLowerCase().split(/[/?#]/);
|
|
588
|
+
return segments.some((seg) => SELF_SERVICE_SEGMENTS.has(seg));
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
var MASKED_LITERALS = ["[REDACTED]", "[FILTERED]", "CHANGE_ME"];
|
|
592
|
+
var MASKED_RE = {
|
|
593
|
+
test: (s) => {
|
|
594
|
+
const upper = s.toUpperCase();
|
|
595
|
+
if (MASKED_LITERALS.some((m) => upper.includes(m))) return true;
|
|
596
|
+
if (s.length > 0 && s.split("").every((c) => c === "*")) return true;
|
|
597
|
+
if (s.length >= 3 && s.split("").every((c) => c === "x" || c === "X")) return true;
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
var DB_PROTOCOLS = ["postgres://", "mysql://", "mongodb://", "redis://"];
|
|
602
|
+
var DB_CONN_RE = {
|
|
603
|
+
test: (s) => DB_PROTOCOLS.some((p) => s.includes(p))
|
|
604
|
+
};
|
|
605
|
+
var SELECT_STAR_RE = {
|
|
606
|
+
test: (s) => {
|
|
607
|
+
const t = s.trimStart().toUpperCase();
|
|
608
|
+
return t.startsWith("SELECT *") || t.startsWith("SELECT *");
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
var SELECT_DOT_STAR_RE = {
|
|
612
|
+
test: (s) => s.toUpperCase().includes(".* FROM")
|
|
613
|
+
};
|
|
504
614
|
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
615
|
var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
507
616
|
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
508
617
|
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
618
|
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
619
|
var RULE_HINTS = {
|
|
518
620
|
"exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
|
|
519
621
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
@@ -1064,6 +1166,7 @@ var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
|
1064
1166
|
var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
1065
1167
|
var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
1066
1168
|
var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
1169
|
+
var DASHBOARD_API_GRAPH = `${DASHBOARD_PREFIX}/api/graph`;
|
|
1067
1170
|
var VALID_TABS_TUPLE = [
|
|
1068
1171
|
"overview",
|
|
1069
1172
|
"actions",
|
|
@@ -1073,7 +1176,8 @@ var VALID_TABS_TUPLE = [
|
|
|
1073
1176
|
"errors",
|
|
1074
1177
|
"logs",
|
|
1075
1178
|
"performance",
|
|
1076
|
-
"security"
|
|
1179
|
+
"security",
|
|
1180
|
+
"graph"
|
|
1077
1181
|
];
|
|
1078
1182
|
var VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
1079
1183
|
|
|
@@ -1081,12 +1185,54 @@ var VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
|
1081
1185
|
var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
1082
1186
|
|
|
1083
1187
|
// src/utils/endpoint.ts
|
|
1084
|
-
var
|
|
1085
|
-
var
|
|
1086
|
-
var
|
|
1087
|
-
|
|
1188
|
+
var UUID_LEN = 36;
|
|
1189
|
+
var MIN_HEX_LEN = 12;
|
|
1190
|
+
var MIN_TOKEN_LEN = 8;
|
|
1191
|
+
function isUUID(s) {
|
|
1192
|
+
if (s.length !== UUID_LEN) return false;
|
|
1193
|
+
for (let i = 0; i < s.length; i++) {
|
|
1194
|
+
const c = s[i];
|
|
1195
|
+
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
|
1196
|
+
if (c !== "-") return false;
|
|
1197
|
+
} else {
|
|
1198
|
+
if (!isHexChar(c)) return false;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
function isHexChar(c) {
|
|
1204
|
+
const code = c.charCodeAt(0);
|
|
1205
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
|
|
1206
|
+
}
|
|
1207
|
+
function isNumericId(s) {
|
|
1208
|
+
if (s.length === 0) return false;
|
|
1209
|
+
for (let i = 0; i < s.length; i++) {
|
|
1210
|
+
const code = s.charCodeAt(i);
|
|
1211
|
+
if (code < 48 || code > 57) return false;
|
|
1212
|
+
}
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
function isHexHash(s) {
|
|
1216
|
+
if (s.length < MIN_HEX_LEN) return false;
|
|
1217
|
+
for (let i = 0; i < s.length; i++) {
|
|
1218
|
+
if (!isHexChar(s[i])) return false;
|
|
1219
|
+
}
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
function isAlphanumericToken(s) {
|
|
1223
|
+
if (s.length < MIN_TOKEN_LEN) return false;
|
|
1224
|
+
let hasLetter = false;
|
|
1225
|
+
let hasDigit = false;
|
|
1226
|
+
for (let i = 0; i < s.length; i++) {
|
|
1227
|
+
const code = s.charCodeAt(i);
|
|
1228
|
+
if (code >= 65 && code <= 90 || code >= 97 && code <= 122) hasLetter = true;
|
|
1229
|
+
else if (code >= 48 && code <= 57) hasDigit = true;
|
|
1230
|
+
else if (code !== 95 && code !== 45) return false;
|
|
1231
|
+
}
|
|
1232
|
+
return hasLetter && hasDigit;
|
|
1233
|
+
}
|
|
1088
1234
|
function isDynamicSegment(segment) {
|
|
1089
|
-
return
|
|
1235
|
+
return isUUID(segment) || isNumericId(segment) || isHexHash(segment) || isAlphanumericToken(segment);
|
|
1090
1236
|
}
|
|
1091
1237
|
var DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
|
|
1092
1238
|
function normalizePath(path) {
|
|
@@ -1097,9 +1243,12 @@ function normalizePath(path) {
|
|
|
1097
1243
|
function getEndpointKey(method, path) {
|
|
1098
1244
|
return `${method} ${normalizePath(path)}`;
|
|
1099
1245
|
}
|
|
1100
|
-
var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
1101
1246
|
function extractEndpointFromDesc(desc) {
|
|
1102
|
-
|
|
1247
|
+
const spaceIdx = desc.indexOf(" ");
|
|
1248
|
+
if (spaceIdx <= 0) return null;
|
|
1249
|
+
const secondSpace = desc.indexOf(" ", spaceIdx + 1);
|
|
1250
|
+
if (secondSpace === -1) return desc;
|
|
1251
|
+
return desc.slice(0, secondSpace);
|
|
1103
1252
|
}
|
|
1104
1253
|
function stripQueryString(path) {
|
|
1105
1254
|
const i = path.indexOf("?");
|
|
@@ -1107,6 +1256,10 @@ function stripQueryString(path) {
|
|
|
1107
1256
|
}
|
|
1108
1257
|
|
|
1109
1258
|
// src/analysis/categorize.ts
|
|
1259
|
+
function isAuthPath(path) {
|
|
1260
|
+
const lower = path.toLowerCase();
|
|
1261
|
+
return lower.startsWith("/api/auth") || lower.startsWith("/clerk") || lower.startsWith("/api/clerk");
|
|
1262
|
+
}
|
|
1110
1263
|
function detectCategory(req) {
|
|
1111
1264
|
const { method, url, statusCode, responseHeaders } = req;
|
|
1112
1265
|
if (req.isStatic) return "static";
|
|
@@ -1115,7 +1268,7 @@ function detectCategory(req) {
|
|
|
1115
1268
|
return "auth-handshake";
|
|
1116
1269
|
}
|
|
1117
1270
|
const effectivePath = getEffectivePath(req);
|
|
1118
|
-
if (
|
|
1271
|
+
if (isAuthPath(effectivePath)) {
|
|
1119
1272
|
return "auth-check";
|
|
1120
1273
|
}
|
|
1121
1274
|
if (method === "POST" && !effectivePath.startsWith("/api/")) {
|
|
@@ -1205,8 +1358,11 @@ function generateHumanLabel(req, category) {
|
|
|
1205
1358
|
return failed ? `${req.method} ${req.path} failed` : `${req.method} ${req.path}`;
|
|
1206
1359
|
}
|
|
1207
1360
|
}
|
|
1361
|
+
function stripApiPrefix(s) {
|
|
1362
|
+
return s.startsWith("/api/") ? s.slice(5) : s;
|
|
1363
|
+
}
|
|
1208
1364
|
function prettifyEndpoint(name) {
|
|
1209
|
-
const cleaned = name.
|
|
1365
|
+
const cleaned = stripApiPrefix(name).split("/").join(" ").split("...").join("").trim();
|
|
1210
1366
|
if (!cleaned) return "data";
|
|
1211
1367
|
return cleaned.split(" ").map((word) => {
|
|
1212
1368
|
if (word.endsWith("ses") || word.endsWith("us") || word.endsWith("ss"))
|
|
@@ -1216,24 +1372,28 @@ function prettifyEndpoint(name) {
|
|
|
1216
1372
|
return word;
|
|
1217
1373
|
}).join(" ");
|
|
1218
1374
|
}
|
|
1375
|
+
var VERB_MAP = [
|
|
1376
|
+
["enhance", "Enhanced"],
|
|
1377
|
+
["generate", "Generated"],
|
|
1378
|
+
["create", "Created"],
|
|
1379
|
+
["update", "Updated"],
|
|
1380
|
+
["delete", "Deleted"],
|
|
1381
|
+
["remove", "Deleted"],
|
|
1382
|
+
["send", "Sent"],
|
|
1383
|
+
["upload", "Uploaded"],
|
|
1384
|
+
["save", "Saved"],
|
|
1385
|
+
["submit", "Submitted"],
|
|
1386
|
+
["login", "Logged in"],
|
|
1387
|
+
["signin", "Logged in"],
|
|
1388
|
+
["logout", "Logged out"],
|
|
1389
|
+
["signout", "Logged out"],
|
|
1390
|
+
["register", "Registered"],
|
|
1391
|
+
["signup", "Registered"]
|
|
1392
|
+
];
|
|
1219
1393
|
function deriveActionVerb(method, endpointName) {
|
|
1220
1394
|
const lower = endpointName.toLowerCase();
|
|
1221
|
-
const
|
|
1222
|
-
|
|
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;
|
|
1395
|
+
for (const [keyword, verb] of VERB_MAP) {
|
|
1396
|
+
if (lower.includes(keyword)) return verb;
|
|
1237
1397
|
}
|
|
1238
1398
|
switch (method) {
|
|
1239
1399
|
case "POST":
|
|
@@ -1248,14 +1408,16 @@ function deriveActionVerb(method, endpointName) {
|
|
|
1248
1408
|
}
|
|
1249
1409
|
}
|
|
1250
1410
|
function getEndpointName(path) {
|
|
1251
|
-
const parts = path
|
|
1411
|
+
const parts = stripApiPrefix(path).split("/");
|
|
1252
1412
|
if (parts.length <= 2) return parts.join("/");
|
|
1253
1413
|
return parts.map((p) => p.length > ENDPOINT_TRUNCATE_LENGTH ? "..." : p).join("/");
|
|
1254
1414
|
}
|
|
1255
1415
|
function prettifyPageName(path) {
|
|
1256
|
-
|
|
1416
|
+
let clean = path;
|
|
1417
|
+
if (clean.startsWith("/")) clean = clean.slice(1);
|
|
1418
|
+
if (clean.endsWith("/")) clean = clean.slice(0, -1);
|
|
1257
1419
|
if (!clean) return "Home";
|
|
1258
|
-
return clean.split("/").map((s) => capitalize(s.
|
|
1420
|
+
return clean.split("/").map((s) => capitalize(s.split("-").join(" ").split("_").join(" "))).join(" ");
|
|
1259
1421
|
}
|
|
1260
1422
|
function capitalize(s) {
|
|
1261
1423
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -1488,7 +1650,15 @@ var TABLE_RE = /(?:FROM|INTO|UPDATE)\s+(?:"?\w+"?\.)?"?(\w+)"?/i;
|
|
|
1488
1650
|
function normalizeSQL(sql) {
|
|
1489
1651
|
if (!sql) return { op: "OTHER", table: "" };
|
|
1490
1652
|
const trimmed = sql.trim();
|
|
1491
|
-
|
|
1653
|
+
let spaceIdx = -1;
|
|
1654
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
1655
|
+
const c = trimmed[i];
|
|
1656
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
1657
|
+
spaceIdx = i;
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const keyword = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toUpperCase();
|
|
1492
1662
|
const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
|
|
1493
1663
|
const table = trimmed.match(TABLE_RE)?.[1] ?? "";
|
|
1494
1664
|
return { op, table };
|
|
@@ -2241,7 +2411,7 @@ var AnalysisEngine = class {
|
|
|
2241
2411
|
};
|
|
2242
2412
|
|
|
2243
2413
|
// src/index.ts
|
|
2244
|
-
var VERSION = "0.
|
|
2414
|
+
var VERSION = "0.10.0";
|
|
2245
2415
|
export {
|
|
2246
2416
|
AdapterRegistry,
|
|
2247
2417
|
AnalysisEngine,
|