@patchstack/connect 0.3.30 → 0.3.32

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,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
4
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
5
+ import { createRequire as createRequire2 } from "module";
5
6
 
6
7
  // src/parsers/index.ts
7
8
  import { access } from "fs/promises";
@@ -330,7 +331,7 @@ async function walkNodeModules(cwd) {
330
331
  seen.set(await realpath(cwd), -1);
331
332
  } catch {
332
333
  }
333
- return dedupeByShallowest(await walk(root, 0, seen));
334
+ return dedupeByShallowest(await walk(root, 0, seen, cwd));
334
335
  }
335
336
  function dedupeByShallowest(found) {
336
337
  const best = /* @__PURE__ */ new Map();
@@ -341,7 +342,7 @@ function dedupeByShallowest(found) {
341
342
  return [...best.values()].map(({ entry }) => entry);
342
343
  }
343
344
  var CONCURRENCY = 32;
344
- async function walk(dir, depth, seen) {
345
+ async function walk(dir, depth, seen, root) {
345
346
  let names;
346
347
  try {
347
348
  names = await readdir(dir);
@@ -353,7 +354,7 @@ async function walk(dir, depth, seen) {
353
354
  const fullPath = path3.join(dir, name);
354
355
  const resolved = await claim(fullPath, seen, depth);
355
356
  if (resolved === null) return [];
356
- if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
357
+ if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
357
358
  if (name.startsWith("@")) {
358
359
  let subNames;
359
360
  try {
@@ -366,16 +367,16 @@ async function walk(dir, depth, seen) {
366
367
  const scopedDir = path3.join(fullPath, sub);
367
368
  const scopedReal = await claim(scopedDir, seen, depth);
368
369
  if (scopedReal === null) return [];
369
- return packageAndNested(scopedDir, scopedReal, depth, seen);
370
+ return packageAndNested(scopedDir, scopedReal, depth, seen, root);
370
371
  });
371
372
  return scoped.flat();
372
373
  }
373
- return packageAndNested(fullPath, resolved, depth, seen);
374
+ return packageAndNested(fullPath, resolved, depth, seen, root);
374
375
  });
375
376
  return perEntry.flat();
376
377
  }
377
- async function packageAndNested(pkgDir, resolved, depth, seen) {
378
- const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
378
+ async function packageAndNested(pkgDir, resolved, depth, seen, root) {
379
+ const [own, nested] = await Promise.all([readPackage(pkgDir, depth, root), walkNested(pkgDir, depth, seen, root)]);
379
380
  return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
380
381
  }
381
382
  async function mapLimit(items, fn) {
@@ -390,7 +391,10 @@ async function mapLimit(items, fn) {
390
391
  await Promise.all(workers);
391
392
  return results;
392
393
  }
393
- async function readPackage(pkgDir, depth) {
394
+ function installLocation(root, pkgDir, impl = path3) {
395
+ return impl.relative(root, pkgDir).split(impl.sep).join("/");
396
+ }
397
+ async function readPackage(pkgDir, depth, root) {
394
398
  let raw;
395
399
  try {
396
400
  raw = await readFile4(path3.join(pkgDir, "package.json"), "utf8");
@@ -409,14 +413,14 @@ async function readPackage(pkgDir, depth) {
409
413
  if (typeof parsed.version !== "string" || parsed.version.length === 0) {
410
414
  return null;
411
415
  }
412
- return { name: parsed.name, version: parsed.version, direct: depth === 0 };
416
+ return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
413
417
  }
414
- async function walkNested(pkgDir, depth, seen) {
418
+ async function walkNested(pkgDir, depth, seen, root) {
415
419
  const nested = path3.join(pkgDir, "node_modules");
416
420
  if (await claim(nested, seen, depth + 1) === null) {
417
421
  return [];
418
422
  }
419
- return walk(nested, depth + 1, seen);
423
+ return walk(nested, depth + 1, seen, root);
420
424
  }
421
425
  async function claim(dir, seen, depth) {
422
426
  let resolved;
@@ -1059,21 +1063,33 @@ async function exists(filePath) {
1059
1063
  }
1060
1064
 
1061
1065
  // src/normalize.ts
1062
- function buildWirePayload(manifest) {
1066
+ function buildWirePayload(manifest, options = {}) {
1067
+ const withPaths = options.installPaths === true;
1063
1068
  const seen = /* @__PURE__ */ new Map();
1064
1069
  const wirePackages = [];
1070
+ const byIdentity = /* @__PURE__ */ new Map();
1071
+ let entriesWithoutPath = 0;
1065
1072
  for (const entry of manifest.packages) {
1073
+ const identity = `${entry.name}@${entry.version}`;
1074
+ const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
1075
+ if (location === void 0) entriesWithoutPath++;
1076
+ const existing = byIdentity.get(identity);
1077
+ if (existing) {
1078
+ if (location !== void 0) addPath(existing, location);
1079
+ continue;
1080
+ }
1066
1081
  const versions = seen.get(entry.name);
1067
1082
  if (versions) {
1068
- if (versions.has(entry.version)) {
1069
- continue;
1070
- }
1071
1083
  versions.add(entry.version);
1072
1084
  } else {
1073
1085
  seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
1074
1086
  }
1075
- wirePackages.push({ name: entry.name, version: entry.version });
1087
+ const wire6 = { name: entry.name, version: entry.version };
1088
+ if (location !== void 0) addPath(wire6, location);
1089
+ byIdentity.set(identity, wire6);
1090
+ wirePackages.push(wire6);
1076
1091
  }
1092
+ for (const wire6 of wirePackages) wire6.paths?.sort();
1077
1093
  wirePackages.sort((a, b) => {
1078
1094
  if (a.name === b.name) {
1079
1095
  return compareVersions(a.version, b.version);
@@ -1087,7 +1103,18 @@ function buildWirePayload(manifest) {
1087
1103
  }
1088
1104
  }
1089
1105
  return {
1090
- payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
1106
+ payload: {
1107
+ ecosystem: manifest.ecosystem,
1108
+ packages: wirePackages,
1109
+ // Every entry had a location, so a missing path anywhere means the package is not installed there.
1110
+ // One entry without one forfeits that for the whole payload: a consumer reading a short `paths` has
1111
+ // no way to tell which entry was the incomplete one.
1112
+ //
1113
+ // No separate check for "locations were not requested": with them off every entry counts as without
1114
+ // one, so this is already false — which is the default, and the safe direction. An explicit
1115
+ // `withPaths &&` here was a guard no test could distinguish from its absence.
1116
+ installPathsComplete: entriesWithoutPath === 0 && manifest.packages.length > 0
1117
+ },
1091
1118
  stats: {
1092
1119
  uniqueNames: seen.size,
1093
1120
  duplicateNames,
@@ -1095,6 +1122,10 @@ function buildWirePayload(manifest) {
1095
1122
  }
1096
1123
  };
1097
1124
  }
1125
+ function addPath(wire6, installPath) {
1126
+ if (wire6.paths === void 0) wire6.paths = [installPath];
1127
+ else if (!wire6.paths.includes(installPath)) wire6.paths.push(installPath);
1128
+ }
1098
1129
  function compareVersions(a, b) {
1099
1130
  if (a === b) {
1100
1131
  return 0;
@@ -2053,38 +2084,83 @@ var PROD_MARKER_GLOBAL = "__PATCHSTACK_PROD__";
2053
2084
  var REGION_OPEN = "{/* #region patchstack (managed by patchstack-connect \u2014 do not edit) */}";
2054
2085
  var REGION_CLOSE = "{/* #endregion patchstack */}";
2055
2086
  var REGION_RE = /[ \t]*\{\/\* #region patchstack[\s\S]*?#endregion patchstack \*\/\}\n?/g;
2087
+ function findJsxShellAnchor(source, tagName) {
2088
+ const candidates = new RegExp(`^([ \\t]*)<${tagName}(?=[\\s/>])`, "gm");
2089
+ let candidate;
2090
+ while ((candidate = candidates.exec(source)) !== null) {
2091
+ let quote = null;
2092
+ let escaped = false;
2093
+ let braces = 0;
2094
+ let blockComment = false;
2095
+ let lineComment = false;
2096
+ for (let index = candidates.lastIndex; index < source.length; index += 1) {
2097
+ const char = source[index];
2098
+ const next = source[index + 1];
2099
+ if (lineComment) {
2100
+ if (char === "\n") lineComment = false;
2101
+ continue;
2102
+ }
2103
+ if (blockComment) {
2104
+ if (char === "*" && next === "/") {
2105
+ blockComment = false;
2106
+ index += 1;
2107
+ }
2108
+ continue;
2109
+ }
2110
+ if (quote !== null) {
2111
+ if (escaped) {
2112
+ escaped = false;
2113
+ } else if (char === "\\") {
2114
+ escaped = true;
2115
+ } else if (char === quote) {
2116
+ quote = null;
2117
+ }
2118
+ continue;
2119
+ }
2120
+ if (char === "'" || char === '"' || char === "`") {
2121
+ quote = char;
2122
+ } else if (braces > 0 && char === "/" && next === "*") {
2123
+ blockComment = true;
2124
+ index += 1;
2125
+ } else if (braces > 0 && char === "/" && next === "/") {
2126
+ lineComment = true;
2127
+ index += 1;
2128
+ } else if (char === "{") {
2129
+ braces += 1;
2130
+ } else if (char === "}") {
2131
+ if (braces === 0) break;
2132
+ braces -= 1;
2133
+ } else if (char === ">" && braces === 0) {
2134
+ const openingTag = source.slice(candidate.index, index + 1);
2135
+ if (/\/\s*>$/.test(openingTag)) break;
2136
+ return { end: index + 1, indent: candidate[1] ?? "" };
2137
+ }
2138
+ }
2139
+ }
2140
+ return null;
2141
+ }
2056
2142
  function ensureMarkerInJsxShell(source, framework) {
2057
2143
  if (!hasJsxShell(framework)) {
2058
2144
  return { source, action: "unsupported" };
2059
2145
  }
2060
2146
  const stripped = source.replace(REGION_RE, "");
2061
2147
  if (stripped.includes(PROD_MARKER_GLOBAL)) {
2062
- return { source, action: "manual" };
2148
+ return { source: stripped, action: "manual" };
2063
2149
  }
2064
2150
  const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
2065
- const widget = stripped.match(/^([ \t]*).*patchstack-widget.*$/m);
2066
- if (widget?.index !== void 0) {
2067
- const indent = widget[1] ?? "";
2068
- return {
2069
- source: `${stripped.slice(0, widget.index)}${block(indent)}
2070
- ${stripped.slice(widget.index)}`,
2071
- action: "added"
2072
- };
2073
- }
2074
- for (const anchor of [/^([ \t]*)<head>[ \t]*$/m, /^([ \t]*)<body>[ \t]*$/m]) {
2075
- const match = stripped.match(anchor);
2076
- if (match?.index === void 0) {
2077
- continue;
2078
- }
2079
- const end = match.index + match[0].length;
2080
- const indent = `${match[1] ?? ""} `;
2151
+ for (const tagName of ["head", "body"]) {
2152
+ const anchor = findJsxShellAnchor(stripped, tagName);
2153
+ if (anchor === null) continue;
2154
+ const indent = `${anchor.indent} `;
2155
+ const remainder = stripped.slice(anchor.end);
2156
+ const separator = remainder.startsWith("\n") || remainder.startsWith("\r") ? "" : "\n";
2081
2157
  return {
2082
- source: `${stripped.slice(0, end)}
2083
- ${block(indent)}${stripped.slice(end)}`,
2158
+ source: `${stripped.slice(0, anchor.end)}
2159
+ ${block(indent)}${separator}${remainder}`,
2084
2160
  action: "added"
2085
2161
  };
2086
2162
  }
2087
- return { source, action: "no-anchor" };
2163
+ return { source: stripped, action: "no-anchor" };
2088
2164
  }
2089
2165
  function ensureSourceMarker(cwd, shell, framework) {
2090
2166
  if (shell === null) {
@@ -2093,7 +2169,7 @@ function ensureSourceMarker(cwd, shell, framework) {
2093
2169
  const file = path7.resolve(cwd, shell);
2094
2170
  const before = readFileSync(file, "utf8");
2095
2171
  const result = ensureMarkerInJsxShell(before, framework);
2096
- if (result.source !== before && result.action === "added") {
2172
+ if (result.source !== before) {
2097
2173
  writeFileSync(file, result.source);
2098
2174
  }
2099
2175
  return { ...result, shell };
@@ -4078,6 +4154,9 @@ async function login(config, onPrompt, deps = {}) {
4078
4154
  return waitForApproval(config, started.pending, deps);
4079
4155
  }
4080
4156
 
4157
+ // src/map-command.ts
4158
+ import { writeFileSync as writeFileSync8 } from "fs";
4159
+
4081
4160
  // src/map/ts-loader.ts
4082
4161
  import { createRequire } from "module";
4083
4162
  import { pathToFileURL } from "url";
@@ -4485,7 +4564,30 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
4485
4564
  "temp",
4486
4565
  "__pycache__"
4487
4566
  ]);
4488
- function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0 }) {
4567
+ var SKIP_IS_DEFINITIONAL = /* @__PURE__ */ new Set(["node_modules", ".git"]);
4568
+ var PROBE_BUDGET = 4096;
4569
+ function probeForSource(dir, budget) {
4570
+ let entries;
4571
+ try {
4572
+ entries = readdirSync5(dir, { withFileTypes: true });
4573
+ } catch {
4574
+ return "unknown";
4575
+ }
4576
+ const dirs = [];
4577
+ for (const e of entries) {
4578
+ if (budget.left-- <= 0) return "unknown";
4579
+ if (e.isDirectory()) {
4580
+ if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join13(dir, e.name));
4581
+ } else if (isSourceFile(e.name)) return true;
4582
+ else if (e.isSymbolicLink()) return "unknown";
4583
+ }
4584
+ for (const sub of dirs) {
4585
+ const found = probeForSource(sub, budget);
4586
+ if (found !== false) return found;
4587
+ }
4588
+ return false;
4589
+ }
4590
+ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
4489
4591
  let key;
4490
4592
  try {
4491
4593
  key = realpathSync(dir);
@@ -4508,9 +4610,18 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
4508
4610
  }
4509
4611
  entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
4510
4612
  for (const e of entries) {
4511
- if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".") continue;
4613
+ if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".") {
4614
+ const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
4615
+ if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
4616
+ else if (probeable && e.isDirectory()) {
4617
+ const held = probeForSource(join13(dir, e.name), probeBudget);
4618
+ if (held === true) stats.skippedWithSource.push(join13(dir, e.name));
4619
+ else if (held === "unknown") stats.skippedUnknown++;
4620
+ }
4621
+ continue;
4622
+ }
4512
4623
  const full = join13(dir, e.name);
4513
- if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
4624
+ if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
4514
4625
  else if (e.isSymbolicLink()) {
4515
4626
  let st, real;
4516
4627
  try {
@@ -4524,7 +4635,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
4524
4635
  if (st.isDirectory() || isSourceFile(e.name)) stats.unwalked++;
4525
4636
  continue;
4526
4637
  }
4527
- if (st.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
4638
+ if (st.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
4528
4639
  else if (st.isFile() && isSourceFile(e.name)) {
4529
4640
  out.push(full);
4530
4641
  stats.discovered++;
@@ -4847,12 +4958,16 @@ var FS_PACKAGES = ["fs-extra", "graceful-fs", "memfs"];
4847
4958
  var isFsPackage = (pkg) => /^node:fs(\/promises)?$/.test(pkg) || FS_PACKAGES.includes(pkg);
4848
4959
  var EXEC_PACKAGES = ["execa", "cross-spawn", "shelljs", "zx"];
4849
4960
  var isExecPackage = (pkg) => pkg === "node:child_process" || EXEC_PACKAGES.includes(pkg);
4961
+ var DESERIALIZE_CALLS = /^unserialize$/;
4962
+ var DESERIALIZE_PACKAGES = ["node-serialize"];
4963
+ var isDeserializePackage = (pkg) => DESERIALIZE_PACKAGES.includes(pkg);
4850
4964
  function recognizedSinkKinds(pkg) {
4851
4965
  const kinds = [];
4852
4966
  if (isDbPackage(pkg)) kinds.push("db");
4853
4967
  if (isFsPackage(pkg)) kinds.push("fs");
4854
4968
  if (isExecPackage(pkg)) kinds.push("exec");
4855
4969
  if (isHttpPackage(pkg)) kinds.push("http");
4970
+ if (isDeserializePackage(pkg)) kinds.push("eval");
4856
4971
  return kinds;
4857
4972
  }
4858
4973
  function collectLocalSinks(sf, ts, bindings, ctx) {
@@ -4966,6 +5081,9 @@ function directSinks(node, ts, bindings, ctx) {
4966
5081
  if (EXEC_CALLS.test(method) && b.pkg && isExecPackage(b.pkg)) {
4967
5082
  push({ kind: "exec", package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
4968
5083
  }
5084
+ if (DESERIALIZE_CALLS.test(method) && b.pkg && isDeserializePackage(b.pkg)) {
5085
+ push({ kind: "eval", provider: b.root, package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
5086
+ }
4969
5087
  if (HTTP_MEMBER_METHODS.has(method)) {
4970
5088
  if (b.pkg && isHttpPackage(b.pkg)) {
4971
5089
  push({ kind: "http", provider: b.root, package: b.pkg, op: method, attribution: "import", ...spanOf(n) });
@@ -4993,6 +5111,9 @@ function directSinks(node, ts, bindings, ctx) {
4993
5111
  push({ kind: "exec", package: pkg, op: name, attribution: "import", ...spanOf(n) });
4994
5112
  }
4995
5113
  if (name === "eval" && trueGlobal) push({ kind: "eval", op: "eval", attribution: "global", ...spanOf(n) });
5114
+ if (DESERIALIZE_CALLS.test(name) && pkg && isDeserializePackage(pkg)) {
5115
+ push({ kind: "eval", package: pkg, op: name, attribution: "import", ...spanOf(n) });
5116
+ }
4996
5117
  }
4997
5118
  }
4998
5119
  if (ts.isNewExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "Function" && !bindings.locals.has("Function") && !isShadowedByEnclosingBinding(n, "Function", ts)) {
@@ -5069,6 +5190,7 @@ var CANDIDATE_FAMILIES = {
5069
5190
  };
5070
5191
  function argumentRoleOf(sinkKind, method, index, total = 0) {
5071
5192
  if (sinkKind === "eval" && method === "Function") return index === total - 1 ? "code" : "args";
5193
+ if (sinkKind === "eval" && method === "unserialize") return index === 0 ? "code" : "unknown";
5072
5194
  const table = method ? ARGUMENT_ROLES[sinkKind]?.[method] : void 0;
5073
5195
  return table?.[index] ?? "unknown";
5074
5196
  }
@@ -5349,7 +5471,7 @@ function zodShape(node, ts) {
5349
5471
  }
5350
5472
  return shape;
5351
5473
  }
5352
- var REQ_SOURCES = ["body", "query", "params"];
5474
+ var REQ_SOURCES = ["body", "query", "params", "headers", "cookies", "files"];
5353
5475
  function requestMemberAccesses(params, body, ts, opts = {}) {
5354
5476
  if (!body) return [];
5355
5477
  const out = /* @__PURE__ */ new Map();
@@ -5386,6 +5508,12 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
5386
5508
  if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
5387
5509
  record(n.name.text, sourceOfExpr(n.expression));
5388
5510
  }
5511
+ if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
5512
+ record(n.argumentExpression.text, sourceOfExpr(n.expression));
5513
+ }
5514
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0])) {
5515
+ record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
5516
+ }
5389
5517
  if (ts.isVariableDeclaration(n) && n.initializer) {
5390
5518
  const init = unwrap(n.initializer);
5391
5519
  if (ts.isIdentifier(n.name) && isBodyReadCall(n.initializer)) sourceNames.set(n.name.text, bodyReadSource(n.initializer));
@@ -5410,9 +5538,8 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
5410
5538
  function sourceOfExpr(e) {
5411
5539
  if (isPayloadExpr(e)) return "server-fn-data";
5412
5540
  if (ts.isPropertyAccessExpression(e)) {
5413
- if (e.name.text === "query") return "query";
5414
- if (e.name.text === "params") return "route-param";
5415
- if (e.name.text === "body") return "body";
5541
+ const named = namespaceSource(e.name.text);
5542
+ if (REQ_SOURCES.includes(e.name.text)) return named;
5416
5543
  }
5417
5544
  if (ts.isIdentifier(e)) {
5418
5545
  const recorded = sourceNames.get(e.text);
@@ -5423,6 +5550,9 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
5423
5550
  function namespaceSource(key) {
5424
5551
  if (key === "query") return "query";
5425
5552
  if (key === "params") return "route-param";
5553
+ if (key === "headers") return "header";
5554
+ if (key === "cookies") return "cookie";
5555
+ if (key === "files") return "file";
5426
5556
  return "body";
5427
5557
  }
5428
5558
  function bodyReadSource(init) {
@@ -5437,6 +5567,9 @@ function spaceOfKey(key) {
5437
5567
  if (key === "params") return "route-param";
5438
5568
  if (key === "body") return "post";
5439
5569
  if (key === "data" || key === "input" || key === "payload") return "post";
5570
+ if (key === "headers") return "server";
5571
+ if (key === "cookies") return "cookie";
5572
+ if (key === "files") return "files";
5440
5573
  return void 0;
5441
5574
  }
5442
5575
  function linkedFlows(body, params, inputs, sinks, ts) {
@@ -6216,7 +6349,7 @@ async function extractInputMap(cwd, ts, options = {}) {
6216
6349
  } catch {
6217
6350
  }
6218
6351
  const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
6219
- const stats = { discovered: 0, unwalked: 0 };
6352
+ const stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 };
6220
6353
  const files = collectSources(cwd, boundary, { followOutside: options.followSymlinks }, [], /* @__PURE__ */ new Set(), stats);
6221
6354
  const imports = createImportInventory(readPathAliases(cwd));
6222
6355
  const invocations = createInvocationInventory();
@@ -6321,19 +6454,31 @@ async function extractInputMap(cwd, ts, options = {}) {
6321
6454
  if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
6322
6455
  const importList = imports.list();
6323
6456
  const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
6457
+ const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative4(cwd, d));
6324
6458
  const importCoverageGaps = {
6325
6459
  unreadableFiles: failed.length,
6326
6460
  unscannableFiles: importScanFailures,
6327
6461
  unwalkedPaths: stats.unwalked,
6328
- unresolvableImports
6462
+ unresolvableImports,
6463
+ // A directory skipped by NAME that turned out to hold source. It moved no other counter, so before
6464
+ // this an app whose server lived under `vendor/` was indistinguishable from one with no such
6465
+ // directory — and the inventory still claimed to be complete.
6466
+ skippedDirsWithSource,
6467
+ skippedDirsUnsettled: stats.skippedUnknown
6329
6468
  };
6330
- const environmentalGaps = failed.length + importScanFailures + stats.unwalked;
6469
+ const environmentalGaps = failed.length + importScanFailures + stats.unwalked + stats.skippedWithSource.length + stats.skippedUnknown;
6331
6470
  const importsComplete = environmentalGaps === 0 && unresolvableImports === 0;
6332
6471
  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.");
6333
6472
  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.`);
6334
6473
  if (environmentalGaps > 0) {
6335
6474
  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.`);
6336
6475
  }
6476
+ if (stats.skippedWithSource.length > 0) {
6477
+ notes.push(`${stats.skippedWithSource.length} director(ies) skipped by name hold source that was NOT analyzed: ${skippedDirsWithSource.slice(0, 10).join(", ")}${skippedDirsWithSource.length > 10 ? ", \u2026" : ""}. The walk excludes these names because they are usually generated or vendored, which is a guess about where app source lives \u2014 and here the guess was wrong. Code in them can import, and serve, anything. Point the scan at the directory, or move the app source out of it, before reading any package's absence as evidence.`);
6478
+ }
6479
+ if (stats.skippedUnknown > 0) {
6480
+ notes.push(`${stats.skippedUnknown} skipped director(ies) could not be settled either way \u2014 the probe for source hit its budget or the directory could not be read. Counted against completeness deliberately: "we stopped looking" is not the same answer as "nothing there".`);
6481
+ }
6337
6482
  if (unresolvableImports > 0) {
6338
6483
  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.`);
6339
6484
  }
@@ -6416,8 +6561,88 @@ async function buildInputMap(cwd, options = {}) {
6416
6561
  return { map };
6417
6562
  }
6418
6563
 
6564
+ // src/flags.ts
6565
+ function getStringFlag(flags, name) {
6566
+ const value = flags.get(name);
6567
+ return typeof value === "string" ? value : void 0;
6568
+ }
6569
+
6570
+ // src/map-command.ts
6571
+ async function runMap(flags) {
6572
+ const cwd = getStringFlag(flags, "dir") ?? process.cwd();
6573
+ const { map, error } = await buildInputMap(cwd, {
6574
+ followSymlinks: flags.get("follow-symlinks") === true
6575
+ });
6576
+ if (!map) {
6577
+ console.error(`patchstack: ${error}`);
6578
+ return 1;
6579
+ }
6580
+ const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
6581
+ const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
6582
+ const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
6583
+ const c = map.coverage;
6584
+ console.error(
6585
+ `patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
6586
+ );
6587
+ console.error(
6588
+ // All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
6589
+ // simply contain no server entry point (most of a project is client code). Only `skipped` is a
6590
+ // failure to analyse.
6591
+ `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).`
6592
+ );
6593
+ const invoked = map.apiInvocations ?? [];
6594
+ if (invoked.length > 0) {
6595
+ const c2 = map.coverage;
6596
+ const dependency = c2.callsDependency ?? 0;
6597
+ const ambiguous = c2.callsAmbiguous ?? 0;
6598
+ const denominator = dependency + ambiguous;
6599
+ const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
6600
+ console.error(
6601
+ `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.`
6602
+ );
6603
+ }
6604
+ const imported = map.imports ?? [];
6605
+ if (imported.length > 0) {
6606
+ const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
6607
+ console.error(
6608
+ `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.`
6609
+ );
6610
+ }
6611
+ const json = JSON.stringify(map, null, 2);
6612
+ const out = getStringFlag(flags, "out");
6613
+ if (out) {
6614
+ writeFileSync8(out, json);
6615
+ console.error(`patchstack: wrote ${out}`);
6616
+ } else if (flags.get("upload") !== true) {
6617
+ console.log(json);
6618
+ }
6619
+ if (flags.get("upload") === true) {
6620
+ if (map.endpoints.length === 0) {
6621
+ console.error(
6622
+ "patchstack: no server entry points were recognized \u2014 uploading the import inventory and coverage notes anyway, so a vulnerability can still be judged imported or not. Nothing here can decide whether a request reaches it."
6623
+ );
6624
+ }
6625
+ const config = await resolveConfig({
6626
+ cwd,
6627
+ cliSiteUuid: getStringFlag(flags, "site-uuid"),
6628
+ cliEndpoint: getStringFlag(flags, "endpoint")
6629
+ });
6630
+ const outcome = await postInputMap(config, map);
6631
+ if (outcome.result === "stored") {
6632
+ console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
6633
+ } else if (outcome.result === "unchanged") {
6634
+ console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
6635
+ } else if (outcome.result === "skipped") {
6636
+ console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
6637
+ } else {
6638
+ console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
6639
+ }
6640
+ }
6641
+ return 0;
6642
+ }
6643
+
6419
6644
  // src/setup.ts
6420
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
6645
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
6421
6646
  import path11 from "path";
6422
6647
  var SCAN_COMMAND = "patchstack-connect scan";
6423
6648
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
@@ -6486,7 +6711,7 @@ function wireBuildScripts(cwd, packageManager) {
6486
6711
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
6487
6712
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
6488
6713
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
6489
- writeFileSync8(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
6714
+ writeFileSync9(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
6490
6715
  if (build === void 0 || build.trim().length === 0) {
6491
6716
  return {
6492
6717
  changed: true,
@@ -6583,10 +6808,19 @@ Usage:
6583
6808
  the old one must be updated. Not usable in CI
6584
6809
  patchstack-connect help Print this message
6585
6810
 
6811
+ Global:
6812
+ --version Print the installed version of this package and exit
6813
+ --help Print this help and exit
6814
+
6586
6815
  Options (for scan, setup, status, and uninstall):
6587
6816
  --site-uuid <uuid> Override the configured site UUID
6588
6817
  --endpoint <url> Override the API endpoint
6589
6818
  --dry-run (scan only) Show the payload without posting
6819
+ --install-paths (scan only) Also send where each package is installed
6820
+ in the dependency tree (node_modules/lodash, and the
6821
+ same under a workspace that pins its own copy), so an
6822
+ advisory can be matched to the copy your code actually
6823
+ loads. Off by default; never source file paths
6590
6824
 
6591
6825
  Options (for mark-build):
6592
6826
  --dir <path> Build output directory (default: auto-detect
@@ -6651,10 +6885,6 @@ function parseArgs(argv) {
6651
6885
  flags
6652
6886
  };
6653
6887
  }
6654
- function getStringFlag(flags, name) {
6655
- const value = flags.get(name);
6656
- return typeof value === "string" ? value : void 0;
6657
- }
6658
6888
  async function runInit(args) {
6659
6889
  const uuid = args.positional[0];
6660
6890
  if (!uuid) {
@@ -6752,77 +6982,6 @@ async function runLogin(args) {
6752
6982
  `);
6753
6983
  return 1;
6754
6984
  }
6755
- async function runMap(args) {
6756
- const cwd = getStringFlag(args.flags, "dir") ?? process.cwd();
6757
- const { map, error } = await buildInputMap(cwd, {
6758
- followSymlinks: args.flags.get("follow-symlinks") === true
6759
- });
6760
- if (!map) {
6761
- console.error(`patchstack: ${error}`);
6762
- return 1;
6763
- }
6764
- const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
6765
- const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
6766
- const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
6767
- const c = map.coverage;
6768
- console.error(
6769
- `patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
6770
- );
6771
- console.error(
6772
- // All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
6773
- // simply contain no server entry point (most of a project is client code). Only `skipped` is a
6774
- // failure to analyse.
6775
- `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).`
6776
- );
6777
- const invoked = map.apiInvocations ?? [];
6778
- if (invoked.length > 0) {
6779
- const c2 = map.coverage;
6780
- const dependency = c2.callsDependency ?? 0;
6781
- const ambiguous = c2.callsAmbiguous ?? 0;
6782
- const denominator = dependency + ambiguous;
6783
- const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
6784
- console.error(
6785
- `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.`
6786
- );
6787
- }
6788
- const imported = map.imports ?? [];
6789
- if (imported.length > 0) {
6790
- const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
6791
- console.error(
6792
- `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.`
6793
- );
6794
- }
6795
- const json = JSON.stringify(map, null, 2);
6796
- const out = getStringFlag(args.flags, "out");
6797
- if (out) {
6798
- writeFileSync9(out, json);
6799
- console.error(`patchstack: wrote ${out}`);
6800
- } else if (args.flags.get("upload") !== true) {
6801
- console.log(json);
6802
- }
6803
- if (args.flags.get("upload") === true) {
6804
- if (map.endpoints.length === 0) {
6805
- console.error("patchstack: nothing to upload \u2014 no server entry points were detected.");
6806
- return 0;
6807
- }
6808
- const config = await resolveConfig({
6809
- cwd,
6810
- cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
6811
- cliEndpoint: getStringFlag(args.flags, "endpoint")
6812
- });
6813
- const outcome = await postInputMap(config, map);
6814
- if (outcome.result === "stored") {
6815
- console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
6816
- } else if (outcome.result === "unchanged") {
6817
- console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
6818
- } else if (outcome.result === "skipped") {
6819
- console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
6820
- } else {
6821
- console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
6822
- }
6823
- }
6824
- return 0;
6825
- }
6826
6985
  async function runScan(args, options = {}) {
6827
6986
  const dryRun = args.flags.get("dry-run") === true;
6828
6987
  const config = await resolveConfig({
@@ -6834,10 +6993,17 @@ async function runScan(args, options = {}) {
6834
6993
  for (const warning of manifest.warnings ?? []) {
6835
6994
  console.warn(`patchstack: ${warning}`);
6836
6995
  }
6837
- const { payload, stats } = buildWirePayload(manifest);
6996
+ const installPaths = args.flags.get("install-paths") === true;
6997
+ const { payload, stats } = buildWirePayload(manifest, { installPaths });
6838
6998
  console.log(
6839
6999
  `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
6840
7000
  );
7001
+ if (installPaths) {
7002
+ const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
7003
+ console.log(
7004
+ payload.installPathsComplete ? `Including each package's install location in the dependency tree (--install-paths), for all ${located}.` : `Including install locations (--install-paths), for ${located} of ${payload.packages.length} \u2014 this lockfile format does not record them for the rest, which will be reported as "not recorded" rather than "not installed there".`
7005
+ );
7006
+ }
6841
7007
  console.log(
6842
7008
  `Reporting under the ${config.environment} environment (override with PATCHSTACK_ENVIRONMENT).`
6843
7009
  );
@@ -7330,7 +7496,7 @@ async function runMarkBuild(args) {
7330
7496
  after = ensured.html;
7331
7497
  }
7332
7498
  if (after !== before) {
7333
- writeFileSync9(file, after);
7499
+ writeFileSync10(file, after);
7334
7500
  marked += 1;
7335
7501
  }
7336
7502
  }
@@ -7340,8 +7506,21 @@ async function runMarkBuild(args) {
7340
7506
  );
7341
7507
  return 0;
7342
7508
  }
7509
+ function packageVersion() {
7510
+ try {
7511
+ const require2 = createRequire2(import.meta.url);
7512
+ const manifest = require2("../package.json");
7513
+ return typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : "unknown";
7514
+ } catch {
7515
+ return "unknown";
7516
+ }
7517
+ }
7343
7518
  async function main() {
7344
7519
  const args = parseArgs(process.argv);
7520
+ if (args.flags.has("version") || args.command === "version") {
7521
+ console.log(packageVersion());
7522
+ return 0;
7523
+ }
7345
7524
  if (args.flags.has("help") || args.command === "help" || args.command === null) {
7346
7525
  console.log(HELP);
7347
7526
  return 0;
@@ -7368,7 +7547,7 @@ async function main() {
7368
7547
  case "setup":
7369
7548
  return runSetup(args);
7370
7549
  case "map":
7371
- return runMap(args);
7550
+ return runMap(args.flags);
7372
7551
  case "login":
7373
7552
  return runLogin(args);
7374
7553
  default: