@vibecheckai/cli 3.1.0 → 3.1.2
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/registry.js +105 -105
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/analysis-core.js +271 -271
- package/bin/runners/lib/analyzers.js +579 -579
- 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-output.js +368 -368
- 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/detectors-v2.js +703 -703
- 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/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/entitlements-v2.js +490 -489
- package/bin/runners/lib/entitlements.js +6 -3
- 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/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/init-wizard.js +308 -308
- 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/missions/plan.js +69 -69
- package/bin/runners/lib/missions/templates.js +192 -192
- 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-engine.js +447 -447
- package/bin/runners/lib/report-html.js +1499 -1499
- package/bin/runners/lib/report-templates.js +969 -969
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/route-truth.js +477 -477
- 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/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/server-usage.js +12 -0
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/truth.js +667 -667
- 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/runAuth.js +51 -0
- package/bin/runners/runClaimVerifier.js +483 -483
- package/bin/runners/runContext.js +56 -56
- package/bin/runners/runContextCompiler.js +385 -385
- package/bin/runners/runCtx.js +674 -674
- package/bin/runners/runCtxDiff.js +301 -301
- package/bin/runners/runCtxGuard.js +176 -176
- package/bin/runners/runCtxSync.js +116 -116
- package/bin/runners/runGate.js +17 -17
- package/bin/runners/runGraph.js +454 -454
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runInitGha.js +164 -164
- package/bin/runners/runInstall.js +277 -277
- package/bin/runners/runInteractive.js +388 -388
- package/bin/runners/runLabs.js +340 -340
- package/bin/runners/runMissionGenerator.js +282 -282
- package/bin/runners/runPR.js +255 -255
- package/bin/runners/runPermissions.js +304 -304
- package/bin/runners/runPreflight.js +580 -553
- package/bin/runners/runProve.js +1252 -1252
- package/bin/runners/runReality.js +1328 -1328
- package/bin/runners/runReplay.js +499 -499
- package/bin/runners/runReport.js +584 -584
- package/bin/runners/runShare.js +212 -212
- package/bin/runners/runStatus.js +138 -138
- package/bin/runners/runTruthpack.js +636 -636
- package/bin/runners/runVerify.js +272 -272
- package/bin/runners/runWatch.js +407 -407
- package/bin/vibecheck.js +2 -1
- package/mcp-server/consolidated-tools.js +804 -804
- package/mcp-server/package.json +1 -1
- package/mcp-server/tools/index.js +72 -72
- package/mcp-server/truth-context.js +581 -581
- package/mcp-server/truth-firewall-tools.js +1500 -1500
- package/package.json +1 -1
- package/bin/runners/runProof.zip +0 -0
package/bin/runners/lib/truth.js
CHANGED
|
@@ -1,667 +1,667 @@
|
|
|
1
|
-
// bin/runners/lib/truth.js
|
|
2
|
-
const fg = require("fast-glob");
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const crypto = require("crypto");
|
|
6
|
-
const parser = require("@babel/parser");
|
|
7
|
-
const traverse = require("@babel/traverse").default;
|
|
8
|
-
const t = require("@babel/types");
|
|
9
|
-
|
|
10
|
-
// Env Truth v1
|
|
11
|
-
const { buildEnvTruth } = require("./env");
|
|
12
|
-
// Auth Truth v1
|
|
13
|
-
const { buildAuthTruth } = require("./auth-truth");
|
|
14
|
-
// Billing Truth v1
|
|
15
|
-
const { buildBillingTruth } = require("./billing");
|
|
16
|
-
// Enforcement Truth v1
|
|
17
|
-
const { buildEnforcementTruth } = require("./enforcement");
|
|
18
|
-
// Multi-framework route detection v2
|
|
19
|
-
const { resolveAllRoutes, detectFrameworks } = require("./route-detection");
|
|
20
|
-
|
|
21
|
-
// ---------- helpers ----------
|
|
22
|
-
function sha256(text) {
|
|
23
|
-
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function canonicalizeMethod(m) {
|
|
27
|
-
const u = String(m || "").toUpperCase();
|
|
28
|
-
if (u === "ALL" || u === "ANY" || u === "*") return "*";
|
|
29
|
-
return u;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function canonicalizePath(p) {
|
|
33
|
-
let s = String(p || "").trim();
|
|
34
|
-
if (!s.startsWith("/")) s = "/" + s;
|
|
35
|
-
s = s.replace(/\/+/g, "/");
|
|
36
|
-
|
|
37
|
-
// Next dynamic segments
|
|
38
|
-
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
|
|
39
|
-
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
|
|
40
|
-
s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
|
|
41
|
-
|
|
42
|
-
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
43
|
-
return s;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function joinPaths(prefix, p) {
|
|
47
|
-
const a = canonicalizePath(prefix || "/");
|
|
48
|
-
const b = canonicalizePath(p || "/");
|
|
49
|
-
if (a === "/") return b;
|
|
50
|
-
if (b === "/") return a;
|
|
51
|
-
return canonicalizePath(a + "/" + b);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function parseFile(code) {
|
|
55
|
-
return parser.parse(code, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function safeRead(fileAbs) {
|
|
59
|
-
return fs.readFileSync(fileAbs, "utf8");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function ensureDir(p) {
|
|
63
|
-
fs.mkdirSync(p, { recursive: true });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
67
|
-
if (!loc) return null;
|
|
68
|
-
const lines = fs.readFileSync(fileAbs, "utf8").split(/\r?\n/);
|
|
69
|
-
const start = Math.max(1, loc.start?.line || 1);
|
|
70
|
-
const end = Math.max(start, loc.end?.line || start);
|
|
71
|
-
const snippet = lines.slice(start - 1, end).join("\n");
|
|
72
|
-
return {
|
|
73
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
74
|
-
file: fileRel,
|
|
75
|
-
lines: `${start}-${end}`,
|
|
76
|
-
snippetHash: sha256(snippet),
|
|
77
|
-
reason
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---------- Next: app router API ----------
|
|
82
|
-
const HTTP_EXPORTS = new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]);
|
|
83
|
-
|
|
84
|
-
async function resolveNextAppApiRoutes(repoRoot) {
|
|
85
|
-
const files = await fg(["**/app/api/**/route.@(ts|js)"], {
|
|
86
|
-
cwd: repoRoot,
|
|
87
|
-
absolute: true,
|
|
88
|
-
ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const out = [];
|
|
92
|
-
|
|
93
|
-
for (const fileAbs of files) {
|
|
94
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
95
|
-
const idx = fileRel.indexOf("app/api/");
|
|
96
|
-
const sub = fileRel.slice(idx + "app/api/".length).replace(/\/route\.(ts|js)$/, "");
|
|
97
|
-
const routePath = canonicalizePath("/api/" + sub);
|
|
98
|
-
|
|
99
|
-
const code = safeRead(fileAbs);
|
|
100
|
-
let ast;
|
|
101
|
-
try { ast = parseFile(code); } catch { continue; }
|
|
102
|
-
|
|
103
|
-
const methods = [];
|
|
104
|
-
traverse(ast, {
|
|
105
|
-
ExportNamedDeclaration(p) {
|
|
106
|
-
const decl = p.node.declaration;
|
|
107
|
-
if (t.isFunctionDeclaration(decl) && decl.id?.name) {
|
|
108
|
-
const n = decl.id.name.toUpperCase();
|
|
109
|
-
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (methods.length === 0) {
|
|
115
|
-
out.push({ method: "*", path: routePath, handler: fileRel, confidence: "low", evidence: [] });
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const m of methods) {
|
|
120
|
-
const ev = evidenceFromLoc({
|
|
121
|
-
fileAbs, fileRel, loc: m.loc,
|
|
122
|
-
reason: `Next app router export ${m.method}`
|
|
123
|
-
});
|
|
124
|
-
out.push({
|
|
125
|
-
method: m.method,
|
|
126
|
-
path: routePath,
|
|
127
|
-
handler: fileRel,
|
|
128
|
-
confidence: "high",
|
|
129
|
-
evidence: ev ? [ev] : []
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return out;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------- Next: pages router API ----------
|
|
138
|
-
async function resolveNextPagesApiRoutes(repoRoot) {
|
|
139
|
-
const files = await fg(["**/pages/api/**/*.@(ts|js)"], {
|
|
140
|
-
cwd: repoRoot,
|
|
141
|
-
absolute: true,
|
|
142
|
-
ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const out = [];
|
|
146
|
-
for (const fileAbs of files) {
|
|
147
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
148
|
-
const idx = fileRel.indexOf("pages/api/");
|
|
149
|
-
const sub = fileRel.slice(idx + "pages/api/".length).replace(/\.(ts|js)$/, "");
|
|
150
|
-
const routePath = canonicalizePath("/api/" + sub);
|
|
151
|
-
|
|
152
|
-
out.push({
|
|
153
|
-
method: "*",
|
|
154
|
-
path: routePath,
|
|
155
|
-
handler: fileRel,
|
|
156
|
-
confidence: "med",
|
|
157
|
-
evidence: []
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
return out;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ---------- minimal relative module resolver ----------
|
|
164
|
-
function exists(p) {
|
|
165
|
-
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
166
|
-
}
|
|
167
|
-
function resolveRelativeModule(fromFileAbs, spec) {
|
|
168
|
-
if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
|
|
169
|
-
const base = path.resolve(path.dirname(fromFileAbs), spec);
|
|
170
|
-
const candidates = [
|
|
171
|
-
base,
|
|
172
|
-
base + ".ts",
|
|
173
|
-
base + ".js",
|
|
174
|
-
path.join(base, "index.ts"),
|
|
175
|
-
path.join(base, "index.js")
|
|
176
|
-
];
|
|
177
|
-
for (const c of candidates) if (exists(c)) return c;
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ---------- Fastify route extraction ----------
|
|
182
|
-
const FASTIFY_METHODS = new Set(["get","post","put","patch","delete","options","head","all"]);
|
|
183
|
-
|
|
184
|
-
function isFastifyMethod(name) {
|
|
185
|
-
return FASTIFY_METHODS.has(name);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function extractStringLiteral(node) {
|
|
189
|
-
return t.isStringLiteral(node) ? node.value : null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function extractPrefixFromOpts(node) {
|
|
193
|
-
if (!t.isObjectExpression(node)) return null;
|
|
194
|
-
for (const p of node.properties) {
|
|
195
|
-
if (!t.isObjectProperty(p)) continue;
|
|
196
|
-
const key =
|
|
197
|
-
t.isIdentifier(p.key) ? p.key.name :
|
|
198
|
-
t.isStringLiteral(p.key) ? p.key.value :
|
|
199
|
-
null;
|
|
200
|
-
if (key === "prefix" && t.isStringLiteral(p.value)) return p.value.value;
|
|
201
|
-
}
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function extractRouteObject(objExpr) {
|
|
206
|
-
let url = null;
|
|
207
|
-
let methods = [];
|
|
208
|
-
let hasHandler = false;
|
|
209
|
-
const hooks = [];
|
|
210
|
-
|
|
211
|
-
for (const p of objExpr.properties) {
|
|
212
|
-
if (!t.isObjectProperty(p)) continue;
|
|
213
|
-
|
|
214
|
-
const key =
|
|
215
|
-
t.isIdentifier(p.key) ? p.key.name :
|
|
216
|
-
t.isStringLiteral(p.key) ? p.key.value :
|
|
217
|
-
null;
|
|
218
|
-
if (!key) continue;
|
|
219
|
-
|
|
220
|
-
if (key === "url" && t.isStringLiteral(p.value)) url = p.value.value;
|
|
221
|
-
|
|
222
|
-
if (key === "method") {
|
|
223
|
-
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
224
|
-
if (t.isArrayExpression(p.value)) {
|
|
225
|
-
methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (key === "handler") hasHandler = true;
|
|
230
|
-
if (["preHandler","onRequest","preValidation","preSerialization"].includes(key)) hooks.push(key);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return { url, methods, hasHandler, hooks };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function resolveFastifyRoutes(repoRoot, entryAbs) {
|
|
237
|
-
const seen = new Set();
|
|
238
|
-
const routes = [];
|
|
239
|
-
const gaps = [];
|
|
240
|
-
|
|
241
|
-
function scanFile(fileAbs, prefix) {
|
|
242
|
-
if (!fileAbs || seen.has(fileAbs)) return;
|
|
243
|
-
seen.add(fileAbs);
|
|
244
|
-
|
|
245
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
246
|
-
const code = safeRead(fileAbs);
|
|
247
|
-
|
|
248
|
-
let ast;
|
|
249
|
-
try { ast = parseFile(code); } catch { return; }
|
|
250
|
-
|
|
251
|
-
// best-effort: fastify instance identifiers
|
|
252
|
-
const fastifyNames = new Set(["fastify"]);
|
|
253
|
-
|
|
254
|
-
traverse(ast, {
|
|
255
|
-
VariableDeclarator(p) {
|
|
256
|
-
if (!t.isIdentifier(p.node.id)) return;
|
|
257
|
-
const id = p.node.id.name;
|
|
258
|
-
const init = p.node.init;
|
|
259
|
-
if (!init) return;
|
|
260
|
-
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
261
|
-
const cal = init.callee.name;
|
|
262
|
-
if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// helper: resolve imports for register(pluginIdent,...)
|
|
268
|
-
function resolveImportSpecForLocal(localName) {
|
|
269
|
-
let spec = null;
|
|
270
|
-
|
|
271
|
-
traverse(ast, {
|
|
272
|
-
ImportDeclaration(ip) {
|
|
273
|
-
for (const s of ip.node.specifiers) {
|
|
274
|
-
if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
|
|
275
|
-
spec = ip.node.source.value;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
VariableDeclarator(vp) {
|
|
280
|
-
if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
|
|
281
|
-
const init = vp.node.init;
|
|
282
|
-
if (!t.isCallExpression(init)) return;
|
|
283
|
-
if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
|
|
284
|
-
const a0 = init.arguments[0];
|
|
285
|
-
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
return spec;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
traverse(ast, {
|
|
293
|
-
CallExpression(p) {
|
|
294
|
-
const callee = p.node.callee;
|
|
295
|
-
if (!t.isMemberExpression(callee)) return;
|
|
296
|
-
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
297
|
-
|
|
298
|
-
const obj = callee.object.name;
|
|
299
|
-
const prop = callee.property.name;
|
|
300
|
-
|
|
301
|
-
if (!fastifyNames.has(obj)) return;
|
|
302
|
-
|
|
303
|
-
// fastify.get('/x', ...)
|
|
304
|
-
if (isFastifyMethod(prop)) {
|
|
305
|
-
const routeStr = extractStringLiteral(p.node.arguments[0]);
|
|
306
|
-
if (!routeStr) return;
|
|
307
|
-
|
|
308
|
-
const fullPath = joinPaths(prefix, routeStr);
|
|
309
|
-
const method = canonicalizeMethod(prop);
|
|
310
|
-
|
|
311
|
-
const ev = evidenceFromLoc({
|
|
312
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
313
|
-
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
routes.push({
|
|
317
|
-
method,
|
|
318
|
-
path: fullPath,
|
|
319
|
-
handler: fileRel,
|
|
320
|
-
confidence: "med",
|
|
321
|
-
evidence: ev ? [ev] : []
|
|
322
|
-
});
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// fastify.route({ method, url, handler })
|
|
327
|
-
if (prop === "route") {
|
|
328
|
-
const arg0 = p.node.arguments[0];
|
|
329
|
-
if (!t.isObjectExpression(arg0)) return;
|
|
330
|
-
|
|
331
|
-
const r = extractRouteObject(arg0);
|
|
332
|
-
if (!r.url) return;
|
|
333
|
-
|
|
334
|
-
const fullPath = joinPaths(prefix, r.url);
|
|
335
|
-
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
336
|
-
|
|
337
|
-
const ev = evidenceFromLoc({
|
|
338
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
339
|
-
reason: `Fastify.route({ url: "${r.url}" })`
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
for (const m of ms) {
|
|
343
|
-
routes.push({
|
|
344
|
-
method: m,
|
|
345
|
-
path: fullPath,
|
|
346
|
-
handler: fileRel,
|
|
347
|
-
hooks: r.hooks,
|
|
348
|
-
confidence: r.hasHandler ? "med" : "low",
|
|
349
|
-
evidence: ev ? [ev] : []
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// fastify.register(plugin, { prefix })
|
|
356
|
-
if (prop === "register") {
|
|
357
|
-
const pluginArg = p.node.arguments[0];
|
|
358
|
-
const optsArg = p.node.arguments[1];
|
|
359
|
-
const childPrefixRaw = extractPrefixFromOpts(optsArg);
|
|
360
|
-
const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
|
|
361
|
-
|
|
362
|
-
// inline plugin
|
|
363
|
-
if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
|
|
364
|
-
const param0 = pluginArg.params[0];
|
|
365
|
-
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
366
|
-
|
|
367
|
-
// traverse just the plugin body (best effort)
|
|
368
|
-
traverse(pluginArg.body, {
|
|
369
|
-
CallExpression(pp) {
|
|
370
|
-
const c = pp.node.callee;
|
|
371
|
-
if (!t.isMemberExpression(c)) return;
|
|
372
|
-
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
373
|
-
if (c.object.name !== innerName) return;
|
|
374
|
-
|
|
375
|
-
const pr = c.property.name;
|
|
376
|
-
|
|
377
|
-
if (isFastifyMethod(pr)) {
|
|
378
|
-
const rs = extractStringLiteral(pp.node.arguments[0]);
|
|
379
|
-
if (!rs) return;
|
|
380
|
-
const fullPath = joinPaths(childPrefix, rs);
|
|
381
|
-
const method = canonicalizeMethod(pr);
|
|
382
|
-
|
|
383
|
-
const ev = evidenceFromLoc({
|
|
384
|
-
fileAbs, fileRel, loc: pp.node.loc,
|
|
385
|
-
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
routes.push({ method, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (pr === "route") {
|
|
392
|
-
const a0 = pp.node.arguments[0];
|
|
393
|
-
if (!t.isObjectExpression(a0)) return;
|
|
394
|
-
const r = extractRouteObject(a0);
|
|
395
|
-
if (!r.url) return;
|
|
396
|
-
const fullPath = joinPaths(childPrefix, r.url);
|
|
397
|
-
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
398
|
-
|
|
399
|
-
const ev = evidenceFromLoc({
|
|
400
|
-
fileAbs, fileRel, loc: pp.node.loc,
|
|
401
|
-
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
for (const m of ms) routes.push({ method: m, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}, p.scope, p);
|
|
408
|
-
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// imported plugin identifier
|
|
413
|
-
if (t.isIdentifier(pluginArg)) {
|
|
414
|
-
const localName = pluginArg.name;
|
|
415
|
-
const spec = resolveImportSpecForLocal(localName);
|
|
416
|
-
|
|
417
|
-
if (!spec) {
|
|
418
|
-
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const resolved = resolveRelativeModule(fileAbs, spec);
|
|
423
|
-
if (!resolved) {
|
|
424
|
-
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
scanFile(resolved, childPrefix);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
scanFile(entryAbs, "/");
|
|
436
|
-
return { routes, gaps };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// ---------- client refs (fetch + axios string literal only) ----------
|
|
440
|
-
function isAxiosMember(node) {
|
|
441
|
-
return t.isMemberExpression(node) &&
|
|
442
|
-
t.isIdentifier(node.object) &&
|
|
443
|
-
t.isIdentifier(node.property) &&
|
|
444
|
-
["get","post","put","patch","delete"].includes(node.property.name);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
async function resolveClientRouteRefs(repoRoot) {
|
|
448
|
-
const files = await fg(["**/*.{ts,tsx,js,jsx}"], {
|
|
449
|
-
cwd: repoRoot,
|
|
450
|
-
absolute: true,
|
|
451
|
-
ignore: [
|
|
452
|
-
"**/node_modules/**",
|
|
453
|
-
"**/.next/**",
|
|
454
|
-
"**/dist/**",
|
|
455
|
-
"**/build/**",
|
|
456
|
-
"**/test/**",
|
|
457
|
-
"**/tests/**",
|
|
458
|
-
"**/__tests__/**",
|
|
459
|
-
"**/*.test.*",
|
|
460
|
-
"**/*.spec.*",
|
|
461
|
-
"**/jest.setup.*",
|
|
462
|
-
"**/jest.config.*",
|
|
463
|
-
"**/*.mock.*",
|
|
464
|
-
"**/mocks/**",
|
|
465
|
-
"**/fixtures/**"
|
|
466
|
-
]
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
const out = [];
|
|
470
|
-
|
|
471
|
-
for (const fileAbs of files) {
|
|
472
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
473
|
-
const code = safeRead(fileAbs);
|
|
474
|
-
|
|
475
|
-
let ast;
|
|
476
|
-
try { ast = parseFile(code); } catch { continue; }
|
|
477
|
-
|
|
478
|
-
traverse(ast, {
|
|
479
|
-
CallExpression(p) {
|
|
480
|
-
const callee = p.node.callee;
|
|
481
|
-
|
|
482
|
-
// fetch("/api/x", { method: "POST" })
|
|
483
|
-
if (t.isIdentifier(callee) && callee.name === "fetch") {
|
|
484
|
-
const a0 = p.node.arguments[0];
|
|
485
|
-
if (!t.isStringLiteral(a0)) return;
|
|
486
|
-
|
|
487
|
-
const url = a0.value;
|
|
488
|
-
if (!url.startsWith("/")) return;
|
|
489
|
-
|
|
490
|
-
let method = "*";
|
|
491
|
-
const a1 = p.node.arguments[1];
|
|
492
|
-
if (t.isObjectExpression(a1)) {
|
|
493
|
-
for (const prop of a1.properties) {
|
|
494
|
-
if (!t.isObjectProperty(prop)) continue;
|
|
495
|
-
const key =
|
|
496
|
-
t.isIdentifier(prop.key) ? prop.key.name :
|
|
497
|
-
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
498
|
-
null;
|
|
499
|
-
if (key === "method" && t.isStringLiteral(prop.value)) {
|
|
500
|
-
method = canonicalizeMethod(prop.value.value);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const ev = evidenceFromLoc({
|
|
506
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
507
|
-
reason: `Client fetch("${url}")`
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
out.push({
|
|
511
|
-
method,
|
|
512
|
-
path: canonicalizePath(url),
|
|
513
|
-
source: fileRel,
|
|
514
|
-
confidence: "high",
|
|
515
|
-
evidence: ev ? [ev] : []
|
|
516
|
-
});
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// axios.get("/api/x")
|
|
521
|
-
if (isAxiosMember(callee)) {
|
|
522
|
-
const verb = callee.property.name.toUpperCase();
|
|
523
|
-
const a0 = p.node.arguments[0];
|
|
524
|
-
if (!t.isStringLiteral(a0)) return;
|
|
525
|
-
|
|
526
|
-
const url = a0.value;
|
|
527
|
-
if (!url.startsWith("/")) return;
|
|
528
|
-
|
|
529
|
-
const ev = evidenceFromLoc({
|
|
530
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
531
|
-
reason: `Client axios.${verb.toLowerCase()}("${url}")`
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
out.push({
|
|
535
|
-
method: canonicalizeMethod(verb),
|
|
536
|
-
path: canonicalizePath(url),
|
|
537
|
-
source: fileRel,
|
|
538
|
-
confidence: "high",
|
|
539
|
-
evidence: ev ? [ev] : []
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
return out;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// ---------- fastify entry detection ----------
|
|
550
|
-
function detectFastifyEntry(repoRoot) {
|
|
551
|
-
const candidates = [
|
|
552
|
-
"src/server.ts","src/server.js",
|
|
553
|
-
"server.ts","server.js",
|
|
554
|
-
"src/index.ts","src/index.js",
|
|
555
|
-
"index.ts","index.js"
|
|
556
|
-
];
|
|
557
|
-
for (const rel of candidates) {
|
|
558
|
-
const abs = path.join(repoRoot, rel);
|
|
559
|
-
if (exists(abs)) return rel;
|
|
560
|
-
}
|
|
561
|
-
return null;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// ---------- truthpack build/write ----------
|
|
565
|
-
async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
566
|
-
// Next.js routes (App Router + Pages Router)
|
|
567
|
-
const nextApp = await resolveNextAppApiRoutes(repoRoot);
|
|
568
|
-
const nextPages = await resolveNextPagesApiRoutes(repoRoot);
|
|
569
|
-
|
|
570
|
-
// Fastify routes (legacy detection)
|
|
571
|
-
const entryRel = fastifyEntry || detectFastifyEntry(repoRoot);
|
|
572
|
-
let fastify = { routes: [], gaps: [] };
|
|
573
|
-
if (entryRel) {
|
|
574
|
-
const entryAbs = path.isAbsolute(entryRel) ? entryRel : path.join(repoRoot, entryRel);
|
|
575
|
-
if (exists(entryAbs)) fastify = resolveFastifyRoutes(repoRoot, entryAbs);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Multi-framework route detection v2 (Express, Flask, FastAPI, Django, Hono, Koa, etc.)
|
|
579
|
-
const multiFramework = await resolveAllRoutes(repoRoot);
|
|
580
|
-
const detectedFrameworks = await detectFrameworks(repoRoot);
|
|
581
|
-
|
|
582
|
-
// Client refs (JS/TS fetch/axios + Python requests/httpx)
|
|
583
|
-
const clientRefs = await resolveClientRouteRefs(repoRoot);
|
|
584
|
-
const allClientRefs = [...clientRefs, ...multiFramework.clientRefs];
|
|
585
|
-
|
|
586
|
-
// Merge all server routes (dedupe by method+path)
|
|
587
|
-
const serverRoutesRaw = [...nextApp, ...nextPages, ...fastify.routes, ...multiFramework.routes];
|
|
588
|
-
const seenRoutes = new Set();
|
|
589
|
-
const server = [];
|
|
590
|
-
for (const r of serverRoutesRaw) {
|
|
591
|
-
const key = `${r.method}:${r.path}`;
|
|
592
|
-
if (!seenRoutes.has(key)) {
|
|
593
|
-
seenRoutes.add(key);
|
|
594
|
-
server.push(r);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Merge gaps
|
|
599
|
-
const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
|
|
600
|
-
|
|
601
|
-
// Env Truth v1
|
|
602
|
-
const env = await buildEnvTruth(repoRoot);
|
|
603
|
-
|
|
604
|
-
// Auth Truth v1
|
|
605
|
-
const auth = await buildAuthTruth(repoRoot, server);
|
|
606
|
-
|
|
607
|
-
// Billing Truth v1
|
|
608
|
-
const billing = await buildBillingTruth(repoRoot);
|
|
609
|
-
|
|
610
|
-
// Enforcement Truth v1
|
|
611
|
-
const enforcement = buildEnforcementTruth(repoRoot, server);
|
|
612
|
-
|
|
613
|
-
// Determine frameworks
|
|
614
|
-
const frameworks = new Set(["next", "fastify"]);
|
|
615
|
-
detectedFrameworks.forEach(f => frameworks.add(f));
|
|
616
|
-
server.forEach(r => r.framework && frameworks.add(r.framework));
|
|
617
|
-
|
|
618
|
-
const truthpack = {
|
|
619
|
-
meta: {
|
|
620
|
-
version: "2.0.0",
|
|
621
|
-
generatedAt: new Date().toISOString(),
|
|
622
|
-
repoRoot,
|
|
623
|
-
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" }
|
|
624
|
-
},
|
|
625
|
-
project: { frameworks: Array.from(frameworks), workspaces: [], entrypoints: [] },
|
|
626
|
-
routes: { server, clientRefs: allClientRefs, gaps: allGaps },
|
|
627
|
-
env,
|
|
628
|
-
auth,
|
|
629
|
-
billing,
|
|
630
|
-
enforcement
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
const hash = sha256(JSON.stringify(truthpack));
|
|
634
|
-
truthpack.index = { hashes: { truthpackHash: hash }, evidenceRefs: [] };
|
|
635
|
-
|
|
636
|
-
return truthpack;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function writeTruthpack(repoRoot, truthpack) {
|
|
640
|
-
const dir = path.join(repoRoot, ".vibecheck");
|
|
641
|
-
ensureDir(dir);
|
|
642
|
-
// Spec: .vibecheck/truthpack.json (not .vibecheck/truth/truthpack.json)
|
|
643
|
-
fs.writeFileSync(path.join(dir, "truthpack.json"), JSON.stringify(truthpack, null, 2));
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function loadTruthpack(repoRoot) {
|
|
647
|
-
// Spec path: .vibecheck/truthpack.json
|
|
648
|
-
const specPath = path.join(repoRoot, ".vibecheck", "truthpack.json");
|
|
649
|
-
// Legacy path: .vibecheck/truth/truthpack.json (backward compat)
|
|
650
|
-
const legacyPath = path.join(repoRoot, ".vibecheck", "truth", "truthpack.json");
|
|
651
|
-
|
|
652
|
-
try {
|
|
653
|
-
return JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
654
|
-
} catch {
|
|
655
|
-
// Try legacy path
|
|
656
|
-
try { return JSON.parse(fs.readFileSync(legacyPath, "utf8")); } catch { return null; }
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
module.exports = {
|
|
661
|
-
canonicalizeMethod,
|
|
662
|
-
canonicalizePath,
|
|
663
|
-
buildTruthpack,
|
|
664
|
-
writeTruthpack,
|
|
665
|
-
loadTruthpack,
|
|
666
|
-
detectFastifyEntry
|
|
667
|
-
};
|
|
1
|
+
// bin/runners/lib/truth.js
|
|
2
|
+
const fg = require("fast-glob");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const parser = require("@babel/parser");
|
|
7
|
+
const traverse = require("@babel/traverse").default;
|
|
8
|
+
const t = require("@babel/types");
|
|
9
|
+
|
|
10
|
+
// Env Truth v1
|
|
11
|
+
const { buildEnvTruth } = require("./env");
|
|
12
|
+
// Auth Truth v1
|
|
13
|
+
const { buildAuthTruth } = require("./auth-truth");
|
|
14
|
+
// Billing Truth v1
|
|
15
|
+
const { buildBillingTruth } = require("./billing");
|
|
16
|
+
// Enforcement Truth v1
|
|
17
|
+
const { buildEnforcementTruth } = require("./enforcement");
|
|
18
|
+
// Multi-framework route detection v2
|
|
19
|
+
const { resolveAllRoutes, detectFrameworks } = require("./route-detection");
|
|
20
|
+
|
|
21
|
+
// ---------- helpers ----------
|
|
22
|
+
function sha256(text) {
|
|
23
|
+
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalizeMethod(m) {
|
|
27
|
+
const u = String(m || "").toUpperCase();
|
|
28
|
+
if (u === "ALL" || u === "ANY" || u === "*") return "*";
|
|
29
|
+
return u;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canonicalizePath(p) {
|
|
33
|
+
let s = String(p || "").trim();
|
|
34
|
+
if (!s.startsWith("/")) s = "/" + s;
|
|
35
|
+
s = s.replace(/\/+/g, "/");
|
|
36
|
+
|
|
37
|
+
// Next dynamic segments
|
|
38
|
+
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
|
|
39
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
|
|
40
|
+
s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
|
|
41
|
+
|
|
42
|
+
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function joinPaths(prefix, p) {
|
|
47
|
+
const a = canonicalizePath(prefix || "/");
|
|
48
|
+
const b = canonicalizePath(p || "/");
|
|
49
|
+
if (a === "/") return b;
|
|
50
|
+
if (b === "/") return a;
|
|
51
|
+
return canonicalizePath(a + "/" + b);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseFile(code) {
|
|
55
|
+
return parser.parse(code, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function safeRead(fileAbs) {
|
|
59
|
+
return fs.readFileSync(fileAbs, "utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ensureDir(p) {
|
|
63
|
+
fs.mkdirSync(p, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
67
|
+
if (!loc) return null;
|
|
68
|
+
const lines = fs.readFileSync(fileAbs, "utf8").split(/\r?\n/);
|
|
69
|
+
const start = Math.max(1, loc.start?.line || 1);
|
|
70
|
+
const end = Math.max(start, loc.end?.line || start);
|
|
71
|
+
const snippet = lines.slice(start - 1, end).join("\n");
|
|
72
|
+
return {
|
|
73
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
74
|
+
file: fileRel,
|
|
75
|
+
lines: `${start}-${end}`,
|
|
76
|
+
snippetHash: sha256(snippet),
|
|
77
|
+
reason
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------- Next: app router API ----------
|
|
82
|
+
const HTTP_EXPORTS = new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]);
|
|
83
|
+
|
|
84
|
+
async function resolveNextAppApiRoutes(repoRoot) {
|
|
85
|
+
const files = await fg(["**/app/api/**/route.@(ts|js)"], {
|
|
86
|
+
cwd: repoRoot,
|
|
87
|
+
absolute: true,
|
|
88
|
+
ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const out = [];
|
|
92
|
+
|
|
93
|
+
for (const fileAbs of files) {
|
|
94
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
95
|
+
const idx = fileRel.indexOf("app/api/");
|
|
96
|
+
const sub = fileRel.slice(idx + "app/api/".length).replace(/\/route\.(ts|js)$/, "");
|
|
97
|
+
const routePath = canonicalizePath("/api/" + sub);
|
|
98
|
+
|
|
99
|
+
const code = safeRead(fileAbs);
|
|
100
|
+
let ast;
|
|
101
|
+
try { ast = parseFile(code); } catch { continue; }
|
|
102
|
+
|
|
103
|
+
const methods = [];
|
|
104
|
+
traverse(ast, {
|
|
105
|
+
ExportNamedDeclaration(p) {
|
|
106
|
+
const decl = p.node.declaration;
|
|
107
|
+
if (t.isFunctionDeclaration(decl) && decl.id?.name) {
|
|
108
|
+
const n = decl.id.name.toUpperCase();
|
|
109
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (methods.length === 0) {
|
|
115
|
+
out.push({ method: "*", path: routePath, handler: fileRel, confidence: "low", evidence: [] });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const m of methods) {
|
|
120
|
+
const ev = evidenceFromLoc({
|
|
121
|
+
fileAbs, fileRel, loc: m.loc,
|
|
122
|
+
reason: `Next app router export ${m.method}`
|
|
123
|
+
});
|
|
124
|
+
out.push({
|
|
125
|
+
method: m.method,
|
|
126
|
+
path: routePath,
|
|
127
|
+
handler: fileRel,
|
|
128
|
+
confidence: "high",
|
|
129
|
+
evidence: ev ? [ev] : []
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------- Next: pages router API ----------
|
|
138
|
+
async function resolveNextPagesApiRoutes(repoRoot) {
|
|
139
|
+
const files = await fg(["**/pages/api/**/*.@(ts|js)"], {
|
|
140
|
+
cwd: repoRoot,
|
|
141
|
+
absolute: true,
|
|
142
|
+
ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const fileAbs of files) {
|
|
147
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
148
|
+
const idx = fileRel.indexOf("pages/api/");
|
|
149
|
+
const sub = fileRel.slice(idx + "pages/api/".length).replace(/\.(ts|js)$/, "");
|
|
150
|
+
const routePath = canonicalizePath("/api/" + sub);
|
|
151
|
+
|
|
152
|
+
out.push({
|
|
153
|
+
method: "*",
|
|
154
|
+
path: routePath,
|
|
155
|
+
handler: fileRel,
|
|
156
|
+
confidence: "med",
|
|
157
|
+
evidence: []
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------- minimal relative module resolver ----------
|
|
164
|
+
function exists(p) {
|
|
165
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
166
|
+
}
|
|
167
|
+
function resolveRelativeModule(fromFileAbs, spec) {
|
|
168
|
+
if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
|
|
169
|
+
const base = path.resolve(path.dirname(fromFileAbs), spec);
|
|
170
|
+
const candidates = [
|
|
171
|
+
base,
|
|
172
|
+
base + ".ts",
|
|
173
|
+
base + ".js",
|
|
174
|
+
path.join(base, "index.ts"),
|
|
175
|
+
path.join(base, "index.js")
|
|
176
|
+
];
|
|
177
|
+
for (const c of candidates) if (exists(c)) return c;
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------- Fastify route extraction ----------
|
|
182
|
+
const FASTIFY_METHODS = new Set(["get","post","put","patch","delete","options","head","all"]);
|
|
183
|
+
|
|
184
|
+
function isFastifyMethod(name) {
|
|
185
|
+
return FASTIFY_METHODS.has(name);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function extractStringLiteral(node) {
|
|
189
|
+
return t.isStringLiteral(node) ? node.value : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractPrefixFromOpts(node) {
|
|
193
|
+
if (!t.isObjectExpression(node)) return null;
|
|
194
|
+
for (const p of node.properties) {
|
|
195
|
+
if (!t.isObjectProperty(p)) continue;
|
|
196
|
+
const key =
|
|
197
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
198
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
199
|
+
null;
|
|
200
|
+
if (key === "prefix" && t.isStringLiteral(p.value)) return p.value.value;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function extractRouteObject(objExpr) {
|
|
206
|
+
let url = null;
|
|
207
|
+
let methods = [];
|
|
208
|
+
let hasHandler = false;
|
|
209
|
+
const hooks = [];
|
|
210
|
+
|
|
211
|
+
for (const p of objExpr.properties) {
|
|
212
|
+
if (!t.isObjectProperty(p)) continue;
|
|
213
|
+
|
|
214
|
+
const key =
|
|
215
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
216
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
217
|
+
null;
|
|
218
|
+
if (!key) continue;
|
|
219
|
+
|
|
220
|
+
if (key === "url" && t.isStringLiteral(p.value)) url = p.value.value;
|
|
221
|
+
|
|
222
|
+
if (key === "method") {
|
|
223
|
+
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
224
|
+
if (t.isArrayExpression(p.value)) {
|
|
225
|
+
methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (key === "handler") hasHandler = true;
|
|
230
|
+
if (["preHandler","onRequest","preValidation","preSerialization"].includes(key)) hooks.push(key);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { url, methods, hasHandler, hooks };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveFastifyRoutes(repoRoot, entryAbs) {
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
const routes = [];
|
|
239
|
+
const gaps = [];
|
|
240
|
+
|
|
241
|
+
function scanFile(fileAbs, prefix) {
|
|
242
|
+
if (!fileAbs || seen.has(fileAbs)) return;
|
|
243
|
+
seen.add(fileAbs);
|
|
244
|
+
|
|
245
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
246
|
+
const code = safeRead(fileAbs);
|
|
247
|
+
|
|
248
|
+
let ast;
|
|
249
|
+
try { ast = parseFile(code); } catch { return; }
|
|
250
|
+
|
|
251
|
+
// best-effort: fastify instance identifiers
|
|
252
|
+
const fastifyNames = new Set(["fastify"]);
|
|
253
|
+
|
|
254
|
+
traverse(ast, {
|
|
255
|
+
VariableDeclarator(p) {
|
|
256
|
+
if (!t.isIdentifier(p.node.id)) return;
|
|
257
|
+
const id = p.node.id.name;
|
|
258
|
+
const init = p.node.init;
|
|
259
|
+
if (!init) return;
|
|
260
|
+
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
261
|
+
const cal = init.callee.name;
|
|
262
|
+
if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// helper: resolve imports for register(pluginIdent,...)
|
|
268
|
+
function resolveImportSpecForLocal(localName) {
|
|
269
|
+
let spec = null;
|
|
270
|
+
|
|
271
|
+
traverse(ast, {
|
|
272
|
+
ImportDeclaration(ip) {
|
|
273
|
+
for (const s of ip.node.specifiers) {
|
|
274
|
+
if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
|
|
275
|
+
spec = ip.node.source.value;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
VariableDeclarator(vp) {
|
|
280
|
+
if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
|
|
281
|
+
const init = vp.node.init;
|
|
282
|
+
if (!t.isCallExpression(init)) return;
|
|
283
|
+
if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
|
|
284
|
+
const a0 = init.arguments[0];
|
|
285
|
+
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return spec;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
traverse(ast, {
|
|
293
|
+
CallExpression(p) {
|
|
294
|
+
const callee = p.node.callee;
|
|
295
|
+
if (!t.isMemberExpression(callee)) return;
|
|
296
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
297
|
+
|
|
298
|
+
const obj = callee.object.name;
|
|
299
|
+
const prop = callee.property.name;
|
|
300
|
+
|
|
301
|
+
if (!fastifyNames.has(obj)) return;
|
|
302
|
+
|
|
303
|
+
// fastify.get('/x', ...)
|
|
304
|
+
if (isFastifyMethod(prop)) {
|
|
305
|
+
const routeStr = extractStringLiteral(p.node.arguments[0]);
|
|
306
|
+
if (!routeStr) return;
|
|
307
|
+
|
|
308
|
+
const fullPath = joinPaths(prefix, routeStr);
|
|
309
|
+
const method = canonicalizeMethod(prop);
|
|
310
|
+
|
|
311
|
+
const ev = evidenceFromLoc({
|
|
312
|
+
fileAbs, fileRel, loc: p.node.loc,
|
|
313
|
+
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
routes.push({
|
|
317
|
+
method,
|
|
318
|
+
path: fullPath,
|
|
319
|
+
handler: fileRel,
|
|
320
|
+
confidence: "med",
|
|
321
|
+
evidence: ev ? [ev] : []
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// fastify.route({ method, url, handler })
|
|
327
|
+
if (prop === "route") {
|
|
328
|
+
const arg0 = p.node.arguments[0];
|
|
329
|
+
if (!t.isObjectExpression(arg0)) return;
|
|
330
|
+
|
|
331
|
+
const r = extractRouteObject(arg0);
|
|
332
|
+
if (!r.url) return;
|
|
333
|
+
|
|
334
|
+
const fullPath = joinPaths(prefix, r.url);
|
|
335
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
336
|
+
|
|
337
|
+
const ev = evidenceFromLoc({
|
|
338
|
+
fileAbs, fileRel, loc: p.node.loc,
|
|
339
|
+
reason: `Fastify.route({ url: "${r.url}" })`
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
for (const m of ms) {
|
|
343
|
+
routes.push({
|
|
344
|
+
method: m,
|
|
345
|
+
path: fullPath,
|
|
346
|
+
handler: fileRel,
|
|
347
|
+
hooks: r.hooks,
|
|
348
|
+
confidence: r.hasHandler ? "med" : "low",
|
|
349
|
+
evidence: ev ? [ev] : []
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// fastify.register(plugin, { prefix })
|
|
356
|
+
if (prop === "register") {
|
|
357
|
+
const pluginArg = p.node.arguments[0];
|
|
358
|
+
const optsArg = p.node.arguments[1];
|
|
359
|
+
const childPrefixRaw = extractPrefixFromOpts(optsArg);
|
|
360
|
+
const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
|
|
361
|
+
|
|
362
|
+
// inline plugin
|
|
363
|
+
if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
|
|
364
|
+
const param0 = pluginArg.params[0];
|
|
365
|
+
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
366
|
+
|
|
367
|
+
// traverse just the plugin body (best effort)
|
|
368
|
+
traverse(pluginArg.body, {
|
|
369
|
+
CallExpression(pp) {
|
|
370
|
+
const c = pp.node.callee;
|
|
371
|
+
if (!t.isMemberExpression(c)) return;
|
|
372
|
+
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
373
|
+
if (c.object.name !== innerName) return;
|
|
374
|
+
|
|
375
|
+
const pr = c.property.name;
|
|
376
|
+
|
|
377
|
+
if (isFastifyMethod(pr)) {
|
|
378
|
+
const rs = extractStringLiteral(pp.node.arguments[0]);
|
|
379
|
+
if (!rs) return;
|
|
380
|
+
const fullPath = joinPaths(childPrefix, rs);
|
|
381
|
+
const method = canonicalizeMethod(pr);
|
|
382
|
+
|
|
383
|
+
const ev = evidenceFromLoc({
|
|
384
|
+
fileAbs, fileRel, loc: pp.node.loc,
|
|
385
|
+
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
routes.push({ method, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (pr === "route") {
|
|
392
|
+
const a0 = pp.node.arguments[0];
|
|
393
|
+
if (!t.isObjectExpression(a0)) return;
|
|
394
|
+
const r = extractRouteObject(a0);
|
|
395
|
+
if (!r.url) return;
|
|
396
|
+
const fullPath = joinPaths(childPrefix, r.url);
|
|
397
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
398
|
+
|
|
399
|
+
const ev = evidenceFromLoc({
|
|
400
|
+
fileAbs, fileRel, loc: pp.node.loc,
|
|
401
|
+
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
for (const m of ms) routes.push({ method: m, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}, p.scope, p);
|
|
408
|
+
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// imported plugin identifier
|
|
413
|
+
if (t.isIdentifier(pluginArg)) {
|
|
414
|
+
const localName = pluginArg.name;
|
|
415
|
+
const spec = resolveImportSpecForLocal(localName);
|
|
416
|
+
|
|
417
|
+
if (!spec) {
|
|
418
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const resolved = resolveRelativeModule(fileAbs, spec);
|
|
423
|
+
if (!resolved) {
|
|
424
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
scanFile(resolved, childPrefix);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
scanFile(entryAbs, "/");
|
|
436
|
+
return { routes, gaps };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---------- client refs (fetch + axios string literal only) ----------
|
|
440
|
+
function isAxiosMember(node) {
|
|
441
|
+
return t.isMemberExpression(node) &&
|
|
442
|
+
t.isIdentifier(node.object) &&
|
|
443
|
+
t.isIdentifier(node.property) &&
|
|
444
|
+
["get","post","put","patch","delete"].includes(node.property.name);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function resolveClientRouteRefs(repoRoot) {
|
|
448
|
+
const files = await fg(["**/*.{ts,tsx,js,jsx}"], {
|
|
449
|
+
cwd: repoRoot,
|
|
450
|
+
absolute: true,
|
|
451
|
+
ignore: [
|
|
452
|
+
"**/node_modules/**",
|
|
453
|
+
"**/.next/**",
|
|
454
|
+
"**/dist/**",
|
|
455
|
+
"**/build/**",
|
|
456
|
+
"**/test/**",
|
|
457
|
+
"**/tests/**",
|
|
458
|
+
"**/__tests__/**",
|
|
459
|
+
"**/*.test.*",
|
|
460
|
+
"**/*.spec.*",
|
|
461
|
+
"**/jest.setup.*",
|
|
462
|
+
"**/jest.config.*",
|
|
463
|
+
"**/*.mock.*",
|
|
464
|
+
"**/mocks/**",
|
|
465
|
+
"**/fixtures/**"
|
|
466
|
+
]
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const out = [];
|
|
470
|
+
|
|
471
|
+
for (const fileAbs of files) {
|
|
472
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
473
|
+
const code = safeRead(fileAbs);
|
|
474
|
+
|
|
475
|
+
let ast;
|
|
476
|
+
try { ast = parseFile(code); } catch { continue; }
|
|
477
|
+
|
|
478
|
+
traverse(ast, {
|
|
479
|
+
CallExpression(p) {
|
|
480
|
+
const callee = p.node.callee;
|
|
481
|
+
|
|
482
|
+
// fetch("/api/x", { method: "POST" })
|
|
483
|
+
if (t.isIdentifier(callee) && callee.name === "fetch") {
|
|
484
|
+
const a0 = p.node.arguments[0];
|
|
485
|
+
if (!t.isStringLiteral(a0)) return;
|
|
486
|
+
|
|
487
|
+
const url = a0.value;
|
|
488
|
+
if (!url.startsWith("/")) return;
|
|
489
|
+
|
|
490
|
+
let method = "*";
|
|
491
|
+
const a1 = p.node.arguments[1];
|
|
492
|
+
if (t.isObjectExpression(a1)) {
|
|
493
|
+
for (const prop of a1.properties) {
|
|
494
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
495
|
+
const key =
|
|
496
|
+
t.isIdentifier(prop.key) ? prop.key.name :
|
|
497
|
+
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
498
|
+
null;
|
|
499
|
+
if (key === "method" && t.isStringLiteral(prop.value)) {
|
|
500
|
+
method = canonicalizeMethod(prop.value.value);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const ev = evidenceFromLoc({
|
|
506
|
+
fileAbs, fileRel, loc: p.node.loc,
|
|
507
|
+
reason: `Client fetch("${url}")`
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
out.push({
|
|
511
|
+
method,
|
|
512
|
+
path: canonicalizePath(url),
|
|
513
|
+
source: fileRel,
|
|
514
|
+
confidence: "high",
|
|
515
|
+
evidence: ev ? [ev] : []
|
|
516
|
+
});
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// axios.get("/api/x")
|
|
521
|
+
if (isAxiosMember(callee)) {
|
|
522
|
+
const verb = callee.property.name.toUpperCase();
|
|
523
|
+
const a0 = p.node.arguments[0];
|
|
524
|
+
if (!t.isStringLiteral(a0)) return;
|
|
525
|
+
|
|
526
|
+
const url = a0.value;
|
|
527
|
+
if (!url.startsWith("/")) return;
|
|
528
|
+
|
|
529
|
+
const ev = evidenceFromLoc({
|
|
530
|
+
fileAbs, fileRel, loc: p.node.loc,
|
|
531
|
+
reason: `Client axios.${verb.toLowerCase()}("${url}")`
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
out.push({
|
|
535
|
+
method: canonicalizeMethod(verb),
|
|
536
|
+
path: canonicalizePath(url),
|
|
537
|
+
source: fileRel,
|
|
538
|
+
confidence: "high",
|
|
539
|
+
evidence: ev ? [ev] : []
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ---------- fastify entry detection ----------
|
|
550
|
+
function detectFastifyEntry(repoRoot) {
|
|
551
|
+
const candidates = [
|
|
552
|
+
"src/server.ts","src/server.js",
|
|
553
|
+
"server.ts","server.js",
|
|
554
|
+
"src/index.ts","src/index.js",
|
|
555
|
+
"index.ts","index.js"
|
|
556
|
+
];
|
|
557
|
+
for (const rel of candidates) {
|
|
558
|
+
const abs = path.join(repoRoot, rel);
|
|
559
|
+
if (exists(abs)) return rel;
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---------- truthpack build/write ----------
|
|
565
|
+
async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
566
|
+
// Next.js routes (App Router + Pages Router)
|
|
567
|
+
const nextApp = await resolveNextAppApiRoutes(repoRoot);
|
|
568
|
+
const nextPages = await resolveNextPagesApiRoutes(repoRoot);
|
|
569
|
+
|
|
570
|
+
// Fastify routes (legacy detection)
|
|
571
|
+
const entryRel = fastifyEntry || detectFastifyEntry(repoRoot);
|
|
572
|
+
let fastify = { routes: [], gaps: [] };
|
|
573
|
+
if (entryRel) {
|
|
574
|
+
const entryAbs = path.isAbsolute(entryRel) ? entryRel : path.join(repoRoot, entryRel);
|
|
575
|
+
if (exists(entryAbs)) fastify = resolveFastifyRoutes(repoRoot, entryAbs);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Multi-framework route detection v2 (Express, Flask, FastAPI, Django, Hono, Koa, etc.)
|
|
579
|
+
const multiFramework = await resolveAllRoutes(repoRoot);
|
|
580
|
+
const detectedFrameworks = await detectFrameworks(repoRoot);
|
|
581
|
+
|
|
582
|
+
// Client refs (JS/TS fetch/axios + Python requests/httpx)
|
|
583
|
+
const clientRefs = await resolveClientRouteRefs(repoRoot);
|
|
584
|
+
const allClientRefs = [...clientRefs, ...multiFramework.clientRefs];
|
|
585
|
+
|
|
586
|
+
// Merge all server routes (dedupe by method+path)
|
|
587
|
+
const serverRoutesRaw = [...nextApp, ...nextPages, ...fastify.routes, ...multiFramework.routes];
|
|
588
|
+
const seenRoutes = new Set();
|
|
589
|
+
const server = [];
|
|
590
|
+
for (const r of serverRoutesRaw) {
|
|
591
|
+
const key = `${r.method}:${r.path}`;
|
|
592
|
+
if (!seenRoutes.has(key)) {
|
|
593
|
+
seenRoutes.add(key);
|
|
594
|
+
server.push(r);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Merge gaps
|
|
599
|
+
const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
|
|
600
|
+
|
|
601
|
+
// Env Truth v1
|
|
602
|
+
const env = await buildEnvTruth(repoRoot);
|
|
603
|
+
|
|
604
|
+
// Auth Truth v1
|
|
605
|
+
const auth = await buildAuthTruth(repoRoot, server);
|
|
606
|
+
|
|
607
|
+
// Billing Truth v1
|
|
608
|
+
const billing = await buildBillingTruth(repoRoot);
|
|
609
|
+
|
|
610
|
+
// Enforcement Truth v1
|
|
611
|
+
const enforcement = buildEnforcementTruth(repoRoot, server);
|
|
612
|
+
|
|
613
|
+
// Determine frameworks
|
|
614
|
+
const frameworks = new Set(["next", "fastify"]);
|
|
615
|
+
detectedFrameworks.forEach(f => frameworks.add(f));
|
|
616
|
+
server.forEach(r => r.framework && frameworks.add(r.framework));
|
|
617
|
+
|
|
618
|
+
const truthpack = {
|
|
619
|
+
meta: {
|
|
620
|
+
version: "2.0.0",
|
|
621
|
+
generatedAt: new Date().toISOString(),
|
|
622
|
+
repoRoot,
|
|
623
|
+
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" }
|
|
624
|
+
},
|
|
625
|
+
project: { frameworks: Array.from(frameworks), workspaces: [], entrypoints: [] },
|
|
626
|
+
routes: { server, clientRefs: allClientRefs, gaps: allGaps },
|
|
627
|
+
env,
|
|
628
|
+
auth,
|
|
629
|
+
billing,
|
|
630
|
+
enforcement
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const hash = sha256(JSON.stringify(truthpack));
|
|
634
|
+
truthpack.index = { hashes: { truthpackHash: hash }, evidenceRefs: [] };
|
|
635
|
+
|
|
636
|
+
return truthpack;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function writeTruthpack(repoRoot, truthpack) {
|
|
640
|
+
const dir = path.join(repoRoot, ".vibecheck");
|
|
641
|
+
ensureDir(dir);
|
|
642
|
+
// Spec: .vibecheck/truthpack.json (not .vibecheck/truth/truthpack.json)
|
|
643
|
+
fs.writeFileSync(path.join(dir, "truthpack.json"), JSON.stringify(truthpack, null, 2));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function loadTruthpack(repoRoot) {
|
|
647
|
+
// Spec path: .vibecheck/truthpack.json
|
|
648
|
+
const specPath = path.join(repoRoot, ".vibecheck", "truthpack.json");
|
|
649
|
+
// Legacy path: .vibecheck/truth/truthpack.json (backward compat)
|
|
650
|
+
const legacyPath = path.join(repoRoot, ".vibecheck", "truth", "truthpack.json");
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
return JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
654
|
+
} catch {
|
|
655
|
+
// Try legacy path
|
|
656
|
+
try { return JSON.parse(fs.readFileSync(legacyPath, "utf8")); } catch { return null; }
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
module.exports = {
|
|
661
|
+
canonicalizeMethod,
|
|
662
|
+
canonicalizePath,
|
|
663
|
+
buildTruthpack,
|
|
664
|
+
writeTruthpack,
|
|
665
|
+
loadTruthpack,
|
|
666
|
+
detectFastifyEntry
|
|
667
|
+
};
|