@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,990 +1,990 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client Call Extractor v2
|
|
3
|
-
*
|
|
4
|
-
* Extracts client-side API calls: fetch, axios, tRPC, GraphQL, Server Actions, SDK calls.
|
|
5
|
-
* Links them to the Reality Proof Graph for dead UI detection.
|
|
6
|
-
*
|
|
7
|
-
* Output: ClientCall records with evidence, confidence, and canonical paths.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
"use strict";
|
|
11
|
-
|
|
12
|
-
const fs = require("fs");
|
|
13
|
-
const path = require("path");
|
|
14
|
-
const fg = require("fast-glob");
|
|
15
|
-
const crypto = require("crypto");
|
|
16
|
-
const parser = require("@babel/parser");
|
|
17
|
-
const traverse = require("@babel/traverse").default;
|
|
18
|
-
const t = require("@babel/types");
|
|
19
|
-
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// TYPES AND CONSTANTS
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
|
-
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
25
|
-
|
|
26
|
-
const CALL_KINDS = {
|
|
27
|
-
HTTP: "http",
|
|
28
|
-
TRPC: "trpc",
|
|
29
|
-
GRAPHQL: "graphql",
|
|
30
|
-
SERVER_ACTION: "server_action",
|
|
31
|
-
SDK: "sdk",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const RUNTIMES = {
|
|
35
|
-
CLIENT: "client",
|
|
36
|
-
SERVER: "server",
|
|
37
|
-
SHARED: "shared",
|
|
38
|
-
UNKNOWN: "unknown",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// =============================================================================
|
|
42
|
-
// MAIN EXTRACTION
|
|
43
|
-
// =============================================================================
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Extract all client calls from a project
|
|
47
|
-
*/
|
|
48
|
-
function extractClientCalls(projectRoot, options = {}) {
|
|
49
|
-
const { include = [], exclude = [] } = options;
|
|
50
|
-
|
|
51
|
-
// Find source files
|
|
52
|
-
const defaultInclude = ["**/*.{ts,tsx,js,jsx,mts,cts}"];
|
|
53
|
-
const defaultExclude = [
|
|
54
|
-
"**/node_modules/**",
|
|
55
|
-
"**/.next/**",
|
|
56
|
-
"**/dist/**",
|
|
57
|
-
"**/build/**",
|
|
58
|
-
"**/coverage/**",
|
|
59
|
-
"**/*.min.js",
|
|
60
|
-
"**/*.d.ts",
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
const patterns = include.length > 0 ? include : defaultInclude;
|
|
64
|
-
const ignorePatterns = [...defaultExclude, ...exclude];
|
|
65
|
-
|
|
66
|
-
const files = fg.sync(patterns, {
|
|
67
|
-
cwd: projectRoot,
|
|
68
|
-
absolute: true,
|
|
69
|
-
ignore: ignorePatterns,
|
|
70
|
-
onlyFiles: true,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const calls = [];
|
|
74
|
-
const wrappers = [];
|
|
75
|
-
const uiBindings = [];
|
|
76
|
-
const errors = [];
|
|
77
|
-
|
|
78
|
-
// Pass 1: Extract wrappers (axios instances, custom request helpers)
|
|
79
|
-
for (const file of files) {
|
|
80
|
-
try {
|
|
81
|
-
const content = fs.readFileSync(file, "utf8");
|
|
82
|
-
const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
83
|
-
const fileWrappers = extractWrappers(content, relPath, projectRoot);
|
|
84
|
-
wrappers.push(...fileWrappers);
|
|
85
|
-
} catch (err) {
|
|
86
|
-
errors.push({ file, phase: "wrappers", error: err.message });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Build wrapper lookup
|
|
91
|
-
const wrapperMap = new Map();
|
|
92
|
-
for (const w of wrappers) {
|
|
93
|
-
wrapperMap.set(w.name, w);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Pass 2: Extract calls
|
|
97
|
-
for (const file of files) {
|
|
98
|
-
try {
|
|
99
|
-
const content = fs.readFileSync(file, "utf8");
|
|
100
|
-
const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
101
|
-
const runtime = inferRuntime(content, relPath);
|
|
102
|
-
|
|
103
|
-
const fileCalls = extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot);
|
|
104
|
-
calls.push(...fileCalls);
|
|
105
|
-
|
|
106
|
-
const bindings = extractUIBindings(content, relPath, fileCalls);
|
|
107
|
-
uiBindings.push(...bindings);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
errors.push({ file, phase: "calls", error: err.message });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
calls,
|
|
115
|
-
wrappers,
|
|
116
|
-
uiBindings,
|
|
117
|
-
errors,
|
|
118
|
-
stats: {
|
|
119
|
-
filesScanned: files.length,
|
|
120
|
-
callsFound: calls.length,
|
|
121
|
-
wrappersFound: wrappers.length,
|
|
122
|
-
bindingsFound: uiBindings.length,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Extract calls from a single file
|
|
129
|
-
*/
|
|
130
|
-
function extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot) {
|
|
131
|
-
const calls = [];
|
|
132
|
-
|
|
133
|
-
let ast;
|
|
134
|
-
try {
|
|
135
|
-
ast = parser.parse(content, {
|
|
136
|
-
sourceType: "module",
|
|
137
|
-
plugins: ["typescript", "jsx", "decorators-legacy"],
|
|
138
|
-
errorRecovery: true,
|
|
139
|
-
});
|
|
140
|
-
} catch {
|
|
141
|
-
return calls;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
traverse(ast, {
|
|
145
|
-
CallExpression(path) {
|
|
146
|
-
// fetch() calls
|
|
147
|
-
const fetchCall = extractFetchCall(path, content, relPath, runtime);
|
|
148
|
-
if (fetchCall) {
|
|
149
|
-
calls.push(fetchCall);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// axios calls
|
|
154
|
-
const axiosCall = extractAxiosCall(path, content, relPath, runtime, wrapperMap);
|
|
155
|
-
if (axiosCall) {
|
|
156
|
-
calls.push(axiosCall);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// tRPC calls
|
|
161
|
-
const trpcCall = extractTRPCCall(path, content, relPath, runtime);
|
|
162
|
-
if (trpcCall) {
|
|
163
|
-
calls.push(trpcCall);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// GraphQL calls
|
|
168
|
-
const gqlCall = extractGraphQLCall(path, content, relPath, runtime);
|
|
169
|
-
if (gqlCall) {
|
|
170
|
-
calls.push(gqlCall);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Wrapper calls
|
|
175
|
-
const wrapperCall = extractWrapperCall(path, content, relPath, runtime, wrapperMap);
|
|
176
|
-
if (wrapperCall) {
|
|
177
|
-
calls.push(wrapperCall);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
// Server Actions (form action={...})
|
|
183
|
-
JSXAttribute(path) {
|
|
184
|
-
if (path.node.name.name === "action" && t.isJSXExpressionContainer(path.node.value)) {
|
|
185
|
-
const actionCall = extractServerAction(path, content, relPath);
|
|
186
|
-
if (actionCall) {
|
|
187
|
-
calls.push(actionCall);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
return calls;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// =============================================================================
|
|
197
|
-
// FETCH EXTRACTION
|
|
198
|
-
// =============================================================================
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Extract fetch() call
|
|
202
|
-
*/
|
|
203
|
-
function extractFetchCall(path, content, relPath, runtime) {
|
|
204
|
-
const { node } = path;
|
|
205
|
-
|
|
206
|
-
// Check if callee is 'fetch'
|
|
207
|
-
if (!t.isIdentifier(node.callee, { name: "fetch" })) {
|
|
208
|
-
// Also check for fetch(new Request(...))
|
|
209
|
-
if (t.isNewExpression(node.callee) && t.isIdentifier(node.callee.callee, { name: "Request" })) {
|
|
210
|
-
return extractRequestCall(path, content, relPath, runtime);
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const urlArg = node.arguments[0];
|
|
216
|
-
const initArg = node.arguments[1];
|
|
217
|
-
|
|
218
|
-
if (!urlArg) return null;
|
|
219
|
-
|
|
220
|
-
// Extract URL
|
|
221
|
-
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
222
|
-
if (!urlInfo.value) return null;
|
|
223
|
-
|
|
224
|
-
// Extract method
|
|
225
|
-
const method = extractMethodFromInit(initArg) || "GET";
|
|
226
|
-
|
|
227
|
-
// Extract other info
|
|
228
|
-
const expectsJson = extractExpectsJson(initArg, path);
|
|
229
|
-
const authHint = extractAuthHint(initArg);
|
|
230
|
-
|
|
231
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
232
|
-
|
|
233
|
-
return createClientCall({
|
|
234
|
-
kind: CALL_KINDS.HTTP,
|
|
235
|
-
runtime,
|
|
236
|
-
method,
|
|
237
|
-
urlTemplate: urlInfo.value,
|
|
238
|
-
canonicalPath: canonicalizeUrl(urlInfo.value),
|
|
239
|
-
expectsJson,
|
|
240
|
-
authHint,
|
|
241
|
-
confidence: urlInfo.confidence,
|
|
242
|
-
file: relPath,
|
|
243
|
-
lines: `${lineNum}-${lineNum + 5}`,
|
|
244
|
-
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
245
|
-
reason: `fetch('${urlInfo.value.slice(0, 50)}') call`,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Extract new Request() call inside fetch
|
|
251
|
-
*/
|
|
252
|
-
function extractRequestCall(path, content, relPath, runtime) {
|
|
253
|
-
const { node } = path;
|
|
254
|
-
const requestNode = node.callee;
|
|
255
|
-
|
|
256
|
-
if (!t.isNewExpression(requestNode)) return null;
|
|
257
|
-
|
|
258
|
-
const urlArg = requestNode.arguments[0];
|
|
259
|
-
const initArg = requestNode.arguments[1];
|
|
260
|
-
|
|
261
|
-
if (!urlArg) return null;
|
|
262
|
-
|
|
263
|
-
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
264
|
-
if (!urlInfo.value) return null;
|
|
265
|
-
|
|
266
|
-
const method = extractMethodFromInit(initArg) || "GET";
|
|
267
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
268
|
-
|
|
269
|
-
return createClientCall({
|
|
270
|
-
kind: CALL_KINDS.HTTP,
|
|
271
|
-
runtime,
|
|
272
|
-
method,
|
|
273
|
-
urlTemplate: urlInfo.value,
|
|
274
|
-
canonicalPath: canonicalizeUrl(urlInfo.value),
|
|
275
|
-
expectsJson: false,
|
|
276
|
-
authHint: "unknown",
|
|
277
|
-
confidence: urlInfo.confidence,
|
|
278
|
-
file: relPath,
|
|
279
|
-
lines: `${lineNum}-${lineNum + 3}`,
|
|
280
|
-
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
281
|
-
reason: `fetch(new Request('${urlInfo.value.slice(0, 50)}')) call`,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// =============================================================================
|
|
286
|
-
// AXIOS EXTRACTION
|
|
287
|
-
// =============================================================================
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Extract axios call
|
|
291
|
-
*/
|
|
292
|
-
function extractAxiosCall(path, content, relPath, runtime, wrapperMap) {
|
|
293
|
-
const { node } = path;
|
|
294
|
-
|
|
295
|
-
// Pattern A: axios.get/post/etc(url)
|
|
296
|
-
if (t.isMemberExpression(node.callee)) {
|
|
297
|
-
const obj = node.callee.object;
|
|
298
|
-
const prop = node.callee.property;
|
|
299
|
-
|
|
300
|
-
if (t.isIdentifier(obj, { name: "axios" }) && t.isIdentifier(prop)) {
|
|
301
|
-
const methodName = prop.name.toUpperCase();
|
|
302
|
-
if (HTTP_METHODS.includes(methodName) || methodName === "REQUEST") {
|
|
303
|
-
const urlArg = node.arguments[0];
|
|
304
|
-
if (!urlArg) return null;
|
|
305
|
-
|
|
306
|
-
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
307
|
-
if (!urlInfo.value) return null;
|
|
308
|
-
|
|
309
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
310
|
-
|
|
311
|
-
return createClientCall({
|
|
312
|
-
kind: CALL_KINDS.HTTP,
|
|
313
|
-
runtime,
|
|
314
|
-
method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
|
|
315
|
-
urlTemplate: urlInfo.value,
|
|
316
|
-
canonicalPath: canonicalizeUrl(urlInfo.value),
|
|
317
|
-
expectsJson: true,
|
|
318
|
-
authHint: "unknown",
|
|
319
|
-
confidence: urlInfo.confidence,
|
|
320
|
-
file: relPath,
|
|
321
|
-
lines: `${lineNum}-${lineNum + 3}`,
|
|
322
|
-
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
323
|
-
reason: `axios.${prop.name}('${urlInfo.value.slice(0, 50)}') call`,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Pattern B: axios({ method, url })
|
|
330
|
-
if (t.isIdentifier(node.callee, { name: "axios" })) {
|
|
331
|
-
const configArg = node.arguments[0];
|
|
332
|
-
if (t.isObjectExpression(configArg)) {
|
|
333
|
-
const config = extractObjectLiteralProps(configArg);
|
|
334
|
-
if (config.url) {
|
|
335
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
336
|
-
|
|
337
|
-
return createClientCall({
|
|
338
|
-
kind: CALL_KINDS.HTTP,
|
|
339
|
-
runtime,
|
|
340
|
-
method: (config.method || "GET").toUpperCase(),
|
|
341
|
-
urlTemplate: config.url,
|
|
342
|
-
canonicalPath: canonicalizeUrl(combineBaseUrl(config.baseURL, config.url)),
|
|
343
|
-
expectsJson: true,
|
|
344
|
-
authHint: "unknown",
|
|
345
|
-
confidence: "medium",
|
|
346
|
-
file: relPath,
|
|
347
|
-
lines: `${lineNum}-${lineNum + 5}`,
|
|
348
|
-
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
349
|
-
reason: `axios({ url: '${config.url.slice(0, 50)}' }) call`,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// =============================================================================
|
|
359
|
-
// TRPC EXTRACTION
|
|
360
|
-
// =============================================================================
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Extract tRPC call
|
|
364
|
-
*/
|
|
365
|
-
function extractTRPCCall(path, content, relPath, runtime) {
|
|
366
|
-
const { node } = path;
|
|
367
|
-
|
|
368
|
-
// Pattern: trpc.user.get.useQuery() or trpc.billing.portal.useMutation()
|
|
369
|
-
if (t.isMemberExpression(node.callee)) {
|
|
370
|
-
const calleeCode = extractMemberChain(node.callee);
|
|
371
|
-
|
|
372
|
-
// Check for tRPC patterns
|
|
373
|
-
const trpcMatch = calleeCode.match(/^(?:trpc|api)\.(.+?)\.(useQuery|useMutation|query|mutate)$/);
|
|
374
|
-
if (trpcMatch) {
|
|
375
|
-
const procedure = trpcMatch[1];
|
|
376
|
-
const operationType = trpcMatch[2].includes("Mutation") || trpcMatch[2] === "mutate" ? "mutation" : "query";
|
|
377
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
378
|
-
|
|
379
|
-
return createClientCall({
|
|
380
|
-
kind: CALL_KINDS.TRPC,
|
|
381
|
-
runtime,
|
|
382
|
-
method: "POST",
|
|
383
|
-
urlTemplate: `/api/trpc/${procedure}`,
|
|
384
|
-
canonicalPath: `/api/trpc/${procedure}`,
|
|
385
|
-
expectsJson: true,
|
|
386
|
-
authHint: "unknown",
|
|
387
|
-
confidence: "medium",
|
|
388
|
-
file: relPath,
|
|
389
|
-
lines: `${lineNum}-${lineNum + 3}`,
|
|
390
|
-
snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
|
|
391
|
-
reason: `tRPC ${operationType}: ${procedure}`,
|
|
392
|
-
meta: {
|
|
393
|
-
procedure,
|
|
394
|
-
operationType,
|
|
395
|
-
},
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// =============================================================================
|
|
404
|
-
// GRAPHQL EXTRACTION
|
|
405
|
-
// =============================================================================
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Extract GraphQL call
|
|
409
|
-
*/
|
|
410
|
-
function extractGraphQLCall(path, content, relPath, runtime) {
|
|
411
|
-
const { node } = path;
|
|
412
|
-
|
|
413
|
-
// Pattern: useQuery(gql`...`) or useMutation(gql`...`)
|
|
414
|
-
if (t.isIdentifier(node.callee)) {
|
|
415
|
-
const calleeName = node.callee.name;
|
|
416
|
-
|
|
417
|
-
if (calleeName === "useQuery" || calleeName === "useMutation" || calleeName === "request") {
|
|
418
|
-
const queryArg = node.arguments[0];
|
|
419
|
-
let operationName = null;
|
|
420
|
-
|
|
421
|
-
// Try to extract operation name from gql template
|
|
422
|
-
if (t.isTaggedTemplateExpression(queryArg)) {
|
|
423
|
-
const template = queryArg.quasi.quasis.map(q => q.value.raw).join("");
|
|
424
|
-
const opMatch = template.match(/(?:query|mutation|subscription)\s+(\w+)/);
|
|
425
|
-
if (opMatch) {
|
|
426
|
-
operationName = opMatch[1];
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
431
|
-
|
|
432
|
-
return createClientCall({
|
|
433
|
-
kind: CALL_KINDS.GRAPHQL,
|
|
434
|
-
runtime,
|
|
435
|
-
method: "POST",
|
|
436
|
-
urlTemplate: "/graphql",
|
|
437
|
-
canonicalPath: "/graphql",
|
|
438
|
-
expectsJson: true,
|
|
439
|
-
authHint: "unknown",
|
|
440
|
-
confidence: operationName ? "medium" : "low",
|
|
441
|
-
file: relPath,
|
|
442
|
-
lines: `${lineNum}-${lineNum + 5}`,
|
|
443
|
-
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
444
|
-
reason: `GraphQL ${calleeName}${operationName ? `: ${operationName}` : ""}`,
|
|
445
|
-
meta: {
|
|
446
|
-
operationName,
|
|
447
|
-
operationType: calleeName.includes("Mutation") ? "mutation" : "query",
|
|
448
|
-
},
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// =============================================================================
|
|
457
|
-
// SERVER ACTIONS EXTRACTION
|
|
458
|
-
// =============================================================================
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Extract Next.js Server Action
|
|
462
|
-
*/
|
|
463
|
-
function extractServerAction(path, content, relPath) {
|
|
464
|
-
const jsxAttr = path.node;
|
|
465
|
-
const container = jsxAttr.value;
|
|
466
|
-
|
|
467
|
-
if (!t.isJSXExpressionContainer(container)) return null;
|
|
468
|
-
|
|
469
|
-
const expr = container.expression;
|
|
470
|
-
let actionName = null;
|
|
471
|
-
|
|
472
|
-
if (t.isIdentifier(expr)) {
|
|
473
|
-
actionName = expr.name;
|
|
474
|
-
} else if (t.isMemberExpression(expr)) {
|
|
475
|
-
actionName = extractMemberChain(expr);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (!actionName) return null;
|
|
479
|
-
|
|
480
|
-
const lineNum = jsxAttr.loc?.start?.line || 1;
|
|
481
|
-
|
|
482
|
-
return createClientCall({
|
|
483
|
-
kind: CALL_KINDS.SERVER_ACTION,
|
|
484
|
-
runtime: RUNTIMES.SERVER,
|
|
485
|
-
method: "POST",
|
|
486
|
-
urlTemplate: `action://${relPath}#${actionName}`,
|
|
487
|
-
canonicalPath: `action://${relPath}#${actionName}`,
|
|
488
|
-
expectsJson: false,
|
|
489
|
-
authHint: "unknown",
|
|
490
|
-
confidence: "high",
|
|
491
|
-
file: relPath,
|
|
492
|
-
lines: `${lineNum}-${lineNum + 1}`,
|
|
493
|
-
snippet: `action={${actionName}}`,
|
|
494
|
-
reason: `Server Action: ${actionName}`,
|
|
495
|
-
meta: {
|
|
496
|
-
actionExport: actionName,
|
|
497
|
-
module: relPath,
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// =============================================================================
|
|
503
|
-
// WRAPPER EXTRACTION
|
|
504
|
-
// =============================================================================
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Extract wrapper definitions (axios instances, custom fetch wrappers)
|
|
508
|
-
*/
|
|
509
|
-
function extractWrappers(content, relPath, projectRoot) {
|
|
510
|
-
const wrappers = [];
|
|
511
|
-
|
|
512
|
-
let ast;
|
|
513
|
-
try {
|
|
514
|
-
ast = parser.parse(content, {
|
|
515
|
-
sourceType: "module",
|
|
516
|
-
plugins: ["typescript", "jsx", "decorators-legacy"],
|
|
517
|
-
errorRecovery: true,
|
|
518
|
-
});
|
|
519
|
-
} catch {
|
|
520
|
-
return wrappers;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
traverse(ast, {
|
|
524
|
-
// axios.create()
|
|
525
|
-
CallExpression(path) {
|
|
526
|
-
const { node } = path;
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
t.isMemberExpression(node.callee) &&
|
|
530
|
-
t.isIdentifier(node.callee.object, { name: "axios" }) &&
|
|
531
|
-
t.isIdentifier(node.callee.property, { name: "create" })
|
|
532
|
-
) {
|
|
533
|
-
const configArg = node.arguments[0];
|
|
534
|
-
const config = t.isObjectExpression(configArg) ? extractObjectLiteralProps(configArg) : {};
|
|
535
|
-
|
|
536
|
-
// Find variable name
|
|
537
|
-
let name = "axiosInstance";
|
|
538
|
-
if (t.isVariableDeclarator(path.parent)) {
|
|
539
|
-
if (t.isIdentifier(path.parent.id)) {
|
|
540
|
-
name = path.parent.id.name;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
wrappers.push({
|
|
545
|
-
id: `W_AXIOS_${hashShort(relPath + name)}`,
|
|
546
|
-
name,
|
|
547
|
-
kind: "axios",
|
|
548
|
-
baseURL: config.baseURL || "",
|
|
549
|
-
defaultHeaders: config.headers ? Object.keys(config.headers) : [],
|
|
550
|
-
file: relPath,
|
|
551
|
-
lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
|
|
556
|
-
// Custom fetch wrappers: export const api = { get: ..., post: ... }
|
|
557
|
-
VariableDeclarator(path) {
|
|
558
|
-
const { node } = path;
|
|
559
|
-
|
|
560
|
-
if (t.isIdentifier(node.id) && t.isObjectExpression(node.init)) {
|
|
561
|
-
const props = node.init.properties;
|
|
562
|
-
const methodProps = props.filter(
|
|
563
|
-
p => t.isIdentifier(p.key) && HTTP_METHODS.map(m => m.toLowerCase()).includes(p.key.name)
|
|
564
|
-
);
|
|
565
|
-
|
|
566
|
-
if (methodProps.length >= 2) {
|
|
567
|
-
// Looks like an API client object
|
|
568
|
-
wrappers.push({
|
|
569
|
-
id: `W_CUSTOM_${hashShort(relPath + node.id.name)}`,
|
|
570
|
-
name: node.id.name,
|
|
571
|
-
kind: "custom",
|
|
572
|
-
baseURL: "",
|
|
573
|
-
methods: methodProps.map(p => p.key.name),
|
|
574
|
-
file: relPath,
|
|
575
|
-
lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
return wrappers;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Extract wrapper call (api.get(), apiClient.post(), etc.)
|
|
587
|
-
*/
|
|
588
|
-
function extractWrapperCall(path, content, relPath, runtime, wrapperMap) {
|
|
589
|
-
const { node } = path;
|
|
590
|
-
|
|
591
|
-
if (!t.isMemberExpression(node.callee)) return null;
|
|
592
|
-
|
|
593
|
-
const obj = node.callee.object;
|
|
594
|
-
const prop = node.callee.property;
|
|
595
|
-
|
|
596
|
-
if (!t.isIdentifier(obj) || !t.isIdentifier(prop)) return null;
|
|
597
|
-
|
|
598
|
-
const wrapper = wrapperMap.get(obj.name);
|
|
599
|
-
if (!wrapper) return null;
|
|
600
|
-
|
|
601
|
-
const methodName = prop.name.toUpperCase();
|
|
602
|
-
if (!HTTP_METHODS.includes(methodName) && methodName !== "REQUEST") return null;
|
|
603
|
-
|
|
604
|
-
const urlArg = node.arguments[0];
|
|
605
|
-
if (!urlArg) return null;
|
|
606
|
-
|
|
607
|
-
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
608
|
-
if (!urlInfo.value) return null;
|
|
609
|
-
|
|
610
|
-
// Combine with wrapper baseURL
|
|
611
|
-
const fullUrl = combineBaseUrl(wrapper.baseURL, urlInfo.value);
|
|
612
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
613
|
-
|
|
614
|
-
return createClientCall({
|
|
615
|
-
kind: CALL_KINDS.HTTP,
|
|
616
|
-
runtime,
|
|
617
|
-
method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
|
|
618
|
-
urlTemplate: urlInfo.value,
|
|
619
|
-
canonicalPath: canonicalizeUrl(fullUrl),
|
|
620
|
-
expectsJson: true,
|
|
621
|
-
authHint: "unknown",
|
|
622
|
-
confidence: urlInfo.confidence,
|
|
623
|
-
file: relPath,
|
|
624
|
-
lines: `${lineNum}-${lineNum + 3}`,
|
|
625
|
-
snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
|
|
626
|
-
reason: `${obj.name}.${prop.name}('${urlInfo.value.slice(0, 50)}') via wrapper`,
|
|
627
|
-
meta: {
|
|
628
|
-
wrapperId: wrapper.id,
|
|
629
|
-
},
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// =============================================================================
|
|
634
|
-
// UI BINDING EXTRACTION
|
|
635
|
-
// =============================================================================
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Extract UI bindings (onClick, onSubmit that call API)
|
|
639
|
-
*/
|
|
640
|
-
function extractUIBindings(content, relPath, calls) {
|
|
641
|
-
const bindings = [];
|
|
642
|
-
const callIds = new Set(calls.map(c => c.id));
|
|
643
|
-
|
|
644
|
-
let ast;
|
|
645
|
-
try {
|
|
646
|
-
ast = parser.parse(content, {
|
|
647
|
-
sourceType: "module",
|
|
648
|
-
plugins: ["typescript", "jsx", "decorators-legacy"],
|
|
649
|
-
errorRecovery: true,
|
|
650
|
-
});
|
|
651
|
-
} catch {
|
|
652
|
-
return bindings;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
traverse(ast, {
|
|
656
|
-
JSXAttribute(path) {
|
|
657
|
-
const { node } = path;
|
|
658
|
-
const attrName = node.name?.name;
|
|
659
|
-
|
|
660
|
-
if (!["onClick", "onSubmit", "onBlur", "onChange", "action"].includes(attrName)) return;
|
|
661
|
-
|
|
662
|
-
if (!t.isJSXExpressionContainer(node.value)) return;
|
|
663
|
-
|
|
664
|
-
const expr = node.value.expression;
|
|
665
|
-
const lineNum = node.loc?.start?.line || 1;
|
|
666
|
-
|
|
667
|
-
// Find associated calls in this handler
|
|
668
|
-
const handlerCalls = calls.filter(c => {
|
|
669
|
-
const callLine = parseInt(c.evidence?.[0]?.lines?.split("-")[0] || "0");
|
|
670
|
-
return Math.abs(callLine - lineNum) < 20; // Within 20 lines
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
if (handlerCalls.length > 0) {
|
|
674
|
-
// Try to find label hint
|
|
675
|
-
const labelHint = findLabelHint(path);
|
|
676
|
-
|
|
677
|
-
bindings.push({
|
|
678
|
-
bindingId: `UIB_${hashShort(relPath + lineNum)}`,
|
|
679
|
-
file: relPath,
|
|
680
|
-
lines: `${lineNum}-${lineNum + 5}`,
|
|
681
|
-
event: attrName,
|
|
682
|
-
labelHint,
|
|
683
|
-
calls: handlerCalls.map(c => c.id),
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
},
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
return bindings;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Find label hint from JSX context
|
|
694
|
-
*/
|
|
695
|
-
function findLabelHint(path) {
|
|
696
|
-
// Look for button text, aria-label, etc.
|
|
697
|
-
const parent = path.parentPath;
|
|
698
|
-
if (!parent) return null;
|
|
699
|
-
|
|
700
|
-
// Check for aria-label
|
|
701
|
-
if (t.isJSXOpeningElement(parent.node)) {
|
|
702
|
-
const ariaLabel = parent.node.attributes.find(
|
|
703
|
-
a => t.isJSXAttribute(a) && a.name?.name === "aria-label"
|
|
704
|
-
);
|
|
705
|
-
if (ariaLabel && t.isStringLiteral(ariaLabel.value)) {
|
|
706
|
-
return ariaLabel.value.value;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Check for children text
|
|
711
|
-
const jsxElement = parent.parentPath;
|
|
712
|
-
if (t.isJSXElement(jsxElement?.node)) {
|
|
713
|
-
const textChild = jsxElement.node.children.find(
|
|
714
|
-
c => t.isJSXText(c) && c.value.trim()
|
|
715
|
-
);
|
|
716
|
-
if (textChild) {
|
|
717
|
-
return textChild.value.trim();
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// =============================================================================
|
|
725
|
-
// HELPERS
|
|
726
|
-
// =============================================================================
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* Extract URL value and confidence from AST node
|
|
730
|
-
*/
|
|
731
|
-
function extractUrlFromNode(node, content) {
|
|
732
|
-
// String literal: HIGH confidence
|
|
733
|
-
if (t.isStringLiteral(node)) {
|
|
734
|
-
return { value: node.value, confidence: "high" };
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Template literal
|
|
738
|
-
if (t.isTemplateLiteral(node)) {
|
|
739
|
-
if (node.expressions.length === 0) {
|
|
740
|
-
// No expressions: HIGH
|
|
741
|
-
return { value: node.quasis[0].value.raw, confidence: "high" };
|
|
742
|
-
}
|
|
743
|
-
// Has expressions: MEDIUM
|
|
744
|
-
const template = node.quasis.map((q, i) => {
|
|
745
|
-
const expr = node.expressions[i];
|
|
746
|
-
return q.value.raw + (expr ? `{${i}}` : "");
|
|
747
|
-
}).join("");
|
|
748
|
-
return { value: template, confidence: "medium" };
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Binary expression (concatenation)
|
|
752
|
-
if (t.isBinaryExpression(node, { operator: "+" })) {
|
|
753
|
-
const left = extractUrlFromNode(node.left, content);
|
|
754
|
-
const right = extractUrlFromNode(node.right, content);
|
|
755
|
-
if (left.value && right.value) {
|
|
756
|
-
return {
|
|
757
|
-
value: left.value + right.value,
|
|
758
|
-
confidence: "medium",
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Variable: LOW confidence
|
|
764
|
-
if (t.isIdentifier(node)) {
|
|
765
|
-
return { value: `\${${node.name}}`, confidence: "low" };
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return { value: null, confidence: "low" };
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Extract method from fetch init argument
|
|
773
|
-
*/
|
|
774
|
-
function extractMethodFromInit(initNode) {
|
|
775
|
-
if (!initNode || !t.isObjectExpression(initNode)) return null;
|
|
776
|
-
|
|
777
|
-
const methodProp = initNode.properties.find(
|
|
778
|
-
p => t.isObjectProperty(p) && t.isIdentifier(p.key, { name: "method" })
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
if (methodProp && t.isStringLiteral(methodProp.value)) {
|
|
782
|
-
return methodProp.value.value.toUpperCase();
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return null;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Extract expectsJson from init
|
|
790
|
-
*/
|
|
791
|
-
function extractExpectsJson(initNode, path) {
|
|
792
|
-
if (!initNode) return false;
|
|
793
|
-
|
|
794
|
-
if (t.isObjectExpression(initNode)) {
|
|
795
|
-
const props = extractObjectLiteralProps(initNode);
|
|
796
|
-
if (props.headers?.["content-type"]?.includes("application/json")) return true;
|
|
797
|
-
if (props.body && typeof props.body === "string" && props.body.includes("JSON.stringify")) return true;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Check if .json() is called on response
|
|
801
|
-
// This would require more complex flow analysis
|
|
802
|
-
return false;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Extract auth hint from init
|
|
807
|
-
*/
|
|
808
|
-
function extractAuthHint(initNode) {
|
|
809
|
-
if (!initNode || !t.isObjectExpression(initNode)) return "unknown";
|
|
810
|
-
|
|
811
|
-
const props = extractObjectLiteralProps(initNode);
|
|
812
|
-
|
|
813
|
-
if (props.credentials === "include") return "cookie";
|
|
814
|
-
if (props.headers?.authorization?.toLowerCase().startsWith("bearer")) return "bearer";
|
|
815
|
-
if (props.headers?.Authorization?.toLowerCase().startsWith("bearer")) return "bearer";
|
|
816
|
-
|
|
817
|
-
return "unknown";
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Extract object literal properties as plain object
|
|
822
|
-
*/
|
|
823
|
-
function extractObjectLiteralProps(node) {
|
|
824
|
-
const props = {};
|
|
825
|
-
|
|
826
|
-
for (const prop of node.properties) {
|
|
827
|
-
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
828
|
-
const key = prop.key.name;
|
|
829
|
-
|
|
830
|
-
if (t.isStringLiteral(prop.value)) {
|
|
831
|
-
props[key] = prop.value.value;
|
|
832
|
-
} else if (t.isObjectExpression(prop.value)) {
|
|
833
|
-
props[key] = extractObjectLiteralProps(prop.value);
|
|
834
|
-
} else if (t.isIdentifier(prop.value)) {
|
|
835
|
-
props[key] = `$${prop.value.name}`;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
return props;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Extract member expression chain as string
|
|
845
|
-
*/
|
|
846
|
-
function extractMemberChain(node) {
|
|
847
|
-
const parts = [];
|
|
848
|
-
|
|
849
|
-
let current = node;
|
|
850
|
-
while (t.isMemberExpression(current)) {
|
|
851
|
-
if (t.isIdentifier(current.property)) {
|
|
852
|
-
parts.unshift(current.property.name);
|
|
853
|
-
}
|
|
854
|
-
current = current.object;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
if (t.isIdentifier(current)) {
|
|
858
|
-
parts.unshift(current.name);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
return parts.join(".");
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* Infer runtime from file content
|
|
866
|
-
*/
|
|
867
|
-
function inferRuntime(content, relPath) {
|
|
868
|
-
if (content.includes('"use client"') || content.includes("'use client'")) {
|
|
869
|
-
return RUNTIMES.CLIENT;
|
|
870
|
-
}
|
|
871
|
-
if (content.includes('"use server"') || content.includes("'use server'")) {
|
|
872
|
-
return RUNTIMES.SERVER;
|
|
873
|
-
}
|
|
874
|
-
if (relPath.includes("/api/") || relPath.includes("server")) {
|
|
875
|
-
return RUNTIMES.SERVER;
|
|
876
|
-
}
|
|
877
|
-
if (relPath.includes("/lib/") || relPath.includes("/utils/")) {
|
|
878
|
-
return RUNTIMES.SHARED;
|
|
879
|
-
}
|
|
880
|
-
return RUNTIMES.UNKNOWN;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
/**
|
|
884
|
-
* Canonicalize URL path
|
|
885
|
-
*/
|
|
886
|
-
function canonicalizeUrl(url) {
|
|
887
|
-
if (!url) return null;
|
|
888
|
-
|
|
889
|
-
let canonical = url;
|
|
890
|
-
|
|
891
|
-
// Strip protocol and host
|
|
892
|
-
canonical = canonical.replace(/^https?:\/\/[^/]+/, "");
|
|
893
|
-
|
|
894
|
-
// Strip query string
|
|
895
|
-
canonical = canonical.split("?")[0];
|
|
896
|
-
|
|
897
|
-
// Strip hash
|
|
898
|
-
canonical = canonical.split("#")[0];
|
|
899
|
-
|
|
900
|
-
// Replace template expressions with params
|
|
901
|
-
canonical = canonical.replace(/\$\{[^}]+\}/g, "{param}");
|
|
902
|
-
canonical = canonical.replace(/\{[0-9]+\}/g, "{param}");
|
|
903
|
-
|
|
904
|
-
// Normalize slashes
|
|
905
|
-
canonical = canonical.replace(/\/+/g, "/");
|
|
906
|
-
|
|
907
|
-
// Ensure leading slash
|
|
908
|
-
if (!canonical.startsWith("/") && !canonical.startsWith("action://")) {
|
|
909
|
-
canonical = "/" + canonical;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Remove trailing slash (except root)
|
|
913
|
-
if (canonical.length > 1) {
|
|
914
|
-
canonical = canonical.replace(/\/$/, "");
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
return canonical;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* Combine base URL with path
|
|
922
|
-
*/
|
|
923
|
-
function combineBaseUrl(base, path) {
|
|
924
|
-
if (!base) return path;
|
|
925
|
-
if (!path) return base;
|
|
926
|
-
|
|
927
|
-
const baseTrimmed = base.replace(/\/$/, "");
|
|
928
|
-
const pathTrimmed = path.startsWith("/") ? path : "/" + path;
|
|
929
|
-
|
|
930
|
-
return baseTrimmed + pathTrimmed;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Create a ClientCall record
|
|
935
|
-
*/
|
|
936
|
-
function createClientCall(opts) {
|
|
937
|
-
const id = `C_CALL_${hashShort(opts.file + opts.urlTemplate + opts.method)}`;
|
|
938
|
-
|
|
939
|
-
return {
|
|
940
|
-
id,
|
|
941
|
-
kind: opts.kind,
|
|
942
|
-
runtime: opts.runtime,
|
|
943
|
-
method: opts.method,
|
|
944
|
-
urlTemplate: opts.urlTemplate,
|
|
945
|
-
canonicalPath: opts.canonicalPath,
|
|
946
|
-
expectsJson: opts.expectsJson,
|
|
947
|
-
authHint: opts.authHint,
|
|
948
|
-
confidence: opts.confidence,
|
|
949
|
-
evidence: [{
|
|
950
|
-
id: `E_${hashShort(opts.file + opts.lines)}`,
|
|
951
|
-
kind: "file",
|
|
952
|
-
file: opts.file,
|
|
953
|
-
lines: opts.lines,
|
|
954
|
-
snippetHash: hashSnippet(opts.snippet || ""),
|
|
955
|
-
reason: opts.reason,
|
|
956
|
-
}],
|
|
957
|
-
meta: opts.meta || {},
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
function hashShort(text) {
|
|
962
|
-
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 12).toUpperCase();
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function hashSnippet(text) {
|
|
966
|
-
return `sha256:${crypto.createHash("sha256").update(text).digest("hex")}`;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// =============================================================================
|
|
970
|
-
// EXPORTS
|
|
971
|
-
// =============================================================================
|
|
972
|
-
|
|
973
|
-
module.exports = {
|
|
974
|
-
extractClientCalls,
|
|
975
|
-
extractCallsFromFile,
|
|
976
|
-
extractFetchCall,
|
|
977
|
-
extractAxiosCall,
|
|
978
|
-
extractTRPCCall,
|
|
979
|
-
extractGraphQLCall,
|
|
980
|
-
extractServerAction,
|
|
981
|
-
extractWrappers,
|
|
982
|
-
extractWrapperCall,
|
|
983
|
-
extractUIBindings,
|
|
984
|
-
canonicalizeUrl,
|
|
985
|
-
combineBaseUrl,
|
|
986
|
-
inferRuntime,
|
|
987
|
-
CALL_KINDS,
|
|
988
|
-
RUNTIMES,
|
|
989
|
-
HTTP_METHODS,
|
|
990
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Client Call Extractor v2
|
|
3
|
+
*
|
|
4
|
+
* Extracts client-side API calls: fetch, axios, tRPC, GraphQL, Server Actions, SDK calls.
|
|
5
|
+
* Links them to the Reality Proof Graph for dead UI detection.
|
|
6
|
+
*
|
|
7
|
+
* Output: ClientCall records with evidence, confidence, and canonical paths.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const fg = require("fast-glob");
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const parser = require("@babel/parser");
|
|
17
|
+
const traverse = require("@babel/traverse").default;
|
|
18
|
+
const t = require("@babel/types");
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// TYPES AND CONSTANTS
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
25
|
+
|
|
26
|
+
const CALL_KINDS = {
|
|
27
|
+
HTTP: "http",
|
|
28
|
+
TRPC: "trpc",
|
|
29
|
+
GRAPHQL: "graphql",
|
|
30
|
+
SERVER_ACTION: "server_action",
|
|
31
|
+
SDK: "sdk",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const RUNTIMES = {
|
|
35
|
+
CLIENT: "client",
|
|
36
|
+
SERVER: "server",
|
|
37
|
+
SHARED: "shared",
|
|
38
|
+
UNKNOWN: "unknown",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// MAIN EXTRACTION
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract all client calls from a project
|
|
47
|
+
*/
|
|
48
|
+
function extractClientCalls(projectRoot, options = {}) {
|
|
49
|
+
const { include = [], exclude = [] } = options;
|
|
50
|
+
|
|
51
|
+
// Find source files
|
|
52
|
+
const defaultInclude = ["**/*.{ts,tsx,js,jsx,mts,cts}"];
|
|
53
|
+
const defaultExclude = [
|
|
54
|
+
"**/node_modules/**",
|
|
55
|
+
"**/.next/**",
|
|
56
|
+
"**/dist/**",
|
|
57
|
+
"**/build/**",
|
|
58
|
+
"**/coverage/**",
|
|
59
|
+
"**/*.min.js",
|
|
60
|
+
"**/*.d.ts",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const patterns = include.length > 0 ? include : defaultInclude;
|
|
64
|
+
const ignorePatterns = [...defaultExclude, ...exclude];
|
|
65
|
+
|
|
66
|
+
const files = fg.sync(patterns, {
|
|
67
|
+
cwd: projectRoot,
|
|
68
|
+
absolute: true,
|
|
69
|
+
ignore: ignorePatterns,
|
|
70
|
+
onlyFiles: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const calls = [];
|
|
74
|
+
const wrappers = [];
|
|
75
|
+
const uiBindings = [];
|
|
76
|
+
const errors = [];
|
|
77
|
+
|
|
78
|
+
// Pass 1: Extract wrappers (axios instances, custom request helpers)
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
try {
|
|
81
|
+
const content = fs.readFileSync(file, "utf8");
|
|
82
|
+
const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
83
|
+
const fileWrappers = extractWrappers(content, relPath, projectRoot);
|
|
84
|
+
wrappers.push(...fileWrappers);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
errors.push({ file, phase: "wrappers", error: err.message });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build wrapper lookup
|
|
91
|
+
const wrapperMap = new Map();
|
|
92
|
+
for (const w of wrappers) {
|
|
93
|
+
wrapperMap.set(w.name, w);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Pass 2: Extract calls
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
try {
|
|
99
|
+
const content = fs.readFileSync(file, "utf8");
|
|
100
|
+
const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
101
|
+
const runtime = inferRuntime(content, relPath);
|
|
102
|
+
|
|
103
|
+
const fileCalls = extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot);
|
|
104
|
+
calls.push(...fileCalls);
|
|
105
|
+
|
|
106
|
+
const bindings = extractUIBindings(content, relPath, fileCalls);
|
|
107
|
+
uiBindings.push(...bindings);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
errors.push({ file, phase: "calls", error: err.message });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
calls,
|
|
115
|
+
wrappers,
|
|
116
|
+
uiBindings,
|
|
117
|
+
errors,
|
|
118
|
+
stats: {
|
|
119
|
+
filesScanned: files.length,
|
|
120
|
+
callsFound: calls.length,
|
|
121
|
+
wrappersFound: wrappers.length,
|
|
122
|
+
bindingsFound: uiBindings.length,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extract calls from a single file
|
|
129
|
+
*/
|
|
130
|
+
function extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot) {
|
|
131
|
+
const calls = [];
|
|
132
|
+
|
|
133
|
+
let ast;
|
|
134
|
+
try {
|
|
135
|
+
ast = parser.parse(content, {
|
|
136
|
+
sourceType: "module",
|
|
137
|
+
plugins: ["typescript", "jsx", "decorators-legacy"],
|
|
138
|
+
errorRecovery: true,
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
return calls;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
traverse(ast, {
|
|
145
|
+
CallExpression(path) {
|
|
146
|
+
// fetch() calls
|
|
147
|
+
const fetchCall = extractFetchCall(path, content, relPath, runtime);
|
|
148
|
+
if (fetchCall) {
|
|
149
|
+
calls.push(fetchCall);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// axios calls
|
|
154
|
+
const axiosCall = extractAxiosCall(path, content, relPath, runtime, wrapperMap);
|
|
155
|
+
if (axiosCall) {
|
|
156
|
+
calls.push(axiosCall);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// tRPC calls
|
|
161
|
+
const trpcCall = extractTRPCCall(path, content, relPath, runtime);
|
|
162
|
+
if (trpcCall) {
|
|
163
|
+
calls.push(trpcCall);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// GraphQL calls
|
|
168
|
+
const gqlCall = extractGraphQLCall(path, content, relPath, runtime);
|
|
169
|
+
if (gqlCall) {
|
|
170
|
+
calls.push(gqlCall);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Wrapper calls
|
|
175
|
+
const wrapperCall = extractWrapperCall(path, content, relPath, runtime, wrapperMap);
|
|
176
|
+
if (wrapperCall) {
|
|
177
|
+
calls.push(wrapperCall);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Server Actions (form action={...})
|
|
183
|
+
JSXAttribute(path) {
|
|
184
|
+
if (path.node.name.name === "action" && t.isJSXExpressionContainer(path.node.value)) {
|
|
185
|
+
const actionCall = extractServerAction(path, content, relPath);
|
|
186
|
+
if (actionCall) {
|
|
187
|
+
calls.push(actionCall);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return calls;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// FETCH EXTRACTION
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Extract fetch() call
|
|
202
|
+
*/
|
|
203
|
+
function extractFetchCall(path, content, relPath, runtime) {
|
|
204
|
+
const { node } = path;
|
|
205
|
+
|
|
206
|
+
// Check if callee is 'fetch'
|
|
207
|
+
if (!t.isIdentifier(node.callee, { name: "fetch" })) {
|
|
208
|
+
// Also check for fetch(new Request(...))
|
|
209
|
+
if (t.isNewExpression(node.callee) && t.isIdentifier(node.callee.callee, { name: "Request" })) {
|
|
210
|
+
return extractRequestCall(path, content, relPath, runtime);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const urlArg = node.arguments[0];
|
|
216
|
+
const initArg = node.arguments[1];
|
|
217
|
+
|
|
218
|
+
if (!urlArg) return null;
|
|
219
|
+
|
|
220
|
+
// Extract URL
|
|
221
|
+
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
222
|
+
if (!urlInfo.value) return null;
|
|
223
|
+
|
|
224
|
+
// Extract method
|
|
225
|
+
const method = extractMethodFromInit(initArg) || "GET";
|
|
226
|
+
|
|
227
|
+
// Extract other info
|
|
228
|
+
const expectsJson = extractExpectsJson(initArg, path);
|
|
229
|
+
const authHint = extractAuthHint(initArg);
|
|
230
|
+
|
|
231
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
232
|
+
|
|
233
|
+
return createClientCall({
|
|
234
|
+
kind: CALL_KINDS.HTTP,
|
|
235
|
+
runtime,
|
|
236
|
+
method,
|
|
237
|
+
urlTemplate: urlInfo.value,
|
|
238
|
+
canonicalPath: canonicalizeUrl(urlInfo.value),
|
|
239
|
+
expectsJson,
|
|
240
|
+
authHint,
|
|
241
|
+
confidence: urlInfo.confidence,
|
|
242
|
+
file: relPath,
|
|
243
|
+
lines: `${lineNum}-${lineNum + 5}`,
|
|
244
|
+
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
245
|
+
reason: `fetch('${urlInfo.value.slice(0, 50)}') call`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Extract new Request() call inside fetch
|
|
251
|
+
*/
|
|
252
|
+
function extractRequestCall(path, content, relPath, runtime) {
|
|
253
|
+
const { node } = path;
|
|
254
|
+
const requestNode = node.callee;
|
|
255
|
+
|
|
256
|
+
if (!t.isNewExpression(requestNode)) return null;
|
|
257
|
+
|
|
258
|
+
const urlArg = requestNode.arguments[0];
|
|
259
|
+
const initArg = requestNode.arguments[1];
|
|
260
|
+
|
|
261
|
+
if (!urlArg) return null;
|
|
262
|
+
|
|
263
|
+
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
264
|
+
if (!urlInfo.value) return null;
|
|
265
|
+
|
|
266
|
+
const method = extractMethodFromInit(initArg) || "GET";
|
|
267
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
268
|
+
|
|
269
|
+
return createClientCall({
|
|
270
|
+
kind: CALL_KINDS.HTTP,
|
|
271
|
+
runtime,
|
|
272
|
+
method,
|
|
273
|
+
urlTemplate: urlInfo.value,
|
|
274
|
+
canonicalPath: canonicalizeUrl(urlInfo.value),
|
|
275
|
+
expectsJson: false,
|
|
276
|
+
authHint: "unknown",
|
|
277
|
+
confidence: urlInfo.confidence,
|
|
278
|
+
file: relPath,
|
|
279
|
+
lines: `${lineNum}-${lineNum + 3}`,
|
|
280
|
+
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
281
|
+
reason: `fetch(new Request('${urlInfo.value.slice(0, 50)}')) call`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// AXIOS EXTRACTION
|
|
287
|
+
// =============================================================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Extract axios call
|
|
291
|
+
*/
|
|
292
|
+
function extractAxiosCall(path, content, relPath, runtime, wrapperMap) {
|
|
293
|
+
const { node } = path;
|
|
294
|
+
|
|
295
|
+
// Pattern A: axios.get/post/etc(url)
|
|
296
|
+
if (t.isMemberExpression(node.callee)) {
|
|
297
|
+
const obj = node.callee.object;
|
|
298
|
+
const prop = node.callee.property;
|
|
299
|
+
|
|
300
|
+
if (t.isIdentifier(obj, { name: "axios" }) && t.isIdentifier(prop)) {
|
|
301
|
+
const methodName = prop.name.toUpperCase();
|
|
302
|
+
if (HTTP_METHODS.includes(methodName) || methodName === "REQUEST") {
|
|
303
|
+
const urlArg = node.arguments[0];
|
|
304
|
+
if (!urlArg) return null;
|
|
305
|
+
|
|
306
|
+
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
307
|
+
if (!urlInfo.value) return null;
|
|
308
|
+
|
|
309
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
310
|
+
|
|
311
|
+
return createClientCall({
|
|
312
|
+
kind: CALL_KINDS.HTTP,
|
|
313
|
+
runtime,
|
|
314
|
+
method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
|
|
315
|
+
urlTemplate: urlInfo.value,
|
|
316
|
+
canonicalPath: canonicalizeUrl(urlInfo.value),
|
|
317
|
+
expectsJson: true,
|
|
318
|
+
authHint: "unknown",
|
|
319
|
+
confidence: urlInfo.confidence,
|
|
320
|
+
file: relPath,
|
|
321
|
+
lines: `${lineNum}-${lineNum + 3}`,
|
|
322
|
+
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
323
|
+
reason: `axios.${prop.name}('${urlInfo.value.slice(0, 50)}') call`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Pattern B: axios({ method, url })
|
|
330
|
+
if (t.isIdentifier(node.callee, { name: "axios" })) {
|
|
331
|
+
const configArg = node.arguments[0];
|
|
332
|
+
if (t.isObjectExpression(configArg)) {
|
|
333
|
+
const config = extractObjectLiteralProps(configArg);
|
|
334
|
+
if (config.url) {
|
|
335
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
336
|
+
|
|
337
|
+
return createClientCall({
|
|
338
|
+
kind: CALL_KINDS.HTTP,
|
|
339
|
+
runtime,
|
|
340
|
+
method: (config.method || "GET").toUpperCase(),
|
|
341
|
+
urlTemplate: config.url,
|
|
342
|
+
canonicalPath: canonicalizeUrl(combineBaseUrl(config.baseURL, config.url)),
|
|
343
|
+
expectsJson: true,
|
|
344
|
+
authHint: "unknown",
|
|
345
|
+
confidence: "medium",
|
|
346
|
+
file: relPath,
|
|
347
|
+
lines: `${lineNum}-${lineNum + 5}`,
|
|
348
|
+
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
349
|
+
reason: `axios({ url: '${config.url.slice(0, 50)}' }) call`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// =============================================================================
|
|
359
|
+
// TRPC EXTRACTION
|
|
360
|
+
// =============================================================================
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Extract tRPC call
|
|
364
|
+
*/
|
|
365
|
+
function extractTRPCCall(path, content, relPath, runtime) {
|
|
366
|
+
const { node } = path;
|
|
367
|
+
|
|
368
|
+
// Pattern: trpc.user.get.useQuery() or trpc.billing.portal.useMutation()
|
|
369
|
+
if (t.isMemberExpression(node.callee)) {
|
|
370
|
+
const calleeCode = extractMemberChain(node.callee);
|
|
371
|
+
|
|
372
|
+
// Check for tRPC patterns
|
|
373
|
+
const trpcMatch = calleeCode.match(/^(?:trpc|api)\.(.+?)\.(useQuery|useMutation|query|mutate)$/);
|
|
374
|
+
if (trpcMatch) {
|
|
375
|
+
const procedure = trpcMatch[1];
|
|
376
|
+
const operationType = trpcMatch[2].includes("Mutation") || trpcMatch[2] === "mutate" ? "mutation" : "query";
|
|
377
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
378
|
+
|
|
379
|
+
return createClientCall({
|
|
380
|
+
kind: CALL_KINDS.TRPC,
|
|
381
|
+
runtime,
|
|
382
|
+
method: "POST",
|
|
383
|
+
urlTemplate: `/api/trpc/${procedure}`,
|
|
384
|
+
canonicalPath: `/api/trpc/${procedure}`,
|
|
385
|
+
expectsJson: true,
|
|
386
|
+
authHint: "unknown",
|
|
387
|
+
confidence: "medium",
|
|
388
|
+
file: relPath,
|
|
389
|
+
lines: `${lineNum}-${lineNum + 3}`,
|
|
390
|
+
snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
|
|
391
|
+
reason: `tRPC ${operationType}: ${procedure}`,
|
|
392
|
+
meta: {
|
|
393
|
+
procedure,
|
|
394
|
+
operationType,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// =============================================================================
|
|
404
|
+
// GRAPHQL EXTRACTION
|
|
405
|
+
// =============================================================================
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Extract GraphQL call
|
|
409
|
+
*/
|
|
410
|
+
function extractGraphQLCall(path, content, relPath, runtime) {
|
|
411
|
+
const { node } = path;
|
|
412
|
+
|
|
413
|
+
// Pattern: useQuery(gql`...`) or useMutation(gql`...`)
|
|
414
|
+
if (t.isIdentifier(node.callee)) {
|
|
415
|
+
const calleeName = node.callee.name;
|
|
416
|
+
|
|
417
|
+
if (calleeName === "useQuery" || calleeName === "useMutation" || calleeName === "request") {
|
|
418
|
+
const queryArg = node.arguments[0];
|
|
419
|
+
let operationName = null;
|
|
420
|
+
|
|
421
|
+
// Try to extract operation name from gql template
|
|
422
|
+
if (t.isTaggedTemplateExpression(queryArg)) {
|
|
423
|
+
const template = queryArg.quasi.quasis.map(q => q.value.raw).join("");
|
|
424
|
+
const opMatch = template.match(/(?:query|mutation|subscription)\s+(\w+)/);
|
|
425
|
+
if (opMatch) {
|
|
426
|
+
operationName = opMatch[1];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
431
|
+
|
|
432
|
+
return createClientCall({
|
|
433
|
+
kind: CALL_KINDS.GRAPHQL,
|
|
434
|
+
runtime,
|
|
435
|
+
method: "POST",
|
|
436
|
+
urlTemplate: "/graphql",
|
|
437
|
+
canonicalPath: "/graphql",
|
|
438
|
+
expectsJson: true,
|
|
439
|
+
authHint: "unknown",
|
|
440
|
+
confidence: operationName ? "medium" : "low",
|
|
441
|
+
file: relPath,
|
|
442
|
+
lines: `${lineNum}-${lineNum + 5}`,
|
|
443
|
+
snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
|
|
444
|
+
reason: `GraphQL ${calleeName}${operationName ? `: ${operationName}` : ""}`,
|
|
445
|
+
meta: {
|
|
446
|
+
operationName,
|
|
447
|
+
operationType: calleeName.includes("Mutation") ? "mutation" : "query",
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// =============================================================================
|
|
457
|
+
// SERVER ACTIONS EXTRACTION
|
|
458
|
+
// =============================================================================
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Extract Next.js Server Action
|
|
462
|
+
*/
|
|
463
|
+
function extractServerAction(path, content, relPath) {
|
|
464
|
+
const jsxAttr = path.node;
|
|
465
|
+
const container = jsxAttr.value;
|
|
466
|
+
|
|
467
|
+
if (!t.isJSXExpressionContainer(container)) return null;
|
|
468
|
+
|
|
469
|
+
const expr = container.expression;
|
|
470
|
+
let actionName = null;
|
|
471
|
+
|
|
472
|
+
if (t.isIdentifier(expr)) {
|
|
473
|
+
actionName = expr.name;
|
|
474
|
+
} else if (t.isMemberExpression(expr)) {
|
|
475
|
+
actionName = extractMemberChain(expr);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!actionName) return null;
|
|
479
|
+
|
|
480
|
+
const lineNum = jsxAttr.loc?.start?.line || 1;
|
|
481
|
+
|
|
482
|
+
return createClientCall({
|
|
483
|
+
kind: CALL_KINDS.SERVER_ACTION,
|
|
484
|
+
runtime: RUNTIMES.SERVER,
|
|
485
|
+
method: "POST",
|
|
486
|
+
urlTemplate: `action://${relPath}#${actionName}`,
|
|
487
|
+
canonicalPath: `action://${relPath}#${actionName}`,
|
|
488
|
+
expectsJson: false,
|
|
489
|
+
authHint: "unknown",
|
|
490
|
+
confidence: "high",
|
|
491
|
+
file: relPath,
|
|
492
|
+
lines: `${lineNum}-${lineNum + 1}`,
|
|
493
|
+
snippet: `action={${actionName}}`,
|
|
494
|
+
reason: `Server Action: ${actionName}`,
|
|
495
|
+
meta: {
|
|
496
|
+
actionExport: actionName,
|
|
497
|
+
module: relPath,
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// =============================================================================
|
|
503
|
+
// WRAPPER EXTRACTION
|
|
504
|
+
// =============================================================================
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Extract wrapper definitions (axios instances, custom fetch wrappers)
|
|
508
|
+
*/
|
|
509
|
+
function extractWrappers(content, relPath, projectRoot) {
|
|
510
|
+
const wrappers = [];
|
|
511
|
+
|
|
512
|
+
let ast;
|
|
513
|
+
try {
|
|
514
|
+
ast = parser.parse(content, {
|
|
515
|
+
sourceType: "module",
|
|
516
|
+
plugins: ["typescript", "jsx", "decorators-legacy"],
|
|
517
|
+
errorRecovery: true,
|
|
518
|
+
});
|
|
519
|
+
} catch {
|
|
520
|
+
return wrappers;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
traverse(ast, {
|
|
524
|
+
// axios.create()
|
|
525
|
+
CallExpression(path) {
|
|
526
|
+
const { node } = path;
|
|
527
|
+
|
|
528
|
+
if (
|
|
529
|
+
t.isMemberExpression(node.callee) &&
|
|
530
|
+
t.isIdentifier(node.callee.object, { name: "axios" }) &&
|
|
531
|
+
t.isIdentifier(node.callee.property, { name: "create" })
|
|
532
|
+
) {
|
|
533
|
+
const configArg = node.arguments[0];
|
|
534
|
+
const config = t.isObjectExpression(configArg) ? extractObjectLiteralProps(configArg) : {};
|
|
535
|
+
|
|
536
|
+
// Find variable name
|
|
537
|
+
let name = "axiosInstance";
|
|
538
|
+
if (t.isVariableDeclarator(path.parent)) {
|
|
539
|
+
if (t.isIdentifier(path.parent.id)) {
|
|
540
|
+
name = path.parent.id.name;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
wrappers.push({
|
|
545
|
+
id: `W_AXIOS_${hashShort(relPath + name)}`,
|
|
546
|
+
name,
|
|
547
|
+
kind: "axios",
|
|
548
|
+
baseURL: config.baseURL || "",
|
|
549
|
+
defaultHeaders: config.headers ? Object.keys(config.headers) : [],
|
|
550
|
+
file: relPath,
|
|
551
|
+
lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
// Custom fetch wrappers: export const api = { get: ..., post: ... }
|
|
557
|
+
VariableDeclarator(path) {
|
|
558
|
+
const { node } = path;
|
|
559
|
+
|
|
560
|
+
if (t.isIdentifier(node.id) && t.isObjectExpression(node.init)) {
|
|
561
|
+
const props = node.init.properties;
|
|
562
|
+
const methodProps = props.filter(
|
|
563
|
+
p => t.isIdentifier(p.key) && HTTP_METHODS.map(m => m.toLowerCase()).includes(p.key.name)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
if (methodProps.length >= 2) {
|
|
567
|
+
// Looks like an API client object
|
|
568
|
+
wrappers.push({
|
|
569
|
+
id: `W_CUSTOM_${hashShort(relPath + node.id.name)}`,
|
|
570
|
+
name: node.id.name,
|
|
571
|
+
kind: "custom",
|
|
572
|
+
baseURL: "",
|
|
573
|
+
methods: methodProps.map(p => p.key.name),
|
|
574
|
+
file: relPath,
|
|
575
|
+
lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
return wrappers;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Extract wrapper call (api.get(), apiClient.post(), etc.)
|
|
587
|
+
*/
|
|
588
|
+
function extractWrapperCall(path, content, relPath, runtime, wrapperMap) {
|
|
589
|
+
const { node } = path;
|
|
590
|
+
|
|
591
|
+
if (!t.isMemberExpression(node.callee)) return null;
|
|
592
|
+
|
|
593
|
+
const obj = node.callee.object;
|
|
594
|
+
const prop = node.callee.property;
|
|
595
|
+
|
|
596
|
+
if (!t.isIdentifier(obj) || !t.isIdentifier(prop)) return null;
|
|
597
|
+
|
|
598
|
+
const wrapper = wrapperMap.get(obj.name);
|
|
599
|
+
if (!wrapper) return null;
|
|
600
|
+
|
|
601
|
+
const methodName = prop.name.toUpperCase();
|
|
602
|
+
if (!HTTP_METHODS.includes(methodName) && methodName !== "REQUEST") return null;
|
|
603
|
+
|
|
604
|
+
const urlArg = node.arguments[0];
|
|
605
|
+
if (!urlArg) return null;
|
|
606
|
+
|
|
607
|
+
const urlInfo = extractUrlFromNode(urlArg, content);
|
|
608
|
+
if (!urlInfo.value) return null;
|
|
609
|
+
|
|
610
|
+
// Combine with wrapper baseURL
|
|
611
|
+
const fullUrl = combineBaseUrl(wrapper.baseURL, urlInfo.value);
|
|
612
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
613
|
+
|
|
614
|
+
return createClientCall({
|
|
615
|
+
kind: CALL_KINDS.HTTP,
|
|
616
|
+
runtime,
|
|
617
|
+
method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
|
|
618
|
+
urlTemplate: urlInfo.value,
|
|
619
|
+
canonicalPath: canonicalizeUrl(fullUrl),
|
|
620
|
+
expectsJson: true,
|
|
621
|
+
authHint: "unknown",
|
|
622
|
+
confidence: urlInfo.confidence,
|
|
623
|
+
file: relPath,
|
|
624
|
+
lines: `${lineNum}-${lineNum + 3}`,
|
|
625
|
+
snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
|
|
626
|
+
reason: `${obj.name}.${prop.name}('${urlInfo.value.slice(0, 50)}') via wrapper`,
|
|
627
|
+
meta: {
|
|
628
|
+
wrapperId: wrapper.id,
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// =============================================================================
|
|
634
|
+
// UI BINDING EXTRACTION
|
|
635
|
+
// =============================================================================
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Extract UI bindings (onClick, onSubmit that call API)
|
|
639
|
+
*/
|
|
640
|
+
function extractUIBindings(content, relPath, calls) {
|
|
641
|
+
const bindings = [];
|
|
642
|
+
const callIds = new Set(calls.map(c => c.id));
|
|
643
|
+
|
|
644
|
+
let ast;
|
|
645
|
+
try {
|
|
646
|
+
ast = parser.parse(content, {
|
|
647
|
+
sourceType: "module",
|
|
648
|
+
plugins: ["typescript", "jsx", "decorators-legacy"],
|
|
649
|
+
errorRecovery: true,
|
|
650
|
+
});
|
|
651
|
+
} catch {
|
|
652
|
+
return bindings;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
traverse(ast, {
|
|
656
|
+
JSXAttribute(path) {
|
|
657
|
+
const { node } = path;
|
|
658
|
+
const attrName = node.name?.name;
|
|
659
|
+
|
|
660
|
+
if (!["onClick", "onSubmit", "onBlur", "onChange", "action"].includes(attrName)) return;
|
|
661
|
+
|
|
662
|
+
if (!t.isJSXExpressionContainer(node.value)) return;
|
|
663
|
+
|
|
664
|
+
const expr = node.value.expression;
|
|
665
|
+
const lineNum = node.loc?.start?.line || 1;
|
|
666
|
+
|
|
667
|
+
// Find associated calls in this handler
|
|
668
|
+
const handlerCalls = calls.filter(c => {
|
|
669
|
+
const callLine = parseInt(c.evidence?.[0]?.lines?.split("-")[0] || "0");
|
|
670
|
+
return Math.abs(callLine - lineNum) < 20; // Within 20 lines
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
if (handlerCalls.length > 0) {
|
|
674
|
+
// Try to find label hint
|
|
675
|
+
const labelHint = findLabelHint(path);
|
|
676
|
+
|
|
677
|
+
bindings.push({
|
|
678
|
+
bindingId: `UIB_${hashShort(relPath + lineNum)}`,
|
|
679
|
+
file: relPath,
|
|
680
|
+
lines: `${lineNum}-${lineNum + 5}`,
|
|
681
|
+
event: attrName,
|
|
682
|
+
labelHint,
|
|
683
|
+
calls: handlerCalls.map(c => c.id),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
return bindings;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Find label hint from JSX context
|
|
694
|
+
*/
|
|
695
|
+
function findLabelHint(path) {
|
|
696
|
+
// Look for button text, aria-label, etc.
|
|
697
|
+
const parent = path.parentPath;
|
|
698
|
+
if (!parent) return null;
|
|
699
|
+
|
|
700
|
+
// Check for aria-label
|
|
701
|
+
if (t.isJSXOpeningElement(parent.node)) {
|
|
702
|
+
const ariaLabel = parent.node.attributes.find(
|
|
703
|
+
a => t.isJSXAttribute(a) && a.name?.name === "aria-label"
|
|
704
|
+
);
|
|
705
|
+
if (ariaLabel && t.isStringLiteral(ariaLabel.value)) {
|
|
706
|
+
return ariaLabel.value.value;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Check for children text
|
|
711
|
+
const jsxElement = parent.parentPath;
|
|
712
|
+
if (t.isJSXElement(jsxElement?.node)) {
|
|
713
|
+
const textChild = jsxElement.node.children.find(
|
|
714
|
+
c => t.isJSXText(c) && c.value.trim()
|
|
715
|
+
);
|
|
716
|
+
if (textChild) {
|
|
717
|
+
return textChild.value.trim();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// =============================================================================
|
|
725
|
+
// HELPERS
|
|
726
|
+
// =============================================================================
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Extract URL value and confidence from AST node
|
|
730
|
+
*/
|
|
731
|
+
function extractUrlFromNode(node, content) {
|
|
732
|
+
// String literal: HIGH confidence
|
|
733
|
+
if (t.isStringLiteral(node)) {
|
|
734
|
+
return { value: node.value, confidence: "high" };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Template literal
|
|
738
|
+
if (t.isTemplateLiteral(node)) {
|
|
739
|
+
if (node.expressions.length === 0) {
|
|
740
|
+
// No expressions: HIGH
|
|
741
|
+
return { value: node.quasis[0].value.raw, confidence: "high" };
|
|
742
|
+
}
|
|
743
|
+
// Has expressions: MEDIUM
|
|
744
|
+
const template = node.quasis.map((q, i) => {
|
|
745
|
+
const expr = node.expressions[i];
|
|
746
|
+
return q.value.raw + (expr ? `{${i}}` : "");
|
|
747
|
+
}).join("");
|
|
748
|
+
return { value: template, confidence: "medium" };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Binary expression (concatenation)
|
|
752
|
+
if (t.isBinaryExpression(node, { operator: "+" })) {
|
|
753
|
+
const left = extractUrlFromNode(node.left, content);
|
|
754
|
+
const right = extractUrlFromNode(node.right, content);
|
|
755
|
+
if (left.value && right.value) {
|
|
756
|
+
return {
|
|
757
|
+
value: left.value + right.value,
|
|
758
|
+
confidence: "medium",
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Variable: LOW confidence
|
|
764
|
+
if (t.isIdentifier(node)) {
|
|
765
|
+
return { value: `\${${node.name}}`, confidence: "low" };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return { value: null, confidence: "low" };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Extract method from fetch init argument
|
|
773
|
+
*/
|
|
774
|
+
function extractMethodFromInit(initNode) {
|
|
775
|
+
if (!initNode || !t.isObjectExpression(initNode)) return null;
|
|
776
|
+
|
|
777
|
+
const methodProp = initNode.properties.find(
|
|
778
|
+
p => t.isObjectProperty(p) && t.isIdentifier(p.key, { name: "method" })
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
if (methodProp && t.isStringLiteral(methodProp.value)) {
|
|
782
|
+
return methodProp.value.value.toUpperCase();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Extract expectsJson from init
|
|
790
|
+
*/
|
|
791
|
+
function extractExpectsJson(initNode, path) {
|
|
792
|
+
if (!initNode) return false;
|
|
793
|
+
|
|
794
|
+
if (t.isObjectExpression(initNode)) {
|
|
795
|
+
const props = extractObjectLiteralProps(initNode);
|
|
796
|
+
if (props.headers?.["content-type"]?.includes("application/json")) return true;
|
|
797
|
+
if (props.body && typeof props.body === "string" && props.body.includes("JSON.stringify")) return true;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Check if .json() is called on response
|
|
801
|
+
// This would require more complex flow analysis
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Extract auth hint from init
|
|
807
|
+
*/
|
|
808
|
+
function extractAuthHint(initNode) {
|
|
809
|
+
if (!initNode || !t.isObjectExpression(initNode)) return "unknown";
|
|
810
|
+
|
|
811
|
+
const props = extractObjectLiteralProps(initNode);
|
|
812
|
+
|
|
813
|
+
if (props.credentials === "include") return "cookie";
|
|
814
|
+
if (props.headers?.authorization?.toLowerCase().startsWith("bearer")) return "bearer";
|
|
815
|
+
if (props.headers?.Authorization?.toLowerCase().startsWith("bearer")) return "bearer";
|
|
816
|
+
|
|
817
|
+
return "unknown";
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Extract object literal properties as plain object
|
|
822
|
+
*/
|
|
823
|
+
function extractObjectLiteralProps(node) {
|
|
824
|
+
const props = {};
|
|
825
|
+
|
|
826
|
+
for (const prop of node.properties) {
|
|
827
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
828
|
+
const key = prop.key.name;
|
|
829
|
+
|
|
830
|
+
if (t.isStringLiteral(prop.value)) {
|
|
831
|
+
props[key] = prop.value.value;
|
|
832
|
+
} else if (t.isObjectExpression(prop.value)) {
|
|
833
|
+
props[key] = extractObjectLiteralProps(prop.value);
|
|
834
|
+
} else if (t.isIdentifier(prop.value)) {
|
|
835
|
+
props[key] = `$${prop.value.name}`;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return props;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Extract member expression chain as string
|
|
845
|
+
*/
|
|
846
|
+
function extractMemberChain(node) {
|
|
847
|
+
const parts = [];
|
|
848
|
+
|
|
849
|
+
let current = node;
|
|
850
|
+
while (t.isMemberExpression(current)) {
|
|
851
|
+
if (t.isIdentifier(current.property)) {
|
|
852
|
+
parts.unshift(current.property.name);
|
|
853
|
+
}
|
|
854
|
+
current = current.object;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (t.isIdentifier(current)) {
|
|
858
|
+
parts.unshift(current.name);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return parts.join(".");
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Infer runtime from file content
|
|
866
|
+
*/
|
|
867
|
+
function inferRuntime(content, relPath) {
|
|
868
|
+
if (content.includes('"use client"') || content.includes("'use client'")) {
|
|
869
|
+
return RUNTIMES.CLIENT;
|
|
870
|
+
}
|
|
871
|
+
if (content.includes('"use server"') || content.includes("'use server'")) {
|
|
872
|
+
return RUNTIMES.SERVER;
|
|
873
|
+
}
|
|
874
|
+
if (relPath.includes("/api/") || relPath.includes("server")) {
|
|
875
|
+
return RUNTIMES.SERVER;
|
|
876
|
+
}
|
|
877
|
+
if (relPath.includes("/lib/") || relPath.includes("/utils/")) {
|
|
878
|
+
return RUNTIMES.SHARED;
|
|
879
|
+
}
|
|
880
|
+
return RUNTIMES.UNKNOWN;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Canonicalize URL path
|
|
885
|
+
*/
|
|
886
|
+
function canonicalizeUrl(url) {
|
|
887
|
+
if (!url) return null;
|
|
888
|
+
|
|
889
|
+
let canonical = url;
|
|
890
|
+
|
|
891
|
+
// Strip protocol and host
|
|
892
|
+
canonical = canonical.replace(/^https?:\/\/[^/]+/, "");
|
|
893
|
+
|
|
894
|
+
// Strip query string
|
|
895
|
+
canonical = canonical.split("?")[0];
|
|
896
|
+
|
|
897
|
+
// Strip hash
|
|
898
|
+
canonical = canonical.split("#")[0];
|
|
899
|
+
|
|
900
|
+
// Replace template expressions with params
|
|
901
|
+
canonical = canonical.replace(/\$\{[^}]+\}/g, "{param}");
|
|
902
|
+
canonical = canonical.replace(/\{[0-9]+\}/g, "{param}");
|
|
903
|
+
|
|
904
|
+
// Normalize slashes
|
|
905
|
+
canonical = canonical.replace(/\/+/g, "/");
|
|
906
|
+
|
|
907
|
+
// Ensure leading slash
|
|
908
|
+
if (!canonical.startsWith("/") && !canonical.startsWith("action://")) {
|
|
909
|
+
canonical = "/" + canonical;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Remove trailing slash (except root)
|
|
913
|
+
if (canonical.length > 1) {
|
|
914
|
+
canonical = canonical.replace(/\/$/, "");
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return canonical;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Combine base URL with path
|
|
922
|
+
*/
|
|
923
|
+
function combineBaseUrl(base, path) {
|
|
924
|
+
if (!base) return path;
|
|
925
|
+
if (!path) return base;
|
|
926
|
+
|
|
927
|
+
const baseTrimmed = base.replace(/\/$/, "");
|
|
928
|
+
const pathTrimmed = path.startsWith("/") ? path : "/" + path;
|
|
929
|
+
|
|
930
|
+
return baseTrimmed + pathTrimmed;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Create a ClientCall record
|
|
935
|
+
*/
|
|
936
|
+
function createClientCall(opts) {
|
|
937
|
+
const id = `C_CALL_${hashShort(opts.file + opts.urlTemplate + opts.method)}`;
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
id,
|
|
941
|
+
kind: opts.kind,
|
|
942
|
+
runtime: opts.runtime,
|
|
943
|
+
method: opts.method,
|
|
944
|
+
urlTemplate: opts.urlTemplate,
|
|
945
|
+
canonicalPath: opts.canonicalPath,
|
|
946
|
+
expectsJson: opts.expectsJson,
|
|
947
|
+
authHint: opts.authHint,
|
|
948
|
+
confidence: opts.confidence,
|
|
949
|
+
evidence: [{
|
|
950
|
+
id: `E_${hashShort(opts.file + opts.lines)}`,
|
|
951
|
+
kind: "file",
|
|
952
|
+
file: opts.file,
|
|
953
|
+
lines: opts.lines,
|
|
954
|
+
snippetHash: hashSnippet(opts.snippet || ""),
|
|
955
|
+
reason: opts.reason,
|
|
956
|
+
}],
|
|
957
|
+
meta: opts.meta || {},
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function hashShort(text) {
|
|
962
|
+
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 12).toUpperCase();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function hashSnippet(text) {
|
|
966
|
+
return `sha256:${crypto.createHash("sha256").update(text).digest("hex")}`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// =============================================================================
|
|
970
|
+
// EXPORTS
|
|
971
|
+
// =============================================================================
|
|
972
|
+
|
|
973
|
+
module.exports = {
|
|
974
|
+
extractClientCalls,
|
|
975
|
+
extractCallsFromFile,
|
|
976
|
+
extractFetchCall,
|
|
977
|
+
extractAxiosCall,
|
|
978
|
+
extractTRPCCall,
|
|
979
|
+
extractGraphQLCall,
|
|
980
|
+
extractServerAction,
|
|
981
|
+
extractWrappers,
|
|
982
|
+
extractWrapperCall,
|
|
983
|
+
extractUIBindings,
|
|
984
|
+
canonicalizeUrl,
|
|
985
|
+
combineBaseUrl,
|
|
986
|
+
inferRuntime,
|
|
987
|
+
CALL_KINDS,
|
|
988
|
+
RUNTIMES,
|
|
989
|
+
HTTP_METHODS,
|
|
990
|
+
};
|