@patchstack/connect 0.5.15 → 0.5.17
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/AGENT-INSTALL.md +42 -14
- package/README.md +5 -5
- package/dist/cli.js +695 -183
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +2 -2
- package/dist/protect.cjs +1388 -748
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +110 -16
- package/dist/protect.d.ts +110 -16
- package/dist/protect.edge.js +1376 -736
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +1376 -736
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2985,8 +2985,9 @@ function productionGate(framework) {
|
|
|
2985
2985
|
const mapped = framework !== null ? PRODUCTION_GATES[framework] : void 0;
|
|
2986
2986
|
return mapped ?? DEFAULT_PRODUCTION_GATE;
|
|
2987
2987
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2988
|
+
var ASTRO = "astro";
|
|
2989
|
+
function hasEditableShell(framework) {
|
|
2990
|
+
return framework !== null && (JSX_SHELL_FRAMEWORKS.has(framework) || framework === ASTRO);
|
|
2990
2991
|
}
|
|
2991
2992
|
function buildSourceMarkerSnippet(framework, checksum = null) {
|
|
2992
2993
|
const gate = productionGate(framework);
|
|
@@ -2994,6 +2995,11 @@ function buildSourceMarkerSnippet(framework, checksum = null) {
|
|
|
2994
2995
|
if (checksum !== null && checksum !== "") {
|
|
2995
2996
|
statements.push(`window.__PATCHSTACK_BUILD__=${JSON.stringify(checksum)};`);
|
|
2996
2997
|
}
|
|
2998
|
+
if (framework === ASTRO) {
|
|
2999
|
+
return `{${gate} && (
|
|
3000
|
+
<script is:inline ${MARKER_ATTR}="true">${statements.join("")}</script>
|
|
3001
|
+
)}`;
|
|
3002
|
+
}
|
|
2997
3003
|
return `{${gate} && (
|
|
2998
3004
|
<script
|
|
2999
3005
|
${MARKER_ATTR}="true"
|
|
@@ -3005,8 +3011,9 @@ var PROD_MARKER_GLOBAL = "__PATCHSTACK_PROD__";
|
|
|
3005
3011
|
var REGION_OPEN = "{/* #region patchstack (managed by patchstack-connect \u2014 do not edit) */}";
|
|
3006
3012
|
var REGION_CLOSE = "{/* #endregion patchstack */}";
|
|
3007
3013
|
var REGION_RE = /[ \t]*\{\/\* #region patchstack[\s\S]*?#endregion patchstack \*\/\}\n?/g;
|
|
3008
|
-
function findJsxShellAnchor(source, tagName) {
|
|
3014
|
+
function findJsxShellAnchor(source, tagName, from = 0) {
|
|
3009
3015
|
const candidates = new RegExp(`^([ \\t]*)<${tagName}(?=[\\s/>])`, "gm");
|
|
3016
|
+
candidates.lastIndex = from;
|
|
3010
3017
|
let candidate;
|
|
3011
3018
|
while ((candidate = candidates.exec(source)) !== null) {
|
|
3012
3019
|
let quote = null;
|
|
@@ -3060,8 +3067,14 @@ function findJsxShellAnchor(source, tagName) {
|
|
|
3060
3067
|
}
|
|
3061
3068
|
return null;
|
|
3062
3069
|
}
|
|
3070
|
+
function astroTemplateStart(source) {
|
|
3071
|
+
const open2 = /^\s*---[ \t]*\r?\n/.exec(source);
|
|
3072
|
+
if (open2 === null) return 0;
|
|
3073
|
+
const close = /^---[ \t]*$/m.exec(source.slice(open2[0].length));
|
|
3074
|
+
return close === null ? 0 : open2[0].length + close.index + close[0].length;
|
|
3075
|
+
}
|
|
3063
3076
|
function ensureMarkerInJsxShell(source, framework, checksum = null) {
|
|
3064
|
-
if (!
|
|
3077
|
+
if (!hasEditableShell(framework)) {
|
|
3065
3078
|
return { source, action: "unsupported" };
|
|
3066
3079
|
}
|
|
3067
3080
|
const stripped = source.replace(REGION_RE, "");
|
|
@@ -3069,8 +3082,9 @@ function ensureMarkerInJsxShell(source, framework, checksum = null) {
|
|
|
3069
3082
|
return { source: stripped, action: "manual" };
|
|
3070
3083
|
}
|
|
3071
3084
|
const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework, checksum).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
|
|
3085
|
+
const from = framework === ASTRO ? astroTemplateStart(stripped) : 0;
|
|
3072
3086
|
for (const tagName of ["head", "body"]) {
|
|
3073
|
-
const anchor = findJsxShellAnchor(stripped, tagName);
|
|
3087
|
+
const anchor = findJsxShellAnchor(stripped, tagName, from);
|
|
3074
3088
|
if (anchor === null) continue;
|
|
3075
3089
|
const indent = `${anchor.indent} `;
|
|
3076
3090
|
const remainder = stripped.slice(anchor.end);
|
|
@@ -3119,6 +3133,16 @@ function rootIdentifier(node, ts) {
|
|
|
3119
3133
|
}
|
|
3120
3134
|
return void 0;
|
|
3121
3135
|
}
|
|
3136
|
+
function rootIdentifierNode(node, ts) {
|
|
3137
|
+
let cur = node;
|
|
3138
|
+
while (cur) {
|
|
3139
|
+
if (ts.isIdentifier(cur)) return cur;
|
|
3140
|
+
if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur) || ts.isCallExpression(cur) || ts.isNewExpression(cur) || ts.isNonNullExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAwaitExpression(cur)) {
|
|
3141
|
+
cur = cur.expression;
|
|
3142
|
+
} else return void 0;
|
|
3143
|
+
}
|
|
3144
|
+
return void 0;
|
|
3145
|
+
}
|
|
3122
3146
|
function spanOf(node) {
|
|
3123
3147
|
const out = { line: lineOf(node) };
|
|
3124
3148
|
try {
|
|
@@ -3223,6 +3247,15 @@ function opCallOf(propAccess, ts) {
|
|
|
3223
3247
|
const p = propAccess?.parent;
|
|
3224
3248
|
return p && ts.isCallExpression(p) && p.expression === propAccess ? p : propAccess;
|
|
3225
3249
|
}
|
|
3250
|
+
function localCallIdentifiers(node, ts) {
|
|
3251
|
+
const out = [];
|
|
3252
|
+
const visit = (n) => {
|
|
3253
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) out.push(n.expression);
|
|
3254
|
+
ts.forEachChild(n, visit);
|
|
3255
|
+
};
|
|
3256
|
+
visit(node);
|
|
3257
|
+
return out;
|
|
3258
|
+
}
|
|
3226
3259
|
function localCalls(node, ts) {
|
|
3227
3260
|
const names = [];
|
|
3228
3261
|
const visit = (n) => {
|
|
@@ -3259,8 +3292,9 @@ function routeFromFilePath(relFile) {
|
|
|
3259
3292
|
segs = dirs.slice(at("routes") + 1);
|
|
3260
3293
|
} else if (at("pages") !== -1) {
|
|
3261
3294
|
segs = [...dirs.slice(at("pages") + 1), ...base === "index" ? [] : [base]];
|
|
3262
|
-
} else if (
|
|
3263
|
-
|
|
3295
|
+
} else if (nuxtRoot(dirs) !== void 0) {
|
|
3296
|
+
const root = nuxtRoot(dirs);
|
|
3297
|
+
segs = [...root, ...base === "index" ? [] : [base.replace(/\.(get|post|put|patch|delete|head|options)$/i, "")]];
|
|
3264
3298
|
}
|
|
3265
3299
|
if (!segs) return {};
|
|
3266
3300
|
segs = segs.filter((s) => !(s.startsWith("(") && s.endsWith(")")) && !s.startsWith("@") && !s.startsWith("_"));
|
|
@@ -3276,6 +3310,21 @@ function routeFromFilePath(relFile) {
|
|
|
3276
3310
|
const route = "/" + mapped.join("/");
|
|
3277
3311
|
return { route: route.length > 1 ? route.replace(/\/+$/, "") : "/", dynamic };
|
|
3278
3312
|
}
|
|
3313
|
+
function isPagesApiFile(relFile) {
|
|
3314
|
+
const dirs = relFile.split(/[\\/]/).filter(Boolean).slice(0, -1);
|
|
3315
|
+
const i = dirs.lastIndexOf("pages");
|
|
3316
|
+
return i !== -1 && dirs[i + 1] === "api";
|
|
3317
|
+
}
|
|
3318
|
+
function nuxtRoot(dirs) {
|
|
3319
|
+
const i = dirs.lastIndexOf("server");
|
|
3320
|
+
if (i === -1) return void 0;
|
|
3321
|
+
if (dirs[i + 1] === "api") return dirs.slice(i + 1);
|
|
3322
|
+
if (dirs[i + 1] === "routes") return dirs.slice(i + 2);
|
|
3323
|
+
return void 0;
|
|
3324
|
+
}
|
|
3325
|
+
function isRoutePath(route) {
|
|
3326
|
+
return route.startsWith("/") || route === "*";
|
|
3327
|
+
}
|
|
3279
3328
|
function routeFromChain(expr, ts) {
|
|
3280
3329
|
let cur = expr;
|
|
3281
3330
|
while (cur && ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) {
|
|
@@ -3336,7 +3385,35 @@ function detectFramework(cwd) {
|
|
|
3336
3385
|
}
|
|
3337
3386
|
return "unknown";
|
|
3338
3387
|
}
|
|
3339
|
-
var isSourceFile = (name) => /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(name) && !name.endsWith(".d.ts");
|
|
3388
|
+
var isSourceFile = (name) => /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(name) && !name.endsWith(".d.ts") || isComponentFile(name);
|
|
3389
|
+
var isComponentFile = (name) => /\.(vue|svelte|astro)$/.test(name);
|
|
3390
|
+
function componentScript(text, file) {
|
|
3391
|
+
const keep = [];
|
|
3392
|
+
if (file.endsWith(".astro")) {
|
|
3393
|
+
const open2 = /^?\s*---[^\S\r\n]*\r?\n/.exec(text);
|
|
3394
|
+
if (open2) {
|
|
3395
|
+
const start = open2[0].length;
|
|
3396
|
+
const close = /\r?\n---[^\S\r\n]*(\r?\n|$)/g;
|
|
3397
|
+
close.lastIndex = start - 1;
|
|
3398
|
+
const end = close.exec(text);
|
|
3399
|
+
if (!end) return null;
|
|
3400
|
+
if (end.index > start) keep.push([start, end.index]);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
const opening = /<script\b[^>]*>/gi;
|
|
3404
|
+
for (let m = opening.exec(text); m; m = opening.exec(text)) {
|
|
3405
|
+
const start = m.index + m[0].length;
|
|
3406
|
+
const closing = /<\/script\s*>/gi;
|
|
3407
|
+
closing.lastIndex = start;
|
|
3408
|
+
const end = closing.exec(text);
|
|
3409
|
+
if (!end) return null;
|
|
3410
|
+
keep.push([start, end.index]);
|
|
3411
|
+
opening.lastIndex = end.index + end[0].length;
|
|
3412
|
+
}
|
|
3413
|
+
let out = text.replace(/[^\r\n]/g, " ");
|
|
3414
|
+
for (const [start, end] of keep) out = out.slice(0, start) + text.slice(start, end) + out.slice(end);
|
|
3415
|
+
return out;
|
|
3416
|
+
}
|
|
3340
3417
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3341
3418
|
"node_modules",
|
|
3342
3419
|
"dist",
|
|
@@ -5702,7 +5779,7 @@ var WIDGET_FILE_CANDIDATES = {
|
|
|
5702
5779
|
"react-router": ["app/root.tsx", "src/root.tsx"],
|
|
5703
5780
|
"tanstack-start": ["src/routes/__root.tsx", "app/routes/__root.tsx"],
|
|
5704
5781
|
sveltekit: ["src/app.html"],
|
|
5705
|
-
astro: ["src/layouts/Layout.astro"],
|
|
5782
|
+
astro: ["src/layouts/Layout.astro", "src/layouts/Base.astro", "src/layouts/BaseLayout.astro"],
|
|
5706
5783
|
gatsby: ["src/html.js"]
|
|
5707
5784
|
};
|
|
5708
5785
|
var GENERIC_WIDGET_FILES = ["index.html", "public/index.html"];
|
|
@@ -5846,6 +5923,24 @@ function resolveWidgetFileHint(cwd, framework) {
|
|
|
5846
5923
|
return candidate;
|
|
5847
5924
|
}
|
|
5848
5925
|
}
|
|
5926
|
+
return framework === "astro" ? findAstroDocumentLayout(cwd) : null;
|
|
5927
|
+
}
|
|
5928
|
+
function findAstroDocumentLayout(cwd) {
|
|
5929
|
+
const dir = path12.join("src", "layouts");
|
|
5930
|
+
let names;
|
|
5931
|
+
try {
|
|
5932
|
+
names = readdirSync5(path12.join(cwd, dir)).filter((name) => name.endsWith(".astro")).sort();
|
|
5933
|
+
} catch {
|
|
5934
|
+
return null;
|
|
5935
|
+
}
|
|
5936
|
+
for (const name of names) {
|
|
5937
|
+
try {
|
|
5938
|
+
if (/<\/body>/i.test(readFileSync11(path12.join(cwd, dir, name), "utf8"))) {
|
|
5939
|
+
return path12.posix.join("src", "layouts", name);
|
|
5940
|
+
}
|
|
5941
|
+
} catch {
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5849
5944
|
return null;
|
|
5850
5945
|
}
|
|
5851
5946
|
async function collectGuideState(cwd) {
|
|
@@ -6032,7 +6127,7 @@ function guideMissing(state, known = {}) {
|
|
|
6032
6127
|
if (needsSourceProductionMarker(state) && !state.productionMarkerWired) {
|
|
6033
6128
|
const gate = productionGate(state.framework);
|
|
6034
6129
|
missing.push(
|
|
6035
|
-
|
|
6130
|
+
hasEditableShell(state.framework) ? {
|
|
6036
6131
|
text: "Your live app does not tell Patchstack it is live yet",
|
|
6037
6132
|
hint: [`Run: npx @patchstack/connect scan (it edits ${state.widgetFileHint})`],
|
|
6038
6133
|
detail: [`Or add inside <head>:`, ...buildSourceMarkerSnippet(state.framework).split("\n").map((line) => ` ${line}`)]
|
|
@@ -7017,6 +7112,97 @@ function npmPackageOf(spec) {
|
|
|
7017
7112
|
|
|
7018
7113
|
// src/map/sinks.ts
|
|
7019
7114
|
import { createHash as createHash4 } from "crypto";
|
|
7115
|
+
|
|
7116
|
+
// src/map/scope.ts
|
|
7117
|
+
var scopeCache = /* @__PURE__ */ new WeakMap();
|
|
7118
|
+
function declarationOf(id, ts) {
|
|
7119
|
+
if (!id || !ts.isIdentifier(id)) return void 0;
|
|
7120
|
+
const name = id.text;
|
|
7121
|
+
for (let cur = id.parent; cur; cur = cur.parent) {
|
|
7122
|
+
const declared = declaredIn(cur, ts);
|
|
7123
|
+
if (declared?.has(name)) return declared.get(name);
|
|
7124
|
+
}
|
|
7125
|
+
return void 0;
|
|
7126
|
+
}
|
|
7127
|
+
function isGlobal(node, name, ts) {
|
|
7128
|
+
if (ts.isIdentifier(node)) return node.text === name && declarationOf(node, ts) === void 0;
|
|
7129
|
+
return ts.isPropertyAccessExpression(node) && node.name.text === name && ts.isIdentifier(node.expression) && node.expression.text === "globalThis" && declarationOf(node.expression, ts) === void 0;
|
|
7130
|
+
}
|
|
7131
|
+
function boundIdentifiers(name, ts) {
|
|
7132
|
+
if (!name) return [];
|
|
7133
|
+
if (ts.isIdentifier(name)) return [name];
|
|
7134
|
+
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
7135
|
+
return name.elements.flatMap((el) => ts.isBindingElement(el) ? boundIdentifiers(el.name, ts) : []);
|
|
7136
|
+
}
|
|
7137
|
+
return [];
|
|
7138
|
+
}
|
|
7139
|
+
function isFunctionScope(n, ts) {
|
|
7140
|
+
return ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isMethodDeclaration(n) || ts.isConstructorDeclaration(n) || ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n) || ts.isSourceFile(n);
|
|
7141
|
+
}
|
|
7142
|
+
function declaredIn(n, ts) {
|
|
7143
|
+
const cached2 = scopeCache.get(n);
|
|
7144
|
+
if (cached2) return cached2;
|
|
7145
|
+
const out = /* @__PURE__ */ new Map();
|
|
7146
|
+
const add = (ids) => {
|
|
7147
|
+
for (const i of ids) if (!out.has(i.text)) out.set(i.text, i);
|
|
7148
|
+
};
|
|
7149
|
+
if (isFunctionScope(n, ts)) {
|
|
7150
|
+
if ((ts.isFunctionExpression(n) || ts.isClassExpression?.(n)) && n.name) add([n.name]);
|
|
7151
|
+
for (const p of n.parameters ?? []) add(boundIdentifiers(p.name, ts));
|
|
7152
|
+
collectVarScoped(ts.isSourceFile(n) ? n : n.body, ts, add);
|
|
7153
|
+
if (ts.isSourceFile(n)) collectLexical(n.statements, ts, add, true);
|
|
7154
|
+
} else if (ts.isBlock(n) || ts.isModuleBlock(n) || ts.isCaseBlock(n)) {
|
|
7155
|
+
const statements = ts.isCaseBlock(n) ? n.clauses.flatMap((c) => [...c.statements]) : n.statements;
|
|
7156
|
+
collectLexical(statements, ts, add, false);
|
|
7157
|
+
} else if (ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n)) {
|
|
7158
|
+
const init = n.initializer;
|
|
7159
|
+
if (init && ts.isVariableDeclarationList(init) && isLexicalList(init, ts)) {
|
|
7160
|
+
for (const d of init.declarations) add(boundIdentifiers(d.name, ts));
|
|
7161
|
+
}
|
|
7162
|
+
} else if (ts.isCatchClause(n)) {
|
|
7163
|
+
if (n.variableDeclaration) add(boundIdentifiers(n.variableDeclaration.name, ts));
|
|
7164
|
+
} else if (ts.isClassExpression?.(n) && n.name) {
|
|
7165
|
+
add([n.name]);
|
|
7166
|
+
} else {
|
|
7167
|
+
return void 0;
|
|
7168
|
+
}
|
|
7169
|
+
scopeCache.set(n, out);
|
|
7170
|
+
return out;
|
|
7171
|
+
}
|
|
7172
|
+
function isLexicalList(list, ts) {
|
|
7173
|
+
return (list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
|
|
7174
|
+
}
|
|
7175
|
+
function collectLexical(statements, ts, add, topLevel) {
|
|
7176
|
+
for (const s of statements ?? []) {
|
|
7177
|
+
if (ts.isVariableStatement(s) && isLexicalList(s.declarationList, ts)) {
|
|
7178
|
+
for (const d of s.declarationList.declarations) add(boundIdentifiers(d.name, ts));
|
|
7179
|
+
} else if ((ts.isClassDeclaration(s) || ts.isFunctionDeclaration(s) || ts.isEnumDeclaration(s)) && s.name) {
|
|
7180
|
+
add([s.name]);
|
|
7181
|
+
} else if (topLevel && ts.isImportDeclaration(s) && s.importClause) {
|
|
7182
|
+
const clause = s.importClause;
|
|
7183
|
+
if (clause.name) add([clause.name]);
|
|
7184
|
+
const bindings = clause.namedBindings;
|
|
7185
|
+
if (bindings && ts.isNamespaceImport(bindings)) add([bindings.name]);
|
|
7186
|
+
if (bindings && ts.isNamedImports(bindings)) add(bindings.elements.map((e) => e.name));
|
|
7187
|
+
} else if (topLevel && ts.isImportEqualsDeclaration(s)) {
|
|
7188
|
+
add([s.name]);
|
|
7189
|
+
}
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
function collectVarScoped(node, ts, add) {
|
|
7193
|
+
const visit = (n) => {
|
|
7194
|
+
if (!n) return;
|
|
7195
|
+
if (ts.isVariableDeclarationList(n) && !isLexicalList(n, ts)) {
|
|
7196
|
+
for (const d of n.declarations) add(boundIdentifiers(d.name, ts));
|
|
7197
|
+
}
|
|
7198
|
+
if (n !== node && isFunctionScope(n, ts)) return;
|
|
7199
|
+
if (ts.isClassDeclaration(n) || ts.isClassExpression?.(n)) return;
|
|
7200
|
+
ts.forEachChild(n, visit);
|
|
7201
|
+
};
|
|
7202
|
+
visit(node);
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
// src/map/sinks.ts
|
|
7020
7206
|
var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
|
|
7021
7207
|
var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
|
|
7022
7208
|
var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
|
|
@@ -7059,29 +7245,39 @@ function recognizedSinkKinds(pkg) {
|
|
|
7059
7245
|
return kinds;
|
|
7060
7246
|
}
|
|
7061
7247
|
function collectLocalSinks(sf, ts, bindings, ctx) {
|
|
7062
|
-
const
|
|
7063
|
-
const
|
|
7064
|
-
|
|
7248
|
+
const byDeclaration = /* @__PURE__ */ new Map();
|
|
7249
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7250
|
+
const record = (name, body, topLevel) => {
|
|
7251
|
+
const sinks = directSinks(body, ts, bindings, ctx);
|
|
7252
|
+
byDeclaration.set(name, sinks);
|
|
7253
|
+
if (topLevel || !byName.has(name.text)) byName.set(name.text, sinks);
|
|
7254
|
+
};
|
|
7255
|
+
const visit = (node, depth) => {
|
|
7256
|
+
const topLevel = depth === 0;
|
|
7257
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body) record(node.name, node.body, topLevel);
|
|
7065
7258
|
else if (ts.isVariableStatement(node)) {
|
|
7066
7259
|
for (const decl of node.declarationList.declarations) {
|
|
7067
7260
|
if (ts.isIdentifier(decl.name) && decl.initializer && isFnLike(decl.initializer, ts)) {
|
|
7068
|
-
|
|
7261
|
+
record(decl.name, decl.initializer.body, topLevel);
|
|
7069
7262
|
}
|
|
7070
7263
|
}
|
|
7071
7264
|
}
|
|
7072
|
-
ts.forEachChild(node, visit);
|
|
7265
|
+
ts.forEachChild(node, (child) => visit(child, depth + (ts.isSourceFile(node) ? 0 : 1)));
|
|
7073
7266
|
};
|
|
7074
|
-
visit(sf);
|
|
7075
|
-
return
|
|
7267
|
+
visit(sf, 0);
|
|
7268
|
+
return { byDeclaration, byName };
|
|
7076
7269
|
}
|
|
7077
7270
|
function sinksFrom(arrowOrNode, ts, localSinks, bindings, ctx) {
|
|
7078
7271
|
if (!arrowOrNode) return [];
|
|
7079
7272
|
const body = arrowOrNode.isSyntheticBody ? arrowOrNode.body : isFnLike(arrowOrNode, ts) ? arrowOrNode.body : arrowOrNode;
|
|
7080
7273
|
if (!body) return [];
|
|
7081
7274
|
const sinks = directSinks(body, ts, bindings, ctx);
|
|
7082
|
-
for (const
|
|
7083
|
-
|
|
7084
|
-
|
|
7275
|
+
for (const callee of localCallIdentifiers(body, ts)) {
|
|
7276
|
+
const called = callee.text;
|
|
7277
|
+
const declaration = declarationOf(callee, ts);
|
|
7278
|
+
const helper = declaration === void 0 ? void 0 : localSinks.byDeclaration.get(declaration);
|
|
7279
|
+
for (const s of helper ?? []) sinks.push(s);
|
|
7280
|
+
if (ctx && (declaration === void 0 || isModuleLevel(declaration, ts))) {
|
|
7085
7281
|
const spec = bindings.resolve(called);
|
|
7086
7282
|
if (spec && spec.startsWith(".")) {
|
|
7087
7283
|
for (const s of ctx.graph.importedSinks(ctx.file, spec, bindings.exportNameOf(called) ?? called)) sinks.push(s);
|
|
@@ -7098,6 +7294,22 @@ function sinksFrom(arrowOrNode, ts, localSinks, bindings, ctx) {
|
|
|
7098
7294
|
}
|
|
7099
7295
|
return dedupeSinks(sinks, ctx?.owner ?? "");
|
|
7100
7296
|
}
|
|
7297
|
+
function isContextRequest(node, ts) {
|
|
7298
|
+
if (!ts.isPropertyAccessExpression(node) || node.name.text !== "req" || !ts.isIdentifier(node.expression)) return false;
|
|
7299
|
+
const declaration = declarationOf(node.expression, ts);
|
|
7300
|
+
for (let cur = declaration?.parent; cur; cur = cur.parent) {
|
|
7301
|
+
if (ts.isParameter(cur)) return true;
|
|
7302
|
+
if (!ts.isBindingElement(cur) && !ts.isObjectBindingPattern(cur) && !ts.isArrayBindingPattern(cur)) return false;
|
|
7303
|
+
}
|
|
7304
|
+
return false;
|
|
7305
|
+
}
|
|
7306
|
+
function isModuleLevel(declaration, ts) {
|
|
7307
|
+
for (let cur = declaration?.parent; cur; cur = cur.parent) {
|
|
7308
|
+
if (ts.isSourceFile(cur)) return true;
|
|
7309
|
+
if (ts.isFunctionLike(cur) || ts.isBlock(cur) || ts.isClassLike(cur)) return false;
|
|
7310
|
+
}
|
|
7311
|
+
return false;
|
|
7312
|
+
}
|
|
7101
7313
|
function namespaceMemberCalls(node, ts) {
|
|
7102
7314
|
const out = [];
|
|
7103
7315
|
const visit = (n) => {
|
|
@@ -7158,7 +7370,7 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
7158
7370
|
const prismaLikely = b.pkg === "@prisma/client" || !b.pkg && (bindings.imports.has("@prisma/client") || /prisma/i.test(b.root ?? ""));
|
|
7159
7371
|
if (prismaLikely) push({ kind: "db", ...b.pkg ? { provider: "prisma" } : {}, package: "@prisma/client", table: callee.expression.name.text, op: method, attribution: b.pkg ? "import" : "inferred", ...spanOf(n) });
|
|
7160
7372
|
}
|
|
7161
|
-
if (method === "query" || method === "execute") {
|
|
7373
|
+
if ((method === "query" || method === "execute") && !isContextRequest(callee.expression, ts)) {
|
|
7162
7374
|
const pkg = b.pkg ?? infer("db");
|
|
7163
7375
|
const attribution = attributionOf(b, pkg);
|
|
7164
7376
|
push({ kind: "db", ...dbApi(pkg, attribution), package: pkg, op: method, attribution, ...spanOf(n) });
|
|
@@ -7298,114 +7510,6 @@ function dedupeSinks(sinks, owner) {
|
|
|
7298
7510
|
return out;
|
|
7299
7511
|
}
|
|
7300
7512
|
|
|
7301
|
-
// src/map/module-graph.ts
|
|
7302
|
-
import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
|
|
7303
|
-
import { dirname as dirname8, join as join16, relative as relative5, resolve as resolvePath } from "path";
|
|
7304
|
-
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
7305
|
-
function createModuleGraph(ts, opts) {
|
|
7306
|
-
const cache = /* @__PURE__ */ new Map();
|
|
7307
|
-
const load = (file) => {
|
|
7308
|
-
if (cache.has(file)) return cache.get(file) ?? null;
|
|
7309
|
-
let entry = null;
|
|
7310
|
-
try {
|
|
7311
|
-
const text = readFileSync14(file, "utf8");
|
|
7312
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
7313
|
-
const bindings = buildModuleBindings(sf, ts);
|
|
7314
|
-
entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
|
|
7315
|
-
} catch {
|
|
7316
|
-
entry = null;
|
|
7317
|
-
}
|
|
7318
|
-
cache.set(file, entry);
|
|
7319
|
-
return entry;
|
|
7320
|
-
};
|
|
7321
|
-
const resolveInProject = (fromFile, specifier) => {
|
|
7322
|
-
const target = resolveRelativeModule(fromFile, specifier);
|
|
7323
|
-
if (!target) return void 0;
|
|
7324
|
-
if (!opts.followOutside) {
|
|
7325
|
-
let real = target;
|
|
7326
|
-
try {
|
|
7327
|
-
real = realpathSync5(target);
|
|
7328
|
-
} catch {
|
|
7329
|
-
}
|
|
7330
|
-
if (!isInside(real, opts.boundary)) return void 0;
|
|
7331
|
-
}
|
|
7332
|
-
return target;
|
|
7333
|
-
};
|
|
7334
|
-
return {
|
|
7335
|
-
resolveLocal: resolveInProject,
|
|
7336
|
-
importedSinks(fromFile, specifier, exportName) {
|
|
7337
|
-
const target = resolveInProject(fromFile, specifier);
|
|
7338
|
-
if (!target) return [];
|
|
7339
|
-
const mod = load(target);
|
|
7340
|
-
if (!mod) return [];
|
|
7341
|
-
const collected = [...mod.fnSinks.get(exportName) ?? []];
|
|
7342
|
-
for (const callee of mod.calleesOf.get(exportName) ?? []) {
|
|
7343
|
-
for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
|
|
7344
|
-
}
|
|
7345
|
-
const rel = relative5(opts.cwd, target);
|
|
7346
|
-
return collected.map((s) => ({ ...s, file: rel }));
|
|
7347
|
-
},
|
|
7348
|
-
// A different question about the same module: not "what sinks are in there" but "what does this
|
|
7349
|
-
// export TRACE TO". The common AI-built layout puts the client in a lib file —
|
|
7350
|
-
// `export const db = createClient(...)` in `lib/db.ts`, imported everywhere — so the receiver of
|
|
7351
|
-
// `db.from('orders').insert(...)` resolves to a relative specifier and nothing else. Refusing it (as
|
|
7352
|
-
// an unattributable receiver) is right for app code but wrong here: one hop away it is a real
|
|
7353
|
-
// dependency, and that chain is import-to-import, fully static — evidence, not inference.
|
|
7354
|
-
importedBinding(fromFile, specifier, exportName) {
|
|
7355
|
-
const target = resolveInProject(fromFile, specifier);
|
|
7356
|
-
if (!target) return void 0;
|
|
7357
|
-
const mod = load(target);
|
|
7358
|
-
if (!mod) return void 0;
|
|
7359
|
-
const pkg = npmPackageOf(mod.bindings.resolve(exportName));
|
|
7360
|
-
if (pkg === void 0) return void 0;
|
|
7361
|
-
return {
|
|
7362
|
-
package: pkg,
|
|
7363
|
-
origin: mod.bindings.originOf(exportName) ?? "direct",
|
|
7364
|
-
name: mod.bindings.exportNameOf(exportName) ?? exportName
|
|
7365
|
-
};
|
|
7366
|
-
}
|
|
7367
|
-
};
|
|
7368
|
-
}
|
|
7369
|
-
function resolveRelativeModule(fromFile, spec) {
|
|
7370
|
-
if (!spec.startsWith(".")) return void 0;
|
|
7371
|
-
const dir = dirname8(fromFile);
|
|
7372
|
-
const base = resolvePath(dir, spec);
|
|
7373
|
-
const candidates = [];
|
|
7374
|
-
const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
|
|
7375
|
-
if (jsLike) {
|
|
7376
|
-
const stem = base.slice(0, -jsLike[0].length);
|
|
7377
|
-
for (const e of RESOLVE_EXTS) candidates.push(stem + e);
|
|
7378
|
-
}
|
|
7379
|
-
candidates.push(base);
|
|
7380
|
-
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
7381
|
-
for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
|
|
7382
|
-
for (const c of candidates) {
|
|
7383
|
-
try {
|
|
7384
|
-
if (statSync7(c).isFile()) return c;
|
|
7385
|
-
} catch {
|
|
7386
|
-
}
|
|
7387
|
-
}
|
|
7388
|
-
return void 0;
|
|
7389
|
-
}
|
|
7390
|
-
function collectCallees(sf, ts) {
|
|
7391
|
-
const map = /* @__PURE__ */ new Map();
|
|
7392
|
-
const visit = (node) => {
|
|
7393
|
-
let name;
|
|
7394
|
-
let body;
|
|
7395
|
-
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
|
|
7396
|
-
name = node.name.text;
|
|
7397
|
-
body = node.body;
|
|
7398
|
-
} else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isFnLike(node.initializer, ts)) {
|
|
7399
|
-
name = node.name.text;
|
|
7400
|
-
body = node.initializer.body;
|
|
7401
|
-
}
|
|
7402
|
-
if (name && body) map.set(name, localCalls(body, ts));
|
|
7403
|
-
ts.forEachChild(node, visit);
|
|
7404
|
-
};
|
|
7405
|
-
visit(sf);
|
|
7406
|
-
return map;
|
|
7407
|
-
}
|
|
7408
|
-
|
|
7409
7513
|
// src/map/coordinates.ts
|
|
7410
7514
|
function runtimeCoordinate(source, path17) {
|
|
7411
7515
|
if (/\[\d*\]/.test(path17)) {
|
|
@@ -7477,7 +7581,9 @@ function inputsFromValidator(validatorCall, ts, bindings) {
|
|
|
7477
7581
|
}
|
|
7478
7582
|
function inputsFromHandler(params, body, ts, bindings, opts = {}) {
|
|
7479
7583
|
const schemaSource = opts.validatorSource ?? "json-body";
|
|
7480
|
-
const
|
|
7584
|
+
const inline = zodObjectFields(body, ts, bindings);
|
|
7585
|
+
const referenced = inline.length > 0 ? { fields: [], unresolved: false } : referencedSchemaFields(params, body, ts, bindings);
|
|
7586
|
+
const schemaFields = inline.length > 0 ? inline : referenced.fields;
|
|
7481
7587
|
const reads = requestMemberAccesses(params, body, ts, opts);
|
|
7482
7588
|
const byId = /* @__PURE__ */ new Map();
|
|
7483
7589
|
const put = (name, source, extra = {}) => {
|
|
@@ -7490,7 +7596,58 @@ function inputsFromHandler(params, body, ts, bindings, opts = {}) {
|
|
|
7490
7596
|
put(name, source ?? schemaSource, shape);
|
|
7491
7597
|
}
|
|
7492
7598
|
for (const { name, sources } of reads) for (const source of sources) put(name, source);
|
|
7493
|
-
return [...byId.values()];
|
|
7599
|
+
return { inputs: [...byId.values()], schemaUnresolved: referenced.unresolved };
|
|
7600
|
+
}
|
|
7601
|
+
var SCHEMA_METHODS = /* @__PURE__ */ new Set(["parse", "safeParse", "parseAsync", "safeParseAsync"]);
|
|
7602
|
+
function referencedSchemaFields(params, body, ts, bindings) {
|
|
7603
|
+
if (!body) return { fields: [], unresolved: false };
|
|
7604
|
+
const paramDecls = /* @__PURE__ */ new Set();
|
|
7605
|
+
for (const p of params ?? []) {
|
|
7606
|
+
if (!p?.name) continue;
|
|
7607
|
+
if (ts.isIdentifier(p.name)) paramDecls.add(p.name);
|
|
7608
|
+
else if (ts.isObjectBindingPattern(p.name)) {
|
|
7609
|
+
for (const el of p.name.elements) if (ts.isIdentifier(el.name)) paramDecls.add(el.name);
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
const fromParams = (e) => {
|
|
7613
|
+
const root = rootIdentifierNode(e, ts);
|
|
7614
|
+
const declaration = root ? declarationOf(root, ts) : void 0;
|
|
7615
|
+
if (declaration === void 0) return false;
|
|
7616
|
+
if (paramDecls.has(declaration)) return true;
|
|
7617
|
+
const owner = declaration.parent;
|
|
7618
|
+
if (owner && ts.isVariableDeclaration(owner) && owner.name === declaration && owner.initializer) {
|
|
7619
|
+
const inner = rootIdentifierNode(owner.initializer, ts);
|
|
7620
|
+
const innerDeclaration = inner ? declarationOf(inner, ts) : void 0;
|
|
7621
|
+
return innerDeclaration !== void 0 && paramDecls.has(innerDeclaration);
|
|
7622
|
+
}
|
|
7623
|
+
return false;
|
|
7624
|
+
};
|
|
7625
|
+
let fields = [];
|
|
7626
|
+
let unresolved = false;
|
|
7627
|
+
const visit = (n) => {
|
|
7628
|
+
if (fields.length > 0) return;
|
|
7629
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && SCHEMA_METHODS.has(n.expression.name.text) && ts.isIdentifier(n.expression.expression) && n.arguments.some((a) => fromParams(a))) {
|
|
7630
|
+
const declaration = declarationOf(n.expression.expression, ts);
|
|
7631
|
+
const owner = declaration?.parent;
|
|
7632
|
+
if (owner && ts.isVariableDeclaration(owner) && owner.name === declaration && owner.initializer) {
|
|
7633
|
+
const literal = findValidatorObject(owner.initializer, ts, bindings);
|
|
7634
|
+
if (literal) fields = fieldsOfObject(literal, ts, bindings, "");
|
|
7635
|
+
else unresolved = true;
|
|
7636
|
+
} else if (declaration !== void 0 && isImported(declaration, ts)) {
|
|
7637
|
+
unresolved = true;
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
ts.forEachChild(n, visit);
|
|
7641
|
+
};
|
|
7642
|
+
visit(body);
|
|
7643
|
+
return { fields, unresolved: fields.length === 0 && unresolved };
|
|
7644
|
+
}
|
|
7645
|
+
function isImported(declaration, ts) {
|
|
7646
|
+
for (let cur = declaration?.parent; cur; cur = cur.parent) {
|
|
7647
|
+
if (ts.isImportDeclaration(cur)) return true;
|
|
7648
|
+
if (ts.isSourceFile(cur) || ts.isStatement(cur)) return false;
|
|
7649
|
+
}
|
|
7650
|
+
return false;
|
|
7494
7651
|
}
|
|
7495
7652
|
function findValidatorObject(node, ts, bindings) {
|
|
7496
7653
|
let found = null;
|
|
@@ -7569,16 +7726,29 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7569
7726
|
out.set(name, list);
|
|
7570
7727
|
};
|
|
7571
7728
|
const p0 = params?.[0];
|
|
7572
|
-
const
|
|
7729
|
+
const reqDecl = p0 && ts.isIdentifier(p0.name) ? p0.name : void 0;
|
|
7730
|
+
const requestDecls = new Set(reqDecl ? [reqDecl] : []);
|
|
7731
|
+
const isRequest = (e) => ts.isIdentifier(e) && requestDecls.has(declarationOf(e, ts));
|
|
7732
|
+
const isRequestObject = (e) => isRequest(e) || ts.isPropertyAccessExpression(e) && e.name.text === "req" && isRequest(e.expression);
|
|
7733
|
+
const urlDecls = /* @__PURE__ */ new Set();
|
|
7734
|
+
const searchParamDecls = /* @__PURE__ */ new Set();
|
|
7573
7735
|
const sourceNames = /* @__PURE__ */ new Map();
|
|
7574
7736
|
const payloadNames = /* @__PURE__ */ new Set();
|
|
7575
|
-
|
|
7576
|
-
if (
|
|
7737
|
+
const sourceOf = (e) => ts.isIdentifier(e) ? sourceNames.get(declarationOf(e, ts)) : void 0;
|
|
7738
|
+
if (opts.payloadParam && p0 && ts.isIdentifier(p0.name)) payloadNames.add(p0.name);
|
|
7739
|
+
if (p0 && !reqDecl && ts.isObjectBindingPattern(p0.name)) {
|
|
7577
7740
|
for (const el of p0.name.elements) {
|
|
7578
7741
|
const key = bindingKey(el, ts);
|
|
7579
|
-
if (key
|
|
7580
|
-
|
|
7581
|
-
|
|
7742
|
+
if (!key || !ts.isIdentifier(el.name)) continue;
|
|
7743
|
+
if (REQ_SOURCES.includes(key)) sourceNames.set(el.name, namespaceSource(key));
|
|
7744
|
+
else if (key === "request") requestDecls.add(el.name);
|
|
7745
|
+
else if (key === "url") urlDecls.add(el.name);
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
for (const p of (params ?? []).slice(1)) {
|
|
7749
|
+
if (!p?.name || !ts.isObjectBindingPattern(p.name)) continue;
|
|
7750
|
+
for (const el of p.name.elements) {
|
|
7751
|
+
if (bindingKey(el, ts) === "params" && ts.isIdentifier(el.name)) sourceNames.set(el.name, "route-param");
|
|
7582
7752
|
}
|
|
7583
7753
|
}
|
|
7584
7754
|
const unwrap = (e) => {
|
|
@@ -7586,14 +7756,36 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7586
7756
|
while (cur && (ts.isAwaitExpression(cur) || ts.isAsExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7587
7757
|
return cur;
|
|
7588
7758
|
};
|
|
7589
|
-
const isPayloadExpr = (e) => ts.isIdentifier(e) && payloadNames.has(e
|
|
7590
|
-
const isReqSourceExpr = (e) => isPayloadExpr(e) || ts.isPropertyAccessExpression(e) &&
|
|
7759
|
+
const isPayloadExpr = (e) => ts.isIdentifier(e) && payloadNames.has(declarationOf(e, ts));
|
|
7760
|
+
const isReqSourceExpr = (e) => isPayloadExpr(e) || ts.isPropertyAccessExpression(e) && isRequest(e.expression) && REQ_SOURCES.includes(e.name.text) || sourceOf(e) !== void 0;
|
|
7591
7761
|
const isBodyReadCall = (e) => {
|
|
7592
7762
|
const inner = unwrap(e);
|
|
7593
|
-
return Boolean(inner && ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression) && ["json", "formData"].includes(inner.expression.name.text) &&
|
|
7763
|
+
return Boolean(inner && ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression) && ["json", "formData"].includes(inner.expression.name.text) && isRequestObject(inner.expression.expression));
|
|
7764
|
+
};
|
|
7765
|
+
const isRequestUrl = (e) => {
|
|
7766
|
+
const cur = unwrap(e);
|
|
7767
|
+
if (!cur) return false;
|
|
7768
|
+
if (ts.isIdentifier(cur)) return urlDecls.has(declarationOf(cur, ts));
|
|
7769
|
+
if (ts.isPropertyAccessExpression(cur) && cur.name.text === "nextUrl") return isRequest(cur.expression);
|
|
7770
|
+
if (ts.isNewExpression(cur) && isGlobal(cur.expression, "URL", ts)) {
|
|
7771
|
+
const [href] = cur.arguments ?? [];
|
|
7772
|
+
return Boolean(href && ts.isPropertyAccessExpression(href) && href.name.text === "url" && isRequestObject(href.expression));
|
|
7773
|
+
}
|
|
7774
|
+
return false;
|
|
7775
|
+
};
|
|
7776
|
+
const isSearchParams = (e) => {
|
|
7777
|
+
const cur = unwrap(e);
|
|
7778
|
+
if (!cur) return false;
|
|
7779
|
+
if (ts.isIdentifier(cur)) return searchParamDecls.has(declarationOf(cur, ts));
|
|
7780
|
+
return ts.isPropertyAccessExpression(cur) && cur.name.text === "searchParams" && isRequestUrl(cur.expression);
|
|
7781
|
+
};
|
|
7782
|
+
const literalArgument = (call) => {
|
|
7783
|
+
const [arg] = call.arguments ?? [];
|
|
7784
|
+
return call.arguments?.length === 1 && arg && ts.isStringLiteral(arg) ? arg.text : void 0;
|
|
7594
7785
|
};
|
|
7595
7786
|
const isCallee = (n) => Boolean(n.parent && (ts.isCallExpression(n.parent) || ts.isNewExpression(n.parent)) && n.parent.expression === n);
|
|
7596
7787
|
const ACCESSOR_SOURCES = /* @__PURE__ */ new Set(["header", "cookie", "form-body"]);
|
|
7788
|
+
const HONO_ACCESSORS = { query: "query", param: "route-param", header: "header" };
|
|
7597
7789
|
const visit = (n) => {
|
|
7598
7790
|
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression) && !isCallee(n)) {
|
|
7599
7791
|
record(n.name.text, sourceOfExpr(n.expression));
|
|
@@ -7604,13 +7796,34 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7604
7796
|
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0]) && ACCESSOR_SOURCES.has(sourceOfExpr(n.expression.expression))) {
|
|
7605
7797
|
record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
|
|
7606
7798
|
}
|
|
7799
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isSearchParams(n.expression.expression)) {
|
|
7800
|
+
const name = literalArgument(n);
|
|
7801
|
+
if (name !== void 0) record(name, "query");
|
|
7802
|
+
}
|
|
7803
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && ts.isPropertyAccessExpression(n.expression.expression) && n.expression.expression.name.text === "req" && isRequest(n.expression.expression.expression)) {
|
|
7804
|
+
const source = HONO_ACCESSORS[n.expression.name.text];
|
|
7805
|
+
const name = literalArgument(n);
|
|
7806
|
+
if (source && name !== void 0) record(name, source);
|
|
7807
|
+
}
|
|
7607
7808
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
7608
7809
|
const init = unwrap(n.initializer);
|
|
7609
|
-
if (ts.isIdentifier(n.name)
|
|
7610
|
-
|
|
7810
|
+
if (ts.isIdentifier(n.name)) {
|
|
7811
|
+
if (isRequest(init)) requestDecls.add(n.name);
|
|
7812
|
+
else if (ts.isPropertyAccessExpression(init) && isRequest(init.expression) && REQ_SOURCES.includes(init.name.text)) {
|
|
7813
|
+
sourceNames.set(n.name, namespaceSource(init.name.text));
|
|
7814
|
+
} else if (isRequestUrl(init)) urlDecls.add(n.name);
|
|
7815
|
+
else if (isSearchParams(init)) searchParamDecls.add(n.name);
|
|
7816
|
+
}
|
|
7817
|
+
if (ts.isObjectBindingPattern(n.name) && isRequestUrl(init)) {
|
|
7818
|
+
for (const el of n.name.elements) {
|
|
7819
|
+
if (bindingKey(el, ts) === "searchParams" && ts.isIdentifier(el.name)) searchParamDecls.add(el.name);
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
if (ts.isIdentifier(n.name) && isBodyReadCall(n.initializer)) sourceNames.set(n.name, bodyReadSource(n.initializer));
|
|
7823
|
+
if (ts.isObjectBindingPattern(n.name) && isRequest(init)) {
|
|
7611
7824
|
for (const el of n.name.elements) {
|
|
7612
7825
|
const key = bindingKey(el, ts);
|
|
7613
|
-
if (key && REQ_SOURCES.includes(key) && ts.isIdentifier(el.name)) sourceNames.set(el.name
|
|
7826
|
+
if (key && REQ_SOURCES.includes(key) && ts.isIdentifier(el.name)) sourceNames.set(el.name, namespaceSource(key));
|
|
7614
7827
|
}
|
|
7615
7828
|
}
|
|
7616
7829
|
if (ts.isObjectBindingPattern(n.name) && (isReqSourceExpr(init) || isBodyReadCall(n.initializer))) {
|
|
@@ -7632,7 +7845,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7632
7845
|
if (REQ_SOURCES.includes(e.name.text)) return named;
|
|
7633
7846
|
}
|
|
7634
7847
|
if (ts.isIdentifier(e)) {
|
|
7635
|
-
const recorded =
|
|
7848
|
+
const recorded = sourceOf(e);
|
|
7636
7849
|
if (recorded) return recorded;
|
|
7637
7850
|
}
|
|
7638
7851
|
return "body";
|
|
@@ -7653,6 +7866,74 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7653
7866
|
|
|
7654
7867
|
// src/map/flows.ts
|
|
7655
7868
|
var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
|
|
7869
|
+
var MAX_UNPROVEN_FLOWS_PER_ENDPOINT = 200;
|
|
7870
|
+
function boundUnprovenFlows(flows, limit) {
|
|
7871
|
+
let unproven = 0;
|
|
7872
|
+
let truncated = false;
|
|
7873
|
+
const kept = flows.filter((f) => {
|
|
7874
|
+
if (isProvenFlow(f.confidence)) return true;
|
|
7875
|
+
if (unproven >= limit) {
|
|
7876
|
+
truncated = true;
|
|
7877
|
+
return false;
|
|
7878
|
+
}
|
|
7879
|
+
unproven++;
|
|
7880
|
+
return true;
|
|
7881
|
+
});
|
|
7882
|
+
return { flows: kept, truncated };
|
|
7883
|
+
}
|
|
7884
|
+
var Roots = class {
|
|
7885
|
+
constructor(ts) {
|
|
7886
|
+
this.ts = ts;
|
|
7887
|
+
}
|
|
7888
|
+
ts;
|
|
7889
|
+
byDeclaration = /* @__PURE__ */ new Map();
|
|
7890
|
+
add(declaration, root) {
|
|
7891
|
+
if (!this.byDeclaration.has(declaration)) this.byDeclaration.set(declaration, root);
|
|
7892
|
+
}
|
|
7893
|
+
/** The root an identifier occurrence refers to, when its binding is tainted. */
|
|
7894
|
+
get(id) {
|
|
7895
|
+
const declaration = declarationOf(id, this.ts);
|
|
7896
|
+
return declaration === void 0 ? void 0 : this.byDeclaration.get(declaration);
|
|
7897
|
+
}
|
|
7898
|
+
};
|
|
7899
|
+
function reassignedDeclarations(body, ts) {
|
|
7900
|
+
const out = /* @__PURE__ */ new Set();
|
|
7901
|
+
const target = (n) => {
|
|
7902
|
+
if (!n) return;
|
|
7903
|
+
if (ts.isParenthesizedExpression(n)) return target(n.expression);
|
|
7904
|
+
if (ts.isIdentifier(n)) {
|
|
7905
|
+
const declaration = declarationOf(n, ts);
|
|
7906
|
+
if (declaration !== void 0) out.add(declaration);
|
|
7907
|
+
return;
|
|
7908
|
+
}
|
|
7909
|
+
if (ts.isObjectLiteralExpression(n)) {
|
|
7910
|
+
for (const p of n.properties) {
|
|
7911
|
+
if (ts.isShorthandPropertyAssignment(p)) target(p.name);
|
|
7912
|
+
else if (ts.isPropertyAssignment(p)) target(p.initializer);
|
|
7913
|
+
else if (ts.isSpreadAssignment(p)) target(p.expression);
|
|
7914
|
+
}
|
|
7915
|
+
return;
|
|
7916
|
+
}
|
|
7917
|
+
if (ts.isArrayLiteralExpression(n)) {
|
|
7918
|
+
for (const e of n.elements) target(ts.isSpreadElement(e) ? e.expression : e);
|
|
7919
|
+
return;
|
|
7920
|
+
}
|
|
7921
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) target(n.left);
|
|
7922
|
+
};
|
|
7923
|
+
const visit = (n) => {
|
|
7924
|
+
if (!n) return;
|
|
7925
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
|
|
7926
|
+
target(n.left);
|
|
7927
|
+
}
|
|
7928
|
+
if ((ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && (n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
7929
|
+
target(n.operand);
|
|
7930
|
+
}
|
|
7931
|
+
if ((ts.isForOfStatement(n) || ts.isForInStatement(n)) && !ts.isVariableDeclarationList(n.initializer)) target(n.initializer);
|
|
7932
|
+
ts.forEachChild(n, visit);
|
|
7933
|
+
};
|
|
7934
|
+
visit(body);
|
|
7935
|
+
return out;
|
|
7936
|
+
}
|
|
7656
7937
|
var ACCESSOR_NAMESPACES = /* @__PURE__ */ new Set(["headers", "cookies"]);
|
|
7657
7938
|
function spaceOfKey(key) {
|
|
7658
7939
|
if (key === "query") return "get";
|
|
@@ -7665,9 +7946,11 @@ function spaceOfKey(key) {
|
|
|
7665
7946
|
return void 0;
|
|
7666
7947
|
}
|
|
7667
7948
|
function linkedFlows(body, params, inputs, sinks, ts, invocations = []) {
|
|
7668
|
-
const { flows, limitations, dependencyInputFlows, dependencyInputFlowsTruncated } = linkFlows(body, params, inputs, sinks, ts, invocations);
|
|
7949
|
+
const { flows: linked, limitations, dependencyInputFlows, dependencyInputFlowsTruncated } = linkFlows(body, params, inputs, sinks, ts, invocations);
|
|
7950
|
+
const { flows, truncated } = boundUnprovenFlows(linked, MAX_UNPROVEN_FLOWS_PER_ENDPOINT);
|
|
7669
7951
|
return {
|
|
7670
7952
|
flows,
|
|
7953
|
+
...truncated ? { flowsTruncated: true } : {},
|
|
7671
7954
|
...limitations.length > 0 ? { limitations } : {},
|
|
7672
7955
|
...dependencyInputFlows.length > 0 ? { dependencyInputFlows } : {},
|
|
7673
7956
|
...dependencyInputFlowsTruncated ? { dependencyInputFlowsTruncated: true } : {}
|
|
@@ -7678,19 +7961,29 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7678
7961
|
return { flows: [], limitations: [], dependencyInputFlows: [], dependencyInputFlowsTruncated: false };
|
|
7679
7962
|
}
|
|
7680
7963
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
7681
|
-
const rootPath =
|
|
7682
|
-
const
|
|
7683
|
-
|
|
7964
|
+
const rootPath = new Roots(ts);
|
|
7965
|
+
const reassigned = reassignedDeclarations(bodyNode, ts);
|
|
7966
|
+
const addRoot = (declaration, path17, space, accessor = false, request = false, inherited = false, url = false) => {
|
|
7967
|
+
const changed = inherited || reassigned.has(declaration);
|
|
7968
|
+
rootPath.add(declaration, { path: path17, space, ...accessor ? { accessor } : {}, ...request ? { request } : {}, ...changed ? { reassigned: true } : {}, ...url ? { url } : {} });
|
|
7684
7969
|
};
|
|
7685
7970
|
for (const [index, p] of (params ?? []).entries()) {
|
|
7686
7971
|
if (!p?.name) continue;
|
|
7687
|
-
if (ts.isIdentifier(p.name)) addRoot(p.name
|
|
7972
|
+
if (ts.isIdentifier(p.name)) addRoot(p.name, "", void 0, false, index === 0);
|
|
7688
7973
|
else if (ts.isObjectBindingPattern(p.name)) {
|
|
7689
7974
|
for (const el of p.name.elements) {
|
|
7690
7975
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7691
7976
|
const key = bindingKey(el, ts);
|
|
7977
|
+
if (index === 0 && key === "request") {
|
|
7978
|
+
addRoot(el.name, "", void 0, false, true);
|
|
7979
|
+
continue;
|
|
7980
|
+
}
|
|
7981
|
+
if (index === 0 && key === "url") {
|
|
7982
|
+
addRoot(el.name, "", void 0, false, false, false, true);
|
|
7983
|
+
continue;
|
|
7984
|
+
}
|
|
7692
7985
|
const container = key !== void 0 && CONTAINER_KEYS.has(key);
|
|
7693
|
-
addRoot(el.name
|
|
7986
|
+
addRoot(el.name, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0, container && ACCESSOR_NAMESPACES.has(key));
|
|
7694
7987
|
}
|
|
7695
7988
|
}
|
|
7696
7989
|
}
|
|
@@ -7698,12 +7991,17 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7698
7991
|
let cur = init;
|
|
7699
7992
|
while (cur && (ts.isAwaitExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAsExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7700
7993
|
if (!cur) return void 0;
|
|
7994
|
+
const params2 = searchParamsOrigin(cur, ts, rootPath);
|
|
7995
|
+
if (params2) return { path: "", space: "get", accessor: true, ...carried(params2) };
|
|
7996
|
+
const url = requestUrlOrigin(cur, ts, rootPath);
|
|
7997
|
+
if (url) return { path: "", url: true, ...carried(url) };
|
|
7701
7998
|
if (ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) {
|
|
7702
7999
|
const m = cur.expression.name.text;
|
|
7703
8000
|
if (["json", "formData", "text"].includes(m)) {
|
|
7704
|
-
const root =
|
|
7705
|
-
|
|
7706
|
-
|
|
8001
|
+
const root = rootIdentifierNode(cur.expression.expression, ts);
|
|
8002
|
+
const owner = root ? rootPath.get(root) : void 0;
|
|
8003
|
+
if (!owner) return void 0;
|
|
8004
|
+
return m === "formData" ? { path: "", space: "post", accessor: true, ...carried(owner) } : { path: "", space: "post", ...carried(owner) };
|
|
7707
8005
|
}
|
|
7708
8006
|
if (["parse", "safeParse", "validate", "cast"].includes(m)) {
|
|
7709
8007
|
for (const a of cur.arguments) {
|
|
@@ -7719,15 +8017,19 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7719
8017
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
7720
8018
|
const base = requestReadPath(n.initializer);
|
|
7721
8019
|
if (base !== void 0) {
|
|
7722
|
-
if (ts.isIdentifier(n.name)) addRoot(n.name
|
|
8020
|
+
if (ts.isIdentifier(n.name)) addRoot(n.name, base.path, base.space, base.accessor === true, base.request === true, base.reassigned === true, base.url === true);
|
|
7723
8021
|
else if (ts.isObjectBindingPattern(n.name)) {
|
|
7724
8022
|
for (const el of n.name.elements) {
|
|
7725
8023
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7726
8024
|
const key = bindingKey(el, ts);
|
|
8025
|
+
if (base.url === true) {
|
|
8026
|
+
if (key === "searchParams") addRoot(el.name, "", "get", true, false, base.reassigned === true);
|
|
8027
|
+
continue;
|
|
8028
|
+
}
|
|
7727
8029
|
const namespace = base.request === true && key !== void 0 && REQ_SOURCES.includes(key);
|
|
7728
8030
|
const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key) && (namespace || !REQ_SOURCES.includes(key));
|
|
7729
8031
|
const space = base.space ?? (namespace ? spaceOfKey(key) : void 0);
|
|
7730
|
-
addRoot(el.name
|
|
8032
|
+
addRoot(el.name, container ? "" : join22(base.path, key ?? el.name.text), space, namespace && ACCESSOR_NAMESPACES.has(key), false, base.reassigned === true);
|
|
7731
8033
|
}
|
|
7732
8034
|
}
|
|
7733
8035
|
}
|
|
@@ -7762,7 +8064,7 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7762
8064
|
const whole = pathFromTainted(args[i], ts, rootPath) ?? accessorRead(args[i], ts, rootPath);
|
|
7763
8065
|
for (const read2 of taintedReadPaths(args[i], ts, rootPath)) {
|
|
7764
8066
|
const key = `${read2.space ?? "*"}:${read2.path}`;
|
|
7765
|
-
const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space;
|
|
8067
|
+
const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space && whole.reassigned !== true;
|
|
7766
8068
|
const entry = reads.get(key) ?? { read: read2, roles: /* @__PURE__ */ new Set(), exact: false };
|
|
7767
8069
|
entry.roles.add(role);
|
|
7768
8070
|
entry.exact = entry.exact || exact;
|
|
@@ -7852,7 +8154,7 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7852
8154
|
kind: invocation.kind,
|
|
7853
8155
|
resolution: invocation.resolution,
|
|
7854
8156
|
argumentIndex,
|
|
7855
|
-
argumentUse: whole?.path === read2.path && whole.space === read2.space ? "direct" : "within-expression",
|
|
8157
|
+
argumentUse: whole?.path === read2.path && whole.space === read2.space && whole.reassigned !== true ? "direct" : "within-expression",
|
|
7856
8158
|
line: site.line,
|
|
7857
8159
|
start: site.start,
|
|
7858
8160
|
end: site.end
|
|
@@ -7940,9 +8242,9 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7940
8242
|
return;
|
|
7941
8243
|
}
|
|
7942
8244
|
}
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
if (
|
|
8245
|
+
const bound = ts.isIdentifier(n) && isValueRead(n, ts) ? rootPath.get(n) : void 0;
|
|
8246
|
+
if (bound) {
|
|
8247
|
+
if (bound.path) add({ path: normalizePath(bound.path), space: bound.space });
|
|
7946
8248
|
}
|
|
7947
8249
|
ts.forEachChild(n, visit);
|
|
7948
8250
|
};
|
|
@@ -7968,15 +8270,15 @@ function sinkArgumentLimitations(node, ts, rootPath) {
|
|
|
7968
8270
|
const visit = (n) => {
|
|
7969
8271
|
if (!n) return;
|
|
7970
8272
|
if (ts.isElementAccessExpression(n)) {
|
|
7971
|
-
const root =
|
|
8273
|
+
const root = rootIdentifierNode(n.expression, ts);
|
|
7972
8274
|
const arg = n.argumentExpression;
|
|
7973
|
-
if (root && rootPath.
|
|
8275
|
+
if (root && rootPath.get(root) && arg && !ts.isStringLiteralLike(arg) && !ts.isNumericLiteral(arg)) {
|
|
7974
8276
|
add("dynamic-key", text(n), n);
|
|
7975
8277
|
}
|
|
7976
8278
|
}
|
|
7977
8279
|
if ((ts.isSpreadAssignment?.(n) || ts.isSpreadElement(n)) && n.expression) {
|
|
7978
|
-
const root =
|
|
7979
|
-
if (root && rootPath.
|
|
8280
|
+
const root = rootIdentifierNode(n.expression, ts);
|
|
8281
|
+
if (root && rootPath.get(root)) add("spread-into-sink", text(n.parent ?? n), n);
|
|
7980
8282
|
}
|
|
7981
8283
|
ts.forEachChild(n, visit);
|
|
7982
8284
|
};
|
|
@@ -7988,16 +8290,66 @@ function accessorRead(node, ts, rootPath) {
|
|
|
7988
8290
|
while (cur && (ts.isAwaitExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7989
8291
|
if (!cur || !ts.isCallExpression(cur) || !ts.isPropertyAccessExpression(cur.expression)) return void 0;
|
|
7990
8292
|
const [name] = cur.arguments;
|
|
7991
|
-
if (cur.
|
|
7992
|
-
|
|
7993
|
-
|
|
8293
|
+
if (cur.arguments.length !== 1 || !name || !ts.isStringLiteralLike(name)) return void 0;
|
|
8294
|
+
const method = cur.expression.name.text;
|
|
8295
|
+
const receiver = cur.expression.expression;
|
|
8296
|
+
const hono = HONO_ACCESSOR_SPACES[method];
|
|
8297
|
+
if (hono && ts.isPropertyAccessExpression(receiver) && receiver.name.text === "req" && ts.isIdentifier(receiver.expression)) {
|
|
8298
|
+
const context = rootPath.get(receiver.expression);
|
|
8299
|
+
if (context?.request === true) return { path: normalizePath(name.text), space: hono, ...carried(context) };
|
|
8300
|
+
}
|
|
8301
|
+
if (method !== "get") return void 0;
|
|
8302
|
+
const params = ts.isIdentifier(receiver) ? void 0 : searchParamsOrigin(receiver, ts, rootPath);
|
|
8303
|
+
if (params) return { path: normalizePath(name.text), space: "get", ...carried(params) };
|
|
8304
|
+
if (!isAccessor(receiver, ts, rootPath)) return void 0;
|
|
8305
|
+
return pathFromTainted(receiver, ts, rootPath, [name.text]);
|
|
8306
|
+
}
|
|
8307
|
+
var HONO_ACCESSOR_SPACES = { query: "get", param: "route-param", header: "server" };
|
|
8308
|
+
function carried(origin) {
|
|
8309
|
+
return origin.reassigned === true ? { reassigned: true } : {};
|
|
8310
|
+
}
|
|
8311
|
+
function requestUrlOrigin(node, ts, rootPath) {
|
|
8312
|
+
let cur = node;
|
|
8313
|
+
while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur) || ts.isAwaitExpression(cur) || ts.isAsExpression(cur))) cur = cur.expression;
|
|
8314
|
+
if (!cur) return void 0;
|
|
8315
|
+
if (ts.isIdentifier(cur)) {
|
|
8316
|
+
const root = rootPath.get(cur);
|
|
8317
|
+
return root?.url === true ? root : void 0;
|
|
8318
|
+
}
|
|
8319
|
+
const request = (e) => {
|
|
8320
|
+
let inner = e;
|
|
8321
|
+
while (inner && (ts.isParenthesizedExpression(inner) || ts.isNonNullExpression(inner) || ts.isAsExpression(inner))) inner = inner.expression;
|
|
8322
|
+
if (!inner || !ts.isIdentifier(inner)) return void 0;
|
|
8323
|
+
const root = rootPath.get(inner);
|
|
8324
|
+
return root?.request === true ? root : void 0;
|
|
8325
|
+
};
|
|
8326
|
+
if (ts.isPropertyAccessExpression(cur) && cur.name.text === "nextUrl") return request(cur.expression);
|
|
8327
|
+
if (ts.isNewExpression(cur) && isGlobal(cur.expression, "URL", ts)) {
|
|
8328
|
+
const [href] = cur.arguments ?? [];
|
|
8329
|
+
if (!href || !ts.isPropertyAccessExpression(href) || href.name.text !== "url") return void 0;
|
|
8330
|
+
const owner = href.expression;
|
|
8331
|
+
if (ts.isPropertyAccessExpression(owner) && owner.name.text === "req") return request(owner.expression) ?? request(owner);
|
|
8332
|
+
return request(owner);
|
|
8333
|
+
}
|
|
8334
|
+
return void 0;
|
|
8335
|
+
}
|
|
8336
|
+
function searchParamsOrigin(node, ts, rootPath) {
|
|
8337
|
+
let cur = node;
|
|
8338
|
+
while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur) || ts.isAsExpression(cur))) cur = cur.expression;
|
|
8339
|
+
if (!cur) return void 0;
|
|
8340
|
+
if (ts.isIdentifier(cur)) {
|
|
8341
|
+
const root = rootPath.get(cur);
|
|
8342
|
+
return root?.accessor === true && root.space === "get" ? root : void 0;
|
|
8343
|
+
}
|
|
8344
|
+
if (!ts.isPropertyAccessExpression(cur) || cur.name.text !== "searchParams") return void 0;
|
|
8345
|
+
return requestUrlOrigin(cur.expression, ts, rootPath);
|
|
7994
8346
|
}
|
|
7995
8347
|
function isAccessor(node, ts, rootPath) {
|
|
7996
8348
|
let cur = node;
|
|
7997
8349
|
while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7998
|
-
if (ts.isIdentifier(cur)) return rootPath.get(cur
|
|
8350
|
+
if (ts.isIdentifier(cur)) return rootPath.get(cur)?.accessor === true;
|
|
7999
8351
|
if (ts.isPropertyAccessExpression(cur) && ACCESSOR_NAMESPACES.has(cur.name.text) && ts.isIdentifier(cur.expression)) {
|
|
8000
|
-
return rootPath.get(cur.expression
|
|
8352
|
+
return rootPath.get(cur.expression)?.request === true;
|
|
8001
8353
|
}
|
|
8002
8354
|
return false;
|
|
8003
8355
|
}
|
|
@@ -8023,16 +8375,128 @@ function pathFromTainted(node, ts, rootPath, trailing = []) {
|
|
|
8023
8375
|
break;
|
|
8024
8376
|
}
|
|
8025
8377
|
if (!cur || !ts.isIdentifier(cur)) return void 0;
|
|
8026
|
-
const base = rootPath.get(cur
|
|
8027
|
-
if (base === void 0) return void 0;
|
|
8378
|
+
const base = rootPath.get(cur);
|
|
8379
|
+
if (base === void 0 || base.url === true) return void 0;
|
|
8028
8380
|
segs.push(...trailing);
|
|
8029
8381
|
let space = base.space;
|
|
8030
|
-
|
|
8031
|
-
|
|
8382
|
+
let namespace;
|
|
8383
|
+
if (base.request === true && base.path === "" && segs.length > 0 && REQ_SOURCES.includes(segs[0])) {
|
|
8384
|
+
namespace = segs[0];
|
|
8385
|
+
space = spaceOfKey(namespace) ?? space;
|
|
8032
8386
|
segs.shift();
|
|
8033
8387
|
}
|
|
8034
|
-
const request = base.request === true && segs.length === 0 ? { request: true } : {};
|
|
8035
|
-
|
|
8388
|
+
const request = base.request === true && namespace === void 0 && segs.length === 0 ? { request: true } : {};
|
|
8389
|
+
const accessor = namespace !== void 0 && segs.length === 0 && ACCESSOR_NAMESPACES.has(namespace) ? { accessor: true } : {};
|
|
8390
|
+
const reassigned = base.reassigned === true ? { reassigned: true } : {};
|
|
8391
|
+
return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space, ...request, ...accessor, ...reassigned };
|
|
8392
|
+
}
|
|
8393
|
+
|
|
8394
|
+
// src/map/module-graph.ts
|
|
8395
|
+
import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
|
|
8396
|
+
import { dirname as dirname8, join as join16, relative as relative5, resolve as resolvePath } from "path";
|
|
8397
|
+
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
8398
|
+
function createModuleGraph(ts, opts) {
|
|
8399
|
+
const cache = /* @__PURE__ */ new Map();
|
|
8400
|
+
const load = (file) => {
|
|
8401
|
+
if (cache.has(file)) return cache.get(file) ?? null;
|
|
8402
|
+
let entry = null;
|
|
8403
|
+
try {
|
|
8404
|
+
const text = readFileSync14(file, "utf8");
|
|
8405
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
8406
|
+
const bindings = buildModuleBindings(sf, ts);
|
|
8407
|
+
entry = { fnSinks: collectLocalSinks(sf, ts, bindings).byName, calleesOf: collectCallees(sf, ts), bindings };
|
|
8408
|
+
} catch {
|
|
8409
|
+
entry = null;
|
|
8410
|
+
}
|
|
8411
|
+
cache.set(file, entry);
|
|
8412
|
+
return entry;
|
|
8413
|
+
};
|
|
8414
|
+
const resolveInProject = (fromFile, specifier) => {
|
|
8415
|
+
const target = resolveRelativeModule(fromFile, specifier);
|
|
8416
|
+
if (!target) return void 0;
|
|
8417
|
+
if (!opts.followOutside) {
|
|
8418
|
+
let real = target;
|
|
8419
|
+
try {
|
|
8420
|
+
real = realpathSync5(target);
|
|
8421
|
+
} catch {
|
|
8422
|
+
}
|
|
8423
|
+
if (!isInside(real, opts.boundary)) return void 0;
|
|
8424
|
+
}
|
|
8425
|
+
return target;
|
|
8426
|
+
};
|
|
8427
|
+
return {
|
|
8428
|
+
resolveLocal: resolveInProject,
|
|
8429
|
+
importedSinks(fromFile, specifier, exportName) {
|
|
8430
|
+
const target = resolveInProject(fromFile, specifier);
|
|
8431
|
+
if (!target) return [];
|
|
8432
|
+
const mod = load(target);
|
|
8433
|
+
if (!mod) return [];
|
|
8434
|
+
const collected = [...mod.fnSinks.get(exportName) ?? []];
|
|
8435
|
+
for (const callee of mod.calleesOf.get(exportName) ?? []) {
|
|
8436
|
+
for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
|
|
8437
|
+
}
|
|
8438
|
+
const rel = relative5(opts.cwd, target);
|
|
8439
|
+
return collected.map((s) => ({ ...s, file: rel }));
|
|
8440
|
+
},
|
|
8441
|
+
// A different question about the same module: not "what sinks are in there" but "what does this
|
|
8442
|
+
// export TRACE TO". The common AI-built layout puts the client in a lib file —
|
|
8443
|
+
// `export const db = createClient(...)` in `lib/db.ts`, imported everywhere — so the receiver of
|
|
8444
|
+
// `db.from('orders').insert(...)` resolves to a relative specifier and nothing else. Refusing it (as
|
|
8445
|
+
// an unattributable receiver) is right for app code but wrong here: one hop away it is a real
|
|
8446
|
+
// dependency, and that chain is import-to-import, fully static — evidence, not inference.
|
|
8447
|
+
importedBinding(fromFile, specifier, exportName) {
|
|
8448
|
+
const target = resolveInProject(fromFile, specifier);
|
|
8449
|
+
if (!target) return void 0;
|
|
8450
|
+
const mod = load(target);
|
|
8451
|
+
if (!mod) return void 0;
|
|
8452
|
+
const pkg = npmPackageOf(mod.bindings.resolve(exportName));
|
|
8453
|
+
if (pkg === void 0) return void 0;
|
|
8454
|
+
return {
|
|
8455
|
+
package: pkg,
|
|
8456
|
+
origin: mod.bindings.originOf(exportName) ?? "direct",
|
|
8457
|
+
name: mod.bindings.exportNameOf(exportName) ?? exportName
|
|
8458
|
+
};
|
|
8459
|
+
}
|
|
8460
|
+
};
|
|
8461
|
+
}
|
|
8462
|
+
function resolveRelativeModule(fromFile, spec) {
|
|
8463
|
+
if (!spec.startsWith(".")) return void 0;
|
|
8464
|
+
const dir = dirname8(fromFile);
|
|
8465
|
+
const base = resolvePath(dir, spec);
|
|
8466
|
+
const candidates = [];
|
|
8467
|
+
const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
|
|
8468
|
+
if (jsLike) {
|
|
8469
|
+
const stem = base.slice(0, -jsLike[0].length);
|
|
8470
|
+
for (const e of RESOLVE_EXTS) candidates.push(stem + e);
|
|
8471
|
+
}
|
|
8472
|
+
candidates.push(base);
|
|
8473
|
+
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
8474
|
+
for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
|
|
8475
|
+
for (const c of candidates) {
|
|
8476
|
+
try {
|
|
8477
|
+
if (statSync7(c).isFile()) return c;
|
|
8478
|
+
} catch {
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
return void 0;
|
|
8482
|
+
}
|
|
8483
|
+
function collectCallees(sf, ts) {
|
|
8484
|
+
const map = /* @__PURE__ */ new Map();
|
|
8485
|
+
const visit = (node) => {
|
|
8486
|
+
let name;
|
|
8487
|
+
let body;
|
|
8488
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
|
|
8489
|
+
name = node.name.text;
|
|
8490
|
+
body = node.body;
|
|
8491
|
+
} else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isFnLike(node.initializer, ts)) {
|
|
8492
|
+
name = node.name.text;
|
|
8493
|
+
body = node.initializer.body;
|
|
8494
|
+
}
|
|
8495
|
+
if (name && body) map.set(name, localCalls(body, ts));
|
|
8496
|
+
ts.forEachChild(node, visit);
|
|
8497
|
+
};
|
|
8498
|
+
visit(sf);
|
|
8499
|
+
return map;
|
|
8036
8500
|
}
|
|
8037
8501
|
|
|
8038
8502
|
// src/map/invocations.ts
|
|
@@ -8154,6 +8618,7 @@ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELE
|
|
|
8154
8618
|
function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
8155
8619
|
const out = [];
|
|
8156
8620
|
const isServerActionsFile = fileHasUseServer(sf, ts);
|
|
8621
|
+
const pagesApi = isPagesApiFile(ctx.owner);
|
|
8157
8622
|
const visit = (node) => {
|
|
8158
8623
|
if (ts.isVariableStatement(node) && hasExport(node, ts)) {
|
|
8159
8624
|
for (const decl of node.declarationList.declarations) {
|
|
@@ -8195,6 +8660,12 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8195
8660
|
out.push(handlerEntry(node.name.text, "server-action", node.parameters, node.body, ts, localSinks, bindings, ctx, spanOf(node)));
|
|
8196
8661
|
}
|
|
8197
8662
|
}
|
|
8663
|
+
if (pagesApi) {
|
|
8664
|
+
const handler = defaultExportedHandler(node, ts);
|
|
8665
|
+
if (handler) {
|
|
8666
|
+
out.push(handlerEntry(handler.name ?? "default", "default-export", handler.fn.parameters, handler.fn.body, ts, localSinks, bindings, ctx, spanOf(handler.fn)));
|
|
8667
|
+
}
|
|
8668
|
+
}
|
|
8198
8669
|
if (ts.isCallExpression(node)) {
|
|
8199
8670
|
const c = node.expression;
|
|
8200
8671
|
const denoServe = ts.isPropertyAccessExpression(c) && c.name.text === "serve" && ts.isIdentifier(c.expression) && c.expression.text === "Deno";
|
|
@@ -8213,7 +8684,7 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8213
8684
|
const first = args[0];
|
|
8214
8685
|
const route = first && ts.isStringLiteralLike(first) ? first.text : routeFromChain(node.expression.expression, ts);
|
|
8215
8686
|
const handler = args[args.length - 1];
|
|
8216
|
-
if (route !== void 0 && handler && isFnLike(handler, ts)) {
|
|
8687
|
+
if (route !== void 0 && isRoutePath(route) && handler && isFnLike(handler, ts)) {
|
|
8217
8688
|
out.push(handlerEntry(route, "route-registration", handler.parameters, handler.body, ts, localSinks, bindings, ctx, {
|
|
8218
8689
|
// `use`/`all` register handlers but are not HTTP methods — leave method undefined.
|
|
8219
8690
|
method: HTTP_METHODS.has(mname.toUpperCase()) ? mname.toUpperCase() : void 0,
|
|
@@ -8226,7 +8697,7 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8226
8697
|
const arg = node.arguments[0];
|
|
8227
8698
|
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
8228
8699
|
const reg = routeObject(arg, ts);
|
|
8229
|
-
if (reg.url && reg.handler) {
|
|
8700
|
+
if (reg.url && isRoutePath(reg.url) && reg.handler) {
|
|
8230
8701
|
for (const m of reg.methods.length ? reg.methods : [void 0]) {
|
|
8231
8702
|
out.push(handlerEntry(reg.url, "route-registration", reg.handler.parameters, reg.handler.body, ts, localSinks, bindings, ctx, { method: m, route: reg.url, ...spanOf(node) }));
|
|
8232
8703
|
}
|
|
@@ -8239,6 +8710,19 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8239
8710
|
visit(sf);
|
|
8240
8711
|
return out;
|
|
8241
8712
|
}
|
|
8713
|
+
function defaultExportedHandler(node, ts) {
|
|
8714
|
+
const isDefault = (n) => Boolean(n.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
|
|
8715
|
+
if (ts.isFunctionDeclaration(node) && hasExport(node, ts) && isDefault(node) && node.body) return { name: node.name?.text, fn: node };
|
|
8716
|
+
if (!ts.isExportAssignment(node) || node.isExportEquals) return void 0;
|
|
8717
|
+
const target = node.expression;
|
|
8718
|
+
if (isFnLike(target, ts)) return { fn: target };
|
|
8719
|
+
if (!ts.isIdentifier(target)) return void 0;
|
|
8720
|
+
const declaration = declarationOf(target, ts);
|
|
8721
|
+
const owner = declaration?.parent;
|
|
8722
|
+
if (owner && ts.isFunctionDeclaration(owner) && owner.body) return { name: target.text, fn: owner };
|
|
8723
|
+
if (owner && ts.isVariableDeclaration(owner) && owner.initializer && isFnLike(owner.initializer, ts)) return { name: target.text, fn: owner.initializer };
|
|
8724
|
+
return void 0;
|
|
8725
|
+
}
|
|
8242
8726
|
function fileHasUseServer(sf, ts) {
|
|
8243
8727
|
const first = sf.statements?.[0];
|
|
8244
8728
|
return Boolean(first && ts.isExpressionStatement(first) && ts.isStringLiteralLike(first.expression) && first.expression.text === "use server");
|
|
@@ -8254,7 +8738,7 @@ function handlerInvocations(body, ts, bindings, ctx) {
|
|
|
8254
8738
|
function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, ctx, extra = {}) {
|
|
8255
8739
|
const entryKind = kindLabel === "route-registration" || kindLabel === "server-action" || kindLabel === "edge-function" ? kindLabel : "route-handler";
|
|
8256
8740
|
const payloadStyle = kindLabel === "server-action";
|
|
8257
|
-
const inputs = inputsFromHandler(params, body, ts, bindings, {
|
|
8741
|
+
const { inputs, schemaUnresolved } = inputsFromHandler(params, body, ts, bindings, {
|
|
8258
8742
|
payloadParam: payloadStyle,
|
|
8259
8743
|
validatorSource: payloadStyle ? "server-fn-data" : "json-body"
|
|
8260
8744
|
});
|
|
@@ -8269,7 +8753,9 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
8269
8753
|
end: extra.end,
|
|
8270
8754
|
inputs,
|
|
8271
8755
|
sinks,
|
|
8272
|
-
...linkedFlows(body, params, inputs, sinks, ts, handlerInvocations(body, ts, bindings, ctx))
|
|
8756
|
+
...linkedFlows(body, params, inputs, sinks, ts, handlerInvocations(body, ts, bindings, ctx)),
|
|
8757
|
+
// The handler validates its request with a schema declared in another module: its fields are unknown.
|
|
8758
|
+
...schemaUnresolved ? { inputsResolved: false } : {}
|
|
8273
8759
|
};
|
|
8274
8760
|
}
|
|
8275
8761
|
|
|
@@ -8528,6 +9014,7 @@ function lineAt(starts, pos) {
|
|
|
8528
9014
|
|
|
8529
9015
|
// src/map/extract.ts
|
|
8530
9016
|
var MAX_DEPENDENCY_INPUT_FLOWS_PER_MAP = 500;
|
|
9017
|
+
var MAX_UNPROVEN_FLOWS_PER_MAP = 1e3;
|
|
8531
9018
|
async function extractInputMap(cwd, ts, options = {}) {
|
|
8532
9019
|
const notes = [];
|
|
8533
9020
|
const endpoints = [];
|
|
@@ -8543,9 +9030,11 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8543
9030
|
const imports = createImportInventory(readPathAliases(cwd));
|
|
8544
9031
|
const invocations = createInvocationInventory();
|
|
8545
9032
|
let dependencyInputFlowCount = 0;
|
|
9033
|
+
let unprovenFlowCount = 0;
|
|
8546
9034
|
let parsed = 0;
|
|
8547
9035
|
let preFiltered = 0;
|
|
8548
9036
|
let importScanFailures = 0;
|
|
9037
|
+
let componentFiles = 0;
|
|
8549
9038
|
let unresolvableImports = 0;
|
|
8550
9039
|
let sourceBytes = 0;
|
|
8551
9040
|
const calls = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
|
|
@@ -8557,7 +9046,17 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8557
9046
|
const text = readFileSync16(file, "utf8");
|
|
8558
9047
|
const relFile = relative6(cwd, file);
|
|
8559
9048
|
sourceBytes += text.length;
|
|
8560
|
-
if (
|
|
9049
|
+
if (isComponentFile(file)) {
|
|
9050
|
+
componentFiles++;
|
|
9051
|
+
preFiltered++;
|
|
9052
|
+
const script = componentScript(text, file);
|
|
9053
|
+
const scanned = script === null ? null : scanFileImports(script, ts);
|
|
9054
|
+
if (scanned === null) importScanFailures++;
|
|
9055
|
+
else imports.add(relFile, scanned, false);
|
|
9056
|
+
if (script !== null) unresolvableImports += countUnresolvableImports(script, ts);
|
|
9057
|
+
continue;
|
|
9058
|
+
}
|
|
9059
|
+
if (!hasEntrySignal(text) && !isPagesApiFile(relFile)) {
|
|
8561
9060
|
preFiltered++;
|
|
8562
9061
|
const scanned = scanFileImports(text, ts);
|
|
8563
9062
|
if (scanned === null) importScanFailures++;
|
|
@@ -8593,11 +9092,17 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8593
9092
|
ep.dependencyInputFlowsTruncated = true;
|
|
8594
9093
|
}
|
|
8595
9094
|
dependencyInputFlowCount += ep.dependencyInputFlows?.length ?? 0;
|
|
9095
|
+
const bounded = boundUnprovenFlows(ep.flows, Math.max(0, MAX_UNPROVEN_FLOWS_PER_MAP - unprovenFlowCount));
|
|
9096
|
+
if (bounded.truncated) {
|
|
9097
|
+
ep.flows = bounded.flows;
|
|
9098
|
+
ep.flowsTruncated = true;
|
|
9099
|
+
}
|
|
9100
|
+
unprovenFlowCount += ep.flows.filter((f) => !isProvenFlow(f.confidence)).length;
|
|
8596
9101
|
if (ep.route === void 0 && ep.entryKind === "edge-function") {
|
|
8597
9102
|
const fn = functionNameFromPath(relFile);
|
|
8598
9103
|
if (fn) ep.route = "/" + fn;
|
|
8599
9104
|
}
|
|
8600
|
-
if (ep.route === void 0 &&
|
|
9105
|
+
if (ep.route === void 0 && ep.entryKind === "route-handler") {
|
|
8601
9106
|
const derived = routeFromFilePath(relFile);
|
|
8602
9107
|
if (derived.route) {
|
|
8603
9108
|
ep.route = derived.route;
|
|
@@ -8648,6 +9153,10 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8648
9153
|
if (truncatedDependencyFlows > 0) {
|
|
8649
9154
|
notes.push(`${truncatedDependencyFlows} endpoint(s) had more dependency-input links than the bounded map carries; those endpoint records are marked dependencyInputFlowsTruncated.`);
|
|
8650
9155
|
}
|
|
9156
|
+
const truncatedFlows = endpoints.filter((e) => e.flowsTruncated).length;
|
|
9157
|
+
if (truncatedFlows > 0) {
|
|
9158
|
+
notes.push(`${truncatedFlows} endpoint(s) had more unproven flows than the bounded map carries; those endpoint records are marked flowsTruncated. Every proven flow is kept.`);
|
|
9159
|
+
}
|
|
8651
9160
|
if (unresolved > 0) {
|
|
8652
9161
|
notes.push(`${unresolved} endpoint(s) declare an input validator that could not be statically parsed \u2014 their inputs are UNKNOWN, not empty (marked inputsResolved: false).`);
|
|
8653
9162
|
}
|
|
@@ -8655,6 +9164,9 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8655
9164
|
if (heuristicOnly > 0) {
|
|
8656
9165
|
notes.push(`${heuristicOnly} endpoint(s) have inputs and sinks but no proven data link \u2014 their flows say "may reach", not "does reach" (see each flow's confidence).`);
|
|
8657
9166
|
}
|
|
9167
|
+
if (componentFiles > 0) {
|
|
9168
|
+
notes.push(`${componentFiles} single-file component(s) (.vue, .svelte, .astro) were scanned for imports only. Code in their script blocks and Astro frontmatter can run on the server, but it is not analyzed for endpoints, inputs or sinks.`);
|
|
9169
|
+
}
|
|
8658
9170
|
if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
|
|
8659
9171
|
const importList = imports.list();
|
|
8660
9172
|
const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
@@ -9824,7 +10336,7 @@ function reportSourceWidget(siteUuid, framework, report) {
|
|
|
9824
10336
|
});
|
|
9825
10337
|
};
|
|
9826
10338
|
try {
|
|
9827
|
-
const hint =
|
|
10339
|
+
const hint = hasEditableShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
|
|
9828
10340
|
const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
|
|
9829
10341
|
const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
|
|
9830
10342
|
switch (result.action) {
|
|
@@ -9871,7 +10383,7 @@ function reportSourceMarker(framework, checksum, report) {
|
|
|
9871
10383
|
case "unsupported":
|
|
9872
10384
|
report.missing.push({
|
|
9873
10385
|
text: "Your live app does not tell Patchstack it is live yet",
|
|
9874
|
-
hint:
|
|
10386
|
+
hint: hasEditableShell(framework) ? [`Add this inside <head> in ${shell}:`, ...buildSourceMarkerSnippet(framework).split("\n").map((line) => ` ${line}`)] : [
|
|
9875
10387
|
`Add this inside <head> in ${shell}, only when ${productionGate(framework)}:`,
|
|
9876
10388
|
" <script>window.__PATCHSTACK_PROD__=true;</script>"
|
|
9877
10389
|
]
|