@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/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
- function hasJsxShell(framework) {
2989
- return framework !== null && JSX_SHELL_FRAMEWORKS.has(framework);
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 (!hasJsxShell(framework)) {
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 (at("server") !== -1) {
3263
- segs = [...dirs.slice(at("server") + 1), ...base === "index" ? [] : [base.replace(/\.(get|post|put|patch|delete|head|options)$/i, "")]];
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
- hasJsxShell(state.framework) ? {
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 map = /* @__PURE__ */ new Map();
7063
- const visit = (node) => {
7064
- if (ts.isFunctionDeclaration(node) && node.name && node.body) map.set(node.name.text, directSinks(node.body, ts, bindings, ctx));
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
- map.set(decl.name.text, directSinks(decl.initializer.body, ts, bindings, ctx));
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 map;
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 called of localCalls(body, ts)) {
7083
- for (const s of localSinks.get(called) ?? []) sinks.push(s);
7084
- if (ctx) {
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 schemaFields = zodObjectFields(body, ts, bindings);
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 reqName = p0 && ts.isIdentifier(p0.name) ? p0.name.text : void 0;
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
- if (opts.payloadParam && p0 && ts.isIdentifier(p0.name)) payloadNames.add(p0.name.text);
7576
- if (p0 && !reqName && ts.isObjectBindingPattern(p0.name)) {
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 && REQ_SOURCES.includes(key) && ts.isIdentifier(el.name)) {
7580
- sourceNames.set(el.name.text, namespaceSource(key));
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.text);
7590
- const isReqSourceExpr = (e) => isPayloadExpr(e) || ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === reqName && REQ_SOURCES.includes(e.name.text) || ts.isIdentifier(e) && sourceNames.has(e.text);
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) && ts.isIdentifier(inner.expression.expression) && inner.expression.expression.text === reqName);
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) && isBodyReadCall(n.initializer)) sourceNames.set(n.name.text, bodyReadSource(n.initializer));
7610
- if (ts.isObjectBindingPattern(n.name) && ts.isIdentifier(init) && reqName && init.text === reqName) {
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.text, namespaceSource(key));
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 = sourceNames.get(e.text);
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 = /* @__PURE__ */ new Map();
7682
- const addRoot = (name, path17, space, accessor = false, request = false) => {
7683
- if (!rootPath.has(name)) rootPath.set(name, { path: path17, space, ...accessor ? { accessor } : {}, ...request ? { request } : {} });
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.text, "", void 0, false, index === 0);
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.text, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0, container && ACCESSOR_NAMESPACES.has(key));
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 = rootIdentifier(cur.expression.expression, ts);
7705
- if (!root || !rootPath.has(root)) return void 0;
7706
- return m === "formData" ? { path: "", space: "post", accessor: true } : { path: "", space: "post" };
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.text, base.path, base.space, base.accessor === true);
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.text, container ? "" : join22(base.path, key ?? el.name.text), space, namespace && ACCESSOR_NAMESPACES.has(key));
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
- if (ts.isIdentifier(n) && rootPath.has(n.text) && isValueRead(n, ts)) {
7944
- const r = rootPath.get(n.text);
7945
- if (r.path) add({ path: normalizePath(r.path), space: r.space });
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 = rootIdentifier(n.expression, ts);
8273
+ const root = rootIdentifierNode(n.expression, ts);
7972
8274
  const arg = n.argumentExpression;
7973
- if (root && rootPath.has(root) && arg && !ts.isStringLiteralLike(arg) && !ts.isNumericLiteral(arg)) {
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 = rootIdentifier(n.expression, ts);
7979
- if (root && rootPath.has(root)) add("spread-into-sink", text(n.parent ?? n), n);
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.expression.name.text !== "get" || cur.arguments.length !== 1 || !name || !ts.isStringLiteralLike(name)) return void 0;
7992
- if (!isAccessor(cur.expression.expression, ts, rootPath)) return void 0;
7993
- return pathFromTainted(cur.expression.expression, ts, rootPath, [name.text]);
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.text)?.accessor === true;
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.text)?.request === true;
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.text);
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
- if (base.request === true && base.path === "" && segs.length > 1 && REQ_SOURCES.includes(segs[0])) {
8031
- space = spaceOfKey(segs[0]) ?? space;
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
- return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space, ...request };
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 (!hasEntrySignal(text)) {
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 && (ep.entryKind === "route-handler" || ep.entryKind === "server-action")) {
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 = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
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: hasJsxShell(framework) ? [`Add this inside <head> in ${shell}:`, ...buildSourceMarkerSnippet(framework).split("\n").map((line) => ` ${line}`)] : [
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
  ]