@vibecheckai/cli 3.2.2 → 3.2.4
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/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
- package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analyzers.js +606 -325
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +213 -213
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/interactive-menu.js +1496 -1496
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-output.js +187 -187
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/scan-output.js +525 -190
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/status-output.js +253 -253
- package/bin/runners/lib/terminal-ui.js +351 -271
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +8 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runScan.js +17 -1
- package/bin/runners/runTruth.js +15 -3
- package/mcp-server/tier-auth.js +4 -4
- package/mcp-server/tools/index.js +72 -72
- package/package.json +1 -1
|
@@ -1,451 +1,451 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route Matcher v2
|
|
3
|
-
*
|
|
4
|
-
* Canonical normalization and route matching algorithm.
|
|
5
|
-
* Handles path normalization, specificity scoring, and method matching.
|
|
6
|
-
*
|
|
7
|
-
* Canonical path format:
|
|
8
|
-
* - Static: /api/billing/portal
|
|
9
|
-
* - Params: /api/users/{id}
|
|
10
|
-
* - Catch-all: /api/files/{*path}
|
|
11
|
-
* - Optional catch-all: /api/blog/{*slug?}
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
"use strict";
|
|
15
|
-
|
|
16
|
-
const { URL } = require("url");
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Normalize a URL or path for matching
|
|
20
|
-
*
|
|
21
|
-
* Steps:
|
|
22
|
-
* 1. Strip scheme + host if present
|
|
23
|
-
* 2. Remove query + fragment
|
|
24
|
-
* 3. Decode safe characters
|
|
25
|
-
* 4. Collapse multiple slashes
|
|
26
|
-
* 5. Apply basePath handling
|
|
27
|
-
* 6. Handle trailingSlash policy
|
|
28
|
-
*/
|
|
29
|
-
function normalizeUrl(urlOrPath, options = {}) {
|
|
30
|
-
const { basePath = "", trailingSlash = false } = options;
|
|
31
|
-
|
|
32
|
-
let normalized = urlOrPath;
|
|
33
|
-
|
|
34
|
-
// Strip scheme + host if it's a full URL
|
|
35
|
-
if (normalized.startsWith("http://") || normalized.startsWith("https://")) {
|
|
36
|
-
try {
|
|
37
|
-
const url = new URL(normalized);
|
|
38
|
-
normalized = url.pathname;
|
|
39
|
-
} catch {
|
|
40
|
-
// Invalid URL, treat as path
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Remove query string and fragment
|
|
45
|
-
normalized = normalized.split("?")[0].split("#")[0];
|
|
46
|
-
|
|
47
|
-
// Decode URI components
|
|
48
|
-
try {
|
|
49
|
-
normalized = decodeURIComponent(normalized);
|
|
50
|
-
} catch {
|
|
51
|
-
// Invalid encoding, keep as-is
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Collapse multiple slashes
|
|
55
|
-
normalized = normalized.replace(/\/+/g, "/");
|
|
56
|
-
|
|
57
|
-
// Ensure leading slash
|
|
58
|
-
if (!normalized.startsWith("/")) {
|
|
59
|
-
normalized = "/" + normalized;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Strip basePath if present
|
|
63
|
-
if (basePath && normalized.startsWith(basePath)) {
|
|
64
|
-
normalized = normalized.slice(basePath.length) || "/";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Handle trailing slash
|
|
68
|
-
if (!trailingSlash && normalized.length > 1) {
|
|
69
|
-
normalized = normalized.replace(/\/$/, "");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return normalized;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Convert any path format to canonical format
|
|
77
|
-
*
|
|
78
|
-
* Input formats:
|
|
79
|
-
* - Next.js: [id], [...slug], [[...slug]]
|
|
80
|
-
* - Fastify: :id, :path*, /*
|
|
81
|
-
* - Express: :id, :path(*)
|
|
82
|
-
*
|
|
83
|
-
* Output format:
|
|
84
|
-
* - Params: {id}
|
|
85
|
-
* - Catch-all: {*slug}
|
|
86
|
-
* - Optional catch-all: {*slug?}
|
|
87
|
-
*/
|
|
88
|
-
function toCanonicalPath(rawPath, framework = "auto") {
|
|
89
|
-
let canonical = rawPath;
|
|
90
|
-
|
|
91
|
-
// Next.js conversions
|
|
92
|
-
canonical = canonical
|
|
93
|
-
// Optional catch-all [[...slug]]
|
|
94
|
-
.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "{*$1?}")
|
|
95
|
-
// Catch-all [...slug]
|
|
96
|
-
.replace(/\[\.\.\.([^\]]+)\]/g, "{*$1}")
|
|
97
|
-
// Dynamic segment [id]
|
|
98
|
-
.replace(/\[([^\]]+)\]/g, "{$1}")
|
|
99
|
-
// Route groups (group) - remove
|
|
100
|
-
.replace(/\/\([^)]+\)/g, "")
|
|
101
|
-
// Parallel routes @slot - remove
|
|
102
|
-
.replace(/\/@[^/]+/g, "");
|
|
103
|
-
|
|
104
|
-
// Fastify/Express conversions
|
|
105
|
-
canonical = canonical
|
|
106
|
-
// Wildcard catch-all /*
|
|
107
|
-
.replace(/\/\*$/, "/{*path}")
|
|
108
|
-
// Named wildcard :path*
|
|
109
|
-
.replace(/:(\w+)\*/g, "{*$1}")
|
|
110
|
-
// Named param :id
|
|
111
|
-
.replace(/:(\w+)/g, "{$1}");
|
|
112
|
-
|
|
113
|
-
// Clean up
|
|
114
|
-
canonical = canonical
|
|
115
|
-
.replace(/\/+/g, "/")
|
|
116
|
-
.replace(/(.)\/$/, "$1");
|
|
117
|
-
|
|
118
|
-
// Ensure leading slash
|
|
119
|
-
if (!canonical.startsWith("/")) {
|
|
120
|
-
canonical = "/" + canonical;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return canonical;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Convert canonical path to regex for matching
|
|
128
|
-
*/
|
|
129
|
-
function canonicalToRegex(canonicalPath) {
|
|
130
|
-
let pattern = canonicalPath
|
|
131
|
-
// Escape regex special chars (except our placeholders)
|
|
132
|
-
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
133
|
-
// Optional catch-all {*slug?}
|
|
134
|
-
.replace(/\\\{\\\*(\w+)\?\\\}/g, "(?:/.*)?")
|
|
135
|
-
// Required catch-all {*slug}
|
|
136
|
-
.replace(/\\\{\\\*(\w+)\\\}/g, "/.+")
|
|
137
|
-
// Named param {id}
|
|
138
|
-
.replace(/\\\{(\w+)\\\}/g, "[^/]+");
|
|
139
|
-
|
|
140
|
-
return new RegExp(`^${pattern}$`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Calculate specificity score for a route
|
|
145
|
-
* Higher score = more specific = better match
|
|
146
|
-
*
|
|
147
|
-
* Scoring:
|
|
148
|
-
* - Static segment: +10
|
|
149
|
-
* - Named param: +5
|
|
150
|
-
* - Catch-all: +1
|
|
151
|
-
* - Optional catch-all: +0
|
|
152
|
-
*/
|
|
153
|
-
function calculateSpecificity(canonicalPath) {
|
|
154
|
-
const segments = canonicalPath.split("/").filter(Boolean);
|
|
155
|
-
let score = 0;
|
|
156
|
-
|
|
157
|
-
for (const segment of segments) {
|
|
158
|
-
if (segment.startsWith("{*") && segment.endsWith("?}")) {
|
|
159
|
-
// Optional catch-all
|
|
160
|
-
score += 0;
|
|
161
|
-
} else if (segment.startsWith("{*")) {
|
|
162
|
-
// Required catch-all
|
|
163
|
-
score += 1;
|
|
164
|
-
} else if (segment.startsWith("{")) {
|
|
165
|
-
// Named param
|
|
166
|
-
score += 5;
|
|
167
|
-
} else {
|
|
168
|
-
// Static segment
|
|
169
|
-
score += 10;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return score;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Match a client call path against server routes
|
|
178
|
-
*
|
|
179
|
-
* Algorithm:
|
|
180
|
-
* 1. Exact match (method + path)
|
|
181
|
-
* 2. Dynamic match with specificity scoring
|
|
182
|
-
* 3. Method fallback (UNKNOWN or wildcard)
|
|
183
|
-
*
|
|
184
|
-
* Returns: { matched: boolean, route: Route | null, confidence: string, matchType: string }
|
|
185
|
-
*/
|
|
186
|
-
function matchRoute(normalizedPath, method, serverRoutes, options = {}) {
|
|
187
|
-
const { allowMethodMismatch = false, considerRewrites = [] } = options;
|
|
188
|
-
const methodUpper = method?.toUpperCase() || "UNKNOWN";
|
|
189
|
-
|
|
190
|
-
// Step 1: Exact match
|
|
191
|
-
for (const route of serverRoutes) {
|
|
192
|
-
if (route.canonicalPath === normalizedPath || route.path === normalizedPath) {
|
|
193
|
-
if (route.methods.includes(methodUpper) || route.methods.includes("UNKNOWN")) {
|
|
194
|
-
return {
|
|
195
|
-
matched: true,
|
|
196
|
-
route,
|
|
197
|
-
confidence: "high",
|
|
198
|
-
matchType: "exact",
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
// Path matches but method doesn't
|
|
202
|
-
if (allowMethodMismatch) {
|
|
203
|
-
return {
|
|
204
|
-
matched: true,
|
|
205
|
-
route,
|
|
206
|
-
confidence: "medium",
|
|
207
|
-
matchType: "path_only",
|
|
208
|
-
methodMismatch: true,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Step 2: Dynamic match with specificity
|
|
215
|
-
const dynamicMatches = [];
|
|
216
|
-
|
|
217
|
-
for (const route of serverRoutes) {
|
|
218
|
-
const regex = canonicalToRegex(route.canonicalPath);
|
|
219
|
-
if (regex.test(normalizedPath)) {
|
|
220
|
-
const methodMatches = route.methods.includes(methodUpper) ||
|
|
221
|
-
route.methods.includes("UNKNOWN") ||
|
|
222
|
-
route.methods.includes("*");
|
|
223
|
-
|
|
224
|
-
dynamicMatches.push({
|
|
225
|
-
route,
|
|
226
|
-
specificity: calculateSpecificity(route.canonicalPath),
|
|
227
|
-
methodMatches,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (dynamicMatches.length > 0) {
|
|
233
|
-
// Sort by specificity (descending), then method match
|
|
234
|
-
dynamicMatches.sort((a, b) => {
|
|
235
|
-
if (a.methodMatches !== b.methodMatches) {
|
|
236
|
-
return a.methodMatches ? -1 : 1;
|
|
237
|
-
}
|
|
238
|
-
return b.specificity - a.specificity;
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const best = dynamicMatches[0];
|
|
242
|
-
return {
|
|
243
|
-
matched: true,
|
|
244
|
-
route: best.route,
|
|
245
|
-
confidence: best.methodMatches ? "high" : "medium",
|
|
246
|
-
matchType: "dynamic",
|
|
247
|
-
specificity: best.specificity,
|
|
248
|
-
methodMismatch: !best.methodMatches,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Step 3: Check rewrites (Next.js)
|
|
253
|
-
for (const rewrite of considerRewrites) {
|
|
254
|
-
if (matchesGlob(normalizedPath, rewrite.source)) {
|
|
255
|
-
return {
|
|
256
|
-
matched: true,
|
|
257
|
-
route: null,
|
|
258
|
-
confidence: "low",
|
|
259
|
-
matchType: "rewrite",
|
|
260
|
-
rewrite,
|
|
261
|
-
isExternal: rewrite.isExternal,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// No match
|
|
267
|
-
return {
|
|
268
|
-
matched: false,
|
|
269
|
-
route: null,
|
|
270
|
-
confidence: "high", // High confidence it doesn't exist
|
|
271
|
-
matchType: "none",
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Match a glob pattern (simple implementation)
|
|
277
|
-
*/
|
|
278
|
-
function matchesGlob(path, pattern) {
|
|
279
|
-
const regex = new RegExp(
|
|
280
|
-
"^" + pattern
|
|
281
|
-
.replace(/\*/g, ".*")
|
|
282
|
-
.replace(/\//g, "\\/")
|
|
283
|
-
+ "$"
|
|
284
|
-
);
|
|
285
|
-
return regex.test(path);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Find all routes that match a path (for debugging/reporting)
|
|
290
|
-
*/
|
|
291
|
-
function findAllMatches(normalizedPath, method, serverRoutes) {
|
|
292
|
-
const matches = [];
|
|
293
|
-
const methodUpper = method?.toUpperCase() || "UNKNOWN";
|
|
294
|
-
|
|
295
|
-
for (const route of serverRoutes) {
|
|
296
|
-
// Check exact match
|
|
297
|
-
if (route.canonicalPath === normalizedPath || route.path === normalizedPath) {
|
|
298
|
-
matches.push({
|
|
299
|
-
route,
|
|
300
|
-
matchType: "exact",
|
|
301
|
-
methodMatches: route.methods.includes(methodUpper),
|
|
302
|
-
specificity: calculateSpecificity(route.canonicalPath),
|
|
303
|
-
});
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Check dynamic match
|
|
308
|
-
const regex = canonicalToRegex(route.canonicalPath);
|
|
309
|
-
if (regex.test(normalizedPath)) {
|
|
310
|
-
matches.push({
|
|
311
|
-
route,
|
|
312
|
-
matchType: "dynamic",
|
|
313
|
-
methodMatches: route.methods.includes(methodUpper) || route.methods.includes("UNKNOWN"),
|
|
314
|
-
specificity: calculateSpecificity(route.canonicalPath),
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Sort by specificity
|
|
320
|
-
matches.sort((a, b) => b.specificity - a.specificity);
|
|
321
|
-
|
|
322
|
-
return matches;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Confidence matrix for missing route severity
|
|
327
|
-
*
|
|
328
|
-
* Returns: "BLOCK" | "WARN" | "INFO"
|
|
329
|
-
*/
|
|
330
|
-
function getMissingRouteSeverity(options = {}) {
|
|
331
|
-
const {
|
|
332
|
-
framework = "unknown",
|
|
333
|
-
staticConfidence = "medium",
|
|
334
|
-
hasRuntimeProof = false,
|
|
335
|
-
runtimeResult = null, // "404" | "405" | "2xx" | null
|
|
336
|
-
isRewriteTarget = false,
|
|
337
|
-
isCriticalPath = false,
|
|
338
|
-
} = options;
|
|
339
|
-
|
|
340
|
-
// Runtime beats static
|
|
341
|
-
if (hasRuntimeProof) {
|
|
342
|
-
if (runtimeResult === "404" || runtimeResult === "405") {
|
|
343
|
-
return "BLOCK"; // Confirmed missing
|
|
344
|
-
}
|
|
345
|
-
if (runtimeResult === "2xx") {
|
|
346
|
-
return "INFO"; // Actually exists (dynamic registration)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Rewrite targets get WARN unless runtime confirms
|
|
351
|
-
if (isRewriteTarget) {
|
|
352
|
-
return "WARN";
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Next.js filesystem routes are high confidence
|
|
356
|
-
if (framework === "next" && staticConfidence === "high") {
|
|
357
|
-
return isCriticalPath ? "BLOCK" : "WARN";
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Fastify with low confidence (dynamic routes possible)
|
|
361
|
-
if (framework === "fastify" && staticConfidence === "low") {
|
|
362
|
-
return "WARN";
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Default: BLOCK for high confidence, WARN for medium/low
|
|
366
|
-
if (staticConfidence === "high") {
|
|
367
|
-
return "BLOCK";
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return "WARN";
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Check if a path is a critical path (checkout, auth, payment)
|
|
375
|
-
*/
|
|
376
|
-
function isCriticalPath(path) {
|
|
377
|
-
const criticalPatterns = [
|
|
378
|
-
/\/checkout/i,
|
|
379
|
-
/\/pay/i,
|
|
380
|
-
/\/login/i,
|
|
381
|
-
/\/auth/i,
|
|
382
|
-
/\/register/i,
|
|
383
|
-
/\/signup/i,
|
|
384
|
-
/\/billing/i,
|
|
385
|
-
/\/subscription/i,
|
|
386
|
-
/\/save/i,
|
|
387
|
-
/\/submit/i,
|
|
388
|
-
/\/order/i,
|
|
389
|
-
/\/purchase/i,
|
|
390
|
-
];
|
|
391
|
-
|
|
392
|
-
return criticalPatterns.some(p => p.test(path));
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Build a route index for fast lookups
|
|
397
|
-
*/
|
|
398
|
-
class RouteIndex {
|
|
399
|
-
constructor(routes = []) {
|
|
400
|
-
this.routes = routes;
|
|
401
|
-
this.exactIndex = new Map(); // path -> routes
|
|
402
|
-
this.methodIndex = new Map(); // method -> routes
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
add(route) {
|
|
406
|
-
this.routes.push(route);
|
|
407
|
-
|
|
408
|
-
// Index by canonical path
|
|
409
|
-
if (!this.exactIndex.has(route.canonicalPath)) {
|
|
410
|
-
this.exactIndex.set(route.canonicalPath, []);
|
|
411
|
-
}
|
|
412
|
-
this.exactIndex.get(route.canonicalPath).push(route);
|
|
413
|
-
|
|
414
|
-
// Index by method
|
|
415
|
-
for (const method of route.methods) {
|
|
416
|
-
if (!this.methodIndex.has(method)) {
|
|
417
|
-
this.methodIndex.set(method, []);
|
|
418
|
-
}
|
|
419
|
-
this.methodIndex.get(method).push(route);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
findExact(path) {
|
|
424
|
-
return this.exactIndex.get(path) || [];
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
findByMethod(method) {
|
|
428
|
-
return this.methodIndex.get(method.toUpperCase()) || [];
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
match(normalizedPath, method, options = {}) {
|
|
432
|
-
return matchRoute(normalizedPath, method, this.routes, options);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
findAll(normalizedPath, method) {
|
|
436
|
-
return findAllMatches(normalizedPath, method, this.routes);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
module.exports = {
|
|
441
|
-
normalizeUrl,
|
|
442
|
-
toCanonicalPath,
|
|
443
|
-
canonicalToRegex,
|
|
444
|
-
calculateSpecificity,
|
|
445
|
-
matchRoute,
|
|
446
|
-
matchesGlob,
|
|
447
|
-
findAllMatches,
|
|
448
|
-
getMissingRouteSeverity,
|
|
449
|
-
isCriticalPath,
|
|
450
|
-
RouteIndex,
|
|
451
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Route Matcher v2
|
|
3
|
+
*
|
|
4
|
+
* Canonical normalization and route matching algorithm.
|
|
5
|
+
* Handles path normalization, specificity scoring, and method matching.
|
|
6
|
+
*
|
|
7
|
+
* Canonical path format:
|
|
8
|
+
* - Static: /api/billing/portal
|
|
9
|
+
* - Params: /api/users/{id}
|
|
10
|
+
* - Catch-all: /api/files/{*path}
|
|
11
|
+
* - Optional catch-all: /api/blog/{*slug?}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const { URL } = require("url");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a URL or path for matching
|
|
20
|
+
*
|
|
21
|
+
* Steps:
|
|
22
|
+
* 1. Strip scheme + host if present
|
|
23
|
+
* 2. Remove query + fragment
|
|
24
|
+
* 3. Decode safe characters
|
|
25
|
+
* 4. Collapse multiple slashes
|
|
26
|
+
* 5. Apply basePath handling
|
|
27
|
+
* 6. Handle trailingSlash policy
|
|
28
|
+
*/
|
|
29
|
+
function normalizeUrl(urlOrPath, options = {}) {
|
|
30
|
+
const { basePath = "", trailingSlash = false } = options;
|
|
31
|
+
|
|
32
|
+
let normalized = urlOrPath;
|
|
33
|
+
|
|
34
|
+
// Strip scheme + host if it's a full URL
|
|
35
|
+
if (normalized.startsWith("http://") || normalized.startsWith("https://")) {
|
|
36
|
+
try {
|
|
37
|
+
const url = new URL(normalized);
|
|
38
|
+
normalized = url.pathname;
|
|
39
|
+
} catch {
|
|
40
|
+
// Invalid URL, treat as path
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Remove query string and fragment
|
|
45
|
+
normalized = normalized.split("?")[0].split("#")[0];
|
|
46
|
+
|
|
47
|
+
// Decode URI components
|
|
48
|
+
try {
|
|
49
|
+
normalized = decodeURIComponent(normalized);
|
|
50
|
+
} catch {
|
|
51
|
+
// Invalid encoding, keep as-is
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Collapse multiple slashes
|
|
55
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
56
|
+
|
|
57
|
+
// Ensure leading slash
|
|
58
|
+
if (!normalized.startsWith("/")) {
|
|
59
|
+
normalized = "/" + normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strip basePath if present
|
|
63
|
+
if (basePath && normalized.startsWith(basePath)) {
|
|
64
|
+
normalized = normalized.slice(basePath.length) || "/";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle trailing slash
|
|
68
|
+
if (!trailingSlash && normalized.length > 1) {
|
|
69
|
+
normalized = normalized.replace(/\/$/, "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert any path format to canonical format
|
|
77
|
+
*
|
|
78
|
+
* Input formats:
|
|
79
|
+
* - Next.js: [id], [...slug], [[...slug]]
|
|
80
|
+
* - Fastify: :id, :path*, /*
|
|
81
|
+
* - Express: :id, :path(*)
|
|
82
|
+
*
|
|
83
|
+
* Output format:
|
|
84
|
+
* - Params: {id}
|
|
85
|
+
* - Catch-all: {*slug}
|
|
86
|
+
* - Optional catch-all: {*slug?}
|
|
87
|
+
*/
|
|
88
|
+
function toCanonicalPath(rawPath, framework = "auto") {
|
|
89
|
+
let canonical = rawPath;
|
|
90
|
+
|
|
91
|
+
// Next.js conversions
|
|
92
|
+
canonical = canonical
|
|
93
|
+
// Optional catch-all [[...slug]]
|
|
94
|
+
.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "{*$1?}")
|
|
95
|
+
// Catch-all [...slug]
|
|
96
|
+
.replace(/\[\.\.\.([^\]]+)\]/g, "{*$1}")
|
|
97
|
+
// Dynamic segment [id]
|
|
98
|
+
.replace(/\[([^\]]+)\]/g, "{$1}")
|
|
99
|
+
// Route groups (group) - remove
|
|
100
|
+
.replace(/\/\([^)]+\)/g, "")
|
|
101
|
+
// Parallel routes @slot - remove
|
|
102
|
+
.replace(/\/@[^/]+/g, "");
|
|
103
|
+
|
|
104
|
+
// Fastify/Express conversions
|
|
105
|
+
canonical = canonical
|
|
106
|
+
// Wildcard catch-all /*
|
|
107
|
+
.replace(/\/\*$/, "/{*path}")
|
|
108
|
+
// Named wildcard :path*
|
|
109
|
+
.replace(/:(\w+)\*/g, "{*$1}")
|
|
110
|
+
// Named param :id
|
|
111
|
+
.replace(/:(\w+)/g, "{$1}");
|
|
112
|
+
|
|
113
|
+
// Clean up
|
|
114
|
+
canonical = canonical
|
|
115
|
+
.replace(/\/+/g, "/")
|
|
116
|
+
.replace(/(.)\/$/, "$1");
|
|
117
|
+
|
|
118
|
+
// Ensure leading slash
|
|
119
|
+
if (!canonical.startsWith("/")) {
|
|
120
|
+
canonical = "/" + canonical;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return canonical;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Convert canonical path to regex for matching
|
|
128
|
+
*/
|
|
129
|
+
function canonicalToRegex(canonicalPath) {
|
|
130
|
+
let pattern = canonicalPath
|
|
131
|
+
// Escape regex special chars (except our placeholders)
|
|
132
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
133
|
+
// Optional catch-all {*slug?}
|
|
134
|
+
.replace(/\\\{\\\*(\w+)\?\\\}/g, "(?:/.*)?")
|
|
135
|
+
// Required catch-all {*slug}
|
|
136
|
+
.replace(/\\\{\\\*(\w+)\\\}/g, "/.+")
|
|
137
|
+
// Named param {id}
|
|
138
|
+
.replace(/\\\{(\w+)\\\}/g, "[^/]+");
|
|
139
|
+
|
|
140
|
+
return new RegExp(`^${pattern}$`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Calculate specificity score for a route
|
|
145
|
+
* Higher score = more specific = better match
|
|
146
|
+
*
|
|
147
|
+
* Scoring:
|
|
148
|
+
* - Static segment: +10
|
|
149
|
+
* - Named param: +5
|
|
150
|
+
* - Catch-all: +1
|
|
151
|
+
* - Optional catch-all: +0
|
|
152
|
+
*/
|
|
153
|
+
function calculateSpecificity(canonicalPath) {
|
|
154
|
+
const segments = canonicalPath.split("/").filter(Boolean);
|
|
155
|
+
let score = 0;
|
|
156
|
+
|
|
157
|
+
for (const segment of segments) {
|
|
158
|
+
if (segment.startsWith("{*") && segment.endsWith("?}")) {
|
|
159
|
+
// Optional catch-all
|
|
160
|
+
score += 0;
|
|
161
|
+
} else if (segment.startsWith("{*")) {
|
|
162
|
+
// Required catch-all
|
|
163
|
+
score += 1;
|
|
164
|
+
} else if (segment.startsWith("{")) {
|
|
165
|
+
// Named param
|
|
166
|
+
score += 5;
|
|
167
|
+
} else {
|
|
168
|
+
// Static segment
|
|
169
|
+
score += 10;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return score;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Match a client call path against server routes
|
|
178
|
+
*
|
|
179
|
+
* Algorithm:
|
|
180
|
+
* 1. Exact match (method + path)
|
|
181
|
+
* 2. Dynamic match with specificity scoring
|
|
182
|
+
* 3. Method fallback (UNKNOWN or wildcard)
|
|
183
|
+
*
|
|
184
|
+
* Returns: { matched: boolean, route: Route | null, confidence: string, matchType: string }
|
|
185
|
+
*/
|
|
186
|
+
function matchRoute(normalizedPath, method, serverRoutes, options = {}) {
|
|
187
|
+
const { allowMethodMismatch = false, considerRewrites = [] } = options;
|
|
188
|
+
const methodUpper = method?.toUpperCase() || "UNKNOWN";
|
|
189
|
+
|
|
190
|
+
// Step 1: Exact match
|
|
191
|
+
for (const route of serverRoutes) {
|
|
192
|
+
if (route.canonicalPath === normalizedPath || route.path === normalizedPath) {
|
|
193
|
+
if (route.methods.includes(methodUpper) || route.methods.includes("UNKNOWN")) {
|
|
194
|
+
return {
|
|
195
|
+
matched: true,
|
|
196
|
+
route,
|
|
197
|
+
confidence: "high",
|
|
198
|
+
matchType: "exact",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Path matches but method doesn't
|
|
202
|
+
if (allowMethodMismatch) {
|
|
203
|
+
return {
|
|
204
|
+
matched: true,
|
|
205
|
+
route,
|
|
206
|
+
confidence: "medium",
|
|
207
|
+
matchType: "path_only",
|
|
208
|
+
methodMismatch: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Step 2: Dynamic match with specificity
|
|
215
|
+
const dynamicMatches = [];
|
|
216
|
+
|
|
217
|
+
for (const route of serverRoutes) {
|
|
218
|
+
const regex = canonicalToRegex(route.canonicalPath);
|
|
219
|
+
if (regex.test(normalizedPath)) {
|
|
220
|
+
const methodMatches = route.methods.includes(methodUpper) ||
|
|
221
|
+
route.methods.includes("UNKNOWN") ||
|
|
222
|
+
route.methods.includes("*");
|
|
223
|
+
|
|
224
|
+
dynamicMatches.push({
|
|
225
|
+
route,
|
|
226
|
+
specificity: calculateSpecificity(route.canonicalPath),
|
|
227
|
+
methodMatches,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (dynamicMatches.length > 0) {
|
|
233
|
+
// Sort by specificity (descending), then method match
|
|
234
|
+
dynamicMatches.sort((a, b) => {
|
|
235
|
+
if (a.methodMatches !== b.methodMatches) {
|
|
236
|
+
return a.methodMatches ? -1 : 1;
|
|
237
|
+
}
|
|
238
|
+
return b.specificity - a.specificity;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const best = dynamicMatches[0];
|
|
242
|
+
return {
|
|
243
|
+
matched: true,
|
|
244
|
+
route: best.route,
|
|
245
|
+
confidence: best.methodMatches ? "high" : "medium",
|
|
246
|
+
matchType: "dynamic",
|
|
247
|
+
specificity: best.specificity,
|
|
248
|
+
methodMismatch: !best.methodMatches,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Step 3: Check rewrites (Next.js)
|
|
253
|
+
for (const rewrite of considerRewrites) {
|
|
254
|
+
if (matchesGlob(normalizedPath, rewrite.source)) {
|
|
255
|
+
return {
|
|
256
|
+
matched: true,
|
|
257
|
+
route: null,
|
|
258
|
+
confidence: "low",
|
|
259
|
+
matchType: "rewrite",
|
|
260
|
+
rewrite,
|
|
261
|
+
isExternal: rewrite.isExternal,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// No match
|
|
267
|
+
return {
|
|
268
|
+
matched: false,
|
|
269
|
+
route: null,
|
|
270
|
+
confidence: "high", // High confidence it doesn't exist
|
|
271
|
+
matchType: "none",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Match a glob pattern (simple implementation)
|
|
277
|
+
*/
|
|
278
|
+
function matchesGlob(path, pattern) {
|
|
279
|
+
const regex = new RegExp(
|
|
280
|
+
"^" + pattern
|
|
281
|
+
.replace(/\*/g, ".*")
|
|
282
|
+
.replace(/\//g, "\\/")
|
|
283
|
+
+ "$"
|
|
284
|
+
);
|
|
285
|
+
return regex.test(path);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Find all routes that match a path (for debugging/reporting)
|
|
290
|
+
*/
|
|
291
|
+
function findAllMatches(normalizedPath, method, serverRoutes) {
|
|
292
|
+
const matches = [];
|
|
293
|
+
const methodUpper = method?.toUpperCase() || "UNKNOWN";
|
|
294
|
+
|
|
295
|
+
for (const route of serverRoutes) {
|
|
296
|
+
// Check exact match
|
|
297
|
+
if (route.canonicalPath === normalizedPath || route.path === normalizedPath) {
|
|
298
|
+
matches.push({
|
|
299
|
+
route,
|
|
300
|
+
matchType: "exact",
|
|
301
|
+
methodMatches: route.methods.includes(methodUpper),
|
|
302
|
+
specificity: calculateSpecificity(route.canonicalPath),
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check dynamic match
|
|
308
|
+
const regex = canonicalToRegex(route.canonicalPath);
|
|
309
|
+
if (regex.test(normalizedPath)) {
|
|
310
|
+
matches.push({
|
|
311
|
+
route,
|
|
312
|
+
matchType: "dynamic",
|
|
313
|
+
methodMatches: route.methods.includes(methodUpper) || route.methods.includes("UNKNOWN"),
|
|
314
|
+
specificity: calculateSpecificity(route.canonicalPath),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Sort by specificity
|
|
320
|
+
matches.sort((a, b) => b.specificity - a.specificity);
|
|
321
|
+
|
|
322
|
+
return matches;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Confidence matrix for missing route severity
|
|
327
|
+
*
|
|
328
|
+
* Returns: "BLOCK" | "WARN" | "INFO"
|
|
329
|
+
*/
|
|
330
|
+
function getMissingRouteSeverity(options = {}) {
|
|
331
|
+
const {
|
|
332
|
+
framework = "unknown",
|
|
333
|
+
staticConfidence = "medium",
|
|
334
|
+
hasRuntimeProof = false,
|
|
335
|
+
runtimeResult = null, // "404" | "405" | "2xx" | null
|
|
336
|
+
isRewriteTarget = false,
|
|
337
|
+
isCriticalPath = false,
|
|
338
|
+
} = options;
|
|
339
|
+
|
|
340
|
+
// Runtime beats static
|
|
341
|
+
if (hasRuntimeProof) {
|
|
342
|
+
if (runtimeResult === "404" || runtimeResult === "405") {
|
|
343
|
+
return "BLOCK"; // Confirmed missing
|
|
344
|
+
}
|
|
345
|
+
if (runtimeResult === "2xx") {
|
|
346
|
+
return "INFO"; // Actually exists (dynamic registration)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Rewrite targets get WARN unless runtime confirms
|
|
351
|
+
if (isRewriteTarget) {
|
|
352
|
+
return "WARN";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Next.js filesystem routes are high confidence
|
|
356
|
+
if (framework === "next" && staticConfidence === "high") {
|
|
357
|
+
return isCriticalPath ? "BLOCK" : "WARN";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Fastify with low confidence (dynamic routes possible)
|
|
361
|
+
if (framework === "fastify" && staticConfidence === "low") {
|
|
362
|
+
return "WARN";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Default: BLOCK for high confidence, WARN for medium/low
|
|
366
|
+
if (staticConfidence === "high") {
|
|
367
|
+
return "BLOCK";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return "WARN";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Check if a path is a critical path (checkout, auth, payment)
|
|
375
|
+
*/
|
|
376
|
+
function isCriticalPath(path) {
|
|
377
|
+
const criticalPatterns = [
|
|
378
|
+
/\/checkout/i,
|
|
379
|
+
/\/pay/i,
|
|
380
|
+
/\/login/i,
|
|
381
|
+
/\/auth/i,
|
|
382
|
+
/\/register/i,
|
|
383
|
+
/\/signup/i,
|
|
384
|
+
/\/billing/i,
|
|
385
|
+
/\/subscription/i,
|
|
386
|
+
/\/save/i,
|
|
387
|
+
/\/submit/i,
|
|
388
|
+
/\/order/i,
|
|
389
|
+
/\/purchase/i,
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
return criticalPatterns.some(p => p.test(path));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Build a route index for fast lookups
|
|
397
|
+
*/
|
|
398
|
+
class RouteIndex {
|
|
399
|
+
constructor(routes = []) {
|
|
400
|
+
this.routes = routes;
|
|
401
|
+
this.exactIndex = new Map(); // path -> routes
|
|
402
|
+
this.methodIndex = new Map(); // method -> routes
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
add(route) {
|
|
406
|
+
this.routes.push(route);
|
|
407
|
+
|
|
408
|
+
// Index by canonical path
|
|
409
|
+
if (!this.exactIndex.has(route.canonicalPath)) {
|
|
410
|
+
this.exactIndex.set(route.canonicalPath, []);
|
|
411
|
+
}
|
|
412
|
+
this.exactIndex.get(route.canonicalPath).push(route);
|
|
413
|
+
|
|
414
|
+
// Index by method
|
|
415
|
+
for (const method of route.methods) {
|
|
416
|
+
if (!this.methodIndex.has(method)) {
|
|
417
|
+
this.methodIndex.set(method, []);
|
|
418
|
+
}
|
|
419
|
+
this.methodIndex.get(method).push(route);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
findExact(path) {
|
|
424
|
+
return this.exactIndex.get(path) || [];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
findByMethod(method) {
|
|
428
|
+
return this.methodIndex.get(method.toUpperCase()) || [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
match(normalizedPath, method, options = {}) {
|
|
432
|
+
return matchRoute(normalizedPath, method, this.routes, options);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
findAll(normalizedPath, method) {
|
|
436
|
+
return findAllMatches(normalizedPath, method, this.routes);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = {
|
|
441
|
+
normalizeUrl,
|
|
442
|
+
toCanonicalPath,
|
|
443
|
+
canonicalToRegex,
|
|
444
|
+
calculateSpecificity,
|
|
445
|
+
matchRoute,
|
|
446
|
+
matchesGlob,
|
|
447
|
+
findAllMatches,
|
|
448
|
+
getMissingRouteSeverity,
|
|
449
|
+
isCriticalPath,
|
|
450
|
+
RouteIndex,
|
|
451
|
+
};
|