@patchstack/connect 0.3.23 → 0.3.24

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
4
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -927,6 +927,88 @@ function computeManifestChecksum(packages) {
927
927
  return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 12);
928
928
  }
929
929
 
930
+ // src/pulse-token.ts
931
+ var TOKEN_SKEW_MS = 6e4;
932
+ function buildTokenUrl(manifestEndpoint) {
933
+ const url = new URL(manifestEndpoint);
934
+ const path11 = url.pathname.replace(/\/$/, "");
935
+ url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
936
+ url.search = "";
937
+ url.hash = "";
938
+ return url.toString();
939
+ }
940
+ function parsePulseAuth(credential) {
941
+ const index = credential.lastIndexOf("-");
942
+ if (index <= 0 || index === credential.length - 1) return null;
943
+ const clientId = credential.slice(index + 1);
944
+ if (!/^\d+$/.test(clientId)) return null;
945
+ return { clientId, clientSecret: credential.slice(0, index) };
946
+ }
947
+ var cached = null;
948
+ var inflight = null;
949
+ function clearPulseToken() {
950
+ cached = null;
951
+ }
952
+ async function getPulseToken(config, fetchImpl = fetch) {
953
+ if (typeof config.pulseAuth !== "string" || config.pulseAuth.length === 0) return null;
954
+ if (cached !== null && Date.now() < cached.expiresAt - TOKEN_SKEW_MS) {
955
+ return cached.token;
956
+ }
957
+ if (inflight !== null) return inflight;
958
+ const credentials = parsePulseAuth(config.pulseAuth);
959
+ if (credentials === null) return null;
960
+ inflight = (async () => {
961
+ try {
962
+ const response = await fetchImpl(buildTokenUrl(config.endpoint), {
963
+ method: "POST",
964
+ headers: {
965
+ "Content-Type": "application/json",
966
+ Accept: "application/json",
967
+ "User-Agent": "@patchstack/connect"
968
+ },
969
+ body: JSON.stringify({
970
+ grant_type: "client_credentials",
971
+ client_id: credentials.clientId,
972
+ client_secret: credentials.clientSecret
973
+ }),
974
+ signal: AbortSignal.timeout(config.timeoutMs)
975
+ });
976
+ if (!response.ok) return null;
977
+ const body = await response.json();
978
+ if (typeof body.access_token !== "string" || body.access_token.length === 0) return null;
979
+ const expiresIn = Number(body.expires_in);
980
+ const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
981
+ cached = { token: body.access_token, expiresAt: Date.now() + ttlMs };
982
+ return body.access_token;
983
+ } catch {
984
+ return null;
985
+ } finally {
986
+ inflight = null;
987
+ }
988
+ })();
989
+ return inflight;
990
+ }
991
+ async function pulseAuthHeader(config, fetchImpl = fetch) {
992
+ const token = await getPulseToken(config, fetchImpl);
993
+ return token === null ? {} : { Authorization: `Bearer ${token}` };
994
+ }
995
+ async function pulseFetch(config, url, init, fetchImpl = fetch) {
996
+ const send = async () => {
997
+ const auth = await pulseAuthHeader(config, fetchImpl);
998
+ const response = await fetchImpl(url, {
999
+ ...init,
1000
+ headers: { ...init.headers, ...auth }
1001
+ });
1002
+ return { response, authenticated: auth.Authorization !== void 0 };
1003
+ };
1004
+ const first = await send();
1005
+ if (first.response.status === 401 && first.authenticated) {
1006
+ clearPulseToken();
1007
+ return (await send()).response;
1008
+ }
1009
+ return first.response;
1010
+ }
1011
+
930
1012
  // src/client.ts
931
1013
  var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
932
1014
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -950,6 +1032,86 @@ function buildSettingsUrl(endpoint, siteUuid) {
950
1032
  const origin = new URL(endpoint).origin;
951
1033
  return `${origin}/monitor/widget/settings/${encodeURIComponent(siteUuid)}`;
952
1034
  }
1035
+ function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1036
+ const url = new URL(manifestEndpoint);
1037
+ const path11 = url.pathname.replace(/\/$/, "");
1038
+ url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1039
+ url.search = "";
1040
+ url.hash = "";
1041
+ return url.toString();
1042
+ }
1043
+ function buildInputMapUrl(manifestEndpoint, siteUuid) {
1044
+ const url = new URL(manifestEndpoint);
1045
+ const path11 = url.pathname.replace(/\/$/, "");
1046
+ url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1047
+ url.search = "";
1048
+ url.hash = "";
1049
+ return url.toString();
1050
+ }
1051
+ async function postInputMap(config, map) {
1052
+ if (config.siteUuid === null) {
1053
+ return { result: "skipped", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
1054
+ }
1055
+ const url = buildInputMapUrl(config.endpoint, config.siteUuid);
1056
+ try {
1057
+ const response = await pulseFetch(config, url, {
1058
+ method: "POST",
1059
+ headers: {
1060
+ Accept: "application/json",
1061
+ "Content-Type": "application/json",
1062
+ "User-Agent": "@patchstack/connect"
1063
+ },
1064
+ body: JSON.stringify(map),
1065
+ signal: AbortSignal.timeout(config.timeoutMs)
1066
+ });
1067
+ if (response.status === 404) {
1068
+ return { result: "failed", message: "Site not found \u2014 the configured site UUID is unknown to Patchstack." };
1069
+ }
1070
+ if (response.status === 422) {
1071
+ return { result: "failed", message: `Patchstack does not accept this map schema (version ${map.version}). Update @patchstack/connect.` };
1072
+ }
1073
+ if (!response.ok) {
1074
+ return { result: "failed", message: `Patchstack returned ${response.status}.` };
1075
+ }
1076
+ const body = await response.json();
1077
+ const revision = Number(body.revision);
1078
+ if ((body.result === "stored" || body.result === "unchanged") && Number.isInteger(revision) && revision > 0) {
1079
+ return { result: body.result, revision };
1080
+ }
1081
+ return { result: "failed", message: "Patchstack returned an unexpected response." };
1082
+ } catch {
1083
+ return { result: "failed", message: `Could not reach Patchstack at ${url}.` };
1084
+ }
1085
+ }
1086
+ async function postPackageRemoved(config) {
1087
+ if (config.siteUuid === null) {
1088
+ return { result: "failed", message: "No site UUID configured." };
1089
+ }
1090
+ const url = buildPackageRemovedUrl(config.endpoint, config.siteUuid);
1091
+ try {
1092
+ const response = await pulseFetch(config, url, {
1093
+ method: "POST",
1094
+ headers: {
1095
+ Accept: "application/json",
1096
+ "User-Agent": "@patchstack/connect"
1097
+ },
1098
+ signal: AbortSignal.timeout(config.timeoutMs)
1099
+ });
1100
+ if (response.status === 404) {
1101
+ return { result: "gone", message: null };
1102
+ }
1103
+ if (!response.ok) {
1104
+ return { result: "failed", message: `Patchstack returned ${response.status}.` };
1105
+ }
1106
+ const body = await response.json();
1107
+ if (body.status === "deleted" || body.status === "flagged") {
1108
+ return { result: body.status, message: body.message ?? null };
1109
+ }
1110
+ return { result: "failed", message: "Patchstack returned an unexpected response." };
1111
+ } catch {
1112
+ return { result: "failed", message: `Could not reach Patchstack at ${url}.` };
1113
+ }
1114
+ }
953
1115
  async function fetchSiteStatus(config) {
954
1116
  if (config.siteUuid === null) return "unknown";
955
1117
  const url = new URL(buildSettingsUrl(config.endpoint, config.siteUuid));
@@ -976,7 +1138,7 @@ async function postManifest(config, payload) {
976
1138
  const timeoutMs = config.timeoutMs;
977
1139
  let response;
978
1140
  try {
979
- response = await fetch(url, {
1141
+ response = await pulseFetch(config, url, {
980
1142
  method: "POST",
981
1143
  headers: {
982
1144
  "Content-Type": "application/json",
@@ -1273,9 +1435,11 @@ async function resolveConfig(options) {
1273
1435
  );
1274
1436
  }
1275
1437
  const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
1438
+ const pulseAuthRaw = fromEnv.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
1276
1439
  return {
1277
1440
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1278
1441
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
1442
+ pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
1279
1443
  endpoint,
1280
1444
  timeoutMs,
1281
1445
  environment,
@@ -1296,6 +1460,10 @@ async function persistApiKey(cwd, apiKey) {
1296
1460
  const existing = await readConfigFile(cwd);
1297
1461
  return writeConfigFile(cwd, { ...existing, apiKey });
1298
1462
  }
1463
+ async function persistPulseAuth(cwd, pulseAuth) {
1464
+ const existing = await readConfigFile(cwd);
1465
+ return writeConfigFile(cwd, { ...existing, pulseAuth });
1466
+ }
1299
1467
  async function readConfigFile(cwd) {
1300
1468
  const target = path6.join(cwd, CONFIG_FILENAME);
1301
1469
  let raw;
@@ -1338,6 +1506,7 @@ function readEnv() {
1338
1506
  return {
1339
1507
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
1340
1508
  apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
1509
+ pulseAuth: process.env.PATCHSTACK_PULSE_AUTH ?? void 0,
1341
1510
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
1342
1511
  timeoutMs,
1343
1512
  environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
@@ -2783,145 +2952,2562 @@ function renderGuideChecklist(state, useColor) {
2783
2952
  return lines.join("\n");
2784
2953
  }
2785
2954
 
2786
- // src/setup.ts
2787
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
2788
- import path10 from "path";
2789
- var SCAN_COMMAND = "patchstack-connect scan";
2790
- var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
2791
- function setupProtection(cwd) {
2792
- const install = runProtect(cwd);
2793
- const verification = runVerify(cwd);
2794
- return { install, verification };
2955
+ // src/login.ts
2956
+ function baseFrom(manifestEndpoint) {
2957
+ const url = new URL(manifestEndpoint);
2958
+ const path11 = url.pathname.replace(/\/$/, "");
2959
+ url.pathname = path11.endsWith("/manifest") ? path11.slice(0, -"/manifest".length) : "/monitor/pulse";
2960
+ url.search = "";
2961
+ url.hash = "";
2962
+ return url.toString().replace(/\/$/, "");
2963
+ }
2964
+ async function login(config, onPrompt, deps = {}) {
2965
+ const fetchImpl = deps.fetchImpl ?? fetch;
2966
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2967
+ const now = deps.now ?? (() => Date.now());
2968
+ if (config.siteUuid === null) {
2969
+ return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
2970
+ }
2971
+ const base = baseFrom(config.endpoint);
2972
+ const started = await fetchImpl(`${base}/device/code`, {
2973
+ method: "POST",
2974
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2975
+ body: JSON.stringify({ site_uuid: config.siteUuid })
2976
+ });
2977
+ if (started.status === 409) {
2978
+ return {
2979
+ status: "unclaimed",
2980
+ message: "This site has not been claimed yet, so there is no owner to approve the request. Claim it in the dashboard, or delete .patchstackrc.json to provision a new site."
2981
+ };
2982
+ }
2983
+ if (started.status === 404) {
2984
+ return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
2985
+ }
2986
+ if (!started.ok) {
2987
+ return { status: "failed", message: `Could not start the login (HTTP ${started.status}).` };
2988
+ }
2989
+ const { device_code: deviceCode, user_code: userCode, expires_in: expiresIn, interval } = await started.json();
2990
+ const verificationUri = `${new URL(base).origin}/activate`;
2991
+ onPrompt(userCode, verificationUri);
2992
+ const deadline = now() + expiresIn * 1e3;
2993
+ const intervalMs = Math.max(1, interval) * 1e3;
2994
+ while (now() < deadline) {
2995
+ await sleep(intervalMs);
2996
+ const polled = await fetchImpl(`${base}/device/token`, {
2997
+ method: "POST",
2998
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2999
+ body: JSON.stringify({ device_code: deviceCode })
3000
+ });
3001
+ if (polled.status === 428) continue;
3002
+ if (!polled.ok) return { status: "expired", message: "The login request expired. Run the command again." };
3003
+ const { api_key: apiKey } = await polled.json();
3004
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
3005
+ return { status: "failed", message: "Patchstack approved the request but returned no credential." };
3006
+ }
3007
+ await persistPulseAuth(process.cwd(), apiKey);
3008
+ await persistApiKey(process.cwd(), apiKey);
3009
+ return { status: "approved", userCode, verificationUri };
3010
+ }
3011
+ return { status: "expired", message: "The login request expired. Run the command again." };
2795
3012
  }
2796
- function appendHook(existing, command) {
2797
- if (existing === void 0 || existing.trim().length === 0) {
2798
- return command;
3013
+
3014
+ // src/map/ts-loader.ts
3015
+ import { createRequire } from "module";
3016
+ import { pathToFileURL } from "url";
3017
+ import { join as join11 } from "path";
3018
+ async function loadTypeScript(cwd) {
3019
+ try {
3020
+ const req = createRequire(pathToFileURL(join11(cwd, "package.json")));
3021
+ const resolved = req.resolve("typescript");
3022
+ const mod = await import(pathToFileURL(resolved).href);
3023
+ return mod.default ?? mod;
3024
+ } catch {
2799
3025
  }
2800
- if (existing.includes(command)) {
2801
- return existing;
3026
+ try {
3027
+ const mod = await import("typescript");
3028
+ return mod.default ?? mod;
3029
+ } catch {
3030
+ return null;
2802
3031
  }
2803
- return `${existing} && ${command}`;
2804
3032
  }
2805
- function wireBuildScripts(cwd, packageManager) {
2806
- const target = path10.join(cwd, "package.json");
2807
- const raw = readFileSync6(target, "utf8");
2808
- const pkg = JSON.parse(raw);
2809
- const scripts = pkg.scripts ?? {};
2810
- const build = scripts.build;
2811
- const postinstall = appendHook(scripts.postinstall, SCAN_COMMAND);
2812
- if (build === void 0 || build.trim().length === 0) {
2813
- if (postinstall === scripts.postinstall) {
2814
- return {
2815
- changed: false,
2816
- strategy: "postinstall-only",
2817
- detail: "dependency-install scan already wired; no build script to integrate."
2818
- };
3033
+
3034
+ // src/map/extract.ts
3035
+ import { readFileSync as readFileSync10, realpathSync as realpathSync3 } from "fs";
3036
+ import { createHash as createHash3 } from "crypto";
3037
+ import { relative as relative4 } from "path";
3038
+
3039
+ // src/map/ast.ts
3040
+ function rootIdentifier(node, ts) {
3041
+ let cur = node;
3042
+ while (cur) {
3043
+ if (ts.isIdentifier(cur)) return cur.text;
3044
+ if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur) || ts.isCallExpression(cur) || ts.isNewExpression(cur) || ts.isNonNullExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAwaitExpression(cur)) {
3045
+ cur = cur.expression;
3046
+ } else return void 0;
3047
+ }
3048
+ return void 0;
3049
+ }
3050
+ function spanOf(node) {
3051
+ const out = { line: lineOf(node) };
3052
+ try {
3053
+ out.start = node.getStart();
3054
+ out.end = node.getEnd();
3055
+ } catch {
3056
+ }
3057
+ return out;
3058
+ }
3059
+ function lineOf(node) {
3060
+ const sf = typeof node?.getSourceFile === "function" ? node.getSourceFile() : void 0;
3061
+ if (!sf) return void 0;
3062
+ try {
3063
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
3064
+ } catch {
3065
+ return void 0;
3066
+ }
3067
+ }
3068
+ function guessScriptKind(ts, file) {
3069
+ if (file.endsWith(".tsx")) return ts.ScriptKind.TSX;
3070
+ if (file.endsWith(".jsx")) return ts.ScriptKind.JSX;
3071
+ if (file.endsWith(".js")) return ts.ScriptKind.JS;
3072
+ return ts.ScriptKind.TS;
3073
+ }
3074
+ function isFnLike(n, ts) {
3075
+ return ts.isArrowFunction(n) || ts.isFunctionExpression(n);
3076
+ }
3077
+ function hasExport(node, ts) {
3078
+ return Boolean(node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword));
3079
+ }
3080
+ function unwindChain(node, ts) {
3081
+ const calls = {};
3082
+ let cur = node;
3083
+ while (cur && ts.isCallExpression(cur)) {
3084
+ const callee = cur.expression;
3085
+ if (ts.isPropertyAccessExpression(callee)) {
3086
+ calls[callee.name.text] = cur;
3087
+ cur = callee.expression;
3088
+ } else if (ts.isIdentifier(callee)) return { baseName: callee.text, baseCall: cur, calls };
3089
+ else break;
3090
+ }
3091
+ return { calls };
3092
+ }
3093
+ function methodFromObjectArg(baseCall, ts) {
3094
+ const arg = baseCall?.arguments?.[0];
3095
+ if (!arg || !ts.isObjectLiteralExpression(arg)) return void 0;
3096
+ for (const p of arg.properties) {
3097
+ if (ts.isPropertyAssignment(p) && p.name?.text === "method" && ts.isStringLiteralLike(p.initializer)) {
3098
+ return p.initializer.text.toUpperCase();
2819
3099
  }
2820
- scripts.postinstall = postinstall;
2821
- } else if (packageManager === "bun") {
2822
- let nextBuild = build;
2823
- if (!nextBuild.includes(SCAN_COMMAND)) {
2824
- nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
3100
+ }
3101
+ return void 0;
3102
+ }
3103
+ function bindingKey(el, ts) {
3104
+ if (!ts.isBindingElement(el)) return void 0;
3105
+ const prop = el.propertyName ?? el.name;
3106
+ return prop && ts.isIdentifier(prop) ? prop.text : void 0;
3107
+ }
3108
+ function isUninvokedFunctionDeclaration(n, ts) {
3109
+ if (ts.isFunctionDeclaration(n)) return true;
3110
+ if (isFnLike(n, ts)) {
3111
+ const p = n.parent;
3112
+ if (p && (ts.isVariableDeclaration(p) || ts.isPropertyAssignment(p) || ts.isPropertyDeclaration(p))) return true;
3113
+ }
3114
+ return false;
3115
+ }
3116
+ function isShadowedByEnclosingBinding(node, name, ts) {
3117
+ for (let cur = node?.parent; cur; cur = cur.parent) {
3118
+ if (ts.isCatchClause(cur) && cur.variableDeclaration && ts.isIdentifier(cur.variableDeclaration.name) && cur.variableDeclaration.name.text === name) return true;
3119
+ const params = cur.parameters;
3120
+ if (!params) continue;
3121
+ for (const p of params) {
3122
+ if (!p?.name) continue;
3123
+ if (ts.isIdentifier(p.name) && p.name.text === name) return true;
3124
+ if (ts.isObjectBindingPattern(p.name) || ts.isArrayBindingPattern(p.name)) {
3125
+ for (const el of p.name.elements) {
3126
+ if (ts.isBindingElement(el) && ts.isIdentifier(el.name) && el.name.text === name) return true;
3127
+ }
3128
+ }
2825
3129
  }
2826
- if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
2827
- nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
3130
+ }
3131
+ return false;
3132
+ }
3133
+ function calleeName(call, ts) {
3134
+ const c = call?.expression;
3135
+ if (!c) return void 0;
3136
+ if (ts.isPropertyAccessExpression(c)) return c.name.text;
3137
+ if (ts.isIdentifier(c)) return c.text;
3138
+ return void 0;
3139
+ }
3140
+ function isValueRead(id, ts) {
3141
+ const p = id.parent;
3142
+ if (!p) return true;
3143
+ if (ts.isPropertyAssignment(p) && p.name === id) return false;
3144
+ if (ts.isPropertyAccessExpression(p) && p.name === id) return false;
3145
+ if (ts.isBindingElement(p) && p.propertyName === id) return false;
3146
+ if ((ts.isVariableDeclaration(p) || ts.isParameter(p) || ts.isBindingElement(p)) && p.name === id) return false;
3147
+ if (ts.isPropertySignature(p) || ts.isMethodSignature(p)) return false;
3148
+ return true;
3149
+ }
3150
+ function opCallOf(propAccess, ts) {
3151
+ const p = propAccess?.parent;
3152
+ return p && ts.isCallExpression(p) && p.expression === propAccess ? p : propAccess;
3153
+ }
3154
+ function localCalls(node, ts) {
3155
+ const names = [];
3156
+ const visit = (n) => {
3157
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) names.push(n.expression.text);
3158
+ ts.forEachChild(n, visit);
3159
+ };
3160
+ visit(node);
3161
+ return names;
3162
+ }
3163
+
3164
+ // src/map/bindings.ts
3165
+ import { builtinModules } from "module";
3166
+ var BUILTINS = new Set(builtinModules);
3167
+ function buildModuleBindings(sf, ts) {
3168
+ const nameToModule = /* @__PURE__ */ new Map();
3169
+ const declared = /* @__PURE__ */ new Set();
3170
+ const exportNames = /* @__PURE__ */ new Map();
3171
+ const members = /* @__PURE__ */ new Set();
3172
+ const imports = /* @__PURE__ */ new Set();
3173
+ const origins = /* @__PURE__ */ new Map();
3174
+ const record = (local, mod, origin = "direct") => {
3175
+ nameToModule.set(local, mod);
3176
+ imports.add(mod);
3177
+ if (!origins.has(local)) origins.set(local, origin);
3178
+ };
3179
+ const declareBound = (nameNode) => {
3180
+ if (ts.isIdentifier(nameNode)) declared.add(nameNode.text);
3181
+ else if (ts.isObjectBindingPattern(nameNode) || ts.isArrayBindingPattern(nameNode)) {
3182
+ for (const el of nameNode.elements) if (ts.isBindingElement(el) && ts.isIdentifier(el.name)) declared.add(el.name.text);
2828
3183
  }
2829
- if (nextBuild === build && postinstall === scripts.postinstall) {
2830
- return {
2831
- changed: false,
2832
- strategy: "build-chain",
2833
- detail: "dependency-install scan and build chain already wired."
2834
- };
3184
+ };
3185
+ const visit = (node) => {
3186
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
3187
+ const mod = node.moduleSpecifier.text;
3188
+ imports.add(mod);
3189
+ const clause = node.importClause;
3190
+ if (clause?.name) record(clause.name.text, mod);
3191
+ const nb = clause?.namedBindings;
3192
+ if (nb) {
3193
+ if (ts.isNamespaceImport(nb)) record(nb.name.text, mod);
3194
+ else if (ts.isNamedImports(nb)) for (const el of nb.elements) {
3195
+ record(el.name.text, mod);
3196
+ members.add(el.name.text);
3197
+ if (el.propertyName && ts.isIdentifier(el.propertyName)) exportNames.set(el.name.text, el.propertyName.text);
3198
+ }
3199
+ }
2835
3200
  }
2836
- scripts.postinstall = postinstall;
2837
- scripts.build = nextBuild;
2838
- } else {
2839
- const prebuild = appendHook(scripts.prebuild, SCAN_COMMAND);
2840
- const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
2841
- if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
2842
- return {
2843
- changed: false,
2844
- strategy: "lifecycle-hooks",
2845
- detail: "dependency-install, prebuild, and postbuild hooks are already wired."
2846
- };
3201
+ if (ts.isFunctionDeclaration(node) && node.name) declared.add(node.name.text);
3202
+ if (ts.isClassDeclaration(node) && node.name) declared.add(node.name.text);
3203
+ if (ts.isVariableStatement(node)) {
3204
+ for (const decl of node.declarationList.declarations) {
3205
+ declareBound(decl.name);
3206
+ const init = decl.initializer;
3207
+ const reqMod = requireSpecifier(init, ts);
3208
+ if (reqMod) {
3209
+ if (ts.isIdentifier(decl.name)) record(decl.name.text, reqMod);
3210
+ else if (ts.isObjectBindingPattern(decl.name)) {
3211
+ for (const el of decl.name.elements) if (ts.isIdentifier(el.name)) {
3212
+ record(el.name.text, reqMod);
3213
+ members.add(el.name.text);
3214
+ if (el.propertyName && ts.isIdentifier(el.propertyName)) exportNames.set(el.name.text, el.propertyName.text);
3215
+ }
3216
+ }
3217
+ }
3218
+ if (init && ts.isIdentifier(decl.name)) {
3219
+ const callee = ts.isCallExpression(init) ? init.expression : ts.isNewExpression(init) ? init.expression : void 0;
3220
+ const root = callee ? rootIdentifier(callee, ts) : void 0;
3221
+ if (root) {
3222
+ pending.push([decl.name.text, root]);
3223
+ if (nameToModule.has(root)) record(decl.name.text, nameToModule.get(root), "factory");
3224
+ }
3225
+ }
3226
+ }
2847
3227
  }
2848
- scripts.postinstall = postinstall;
2849
- scripts.prebuild = prebuild;
2850
- scripts.postbuild = postbuild;
2851
- }
2852
- pkg.scripts = scripts;
2853
- const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
2854
- const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
2855
- const trailingNewline = raw.endsWith("\n") ? "\n" : "";
2856
- writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
2857
- if (build === void 0 || build.trim().length === 0) {
2858
- return {
2859
- changed: true,
2860
- strategy: "postinstall-only",
2861
- detail: "added a dependency-install scan; package.json has no build script."
2862
- };
3228
+ if ((ts.isFunctionDeclaration(node) || isFnLike(node, ts)) && node.body) {
3229
+ const fnName = ts.isFunctionDeclaration(node) && node.name ? node.name.text : ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name) ? node.parent.name.text : void 0;
3230
+ if (fnName) {
3231
+ const root = returnedRootIdentifier(node.body, ts);
3232
+ if (root) pending.push([fnName, root]);
3233
+ }
3234
+ }
3235
+ ts.forEachChild(node, visit);
3236
+ };
3237
+ const pending = [];
3238
+ visit(sf);
3239
+ for (let i = 0; i < 4; i++) {
3240
+ let changed = false;
3241
+ for (const [name, root] of pending) {
3242
+ if (!nameToModule.has(name) && nameToModule.has(root)) {
3243
+ record(name, nameToModule.get(root), "factory");
3244
+ changed = true;
3245
+ }
3246
+ }
3247
+ if (!changed) break;
2863
3248
  }
2864
- return packageManager === "bun" ? {
2865
- changed: true,
2866
- strategy: "build-chain",
2867
- detail: "added a dependency-install scan and chained scan/mark-build around the build."
2868
- } : {
2869
- changed: true,
2870
- strategy: "lifecycle-hooks",
2871
- detail: "added scans to postinstall/prebuild and mark-build to postbuild."
3249
+ const locals = new Set([...declared].filter((n) => !nameToModule.has(n)));
3250
+ return {
3251
+ resolve: (name) => nameToModule.get(name),
3252
+ exportNameOf: (name) => exportNames.get(name),
3253
+ originOf: (name) => origins.get(name),
3254
+ isPackageMember: (name) => members.has(name),
3255
+ imports,
3256
+ locals
2872
3257
  };
2873
3258
  }
3259
+ function returnedRootIdentifier(body, ts) {
3260
+ if (!ts.isBlock(body)) return rootIdentifier(body, ts);
3261
+ let found;
3262
+ const visit = (n) => {
3263
+ if (found) return;
3264
+ if (isUninvokedFunctionDeclaration(n, ts) && n !== body) return;
3265
+ if (ts.isReturnStatement(n) && n.expression) {
3266
+ found = rootIdentifier(n.expression, ts);
3267
+ return;
3268
+ }
3269
+ ts.forEachChild(n, visit);
3270
+ };
3271
+ visit(body);
3272
+ return found;
3273
+ }
3274
+ function requireSpecifier(init, ts) {
3275
+ if (init && ts.isCallExpression(init) && ts.isIdentifier(init.expression) && init.expression.text === "require") {
3276
+ const a = init.arguments[0];
3277
+ if (a && ts.isStringLiteralLike(a)) return a.text;
3278
+ }
3279
+ return void 0;
3280
+ }
3281
+ function npmPackageOf(spec) {
3282
+ if (!spec) return void 0;
3283
+ if (spec.startsWith(".") || spec.startsWith("/")) return void 0;
3284
+ if (spec.startsWith("node:")) return spec;
3285
+ if (BUILTINS.has(spec)) return `node:${spec}`;
3286
+ const parts = spec.split("/");
3287
+ return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
3288
+ }
2874
3289
 
2875
- // src/cli.ts
2876
- var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
2877
-
2878
- Usage:
2879
- patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
2880
- If no UUID is configured, the server
2881
- provisions one and we persist it. After a
2882
- successful post it also adds/updates the
2883
- disclosure-widget <script> tag in the root
2884
- HTML shell (index.html, public/index.html,
2885
- or src/app.html) \u2014 opt out with
2886
- "widget": false in .patchstackrc.json
2887
- patchstack-connect setup [options] Finish the bounded project setup: run scan,
2888
- manage the widget, install + verify runtime
2889
- protection, and wire dependency/build scans.
2890
- Never runs the project build
2891
- patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
2892
- with an existing site UUID
2893
- patchstack-connect status [options] Show current configuration and whether the
2894
- site still exists on Patchstack (active /
2895
- removed)
2896
- patchstack-connect mark-build [options] Stamp built HTML with a production flag +
2897
- build fingerprint, and ensure the widget
2898
- tag in built pages (run as a postbuild step)
2899
- patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
2900
- guard). Auto-wires supported server stacks;
2901
- for others it scaffolds a
2902
- generic guard + prints a wiring plan.
2903
- --demo seeds a broad sample rule set (for
2904
- demonstrations, not production).
2905
- --check verifies the guard is wired (exit 1
2906
- if not) \u2014 for the wire-then-verify loop.
2907
- patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
2908
- walkthrough: verify the vulnerable package,
2909
- scan it, wait for live rule 18843, install +
2910
- verify the guard, and print test requests.
2911
- Does not install vulnerable dependencies.
2912
- patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
2913
- preparing, running, proving, and cleaning up
2914
- the production-backed local demo.
2915
- patchstack-connect guide [--full] Show this project's setup status (what's done,
2916
- what's missing, with tailored commands), then
2917
- print the full setup guide. --full prints the
2918
- guide even when setup is complete
2919
- patchstack-connect help Print this message
2920
-
2921
- Options (for scan, setup, and status):
2922
- --site-uuid <uuid> Override the configured site UUID
2923
- --endpoint <url> Override the API endpoint
2924
- --dry-run (scan only) Show the payload without posting
3290
+ // src/map/sources.ts
3291
+ import { readFileSync as readFileSync6, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
3292
+ import { join as join12, relative as relative2, isAbsolute } from "path";
3293
+
3294
+ // src/map/routes.ts
3295
+ var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
3296
+ var ROUTE_REGISTER = new Set(ROUTE_REGISTER_NAMES);
3297
+ var ROUTE_CALL_RE = new RegExp(`\\.(${[...ROUTE_REGISTER_NAMES, "route"].join("|")})\\s*\\(`);
3298
+ function functionNameFromPath(relFile) {
3299
+ const parts = relFile.split(/[\\/]/).filter(Boolean);
3300
+ const i = parts.lastIndexOf("functions");
3301
+ if (i === -1 || i === parts.length - 1) return void 0;
3302
+ const next = parts[i + 1];
3303
+ if (!next) return void 0;
3304
+ const base = next.replace(/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/, "");
3305
+ return base === "index" ? void 0 : base;
3306
+ }
3307
+ function routeFromFilePath(relFile) {
3308
+ const parts = relFile.split(/[\\/]/).filter(Boolean);
3309
+ if (parts.length === 0) return {};
3310
+ const base = (parts[parts.length - 1] ?? "").replace(/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/, "");
3311
+ const dirs = parts.slice(0, -1);
3312
+ const at = (name) => dirs.lastIndexOf(name);
3313
+ let segs = null;
3314
+ if (base === "route" && at("app") !== -1) {
3315
+ segs = dirs.slice(at("app") + 1);
3316
+ } else if (base === "+server" && at("routes") !== -1) {
3317
+ segs = dirs.slice(at("routes") + 1);
3318
+ } else if (at("pages") !== -1) {
3319
+ segs = [...dirs.slice(at("pages") + 1), ...base === "index" ? [] : [base]];
3320
+ } else if (at("server") !== -1) {
3321
+ segs = [...dirs.slice(at("server") + 1), ...base === "index" ? [] : [base.replace(/\.(get|post|put|patch|delete|head|options)$/i, "")]];
3322
+ }
3323
+ if (!segs) return {};
3324
+ segs = segs.filter((s) => !(s.startsWith("(") && s.endsWith(")")) && !s.startsWith("@") && !s.startsWith("_"));
3325
+ let dynamic = false;
3326
+ const mapped = segs.map((s) => {
3327
+ const m = /^\[+(\.{0,3})(.+?)\]+$/.exec(s);
3328
+ if (m) {
3329
+ dynamic = true;
3330
+ return ":" + m[2];
3331
+ }
3332
+ return s;
3333
+ });
3334
+ const route = "/" + mapped.join("/");
3335
+ return { route: route.length > 1 ? route.replace(/\/+$/, "") : "/", dynamic };
3336
+ }
3337
+ function routeFromChain(expr, ts) {
3338
+ let cur = expr;
3339
+ while (cur && ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) {
3340
+ const nm = cur.expression.name.text;
3341
+ if (nm === "route") {
3342
+ const a = cur.arguments[0];
3343
+ return a && ts.isStringLiteralLike(a) ? a.text : void 0;
3344
+ }
3345
+ if (!ROUTE_REGISTER.has(nm)) return void 0;
3346
+ cur = cur.expression.expression;
3347
+ }
3348
+ return void 0;
3349
+ }
3350
+ function routeObject(obj, ts) {
3351
+ let url;
3352
+ let handler;
3353
+ const methods = [];
3354
+ for (const p of obj.properties) {
3355
+ const key = p.name?.text;
3356
+ if (ts.isPropertyAssignment(p)) {
3357
+ if ((key === "url" || key === "path") && ts.isStringLiteralLike(p.initializer)) url = p.initializer.text;
3358
+ if (key === "method") {
3359
+ if (ts.isStringLiteralLike(p.initializer)) methods.push(p.initializer.text.toUpperCase());
3360
+ else if (ts.isArrayLiteralExpression(p.initializer)) {
3361
+ for (const el of p.initializer.elements) if (ts.isStringLiteralLike(el)) methods.push(el.text.toUpperCase());
3362
+ }
3363
+ }
3364
+ if (key === "handler" && isFnLike(p.initializer, ts)) handler = p.initializer;
3365
+ } else if (ts.isMethodDeclaration(p) && key === "handler") handler = p;
3366
+ }
3367
+ return { url, methods, handler };
3368
+ }
3369
+
3370
+ // src/map/sources.ts
3371
+ function hasEntrySignal(text) {
3372
+ return text.includes("createServerFn") || /\bexport\s+(async\s+)?(function|const)\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/.test(text) || ROUTE_CALL_RE.test(text) || text.includes("'use server'") || text.includes('"use server"') || text.includes("Deno.serve") || /\bserve\s*\(/.test(text);
3373
+ }
3374
+ function detectFramework(cwd) {
3375
+ try {
3376
+ const pkg = JSON.parse(readFileSync6(join12(cwd, "package.json"), "utf8"));
3377
+ const d = { ...pkg.dependencies, ...pkg.devDependencies };
3378
+ if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
3379
+ if (d["next"]) return "next";
3380
+ if (d["@sveltejs/kit"]) return "sveltekit";
3381
+ if (d["@nestjs/core"]) return "nestjs";
3382
+ if (d["fastify"]) return "fastify";
3383
+ if (d["express"]) return "express";
3384
+ if (d["hono"]) return "hono";
3385
+ } catch {
3386
+ }
3387
+ try {
3388
+ if (statSync3(join12(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
3389
+ } catch {
3390
+ }
3391
+ try {
3392
+ if (statSync3(join12(cwd, "functions")).isDirectory()) return "deno-functions";
3393
+ } catch {
3394
+ }
3395
+ return "unknown";
3396
+ }
3397
+ var isSourceFile = (name) => /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(name) && !name.endsWith(".d.ts");
3398
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3399
+ "node_modules",
3400
+ "dist",
3401
+ "build",
3402
+ "out",
3403
+ "coverage",
3404
+ "public",
3405
+ "static",
3406
+ "assets",
3407
+ ".git",
3408
+ ".next",
3409
+ ".nuxt",
3410
+ ".svelte-kit",
3411
+ ".output",
3412
+ ".vercel",
3413
+ ".wrangler",
3414
+ ".turbo",
3415
+ ".cache",
3416
+ "vendor",
3417
+ "tmp",
3418
+ "temp",
3419
+ "__pycache__"
3420
+ ]);
3421
+ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0 }) {
3422
+ let key;
3423
+ try {
3424
+ key = realpathSync(dir);
3425
+ } catch {
3426
+ stats.unwalked++;
3427
+ return out;
3428
+ }
3429
+ if (seen.has(key)) return out;
3430
+ if (!opts.followOutside && !isInside(key, boundary)) {
3431
+ stats.unwalked++;
3432
+ return out;
3433
+ }
3434
+ seen.add(key);
3435
+ let entries;
3436
+ try {
3437
+ entries = readdirSync5(dir, { withFileTypes: true });
3438
+ } catch {
3439
+ stats.unwalked++;
3440
+ return out;
3441
+ }
3442
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
3443
+ for (const e of entries) {
3444
+ if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".") continue;
3445
+ const full = join12(dir, e.name);
3446
+ if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
3447
+ else if (e.isSymbolicLink()) {
3448
+ let st, real;
3449
+ try {
3450
+ st = statSync3(full);
3451
+ real = realpathSync(full);
3452
+ } catch {
3453
+ stats.unwalked++;
3454
+ continue;
3455
+ }
3456
+ if (!opts.followOutside && !isInside(real, boundary)) {
3457
+ if (st.isDirectory() || isSourceFile(e.name)) stats.unwalked++;
3458
+ continue;
3459
+ }
3460
+ if (st.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
3461
+ else if (st.isFile() && isSourceFile(e.name)) {
3462
+ out.push(full);
3463
+ stats.discovered++;
3464
+ }
3465
+ } else if (isSourceFile(e.name)) {
3466
+ out.push(full);
3467
+ stats.discovered++;
3468
+ }
3469
+ }
3470
+ return out;
3471
+ }
3472
+ function isInside(candidate, boundary) {
3473
+ if (candidate === boundary) return true;
3474
+ const rel = relative2(boundary, candidate);
3475
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
3476
+ }
3477
+ var DEPLOYMENT_SHAPES = [
3478
+ // Config first: these are declarations by the project itself, and they survive a build output being
3479
+ // absent (a fresh clone has no `.vercel`/`.wrangler` directory).
3480
+ { shape: "vercel", evidence: "deployment-config", files: ["vercel.json"] },
3481
+ { shape: "netlify", evidence: "deployment-config", files: ["netlify.toml"] },
3482
+ // Wrangler names a Workers/Pages deployment. `.jsonc` and `.json` are both current spellings.
3483
+ { shape: "cloudflare-workers", evidence: "deployment-config", files: ["wrangler.toml", "wrangler.jsonc", "wrangler.json"] },
3484
+ // Pages advanced mode: a single worker entry at the project root takes over routing entirely. Unlike a
3485
+ // wrangler config, this file IS the server — it is a runtime entry, not a deployment declaration.
3486
+ { shape: "cloudflare-pages-advanced", evidence: "runtime-entry", files: ["_worker.js", "_worker.ts"] },
3487
+ { shape: "netlify-functions", evidence: "runtime-entry", dirs: ["netlify/functions", "netlify/edge-functions"] },
3488
+ { shape: "supabase-functions", evidence: "runtime-entry", dirs: ["supabase/functions"], requiresFunctionEntry: true },
3489
+ // Ambiguous by nature and reported as one shape: a root `functions/` directory is Cloudflare Pages
3490
+ // Functions, Firebase functions, or a Deno layout depending on the platform, and nothing inside the
3491
+ // repository always distinguishes them. Naming it honestly is better than guessing a provider.
3492
+ { shape: "root-functions-directory", evidence: "layout", dirs: ["functions"] },
3493
+ // The bare-root Vercel convention: `api/handler.ts` with no framework router. Next owns `pages/api`
3494
+ // and `app/api` instead, which the endpoint walk already recognizes, so this is reported as its own
3495
+ // shape rather than folded into `vercel`.
3496
+ { shape: "root-api-directory", evidence: "layout", dirs: ["api"] }
3497
+ ];
3498
+ function detectDeploymentShapes(cwd, opts = {}) {
3499
+ let boundary = opts.boundary ?? cwd;
3500
+ try {
3501
+ boundary = realpathSync(boundary);
3502
+ } catch {
3503
+ }
3504
+ const inProject = (path11) => {
3505
+ if (opts.followOutside) return true;
3506
+ try {
3507
+ return isInside(realpathSync(path11), boundary);
3508
+ } catch {
3509
+ return false;
3510
+ }
3511
+ };
3512
+ const found = [];
3513
+ for (const candidate of DEPLOYMENT_SHAPES) {
3514
+ for (const file of candidate.files ?? []) {
3515
+ const full = join12(cwd, file);
3516
+ try {
3517
+ if (statSync3(full).isFile() && inProject(full)) {
3518
+ found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
3519
+ break;
3520
+ }
3521
+ } catch {
3522
+ }
3523
+ }
3524
+ for (const dir of candidate.dirs ?? []) {
3525
+ const full = join12(cwd, dir);
3526
+ try {
3527
+ if (!statSync3(full).isDirectory() || !inProject(full)) continue;
3528
+ } catch {
3529
+ continue;
3530
+ }
3531
+ if (candidate.requiresFunctionEntry ? holdsFunctionEntry(full) : holdsSourceFile(full)) {
3532
+ found.push({ shape: candidate.shape, source: dir, evidence: candidate.evidence });
3533
+ break;
3534
+ }
3535
+ }
3536
+ }
3537
+ return found;
3538
+ }
3539
+ function holdsFunctionEntry(dir) {
3540
+ try {
3541
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
3542
+ if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
3543
+ try {
3544
+ for (const nested of readdirSync5(join12(dir, entry.name), { withFileTypes: true })) {
3545
+ if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
3546
+ }
3547
+ } catch {
3548
+ }
3549
+ }
3550
+ } catch {
3551
+ }
3552
+ return false;
3553
+ }
3554
+ function holdsSourceFile(dir) {
3555
+ try {
3556
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
3557
+ if (entry.isFile() && isSourceFile(entry.name)) return true;
3558
+ if (entry.isDirectory()) {
3559
+ try {
3560
+ for (const nested of readdirSync5(join12(dir, entry.name), { withFileTypes: true })) {
3561
+ if (nested.isFile() && isSourceFile(nested.name)) return true;
3562
+ }
3563
+ } catch {
3564
+ }
3565
+ }
3566
+ }
3567
+ } catch {
3568
+ }
3569
+ return false;
3570
+ }
3571
+
3572
+ // src/map/surface.ts
3573
+ import { readFileSync as readFileSync7 } from "fs";
3574
+ import { join as join13 } from "path";
3575
+ var STATIC_GENERATORS = [
3576
+ { dep: "vite", label: "vite" },
3577
+ { dep: "react-scripts", label: "create-react-app" },
3578
+ { dep: "gatsby", label: "gatsby" },
3579
+ { dep: "parcel", label: "parcel" },
3580
+ { dep: "@sveltejs/adapter-static", label: "sveltekit-static-adapter" }
3581
+ ];
3582
+ var SSR_COMPANIONS = ["vike", "vite-plugin-ssr", "@react-router/node", "@react-router/serve", "vite-plugin-node"];
3583
+ var SERVER_DEPENDENCIES = [
3584
+ "express",
3585
+ "fastify",
3586
+ "hono",
3587
+ "koa",
3588
+ "@nestjs/core",
3589
+ "@hapi/hapi",
3590
+ "h3",
3591
+ "polka",
3592
+ "restify",
3593
+ "@tanstack/react-start",
3594
+ "@tanstack/start",
3595
+ "@tanstack/solid-start",
3596
+ "nuxt",
3597
+ "remix",
3598
+ "@remix-run/node",
3599
+ "@remix-run/server-runtime"
3600
+ ];
3601
+ var CONDITIONAL_SERVER_DEPENDENCIES = [
3602
+ { dep: "@sveltejs/kit", staticWhen: "@sveltejs/adapter-static" }
3603
+ ];
3604
+ var SSR_ADAPTERS = ["@astrojs/node", "@astrojs/vercel", "@astrojs/cloudflare", "@astrojs/netlify", "@astrojs/deno"];
3605
+ var SHAPE_SIGNAL = {
3606
+ "runtime-entry": "runtime-entry",
3607
+ "deployment-config": "deployment-config",
3608
+ layout: "ambiguous-layout"
3609
+ };
3610
+ function readManifest(cwd) {
3611
+ try {
3612
+ const parsed = JSON.parse(readFileSync7(join13(cwd, "package.json"), "utf8"));
3613
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
3614
+ } catch {
3615
+ return null;
3616
+ }
3617
+ }
3618
+ function nextExportsStatically(cwd, manifest, ts) {
3619
+ const scripts = Object.values(manifest.scripts ?? {}).join(" ");
3620
+ if (/\bnext\s+export\b/.test(scripts)) return "next export (build script)";
3621
+ if (ts === void 0) return null;
3622
+ for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
3623
+ let text;
3624
+ try {
3625
+ text = readFileSync7(join13(cwd, file), "utf8");
3626
+ } catch {
3627
+ continue;
3628
+ }
3629
+ try {
3630
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
3631
+ if (exportedConfigIsStatic(sf, ts)) return `output: 'export' (${file})`;
3632
+ } catch {
3633
+ }
3634
+ }
3635
+ return null;
3636
+ }
3637
+ function exportedConfigIsStatic(sf, ts) {
3638
+ const objects = [];
3639
+ const followed = /* @__PURE__ */ new Set();
3640
+ const collectFrom = (expr) => {
3641
+ if (!expr) return;
3642
+ if (ts.isObjectLiteralExpression(expr)) {
3643
+ objects.push(expr);
3644
+ return;
3645
+ }
3646
+ if (ts.isCallExpression(expr)) {
3647
+ for (const arg of expr.arguments) collectFrom(arg);
3648
+ return;
3649
+ }
3650
+ if (ts.isIdentifier(expr)) {
3651
+ const name = expr.text;
3652
+ if (followed.has(name)) return;
3653
+ followed.add(name);
3654
+ const visit = (node) => {
3655
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
3656
+ collectFrom(node.initializer);
3657
+ }
3658
+ ts.forEachChild(node, visit);
3659
+ };
3660
+ visit(sf);
3661
+ }
3662
+ };
3663
+ const findExports = (node) => {
3664
+ if (ts.isExportAssignment(node)) collectFrom(node.expression);
3665
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "exports") {
3666
+ collectFrom(node.right);
3667
+ }
3668
+ ts.forEachChild(node, findExports);
3669
+ };
3670
+ findExports(sf);
3671
+ for (const object of objects) {
3672
+ for (const property of object.properties) {
3673
+ if (!ts.isPropertyAssignment(property)) continue;
3674
+ const key = property.name;
3675
+ const keyName = ts.isIdentifier(key) || ts.isStringLiteralLike(key) ? key.text : void 0;
3676
+ if (keyName !== "output") continue;
3677
+ if (ts.isStringLiteralLike(property.initializer) && property.initializer.text === "export") return true;
3678
+ }
3679
+ }
3680
+ return false;
3681
+ }
3682
+ function staticSignals(cwd, ts) {
3683
+ const manifest = readManifest(cwd);
3684
+ if (manifest === null) return [];
3685
+ const deps = { ...manifest.dependencies, ...manifest.devDependencies };
3686
+ const signals = [];
3687
+ for (const generator of STATIC_GENERATORS) {
3688
+ if (deps[generator.dep] !== void 0) {
3689
+ signals.push({ signal: "static-generator", source: generator.label });
3690
+ }
3691
+ }
3692
+ if (deps["astro"] !== void 0 && !SSR_ADAPTERS.some((adapter) => deps[adapter] !== void 0)) {
3693
+ signals.push({ signal: "static-generator", source: "astro (no SSR adapter)" });
3694
+ }
3695
+ if (deps["next"] !== void 0) {
3696
+ const exported = nextExportsStatically(cwd, manifest, ts);
3697
+ if (exported !== null) signals.push({ signal: "static-generator", source: `next: ${exported}` });
3698
+ }
3699
+ return signals;
3700
+ }
3701
+ function serverDependencySignals(cwd, ts) {
3702
+ const manifest = readManifest(cwd);
3703
+ if (manifest === null) return [];
3704
+ const deps = { ...manifest.dependencies, ...manifest.devDependencies };
3705
+ const found = SERVER_DEPENDENCIES.filter((dep) => deps[dep] !== void 0);
3706
+ for (const conditional of CONDITIONAL_SERVER_DEPENDENCIES) {
3707
+ if (deps[conditional.dep] !== void 0 && deps[conditional.staticWhen] === void 0) {
3708
+ found.push(conditional.dep);
3709
+ }
3710
+ }
3711
+ for (const companion of SSR_COMPANIONS) if (deps[companion] !== void 0) found.push(companion);
3712
+ if (deps["next"] !== void 0 && nextExportsStatically(cwd, manifest, ts) === null) found.push("next");
3713
+ if (deps["astro"] !== void 0) {
3714
+ for (const adapter of SSR_ADAPTERS) if (deps[adapter] !== void 0) found.push(adapter);
3715
+ }
3716
+ return found.map((dep) => ({ signal: "server-dependency", source: dep }));
3717
+ }
3718
+ function classifyServerSurface(cwd, endpointCount, deploymentShapes, ts) {
3719
+ const evidence = [];
3720
+ if (endpointCount > 0) {
3721
+ evidence.push({ signal: "endpoint", source: `${endpointCount} recognized entry point(s)` });
3722
+ }
3723
+ for (const shape of deploymentShapes) {
3724
+ evidence.push({
3725
+ signal: SHAPE_SIGNAL[shape.evidence],
3726
+ source: `${shape.shape} (${shape.source})`
3727
+ });
3728
+ }
3729
+ const servers = serverDependencySignals(cwd, ts);
3730
+ const statics = staticSignals(cwd, ts);
3731
+ const servesRequests = deploymentShapes.some((shape) => shape.evidence === "runtime-entry");
3732
+ if (endpointCount > 0 || servesRequests) {
3733
+ return { state: "server-runtime-detected", evidence };
3734
+ }
3735
+ evidence.push(...servers, ...statics);
3736
+ const blocked = servers.length > 0 || deploymentShapes.length > 0;
3737
+ if (statics.length > 0 && !blocked) {
3738
+ return { state: "static-build-detected", evidence };
3739
+ }
3740
+ return { state: "unknown", evidence };
3741
+ }
3742
+ function surfaceNote(surface) {
3743
+ if (surface.state === "server-runtime-detected") {
3744
+ return "serverSurface: a server runtime was recognized in the analysed source (see its evidence). Request-path protection applies to this app.";
3745
+ }
3746
+ if (surface.state === "static-build-detected") {
3747
+ return "serverSurface: a static build was identified and no server runtime was recognized in the analysed source. This is NOT deployment attestation \u2014 it describes the source, not what is deployed, and a serverless function added at the platform level would not appear here. Advisories against this app are dependency and bundle concerns rather than request-path risk.";
3748
+ }
3749
+ return 'serverSurface is UNKNOWN: neither a server runtime nor a static build could be positively identified. This is the honest answer for an unrecognised stack, and it must not be read as "no server side" \u2014 an unparsed framework produces no endpoints, and entry-point recognition has no completeness flag.';
3750
+ }
3751
+
3752
+ // src/map/sinks.ts
3753
+ import { createHash as createHash2 } from "crypto";
3754
+ var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
3755
+ var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
3756
+ var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
3757
+ var EXEC_CALLS = /^(exec|execSync|spawn|spawnSync|execFile|execFileSync|fork)$/;
3758
+ var HTTP_CALLS = /^(fetch|got|request)$/;
3759
+ var HTTP_MEMBER_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "request"]);
3760
+ var DB_PACKAGES = ["@supabase/supabase-js", "@prisma/client", "drizzle-orm", "knex", "kysely", "pg", "mysql2", "mysql", "sequelize", "typeorm", "mongoose", "better-sqlite3"];
3761
+ var MORE_DB_PACKAGES = [
3762
+ "postgres",
3763
+ "mssql",
3764
+ "tedious",
3765
+ "oracledb",
3766
+ "sqlite3",
3767
+ "mongodb",
3768
+ "ioredis",
3769
+ "@planetscale/database",
3770
+ "@neondatabase/serverless",
3771
+ "@libsql/client",
3772
+ "@vercel/postgres",
3773
+ "slonik",
3774
+ "sql.js"
3775
+ ];
3776
+ var isDbPackage = (pkg) => DB_PACKAGES.includes(pkg) || MORE_DB_PACKAGES.includes(pkg) || [...DB_PACKAGES, ...MORE_DB_PACKAGES].some((p) => pkg.startsWith(p + "/"));
3777
+ var HTTP_PACKAGES = ["axios", "got", "node-fetch", "undici", "superagent", "ky"];
3778
+ var isHttpPackage = (pkg) => HTTP_PACKAGES.includes(pkg) || pkg === "node:http" || pkg === "node:https";
3779
+ var FS_PACKAGES = ["fs-extra", "graceful-fs", "memfs"];
3780
+ var isFsPackage = (pkg) => /^node:fs(\/promises)?$/.test(pkg) || FS_PACKAGES.includes(pkg);
3781
+ var EXEC_PACKAGES = ["execa", "cross-spawn", "shelljs", "zx"];
3782
+ var isExecPackage = (pkg) => pkg === "node:child_process" || EXEC_PACKAGES.includes(pkg);
3783
+ function recognizedSinkKinds(pkg) {
3784
+ const kinds = [];
3785
+ if (isDbPackage(pkg)) kinds.push("db");
3786
+ if (isFsPackage(pkg)) kinds.push("fs");
3787
+ if (isExecPackage(pkg)) kinds.push("exec");
3788
+ if (isHttpPackage(pkg)) kinds.push("http");
3789
+ return kinds;
3790
+ }
3791
+ function collectLocalSinks(sf, ts, bindings, ctx) {
3792
+ const map = /* @__PURE__ */ new Map();
3793
+ const visit = (node) => {
3794
+ if (ts.isFunctionDeclaration(node) && node.name && node.body) map.set(node.name.text, directSinks(node.body, ts, bindings, ctx));
3795
+ else if (ts.isVariableStatement(node)) {
3796
+ for (const decl of node.declarationList.declarations) {
3797
+ if (ts.isIdentifier(decl.name) && decl.initializer && isFnLike(decl.initializer, ts)) {
3798
+ map.set(decl.name.text, directSinks(decl.initializer.body, ts, bindings, ctx));
3799
+ }
3800
+ }
3801
+ }
3802
+ ts.forEachChild(node, visit);
3803
+ };
3804
+ visit(sf);
3805
+ return map;
3806
+ }
3807
+ function sinksFrom(arrowOrNode, ts, localSinks, bindings, ctx) {
3808
+ if (!arrowOrNode) return [];
3809
+ const body = arrowOrNode.isSyntheticBody ? arrowOrNode.body : isFnLike(arrowOrNode, ts) ? arrowOrNode.body : arrowOrNode;
3810
+ if (!body) return [];
3811
+ const sinks = directSinks(body, ts, bindings, ctx);
3812
+ for (const called of localCalls(body, ts)) {
3813
+ for (const s of localSinks.get(called) ?? []) sinks.push(s);
3814
+ if (ctx) {
3815
+ const spec = bindings.resolve(called);
3816
+ if (spec && spec.startsWith(".")) {
3817
+ for (const s of ctx.graph.importedSinks(ctx.file, spec, bindings.exportNameOf(called) ?? called)) sinks.push(s);
3818
+ }
3819
+ }
3820
+ }
3821
+ if (ctx) {
3822
+ for (const [root, member] of namespaceMemberCalls(body, ts)) {
3823
+ const spec = bindings.resolve(root);
3824
+ if (spec && spec.startsWith(".")) {
3825
+ for (const s of ctx.graph.importedSinks(ctx.file, spec, member)) sinks.push(s);
3826
+ }
3827
+ }
3828
+ }
3829
+ return dedupeSinks(sinks, ctx?.owner ?? "");
3830
+ }
3831
+ function namespaceMemberCalls(node, ts) {
3832
+ const out = [];
3833
+ const visit = (n) => {
3834
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && ts.isIdentifier(n.expression.expression)) {
3835
+ out.push([n.expression.expression.text, n.expression.name.text]);
3836
+ }
3837
+ ts.forEachChild(n, visit);
3838
+ };
3839
+ visit(node);
3840
+ return out;
3841
+ }
3842
+ function directSinks(node, ts, bindings, ctx) {
3843
+ const sinks = [];
3844
+ const baseOf = (base) => {
3845
+ const root = base ? rootIdentifier(base, ts) : void 0;
3846
+ if (!root) return {};
3847
+ const spec = bindings.resolve(root);
3848
+ const relative5 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
3849
+ const pkg = npmPackageOf(spec);
3850
+ if (pkg) return { pkg, root, spec };
3851
+ if (relative5 && ctx && spec) {
3852
+ const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
3853
+ if (viaModule) return { pkg: viaModule.package, root, spec };
3854
+ }
3855
+ return { local: bindings.locals.has(root), root, spec, relative: relative5 };
3856
+ };
3857
+ const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
3858
+ const dbApi = (pkg, attribution) => {
3859
+ if (pkg && !isDbPackage(pkg)) return { apiUnconfirmed: true };
3860
+ return attribution === "import" || attribution === "global" ? { provider: "sql" } : {};
3861
+ };
3862
+ const infer = (kind) => {
3863
+ const table = kind === "db" ? DB_PACKAGES : HTTP_PACKAGES;
3864
+ for (const p of table) if (bindings.imports.has(p)) return p;
3865
+ return void 0;
3866
+ };
3867
+ const push = (s) => sinks.push(s);
3868
+ const visit = (n) => {
3869
+ if (n !== node && isUninvokedFunctionDeclaration(n, ts)) return;
3870
+ if (ts.isCallExpression(n)) {
3871
+ const callee = n.expression;
3872
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === "from") {
3873
+ const b = baseOf(callee.expression);
3874
+ const t = n.arguments[0];
3875
+ const table = t && ts.isStringLiteralLike(t) ? t.text : void 0;
3876
+ const parent = n.parent;
3877
+ if (!b.local && !b.relative && parent && ts.isPropertyAccessExpression(parent) && DB_OPS.has(parent.name.text)) {
3878
+ const pkg = b.pkg ?? infer("db");
3879
+ const attribution = attributionOf(b, pkg);
3880
+ push({ kind: "db", ...dbApi(pkg, attribution), package: pkg, table, op: parent.name.text, attribution, ...spanOf(opCallOf(parent, ts)) });
3881
+ }
3882
+ }
3883
+ if (ts.isPropertyAccessExpression(callee)) {
3884
+ const method = callee.name.text;
3885
+ const b = baseOf(callee.expression);
3886
+ if (!b.local && !b.relative) {
3887
+ if (PRISMA_OPS.has(method) && ts.isPropertyAccessExpression(callee.expression)) {
3888
+ const prismaLikely = b.pkg === "@prisma/client" || !b.pkg && (bindings.imports.has("@prisma/client") || /prisma/i.test(b.root ?? ""));
3889
+ 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) });
3890
+ }
3891
+ if (method === "query" || method === "execute") {
3892
+ const pkg = b.pkg ?? infer("db");
3893
+ const attribution = attributionOf(b, pkg);
3894
+ push({ kind: "db", ...dbApi(pkg, attribution), package: pkg, op: method, attribution, ...spanOf(n) });
3895
+ }
3896
+ if (FS_CALLS.test(method) && b.pkg && isFsPackage(b.pkg)) {
3897
+ push({ kind: "fs", package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
3898
+ }
3899
+ if (EXEC_CALLS.test(method) && b.pkg && isExecPackage(b.pkg)) {
3900
+ push({ kind: "exec", package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
3901
+ }
3902
+ if (HTTP_MEMBER_METHODS.has(method)) {
3903
+ if (b.pkg && isHttpPackage(b.pkg)) {
3904
+ push({ kind: "http", provider: b.root, package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
3905
+ } else if (!b.spec && ts.isIdentifier(callee.expression) && /^(axios|http|https|got|ky)$/.test(callee.expression.text)) {
3906
+ push({ kind: "http", provider: callee.expression.text, package: infer("http"), op: method, attribution: "inferred", ...spanOf(n) });
3907
+ }
3908
+ }
3909
+ }
3910
+ }
3911
+ if (ts.isIdentifier(callee) && !bindings.locals.has(callee.text)) {
3912
+ const name = callee.text;
3913
+ const spec = bindings.resolve(name);
3914
+ const pkg = npmPackageOf(spec);
3915
+ const shadowed = isShadowedByEnclosingBinding(n, name, ts);
3916
+ const fromModule = spec !== void 0;
3917
+ const trueGlobal = !fromModule && !shadowed;
3918
+ if (HTTP_CALLS.test(name)) {
3919
+ if (pkg && isHttpPackage(pkg)) push({ kind: "http", provider: name, package: pkg, op: "request", attribution: "import", ...spanOf(n) });
3920
+ else if (name === "fetch" && trueGlobal) push({ kind: "http", provider: "fetch", op: "request", attribution: "global", ...spanOf(n) });
3921
+ }
3922
+ if (FS_CALLS.test(name) && pkg && isFsPackage(pkg)) {
3923
+ push({ kind: "fs", package: pkg, op: name, attribution: "import", ...spanOf(n) });
3924
+ }
3925
+ if (EXEC_CALLS.test(name) && pkg && isExecPackage(pkg)) {
3926
+ push({ kind: "exec", package: pkg, op: name, attribution: "import", ...spanOf(n) });
3927
+ }
3928
+ if (name === "eval" && trueGlobal) push({ kind: "eval", op: "eval", attribution: "global", ...spanOf(n) });
3929
+ }
3930
+ }
3931
+ if (ts.isNewExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "Function" && !bindings.locals.has("Function") && !isShadowedByEnclosingBinding(n, "Function", ts)) {
3932
+ push({ kind: "eval", op: "new Function", attribution: "global", ...spanOf(n) });
3933
+ }
3934
+ ts.forEachChild(n, visit);
3935
+ };
3936
+ visit(node);
3937
+ return sinks;
3938
+ }
3939
+ var ARGUMENT_ROLES = {
3940
+ exec: {
3941
+ exec: ["command"],
3942
+ execSync: ["command"],
3943
+ execFile: ["file", "args"],
3944
+ execFileSync: ["file", "args"],
3945
+ spawn: ["command", "args"],
3946
+ spawnSync: ["command", "args"],
3947
+ fork: ["file", "args"]
3948
+ },
3949
+ http: {
3950
+ fetch: ["url", "init"],
3951
+ request: ["url", "options"],
3952
+ get: ["url", "options"],
3953
+ head: ["url", "options"],
3954
+ delete: ["url", "options"],
3955
+ post: ["url", "body"],
3956
+ put: ["url", "body"],
3957
+ patch: ["url", "body"]
3958
+ },
3959
+ fs: {
3960
+ readFile: ["path"],
3961
+ readFileSync: ["path"],
3962
+ open: ["path"],
3963
+ writeFile: ["path", "content"],
3964
+ writeFileSync: ["path", "content"],
3965
+ appendFile: ["path", "content"],
3966
+ unlink: ["path"],
3967
+ rm: ["path"],
3968
+ rmSync: ["path"],
3969
+ mkdir: ["path"],
3970
+ readdir: ["path"],
3971
+ stat: ["path"],
3972
+ createReadStream: ["path"],
3973
+ createWriteStream: ["path"]
3974
+ },
3975
+ db: {
3976
+ query: ["sql", "values"],
3977
+ execute: ["sql", "values"],
3978
+ insert: ["values"],
3979
+ update: ["values"],
3980
+ upsert: ["values"],
3981
+ select: ["columns"],
3982
+ // Filters: the value half is still request data reaching the query, but as a bound parameter.
3983
+ eq: ["column", "value"],
3984
+ neq: ["column", "value"],
3985
+ gt: ["column", "value"],
3986
+ gte: ["column", "value"],
3987
+ lt: ["column", "value"],
3988
+ lte: ["column", "value"],
3989
+ like: ["column", "value"],
3990
+ ilike: ["column", "value"],
3991
+ match: ["values"],
3992
+ filter: ["column", "value"]
3993
+ },
3994
+ eval: { eval: ["code"], Function: ["code"] }
3995
+ };
3996
+ var CANDIDATE_FAMILIES = {
3997
+ http: { url: "ssrf" },
3998
+ exec: { command: "command-injection", file: "command-injection", args: "command-injection" },
3999
+ fs: { path: "path-traversal" },
4000
+ db: { sql: "sql-injection" },
4001
+ eval: { code: "code-injection" }
4002
+ };
4003
+ function argumentRoleOf(sinkKind, method, index, total = 0) {
4004
+ if (sinkKind === "eval" && method === "Function") return index === total - 1 ? "code" : "args";
4005
+ const table = method ? ARGUMENT_ROLES[sinkKind]?.[method] : void 0;
4006
+ return table?.[index] ?? "unknown";
4007
+ }
4008
+ function sinkId(s, owner) {
4009
+ return createHash2("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
4010
+ }
4011
+ function dedupeSinks(sinks, owner) {
4012
+ const seen = /* @__PURE__ */ new Set();
4013
+ const out = [];
4014
+ for (const s of sinks) {
4015
+ const key = `${s.kind}:${s.provider}:${s.package}:${s.table}:${s.op}:${s.line}:${s.start}`;
4016
+ if (!seen.has(key)) {
4017
+ seen.add(key);
4018
+ out.push({ ...s, id: sinkId(s, owner) });
4019
+ }
4020
+ }
4021
+ return out;
4022
+ }
4023
+
4024
+ // src/map/module-graph.ts
4025
+ import { readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
4026
+ import { dirname as dirname4, join as join14, relative as relative3, resolve as resolvePath } from "path";
4027
+ var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
4028
+ function createModuleGraph(ts, opts) {
4029
+ const cache = /* @__PURE__ */ new Map();
4030
+ const load = (file) => {
4031
+ if (cache.has(file)) return cache.get(file) ?? null;
4032
+ let entry = null;
4033
+ try {
4034
+ const text = readFileSync8(file, "utf8");
4035
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
4036
+ const bindings = buildModuleBindings(sf, ts);
4037
+ entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
4038
+ } catch {
4039
+ entry = null;
4040
+ }
4041
+ cache.set(file, entry);
4042
+ return entry;
4043
+ };
4044
+ const resolveInProject = (fromFile, specifier) => {
4045
+ const target = resolveRelativeModule(fromFile, specifier);
4046
+ if (!target) return void 0;
4047
+ if (!opts.followOutside) {
4048
+ let real = target;
4049
+ try {
4050
+ real = realpathSync2(target);
4051
+ } catch {
4052
+ }
4053
+ if (!isInside(real, opts.boundary)) return void 0;
4054
+ }
4055
+ return target;
4056
+ };
4057
+ return {
4058
+ resolveLocal: resolveInProject,
4059
+ importedSinks(fromFile, specifier, exportName) {
4060
+ const target = resolveInProject(fromFile, specifier);
4061
+ if (!target) return [];
4062
+ const mod = load(target);
4063
+ if (!mod) return [];
4064
+ const collected = [...mod.fnSinks.get(exportName) ?? []];
4065
+ for (const callee of mod.calleesOf.get(exportName) ?? []) {
4066
+ for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
4067
+ }
4068
+ const rel = relative3(opts.cwd, target);
4069
+ return collected.map((s) => ({ ...s, file: rel }));
4070
+ },
4071
+ // A different question about the same module: not "what sinks are in there" but "what does this
4072
+ // export TRACE TO". The common AI-built layout puts the client in a lib file —
4073
+ // `export const db = createClient(...)` in `lib/db.ts`, imported everywhere — so the receiver of
4074
+ // `db.from('orders').insert(...)` resolves to a relative specifier and nothing else. Refusing it (as
4075
+ // an unattributable receiver) is right for app code but wrong here: one hop away it is a real
4076
+ // dependency, and that chain is import-to-import, fully static — evidence, not inference.
4077
+ importedBinding(fromFile, specifier, exportName) {
4078
+ const target = resolveInProject(fromFile, specifier);
4079
+ if (!target) return void 0;
4080
+ const mod = load(target);
4081
+ if (!mod) return void 0;
4082
+ const pkg = npmPackageOf(mod.bindings.resolve(exportName));
4083
+ if (pkg === void 0) return void 0;
4084
+ return {
4085
+ package: pkg,
4086
+ origin: mod.bindings.originOf(exportName) ?? "direct",
4087
+ name: mod.bindings.exportNameOf(exportName) ?? exportName
4088
+ };
4089
+ }
4090
+ };
4091
+ }
4092
+ function resolveRelativeModule(fromFile, spec) {
4093
+ if (!spec.startsWith(".")) return void 0;
4094
+ const dir = dirname4(fromFile);
4095
+ const base = resolvePath(dir, spec);
4096
+ const candidates = [];
4097
+ const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
4098
+ if (jsLike) {
4099
+ const stem = base.slice(0, -jsLike[0].length);
4100
+ for (const e of RESOLVE_EXTS) candidates.push(stem + e);
4101
+ }
4102
+ candidates.push(base);
4103
+ for (const e of RESOLVE_EXTS) candidates.push(base + e);
4104
+ for (const e of RESOLVE_EXTS) candidates.push(join14(base, "index" + e));
4105
+ for (const c of candidates) {
4106
+ try {
4107
+ if (statSync4(c).isFile()) return c;
4108
+ } catch {
4109
+ }
4110
+ }
4111
+ return void 0;
4112
+ }
4113
+ function collectCallees(sf, ts) {
4114
+ const map = /* @__PURE__ */ new Map();
4115
+ const visit = (node) => {
4116
+ let name;
4117
+ let body;
4118
+ if (ts.isFunctionDeclaration(node) && node.name && node.body) {
4119
+ name = node.name.text;
4120
+ body = node.body;
4121
+ } else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isFnLike(node.initializer, ts)) {
4122
+ name = node.name.text;
4123
+ body = node.initializer.body;
4124
+ }
4125
+ if (name && body) map.set(name, localCalls(body, ts));
4126
+ ts.forEachChild(node, visit);
4127
+ };
4128
+ visit(sf);
4129
+ return map;
4130
+ }
4131
+
4132
+ // src/map/coordinates.ts
4133
+ function runtimeCoordinate(source, path11) {
4134
+ if (/\[\d*\]/.test(path11)) {
4135
+ return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
4136
+ }
4137
+ switch (source) {
4138
+ case "json-body":
4139
+ case "form-body":
4140
+ case "multipart":
4141
+ case "body":
4142
+ case "server-fn-data":
4143
+ return { runtimeParameter: `post.${path11}` };
4144
+ case "query":
4145
+ return { runtimeParameter: `get.${path11}` };
4146
+ case "cookie":
4147
+ return { runtimeParameter: `cookie.${path11}` };
4148
+ case "file":
4149
+ return { runtimeParameter: `files.${path11}` };
4150
+ case "header":
4151
+ return { runtimeParameter: `server.HTTP_${path11.toUpperCase().replace(/-/g, "_")}` };
4152
+ case "route-param":
4153
+ return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
4154
+ default:
4155
+ return { runtimeParameter: null, runtimeParameterReason: "input source could not be determined" };
4156
+ }
4157
+ }
4158
+ function addressSpaceOf(source) {
4159
+ switch (source) {
4160
+ case "json-body":
4161
+ case "form-body":
4162
+ case "multipart":
4163
+ case "body":
4164
+ case "server-fn-data":
4165
+ return "post";
4166
+ case "query":
4167
+ return "get";
4168
+ case "cookie":
4169
+ return "cookie";
4170
+ case "file":
4171
+ return "files";
4172
+ case "header":
4173
+ return "server";
4174
+ case "route-param":
4175
+ return "route-param";
4176
+ default:
4177
+ return "unknown";
4178
+ }
4179
+ }
4180
+ function isProvenFlow(confidence) {
4181
+ return confidence === "exact-local" || confidence === "transformed-local";
4182
+ }
4183
+ function inputIdOf(source, path11) {
4184
+ return `${addressSpaceOf(source)}:${path11}`;
4185
+ }
4186
+ function withCoordinates(fields, source) {
4187
+ return fields.map((f) => {
4188
+ const src = f.source ?? source;
4189
+ return { ...f, source: src, id: inputIdOf(src, f.name), ...runtimeCoordinate(src, f.name) };
4190
+ });
4191
+ }
4192
+
4193
+ // src/map/inputs.ts
4194
+ var ZOD_BASE = /* @__PURE__ */ new Set(["string", "number", "boolean", "array", "object", "enum", "bigint", "date", "record"]);
4195
+ var STRING_FORMATS = /* @__PURE__ */ new Set(["email", "uuid", "url", "ip", "ipv4", "ipv6", "cuid", "cuid2", "ulid", "emoji", "datetime", "base64", "jwt", "nanoid"]);
4196
+ var VALIDATOR_PACKAGES = /* @__PURE__ */ new Set(["zod", "valibot", "yup", "joi", "@hapi/joi", "superstruct"]);
4197
+ function inputsFromValidator(validatorCall, ts, bindings) {
4198
+ if (!validatorCall) return [];
4199
+ return zodObjectFields(validatorCall, ts, bindings);
4200
+ }
4201
+ function inputsFromHandler(params, body, ts, bindings, opts = {}) {
4202
+ const schemaSource = opts.validatorSource ?? "json-body";
4203
+ const schemaFields = zodObjectFields(body, ts, bindings);
4204
+ const reads = requestMemberAccesses(params, body, ts, opts);
4205
+ const byId = /* @__PURE__ */ new Map();
4206
+ const put = (name, source, extra = {}) => {
4207
+ const id = inputIdOf(source, name);
4208
+ if (byId.has(id)) return;
4209
+ byId.set(id, { ...extra, id, name, source, ...runtimeCoordinate(source, name) });
4210
+ };
4211
+ for (const f of schemaFields) {
4212
+ const { name, source, ...shape } = f;
4213
+ put(name, source ?? schemaSource, shape);
4214
+ }
4215
+ for (const { name, sources } of reads) for (const source of sources) put(name, source);
4216
+ return [...byId.values()];
4217
+ }
4218
+ function findValidatorObject(node, ts, bindings) {
4219
+ let found = null;
4220
+ const find = (n) => {
4221
+ if (found || !n) return;
4222
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "object") {
4223
+ const root = rootIdentifier(n.expression.expression, ts);
4224
+ const pkg = root ? npmPackageOf(bindings.resolve(root)) : void 0;
4225
+ const isValidator = pkg && VALIDATOR_PACKAGES.has(pkg) || !pkg && root === "z" && !bindings.locals.has(root);
4226
+ if (isValidator) {
4227
+ const arg = n.arguments[0];
4228
+ if (arg && ts.isObjectLiteralExpression(arg)) {
4229
+ found = arg;
4230
+ return;
4231
+ }
4232
+ }
4233
+ }
4234
+ ts.forEachChild(n, find);
4235
+ };
4236
+ find(node);
4237
+ return found;
4238
+ }
4239
+ function zodObjectFields(node, ts, bindings) {
4240
+ if (!node) return [];
4241
+ const lit = findValidatorObject(node.body ?? node, ts, bindings);
4242
+ return lit ? fieldsOfObject(lit, ts, bindings, "") : [];
4243
+ }
4244
+ function fieldsOfObject(objectLiteral, ts, bindings, prefix) {
4245
+ const fields = [];
4246
+ for (const p of objectLiteral.properties) {
4247
+ if (!ts.isPropertyAssignment(p) || !p.name) continue;
4248
+ const fname = p.name.text;
4249
+ if (!fname) continue;
4250
+ const shape = zodShape(p.initializer, ts);
4251
+ fields.push({ name: prefix + fname, ...shape });
4252
+ const nested = findValidatorObject(p.initializer, ts, bindings);
4253
+ if (nested) fields.push(...fieldsOfObject(nested, ts, bindings, prefix + fname + (shape.type === "array" ? "[]." : ".")));
4254
+ }
4255
+ return fields;
4256
+ }
4257
+ function numericValue(arg, ts) {
4258
+ if (!arg) return void 0;
4259
+ if (ts.isNumericLiteral(arg)) return Number(arg.text);
4260
+ if (ts.isPrefixUnaryExpression(arg) && arg.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(arg.operand)) return -Number(arg.operand.text);
4261
+ return void 0;
4262
+ }
4263
+ function zodShape(node, ts) {
4264
+ const shape = {};
4265
+ let cur = node;
4266
+ while (cur && ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) {
4267
+ const method = cur.expression.name.text;
4268
+ const arg0 = cur.arguments[0];
4269
+ if (ZOD_BASE.has(method) && !shape.type) shape.type = method;
4270
+ if (method === "min") {
4271
+ const v = numericValue(arg0, ts);
4272
+ if (v !== void 0) shape.min = v;
4273
+ }
4274
+ if (method === "max") {
4275
+ const v = numericValue(arg0, ts);
4276
+ if (v !== void 0) shape.max = v;
4277
+ }
4278
+ if (STRING_FORMATS.has(method) && !shape.format) shape.format = method;
4279
+ if (method === "regex" && arg0 && ts.isRegularExpressionLiteral(arg0) && !shape.pattern) shape.pattern = arg0.text;
4280
+ if (method === "optional" || method === "nullish") shape.optional = true;
4281
+ cur = cur.expression.expression;
4282
+ }
4283
+ return shape;
4284
+ }
4285
+ var REQ_SOURCES = ["body", "query", "params"];
4286
+ function requestMemberAccesses(params, body, ts, opts = {}) {
4287
+ if (!body) return [];
4288
+ const out = /* @__PURE__ */ new Map();
4289
+ const record = (name, source) => {
4290
+ const list = out.get(name) ?? [];
4291
+ if (!list.includes(source)) list.push(source);
4292
+ out.set(name, list);
4293
+ };
4294
+ const p0 = params?.[0];
4295
+ const reqName = p0 && ts.isIdentifier(p0.name) ? p0.name.text : void 0;
4296
+ const sourceNames = /* @__PURE__ */ new Map();
4297
+ const payloadNames = /* @__PURE__ */ new Set();
4298
+ if (opts.payloadParam && p0 && ts.isIdentifier(p0.name)) payloadNames.add(p0.name.text);
4299
+ if (p0 && !reqName && ts.isObjectBindingPattern(p0.name)) {
4300
+ for (const el of p0.name.elements) {
4301
+ const key = bindingKey(el, ts);
4302
+ if (key && REQ_SOURCES.includes(key) && ts.isIdentifier(el.name)) {
4303
+ sourceNames.set(el.name.text, namespaceSource(key));
4304
+ }
4305
+ }
4306
+ }
4307
+ const unwrap = (e) => {
4308
+ let cur = e;
4309
+ while (cur && (ts.isAwaitExpression(cur) || ts.isAsExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
4310
+ return cur;
4311
+ };
4312
+ const isPayloadExpr = (e) => ts.isIdentifier(e) && payloadNames.has(e.text);
4313
+ 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);
4314
+ const isBodyReadCall = (e) => {
4315
+ const inner = unwrap(e);
4316
+ 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);
4317
+ };
4318
+ const visit = (n) => {
4319
+ if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
4320
+ record(n.name.text, sourceOfExpr(n.expression));
4321
+ }
4322
+ if (ts.isVariableDeclaration(n) && n.initializer) {
4323
+ const init = unwrap(n.initializer);
4324
+ if (ts.isIdentifier(n.name) && isBodyReadCall(n.initializer)) sourceNames.set(n.name.text, bodyReadSource(n.initializer));
4325
+ if (ts.isObjectBindingPattern(n.name) && ts.isIdentifier(init) && reqName && init.text === reqName) {
4326
+ for (const el of n.name.elements) {
4327
+ const key = bindingKey(el, ts);
4328
+ if (key && REQ_SOURCES.includes(key) && ts.isIdentifier(el.name)) sourceNames.set(el.name.text, namespaceSource(key));
4329
+ }
4330
+ }
4331
+ if (ts.isObjectBindingPattern(n.name) && (isReqSourceExpr(init) || isBodyReadCall(n.initializer))) {
4332
+ const src = isBodyReadCall(n.initializer) ? bodyReadSource(n.initializer) : sourceOfExpr(init);
4333
+ for (const el of n.name.elements) {
4334
+ const key = bindingKey(el, ts);
4335
+ if (key) record(key, src);
4336
+ }
4337
+ }
4338
+ }
4339
+ ts.forEachChild(n, visit);
4340
+ };
4341
+ visit(body);
4342
+ return [...out].map(([name, sources]) => ({ name, sources }));
4343
+ function sourceOfExpr(e) {
4344
+ if (isPayloadExpr(e)) return "server-fn-data";
4345
+ if (ts.isPropertyAccessExpression(e)) {
4346
+ if (e.name.text === "query") return "query";
4347
+ if (e.name.text === "params") return "route-param";
4348
+ if (e.name.text === "body") return "body";
4349
+ }
4350
+ if (ts.isIdentifier(e)) {
4351
+ const recorded = sourceNames.get(e.text);
4352
+ if (recorded) return recorded;
4353
+ }
4354
+ return "body";
4355
+ }
4356
+ function namespaceSource(key) {
4357
+ if (key === "query") return "query";
4358
+ if (key === "params") return "route-param";
4359
+ return "body";
4360
+ }
4361
+ function bodyReadSource(init) {
4362
+ const t = init?.getText?.() ?? "";
4363
+ return /formData\s*\(/.test(t) ? "form-body" : "json-body";
4364
+ }
4365
+ }
4366
+
4367
+ // src/map/flows.ts
4368
+ function spaceOfKey(key) {
4369
+ if (key === "query") return "get";
4370
+ if (key === "params") return "route-param";
4371
+ if (key === "body") return "post";
4372
+ if (key === "data" || key === "input" || key === "payload") return "post";
4373
+ return void 0;
4374
+ }
4375
+ function linkedFlows(body, params, inputs, sinks, ts) {
4376
+ const { flows, limitations } = linkFlows(body, params, inputs, sinks, ts);
4377
+ return limitations.length > 0 ? { flows, limitations } : { flows };
4378
+ }
4379
+ function linkFlows(bodyNode, params, inputs, sinks, ts) {
4380
+ if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
4381
+ const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
4382
+ const rootPath = /* @__PURE__ */ new Map();
4383
+ const addRoot = (name, path11, space) => {
4384
+ if (!rootPath.has(name)) rootPath.set(name, { path: path11, space });
4385
+ };
4386
+ for (const p of params ?? []) {
4387
+ if (!p?.name) continue;
4388
+ if (ts.isIdentifier(p.name)) addRoot(p.name.text, "");
4389
+ else if (ts.isObjectBindingPattern(p.name)) {
4390
+ for (const el of p.name.elements) {
4391
+ if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
4392
+ const key = bindingKey(el, ts);
4393
+ const container = key !== void 0 && CONTAINER_KEYS.has(key);
4394
+ addRoot(el.name.text, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0);
4395
+ }
4396
+ }
4397
+ }
4398
+ const requestReadPath = (init) => {
4399
+ let cur = init;
4400
+ while (cur && (ts.isAwaitExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAsExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
4401
+ if (!cur) return void 0;
4402
+ if (ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) {
4403
+ const m = cur.expression.name.text;
4404
+ if (["json", "formData", "text"].includes(m)) {
4405
+ const root = rootIdentifier(cur.expression.expression, ts);
4406
+ return root && rootPath.has(root) ? { path: "", space: "post" } : void 0;
4407
+ }
4408
+ if (["parse", "safeParse", "validate", "cast"].includes(m)) {
4409
+ for (const a of cur.arguments) {
4410
+ const inner = requestReadPath(a);
4411
+ if (inner !== void 0) return inner;
4412
+ }
4413
+ return void 0;
4414
+ }
4415
+ }
4416
+ const p = pathFromTainted(cur, ts, rootPath);
4417
+ return p;
4418
+ };
4419
+ const aliasVisit = (n) => {
4420
+ if (ts.isVariableDeclaration(n) && n.initializer) {
4421
+ const base = requestReadPath(n.initializer);
4422
+ if (base !== void 0) {
4423
+ if (ts.isIdentifier(n.name)) addRoot(n.name.text, base.path, base.space);
4424
+ else if (ts.isObjectBindingPattern(n.name)) {
4425
+ for (const el of n.name.elements) {
4426
+ if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
4427
+ const key = bindingKey(el, ts);
4428
+ const space = base.space ?? (base.path === "" ? spaceOfKey(key) : void 0);
4429
+ const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key);
4430
+ addRoot(el.name.text, container ? "" : join22(base.path, key ?? el.name.text), space);
4431
+ }
4432
+ }
4433
+ }
4434
+ }
4435
+ ts.forEachChild(n, aliasVisit);
4436
+ };
4437
+ aliasVisit(bodyNode);
4438
+ const callBySpan = /* @__PURE__ */ new Map();
4439
+ const callVisit = (n) => {
4440
+ if (ts.isCallExpression(n) || ts.isNewExpression(n)) {
4441
+ try {
4442
+ callBySpan.set(`${n.getStart()}:${n.getEnd()}`, n);
4443
+ } catch {
4444
+ }
4445
+ }
4446
+ ts.forEachChild(n, callVisit);
4447
+ };
4448
+ callVisit(bodyNode);
4449
+ const flows = [];
4450
+ const allLimits = [];
4451
+ for (const sink of sinks) {
4452
+ const node = sink.file === void 0 && sink.start !== void 0 && sink.end !== void 0 ? callBySpan.get(`${sink.start}:${sink.end}`) : void 0;
4453
+ const reads = /* @__PURE__ */ new Map();
4454
+ const sinkLimits = [];
4455
+ if (node) {
4456
+ for (const call of fluentChainCalls(node, ts)) {
4457
+ const method = calleeName(call, ts);
4458
+ const args = call.arguments ?? [];
4459
+ for (const a of args) for (const l of sinkArgumentLimitations(a, ts, rootPath)) sinkLimits.push(l);
4460
+ for (let i = 0; i < args.length; i++) {
4461
+ const role = argumentRoleOf(sink.kind, method, i, args.length);
4462
+ const whole = pathFromTainted(args[i], ts, rootPath);
4463
+ for (const read2 of taintedReadPaths(args[i], ts, rootPath)) {
4464
+ const key = `${read2.space ?? "*"}:${read2.path}`;
4465
+ const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space;
4466
+ const entry = reads.get(key) ?? { read: read2, roles: /* @__PURE__ */ new Set(), exact: false };
4467
+ entry.roles.add(role);
4468
+ entry.exact = entry.exact || exact;
4469
+ reads.set(key, entry);
4470
+ }
4471
+ }
4472
+ }
4473
+ }
4474
+ for (const input of inputs) {
4475
+ const inputPath = normalizePath(input.name);
4476
+ const inputSpace = addressSpaceOf(input.source);
4477
+ const matched = [...reads.values()].filter(({ read: read2 }) => (read2.space === void 0 || read2.space === inputSpace) && (read2.path === inputPath || read2.path.startsWith(inputPath + ".")));
4478
+ const proven = matched.length > 0;
4479
+ const confidence = proven ? matched.some((m) => m.exact) ? "exact-local" : "transformed-local" : sink.file !== void 0 ? "imported" : sink.start === void 0 ? "unknown" : "heuristic";
4480
+ const roles = new Set(matched.flatMap(({ roles: rs }) => [...rs]));
4481
+ const family = sink.apiUnconfirmed ? void 0 : [...roles].map((r) => CANDIDATE_FAMILIES[sink.kind]?.[r]).find(Boolean);
4482
+ const argumentRole = family ? [...roles].find((r) => CANDIDATE_FAMILIES[sink.kind]?.[r]) : [...roles].find((r) => r !== "unknown") ?? (proven ? "unknown" : void 0);
4483
+ const reasons = [];
4484
+ if (!proven) reasons.push(`flow evidence is "${confidence}": no proven local read of this input into the sink call`);
4485
+ if (!input.runtimeParameter) reasons.push(input.runtimeParameterReason ?? "input has no runtime parameter");
4486
+ if (sink.file !== void 0) reasons.push("sink is in an imported module: no local call-site evidence");
4487
+ if (sink.start === void 0) reasons.push("sink call could not be located in the source");
4488
+ if (sink.apiUnconfirmed) {
4489
+ reasons.push(`sink package "${sink.package}" is not a known ${sink.kind} provider: it does not establish a ${sink.kind} API (method name alone is not evidence)`);
4490
+ }
4491
+ if (sink.attribution !== "import" && sink.attribution !== "global") {
4492
+ reasons.push(sink.attribution === "inferred" ? `sink package "${sink.package}" was inferred from the file's other imports, not from the receiver (${sink.kind}.${sink.op ?? "?"}): the receiver may be any app object` : `sink receiver could not be traced to a dependency (${sink.kind}.${sink.op ?? "?"} on an unresolved receiver): a rule here would be a guess`);
4493
+ }
4494
+ if (proven && argumentRole === "unknown") reasons.push(`sink argument role is not modelled for ${sink.kind}.${sink.op ?? "?"}`);
4495
+ for (const l of sinkLimits) {
4496
+ reasons.push(l.kind === "dynamic-key" ? `dynamic computed key reaches this sink (${l.detail}): the field cannot be named by a parameter` : `spread reaches this sink (${l.detail}): the specific field is not identifiable`);
4497
+ }
4498
+ if (proven && argumentRole && argumentRole !== "unknown" && !family && !sink.apiUnconfirmed) {
4499
+ reasons.push(`argument role "${argumentRole}" on a ${sink.kind} sink is not a blockable pattern on its own`);
4500
+ }
4501
+ flows.push({
4502
+ input: input.name,
4503
+ inputId: input.id,
4504
+ sink,
4505
+ confidence,
4506
+ line: sink.line,
4507
+ ...argumentRole ? { argumentRole } : {},
4508
+ ...family ? { candidateFamily: family } : {},
4509
+ ruleGeneratable: reasons.length === 0,
4510
+ ruleGeneratableReasons: reasons
4511
+ });
4512
+ }
4513
+ for (const l of sinkLimits) allLimits.push(l);
4514
+ }
4515
+ return { flows, limitations: dedupeLimitations(allLimits) };
4516
+ }
4517
+ function dedupeLimitations(list) {
4518
+ const seen = /* @__PURE__ */ new Set();
4519
+ return list.filter((l) => {
4520
+ const k = `${l.kind}:${l.detail}:${l.line}`;
4521
+ if (seen.has(k)) return false;
4522
+ seen.add(k);
4523
+ return true;
4524
+ });
4525
+ }
4526
+ function join22(base, seg) {
4527
+ return base ? `${base}.${seg}` : seg;
4528
+ }
4529
+ function normalizePath(path11) {
4530
+ return path11.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
4531
+ }
4532
+ function fluentChainCalls(call, ts) {
4533
+ let root = call;
4534
+ for (; ; ) {
4535
+ const p = root.parent;
4536
+ if (p && ts.isPropertyAccessExpression(p) && p.expression === root) {
4537
+ root = p;
4538
+ continue;
4539
+ }
4540
+ if (p && ts.isCallExpression(p) && p.expression === root) {
4541
+ root = p;
4542
+ continue;
4543
+ }
4544
+ if (p && (ts.isAwaitExpression(p) || ts.isParenthesizedExpression(p) || ts.isNonNullExpression(p)) && p.expression === root) {
4545
+ root = p;
4546
+ continue;
4547
+ }
4548
+ break;
4549
+ }
4550
+ const out = [];
4551
+ const collect = (n) => {
4552
+ if (!n) return;
4553
+ if (ts.isCallExpression(n) || ts.isNewExpression(n)) out.push(n);
4554
+ if (ts.isCallExpression(n) || ts.isPropertyAccessExpression(n) || ts.isAwaitExpression(n) || ts.isParenthesizedExpression(n) || ts.isNonNullExpression(n)) {
4555
+ collect(n.expression);
4556
+ }
4557
+ };
4558
+ collect(root);
4559
+ return out;
4560
+ }
4561
+ function taintedReadPaths(node, ts, rootPath) {
4562
+ const out = [];
4563
+ const seen = /* @__PURE__ */ new Set();
4564
+ const add = (r) => {
4565
+ const key = `${r.space ?? "*"}:${r.path}`;
4566
+ if (!seen.has(key)) {
4567
+ seen.add(key);
4568
+ out.push(r);
4569
+ }
4570
+ };
4571
+ const visit = (n) => {
4572
+ if (!n) return;
4573
+ if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
4574
+ const read2 = pathFromTainted(n, ts, rootPath);
4575
+ if (read2 !== void 0) {
4576
+ add(read2);
4577
+ return;
4578
+ }
4579
+ }
4580
+ if (ts.isIdentifier(n) && rootPath.has(n.text) && isValueRead(n, ts)) {
4581
+ const r = rootPath.get(n.text);
4582
+ if (r.path) add({ path: normalizePath(r.path), space: r.space });
4583
+ }
4584
+ ts.forEachChild(n, visit);
4585
+ };
4586
+ visit(node);
4587
+ return out;
4588
+ }
4589
+ function sinkArgumentLimitations(node, ts, rootPath) {
4590
+ const out = [];
4591
+ const seen = /* @__PURE__ */ new Set();
4592
+ const add = (kind, detail, n) => {
4593
+ const key = `${kind}:${detail}`;
4594
+ if (seen.has(key)) return;
4595
+ seen.add(key);
4596
+ out.push({ kind, detail, line: lineOf(n) });
4597
+ };
4598
+ const text = (n) => {
4599
+ try {
4600
+ return String(n.getText()).replace(/\s+/g, " ").slice(0, 120);
4601
+ } catch {
4602
+ return "<expression>";
4603
+ }
4604
+ };
4605
+ const visit = (n) => {
4606
+ if (!n) return;
4607
+ if (ts.isElementAccessExpression(n)) {
4608
+ const root = rootIdentifier(n.expression, ts);
4609
+ const arg = n.argumentExpression;
4610
+ if (root && rootPath.has(root) && arg && !ts.isStringLiteralLike(arg) && !ts.isNumericLiteral(arg)) {
4611
+ add("dynamic-key", text(n), n);
4612
+ }
4613
+ }
4614
+ if ((ts.isSpreadAssignment?.(n) || ts.isSpreadElement(n)) && n.expression) {
4615
+ const root = rootIdentifier(n.expression, ts);
4616
+ if (root && rootPath.has(root)) add("spread-into-sink", text(n.parent ?? n), n);
4617
+ }
4618
+ ts.forEachChild(n, visit);
4619
+ };
4620
+ visit(node);
4621
+ return out;
4622
+ }
4623
+ function pathFromTainted(node, ts, rootPath) {
4624
+ const segs = [];
4625
+ let cur = node;
4626
+ for (; ; ) {
4627
+ if (ts.isPropertyAccessExpression(cur)) {
4628
+ segs.unshift(cur.name.text);
4629
+ cur = cur.expression;
4630
+ continue;
4631
+ }
4632
+ if (ts.isElementAccessExpression(cur)) {
4633
+ const a = cur.argumentExpression;
4634
+ segs.unshift(a && ts.isStringLiteralLike(a) ? a.text : "[]");
4635
+ cur = cur.expression;
4636
+ continue;
4637
+ }
4638
+ if (ts.isNonNullExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAwaitExpression(cur)) {
4639
+ cur = cur.expression;
4640
+ continue;
4641
+ }
4642
+ break;
4643
+ }
4644
+ if (!cur || !ts.isIdentifier(cur)) return void 0;
4645
+ const base = rootPath.get(cur.text);
4646
+ if (base === void 0) return void 0;
4647
+ let space = base.space;
4648
+ if (base.path === "" && segs.length > 1 && REQ_SOURCES.includes(segs[0])) {
4649
+ space = spaceOfKey(segs[0]) ?? space;
4650
+ segs.shift();
4651
+ }
4652
+ return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space };
4653
+ }
4654
+
4655
+ // src/map/entries.ts
4656
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
4657
+ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
4658
+ const out = [];
4659
+ const isServerActionsFile = fileHasUseServer(sf, ts);
4660
+ const visit = (node) => {
4661
+ if (ts.isVariableStatement(node) && hasExport(node, ts)) {
4662
+ for (const decl of node.declarationList.declarations) {
4663
+ if (decl.initializer && ts.isCallExpression(decl.initializer)) {
4664
+ const chain = unwindChain(decl.initializer, ts);
4665
+ if (chain.baseName === "createServerFn" && ts.isIdentifier(decl.name)) {
4666
+ const validatorCall = chain.calls["inputValidator"] ?? chain.calls["validator"];
4667
+ const inputs = withCoordinates(inputsFromValidator(validatorCall, ts, bindings), "server-fn-data");
4668
+ const handlerFn = chain.calls["handler"]?.arguments?.[0];
4669
+ const sinks = sinksFrom(handlerFn, ts, localSinks, bindings, ctx);
4670
+ const handlerBody = handlerFn && isFnLike(handlerFn, ts) ? handlerFn.body : void 0;
4671
+ const ep = {
4672
+ name: decl.name.text,
4673
+ entryKind: "server-fn",
4674
+ method: methodFromObjectArg(chain.baseCall, ts),
4675
+ ...spanOf(decl),
4676
+ inputs,
4677
+ sinks,
4678
+ ...linkedFlows(handlerBody, handlerFn?.parameters, inputs, sinks, ts)
4679
+ };
4680
+ if (validatorCall && inputs.length === 0) ep.inputsResolved = false;
4681
+ out.push(ep);
4682
+ continue;
4683
+ }
4684
+ }
4685
+ if (ts.isIdentifier(decl.name) && decl.initializer && isFnLike(decl.initializer, ts)) {
4686
+ if (HTTP_METHODS.has(decl.name.text)) {
4687
+ out.push(handlerEntry(decl.name.text, decl.name.text, decl.initializer.parameters, decl.initializer.body, ts, localSinks, bindings, ctx, spanOf(decl)));
4688
+ } else if (isServerActionsFile) {
4689
+ out.push(handlerEntry(decl.name.text, "server-action", decl.initializer.parameters, decl.initializer.body, ts, localSinks, bindings, ctx, spanOf(decl)));
4690
+ }
4691
+ }
4692
+ }
4693
+ }
4694
+ if (ts.isFunctionDeclaration(node) && node.name && hasExport(node, ts)) {
4695
+ if (HTTP_METHODS.has(node.name.text)) {
4696
+ out.push(handlerEntry(node.name.text, node.name.text, node.parameters, node.body, ts, localSinks, bindings, ctx, spanOf(node)));
4697
+ } else if (isServerActionsFile || hasUseServerDirective(node, ts)) {
4698
+ out.push(handlerEntry(node.name.text, "server-action", node.parameters, node.body, ts, localSinks, bindings, ctx, spanOf(node)));
4699
+ }
4700
+ }
4701
+ if (ts.isCallExpression(node)) {
4702
+ const c = node.expression;
4703
+ const denoServe = ts.isPropertyAccessExpression(c) && c.name.text === "serve" && ts.isIdentifier(c.expression) && c.expression.text === "Deno";
4704
+ const bareServe = ts.isIdentifier(c) && c.text === "serve";
4705
+ if (denoServe || bareServe) {
4706
+ const handler = node.arguments.find((a) => isFnLike(a, ts));
4707
+ if (handler) {
4708
+ out.push(handlerEntry(functionNameFromPath(ctx.file) ?? "serve", "edge-function", handler.parameters, handler.body, ts, localSinks, bindings, ctx, spanOf(node)));
4709
+ }
4710
+ }
4711
+ }
4712
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
4713
+ const mname = node.expression.name.text;
4714
+ if (ROUTE_REGISTER.has(mname)) {
4715
+ const args = node.arguments;
4716
+ const first = args[0];
4717
+ const route = first && ts.isStringLiteralLike(first) ? first.text : routeFromChain(node.expression.expression, ts);
4718
+ const handler = args[args.length - 1];
4719
+ if (route !== void 0 && handler && isFnLike(handler, ts)) {
4720
+ out.push(handlerEntry(route, "route-registration", handler.parameters, handler.body, ts, localSinks, bindings, ctx, {
4721
+ // `use`/`all` register handlers but are not HTTP methods — leave method undefined.
4722
+ method: HTTP_METHODS.has(mname.toUpperCase()) ? mname.toUpperCase() : void 0,
4723
+ route,
4724
+ ...spanOf(node)
4725
+ }));
4726
+ }
4727
+ }
4728
+ if (mname === "route") {
4729
+ const arg = node.arguments[0];
4730
+ if (arg && ts.isObjectLiteralExpression(arg)) {
4731
+ const reg = routeObject(arg, ts);
4732
+ if (reg.url && reg.handler) {
4733
+ for (const m of reg.methods.length ? reg.methods : [void 0]) {
4734
+ out.push(handlerEntry(reg.url, "route-registration", reg.handler.parameters, reg.handler.body, ts, localSinks, bindings, ctx, { method: m, route: reg.url, ...spanOf(node) }));
4735
+ }
4736
+ }
4737
+ }
4738
+ }
4739
+ }
4740
+ ts.forEachChild(node, visit);
4741
+ };
4742
+ visit(sf);
4743
+ return out;
4744
+ }
4745
+ function fileHasUseServer(sf, ts) {
4746
+ const first = sf.statements?.[0];
4747
+ return Boolean(first && ts.isExpressionStatement(first) && ts.isStringLiteralLike(first.expression) && first.expression.text === "use server");
4748
+ }
4749
+ function hasUseServerDirective(fn, ts) {
4750
+ const first = fn.body?.statements?.[0];
4751
+ return Boolean(first && ts.isExpressionStatement(first) && ts.isStringLiteralLike(first.expression) && first.expression.text === "use server");
4752
+ }
4753
+ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, ctx, extra = {}) {
4754
+ const entryKind = kindLabel === "route-registration" || kindLabel === "server-action" || kindLabel === "edge-function" ? kindLabel : "route-handler";
4755
+ const payloadStyle = kindLabel === "server-action";
4756
+ const inputs = inputsFromHandler(params, body, ts, bindings, {
4757
+ payloadParam: payloadStyle,
4758
+ validatorSource: payloadStyle ? "server-fn-data" : "json-body"
4759
+ });
4760
+ const sinks = sinksFrom({ body, parameters: params, isSyntheticBody: true }, ts, localSinks, bindings, ctx);
4761
+ return {
4762
+ name,
4763
+ entryKind,
4764
+ method: extra.method ?? (HTTP_METHODS.has(name) ? name : void 0),
4765
+ route: extra.route,
4766
+ line: extra.line,
4767
+ start: extra.start,
4768
+ end: extra.end,
4769
+ inputs,
4770
+ sinks,
4771
+ ...linkedFlows(body, params, inputs, sinks, ts)
4772
+ };
4773
+ }
4774
+
4775
+ // src/map/imports.ts
4776
+ import { readFileSync as readFileSync9 } from "fs";
4777
+ import { join as join15 } from "path";
4778
+ var MAX_SITES = 5;
4779
+ var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
4780
+ function isNpmPackageName(pkg) {
4781
+ return pkg.startsWith("node:") || NPM_NAME.test(pkg);
4782
+ }
4783
+ function readPathAliases(cwd) {
4784
+ const aliases = [];
4785
+ for (const name of ["tsconfig.json", "jsconfig.json"]) {
4786
+ let paths;
4787
+ try {
4788
+ const raw = readFileSync9(join15(cwd, name), "utf8");
4789
+ const parsed = JSON.parse(stripJsonComments(raw));
4790
+ paths = parsed.compilerOptions?.paths;
4791
+ } catch {
4792
+ continue;
4793
+ }
4794
+ for (const key of Object.keys(paths ?? {})) {
4795
+ if (key.startsWith(".")) continue;
4796
+ if (key.endsWith("*")) aliases.push({ prefix: key.slice(0, -1), wildcard: true });
4797
+ else aliases.push({ prefix: key, wildcard: false });
4798
+ }
4799
+ }
4800
+ return aliases;
4801
+ }
4802
+ function stripJsonComments(text) {
4803
+ let out = "";
4804
+ let inString = false;
4805
+ let quote = "";
4806
+ for (let i = 0; i < text.length; i++) {
4807
+ const c = text[i];
4808
+ const next = text[i + 1];
4809
+ if (inString) {
4810
+ out += c;
4811
+ if (c === "\\") {
4812
+ out += next ?? "";
4813
+ i++;
4814
+ continue;
4815
+ }
4816
+ if (c === quote) inString = false;
4817
+ continue;
4818
+ }
4819
+ if (c === '"' || c === "'") {
4820
+ inString = true;
4821
+ quote = c;
4822
+ out += c;
4823
+ continue;
4824
+ }
4825
+ if (c === "/" && next === "/") {
4826
+ while (i < text.length && text[i] !== "\n") i++;
4827
+ out += "\n";
4828
+ continue;
4829
+ }
4830
+ if (c === "/" && next === "*") {
4831
+ i += 2;
4832
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
4833
+ i++;
4834
+ continue;
4835
+ }
4836
+ out += c;
4837
+ }
4838
+ return out.replace(/,(\s*[}\]])/g, "$1");
4839
+ }
4840
+ function collectFileImports(sf, ts) {
4841
+ const found = [];
4842
+ const lineOf2 = (node) => {
4843
+ try {
4844
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
4845
+ } catch {
4846
+ return void 0;
4847
+ }
4848
+ };
4849
+ const visit = (node) => {
4850
+ if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
4851
+ const names = [];
4852
+ const clause = node.importClause;
4853
+ if (clause?.name) names.push("default");
4854
+ const nb = clause?.namedBindings;
4855
+ if (nb) {
4856
+ if (ts.isNamespaceImport(nb)) names.push("*");
4857
+ else if (ts.isNamedImports(nb)) {
4858
+ for (const el of nb.elements) names.push((el.propertyName ?? el.name).text);
4859
+ }
4860
+ }
4861
+ found.push({ specifier: node.moduleSpecifier.text, names, line: lineOf2(node) });
4862
+ }
4863
+ if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
4864
+ const names = [];
4865
+ const ec = node.exportClause;
4866
+ if (ec && ts.isNamedExports(ec)) for (const el of ec.elements) names.push((el.propertyName ?? el.name).text);
4867
+ else if (ec && ts.isNamespaceExport(ec)) names.push("*");
4868
+ else names.push("*");
4869
+ found.push({ specifier: node.moduleSpecifier.text, names, line: lineOf2(node) });
4870
+ }
4871
+ if (ts.isCallExpression(node)) {
4872
+ const arg = node.arguments[0];
4873
+ const isRequire = ts.isIdentifier(node.expression) && node.expression.text === "require";
4874
+ const isDynamic = node.expression.kind === ts.SyntaxKind.ImportKeyword;
4875
+ if ((isRequire || isDynamic) && arg && ts.isStringLiteralLike(arg)) {
4876
+ found.push({ specifier: arg.text, names: [isRequire ? "require" : "import()"], line: lineOf2(node) });
4877
+ }
4878
+ }
4879
+ ts.forEachChild(node, visit);
4880
+ };
4881
+ visit(sf);
4882
+ return found;
4883
+ }
4884
+ function scanFileImports(text, ts) {
4885
+ let refs;
4886
+ try {
4887
+ const pre = ts.preProcessFile(
4888
+ text,
4889
+ /* readImportFiles */
4890
+ true,
4891
+ /* detectJavaScriptImports */
4892
+ true
4893
+ );
4894
+ refs = pre.importedFiles ?? [];
4895
+ } catch {
4896
+ return null;
4897
+ }
4898
+ if (refs.length === 0) return [];
4899
+ const lineStarts = lineStartOffsets(text);
4900
+ return refs.map((r) => ({ specifier: r.fileName, names: [], line: lineAt(lineStarts, r.pos) }));
4901
+ }
4902
+ function countUnresolvableImports(text, ts) {
4903
+ let scanner;
4904
+ try {
4905
+ scanner = ts.createScanner(
4906
+ ts.ScriptTarget.Latest,
4907
+ /* skipTrivia */
4908
+ true,
4909
+ void 0,
4910
+ text
4911
+ );
4912
+ } catch {
4913
+ return 0;
4914
+ }
4915
+ const CALLEE_KINDS = new Set([
4916
+ ts.SyntaxKind.Identifier,
4917
+ ts.SyntaxKind.RequireKeyword,
4918
+ ts.SyntaxKind.ImportKeyword
4919
+ ].filter((k) => k !== void 0));
4920
+ const DOT_KINDS = new Set([ts.SyntaxKind.DotToken, ts.SyntaxKind.QuestionDotToken].filter((k) => k !== void 0));
4921
+ const EMPTY = { kind: void 0, text: "" };
4922
+ const history = [EMPTY, EMPTY, EMPTY];
4923
+ const remember = (kind, text2) => {
4924
+ history[2] = history[1];
4925
+ history[1] = history[0];
4926
+ history[0] = { kind, text: text2 };
4927
+ };
4928
+ const escapedLoaderReference = (next) => {
4929
+ const callee = history[0];
4930
+ const qualifier = history[1];
4931
+ if (callee.kind === void 0 || !CALLEE_KINDS.has(callee.kind) || callee.text !== "require") return false;
4932
+ if (qualifier.kind !== void 0 && DOT_KINDS.has(qualifier.kind)) return false;
4933
+ return next !== ts.SyntaxKind.OpenParenToken && next !== ts.SyntaxKind.QuestionDotToken && next !== ts.SyntaxKind.DotToken;
4934
+ };
4935
+ let count = 0;
4936
+ try {
4937
+ for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
4938
+ const text2 = scanner.getTokenText();
4939
+ if (escapedLoaderReference(token)) count++;
4940
+ if (token !== ts.SyntaxKind.OpenParenToken) {
4941
+ remember(token, text2);
4942
+ continue;
4943
+ }
4944
+ const optionalCall = history[0].kind === ts.SyntaxKind.QuestionDotToken;
4945
+ const callee = optionalCall ? history[1] : history[0];
4946
+ const qualifier = optionalCall ? history[2] : history[1];
4947
+ const isRequireCall = callee.kind !== void 0 && CALLEE_KINDS.has(callee.kind) && (callee.text === "require" || callee.text === "import") && !(qualifier.kind !== void 0 && DOT_KINDS.has(qualifier.kind));
4948
+ if (!isRequireCall) {
4949
+ remember(token, text2);
4950
+ continue;
4951
+ }
4952
+ remember(token, text2);
4953
+ const argument = scanner.scan();
4954
+ const argumentText = scanner.getTokenText();
4955
+ remember(argument, argumentText);
4956
+ const isLiteral = argument === ts.SyntaxKind.StringLiteral || argument === ts.SyntaxKind.NoSubstitutionTemplateLiteral;
4957
+ if (!isLiteral) {
4958
+ count++;
4959
+ continue;
4960
+ }
4961
+ const following = scanner.scan();
4962
+ remember(following, scanner.getTokenText());
4963
+ const argumentEnded = following === ts.SyntaxKind.CloseParenToken || following === ts.SyntaxKind.CommaToken;
4964
+ if (!argumentEnded) count++;
4965
+ }
4966
+ if (escapedLoaderReference(ts.SyntaxKind.EndOfFileToken)) count++;
4967
+ } catch {
4968
+ return count;
4969
+ }
4970
+ return count;
4971
+ }
4972
+ function createImportInventory(aliases = []) {
4973
+ const byPackage = /* @__PURE__ */ new Map();
4974
+ return {
4975
+ /** @param parsed whether these edges came from a full parse (names are trustworthy) or the scan. */
4976
+ add(relFile, edges, parsed) {
4977
+ for (const edge of edges) {
4978
+ const aliased = aliases.some((a) => a.wildcard ? edge.specifier.startsWith(a.prefix) : edge.specifier === a.prefix);
4979
+ if (aliased) continue;
4980
+ const pkg = npmPackageOf(edge.specifier);
4981
+ if (!pkg) continue;
4982
+ if (!isNpmPackageName(pkg)) continue;
4983
+ let acc = byPackage.get(pkg);
4984
+ if (!acc) {
4985
+ acc = { specifiers: /* @__PURE__ */ new Set(), names: /* @__PURE__ */ new Set(), namesComplete: true, sites: [], siteCount: 0 };
4986
+ byPackage.set(pkg, acc);
4987
+ }
4988
+ acc.specifiers.add(edge.specifier);
4989
+ for (const n of edge.names) acc.names.add(n);
4990
+ if (!parsed) acc.namesComplete = false;
4991
+ acc.siteCount++;
4992
+ if (acc.sites.length < MAX_SITES) acc.sites.push({ file: relFile, line: edge.line });
4993
+ }
4994
+ },
4995
+ list() {
4996
+ return [...byPackage.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([pkg, acc]) => {
4997
+ const names = [...acc.names].sort();
4998
+ const entry = {
4999
+ package: pkg,
5000
+ specifiers: [...acc.specifiers].sort(),
5001
+ namesComplete: acc.namesComplete,
5002
+ sites: acc.sites,
5003
+ siteCount: acc.siteCount,
5004
+ recognizedSinkKinds: recognizedSinkKinds(pkg)
5005
+ };
5006
+ if (names.length > 0) entry.names = names;
5007
+ return entry;
5008
+ });
5009
+ }
5010
+ };
5011
+ }
5012
+ function lineStartOffsets(text) {
5013
+ const starts = [0];
5014
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
5015
+ return starts;
5016
+ }
5017
+ function lineAt(starts, pos) {
5018
+ let lo = 0;
5019
+ let hi = starts.length - 1;
5020
+ while (lo < hi) {
5021
+ const mid = lo + hi + 1 >> 1;
5022
+ if ((starts[mid] ?? 0) <= pos) lo = mid;
5023
+ else hi = mid - 1;
5024
+ }
5025
+ return lo + 1;
5026
+ }
5027
+
5028
+ // src/map/invocations.ts
5029
+ var MAX_SITES2 = 3;
5030
+ function collectInvocations(sf, ts, bindings, ctx) {
5031
+ const found = [];
5032
+ const counts = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
5033
+ const isLocalName = (name, node) => bindings.locals.has(name) || isShadowedByEnclosingBinding(node, name, ts);
5034
+ const lineOf2 = (node) => {
5035
+ try {
5036
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
5037
+ } catch {
5038
+ return void 0;
5039
+ }
5040
+ };
5041
+ const traceRoot = (root) => {
5042
+ const specifier = bindings.resolve(root);
5043
+ if (specifier === void 0) return null;
5044
+ const direct = npmPackageOf(specifier);
5045
+ if (direct !== void 0) {
5046
+ const resolution = bindings.originOf(root) ?? "direct";
5047
+ return { pkg: direct, specifier, resolution, named: resolution === "direct" };
5048
+ }
5049
+ const exportName = bindings.exportNameOf(root) ?? root;
5050
+ const viaModule = ctx.graph.importedBinding(ctx.file, specifier, exportName);
5051
+ if (viaModule !== void 0) {
5052
+ return {
5053
+ pkg: viaModule.package,
5054
+ specifier,
5055
+ resolution: "reexport",
5056
+ named: viaModule.origin === "direct",
5057
+ apiName: viaModule.name
5058
+ };
5059
+ }
5060
+ return null;
5061
+ };
5062
+ const push = (traced, api, receiver, kind, node) => {
5063
+ const span = spanOf(node);
5064
+ found.push({
5065
+ package: traced.pkg,
5066
+ specifiers: [traced.specifier],
5067
+ api,
5068
+ ...receiver !== void 0 ? { receiver } : {},
5069
+ symbol: receiver !== void 0 ? `${receiver}.${api}` : api,
5070
+ kind,
5071
+ // Only ever `import`: a global has no package to correlate against, and an inferred package is not
5072
+ // evidence about the value being called.
5073
+ attribution: "import",
5074
+ resolution: traced.resolution,
5075
+ callCount: 1,
5076
+ sites: [{ file: ctx.owner, line: lineOf2(node), start: span.start, end: span.end }]
5077
+ });
5078
+ };
5079
+ const visit = (node) => {
5080
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
5081
+ const callee = node.expression;
5082
+ counts.total++;
5083
+ if (ts.isIdentifier(callee)) {
5084
+ const traced = traceRoot(callee.text);
5085
+ if (traced?.named) {
5086
+ const api = traced.apiName ?? bindings.exportNameOf(callee.text) ?? callee.text;
5087
+ push(traced, api, void 0, ts.isNewExpression(node) ? "construct" : "call", node);
5088
+ counts.dependency++;
5089
+ } else if (traced) {
5090
+ counts.ambiguous++;
5091
+ } else if (isLocalName(callee.text, node)) {
5092
+ counts.local++;
5093
+ } else {
5094
+ counts.ambiguous++;
5095
+ }
5096
+ } else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
5097
+ const root = rootIdentifier(callee.expression, ts);
5098
+ const traced = root !== void 0 ? traceRoot(root) : null;
5099
+ if (traced && root !== void 0) {
5100
+ counts.dependency++;
5101
+ const receiver = traced.resolution === "direct" && bindings.isPackageMember(root) ? bindings.exportNameOf(root) ?? root : void 0;
5102
+ push(traced, callee.name.text, receiver, "member", node);
5103
+ } else if (root !== void 0 && ts.isIdentifier(callee.expression) && isLocalName(root, node)) {
5104
+ counts.local++;
5105
+ } else {
5106
+ counts.ambiguous++;
5107
+ }
5108
+ } else {
5109
+ counts.ambiguous++;
5110
+ }
5111
+ }
5112
+ ts.forEachChild(node, visit);
5113
+ };
5114
+ visit(sf);
5115
+ return { invocations: found, counts };
5116
+ }
5117
+ function createInvocationInventory() {
5118
+ const byKey = /* @__PURE__ */ new Map();
5119
+ return {
5120
+ add(invocations) {
5121
+ for (const invocation of invocations) {
5122
+ const key = `${invocation.package}|${invocation.symbol}|${invocation.kind}|${invocation.resolution}`;
5123
+ const existing = byKey.get(key);
5124
+ if (existing === void 0) {
5125
+ byKey.set(key, { ...invocation });
5126
+ continue;
5127
+ }
5128
+ existing.callCount += invocation.callCount;
5129
+ for (const specifier of invocation.specifiers) {
5130
+ if (!existing.specifiers.includes(specifier)) existing.specifiers.push(specifier);
5131
+ }
5132
+ if (existing.sites.length < MAX_SITES2) existing.sites.push(...invocation.sites.slice(0, MAX_SITES2 - existing.sites.length));
5133
+ }
5134
+ },
5135
+ list() {
5136
+ return [...byKey.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, invocation]) => ({ ...invocation, specifiers: [...invocation.specifiers].sort() }));
5137
+ }
5138
+ };
5139
+ }
5140
+
5141
+ // src/map/extract.ts
5142
+ async function extractInputMap(cwd, ts, options = {}) {
5143
+ const notes = [];
5144
+ const endpoints = [];
5145
+ const failed = [];
5146
+ let boundary = cwd;
5147
+ try {
5148
+ boundary = realpathSync3(cwd);
5149
+ } catch {
5150
+ }
5151
+ const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
5152
+ const stats = { discovered: 0, unwalked: 0 };
5153
+ const files = collectSources(cwd, boundary, { followOutside: options.followSymlinks }, [], /* @__PURE__ */ new Set(), stats);
5154
+ const imports = createImportInventory(readPathAliases(cwd));
5155
+ const invocations = createInvocationInventory();
5156
+ let parsed = 0;
5157
+ let preFiltered = 0;
5158
+ let importScanFailures = 0;
5159
+ let unresolvableImports = 0;
5160
+ let sourceBytes = 0;
5161
+ const calls = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
5162
+ const startedAt = Date.now();
5163
+ const entryFiles = /* @__PURE__ */ new Set();
5164
+ const hopTargets = /* @__PURE__ */ new Set();
5165
+ for (const file of files) {
5166
+ try {
5167
+ const text = readFileSync10(file, "utf8");
5168
+ const relFile = relative4(cwd, file);
5169
+ sourceBytes += text.length;
5170
+ if (!hasEntrySignal(text)) {
5171
+ preFiltered++;
5172
+ const scanned = scanFileImports(text, ts);
5173
+ if (scanned === null) importScanFailures++;
5174
+ else imports.add(relFile, scanned, false);
5175
+ unresolvableImports += countUnresolvableImports(text, ts);
5176
+ continue;
5177
+ }
5178
+ parsed++;
5179
+ entryFiles.add(file);
5180
+ const fingerprint = createHash3("sha256").update(text).digest("hex").slice(0, 16);
5181
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
5182
+ const bindings = buildModuleBindings(sf, ts);
5183
+ for (const specifier of bindings.imports) {
5184
+ if (!specifier.startsWith(".")) continue;
5185
+ const target = graph.resolveLocal(file, specifier);
5186
+ if (target !== void 0) hopTargets.add(target);
5187
+ }
5188
+ imports.add(relFile, collectFileImports(sf, ts), true);
5189
+ unresolvableImports += countUnresolvableImports(text, ts);
5190
+ const ctx = { file, owner: relFile, graph };
5191
+ const called = collectInvocations(sf, ts, bindings, ctx);
5192
+ invocations.add(called.invocations);
5193
+ calls.total += called.counts.total;
5194
+ calls.dependency += called.counts.dependency;
5195
+ calls.local += called.counts.local;
5196
+ calls.ambiguous += called.counts.ambiguous;
5197
+ const localSinks = collectLocalSinks(sf, ts, bindings, ctx);
5198
+ for (const ep of extractFromFile(sf, ts, localSinks, bindings, ctx)) {
5199
+ if (ep.route === void 0 && ep.entryKind === "edge-function") {
5200
+ const fn = functionNameFromPath(relFile);
5201
+ if (fn) ep.route = "/" + fn;
5202
+ }
5203
+ if (ep.route === void 0 && (ep.entryKind === "route-handler" || ep.entryKind === "server-action")) {
5204
+ const derived = routeFromFilePath(relFile);
5205
+ if (derived.route) {
5206
+ ep.route = derived.route;
5207
+ if (derived.dynamic) ep.routeDynamic = true;
5208
+ }
5209
+ }
5210
+ endpoints.push({ ...ep, file: relFile, fingerprint });
5211
+ }
5212
+ } catch (e) {
5213
+ if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
5214
+ failed.push(relative4(cwd, file));
5215
+ }
5216
+ }
5217
+ let hopParsed = 0;
5218
+ let hopFailures = 0;
5219
+ for (const target of hopTargets) {
5220
+ if (entryFiles.has(target)) continue;
5221
+ try {
5222
+ const text = readFileSync10(target, "utf8");
5223
+ const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
5224
+ const bindings = buildModuleBindings(sf, ts);
5225
+ const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
5226
+ invocations.add(called.invocations);
5227
+ calls.total += called.counts.total;
5228
+ calls.dependency += called.counts.dependency;
5229
+ calls.local += called.counts.local;
5230
+ calls.ambiguous += called.counts.ambiguous;
5231
+ hopParsed++;
5232
+ } catch (e) {
5233
+ if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map hop error", target, e);
5234
+ hopFailures++;
5235
+ }
5236
+ }
5237
+ notes.push("Static analysis is best-effort \u2014 this is the DETECTED surface, not a proof of completeness.");
5238
+ notes.push('`inputs` and `sinks` are INVENTORIES (both present in the handler). Only `flows` asserts that an input reaches a sink: require confidence "exact-local" or "transformed-local" before pinning a rule, and identify the input by `inputId` \u2014 a field NAME can occur in more than one request namespace.');
5239
+ notes.push("Sinks are followed into same-file helpers and ONE hop into an imported relative module (a dependency\u2019s internals are not followed); deeper or dynamic indirection is not traced. Sinks inside declared-but-uncalled local functions are excluded.");
5240
+ notes.push('A sink `package` is resolved from the file\u2019s imports (`attribution: "import"`) or inferred from another import in the same file (`"inferred"`); an inferred package is a hint for a reviewer, not evidence about the receiver, and never licenses a rule.');
5241
+ if (!options.followSymlinks) notes.push("Symlinks leaving the project directory were not followed (use --follow-symlinks to include them).");
5242
+ if (failed.length > 0) {
5243
+ const sample = failed.slice(0, 5).join(", ");
5244
+ notes.push(`${failed.length} file(s) could not be analyzed and were skipped (fail-open): ${sample}${failed.length > 5 ? ", \u2026" : ""}.`);
5245
+ }
5246
+ const unresolved = endpoints.filter((e) => e.inputsResolved === false).length;
5247
+ if (unresolved > 0) {
5248
+ 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).`);
5249
+ }
5250
+ const heuristicOnly = endpoints.filter((e) => e.sinks.length > 0 && e.inputs.length > 0 && !e.flows.some((f) => isProvenFlow(f.confidence))).length;
5251
+ if (heuristicOnly > 0) {
5252
+ 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).`);
5253
+ }
5254
+ if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
5255
+ const importList = imports.list();
5256
+ const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
5257
+ const importCoverageGaps = {
5258
+ unreadableFiles: failed.length,
5259
+ unscannableFiles: importScanFailures,
5260
+ unwalkedPaths: stats.unwalked,
5261
+ unresolvableImports
5262
+ };
5263
+ const environmentalGaps = failed.length + importScanFailures + stats.unwalked;
5264
+ const importsComplete = environmentalGaps === 0 && unresolvableImports === 0;
5265
+ notes.push("`imports` lists every package the app imports, from ALL source files \u2014 not only files holding an entry point. Absence of a SINK for a package is never evidence; absence of the PACKAGE is evidence only when coverage.importsComplete is true.");
5266
+ notes.push(`${unmodelled} of ${importList.length} imported package(s) have no recognized sink family (recognizedSinkKinds: []). The extractor models a small set of API families, so for those packages it cannot tell whether input reaches them: a vulnerability in one must stay "needs review" and can never be closed as unreachable using this map.`);
5267
+ if (environmentalGaps > 0) {
5268
+ notes.push(`The import inventory is INCOMPLETE: ${failed.length} file(s) could not be read, ${importScanFailures} could not be scanned, and ${stats.unwalked} path(s) could not be walked at all (unreadable directory, broken link, or a symlink leaving the project). A package may therefore be imported without appearing in \`imports\`. Do not read a package's absence as "not imported" while coverage.importsComplete is false.`);
5269
+ }
5270
+ if (unresolvableImports > 0) {
5271
+ notes.push(`The import inventory is INCOMPLETE for a reason no re-run can fix: ${unresolvableImports} import(s) do not name a resolvable module \u2014 either the specifier is computed at runtime (\`require(REGISTRY[kind])\`) or the loader itself was aliased (\`const r = require\`), so which package is loaded is not knowable from the source. Those imports appear NOWHERE in \`imports\` \u2014 not as an unresolved entry, simply absent \u2014 so a package's absence is not evidence of it being unused. This is a property of the application's code, not a scan failure. Reported conservatively: an aliased loader counts even if every call through it uses a literal.`);
5272
+ }
5273
+ const invocationList = invocations.list();
5274
+ if (invocationList.length > 0 || parsed > 0) {
5275
+ notes.push(`\`apiInvocations\` records ${invocationList.length} dependency API call(s) resolved from ${parsed} entry file(s) plus ${hopParsed} local module(s) they import directly. POSITIVE EVIDENCE ONLY: a package missing from it may still have its API called \u2014 see coverage.apiInventoryLimitations.`);
5276
+ }
5277
+ if (hopFailures > 0) {
5278
+ notes.push(`${hopFailures} local module(s) imported by an entry file could not be parsed, so any dependency call written in them is missing from \`apiInvocations\`.`);
5279
+ }
5280
+ let rssBytes;
5281
+ let peakRssBytes;
5282
+ try {
5283
+ if (typeof process !== "undefined") {
5284
+ if (typeof process.memoryUsage === "function") rssBytes = process.memoryUsage().rss;
5285
+ if (typeof process.resourceUsage === "function") {
5286
+ const maxRssKb = process.resourceUsage().maxRSS;
5287
+ if (typeof maxRssKb === "number" && maxRssKb > 0) peakRssBytes = maxRssKb * 1024;
5288
+ }
5289
+ }
5290
+ } catch {
5291
+ }
5292
+ const deploymentShapes = detectDeploymentShapes(cwd, { boundary, followOutside: options.followSymlinks });
5293
+ notes.push(deploymentShapes.length === 0 ? "`deploymentShapes` is EMPTY: no deployment artifact was recognized. That is not evidence the app has no server-side runtime \u2014 a serverless handler this analysis cannot parse produces no endpoint and looks identical to an app that has none, and entry-point recognition has no completeness flag (coverage.importsComplete covers the import inventory only). A definitive answer needs deployment or build attestation, which source analysis cannot supply." : `\`deploymentShapes\` records ${deploymentShapes.length} deployment artifact(s) the project declares (${deploymentShapes.map((s) => s.shape).join(", ")}). POSITIVE EVIDENCE ONLY, and findings differ in strength: only \`runtime-entry\` (a worker entry, or a provider function directory with source) shows something that serves. \`deployment-config\` means the project deploys to a platform \u2014 a static site on Netlify has a \`netlify.toml\` \u2014 and \`layout\` (a root \`api/\` or \`functions/\` folder) is an ordinary application folder. Neither may on its own be read as a server runtime.`);
5294
+ const serverSurface = classifyServerSurface(cwd, endpoints.length, deploymentShapes, ts);
5295
+ notes.push(surfaceNote(serverSurface));
5296
+ return {
5297
+ version: 3,
5298
+ framework: detectFramework(cwd),
5299
+ deploymentShapes,
5300
+ serverSurface,
5301
+ endpoints,
5302
+ imports: importList,
5303
+ apiInvocations: invocationList,
5304
+ coverage: {
5305
+ adapter: "agnostic-v1",
5306
+ filesDiscovered: stats.discovered,
5307
+ filesParsed: parsed,
5308
+ filesPreFiltered: preFiltered,
5309
+ filesHopParsed: hopParsed,
5310
+ filesSkipped: failed.length,
5311
+ pathsUnwalked: stats.unwalked,
5312
+ importsComplete,
5313
+ importCoverageGaps,
5314
+ apiInvocations: invocationList.length,
5315
+ callsTotal: calls.total,
5316
+ callsDependency: calls.dependency,
5317
+ callsLocal: calls.local,
5318
+ callsAmbiguous: calls.ambiguous,
5319
+ sourceBytes,
5320
+ analysisMs: Date.now() - startedAt,
5321
+ ...rssBytes !== void 0 ? { rssBytes } : {},
5322
+ ...peakRssBytes !== void 0 ? { peakRssBytes } : {},
5323
+ // The list IS the partiality statement. There is deliberately no `apiInventoryComplete`: parsing
5324
+ // every file would raise recall without removing any of these, so no parsing budget could make an
5325
+ // absent invocation mean "not called".
5326
+ apiInventoryLimitations: [
5327
+ "only files carrying an entry-point signal, and the local modules they import directly, are parsed \u2014 a call two hops away is unseen",
5328
+ "a computed callee or property (`client[name]()`) cannot be resolved to an API",
5329
+ "a dynamic import()/require() with a non-literal specifier is not traced",
5330
+ "reflection, generated code, and syntax that fails to parse are invisible",
5331
+ "a receiver reached through more than one local hop, or through dependency injection, is not followed"
5332
+ ],
5333
+ roots: ["."],
5334
+ notes
5335
+ }
5336
+ };
5337
+ }
5338
+
5339
+ // src/map/index.ts
5340
+ async function buildInputMap(cwd, options = {}) {
5341
+ const ts = await loadTypeScript(cwd);
5342
+ if (!ts) {
5343
+ return {
5344
+ map: null,
5345
+ error: "Could not resolve a TypeScript compiler to parse the app. `map` uses your project\u2019s own `typescript` (already a devDependency of a TypeScript app) \u2014 install it and retry."
5346
+ };
5347
+ }
5348
+ const map = await extractInputMap(cwd, ts, options);
5349
+ return { map };
5350
+ }
5351
+
5352
+ // src/setup.ts
5353
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
5354
+ import path10 from "path";
5355
+ var SCAN_COMMAND = "patchstack-connect scan";
5356
+ var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
5357
+ function setupProtection(cwd) {
5358
+ const install = runProtect(cwd);
5359
+ const verification = runVerify(cwd);
5360
+ return { install, verification };
5361
+ }
5362
+ function appendHook(existing, command) {
5363
+ if (existing === void 0 || existing.trim().length === 0) {
5364
+ return command;
5365
+ }
5366
+ if (existing.includes(command)) {
5367
+ return existing;
5368
+ }
5369
+ return `${existing} && ${command}`;
5370
+ }
5371
+ function wireBuildScripts(cwd, packageManager) {
5372
+ const target = path10.join(cwd, "package.json");
5373
+ const raw = readFileSync11(target, "utf8");
5374
+ const pkg = JSON.parse(raw);
5375
+ const scripts = pkg.scripts ?? {};
5376
+ const build = scripts.build;
5377
+ const postinstall = appendHook(scripts.postinstall, SCAN_COMMAND);
5378
+ if (build === void 0 || build.trim().length === 0) {
5379
+ if (postinstall === scripts.postinstall) {
5380
+ return {
5381
+ changed: false,
5382
+ strategy: "postinstall-only",
5383
+ detail: "dependency-install scan already wired; no build script to integrate."
5384
+ };
5385
+ }
5386
+ scripts.postinstall = postinstall;
5387
+ } else if (packageManager === "bun") {
5388
+ let nextBuild = build;
5389
+ if (!nextBuild.includes(SCAN_COMMAND)) {
5390
+ nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
5391
+ }
5392
+ if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
5393
+ nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
5394
+ }
5395
+ if (nextBuild === build && postinstall === scripts.postinstall) {
5396
+ return {
5397
+ changed: false,
5398
+ strategy: "build-chain",
5399
+ detail: "dependency-install scan and build chain already wired."
5400
+ };
5401
+ }
5402
+ scripts.postinstall = postinstall;
5403
+ scripts.build = nextBuild;
5404
+ } else {
5405
+ const prebuild = appendHook(scripts.prebuild, SCAN_COMMAND);
5406
+ const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
5407
+ if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
5408
+ return {
5409
+ changed: false,
5410
+ strategy: "lifecycle-hooks",
5411
+ detail: "dependency-install, prebuild, and postbuild hooks are already wired."
5412
+ };
5413
+ }
5414
+ scripts.postinstall = postinstall;
5415
+ scripts.prebuild = prebuild;
5416
+ scripts.postbuild = postbuild;
5417
+ }
5418
+ pkg.scripts = scripts;
5419
+ const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
5420
+ const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
5421
+ const trailingNewline = raw.endsWith("\n") ? "\n" : "";
5422
+ writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
5423
+ if (build === void 0 || build.trim().length === 0) {
5424
+ return {
5425
+ changed: true,
5426
+ strategy: "postinstall-only",
5427
+ detail: "added a dependency-install scan; package.json has no build script."
5428
+ };
5429
+ }
5430
+ return packageManager === "bun" ? {
5431
+ changed: true,
5432
+ strategy: "build-chain",
5433
+ detail: "added a dependency-install scan and chained scan/mark-build around the build."
5434
+ } : {
5435
+ changed: true,
5436
+ strategy: "lifecycle-hooks",
5437
+ detail: "added scans to postinstall/prebuild and mark-build to postbuild."
5438
+ };
5439
+ }
5440
+
5441
+ // src/cli.ts
5442
+ var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
5443
+
5444
+ Usage:
5445
+ patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
5446
+ If no UUID is configured, the server
5447
+ provisions one and we persist it. After a
5448
+ successful post it also adds/updates the
5449
+ disclosure-widget <script> tag in the root
5450
+ HTML shell (index.html, public/index.html,
5451
+ or src/app.html) \u2014 opt out with
5452
+ "widget": false in .patchstackrc.json
5453
+ patchstack-connect setup [options] Finish the bounded project setup: run scan,
5454
+ manage the widget, install + verify runtime
5455
+ protection, and wire dependency/build scans.
5456
+ Never runs the project build
5457
+ patchstack-connect map [--dir <p>] [--out <f>] [--upload]
5458
+ Map the app's attack surface: entry points, the
5459
+ inputs each reads, the sinks it can reach, and
5460
+ evidence-backed input\u2192sink flows (each labelled
5461
+ with how the link was established). Best-effort static
5462
+ analysis \u2014 reports the DETECTED surface, with
5463
+ coverage counters. Prints JSON (--out writes a
5464
+ file; --follow-symlinks leaves the project dir).
5465
+ Uses the app's own TypeScript
5466
+ patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
5467
+ with an existing site UUID
5468
+ patchstack-connect status [options] Show current configuration and whether the
5469
+ site still exists on Patchstack (active /
5470
+ removed)
5471
+ patchstack-connect uninstall [options] Signal Patchstack that this package is being
5472
+ removed from the project. An unclaimed site
5473
+ record is deleted; a claimed site is flagged
5474
+ for its owner to remove in the dashboard.
5475
+ Does NOT touch local files \u2014 see the
5476
+ "Uninstalling" steps in AGENT-INSTALL.md
5477
+ patchstack-connect mark-build [options] Stamp built HTML with a production flag +
5478
+ build fingerprint, and ensure the widget
5479
+ tag in built pages (run as a postbuild step)
5480
+ patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
5481
+ guard). Auto-wires supported server stacks;
5482
+ for others it scaffolds a
5483
+ generic guard + prints a wiring plan.
5484
+ --demo seeds a broad sample rule set (for
5485
+ demonstrations, not production).
5486
+ --check verifies the guard is wired (exit 1
5487
+ if not) \u2014 for the wire-then-verify loop.
5488
+ patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
5489
+ walkthrough: verify the vulnerable package,
5490
+ scan it, wait for live rule 18843, install +
5491
+ verify the guard, and print test requests.
5492
+ Does not install vulnerable dependencies.
5493
+ patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
5494
+ preparing, running, proving, and cleaning up
5495
+ the production-backed local demo.
5496
+ patchstack-connect guide [--full] Show this project's setup status (what's done,
5497
+ what's missing, with tailored commands), then
5498
+ print the full setup guide. --full prints the
5499
+ guide even when setup is complete
5500
+ patchstack-connect login [options] Recover this site's Patchstack credential when
5501
+ .patchstackrc.json has been lost. Prints a short
5502
+ code to approve in the dashboard; approving
5503
+ rotates the credential, so the old one stops
5504
+ working
5505
+ patchstack-connect help Print this message
5506
+
5507
+ Options (for scan, setup, status, and uninstall):
5508
+ --site-uuid <uuid> Override the configured site UUID
5509
+ --endpoint <url> Override the API endpoint
5510
+ --dry-run (scan only) Show the payload without posting
2925
5511
 
2926
5512
  Options (for mark-build):
2927
5513
  --dir <path> Build output directory (default: auto-detect
@@ -2934,6 +5520,7 @@ Options (for demo and demo-guide):
2934
5520
  Environment:
2935
5521
  PATCHSTACK_SITE_UUID Site UUID
2936
5522
  PATCHSTACK_API_KEY WP-format site API key for block-log reporting (never put in the widget)
5523
+ PATCHSTACK_PULSE_AUTH Credential for authenticated Pulse ingest (defaults to PATCHSTACK_API_KEY)
2937
5524
  PATCHSTACK_TELEMETRY Set to off to disable block-log reporting
2938
5525
  PATCHSTACK_API_BASE API origin for /oauth/token and /api/logs/log (default: https://api.patchstack.com)
2939
5526
  PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
@@ -2953,7 +5540,7 @@ Examples:
2953
5540
  npx @patchstack/connect demo node-serialize
2954
5541
  npx @patchstack/connect demo-guide node-serialize
2955
5542
  `;
2956
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url"]);
5543
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out"]);
2957
5544
  function parseArgs(argv) {
2958
5545
  const args = argv.slice(2);
2959
5546
  const positional = [];
@@ -3005,6 +5592,103 @@ async function runInit(args) {
3005
5592
  console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
3006
5593
  return 0;
3007
5594
  }
5595
+ async function runLogin(args) {
5596
+ if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
5597
+ console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
5598
+ return 1;
5599
+ }
5600
+ const config = await resolveConfig({
5601
+ cwd: process.cwd(),
5602
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
5603
+ cliEndpoint: getStringFlag(args.flags, "endpoint")
5604
+ });
5605
+ const result = await login(config, (userCode, verificationUri) => {
5606
+ console.log(`
5607
+ Your code: ${userCode}`);
5608
+ console.log(` Approve at: ${verificationUri}
5609
+ `);
5610
+ console.log(" Waiting for approval\u2026");
5611
+ });
5612
+ if (result.status === "approved") {
5613
+ console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.\n");
5614
+ return 0;
5615
+ }
5616
+ console.error(`
5617
+ ${result.message ?? "Login failed."}
5618
+ `);
5619
+ return 1;
5620
+ }
5621
+ async function runMap(args) {
5622
+ const cwd = getStringFlag(args.flags, "dir") ?? process.cwd();
5623
+ const { map, error } = await buildInputMap(cwd, {
5624
+ followSymlinks: args.flags.get("follow-symlinks") === true
5625
+ });
5626
+ if (!map) {
5627
+ console.error(`patchstack: ${error}`);
5628
+ return 1;
5629
+ }
5630
+ const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
5631
+ const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
5632
+ const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
5633
+ const c = map.coverage;
5634
+ console.error(
5635
+ `patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
5636
+ );
5637
+ console.error(
5638
+ // All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
5639
+ // simply contain no server entry point (most of a project is client code). Only `skipped` is a
5640
+ // failure to analyse.
5641
+ `patchstack: ${c.filesDiscovered} file(s) found \u2014 ${c.filesParsed} analysed, ${c.filesPreFiltered} skipped (no server entry point)` + (c.filesSkipped ? `, ${c.filesSkipped} could not be analysed` : "") + `. DETECTED surface only \u2014 static analysis is best-effort; every flow carries the tier it was established at ("exact-local" and "transformed-local" are proven; "imported", "heuristic" and "unknown" are not).`
5642
+ );
5643
+ const invoked = map.apiInvocations ?? [];
5644
+ if (invoked.length > 0) {
5645
+ const c2 = map.coverage;
5646
+ const dependency = c2.callsDependency ?? 0;
5647
+ const ambiguous = c2.callsAmbiguous ?? 0;
5648
+ const denominator = dependency + ambiguous;
5649
+ const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
5650
+ console.error(
5651
+ `patchstack: ${invoked.length} dependency API call(s) resolved across ${new Set(invoked.map((i) => i.package)).size} package(s) from ${c2.callsTotal ?? 0} call site(s) \u2014 ${quality}% of dependency-candidate receivers resolved (${c2.callsLocal ?? 0} local, ${ambiguous} ambiguous). Positive evidence only: absence here never means an API is not called.`
5652
+ );
5653
+ }
5654
+ const imported = map.imports ?? [];
5655
+ if (imported.length > 0) {
5656
+ const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
5657
+ console.error(
5658
+ `patchstack: ${imported.length} package(s) imported \u2014 ${unmodelled} with no recognized sink family, so a vulnerability in those cannot be judged reachable or unreachable from this map.`
5659
+ );
5660
+ }
5661
+ const json = JSON.stringify(map, null, 2);
5662
+ const out = getStringFlag(args.flags, "out");
5663
+ if (out) {
5664
+ writeFileSync7(out, json);
5665
+ console.error(`patchstack: wrote ${out}`);
5666
+ } else if (args.flags.get("upload") !== true) {
5667
+ console.log(json);
5668
+ }
5669
+ if (args.flags.get("upload") === true) {
5670
+ if (map.endpoints.length === 0) {
5671
+ console.error("patchstack: nothing to upload \u2014 no server entry points were detected.");
5672
+ return 0;
5673
+ }
5674
+ const config = await resolveConfig({
5675
+ cwd,
5676
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
5677
+ cliEndpoint: getStringFlag(args.flags, "endpoint")
5678
+ });
5679
+ const outcome = await postInputMap(config, map);
5680
+ if (outcome.result === "stored") {
5681
+ console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
5682
+ } else if (outcome.result === "unchanged") {
5683
+ console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
5684
+ } else if (outcome.result === "skipped") {
5685
+ console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
5686
+ } else {
5687
+ console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
5688
+ }
5689
+ }
5690
+ return 0;
5691
+ }
3008
5692
  async function runScan(args, options = {}) {
3009
5693
  const dryRun = args.flags.get("dry-run") === true;
3010
5694
  const config = await resolveConfig({
@@ -3061,7 +5745,8 @@ async function runScan(args, options = {}) {
3061
5745
  }
3062
5746
  if (typeof response.api_key === "string" && response.api_key.length > 0) {
3063
5747
  const target = await persistApiKey(process.cwd(), response.api_key);
3064
- console.log(`Saved API key to ${target} (for block-log reporting via /api/logs/log; keep out of the public widget).`);
5748
+ await persistPulseAuth(process.cwd(), response.api_key);
5749
+ console.log(`Saved API key to ${target} (authenticates Pulse ingest and block-log reporting; keep out of the public widget).`);
3065
5750
  }
3066
5751
  if (response.stored) {
3067
5752
  console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
@@ -3263,7 +5948,7 @@ async function runGuide(args) {
3263
5948
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
3264
5949
  console.log("");
3265
5950
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
3266
- console.log(readFileSync7(guidePath, "utf8"));
5951
+ console.log(readFileSync12(guidePath, "utf8"));
3267
5952
  return 0;
3268
5953
  }
3269
5954
  async function runSetup(args) {
@@ -3354,6 +6039,40 @@ async function runStatus(args) {
3354
6039
  }
3355
6040
  return 0;
3356
6041
  }
6042
+ async function runUninstall(args) {
6043
+ const config = await resolveConfig({
6044
+ cwd: process.cwd(),
6045
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
6046
+ cliEndpoint: getStringFlag(args.flags, "endpoint")
6047
+ });
6048
+ if (config.siteUuid === null) {
6049
+ console.log("No site UUID configured \u2014 there is no site record to signal about.");
6050
+ console.log('Continue with the local removal steps in AGENT-INSTALL.md ("Uninstalling").');
6051
+ return 0;
6052
+ }
6053
+ console.log(`Signalling Patchstack that @patchstack/connect is being removed (site ${config.siteUuid})\u2026`);
6054
+ const outcome = await postPackageRemoved(config);
6055
+ switch (outcome.result) {
6056
+ case "deleted":
6057
+ console.log("Site record removed from Patchstack (the site was unclaimed).");
6058
+ break;
6059
+ case "flagged":
6060
+ console.log("This site is claimed by a Patchstack account, so its record was kept and flagged.");
6061
+ console.log("Its owner can remove it at https://app.patchstack.com to free the site slot.");
6062
+ break;
6063
+ case "gone":
6064
+ console.log("The site record no longer exists on Patchstack \u2014 nothing to signal.");
6065
+ break;
6066
+ case "failed":
6067
+ console.warn(`Could not signal Patchstack${outcome.message !== null ? ` (${outcome.message})` : ""}.`);
6068
+ console.warn("The site record may remain \u2014 it can always be removed from the dashboard at https://app.patchstack.com.");
6069
+ break;
6070
+ }
6071
+ console.log("");
6072
+ console.log("This command only signals Patchstack. The local integration files must still be");
6073
+ console.log('removed \u2014 follow the "Uninstalling" steps in AGENT-INSTALL.md.');
6074
+ return 0;
6075
+ }
3357
6076
  function describeStack(stack) {
3358
6077
  const parts = [stack.builder, stack.framework, stack.ui, stack.runtime].filter(
3359
6078
  (part) => part !== null
@@ -3410,7 +6129,7 @@ async function runMarkBuild(args) {
3410
6129
  let marked = 0;
3411
6130
  let widgetTouched = 0;
3412
6131
  for (const file of files) {
3413
- const before = readFileSync7(file, "utf8");
6132
+ const before = readFileSync12(file, "utf8");
3414
6133
  let after = injectMarker(before, snippet);
3415
6134
  if (widgetUuid !== null) {
3416
6135
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -3443,6 +6162,8 @@ async function main() {
3443
6162
  return runScan(args);
3444
6163
  case "status":
3445
6164
  return runStatus(args);
6165
+ case "uninstall":
6166
+ return runUninstall(args);
3446
6167
  case "mark-build":
3447
6168
  return runMarkBuild(args);
3448
6169
  case "protect":
@@ -3455,6 +6176,10 @@ async function main() {
3455
6176
  return runGuide(args);
3456
6177
  case "setup":
3457
6178
  return runSetup(args);
6179
+ case "map":
6180
+ return runMap(args);
6181
+ case "login":
6182
+ return runLogin(args);
3458
6183
  default:
3459
6184
  console.error(`Unknown command: ${args.command}
3460
6185
  `);