@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,573 +1,573 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fastify Runtime Route Dump v2
|
|
3
|
-
*
|
|
4
|
-
* Spawns Fastify app with VIBECHECK_ROUTE_DUMP=1 to extract
|
|
5
|
-
* runtime routes, which are more accurate than static extraction.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
"use strict";
|
|
9
|
-
|
|
10
|
-
const { spawn } = require("child_process");
|
|
11
|
-
const fs = require("fs");
|
|
12
|
-
const path = require("path");
|
|
13
|
-
const crypto = require("crypto");
|
|
14
|
-
|
|
15
|
-
// =============================================================================
|
|
16
|
-
// CONSTANTS
|
|
17
|
-
// =============================================================================
|
|
18
|
-
|
|
19
|
-
const DUMP_ENV_VAR = "VIBECHECK_ROUTE_DUMP";
|
|
20
|
-
const DUMP_TIMEOUT_MS = 30000; // 30s max for app startup
|
|
21
|
-
const OUTPUT_DIR = ".vibecheck/runtime";
|
|
22
|
-
const ROUTES_TXT = "fastify_routes.txt";
|
|
23
|
-
const ROUTES_JSON = "fastify_routes.json";
|
|
24
|
-
|
|
25
|
-
// Fastify route dump script injected at runtime
|
|
26
|
-
const DUMP_SCRIPT = `
|
|
27
|
-
// Vibecheck Fastify Route Dump
|
|
28
|
-
// Injected when VIBECHECK_ROUTE_DUMP=1
|
|
29
|
-
|
|
30
|
-
const originalListen = require('fastify').prototype.listen;
|
|
31
|
-
|
|
32
|
-
require('fastify').prototype.listen = async function(...args) {
|
|
33
|
-
await this.ready();
|
|
34
|
-
|
|
35
|
-
const routes = [];
|
|
36
|
-
const routeTable = [];
|
|
37
|
-
|
|
38
|
-
// Iterate all registered routes
|
|
39
|
-
const printRoutes = (routeArray) => {
|
|
40
|
-
for (const route of routeArray) {
|
|
41
|
-
const methods = Array.isArray(route.method) ? route.method : [route.method];
|
|
42
|
-
for (const method of methods) {
|
|
43
|
-
routes.push({
|
|
44
|
-
method: method.toUpperCase(),
|
|
45
|
-
path: route.path || route.url,
|
|
46
|
-
prefix: route.prefix || '',
|
|
47
|
-
fullPath: (route.prefix || '') + (route.path || route.url),
|
|
48
|
-
});
|
|
49
|
-
routeTable.push(\`\${method.toUpperCase().padEnd(8)} \${(route.prefix || '') + (route.path || route.url)}\`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Try different Fastify versions' APIs
|
|
55
|
-
if (this.printRoutes) {
|
|
56
|
-
// Fastify 4.x has printRoutes
|
|
57
|
-
const output = [];
|
|
58
|
-
this.printRoutes({ commonPrefix: false }, (chunk) => output.push(chunk));
|
|
59
|
-
console.log('[VIBECHECK_ROUTES_TXT]');
|
|
60
|
-
console.log(output.join(''));
|
|
61
|
-
console.log('[/VIBECHECK_ROUTES_TXT]');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Manual route extraction (works on all versions)
|
|
65
|
-
if (this[Symbol.for('fastify.routeList')] || this.routes) {
|
|
66
|
-
const routeList = this[Symbol.for('fastify.routeList')] || this.routes || [];
|
|
67
|
-
printRoutes(routeList);
|
|
68
|
-
} else if (this._router && this._router.routes) {
|
|
69
|
-
printRoutes(this._router.routes);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Output JSON
|
|
73
|
-
console.log('[VIBECHECK_ROUTES_JSON]');
|
|
74
|
-
console.log(JSON.stringify(routes, null, 2));
|
|
75
|
-
console.log('[/VIBECHECK_ROUTES_JSON]');
|
|
76
|
-
|
|
77
|
-
// Output table
|
|
78
|
-
console.log('[VIBECHECK_ROUTES_TABLE]');
|
|
79
|
-
routeTable.forEach(r => console.log(r));
|
|
80
|
-
console.log('[/VIBECHECK_ROUTES_TABLE]');
|
|
81
|
-
|
|
82
|
-
// Exit cleanly
|
|
83
|
-
process.exit(0);
|
|
84
|
-
};
|
|
85
|
-
`;
|
|
86
|
-
|
|
87
|
-
// =============================================================================
|
|
88
|
-
// ROUTE DUMP EXECUTION
|
|
89
|
-
// =============================================================================
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Find Fastify entry point
|
|
93
|
-
*/
|
|
94
|
-
function findFastifyEntry(repoRoot, options = {}) {
|
|
95
|
-
const { fastifyEntry } = options;
|
|
96
|
-
|
|
97
|
-
// User-specified entry
|
|
98
|
-
if (fastifyEntry) {
|
|
99
|
-
const entryPath = path.join(repoRoot, fastifyEntry);
|
|
100
|
-
if (fs.existsSync(entryPath)) {
|
|
101
|
-
return entryPath;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Common patterns
|
|
106
|
-
const candidates = [
|
|
107
|
-
"src/app.ts",
|
|
108
|
-
"src/app.js",
|
|
109
|
-
"src/server.ts",
|
|
110
|
-
"src/server.js",
|
|
111
|
-
"src/index.ts",
|
|
112
|
-
"src/index.js",
|
|
113
|
-
"app.ts",
|
|
114
|
-
"app.js",
|
|
115
|
-
"server.ts",
|
|
116
|
-
"server.js",
|
|
117
|
-
"index.ts",
|
|
118
|
-
"index.js",
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
for (const candidate of candidates) {
|
|
122
|
-
const candidatePath = path.join(repoRoot, candidate);
|
|
123
|
-
if (fs.existsSync(candidatePath)) {
|
|
124
|
-
// Check if it imports fastify
|
|
125
|
-
try {
|
|
126
|
-
const content = fs.readFileSync(candidatePath, "utf8");
|
|
127
|
-
if (content.includes("fastify") || content.includes("Fastify")) {
|
|
128
|
-
return candidatePath;
|
|
129
|
-
}
|
|
130
|
-
} catch {
|
|
131
|
-
// ignore
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Execute runtime route dump
|
|
141
|
-
*/
|
|
142
|
-
async function executeRouteDump(repoRoot, options = {}) {
|
|
143
|
-
const { fastifyEntry, timeout = DUMP_TIMEOUT_MS } = options;
|
|
144
|
-
|
|
145
|
-
const entryPath = findFastifyEntry(repoRoot, { fastifyEntry });
|
|
146
|
-
if (!entryPath) {
|
|
147
|
-
return {
|
|
148
|
-
success: false,
|
|
149
|
-
error: "Could not find Fastify entry point",
|
|
150
|
-
routes: [],
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Determine if we need ts-node
|
|
155
|
-
const isTypeScript = entryPath.endsWith(".ts");
|
|
156
|
-
const nodeCommand = isTypeScript ? "npx" : "node";
|
|
157
|
-
const nodeArgs = isTypeScript
|
|
158
|
-
? ["ts-node", "--transpile-only", entryPath]
|
|
159
|
-
: [entryPath];
|
|
160
|
-
|
|
161
|
-
return new Promise((resolve) => {
|
|
162
|
-
const stdout = [];
|
|
163
|
-
const stderr = [];
|
|
164
|
-
|
|
165
|
-
const child = spawn(nodeCommand, nodeArgs, {
|
|
166
|
-
cwd: repoRoot,
|
|
167
|
-
env: {
|
|
168
|
-
...process.env,
|
|
169
|
-
[DUMP_ENV_VAR]: "1",
|
|
170
|
-
NODE_ENV: "development",
|
|
171
|
-
PORT: "0", // Don't actually bind
|
|
172
|
-
},
|
|
173
|
-
timeout,
|
|
174
|
-
shell: process.platform === "win32",
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
child.stdout.on("data", (data) => {
|
|
178
|
-
stdout.push(data.toString());
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
child.stderr.on("data", (data) => {
|
|
182
|
-
stderr.push(data.toString());
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const timeoutId = setTimeout(() => {
|
|
186
|
-
child.kill("SIGTERM");
|
|
187
|
-
resolve({
|
|
188
|
-
success: false,
|
|
189
|
-
error: `Route dump timed out after ${timeout}ms`,
|
|
190
|
-
routes: [],
|
|
191
|
-
stderr: stderr.join(""),
|
|
192
|
-
});
|
|
193
|
-
}, timeout);
|
|
194
|
-
|
|
195
|
-
child.on("close", (code) => {
|
|
196
|
-
clearTimeout(timeoutId);
|
|
197
|
-
|
|
198
|
-
const output = stdout.join("");
|
|
199
|
-
const routes = parseRouteDumpOutput(output);
|
|
200
|
-
|
|
201
|
-
resolve({
|
|
202
|
-
success: routes.length > 0,
|
|
203
|
-
exitCode: code,
|
|
204
|
-
routes,
|
|
205
|
-
rawOutput: output,
|
|
206
|
-
stderr: stderr.join(""),
|
|
207
|
-
entryPath,
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
child.on("error", (err) => {
|
|
212
|
-
clearTimeout(timeoutId);
|
|
213
|
-
resolve({
|
|
214
|
-
success: false,
|
|
215
|
-
error: err.message,
|
|
216
|
-
routes: [],
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Parse route dump output
|
|
224
|
-
*/
|
|
225
|
-
function parseRouteDumpOutput(output) {
|
|
226
|
-
const routes = [];
|
|
227
|
-
|
|
228
|
-
// Try to parse JSON block
|
|
229
|
-
const jsonMatch = output.match(/\[VIBECHECK_ROUTES_JSON\]([\s\S]*?)\[\/VIBECHECK_ROUTES_JSON\]/);
|
|
230
|
-
if (jsonMatch) {
|
|
231
|
-
try {
|
|
232
|
-
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
233
|
-
if (Array.isArray(parsed)) {
|
|
234
|
-
return parsed.map(normalizeRoute);
|
|
235
|
-
}
|
|
236
|
-
} catch {
|
|
237
|
-
// Fall through to table parsing
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Try to parse table block
|
|
242
|
-
const tableMatch = output.match(/\[VIBECHECK_ROUTES_TABLE\]([\s\S]*?)\[\/VIBECHECK_ROUTES_TABLE\]/);
|
|
243
|
-
if (tableMatch) {
|
|
244
|
-
const lines = tableMatch[1].trim().split("\n");
|
|
245
|
-
for (const line of lines) {
|
|
246
|
-
const match = line.match(/^(\w+)\s+(.+)$/);
|
|
247
|
-
if (match) {
|
|
248
|
-
routes.push(normalizeRoute({
|
|
249
|
-
method: match[1].toUpperCase(),
|
|
250
|
-
path: match[2].trim(),
|
|
251
|
-
}));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return routes;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Try printRoutes output (Fastify default format)
|
|
258
|
-
const printMatch = output.match(/\[VIBECHECK_ROUTES_TXT\]([\s\S]*?)\[\/VIBECHECK_ROUTES_TXT\]/);
|
|
259
|
-
if (printMatch) {
|
|
260
|
-
// Parse Fastify's tree format
|
|
261
|
-
const lines = printMatch[1].trim().split("\n");
|
|
262
|
-
for (const line of lines) {
|
|
263
|
-
// Match patterns like "├── GET /api/users"
|
|
264
|
-
const match = line.match(/(?:├──|└──|│\s+├──|│\s+└──)?\s*(\w+)\s+(.+?)(?:\s*\(|$)/);
|
|
265
|
-
if (match) {
|
|
266
|
-
routes.push(normalizeRoute({
|
|
267
|
-
method: match[1].toUpperCase(),
|
|
268
|
-
path: match[2].trim(),
|
|
269
|
-
}));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return routes;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Normalize route to standard format
|
|
279
|
-
*/
|
|
280
|
-
function normalizeRoute(route) {
|
|
281
|
-
const fullPath = route.fullPath || route.path;
|
|
282
|
-
return {
|
|
283
|
-
method: (route.method || "GET").toUpperCase(),
|
|
284
|
-
path: fullPath,
|
|
285
|
-
canonicalPath: canonicalizePath(fullPath),
|
|
286
|
-
source: "runtime",
|
|
287
|
-
confidence: "high",
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Canonicalize path for matching
|
|
293
|
-
*/
|
|
294
|
-
function canonicalizePath(p) {
|
|
295
|
-
if (!p) return "";
|
|
296
|
-
return p
|
|
297
|
-
.replace(/\/+/g, "/")
|
|
298
|
-
.replace(/\/$/, "")
|
|
299
|
-
.replace(/:(\w+)/g, ":param")
|
|
300
|
-
.toLowerCase();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// =============================================================================
|
|
304
|
-
// ROUTE MERGING
|
|
305
|
-
// =============================================================================
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Merge runtime routes with static routes
|
|
309
|
-
* Runtime routes take precedence for existence/method validation
|
|
310
|
-
*/
|
|
311
|
-
function mergeWithStaticRoutes(runtimeRoutes, staticRoutes) {
|
|
312
|
-
const merged = new Map();
|
|
313
|
-
|
|
314
|
-
// Add static routes first
|
|
315
|
-
for (const route of staticRoutes) {
|
|
316
|
-
const key = `${route.method}:${route.canonicalPath || canonicalizePath(route.path)}`;
|
|
317
|
-
merged.set(key, { ...route, source: "static" });
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Override with runtime routes
|
|
321
|
-
for (const route of runtimeRoutes) {
|
|
322
|
-
const key = `${route.method}:${route.canonicalPath}`;
|
|
323
|
-
const existing = merged.get(key);
|
|
324
|
-
|
|
325
|
-
if (existing) {
|
|
326
|
-
// Runtime confirms static
|
|
327
|
-
merged.set(key, {
|
|
328
|
-
...existing,
|
|
329
|
-
...route,
|
|
330
|
-
source: "runtime",
|
|
331
|
-
staticEvidence: existing.evidence,
|
|
332
|
-
});
|
|
333
|
-
} else {
|
|
334
|
-
// Runtime-only route (autoloaded, etc.)
|
|
335
|
-
merged.set(key, {
|
|
336
|
-
...route,
|
|
337
|
-
source: "runtime",
|
|
338
|
-
confidence: "high",
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return Array.from(merged.values());
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Find routes in static that don't exist in runtime
|
|
348
|
-
* These might be false positives
|
|
349
|
-
*/
|
|
350
|
-
function findStaticOnlyRoutes(runtimeRoutes, staticRoutes) {
|
|
351
|
-
const runtimeKeys = new Set(
|
|
352
|
-
runtimeRoutes.map(r => `${r.method}:${r.canonicalPath}`)
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
return staticRoutes.filter(route => {
|
|
356
|
-
const key = `${route.method}:${canonicalizePath(route.path)}`;
|
|
357
|
-
return !runtimeKeys.has(key);
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// =============================================================================
|
|
362
|
-
// OUTPUT WRITING
|
|
363
|
-
// =============================================================================
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Write route dump results to disk
|
|
367
|
-
*/
|
|
368
|
-
function writeRouteDump(repoRoot, result) {
|
|
369
|
-
const outputDir = path.join(repoRoot, OUTPUT_DIR);
|
|
370
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
371
|
-
|
|
372
|
-
// Write routes.json
|
|
373
|
-
const jsonPath = path.join(outputDir, ROUTES_JSON);
|
|
374
|
-
const jsonData = {
|
|
375
|
-
specVersion: "2.0",
|
|
376
|
-
generatedAt: new Date().toISOString(),
|
|
377
|
-
source: "runtime",
|
|
378
|
-
entryPath: result.entryPath,
|
|
379
|
-
routes: result.routes,
|
|
380
|
-
count: result.routes.length,
|
|
381
|
-
fingerprint: generateRouteFingerprint(result.routes),
|
|
382
|
-
};
|
|
383
|
-
fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
|
|
384
|
-
|
|
385
|
-
// Write routes.txt (human-readable)
|
|
386
|
-
const txtPath = path.join(outputDir, ROUTES_TXT);
|
|
387
|
-
const txtContent = result.routes
|
|
388
|
-
.map(r => `${r.method.padEnd(8)} ${r.path}`)
|
|
389
|
-
.join("\n");
|
|
390
|
-
fs.writeFileSync(txtPath, txtContent);
|
|
391
|
-
|
|
392
|
-
return {
|
|
393
|
-
jsonPath,
|
|
394
|
-
txtPath,
|
|
395
|
-
fingerprint: jsonData.fingerprint,
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Generate fingerprint for routes
|
|
401
|
-
*/
|
|
402
|
-
function generateRouteFingerprint(routes) {
|
|
403
|
-
const sorted = routes
|
|
404
|
-
.map(r => `${r.method}:${r.canonicalPath || r.path}`)
|
|
405
|
-
.sort()
|
|
406
|
-
.join("\n");
|
|
407
|
-
return "sha256:" + crypto.createHash("sha256").update(sorted).digest("hex");
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Load cached route dump
|
|
412
|
-
*/
|
|
413
|
-
function loadCachedRouteDump(repoRoot) {
|
|
414
|
-
const jsonPath = path.join(repoRoot, OUTPUT_DIR, ROUTES_JSON);
|
|
415
|
-
|
|
416
|
-
if (!fs.existsSync(jsonPath)) {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
return JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
|
422
|
-
} catch {
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// =============================================================================
|
|
428
|
-
// SAFETY RULES
|
|
429
|
-
// =============================================================================
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Check if route-missing finding should be blocked
|
|
433
|
-
* Key safety rule: If runtime dump fails, never BLOCK "missing route"
|
|
434
|
-
* for Fastify-only routes unless runtime (Playwright) proved 404/405.
|
|
435
|
-
*/
|
|
436
|
-
function shouldBlockMissingRoute(finding, context = {}) {
|
|
437
|
-
const { runtimeDumpSuccess, runtimeProof, routeFramework } = context;
|
|
438
|
-
|
|
439
|
-
// If route-missing was proven by Playwright (404/405), always allow BLOCK
|
|
440
|
-
if (runtimeProof?.httpStatus === 404 || runtimeProof?.httpStatus === 405) {
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// If runtime dump succeeded and route not found, allow BLOCK
|
|
445
|
-
if (runtimeDumpSuccess) {
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// If Fastify-only and no runtime proof, downgrade to WARN
|
|
450
|
-
if (routeFramework === "fastify" && !runtimeProof) {
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// For other frameworks (Next.js, etc.), static analysis is reliable
|
|
455
|
-
return true;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Apply safety rules to findings
|
|
460
|
-
*/
|
|
461
|
-
function applySafetyRules(findings, context = {}) {
|
|
462
|
-
return findings.map(finding => {
|
|
463
|
-
if (finding.detectorId === "D_ROUTE_MISSING" || finding.detectorId === "D_ROUTE_HALLUCINATED") {
|
|
464
|
-
const shouldBlock = shouldBlockMissingRoute(finding, {
|
|
465
|
-
...context,
|
|
466
|
-
routeFramework: finding.evidence?.[0]?.framework,
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
if (!shouldBlock && finding.severity === "BLOCK") {
|
|
470
|
-
return {
|
|
471
|
-
...finding,
|
|
472
|
-
severity: "WARN",
|
|
473
|
-
why: (finding.why || "") + " [Downgraded: runtime dump unavailable]",
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return finding;
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// =============================================================================
|
|
483
|
-
// MAIN EXPORT: EXTRACT WITH RUNTIME
|
|
484
|
-
// =============================================================================
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Extract Fastify routes with optional runtime dump
|
|
488
|
-
*/
|
|
489
|
-
async function extractFastifyRoutesWithRuntime(repoRoot, options = {}) {
|
|
490
|
-
const {
|
|
491
|
-
staticRoutes = [],
|
|
492
|
-
fastifyEntry,
|
|
493
|
-
useCache = true,
|
|
494
|
-
forceRuntime = false,
|
|
495
|
-
} = options;
|
|
496
|
-
|
|
497
|
-
// Try cached first
|
|
498
|
-
if (useCache && !forceRuntime) {
|
|
499
|
-
const cached = loadCachedRouteDump(repoRoot);
|
|
500
|
-
if (cached && cached.routes?.length > 0) {
|
|
501
|
-
return {
|
|
502
|
-
success: true,
|
|
503
|
-
source: "cache",
|
|
504
|
-
routes: mergeWithStaticRoutes(cached.routes, staticRoutes),
|
|
505
|
-
runtimeRoutes: cached.routes,
|
|
506
|
-
staticOnlyRoutes: findStaticOnlyRoutes(cached.routes, staticRoutes),
|
|
507
|
-
fingerprint: cached.fingerprint,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Execute runtime dump
|
|
513
|
-
const result = await executeRouteDump(repoRoot, { fastifyEntry });
|
|
514
|
-
|
|
515
|
-
if (result.success) {
|
|
516
|
-
// Write to cache
|
|
517
|
-
const written = writeRouteDump(repoRoot, result);
|
|
518
|
-
|
|
519
|
-
return {
|
|
520
|
-
success: true,
|
|
521
|
-
source: "runtime",
|
|
522
|
-
routes: mergeWithStaticRoutes(result.routes, staticRoutes),
|
|
523
|
-
runtimeRoutes: result.routes,
|
|
524
|
-
staticOnlyRoutes: findStaticOnlyRoutes(result.routes, staticRoutes),
|
|
525
|
-
fingerprint: written.fingerprint,
|
|
526
|
-
entryPath: result.entryPath,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Runtime failed - return static only with warning
|
|
531
|
-
return {
|
|
532
|
-
success: false,
|
|
533
|
-
source: "static",
|
|
534
|
-
routes: staticRoutes,
|
|
535
|
-
runtimeRoutes: [],
|
|
536
|
-
staticOnlyRoutes: staticRoutes,
|
|
537
|
-
error: result.error,
|
|
538
|
-
warning: "Runtime route dump failed; static extraction may have false positives",
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// =============================================================================
|
|
543
|
-
// EXPORTS
|
|
544
|
-
// =============================================================================
|
|
545
|
-
|
|
546
|
-
module.exports = {
|
|
547
|
-
// Constants
|
|
548
|
-
DUMP_ENV_VAR,
|
|
549
|
-
OUTPUT_DIR,
|
|
550
|
-
ROUTES_JSON,
|
|
551
|
-
ROUTES_TXT,
|
|
552
|
-
|
|
553
|
-
// Core functions
|
|
554
|
-
findFastifyEntry,
|
|
555
|
-
executeRouteDump,
|
|
556
|
-
parseRouteDumpOutput,
|
|
557
|
-
|
|
558
|
-
// Merging
|
|
559
|
-
mergeWithStaticRoutes,
|
|
560
|
-
findStaticOnlyRoutes,
|
|
561
|
-
|
|
562
|
-
// Output
|
|
563
|
-
writeRouteDump,
|
|
564
|
-
loadCachedRouteDump,
|
|
565
|
-
generateRouteFingerprint,
|
|
566
|
-
|
|
567
|
-
// Safety rules
|
|
568
|
-
shouldBlockMissingRoute,
|
|
569
|
-
applySafetyRules,
|
|
570
|
-
|
|
571
|
-
// Main entry
|
|
572
|
-
extractFastifyRoutesWithRuntime,
|
|
573
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Fastify Runtime Route Dump v2
|
|
3
|
+
*
|
|
4
|
+
* Spawns Fastify app with VIBECHECK_ROUTE_DUMP=1 to extract
|
|
5
|
+
* runtime routes, which are more accurate than static extraction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { spawn } = require("child_process");
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// CONSTANTS
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
const DUMP_ENV_VAR = "VIBECHECK_ROUTE_DUMP";
|
|
20
|
+
const DUMP_TIMEOUT_MS = 30000; // 30s max for app startup
|
|
21
|
+
const OUTPUT_DIR = ".vibecheck/runtime";
|
|
22
|
+
const ROUTES_TXT = "fastify_routes.txt";
|
|
23
|
+
const ROUTES_JSON = "fastify_routes.json";
|
|
24
|
+
|
|
25
|
+
// Fastify route dump script injected at runtime
|
|
26
|
+
const DUMP_SCRIPT = `
|
|
27
|
+
// Vibecheck Fastify Route Dump
|
|
28
|
+
// Injected when VIBECHECK_ROUTE_DUMP=1
|
|
29
|
+
|
|
30
|
+
const originalListen = require('fastify').prototype.listen;
|
|
31
|
+
|
|
32
|
+
require('fastify').prototype.listen = async function(...args) {
|
|
33
|
+
await this.ready();
|
|
34
|
+
|
|
35
|
+
const routes = [];
|
|
36
|
+
const routeTable = [];
|
|
37
|
+
|
|
38
|
+
// Iterate all registered routes
|
|
39
|
+
const printRoutes = (routeArray) => {
|
|
40
|
+
for (const route of routeArray) {
|
|
41
|
+
const methods = Array.isArray(route.method) ? route.method : [route.method];
|
|
42
|
+
for (const method of methods) {
|
|
43
|
+
routes.push({
|
|
44
|
+
method: method.toUpperCase(),
|
|
45
|
+
path: route.path || route.url,
|
|
46
|
+
prefix: route.prefix || '',
|
|
47
|
+
fullPath: (route.prefix || '') + (route.path || route.url),
|
|
48
|
+
});
|
|
49
|
+
routeTable.push(\`\${method.toUpperCase().padEnd(8)} \${(route.prefix || '') + (route.path || route.url)}\`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Try different Fastify versions' APIs
|
|
55
|
+
if (this.printRoutes) {
|
|
56
|
+
// Fastify 4.x has printRoutes
|
|
57
|
+
const output = [];
|
|
58
|
+
this.printRoutes({ commonPrefix: false }, (chunk) => output.push(chunk));
|
|
59
|
+
console.log('[VIBECHECK_ROUTES_TXT]');
|
|
60
|
+
console.log(output.join(''));
|
|
61
|
+
console.log('[/VIBECHECK_ROUTES_TXT]');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Manual route extraction (works on all versions)
|
|
65
|
+
if (this[Symbol.for('fastify.routeList')] || this.routes) {
|
|
66
|
+
const routeList = this[Symbol.for('fastify.routeList')] || this.routes || [];
|
|
67
|
+
printRoutes(routeList);
|
|
68
|
+
} else if (this._router && this._router.routes) {
|
|
69
|
+
printRoutes(this._router.routes);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Output JSON
|
|
73
|
+
console.log('[VIBECHECK_ROUTES_JSON]');
|
|
74
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
75
|
+
console.log('[/VIBECHECK_ROUTES_JSON]');
|
|
76
|
+
|
|
77
|
+
// Output table
|
|
78
|
+
console.log('[VIBECHECK_ROUTES_TABLE]');
|
|
79
|
+
routeTable.forEach(r => console.log(r));
|
|
80
|
+
console.log('[/VIBECHECK_ROUTES_TABLE]');
|
|
81
|
+
|
|
82
|
+
// Exit cleanly
|
|
83
|
+
process.exit(0);
|
|
84
|
+
};
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// ROUTE DUMP EXECUTION
|
|
89
|
+
// =============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Find Fastify entry point
|
|
93
|
+
*/
|
|
94
|
+
function findFastifyEntry(repoRoot, options = {}) {
|
|
95
|
+
const { fastifyEntry } = options;
|
|
96
|
+
|
|
97
|
+
// User-specified entry
|
|
98
|
+
if (fastifyEntry) {
|
|
99
|
+
const entryPath = path.join(repoRoot, fastifyEntry);
|
|
100
|
+
if (fs.existsSync(entryPath)) {
|
|
101
|
+
return entryPath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Common patterns
|
|
106
|
+
const candidates = [
|
|
107
|
+
"src/app.ts",
|
|
108
|
+
"src/app.js",
|
|
109
|
+
"src/server.ts",
|
|
110
|
+
"src/server.js",
|
|
111
|
+
"src/index.ts",
|
|
112
|
+
"src/index.js",
|
|
113
|
+
"app.ts",
|
|
114
|
+
"app.js",
|
|
115
|
+
"server.ts",
|
|
116
|
+
"server.js",
|
|
117
|
+
"index.ts",
|
|
118
|
+
"index.js",
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const candidate of candidates) {
|
|
122
|
+
const candidatePath = path.join(repoRoot, candidate);
|
|
123
|
+
if (fs.existsSync(candidatePath)) {
|
|
124
|
+
// Check if it imports fastify
|
|
125
|
+
try {
|
|
126
|
+
const content = fs.readFileSync(candidatePath, "utf8");
|
|
127
|
+
if (content.includes("fastify") || content.includes("Fastify")) {
|
|
128
|
+
return candidatePath;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Execute runtime route dump
|
|
141
|
+
*/
|
|
142
|
+
async function executeRouteDump(repoRoot, options = {}) {
|
|
143
|
+
const { fastifyEntry, timeout = DUMP_TIMEOUT_MS } = options;
|
|
144
|
+
|
|
145
|
+
const entryPath = findFastifyEntry(repoRoot, { fastifyEntry });
|
|
146
|
+
if (!entryPath) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: "Could not find Fastify entry point",
|
|
150
|
+
routes: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Determine if we need ts-node
|
|
155
|
+
const isTypeScript = entryPath.endsWith(".ts");
|
|
156
|
+
const nodeCommand = isTypeScript ? "npx" : "node";
|
|
157
|
+
const nodeArgs = isTypeScript
|
|
158
|
+
? ["ts-node", "--transpile-only", entryPath]
|
|
159
|
+
: [entryPath];
|
|
160
|
+
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const stdout = [];
|
|
163
|
+
const stderr = [];
|
|
164
|
+
|
|
165
|
+
const child = spawn(nodeCommand, nodeArgs, {
|
|
166
|
+
cwd: repoRoot,
|
|
167
|
+
env: {
|
|
168
|
+
...process.env,
|
|
169
|
+
[DUMP_ENV_VAR]: "1",
|
|
170
|
+
NODE_ENV: "development",
|
|
171
|
+
PORT: "0", // Don't actually bind
|
|
172
|
+
},
|
|
173
|
+
timeout,
|
|
174
|
+
shell: process.platform === "win32",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
child.stdout.on("data", (data) => {
|
|
178
|
+
stdout.push(data.toString());
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
child.stderr.on("data", (data) => {
|
|
182
|
+
stderr.push(data.toString());
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const timeoutId = setTimeout(() => {
|
|
186
|
+
child.kill("SIGTERM");
|
|
187
|
+
resolve({
|
|
188
|
+
success: false,
|
|
189
|
+
error: `Route dump timed out after ${timeout}ms`,
|
|
190
|
+
routes: [],
|
|
191
|
+
stderr: stderr.join(""),
|
|
192
|
+
});
|
|
193
|
+
}, timeout);
|
|
194
|
+
|
|
195
|
+
child.on("close", (code) => {
|
|
196
|
+
clearTimeout(timeoutId);
|
|
197
|
+
|
|
198
|
+
const output = stdout.join("");
|
|
199
|
+
const routes = parseRouteDumpOutput(output);
|
|
200
|
+
|
|
201
|
+
resolve({
|
|
202
|
+
success: routes.length > 0,
|
|
203
|
+
exitCode: code,
|
|
204
|
+
routes,
|
|
205
|
+
rawOutput: output,
|
|
206
|
+
stderr: stderr.join(""),
|
|
207
|
+
entryPath,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
child.on("error", (err) => {
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
resolve({
|
|
214
|
+
success: false,
|
|
215
|
+
error: err.message,
|
|
216
|
+
routes: [],
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Parse route dump output
|
|
224
|
+
*/
|
|
225
|
+
function parseRouteDumpOutput(output) {
|
|
226
|
+
const routes = [];
|
|
227
|
+
|
|
228
|
+
// Try to parse JSON block
|
|
229
|
+
const jsonMatch = output.match(/\[VIBECHECK_ROUTES_JSON\]([\s\S]*?)\[\/VIBECHECK_ROUTES_JSON\]/);
|
|
230
|
+
if (jsonMatch) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
233
|
+
if (Array.isArray(parsed)) {
|
|
234
|
+
return parsed.map(normalizeRoute);
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Fall through to table parsing
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Try to parse table block
|
|
242
|
+
const tableMatch = output.match(/\[VIBECHECK_ROUTES_TABLE\]([\s\S]*?)\[\/VIBECHECK_ROUTES_TABLE\]/);
|
|
243
|
+
if (tableMatch) {
|
|
244
|
+
const lines = tableMatch[1].trim().split("\n");
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
const match = line.match(/^(\w+)\s+(.+)$/);
|
|
247
|
+
if (match) {
|
|
248
|
+
routes.push(normalizeRoute({
|
|
249
|
+
method: match[1].toUpperCase(),
|
|
250
|
+
path: match[2].trim(),
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return routes;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Try printRoutes output (Fastify default format)
|
|
258
|
+
const printMatch = output.match(/\[VIBECHECK_ROUTES_TXT\]([\s\S]*?)\[\/VIBECHECK_ROUTES_TXT\]/);
|
|
259
|
+
if (printMatch) {
|
|
260
|
+
// Parse Fastify's tree format
|
|
261
|
+
const lines = printMatch[1].trim().split("\n");
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
// Match patterns like "├── GET /api/users"
|
|
264
|
+
const match = line.match(/(?:├──|└──|│\s+├──|│\s+└──)?\s*(\w+)\s+(.+?)(?:\s*\(|$)/);
|
|
265
|
+
if (match) {
|
|
266
|
+
routes.push(normalizeRoute({
|
|
267
|
+
method: match[1].toUpperCase(),
|
|
268
|
+
path: match[2].trim(),
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return routes;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Normalize route to standard format
|
|
279
|
+
*/
|
|
280
|
+
function normalizeRoute(route) {
|
|
281
|
+
const fullPath = route.fullPath || route.path;
|
|
282
|
+
return {
|
|
283
|
+
method: (route.method || "GET").toUpperCase(),
|
|
284
|
+
path: fullPath,
|
|
285
|
+
canonicalPath: canonicalizePath(fullPath),
|
|
286
|
+
source: "runtime",
|
|
287
|
+
confidence: "high",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Canonicalize path for matching
|
|
293
|
+
*/
|
|
294
|
+
function canonicalizePath(p) {
|
|
295
|
+
if (!p) return "";
|
|
296
|
+
return p
|
|
297
|
+
.replace(/\/+/g, "/")
|
|
298
|
+
.replace(/\/$/, "")
|
|
299
|
+
.replace(/:(\w+)/g, ":param")
|
|
300
|
+
.toLowerCase();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// ROUTE MERGING
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Merge runtime routes with static routes
|
|
309
|
+
* Runtime routes take precedence for existence/method validation
|
|
310
|
+
*/
|
|
311
|
+
function mergeWithStaticRoutes(runtimeRoutes, staticRoutes) {
|
|
312
|
+
const merged = new Map();
|
|
313
|
+
|
|
314
|
+
// Add static routes first
|
|
315
|
+
for (const route of staticRoutes) {
|
|
316
|
+
const key = `${route.method}:${route.canonicalPath || canonicalizePath(route.path)}`;
|
|
317
|
+
merged.set(key, { ...route, source: "static" });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Override with runtime routes
|
|
321
|
+
for (const route of runtimeRoutes) {
|
|
322
|
+
const key = `${route.method}:${route.canonicalPath}`;
|
|
323
|
+
const existing = merged.get(key);
|
|
324
|
+
|
|
325
|
+
if (existing) {
|
|
326
|
+
// Runtime confirms static
|
|
327
|
+
merged.set(key, {
|
|
328
|
+
...existing,
|
|
329
|
+
...route,
|
|
330
|
+
source: "runtime",
|
|
331
|
+
staticEvidence: existing.evidence,
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
// Runtime-only route (autoloaded, etc.)
|
|
335
|
+
merged.set(key, {
|
|
336
|
+
...route,
|
|
337
|
+
source: "runtime",
|
|
338
|
+
confidence: "high",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return Array.from(merged.values());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Find routes in static that don't exist in runtime
|
|
348
|
+
* These might be false positives
|
|
349
|
+
*/
|
|
350
|
+
function findStaticOnlyRoutes(runtimeRoutes, staticRoutes) {
|
|
351
|
+
const runtimeKeys = new Set(
|
|
352
|
+
runtimeRoutes.map(r => `${r.method}:${r.canonicalPath}`)
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
return staticRoutes.filter(route => {
|
|
356
|
+
const key = `${route.method}:${canonicalizePath(route.path)}`;
|
|
357
|
+
return !runtimeKeys.has(key);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// =============================================================================
|
|
362
|
+
// OUTPUT WRITING
|
|
363
|
+
// =============================================================================
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Write route dump results to disk
|
|
367
|
+
*/
|
|
368
|
+
function writeRouteDump(repoRoot, result) {
|
|
369
|
+
const outputDir = path.join(repoRoot, OUTPUT_DIR);
|
|
370
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
371
|
+
|
|
372
|
+
// Write routes.json
|
|
373
|
+
const jsonPath = path.join(outputDir, ROUTES_JSON);
|
|
374
|
+
const jsonData = {
|
|
375
|
+
specVersion: "2.0",
|
|
376
|
+
generatedAt: new Date().toISOString(),
|
|
377
|
+
source: "runtime",
|
|
378
|
+
entryPath: result.entryPath,
|
|
379
|
+
routes: result.routes,
|
|
380
|
+
count: result.routes.length,
|
|
381
|
+
fingerprint: generateRouteFingerprint(result.routes),
|
|
382
|
+
};
|
|
383
|
+
fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
|
|
384
|
+
|
|
385
|
+
// Write routes.txt (human-readable)
|
|
386
|
+
const txtPath = path.join(outputDir, ROUTES_TXT);
|
|
387
|
+
const txtContent = result.routes
|
|
388
|
+
.map(r => `${r.method.padEnd(8)} ${r.path}`)
|
|
389
|
+
.join("\n");
|
|
390
|
+
fs.writeFileSync(txtPath, txtContent);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
jsonPath,
|
|
394
|
+
txtPath,
|
|
395
|
+
fingerprint: jsonData.fingerprint,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Generate fingerprint for routes
|
|
401
|
+
*/
|
|
402
|
+
function generateRouteFingerprint(routes) {
|
|
403
|
+
const sorted = routes
|
|
404
|
+
.map(r => `${r.method}:${r.canonicalPath || r.path}`)
|
|
405
|
+
.sort()
|
|
406
|
+
.join("\n");
|
|
407
|
+
return "sha256:" + crypto.createHash("sha256").update(sorted).digest("hex");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Load cached route dump
|
|
412
|
+
*/
|
|
413
|
+
function loadCachedRouteDump(repoRoot) {
|
|
414
|
+
const jsonPath = path.join(repoRoot, OUTPUT_DIR, ROUTES_JSON);
|
|
415
|
+
|
|
416
|
+
if (!fs.existsSync(jsonPath)) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
return JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// =============================================================================
|
|
428
|
+
// SAFETY RULES
|
|
429
|
+
// =============================================================================
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if route-missing finding should be blocked
|
|
433
|
+
* Key safety rule: If runtime dump fails, never BLOCK "missing route"
|
|
434
|
+
* for Fastify-only routes unless runtime (Playwright) proved 404/405.
|
|
435
|
+
*/
|
|
436
|
+
function shouldBlockMissingRoute(finding, context = {}) {
|
|
437
|
+
const { runtimeDumpSuccess, runtimeProof, routeFramework } = context;
|
|
438
|
+
|
|
439
|
+
// If route-missing was proven by Playwright (404/405), always allow BLOCK
|
|
440
|
+
if (runtimeProof?.httpStatus === 404 || runtimeProof?.httpStatus === 405) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// If runtime dump succeeded and route not found, allow BLOCK
|
|
445
|
+
if (runtimeDumpSuccess) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// If Fastify-only and no runtime proof, downgrade to WARN
|
|
450
|
+
if (routeFramework === "fastify" && !runtimeProof) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// For other frameworks (Next.js, etc.), static analysis is reliable
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Apply safety rules to findings
|
|
460
|
+
*/
|
|
461
|
+
function applySafetyRules(findings, context = {}) {
|
|
462
|
+
return findings.map(finding => {
|
|
463
|
+
if (finding.detectorId === "D_ROUTE_MISSING" || finding.detectorId === "D_ROUTE_HALLUCINATED") {
|
|
464
|
+
const shouldBlock = shouldBlockMissingRoute(finding, {
|
|
465
|
+
...context,
|
|
466
|
+
routeFramework: finding.evidence?.[0]?.framework,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (!shouldBlock && finding.severity === "BLOCK") {
|
|
470
|
+
return {
|
|
471
|
+
...finding,
|
|
472
|
+
severity: "WARN",
|
|
473
|
+
why: (finding.why || "") + " [Downgraded: runtime dump unavailable]",
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return finding;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// =============================================================================
|
|
483
|
+
// MAIN EXPORT: EXTRACT WITH RUNTIME
|
|
484
|
+
// =============================================================================
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Extract Fastify routes with optional runtime dump
|
|
488
|
+
*/
|
|
489
|
+
async function extractFastifyRoutesWithRuntime(repoRoot, options = {}) {
|
|
490
|
+
const {
|
|
491
|
+
staticRoutes = [],
|
|
492
|
+
fastifyEntry,
|
|
493
|
+
useCache = true,
|
|
494
|
+
forceRuntime = false,
|
|
495
|
+
} = options;
|
|
496
|
+
|
|
497
|
+
// Try cached first
|
|
498
|
+
if (useCache && !forceRuntime) {
|
|
499
|
+
const cached = loadCachedRouteDump(repoRoot);
|
|
500
|
+
if (cached && cached.routes?.length > 0) {
|
|
501
|
+
return {
|
|
502
|
+
success: true,
|
|
503
|
+
source: "cache",
|
|
504
|
+
routes: mergeWithStaticRoutes(cached.routes, staticRoutes),
|
|
505
|
+
runtimeRoutes: cached.routes,
|
|
506
|
+
staticOnlyRoutes: findStaticOnlyRoutes(cached.routes, staticRoutes),
|
|
507
|
+
fingerprint: cached.fingerprint,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Execute runtime dump
|
|
513
|
+
const result = await executeRouteDump(repoRoot, { fastifyEntry });
|
|
514
|
+
|
|
515
|
+
if (result.success) {
|
|
516
|
+
// Write to cache
|
|
517
|
+
const written = writeRouteDump(repoRoot, result);
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
success: true,
|
|
521
|
+
source: "runtime",
|
|
522
|
+
routes: mergeWithStaticRoutes(result.routes, staticRoutes),
|
|
523
|
+
runtimeRoutes: result.routes,
|
|
524
|
+
staticOnlyRoutes: findStaticOnlyRoutes(result.routes, staticRoutes),
|
|
525
|
+
fingerprint: written.fingerprint,
|
|
526
|
+
entryPath: result.entryPath,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Runtime failed - return static only with warning
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
source: "static",
|
|
534
|
+
routes: staticRoutes,
|
|
535
|
+
runtimeRoutes: [],
|
|
536
|
+
staticOnlyRoutes: staticRoutes,
|
|
537
|
+
error: result.error,
|
|
538
|
+
warning: "Runtime route dump failed; static extraction may have false positives",
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// =============================================================================
|
|
543
|
+
// EXPORTS
|
|
544
|
+
// =============================================================================
|
|
545
|
+
|
|
546
|
+
module.exports = {
|
|
547
|
+
// Constants
|
|
548
|
+
DUMP_ENV_VAR,
|
|
549
|
+
OUTPUT_DIR,
|
|
550
|
+
ROUTES_JSON,
|
|
551
|
+
ROUTES_TXT,
|
|
552
|
+
|
|
553
|
+
// Core functions
|
|
554
|
+
findFastifyEntry,
|
|
555
|
+
executeRouteDump,
|
|
556
|
+
parseRouteDumpOutput,
|
|
557
|
+
|
|
558
|
+
// Merging
|
|
559
|
+
mergeWithStaticRoutes,
|
|
560
|
+
findStaticOnlyRoutes,
|
|
561
|
+
|
|
562
|
+
// Output
|
|
563
|
+
writeRouteDump,
|
|
564
|
+
loadCachedRouteDump,
|
|
565
|
+
generateRouteFingerprint,
|
|
566
|
+
|
|
567
|
+
// Safety rules
|
|
568
|
+
shouldBlockMissingRoute,
|
|
569
|
+
applySafetyRules,
|
|
570
|
+
|
|
571
|
+
// Main entry
|
|
572
|
+
extractFastifyRoutesWithRuntime,
|
|
573
|
+
};
|