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 +148 -5
- package/dist/api.js +232 -54
- package/dist/bin/brakit.js +228 -35
- package/dist/dashboard-client.global.js +742 -385
- package/dist/dashboard.html +933 -432
- package/dist/mcp/server.js +99 -21
- package/dist/runtime/index.js +1551 -430
- package/package.json +1 -1
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
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
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
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
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: "
|
|
398
|
-
{ name: "
|
|
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
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
"
|
|
1071
|
-
"fetches",
|
|
1072
|
-
"queries",
|
|
1073
|
-
"errors",
|
|
1074
|
-
"logs",
|
|
1185
|
+
"insights",
|
|
1075
1186
|
"performance",
|
|
1076
|
-
"
|
|
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
|
|
1085
|
-
var
|
|
1086
|
-
var
|
|
1087
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
2422
|
+
var VERSION = "0.10.1";
|
|
2245
2423
|
export {
|
|
2246
2424
|
AdapterRegistry,
|
|
2247
2425
|
AnalysisEngine,
|