@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,426 +1,426 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fastify Route Extractor v2
|
|
3
|
-
*
|
|
4
|
-
* Spec-compliant extraction with prefix stacking and autoload support.
|
|
5
|
-
* Two-tier approach: static AST extraction + optional runtime introspection.
|
|
6
|
-
*
|
|
7
|
-
* Patterns supported:
|
|
8
|
-
* - fastify.route({ method, url, handler })
|
|
9
|
-
* - fastify.get/post/put/patch/delete(path, handler)
|
|
10
|
-
* - fastify.register(plugin, { prefix })
|
|
11
|
-
* - AutoLoad with dir resolution
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
"use strict";
|
|
15
|
-
|
|
16
|
-
const fs = require("fs");
|
|
17
|
-
const path = require("path");
|
|
18
|
-
const fg = require("fast-glob");
|
|
19
|
-
const crypto = require("crypto");
|
|
20
|
-
|
|
21
|
-
const FASTIFY_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "ALL"];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Detect Fastify service and entrypoint
|
|
25
|
-
*/
|
|
26
|
-
function detectFastify(projectRoot) {
|
|
27
|
-
const result = {
|
|
28
|
-
present: false,
|
|
29
|
-
entryFile: null,
|
|
30
|
-
prefixes: [],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Check package.json for fastify dependency
|
|
34
|
-
const pkgPath = path.join(projectRoot, "package.json");
|
|
35
|
-
if (fs.existsSync(pkgPath)) {
|
|
36
|
-
try {
|
|
37
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
38
|
-
if (pkg.dependencies?.fastify || pkg.devDependencies?.fastify) {
|
|
39
|
-
result.present = true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Try to find entry from scripts
|
|
43
|
-
const scripts = pkg.scripts || {};
|
|
44
|
-
for (const [name, cmd] of Object.entries(scripts)) {
|
|
45
|
-
if (name === "start" || name === "dev" || name === "server") {
|
|
46
|
-
const entryMatch = cmd.match(/(?:node|tsx|ts-node|npx\s+tsx?)\s+([^\s]+)/);
|
|
47
|
-
if (entryMatch) {
|
|
48
|
-
const entryPath = path.join(projectRoot, entryMatch[1]);
|
|
49
|
-
if (fs.existsSync(entryPath)) {
|
|
50
|
-
result.entryFile = entryMatch[1];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
} catch {}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Fallback: search common entry files
|
|
59
|
-
if (!result.entryFile) {
|
|
60
|
-
const commonEntries = [
|
|
61
|
-
"server.ts", "server.js",
|
|
62
|
-
"src/server.ts", "src/server.js",
|
|
63
|
-
"app.ts", "app.js",
|
|
64
|
-
"src/app.ts", "src/app.js",
|
|
65
|
-
"index.ts", "index.js",
|
|
66
|
-
"src/index.ts", "src/index.js",
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
for (const entry of commonEntries) {
|
|
70
|
-
const entryPath = path.join(projectRoot, entry);
|
|
71
|
-
if (fs.existsSync(entryPath)) {
|
|
72
|
-
const content = fs.readFileSync(entryPath, "utf8");
|
|
73
|
-
if (/from\s+['"]fastify['"]|require\s*\(\s*['"]fastify['"]\s*\)/.test(content)) {
|
|
74
|
-
result.entryFile = entry;
|
|
75
|
-
result.present = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return result;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Extract routes from Fastify project using static AST analysis
|
|
87
|
-
*/
|
|
88
|
-
function extractFastifyRoutes(projectRoot, entryFile = null) {
|
|
89
|
-
const detection = detectFastify(projectRoot);
|
|
90
|
-
|
|
91
|
-
if (!detection.present) {
|
|
92
|
-
return {
|
|
93
|
-
present: false,
|
|
94
|
-
entryFile: null,
|
|
95
|
-
routes: [],
|
|
96
|
-
prefixes: [],
|
|
97
|
-
autoloadDirs: [],
|
|
98
|
-
gaps: [],
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const entry = entryFile || detection.entryFile;
|
|
103
|
-
const routes = [];
|
|
104
|
-
const prefixes = new Set();
|
|
105
|
-
const autoloadDirs = [];
|
|
106
|
-
const gaps = [];
|
|
107
|
-
const visited = new Set();
|
|
108
|
-
|
|
109
|
-
// Start extraction from entry file
|
|
110
|
-
if (entry) {
|
|
111
|
-
const entryPath = path.join(projectRoot, entry);
|
|
112
|
-
if (fs.existsSync(entryPath)) {
|
|
113
|
-
extractFromFile(entryPath, projectRoot, "", routes, prefixes, autoloadDirs, gaps, visited);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Also scan common route directories
|
|
118
|
-
const routeDirs = ["routes", "src/routes", "api", "src/api"];
|
|
119
|
-
for (const dir of routeDirs) {
|
|
120
|
-
const dirPath = path.join(projectRoot, dir);
|
|
121
|
-
if (fs.existsSync(dirPath)) {
|
|
122
|
-
scanRouteDirectory(dirPath, projectRoot, "", routes, prefixes, gaps, visited);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
present: true,
|
|
128
|
-
entryFile: entry,
|
|
129
|
-
routes,
|
|
130
|
-
prefixes: [...prefixes],
|
|
131
|
-
autoloadDirs,
|
|
132
|
-
gaps,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Extract routes from a single file
|
|
138
|
-
*/
|
|
139
|
-
function extractFromFile(filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited) {
|
|
140
|
-
if (visited.has(filePath)) return;
|
|
141
|
-
visited.add(filePath);
|
|
142
|
-
|
|
143
|
-
if (!fs.existsSync(filePath)) return;
|
|
144
|
-
|
|
145
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
146
|
-
const relPath = path.relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
147
|
-
|
|
148
|
-
// Pattern A: fastify.route({ method, url, ... })
|
|
149
|
-
extractRouteMethod(content, relPath, currentPrefix, routes, projectRoot);
|
|
150
|
-
|
|
151
|
-
// Pattern B: fastify.get/post/put/... shorthand
|
|
152
|
-
extractShorthandMethods(content, relPath, currentPrefix, routes, projectRoot);
|
|
153
|
-
|
|
154
|
-
// Pattern C: fastify.register with prefix
|
|
155
|
-
extractRegisterCalls(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited);
|
|
156
|
-
|
|
157
|
-
// Pattern D: AutoLoad
|
|
158
|
-
extractAutoLoad(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Extract fastify.route({ method, url, ... }) patterns
|
|
163
|
-
*/
|
|
164
|
-
function extractRouteMethod(content, relPath, prefix, routes, projectRoot) {
|
|
165
|
-
// Match fastify.route({ method: "POST", url: "/path", ... })
|
|
166
|
-
const routePattern = /(?:fastify|app|server)\s*\.\s*route\s*\(\s*\{([^}]+)\}/g;
|
|
167
|
-
|
|
168
|
-
let match;
|
|
169
|
-
while ((match = routePattern.exec(content)) !== null) {
|
|
170
|
-
const routeBody = match[1];
|
|
171
|
-
|
|
172
|
-
// Extract method
|
|
173
|
-
const methodMatch = routeBody.match(/method\s*:\s*(?:['"`](\w+)['"`]|\[([^\]]+)\])/);
|
|
174
|
-
let methods = ["UNKNOWN"];
|
|
175
|
-
if (methodMatch) {
|
|
176
|
-
if (methodMatch[1]) {
|
|
177
|
-
methods = [methodMatch[1].toUpperCase()];
|
|
178
|
-
} else if (methodMatch[2]) {
|
|
179
|
-
methods = methodMatch[2].match(/['"`](\w+)['"`]/g)?.map(m => m.replace(/['"`]/g, "").toUpperCase()) || [];
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Extract url/path
|
|
184
|
-
const urlMatch = routeBody.match(/(?:url|path)\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
185
|
-
if (urlMatch) {
|
|
186
|
-
const rawPath = urlMatch[1];
|
|
187
|
-
const fullPath = combinePrefixes(prefix, rawPath);
|
|
188
|
-
const canonicalPath = convertFastifyPathToCanonical(fullPath);
|
|
189
|
-
|
|
190
|
-
const lineNum = content.substring(0, match.index).split("\n").length;
|
|
191
|
-
|
|
192
|
-
routes.push({
|
|
193
|
-
id: `R_FASTIFY_${hashPath(canonicalPath)}`,
|
|
194
|
-
kind: "fastify",
|
|
195
|
-
methods,
|
|
196
|
-
rawPath: fullPath,
|
|
197
|
-
path: canonicalPath,
|
|
198
|
-
canonicalPath,
|
|
199
|
-
authRequired: "unknown",
|
|
200
|
-
confidence: "medium",
|
|
201
|
-
handler: {
|
|
202
|
-
file: relPath,
|
|
203
|
-
export: "route",
|
|
204
|
-
},
|
|
205
|
-
evidence: [{
|
|
206
|
-
id: `E_${crypto.randomBytes(4).toString("hex").toUpperCase()}`,
|
|
207
|
-
kind: "file",
|
|
208
|
-
reason: "fastify.route() call",
|
|
209
|
-
file: relPath,
|
|
210
|
-
lines: `${lineNum}-${lineNum + 5}`,
|
|
211
|
-
snippetHash: hashSnippet(match[0]),
|
|
212
|
-
}],
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Extract fastify.get/post/put shorthand methods
|
|
220
|
-
*/
|
|
221
|
-
function extractShorthandMethods(content, relPath, prefix, routes, projectRoot) {
|
|
222
|
-
for (const method of FASTIFY_METHODS) {
|
|
223
|
-
const lowerMethod = method.toLowerCase();
|
|
224
|
-
// Match fastify.get("/path", ...) or fastify.get('/path', ...)
|
|
225
|
-
const pattern = new RegExp(`(?:fastify|app|server)\\s*\\.\\s*${lowerMethod}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`, "g");
|
|
226
|
-
|
|
227
|
-
let match;
|
|
228
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
229
|
-
const rawPath = match[1];
|
|
230
|
-
const fullPath = combinePrefixes(prefix, rawPath);
|
|
231
|
-
const canonicalPath = convertFastifyPathToCanonical(fullPath);
|
|
232
|
-
|
|
233
|
-
const lineNum = content.substring(0, match.index).split("\n").length;
|
|
234
|
-
|
|
235
|
-
routes.push({
|
|
236
|
-
id: `R_FASTIFY_${hashPath(canonicalPath)}_${method}`,
|
|
237
|
-
kind: "fastify",
|
|
238
|
-
methods: method === "ALL" ? ["GET", "POST", "PUT", "PATCH", "DELETE"] : [method],
|
|
239
|
-
rawPath: fullPath,
|
|
240
|
-
path: canonicalPath,
|
|
241
|
-
canonicalPath,
|
|
242
|
-
authRequired: "unknown",
|
|
243
|
-
confidence: "medium",
|
|
244
|
-
handler: {
|
|
245
|
-
file: relPath,
|
|
246
|
-
export: lowerMethod,
|
|
247
|
-
},
|
|
248
|
-
evidence: [{
|
|
249
|
-
id: `E_${crypto.randomBytes(4).toString("hex").toUpperCase()}`,
|
|
250
|
-
kind: "file",
|
|
251
|
-
reason: `fastify.${lowerMethod}() call`,
|
|
252
|
-
file: relPath,
|
|
253
|
-
lines: `${lineNum}-${lineNum + 3}`,
|
|
254
|
-
snippetHash: hashSnippet(match[0]),
|
|
255
|
-
}],
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Extract fastify.register() calls with prefix stacking
|
|
263
|
-
*/
|
|
264
|
-
function extractRegisterCalls(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited) {
|
|
265
|
-
// Match register(plugin, { prefix: "/api" }) or register(import(...), { prefix })
|
|
266
|
-
const registerPattern = /(?:fastify|app|server)\s*\.\s*register\s*\(\s*(?:import\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|(\w+))\s*(?:,\s*\{[^}]*prefix\s*:\s*['"`]([^'"`]+)['"`][^}]*\})?/g;
|
|
267
|
-
|
|
268
|
-
let match;
|
|
269
|
-
while ((match = registerPattern.exec(content)) !== null) {
|
|
270
|
-
const importPath = match[1] || match[2];
|
|
271
|
-
const varName = match[3];
|
|
272
|
-
const newPrefix = match[4] || "";
|
|
273
|
-
|
|
274
|
-
const combinedPrefix = combinePrefixes(currentPrefix, newPrefix);
|
|
275
|
-
if (newPrefix) {
|
|
276
|
-
prefixes.add(combinedPrefix);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Try to resolve and scan the registered plugin
|
|
280
|
-
if (importPath) {
|
|
281
|
-
const resolvedPath = resolveImportPath(filePath, importPath, projectRoot);
|
|
282
|
-
if (resolvedPath) {
|
|
283
|
-
extractFromFile(resolvedPath, projectRoot, combinedPrefix, routes, prefixes, autoloadDirs, gaps, visited);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Extract AutoLoad patterns
|
|
291
|
-
*/
|
|
292
|
-
function extractAutoLoad(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited) {
|
|
293
|
-
// Match AutoLoad registration
|
|
294
|
-
const autoloadPattern = /register\s*\(\s*(?:AutoLoad|autoLoad|fastifyAutoload)\s*,\s*\{[^}]*dir\s*:\s*(?:path\.join\s*\(\s*__dirname\s*,\s*['"`]([^'"`]+)['"`]\s*\)|['"`]([^'"`]+)['"`])/g;
|
|
295
|
-
|
|
296
|
-
let match;
|
|
297
|
-
while ((match = autoloadPattern.exec(content)) !== null) {
|
|
298
|
-
const dirRelative = match[1] || match[2];
|
|
299
|
-
|
|
300
|
-
if (dirRelative) {
|
|
301
|
-
const fileDir = path.dirname(filePath);
|
|
302
|
-
const autoloadDir = path.resolve(fileDir, dirRelative);
|
|
303
|
-
|
|
304
|
-
if (fs.existsSync(autoloadDir)) {
|
|
305
|
-
autoloadDirs.push(path.relative(projectRoot, autoloadDir));
|
|
306
|
-
scanRouteDirectory(autoloadDir, projectRoot, currentPrefix, routes, prefixes, gaps, visited);
|
|
307
|
-
} else {
|
|
308
|
-
gaps.push({
|
|
309
|
-
type: "autoload_dir_not_found",
|
|
310
|
-
dir: dirRelative,
|
|
311
|
-
file: path.relative(projectRoot, filePath),
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
// Non-literal dir path
|
|
316
|
-
gaps.push({
|
|
317
|
-
type: "autoload_dir_not_resolvable",
|
|
318
|
-
file: path.relative(projectRoot, filePath),
|
|
319
|
-
message: "AutoLoad directory is not statically resolvable",
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Scan a routes directory for plugin files
|
|
327
|
-
*/
|
|
328
|
-
function scanRouteDirectory(dir, projectRoot, prefix, routes, prefixes, gaps, visited) {
|
|
329
|
-
const pattern = path.join(dir, "**", "*.{js,ts}").replace(/\\/g, "/");
|
|
330
|
-
const files = fg.sync(pattern, { onlyFiles: true, absolute: true, ignore: ["**/*.test.*", "**/*.spec.*"] });
|
|
331
|
-
|
|
332
|
-
for (const file of files) {
|
|
333
|
-
// Infer prefix from directory structure
|
|
334
|
-
const relDir = path.relative(dir, path.dirname(file)).replace(/\\/g, "/");
|
|
335
|
-
const inferredPrefix = relDir ? `/${relDir}` : "";
|
|
336
|
-
const combinedPrefix = combinePrefixes(prefix, inferredPrefix);
|
|
337
|
-
|
|
338
|
-
extractFromFile(file, projectRoot, combinedPrefix, routes, prefixes, [], gaps, visited);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Convert Fastify path params to canonical format
|
|
344
|
-
* :id → {id}
|
|
345
|
-
* :path* → {*path}
|
|
346
|
-
* /* → {*path}
|
|
347
|
-
*/
|
|
348
|
-
function convertFastifyPathToCanonical(urlPath) {
|
|
349
|
-
return urlPath
|
|
350
|
-
// Wildcard catch-all
|
|
351
|
-
.replace(/\/\*$/, "/{*path}")
|
|
352
|
-
.replace(/\/:(\w+)\*/, "/{*$1}")
|
|
353
|
-
// Named params
|
|
354
|
-
.replace(/:(\w+)/g, "{$1}")
|
|
355
|
-
// Clean up
|
|
356
|
-
.replace(/\/+/g, "/")
|
|
357
|
-
.replace(/(.)\/$/, "$1");
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Combine prefix segments
|
|
362
|
-
*/
|
|
363
|
-
function combinePrefixes(base, addition) {
|
|
364
|
-
if (!addition) return base;
|
|
365
|
-
if (!base) return addition;
|
|
366
|
-
|
|
367
|
-
const baseTrimmed = base.replace(/\/$/, "");
|
|
368
|
-
const addTrimmed = addition.startsWith("/") ? addition : "/" + addition;
|
|
369
|
-
|
|
370
|
-
return baseTrimmed + addTrimmed;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Resolve import path to absolute file path
|
|
375
|
-
*/
|
|
376
|
-
function resolveImportPath(fromFile, importPath, projectRoot) {
|
|
377
|
-
const fromDir = path.dirname(fromFile);
|
|
378
|
-
|
|
379
|
-
// Relative import
|
|
380
|
-
if (importPath.startsWith(".")) {
|
|
381
|
-
const extensions = [".ts", ".js", ".tsx", ".jsx", ""];
|
|
382
|
-
for (const ext of extensions) {
|
|
383
|
-
const resolved = path.resolve(fromDir, importPath + ext);
|
|
384
|
-
if (fs.existsSync(resolved)) return resolved;
|
|
385
|
-
|
|
386
|
-
// Try index file
|
|
387
|
-
const indexResolved = path.resolve(fromDir, importPath, "index" + ext);
|
|
388
|
-
if (fs.existsSync(indexResolved)) return indexResolved;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Runtime introspection for Fastify routes (optional, high-confidence)
|
|
397
|
-
*/
|
|
398
|
-
async function introspectFastifyRoutes(projectRoot, entryFile) {
|
|
399
|
-
// This would spawn a node process to load and dump routes
|
|
400
|
-
// For now, return null to indicate runtime introspection not performed
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Helpers
|
|
405
|
-
function hashSnippet(text) {
|
|
406
|
-
return `sha256:${crypto.createHash("sha256").update(text).digest("hex")}`;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function hashPath(urlPath) {
|
|
410
|
-
return crypto.createHash("sha256").update(urlPath).digest("hex").slice(0, 12).toUpperCase();
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
module.exports = {
|
|
414
|
-
detectFastify,
|
|
415
|
-
extractFastifyRoutes,
|
|
416
|
-
extractFromFile,
|
|
417
|
-
extractRouteMethod,
|
|
418
|
-
extractShorthandMethods,
|
|
419
|
-
extractRegisterCalls,
|
|
420
|
-
extractAutoLoad,
|
|
421
|
-
convertFastifyPathToCanonical,
|
|
422
|
-
combinePrefixes,
|
|
423
|
-
resolveImportPath,
|
|
424
|
-
introspectFastifyRoutes,
|
|
425
|
-
FASTIFY_METHODS,
|
|
426
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Fastify Route Extractor v2
|
|
3
|
+
*
|
|
4
|
+
* Spec-compliant extraction with prefix stacking and autoload support.
|
|
5
|
+
* Two-tier approach: static AST extraction + optional runtime introspection.
|
|
6
|
+
*
|
|
7
|
+
* Patterns supported:
|
|
8
|
+
* - fastify.route({ method, url, handler })
|
|
9
|
+
* - fastify.get/post/put/patch/delete(path, handler)
|
|
10
|
+
* - fastify.register(plugin, { prefix })
|
|
11
|
+
* - AutoLoad with dir resolution
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const fg = require("fast-glob");
|
|
19
|
+
const crypto = require("crypto");
|
|
20
|
+
|
|
21
|
+
const FASTIFY_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "ALL"];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect Fastify service and entrypoint
|
|
25
|
+
*/
|
|
26
|
+
function detectFastify(projectRoot) {
|
|
27
|
+
const result = {
|
|
28
|
+
present: false,
|
|
29
|
+
entryFile: null,
|
|
30
|
+
prefixes: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Check package.json for fastify dependency
|
|
34
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
35
|
+
if (fs.existsSync(pkgPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
38
|
+
if (pkg.dependencies?.fastify || pkg.devDependencies?.fastify) {
|
|
39
|
+
result.present = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try to find entry from scripts
|
|
43
|
+
const scripts = pkg.scripts || {};
|
|
44
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
45
|
+
if (name === "start" || name === "dev" || name === "server") {
|
|
46
|
+
const entryMatch = cmd.match(/(?:node|tsx|ts-node|npx\s+tsx?)\s+([^\s]+)/);
|
|
47
|
+
if (entryMatch) {
|
|
48
|
+
const entryPath = path.join(projectRoot, entryMatch[1]);
|
|
49
|
+
if (fs.existsSync(entryPath)) {
|
|
50
|
+
result.entryFile = entryMatch[1];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback: search common entry files
|
|
59
|
+
if (!result.entryFile) {
|
|
60
|
+
const commonEntries = [
|
|
61
|
+
"server.ts", "server.js",
|
|
62
|
+
"src/server.ts", "src/server.js",
|
|
63
|
+
"app.ts", "app.js",
|
|
64
|
+
"src/app.ts", "src/app.js",
|
|
65
|
+
"index.ts", "index.js",
|
|
66
|
+
"src/index.ts", "src/index.js",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const entry of commonEntries) {
|
|
70
|
+
const entryPath = path.join(projectRoot, entry);
|
|
71
|
+
if (fs.existsSync(entryPath)) {
|
|
72
|
+
const content = fs.readFileSync(entryPath, "utf8");
|
|
73
|
+
if (/from\s+['"]fastify['"]|require\s*\(\s*['"]fastify['"]\s*\)/.test(content)) {
|
|
74
|
+
result.entryFile = entry;
|
|
75
|
+
result.present = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract routes from Fastify project using static AST analysis
|
|
87
|
+
*/
|
|
88
|
+
function extractFastifyRoutes(projectRoot, entryFile = null) {
|
|
89
|
+
const detection = detectFastify(projectRoot);
|
|
90
|
+
|
|
91
|
+
if (!detection.present) {
|
|
92
|
+
return {
|
|
93
|
+
present: false,
|
|
94
|
+
entryFile: null,
|
|
95
|
+
routes: [],
|
|
96
|
+
prefixes: [],
|
|
97
|
+
autoloadDirs: [],
|
|
98
|
+
gaps: [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const entry = entryFile || detection.entryFile;
|
|
103
|
+
const routes = [];
|
|
104
|
+
const prefixes = new Set();
|
|
105
|
+
const autoloadDirs = [];
|
|
106
|
+
const gaps = [];
|
|
107
|
+
const visited = new Set();
|
|
108
|
+
|
|
109
|
+
// Start extraction from entry file
|
|
110
|
+
if (entry) {
|
|
111
|
+
const entryPath = path.join(projectRoot, entry);
|
|
112
|
+
if (fs.existsSync(entryPath)) {
|
|
113
|
+
extractFromFile(entryPath, projectRoot, "", routes, prefixes, autoloadDirs, gaps, visited);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Also scan common route directories
|
|
118
|
+
const routeDirs = ["routes", "src/routes", "api", "src/api"];
|
|
119
|
+
for (const dir of routeDirs) {
|
|
120
|
+
const dirPath = path.join(projectRoot, dir);
|
|
121
|
+
if (fs.existsSync(dirPath)) {
|
|
122
|
+
scanRouteDirectory(dirPath, projectRoot, "", routes, prefixes, gaps, visited);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
present: true,
|
|
128
|
+
entryFile: entry,
|
|
129
|
+
routes,
|
|
130
|
+
prefixes: [...prefixes],
|
|
131
|
+
autoloadDirs,
|
|
132
|
+
gaps,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Extract routes from a single file
|
|
138
|
+
*/
|
|
139
|
+
function extractFromFile(filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited) {
|
|
140
|
+
if (visited.has(filePath)) return;
|
|
141
|
+
visited.add(filePath);
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(filePath)) return;
|
|
144
|
+
|
|
145
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
146
|
+
const relPath = path.relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
147
|
+
|
|
148
|
+
// Pattern A: fastify.route({ method, url, ... })
|
|
149
|
+
extractRouteMethod(content, relPath, currentPrefix, routes, projectRoot);
|
|
150
|
+
|
|
151
|
+
// Pattern B: fastify.get/post/put/... shorthand
|
|
152
|
+
extractShorthandMethods(content, relPath, currentPrefix, routes, projectRoot);
|
|
153
|
+
|
|
154
|
+
// Pattern C: fastify.register with prefix
|
|
155
|
+
extractRegisterCalls(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited);
|
|
156
|
+
|
|
157
|
+
// Pattern D: AutoLoad
|
|
158
|
+
extractAutoLoad(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract fastify.route({ method, url, ... }) patterns
|
|
163
|
+
*/
|
|
164
|
+
function extractRouteMethod(content, relPath, prefix, routes, projectRoot) {
|
|
165
|
+
// Match fastify.route({ method: "POST", url: "/path", ... })
|
|
166
|
+
const routePattern = /(?:fastify|app|server)\s*\.\s*route\s*\(\s*\{([^}]+)\}/g;
|
|
167
|
+
|
|
168
|
+
let match;
|
|
169
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
170
|
+
const routeBody = match[1];
|
|
171
|
+
|
|
172
|
+
// Extract method
|
|
173
|
+
const methodMatch = routeBody.match(/method\s*:\s*(?:['"`](\w+)['"`]|\[([^\]]+)\])/);
|
|
174
|
+
let methods = ["UNKNOWN"];
|
|
175
|
+
if (methodMatch) {
|
|
176
|
+
if (methodMatch[1]) {
|
|
177
|
+
methods = [methodMatch[1].toUpperCase()];
|
|
178
|
+
} else if (methodMatch[2]) {
|
|
179
|
+
methods = methodMatch[2].match(/['"`](\w+)['"`]/g)?.map(m => m.replace(/['"`]/g, "").toUpperCase()) || [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Extract url/path
|
|
184
|
+
const urlMatch = routeBody.match(/(?:url|path)\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
185
|
+
if (urlMatch) {
|
|
186
|
+
const rawPath = urlMatch[1];
|
|
187
|
+
const fullPath = combinePrefixes(prefix, rawPath);
|
|
188
|
+
const canonicalPath = convertFastifyPathToCanonical(fullPath);
|
|
189
|
+
|
|
190
|
+
const lineNum = content.substring(0, match.index).split("\n").length;
|
|
191
|
+
|
|
192
|
+
routes.push({
|
|
193
|
+
id: `R_FASTIFY_${hashPath(canonicalPath)}`,
|
|
194
|
+
kind: "fastify",
|
|
195
|
+
methods,
|
|
196
|
+
rawPath: fullPath,
|
|
197
|
+
path: canonicalPath,
|
|
198
|
+
canonicalPath,
|
|
199
|
+
authRequired: "unknown",
|
|
200
|
+
confidence: "medium",
|
|
201
|
+
handler: {
|
|
202
|
+
file: relPath,
|
|
203
|
+
export: "route",
|
|
204
|
+
},
|
|
205
|
+
evidence: [{
|
|
206
|
+
id: `E_${crypto.randomBytes(4).toString("hex").toUpperCase()}`,
|
|
207
|
+
kind: "file",
|
|
208
|
+
reason: "fastify.route() call",
|
|
209
|
+
file: relPath,
|
|
210
|
+
lines: `${lineNum}-${lineNum + 5}`,
|
|
211
|
+
snippetHash: hashSnippet(match[0]),
|
|
212
|
+
}],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extract fastify.get/post/put shorthand methods
|
|
220
|
+
*/
|
|
221
|
+
function extractShorthandMethods(content, relPath, prefix, routes, projectRoot) {
|
|
222
|
+
for (const method of FASTIFY_METHODS) {
|
|
223
|
+
const lowerMethod = method.toLowerCase();
|
|
224
|
+
// Match fastify.get("/path", ...) or fastify.get('/path', ...)
|
|
225
|
+
const pattern = new RegExp(`(?:fastify|app|server)\\s*\\.\\s*${lowerMethod}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`, "g");
|
|
226
|
+
|
|
227
|
+
let match;
|
|
228
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
229
|
+
const rawPath = match[1];
|
|
230
|
+
const fullPath = combinePrefixes(prefix, rawPath);
|
|
231
|
+
const canonicalPath = convertFastifyPathToCanonical(fullPath);
|
|
232
|
+
|
|
233
|
+
const lineNum = content.substring(0, match.index).split("\n").length;
|
|
234
|
+
|
|
235
|
+
routes.push({
|
|
236
|
+
id: `R_FASTIFY_${hashPath(canonicalPath)}_${method}`,
|
|
237
|
+
kind: "fastify",
|
|
238
|
+
methods: method === "ALL" ? ["GET", "POST", "PUT", "PATCH", "DELETE"] : [method],
|
|
239
|
+
rawPath: fullPath,
|
|
240
|
+
path: canonicalPath,
|
|
241
|
+
canonicalPath,
|
|
242
|
+
authRequired: "unknown",
|
|
243
|
+
confidence: "medium",
|
|
244
|
+
handler: {
|
|
245
|
+
file: relPath,
|
|
246
|
+
export: lowerMethod,
|
|
247
|
+
},
|
|
248
|
+
evidence: [{
|
|
249
|
+
id: `E_${crypto.randomBytes(4).toString("hex").toUpperCase()}`,
|
|
250
|
+
kind: "file",
|
|
251
|
+
reason: `fastify.${lowerMethod}() call`,
|
|
252
|
+
file: relPath,
|
|
253
|
+
lines: `${lineNum}-${lineNum + 3}`,
|
|
254
|
+
snippetHash: hashSnippet(match[0]),
|
|
255
|
+
}],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Extract fastify.register() calls with prefix stacking
|
|
263
|
+
*/
|
|
264
|
+
function extractRegisterCalls(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited) {
|
|
265
|
+
// Match register(plugin, { prefix: "/api" }) or register(import(...), { prefix })
|
|
266
|
+
const registerPattern = /(?:fastify|app|server)\s*\.\s*register\s*\(\s*(?:import\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|(\w+))\s*(?:,\s*\{[^}]*prefix\s*:\s*['"`]([^'"`]+)['"`][^}]*\})?/g;
|
|
267
|
+
|
|
268
|
+
let match;
|
|
269
|
+
while ((match = registerPattern.exec(content)) !== null) {
|
|
270
|
+
const importPath = match[1] || match[2];
|
|
271
|
+
const varName = match[3];
|
|
272
|
+
const newPrefix = match[4] || "";
|
|
273
|
+
|
|
274
|
+
const combinedPrefix = combinePrefixes(currentPrefix, newPrefix);
|
|
275
|
+
if (newPrefix) {
|
|
276
|
+
prefixes.add(combinedPrefix);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Try to resolve and scan the registered plugin
|
|
280
|
+
if (importPath) {
|
|
281
|
+
const resolvedPath = resolveImportPath(filePath, importPath, projectRoot);
|
|
282
|
+
if (resolvedPath) {
|
|
283
|
+
extractFromFile(resolvedPath, projectRoot, combinedPrefix, routes, prefixes, autoloadDirs, gaps, visited);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Extract AutoLoad patterns
|
|
291
|
+
*/
|
|
292
|
+
function extractAutoLoad(content, filePath, projectRoot, currentPrefix, routes, prefixes, autoloadDirs, gaps, visited) {
|
|
293
|
+
// Match AutoLoad registration
|
|
294
|
+
const autoloadPattern = /register\s*\(\s*(?:AutoLoad|autoLoad|fastifyAutoload)\s*,\s*\{[^}]*dir\s*:\s*(?:path\.join\s*\(\s*__dirname\s*,\s*['"`]([^'"`]+)['"`]\s*\)|['"`]([^'"`]+)['"`])/g;
|
|
295
|
+
|
|
296
|
+
let match;
|
|
297
|
+
while ((match = autoloadPattern.exec(content)) !== null) {
|
|
298
|
+
const dirRelative = match[1] || match[2];
|
|
299
|
+
|
|
300
|
+
if (dirRelative) {
|
|
301
|
+
const fileDir = path.dirname(filePath);
|
|
302
|
+
const autoloadDir = path.resolve(fileDir, dirRelative);
|
|
303
|
+
|
|
304
|
+
if (fs.existsSync(autoloadDir)) {
|
|
305
|
+
autoloadDirs.push(path.relative(projectRoot, autoloadDir));
|
|
306
|
+
scanRouteDirectory(autoloadDir, projectRoot, currentPrefix, routes, prefixes, gaps, visited);
|
|
307
|
+
} else {
|
|
308
|
+
gaps.push({
|
|
309
|
+
type: "autoload_dir_not_found",
|
|
310
|
+
dir: dirRelative,
|
|
311
|
+
file: path.relative(projectRoot, filePath),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// Non-literal dir path
|
|
316
|
+
gaps.push({
|
|
317
|
+
type: "autoload_dir_not_resolvable",
|
|
318
|
+
file: path.relative(projectRoot, filePath),
|
|
319
|
+
message: "AutoLoad directory is not statically resolvable",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Scan a routes directory for plugin files
|
|
327
|
+
*/
|
|
328
|
+
function scanRouteDirectory(dir, projectRoot, prefix, routes, prefixes, gaps, visited) {
|
|
329
|
+
const pattern = path.join(dir, "**", "*.{js,ts}").replace(/\\/g, "/");
|
|
330
|
+
const files = fg.sync(pattern, { onlyFiles: true, absolute: true, ignore: ["**/*.test.*", "**/*.spec.*"] });
|
|
331
|
+
|
|
332
|
+
for (const file of files) {
|
|
333
|
+
// Infer prefix from directory structure
|
|
334
|
+
const relDir = path.relative(dir, path.dirname(file)).replace(/\\/g, "/");
|
|
335
|
+
const inferredPrefix = relDir ? `/${relDir}` : "";
|
|
336
|
+
const combinedPrefix = combinePrefixes(prefix, inferredPrefix);
|
|
337
|
+
|
|
338
|
+
extractFromFile(file, projectRoot, combinedPrefix, routes, prefixes, [], gaps, visited);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Convert Fastify path params to canonical format
|
|
344
|
+
* :id → {id}
|
|
345
|
+
* :path* → {*path}
|
|
346
|
+
* /* → {*path}
|
|
347
|
+
*/
|
|
348
|
+
function convertFastifyPathToCanonical(urlPath) {
|
|
349
|
+
return urlPath
|
|
350
|
+
// Wildcard catch-all
|
|
351
|
+
.replace(/\/\*$/, "/{*path}")
|
|
352
|
+
.replace(/\/:(\w+)\*/, "/{*$1}")
|
|
353
|
+
// Named params
|
|
354
|
+
.replace(/:(\w+)/g, "{$1}")
|
|
355
|
+
// Clean up
|
|
356
|
+
.replace(/\/+/g, "/")
|
|
357
|
+
.replace(/(.)\/$/, "$1");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Combine prefix segments
|
|
362
|
+
*/
|
|
363
|
+
function combinePrefixes(base, addition) {
|
|
364
|
+
if (!addition) return base;
|
|
365
|
+
if (!base) return addition;
|
|
366
|
+
|
|
367
|
+
const baseTrimmed = base.replace(/\/$/, "");
|
|
368
|
+
const addTrimmed = addition.startsWith("/") ? addition : "/" + addition;
|
|
369
|
+
|
|
370
|
+
return baseTrimmed + addTrimmed;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Resolve import path to absolute file path
|
|
375
|
+
*/
|
|
376
|
+
function resolveImportPath(fromFile, importPath, projectRoot) {
|
|
377
|
+
const fromDir = path.dirname(fromFile);
|
|
378
|
+
|
|
379
|
+
// Relative import
|
|
380
|
+
if (importPath.startsWith(".")) {
|
|
381
|
+
const extensions = [".ts", ".js", ".tsx", ".jsx", ""];
|
|
382
|
+
for (const ext of extensions) {
|
|
383
|
+
const resolved = path.resolve(fromDir, importPath + ext);
|
|
384
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
385
|
+
|
|
386
|
+
// Try index file
|
|
387
|
+
const indexResolved = path.resolve(fromDir, importPath, "index" + ext);
|
|
388
|
+
if (fs.existsSync(indexResolved)) return indexResolved;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Runtime introspection for Fastify routes (optional, high-confidence)
|
|
397
|
+
*/
|
|
398
|
+
async function introspectFastifyRoutes(projectRoot, entryFile) {
|
|
399
|
+
// This would spawn a node process to load and dump routes
|
|
400
|
+
// For now, return null to indicate runtime introspection not performed
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Helpers
|
|
405
|
+
function hashSnippet(text) {
|
|
406
|
+
return `sha256:${crypto.createHash("sha256").update(text).digest("hex")}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function hashPath(urlPath) {
|
|
410
|
+
return crypto.createHash("sha256").update(urlPath).digest("hex").slice(0, 12).toUpperCase();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = {
|
|
414
|
+
detectFastify,
|
|
415
|
+
extractFastifyRoutes,
|
|
416
|
+
extractFromFile,
|
|
417
|
+
extractRouteMethod,
|
|
418
|
+
extractShorthandMethods,
|
|
419
|
+
extractRegisterCalls,
|
|
420
|
+
extractAutoLoad,
|
|
421
|
+
convertFastifyPathToCanonical,
|
|
422
|
+
combinePrefixes,
|
|
423
|
+
resolveImportPath,
|
|
424
|
+
introspectFastifyRoutes,
|
|
425
|
+
FASTIFY_METHODS,
|
|
426
|
+
};
|