@shriyanss/js-recon 1.3.1-alpha.2 → 1.3.1-alpha.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/.github/workflows/build-and-prettify.yaml +15 -1
- package/.github/workflows/pr_checker.yml +9 -8
- package/.github/workflows/publish-js-recon.yml +13 -1
- package/CHANGELOG.md +96 -0
- package/CLAUDE.md +225 -0
- package/README.md +2 -0
- package/build/analyze/engine/astEngine.js +57 -29
- package/build/analyze/engine/astEngine.js.map +1 -1
- package/build/analyze/engine/index.js +2 -2
- package/build/analyze/engine/index.js.map +1 -1
- package/build/analyze/helpers/engineHelpers/taintFlow.js +32 -0
- package/build/analyze/helpers/engineHelpers/taintFlow.js.map +1 -1
- package/build/analyze/helpers/initRules.js +9 -0
- package/build/analyze/helpers/initRules.js.map +1 -1
- package/build/analyze/helpers/schemas.js +6 -1
- package/build/analyze/helpers/schemas.js.map +1 -1
- package/build/analyze/helpers/validate.js +49 -9
- package/build/analyze/helpers/validate.js.map +1 -1
- package/build/analyze/index.js +10 -4
- package/build/analyze/index.js.map +1 -1
- package/build/endpoints/next_js/client_mappedJsonFile.js +12 -5
- package/build/endpoints/next_js/client_mappedJsonFile.js.map +1 -1
- package/build/fingerprint/index.js +123 -0
- package/build/fingerprint/index.js.map +1 -0
- package/build/globalConfig.js +1 -1
- package/build/index.js +72 -5
- package/build/index.js.map +1 -1
- package/build/lazyLoad/downloadFilesUtil.js +3 -3
- package/build/lazyLoad/downloadFilesUtil.js.map +1 -1
- package/build/lazyLoad/downloadQueue.js +16 -11
- package/build/lazyLoad/downloadQueue.js.map +1 -1
- package/build/lazyLoad/index.js +260 -163
- package/build/lazyLoad/index.js.map +1 -1
- package/build/lazyLoad/next_js/NextJsCrawler.js +58 -16
- package/build/lazyLoad/next_js/NextJsCrawler.js.map +1 -1
- package/build/lazyLoad/next_js/next_GetJSScript.js +8 -4
- package/build/lazyLoad/next_js/next_GetJSScript.js.map +1 -1
- package/build/lazyLoad/next_js/next_GetLazyResourcesWebpackJs.js +149 -139
- package/build/lazyLoad/next_js/next_GetLazyResourcesWebpackJs.js.map +1 -1
- package/build/lazyLoad/next_js/next_SubsequentRequests.js +25 -4
- package/build/lazyLoad/next_js/next_SubsequentRequests.js.map +1 -1
- package/build/lazyLoad/next_js/next_scriptTagsSubsequentRequests.js +13 -5
- package/build/lazyLoad/next_js/next_scriptTagsSubsequentRequests.js.map +1 -1
- package/build/lazyLoad/react/react_followImports.js +105 -0
- package/build/lazyLoad/react/react_followImports.js.map +1 -0
- package/build/lazyLoad/react/react_getScriptTags.js +28 -5
- package/build/lazyLoad/react/react_getScriptTags.js.map +1 -1
- package/build/lazyLoad/svelte/svelte_discoverPagesFromJs.js +162 -0
- package/build/lazyLoad/svelte/svelte_discoverPagesFromJs.js.map +1 -0
- package/build/lazyLoad/svelte/svelte_getFromPageSource.js +15 -0
- package/build/lazyLoad/svelte/svelte_getFromPageSource.js.map +1 -1
- package/build/lazyLoad/svelte/svelte_recursivePageCrawl.js +180 -0
- package/build/lazyLoad/svelte/svelte_recursivePageCrawl.js.map +1 -0
- package/build/lazyLoad/svelte/svelte_stringAnalysisJSFiles.js +15 -1
- package/build/lazyLoad/svelte/svelte_stringAnalysisJSFiles.js.map +1 -1
- package/build/lazyLoad/techDetect/checkReact.js +67 -36
- package/build/lazyLoad/techDetect/checkReact.js.map +1 -1
- package/build/lazyLoad/techDetect/checkSvelte.js +35 -35
- package/build/lazyLoad/techDetect/checkSvelte.js.map +1 -1
- package/build/lazyLoad/techDetect/index.js +31 -25
- package/build/lazyLoad/techDetect/index.js.map +1 -1
- package/build/lazyLoad/vue/vue_getClientSidePaths.js +6 -0
- package/build/lazyLoad/vue/vue_getClientSidePaths.js.map +1 -1
- package/build/lazyLoad/vue/vue_recursiveClientSidePathDownload.js +6 -0
- package/build/lazyLoad/vue/vue_recursiveClientSidePathDownload.js.map +1 -1
- package/build/lazyLoad/vue/vue_stringJsFiles.js +6 -0
- package/build/lazyLoad/vue/vue_stringJsFiles.js.map +1 -1
- package/build/load/index.js +316 -0
- package/build/load/index.js.map +1 -0
- package/build/map/graphql/resolveGraphql.js +296 -0
- package/build/map/graphql/resolveGraphql.js.map +1 -0
- package/build/map/index.js +104 -6
- package/build/map/index.js.map +1 -1
- package/build/map/next_js/interactive.js +30 -0
- package/build/map/next_js/interactive.js.map +1 -1
- package/build/map/next_js/interactive_helpers/commandHandler.js +26 -0
- package/build/map/next_js/interactive_helpers/commandHandler.js.map +1 -1
- package/build/map/next_js/interactive_helpers/commandHelpers.js +28 -0
- package/build/map/next_js/interactive_helpers/commandHelpers.js.map +1 -1
- package/build/map/next_js/interactive_helpers/esqueryGen.js +370 -0
- package/build/map/next_js/interactive_helpers/esqueryGen.js.map +1 -0
- package/build/map/next_js/interactive_helpers/helpMenu.js +2 -1
- package/build/map/next_js/interactive_helpers/helpMenu.js.map +1 -1
- package/build/map/next_js/interactive_helpers/inputPatch.js +207 -0
- package/build/map/next_js/interactive_helpers/inputPatch.js.map +1 -0
- package/build/map/next_js/interactive_helpers/ui.js +0 -1
- package/build/map/next_js/interactive_helpers/ui.js.map +1 -1
- package/build/map/next_js/resolveServerActions.js +449 -0
- package/build/map/next_js/resolveServerActions.js.map +1 -0
- package/build/map/next_js/utils.js +89 -2
- package/build/map/next_js/utils.js.map +1 -1
- package/build/map/react_js/getReactConnections.js +298 -0
- package/build/map/react_js/getReactConnections.js.map +1 -0
- package/build/map/react_js/interactive.js +4 -0
- package/build/map/react_js/interactive.js.map +1 -0
- package/build/map/react_js/react_resolveFetch.js +6 -0
- package/build/map/react_js/react_resolveFetch.js.map +1 -0
- package/build/map/svelte_js/interactive.js +58 -0
- package/build/map/svelte_js/interactive.js.map +1 -0
- package/build/map/svelte_js/interactive_helpers/commandHandler.js +4 -0
- package/build/map/svelte_js/interactive_helpers/commandHandler.js.map +1 -0
- package/build/map/vue_js/bodyResolver.js +477 -0
- package/build/map/vue_js/bodyResolver.js.map +1 -0
- package/build/map/vue_js/crossFileResolver.js +438 -0
- package/build/map/vue_js/crossFileResolver.js.map +1 -0
- package/build/map/vue_js/getViteConnections.js +151 -106
- package/build/map/vue_js/getViteConnections.js.map +1 -1
- package/build/map/vue_js/interactive.js +28 -0
- package/build/map/vue_js/interactive.js.map +1 -1
- package/build/map/vue_js/interactive_helpers/commandHandler.js +22 -0
- package/build/map/vue_js/interactive_helpers/commandHandler.js.map +1 -1
- package/build/map/vue_js/interactive_helpers/helpMenu.js +1 -0
- package/build/map/vue_js/interactive_helpers/helpMenu.js.map +1 -1
- package/build/map/vue_js/taint_utils.js +621 -0
- package/build/map/vue_js/taint_utils.js.map +1 -0
- package/build/map/vue_js/vue_resolveFetch.js +279 -25
- package/build/map/vue_js/vue_resolveFetch.js.map +1 -1
- package/build/map/vue_js/vue_resolveHttpClient.js +733 -0
- package/build/map/vue_js/vue_resolveHttpClient.js.map +1 -0
- package/build/map/vue_js/vue_resolveXhr.js +279 -0
- package/build/map/vue_js/vue_resolveXhr.js.map +1 -0
- package/build/mcp/chatOneShot.js +101 -0
- package/build/mcp/chatOneShot.js.map +1 -0
- package/build/mcp/claudeCodeCreds.js +150 -0
- package/build/mcp/claudeCodeCreds.js.map +1 -0
- package/build/mcp/cli.js +140 -149
- package/build/mcp/cli.js.map +1 -1
- package/build/mcp/commands.js +80 -0
- package/build/mcp/commands.js.map +1 -1
- package/build/mcp/index.js +30 -9
- package/build/mcp/index.js.map +1 -1
- package/build/mcp/intent.js +204 -0
- package/build/mcp/intent.js.map +1 -0
- package/build/mcp/jobs.js +199 -0
- package/build/mcp/jobs.js.map +1 -0
- package/build/mcp/mcpServer.js +241 -0
- package/build/mcp/mcpServer.js.map +1 -0
- package/build/mcp/providers.js +18 -1
- package/build/mcp/providers.js.map +1 -1
- package/build/mcp/skills.js +115 -0
- package/build/mcp/skills.js.map +1 -0
- package/build/refactor/index.js +6 -0
- package/build/refactor/index.js.map +1 -1
- package/build/refactor/react/index.js +636 -0
- package/build/refactor/react/index.js.map +1 -0
- package/build/report/utility/populateDb/populateAnalysisFindings.js +1 -1
- package/build/report/utility/populateDb/populateAnalysisFindings.js.map +1 -1
- package/build/run/index.js +277 -60
- package/build/run/index.js.map +1 -1
- package/build/run/interruptHandler.js +93 -0
- package/build/run/interruptHandler.js.map +1 -0
- package/build/utility/globals.js +38 -0
- package/build/utility/globals.js.map +1 -1
- package/build/utility/makeReq.js +39 -5
- package/build/utility/makeReq.js.map +1 -1
- package/build/utility/openapiGenerator.js +61 -17
- package/build/utility/openapiGenerator.js.map +1 -1
- package/build/utility/postmanGenerator.js +69 -16
- package/build/utility/postmanGenerator.js.map +1 -1
- package/build/utility/progressLog.js +50 -0
- package/build/utility/progressLog.js.map +1 -0
- package/package.json +7 -4
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import parser from "@babel/parser";
|
|
12
|
+
import _traverse from "@babel/traverse";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { resolveNodeValue, substituteVariablesInString } from "../next_js/utils.js";
|
|
16
|
+
import * as globals from "../../utility/globals.js";
|
|
17
|
+
import { inferEnclosingFn, substituteCallerPlaceholders, substituteCallerHeaders, makeGetCallers, enclosingFnChainHasBinding, } from "./taint_utils.js";
|
|
18
|
+
import { deepResolveValue, hasMarkers, deepSubstituteBodyValue } from "./bodyResolver.js";
|
|
19
|
+
import { substituteCrossFileMarkers, substituteCrossFileMarkersDeep } from "./crossFileResolver.js";
|
|
20
|
+
const stripAstNodes = (fn) => {
|
|
21
|
+
if (!fn)
|
|
22
|
+
return null;
|
|
23
|
+
const root = Object.assign(Object.assign({}, fn), { node: null });
|
|
24
|
+
let cur = root;
|
|
25
|
+
let src = fn.parent;
|
|
26
|
+
while (src) {
|
|
27
|
+
cur.parent = Object.assign(Object.assign({}, src), { node: null });
|
|
28
|
+
cur = cur.parent;
|
|
29
|
+
src = src.parent;
|
|
30
|
+
}
|
|
31
|
+
return root;
|
|
32
|
+
};
|
|
33
|
+
const traverse = _traverse.default;
|
|
34
|
+
const HTTP_VERBS = new Set(["get", "post", "put", "delete", "patch", "head", "options"]);
|
|
35
|
+
/**
|
|
36
|
+
* Heuristic: only treat a resolved string as a URL if, after stripping every
|
|
37
|
+
* `[…]` placeholder produced by resolveNodeValue, the remaining literal text
|
|
38
|
+
* still contains a `/` separator or an explicit scheme. This rejects calls on
|
|
39
|
+
* Map/Set/Headers/EventBus (`x.get("key")`, `bus.post(event)`) while keeping
|
|
40
|
+
* partial URLs like `[call:base()]<literal>/[param:e]/[param:t]`.
|
|
41
|
+
*/
|
|
42
|
+
const looksLikeUrl = (s) => {
|
|
43
|
+
if (!s)
|
|
44
|
+
return false;
|
|
45
|
+
if (s.startsWith("http://") || s.startsWith("https://"))
|
|
46
|
+
return true;
|
|
47
|
+
const stripped = s.replace(/\[[^\]]*\]/g, "");
|
|
48
|
+
return stripped.includes("/");
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Recovers `[unresolved: NAME]` placeholders by chasing later assignments to
|
|
52
|
+
* NAME within the enclosing function. Pattern: `let ue; …; ue = e + "/" + t;`
|
|
53
|
+
* — babel's binding has no `init`, so resolveNodeValue returns the unresolved
|
|
54
|
+
* marker. The assignment shows up under `binding.constantViolations`.
|
|
55
|
+
*
|
|
56
|
+
* Iterates until no further substitutions are made (an assignment may itself
|
|
57
|
+
* resolve to a string containing more unresolved markers).
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Enumerates every concrete URL obtainable by substituting `[param:X]`
|
|
61
|
+
* placeholders with the resolved values from each caller of the enclosing
|
|
62
|
+
* function chain. Returns an empty array if no `[param:X]` markers are
|
|
63
|
+
* present (caller falls back to the single-result substituter).
|
|
64
|
+
*
|
|
65
|
+
* Walks up to 5 levels deep to chain across nested wrappers
|
|
66
|
+
* (callback → wrapper → exported method → external caller).
|
|
67
|
+
*/
|
|
68
|
+
const expandParamPlaceholders = (url, fn, getCallers, depth = 0, deadlineMs = Date.now() + 10000, capacity = { remaining: 150 }, maxDepth = globals.getMaxRecursionDepth()) => {
|
|
69
|
+
var _a, _b, _c;
|
|
70
|
+
if (depth > maxDepth || typeof url !== "string")
|
|
71
|
+
return [];
|
|
72
|
+
if (Date.now() > deadlineMs || capacity.remaining <= 0)
|
|
73
|
+
return [url];
|
|
74
|
+
const all = url.match(/\[param:([A-Za-z_$][\w$]*)\]/g);
|
|
75
|
+
if (!all)
|
|
76
|
+
return [url];
|
|
77
|
+
// Pick the first placeholder; find the function in the chain that
|
|
78
|
+
// declares it. Then substitute *every* placeholder owned by that same
|
|
79
|
+
// function from a single caller's args, so multi-param URLs like
|
|
80
|
+
// `…/[param:e]/[param:t]` stay consistent across one caller.
|
|
81
|
+
const firstName = all[0].slice("[param:".length, -1);
|
|
82
|
+
let owner = fn;
|
|
83
|
+
while (owner && (!owner.bindingName || !((_a = owner.paramNames) !== null && _a !== void 0 ? _a : []).includes(firstName))) {
|
|
84
|
+
owner = (_b = owner.parent) !== null && _b !== void 0 ? _b : null;
|
|
85
|
+
}
|
|
86
|
+
if (!owner || !owner.bindingName)
|
|
87
|
+
return [url];
|
|
88
|
+
const ownerParams = (_c = owner.paramNames) !== null && _c !== void 0 ? _c : [];
|
|
89
|
+
const ownedNames = new Set();
|
|
90
|
+
for (const m of all) {
|
|
91
|
+
const n = m.slice("[param:".length, -1);
|
|
92
|
+
if (ownerParams.includes(n))
|
|
93
|
+
ownedNames.add(n);
|
|
94
|
+
}
|
|
95
|
+
const allCallers = getCallers(owner.bindingName, owner.file);
|
|
96
|
+
if (!allCallers || allCallers.length === 0)
|
|
97
|
+
return [url];
|
|
98
|
+
// Cap callers per level to bound work; deeper levels still recurse but the
|
|
99
|
+
// deadline/capacity guards above stop the explosion.
|
|
100
|
+
const callers = allCallers.slice(0, 25);
|
|
101
|
+
const out = new Set();
|
|
102
|
+
for (const caller of callers) {
|
|
103
|
+
if (Date.now() > deadlineMs || capacity.remaining <= 0)
|
|
104
|
+
break;
|
|
105
|
+
let nextUrl = url;
|
|
106
|
+
let hasArgs = false;
|
|
107
|
+
for (const name of ownedNames) {
|
|
108
|
+
const idx = ownerParams.indexOf(name);
|
|
109
|
+
const arg = caller.args[idx];
|
|
110
|
+
if (!arg)
|
|
111
|
+
continue;
|
|
112
|
+
hasArgs = true;
|
|
113
|
+
let resolved;
|
|
114
|
+
try {
|
|
115
|
+
resolved = resolveNodeValue(arg, caller.scope, "", "fetch", caller.fileContent);
|
|
116
|
+
}
|
|
117
|
+
catch (_d) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (resolved === null || resolved === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
const resolvedStr = typeof resolved === "string" ? resolved : String(resolved);
|
|
123
|
+
nextUrl = nextUrl.split(`[param:${name}]`).join(resolvedStr);
|
|
124
|
+
}
|
|
125
|
+
if (!hasArgs)
|
|
126
|
+
continue;
|
|
127
|
+
if (!/\[param:/.test(nextUrl)) {
|
|
128
|
+
out.add(nextUrl);
|
|
129
|
+
capacity.remaining--;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const subs = expandParamPlaceholders(nextUrl, caller.enclosingFn, getCallers, depth + 1, deadlineMs, capacity, maxDepth);
|
|
133
|
+
for (const s of subs) {
|
|
134
|
+
if (capacity.remaining <= 0)
|
|
135
|
+
break;
|
|
136
|
+
out.add(s);
|
|
137
|
+
capacity.remaining--;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out.size === 0 ? [url] : Array.from(out);
|
|
142
|
+
};
|
|
143
|
+
const UNRESOLVED_IDENT_RE = /\[unresolved: ([A-Za-z_$][\w$]*)\]/g;
|
|
144
|
+
const resolveFromAssignments = (s, scope, fileContent) => {
|
|
145
|
+
if (typeof s !== "string" || !s)
|
|
146
|
+
return s;
|
|
147
|
+
let prev = null;
|
|
148
|
+
let out = s;
|
|
149
|
+
let guard = 0;
|
|
150
|
+
while (prev !== out && guard < 6) {
|
|
151
|
+
prev = out;
|
|
152
|
+
guard++;
|
|
153
|
+
out = out.replace(UNRESOLVED_IDENT_RE, (orig, name) => {
|
|
154
|
+
var _a, _b, _c;
|
|
155
|
+
try {
|
|
156
|
+
const binding = (_a = scope === null || scope === void 0 ? void 0 : scope.getBinding) === null || _a === void 0 ? void 0 : _a.call(scope, name);
|
|
157
|
+
if (!binding)
|
|
158
|
+
return orig;
|
|
159
|
+
for (const vp of (_b = binding.constantViolations) !== null && _b !== void 0 ? _b : []) {
|
|
160
|
+
const n = vp.node;
|
|
161
|
+
if ((n === null || n === void 0 ? void 0 : n.type) !== "AssignmentExpression" || n.operator !== "=")
|
|
162
|
+
continue;
|
|
163
|
+
if (((_c = n.left) === null || _c === void 0 ? void 0 : _c.type) !== "Identifier" || n.left.name !== name)
|
|
164
|
+
continue;
|
|
165
|
+
const r = resolveNodeValue(n.right, vp.scope, "", "fetch", fileContent);
|
|
166
|
+
if (typeof r === "string" && r.length > 0 && !r.startsWith("[unresolved"))
|
|
167
|
+
return r;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (_d) { }
|
|
171
|
+
return orig;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Generic HTTP-client call resolver for Vite/webpack bundles where the actual
|
|
178
|
+
* `new XMLHttpRequest()` lives inside a transport library (axios's xhrAdapter,
|
|
179
|
+
* Got, Ky, custom wrappers). At that depth the URL resolves only to
|
|
180
|
+
* `[member:re.url]` from a dispatcher config object, which is useless on its
|
|
181
|
+
* own.
|
|
182
|
+
*
|
|
183
|
+
* Instead we walk every `<obj>.<verb>(<url>, ...)` callsite where <verb> is a
|
|
184
|
+
* known HTTP method name. The URL is resolved through the existing
|
|
185
|
+
* resolveNodeValue + substituteVariablesInString machinery, and the
|
|
186
|
+
* `looksLikeUrl` heuristic filters out unrelated method calls (Map.get,
|
|
187
|
+
* Headers.delete, EventBus.post, …).
|
|
188
|
+
*
|
|
189
|
+
* A second-pass taint analysis walks back to each call's enclosing function
|
|
190
|
+
* callers to substitute [param:X] / [member:P.X] / [urlsearchparams:P.X]
|
|
191
|
+
* placeholders. This is the same chain `vue_resolveXhr` already uses.
|
|
192
|
+
*/
|
|
193
|
+
const vue_resolveHttpClient = (directory_1, ...args_1) => __awaiter(void 0, [directory_1, ...args_1], void 0, function* (directory, frameworkName = "Vue.JS") {
|
|
194
|
+
console.log(chalk.cyan(`[i] Resolving ${frameworkName} HTTP client method calls`));
|
|
195
|
+
let files;
|
|
196
|
+
try {
|
|
197
|
+
files = fs.readdirSync(directory, { recursive: true, encoding: "utf8" });
|
|
198
|
+
}
|
|
199
|
+
catch (_a) {
|
|
200
|
+
console.log(chalk.red(`[!] Could not read directory: ${directory}`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
files = files
|
|
204
|
+
.filter((f) => f.endsWith(".js") && !f.includes("___subsequent_requests"))
|
|
205
|
+
.filter((f) => !fs.lstatSync(path.join(directory, f)).isDirectory());
|
|
206
|
+
const allFilePaths = files.map((f) => path.join(directory, f));
|
|
207
|
+
const getCallers = makeGetCallers(allFilePaths);
|
|
208
|
+
const entries = [];
|
|
209
|
+
const verbPrefilter = /\.(?:get|post|put|delete|patch|head|options)\s*\(/;
|
|
210
|
+
const scanStartTs = Date.now();
|
|
211
|
+
console.log(chalk.cyan(`[i] Scanning ${files.length} ${frameworkName} JS file(s) for HTTP-client callsites`));
|
|
212
|
+
let lastScanPct = -1;
|
|
213
|
+
for (let _i = 0; _i < files.length; _i++) {
|
|
214
|
+
if (_i > 0 && _i % 50 === 0)
|
|
215
|
+
yield new Promise((r) => setImmediate(r));
|
|
216
|
+
const scanPct = files.length === 0 ? 100 : Math.floor(((_i + 1) * 100) / files.length);
|
|
217
|
+
if (scanPct !== lastScanPct && (scanPct % 10 === 0 || scanPct === 100)) {
|
|
218
|
+
const elapsed = ((Date.now() - scanStartTs) / 1000).toFixed(1);
|
|
219
|
+
console.log(chalk.gray(` [scan] ${scanPct}% (${_i + 1}/${files.length}) entries=${entries.length} elapsed=${elapsed}s`));
|
|
220
|
+
lastScanPct = scanPct;
|
|
221
|
+
}
|
|
222
|
+
const file = files[_i];
|
|
223
|
+
const filePath = path.join(directory, file);
|
|
224
|
+
let fileContent;
|
|
225
|
+
try {
|
|
226
|
+
fileContent = fs.readFileSync(filePath, "utf-8");
|
|
227
|
+
}
|
|
228
|
+
catch (_b) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!verbPrefilter.test(fileContent))
|
|
232
|
+
continue;
|
|
233
|
+
let fileAst;
|
|
234
|
+
try {
|
|
235
|
+
fileAst = parser.parse(fileContent, {
|
|
236
|
+
sourceType: "unambiguous",
|
|
237
|
+
plugins: ["jsx", "typescript"],
|
|
238
|
+
errorRecovery: true,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (_c) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
traverse(fileAst, {
|
|
245
|
+
CallExpression(p) {
|
|
246
|
+
var _a, _b;
|
|
247
|
+
const callee = p.node.callee;
|
|
248
|
+
if (callee.type !== "MemberExpression" || callee.computed)
|
|
249
|
+
return;
|
|
250
|
+
const prop = callee.property;
|
|
251
|
+
if (prop.type !== "Identifier")
|
|
252
|
+
return;
|
|
253
|
+
const verb = prop.name;
|
|
254
|
+
if (!HTTP_VERBS.has(verb))
|
|
255
|
+
return;
|
|
256
|
+
const args = p.node.arguments;
|
|
257
|
+
if (args.length === 0)
|
|
258
|
+
return;
|
|
259
|
+
const obj = callee.object;
|
|
260
|
+
if (obj.type === "ThisExpression")
|
|
261
|
+
return;
|
|
262
|
+
if (obj.type === "StringLiteral" || obj.type === "TemplateLiteral")
|
|
263
|
+
return;
|
|
264
|
+
const rawUrl = resolveNodeValue(args[0], p.scope, "", "fetch", fileContent);
|
|
265
|
+
let url = typeof rawUrl === "string" ? rawUrl : "";
|
|
266
|
+
if (url.includes("[var ") || url.includes("[MemberExpression")) {
|
|
267
|
+
url = substituteVariablesInString(url, fileContent);
|
|
268
|
+
}
|
|
269
|
+
url = resolveFromAssignments(url, p.scope, fileContent);
|
|
270
|
+
if (!looksLikeUrl(url))
|
|
271
|
+
return;
|
|
272
|
+
let body = "";
|
|
273
|
+
let bodyValue = undefined;
|
|
274
|
+
const headers = {};
|
|
275
|
+
const hasBody = verb === "post" || verb === "put" || verb === "patch";
|
|
276
|
+
const configArgIndex = hasBody ? 2 : 1;
|
|
277
|
+
if (hasBody && args.length >= 2) {
|
|
278
|
+
bodyValue = deepResolveValue(args[1], p.scope, fileContent);
|
|
279
|
+
body =
|
|
280
|
+
bodyValue === null || bodyValue === undefined
|
|
281
|
+
? ""
|
|
282
|
+
: typeof bodyValue === "object"
|
|
283
|
+
? JSON.stringify(bodyValue)
|
|
284
|
+
: String(bodyValue);
|
|
285
|
+
}
|
|
286
|
+
const configArg = args[configArgIndex];
|
|
287
|
+
if (configArg && configArg.type === "ObjectExpression") {
|
|
288
|
+
for (const cp of configArg.properties) {
|
|
289
|
+
if (cp.type !== "ObjectProperty")
|
|
290
|
+
continue;
|
|
291
|
+
const k = cp.key.type === "Identifier"
|
|
292
|
+
? cp.key.name
|
|
293
|
+
: cp.key.type === "StringLiteral"
|
|
294
|
+
? cp.key.value
|
|
295
|
+
: null;
|
|
296
|
+
if (k !== "headers")
|
|
297
|
+
continue;
|
|
298
|
+
if (cp.value.type !== "ObjectExpression")
|
|
299
|
+
continue;
|
|
300
|
+
for (const hp of cp.value.properties) {
|
|
301
|
+
if (hp.type !== "ObjectProperty")
|
|
302
|
+
continue;
|
|
303
|
+
const hk = hp.key.type === "Identifier"
|
|
304
|
+
? hp.key.name
|
|
305
|
+
: hp.key.type === "StringLiteral"
|
|
306
|
+
? hp.key.value
|
|
307
|
+
: null;
|
|
308
|
+
if (!hk)
|
|
309
|
+
continue;
|
|
310
|
+
const rawV = resolveNodeValue(hp.value, p.scope, "", "fetch", fileContent);
|
|
311
|
+
headers[hk] =
|
|
312
|
+
typeof rawV === "string"
|
|
313
|
+
? substituteVariablesInString(rawV, fileContent)
|
|
314
|
+
: String(rawV !== null && rawV !== void 0 ? rawV : "");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const line = (_b = (_a = p.node.loc) === null || _a === void 0 ? void 0 : _a.start.line) !== null && _b !== void 0 ? _b : 0;
|
|
319
|
+
const enclosingFn = stripAstNodes(inferEnclosingFn(p, filePath));
|
|
320
|
+
entries.push({
|
|
321
|
+
file,
|
|
322
|
+
filePath,
|
|
323
|
+
fileLine: line,
|
|
324
|
+
url,
|
|
325
|
+
method: verb.toUpperCase(),
|
|
326
|
+
headers,
|
|
327
|
+
body,
|
|
328
|
+
bodyValue,
|
|
329
|
+
enclosingFn,
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// Taint-analysis second pass — same machinery as vue_resolveXhr.
|
|
335
|
+
const MARKER_RE = /\[(urlsearchparams|member|param):/;
|
|
336
|
+
const headersHaveMarkers = (h) => {
|
|
337
|
+
for (const [k, v] of Object.entries(h)) {
|
|
338
|
+
if (k.startsWith("...") && v === "<spread>")
|
|
339
|
+
return true;
|
|
340
|
+
if (MARKER_RE.test(v))
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
};
|
|
345
|
+
// Apply caller-side substitution to every leaf string inside a structured
|
|
346
|
+
// body. Then JSON.stringify the result so it round-trips through the
|
|
347
|
+
// string-typed `body` field. Plain-string bodies fall back to the existing
|
|
348
|
+
// single-pass substituter.
|
|
349
|
+
const substituteBody = (entry, enclosingFn) => {
|
|
350
|
+
if (entry.bodyValue !== undefined && entry.bodyValue !== null && typeof entry.bodyValue === "object") {
|
|
351
|
+
const next = deepSubstituteBodyValue(entry.bodyValue, enclosingFn, getCallers);
|
|
352
|
+
return { body: JSON.stringify(next), bodyValue: next };
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
body: substituteCallerPlaceholders(entry.body, enclosingFn, getCallers),
|
|
356
|
+
bodyValue: entry.bodyValue,
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
const collectParamNames = (s, set) => {
|
|
360
|
+
if (typeof s !== "string")
|
|
361
|
+
return;
|
|
362
|
+
const m = s.match(/\[param:([A-Za-z_$][\w$]*)\]/g);
|
|
363
|
+
if (!m)
|
|
364
|
+
return;
|
|
365
|
+
for (const x of m)
|
|
366
|
+
set.add(x.slice("[param:".length, -1));
|
|
367
|
+
};
|
|
368
|
+
const walkValueForParams = (v, set) => {
|
|
369
|
+
if (v === null || v === undefined)
|
|
370
|
+
return;
|
|
371
|
+
if (typeof v === "string")
|
|
372
|
+
collectParamNames(v, set);
|
|
373
|
+
else if (Array.isArray(v))
|
|
374
|
+
for (const x of v)
|
|
375
|
+
walkValueForParams(x, set);
|
|
376
|
+
else if (typeof v === "object")
|
|
377
|
+
for (const x of Object.values(v))
|
|
378
|
+
walkValueForParams(x, set);
|
|
379
|
+
};
|
|
380
|
+
const substituteParamsDeep = (value, subStr, subVal, owned) => {
|
|
381
|
+
if (value === null || value === undefined)
|
|
382
|
+
return value;
|
|
383
|
+
if (typeof value === "string") {
|
|
384
|
+
const exact = value.match(/^\[param:([A-Za-z_$][\w$]*)\]$/);
|
|
385
|
+
if (exact && owned.has(exact[1])) {
|
|
386
|
+
const v = subVal[exact[1]];
|
|
387
|
+
if (v !== undefined)
|
|
388
|
+
return v;
|
|
389
|
+
}
|
|
390
|
+
let out = value;
|
|
391
|
+
for (const [name, s] of Object.entries(subStr)) {
|
|
392
|
+
if (s === null || s === undefined)
|
|
393
|
+
continue;
|
|
394
|
+
out = out.split(`[param:${name}]`).join(String(s));
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
if (Array.isArray(value))
|
|
399
|
+
return value.map((v) => substituteParamsDeep(v, subStr, subVal, owned));
|
|
400
|
+
if (typeof value === "object") {
|
|
401
|
+
const o = {};
|
|
402
|
+
for (const [k, v] of Object.entries(value)) {
|
|
403
|
+
o[k] = substituteParamsDeep(v, subStr, subVal, owned);
|
|
404
|
+
}
|
|
405
|
+
return o;
|
|
406
|
+
}
|
|
407
|
+
return value;
|
|
408
|
+
};
|
|
409
|
+
// Hard bounds to prevent exponential blowup.
|
|
410
|
+
// depth=2 → up to (callers/level)² forwarding-wrapper chains, enabling
|
|
411
|
+
// resolution through registry-export wrappers like `ae.request → Me`.
|
|
412
|
+
const FANOUT_MAX_DEPTH = 2;
|
|
413
|
+
const FANOUT_MAX_CALLERS_PER_LEVEL = 12;
|
|
414
|
+
const FANOUT_MAX_EXPANSIONS_PER_ENTRY = 80;
|
|
415
|
+
const FANOUT_BUDGET_MS_PER_ENTRY = 1200;
|
|
416
|
+
// Lightweight arg resolver — avoids the deep recursive scope-walking that
|
|
417
|
+
// makes `resolveNodeValue`/`deepResolveValue` expensive (5+s per entry).
|
|
418
|
+
// Only handles the cheap patterns: literals, identifiers bound to literals,
|
|
419
|
+
// simple member chains where the base is a local ObjectExpression literal.
|
|
420
|
+
// Returns null for anything more complex; the outer loop falls back to
|
|
421
|
+
// leaving the [param:X] placeholder unresolved for that caller.
|
|
422
|
+
const lightResolveArg = (node, scope, depth = 0) => {
|
|
423
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
424
|
+
if (!node || depth > 3)
|
|
425
|
+
return { asString: null, asValue: null };
|
|
426
|
+
if (node.type === "StringLiteral")
|
|
427
|
+
return { asString: node.value, asValue: node.value };
|
|
428
|
+
if (node.type === "NumericLiteral")
|
|
429
|
+
return { asString: String(node.value), asValue: node.value };
|
|
430
|
+
if (node.type === "BooleanLiteral")
|
|
431
|
+
return { asString: String(node.value), asValue: node.value };
|
|
432
|
+
if (node.type === "NullLiteral")
|
|
433
|
+
return { asString: "null", asValue: null };
|
|
434
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
435
|
+
const v = (_b = (_a = node.quasis[0].value.cooked) !== null && _a !== void 0 ? _a : node.quasis[0].value.raw) !== null && _b !== void 0 ? _b : "";
|
|
436
|
+
return { asString: v, asValue: v };
|
|
437
|
+
}
|
|
438
|
+
if (node.type === "ObjectExpression") {
|
|
439
|
+
// Build a shallow plain-object representation for structured body subst.
|
|
440
|
+
const o = {};
|
|
441
|
+
for (const p of (_c = node.properties) !== null && _c !== void 0 ? _c : []) {
|
|
442
|
+
if (p.type !== "ObjectProperty")
|
|
443
|
+
continue;
|
|
444
|
+
const k = p.key.type === "Identifier" ? p.key.name : p.key.type === "StringLiteral" ? p.key.value : null;
|
|
445
|
+
if (!k)
|
|
446
|
+
continue;
|
|
447
|
+
o[k] = lightResolveArg(p.value, scope, depth + 1).asValue;
|
|
448
|
+
}
|
|
449
|
+
return { asString: null, asValue: o };
|
|
450
|
+
}
|
|
451
|
+
if (node.type === "ArrayExpression") {
|
|
452
|
+
const arr = [];
|
|
453
|
+
for (const e of (_d = node.elements) !== null && _d !== void 0 ? _d : []) {
|
|
454
|
+
arr.push(e ? lightResolveArg(e, scope, depth + 1).asValue : null);
|
|
455
|
+
}
|
|
456
|
+
return { asString: null, asValue: arr };
|
|
457
|
+
}
|
|
458
|
+
if (node.type === "Identifier") {
|
|
459
|
+
try {
|
|
460
|
+
const b = (_e = scope === null || scope === void 0 ? void 0 : scope.getBinding) === null || _e === void 0 ? void 0 : _e.call(scope, node.name);
|
|
461
|
+
const init = (_g = (_f = b === null || b === void 0 ? void 0 : b.path) === null || _f === void 0 ? void 0 : _f.node) === null || _g === void 0 ? void 0 : _g.init;
|
|
462
|
+
if (init) {
|
|
463
|
+
return lightResolveArg(init, (_h = b.scope) !== null && _h !== void 0 ? _h : scope, depth + 1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (_j) { }
|
|
467
|
+
return { asString: null, asValue: null };
|
|
468
|
+
}
|
|
469
|
+
if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
|
|
470
|
+
const base = lightResolveArg(node.object, scope, depth + 1);
|
|
471
|
+
if (base.asValue && typeof base.asValue === "object" && node.property.name in base.asValue) {
|
|
472
|
+
const v = base.asValue[node.property.name];
|
|
473
|
+
return { asString: typeof v === "string" ? v : v != null ? String(v) : null, asValue: v };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { asString: null, asValue: null };
|
|
477
|
+
};
|
|
478
|
+
const expandEntryAcrossCallers = (url, bodyValue, headers, fn, deadlineMs, capacity, depth = 0) => {
|
|
479
|
+
var _a, _b, _c;
|
|
480
|
+
if (depth > FANOUT_MAX_DEPTH || !fn)
|
|
481
|
+
return [{ url, bodyValue, headers }];
|
|
482
|
+
if (Date.now() > deadlineMs || capacity.remaining <= 0)
|
|
483
|
+
return [{ url, bodyValue, headers }];
|
|
484
|
+
const allNames = new Set();
|
|
485
|
+
collectParamNames(url, allNames);
|
|
486
|
+
walkValueForParams(bodyValue, allNames);
|
|
487
|
+
for (const v of Object.values(headers))
|
|
488
|
+
collectParamNames(v, allNames);
|
|
489
|
+
for (const k of Object.keys(headers))
|
|
490
|
+
collectParamNames(k, allNames);
|
|
491
|
+
if (allNames.size === 0)
|
|
492
|
+
return [{ url, bodyValue, headers }];
|
|
493
|
+
let owner = fn;
|
|
494
|
+
while (owner) {
|
|
495
|
+
const op = (_a = owner.paramNames) !== null && _a !== void 0 ? _a : [];
|
|
496
|
+
if (owner.bindingName && [...allNames].some((n) => op.includes(n)))
|
|
497
|
+
break;
|
|
498
|
+
owner = (_b = owner.parent) !== null && _b !== void 0 ? _b : null;
|
|
499
|
+
}
|
|
500
|
+
if (!owner || !owner.bindingName)
|
|
501
|
+
return [{ url, bodyValue, headers }];
|
|
502
|
+
const ownerParams = (_c = owner.paramNames) !== null && _c !== void 0 ? _c : [];
|
|
503
|
+
const owned = new Set([...allNames].filter((n) => ownerParams.includes(n)));
|
|
504
|
+
const allCallers = getCallers(owner.bindingName, owner.file);
|
|
505
|
+
if (!allCallers || allCallers.length === 0)
|
|
506
|
+
return [{ url, bodyValue, headers }];
|
|
507
|
+
const callers = allCallers.slice(0, FANOUT_MAX_CALLERS_PER_LEVEL);
|
|
508
|
+
// Split owned params by where they appear: only call deepResolveValue
|
|
509
|
+
// (which can walk deeply) when an owned param appears as a bare
|
|
510
|
+
// `[param:X]` LEAF in the structured body. URL/header/string-body
|
|
511
|
+
// substitution only needs resolveNodeValue (string).
|
|
512
|
+
const bodyDeepOwned = new Set();
|
|
513
|
+
const walkBodyForLeaves = (v) => {
|
|
514
|
+
if (v === null || v === undefined)
|
|
515
|
+
return;
|
|
516
|
+
if (typeof v === "string") {
|
|
517
|
+
const m = v.match(/^\[param:([A-Za-z_$][\w$]*)\]$/);
|
|
518
|
+
if (m && owned.has(m[1]))
|
|
519
|
+
bodyDeepOwned.add(m[1]);
|
|
520
|
+
}
|
|
521
|
+
else if (Array.isArray(v))
|
|
522
|
+
for (const x of v)
|
|
523
|
+
walkBodyForLeaves(x);
|
|
524
|
+
else if (typeof v === "object")
|
|
525
|
+
for (const x of Object.values(v))
|
|
526
|
+
walkBodyForLeaves(x);
|
|
527
|
+
};
|
|
528
|
+
walkBodyForLeaves(bodyValue);
|
|
529
|
+
const out = [];
|
|
530
|
+
for (const caller of callers) {
|
|
531
|
+
if (Date.now() > deadlineMs || capacity.remaining <= 0)
|
|
532
|
+
break;
|
|
533
|
+
const subStr = {};
|
|
534
|
+
const subVal = {};
|
|
535
|
+
let hasAny = false;
|
|
536
|
+
for (const name of owned) {
|
|
537
|
+
const idx = ownerParams.indexOf(name);
|
|
538
|
+
const arg = caller.args[idx];
|
|
539
|
+
if (!arg)
|
|
540
|
+
continue;
|
|
541
|
+
hasAny = true;
|
|
542
|
+
// String resolution: use full resolveNodeValue (handles template
|
|
543
|
+
// literals, concat, identifier chains). It's fast — only
|
|
544
|
+
// deepResolveValue is the perf hazard.
|
|
545
|
+
try {
|
|
546
|
+
const s = resolveNodeValue(arg, caller.scope, "", "fetch", caller.fileContent);
|
|
547
|
+
subStr[name] = typeof s === "string" ? s : null;
|
|
548
|
+
}
|
|
549
|
+
catch (_d) {
|
|
550
|
+
subStr[name] = null;
|
|
551
|
+
}
|
|
552
|
+
// Structured body value: only call deepResolveValue if THIS
|
|
553
|
+
// param appears as a bare `[param:X]` leaf in the body — and
|
|
554
|
+
// only within the deadline. Otherwise fall back to the string.
|
|
555
|
+
if (bodyDeepOwned.has(name) && Date.now() < deadlineMs) {
|
|
556
|
+
try {
|
|
557
|
+
subVal[name] = deepResolveValue(arg, caller.scope, caller.fileContent);
|
|
558
|
+
}
|
|
559
|
+
catch (_e) {
|
|
560
|
+
subVal[name] = subStr[name];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
subVal[name] = subStr[name];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (!hasAny)
|
|
568
|
+
continue;
|
|
569
|
+
const urlSub = substituteParamsDeep(url, subStr, subVal, owned);
|
|
570
|
+
const bodyValSub = substituteParamsDeep(bodyValue, subStr, subVal, owned);
|
|
571
|
+
const headersSub = {};
|
|
572
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
573
|
+
const nk = substituteParamsDeep(k, subStr, subVal, owned);
|
|
574
|
+
const nv = substituteParamsDeep(v, subStr, subVal, owned);
|
|
575
|
+
headersSub[typeof nk === "string" ? nk : k] = typeof nv === "string" ? nv : String(nv !== null && nv !== void 0 ? nv : "");
|
|
576
|
+
}
|
|
577
|
+
const stillHasParams = (typeof urlSub === "string" && /\[param:/.test(urlSub)) ||
|
|
578
|
+
hasMarkers(bodyValSub) ||
|
|
579
|
+
Object.values(headersSub).some((v) => typeof v === "string" && /\[param:/.test(v));
|
|
580
|
+
if (stillHasParams && caller.enclosingFn && depth + 1 <= FANOUT_MAX_DEPTH) {
|
|
581
|
+
const recursed = expandEntryAcrossCallers(typeof urlSub === "string" ? urlSub : url, bodyValSub, headersSub, caller.enclosingFn, deadlineMs, capacity, depth + 1);
|
|
582
|
+
for (const r of recursed) {
|
|
583
|
+
if (capacity.remaining <= 0)
|
|
584
|
+
break;
|
|
585
|
+
out.push(r);
|
|
586
|
+
capacity.remaining--;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
out.push({
|
|
591
|
+
url: typeof urlSub === "string" ? urlSub : url,
|
|
592
|
+
bodyValue: bodyValSub,
|
|
593
|
+
headers: headersSub,
|
|
594
|
+
});
|
|
595
|
+
capacity.remaining--;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return out.length === 0 ? [{ url, bodyValue, headers }] : out;
|
|
599
|
+
};
|
|
600
|
+
const expandedEntries = [];
|
|
601
|
+
const totalEntries = entries.length;
|
|
602
|
+
let processedCount = 0;
|
|
603
|
+
let lastProgressPct = -1;
|
|
604
|
+
const progressStartTs = Date.now();
|
|
605
|
+
console.log(chalk.cyan(`[i] Expanding ${totalEntries} ${frameworkName} HTTP-client callsite(s) across callers`));
|
|
606
|
+
for (const entry of entries) {
|
|
607
|
+
processedCount++;
|
|
608
|
+
const pct = totalEntries === 0 ? 100 : Math.floor((processedCount * 100) / totalEntries);
|
|
609
|
+
if (pct !== lastProgressPct && (pct % 10 === 0 || pct === 100)) {
|
|
610
|
+
const elapsed = ((Date.now() - progressStartTs) / 1000).toFixed(1);
|
|
611
|
+
console.log(chalk.gray(` [progress] ${pct}% (${processedCount}/${totalEntries}) elapsed=${elapsed}s`));
|
|
612
|
+
lastProgressPct = pct;
|
|
613
|
+
}
|
|
614
|
+
if (enclosingFnChainHasBinding(entry.enclosingFn) &&
|
|
615
|
+
(MARKER_RE.test(entry.url) ||
|
|
616
|
+
MARKER_RE.test(entry.body) ||
|
|
617
|
+
hasMarkers(entry.bodyValue) ||
|
|
618
|
+
headersHaveMarkers(entry.headers))) {
|
|
619
|
+
// URL fan-out: use the original `expandParamPlaceholders` (string
|
|
620
|
+
// resolveNodeValue per caller). This is the version that produced
|
|
621
|
+
// varied URLs like `<service>/<module>/<method>`.
|
|
622
|
+
//
|
|
623
|
+
// Scale per-entry budget and capacity with maxDepth so deeper
|
|
624
|
+
// recursion actually has room to produce more URLs. At depth=3
|
|
625
|
+
// (default) we get ~10s/150caps. At depth=10 we get ~80s/1200caps
|
|
626
|
+
// per entry.
|
|
627
|
+
const maxDepth = globals.getMaxRecursionDepth();
|
|
628
|
+
const depthFactor = maxDepth + 1;
|
|
629
|
+
// Linear deadline scaling — empirically gives ~5min total runtime
|
|
630
|
+
// at depth 8 once getCallers AST caching is in place.
|
|
631
|
+
const perEntryDeadlineMs = Date.now() + Math.max(2000, (8000 * depthFactor) / 4);
|
|
632
|
+
const perEntryCapacity = { remaining: Math.max(30, 50 * depthFactor) };
|
|
633
|
+
const entryStartTs = Date.now();
|
|
634
|
+
const urls = expandParamPlaceholders(entry.url, entry.enclosingFn, getCallers, 0, perEntryDeadlineMs, perEntryCapacity, maxDepth);
|
|
635
|
+
// Body+headers substitution: resolves [param:X] body leaves to the
|
|
636
|
+
// first matching caller's structured value (credentials object,
|
|
637
|
+
// etc.) via resolveParamToAnyValue.
|
|
638
|
+
const subBody = substituteBody(entry, entry.enclosingFn);
|
|
639
|
+
const headersSub = substituteCallerHeaders(entry.headers, entry.enclosingFn, getCallers);
|
|
640
|
+
const entryElapsed = Date.now() - entryStartTs;
|
|
641
|
+
if (entryElapsed > 200) {
|
|
642
|
+
console.log(chalk.gray(` [expand] entry ${processedCount}/${totalEntries} took ${entryElapsed}ms (${urls.length} urls)`));
|
|
643
|
+
}
|
|
644
|
+
const expansions = urls.length === 0
|
|
645
|
+
? [
|
|
646
|
+
{
|
|
647
|
+
url: substituteCallerPlaceholders(entry.url, entry.enclosingFn, getCallers),
|
|
648
|
+
bodyValue: subBody.bodyValue,
|
|
649
|
+
headers: headersSub,
|
|
650
|
+
},
|
|
651
|
+
]
|
|
652
|
+
: urls.map((u) => ({ url: u, bodyValue: subBody.bodyValue, headers: headersSub }));
|
|
653
|
+
for (const exp of expansions) {
|
|
654
|
+
// Cross-file pass (later) handles [member:X.Y] / [call:X.Y()].
|
|
655
|
+
const urlFinal = exp.url;
|
|
656
|
+
const headersFinal = exp.headers;
|
|
657
|
+
const bodyValueFinal = exp.bodyValue;
|
|
658
|
+
const bodyStr = bodyValueFinal === null || bodyValueFinal === undefined
|
|
659
|
+
? ""
|
|
660
|
+
: typeof bodyValueFinal === "object"
|
|
661
|
+
? JSON.stringify(bodyValueFinal)
|
|
662
|
+
: String(bodyValueFinal);
|
|
663
|
+
expandedEntries.push(Object.assign(Object.assign({}, entry), { url: urlFinal, body: bodyStr, bodyValue: bodyValueFinal, headers: headersFinal }));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
expandedEntries.push(entry);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
entries.length = 0;
|
|
671
|
+
entries.push(...expandedEntries);
|
|
672
|
+
// Final pass: cross-file resolution for [member:X.Y] and [call:X.Y.Z()]
|
|
673
|
+
// markers that survived caller-chain substitution. These come from imports
|
|
674
|
+
// of other Vite chunks, which the in-file resolver can't follow.
|
|
675
|
+
console.log(chalk.cyan(`[i] Cross-file resolution pass for ${entries.length} entries`));
|
|
676
|
+
const crossStartTs = Date.now();
|
|
677
|
+
let lastCrossPct = -1;
|
|
678
|
+
for (let i = 0; i < entries.length; i++) {
|
|
679
|
+
const entry = entries[i];
|
|
680
|
+
const pct = entries.length === 0 ? 100 : Math.floor(((i + 1) * 100) / entries.length);
|
|
681
|
+
if (pct !== lastCrossPct && (pct % 20 === 0 || pct === 100)) {
|
|
682
|
+
const elapsed = ((Date.now() - crossStartTs) / 1000).toFixed(1);
|
|
683
|
+
console.log(chalk.gray(` [cross-file] ${pct}% (${i + 1}/${entries.length}) elapsed=${elapsed}s`));
|
|
684
|
+
lastCrossPct = pct;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
entry.url = substituteCrossFileMarkers(entry.url, entry.filePath, directory);
|
|
688
|
+
const headersResolved = {};
|
|
689
|
+
for (const [hk, hv] of Object.entries(entry.headers)) {
|
|
690
|
+
const nk = substituteCrossFileMarkers(hk, entry.filePath, directory);
|
|
691
|
+
const nv = substituteCrossFileMarkers(hv, entry.filePath, directory);
|
|
692
|
+
headersResolved[nk] = nv;
|
|
693
|
+
}
|
|
694
|
+
entry.headers = headersResolved;
|
|
695
|
+
if (entry.bodyValue !== undefined && entry.bodyValue !== null && typeof entry.bodyValue === "object") {
|
|
696
|
+
entry.bodyValue = substituteCrossFileMarkersDeep(entry.bodyValue, entry.filePath, directory);
|
|
697
|
+
entry.body = JSON.stringify(entry.bodyValue);
|
|
698
|
+
}
|
|
699
|
+
else if (typeof entry.body === "string" && entry.body) {
|
|
700
|
+
entry.body = substituteCrossFileMarkers(entry.body, entry.filePath, directory);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch (_d) {
|
|
704
|
+
// Cross-file resolution is best-effort; never block emission on failure.
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
let emitted = 0;
|
|
708
|
+
for (const entry of entries) {
|
|
709
|
+
if (!looksLikeUrl(entry.url))
|
|
710
|
+
continue;
|
|
711
|
+
console.log(chalk.blue(`[+] Found ${entry.method} client call in "${entry.filePath}":${entry.fileLine}`));
|
|
712
|
+
console.log(chalk.green(` URL: ${entry.url}`));
|
|
713
|
+
if (Object.keys(entry.headers).length > 0) {
|
|
714
|
+
console.log(chalk.green(` Headers: ${JSON.stringify(entry.headers)}`));
|
|
715
|
+
}
|
|
716
|
+
if (entry.body)
|
|
717
|
+
console.log(chalk.green(` Body: ${entry.body}`));
|
|
718
|
+
globals.addOpenapiOutput({
|
|
719
|
+
url: entry.url,
|
|
720
|
+
method: entry.method,
|
|
721
|
+
path: entry.url,
|
|
722
|
+
headers: entry.headers,
|
|
723
|
+
body: entry.body,
|
|
724
|
+
chunkId: `${entry.file}:${entry.fileLine}`,
|
|
725
|
+
functionFile: entry.filePath,
|
|
726
|
+
functionFileLine: entry.fileLine,
|
|
727
|
+
});
|
|
728
|
+
emitted++;
|
|
729
|
+
}
|
|
730
|
+
console.log(chalk.green(`[✓] Emitted ${emitted} HTTP client call(s) across ${frameworkName} files`));
|
|
731
|
+
});
|
|
732
|
+
export default vue_resolveHttpClient;
|
|
733
|
+
//# sourceMappingURL=vue_resolveHttpClient.js.map
|