@patchstack/connect 0.3.17 → 0.3.18

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
@@ -934,6 +934,14 @@ function buildEndpointUrl(base, siteUuid) {
934
934
  const trimmed = base.replace(/\/$/, "");
935
935
  return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
936
936
  }
937
+ function buildRulesUrl(manifestEndpoint, siteUuid) {
938
+ const url = new URL(manifestEndpoint);
939
+ const path11 = url.pathname.replace(/\/$/, "");
940
+ url.pathname = path11.endsWith("/manifest") ? `${path11.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
941
+ url.search = "";
942
+ url.hash = "";
943
+ return url.toString();
944
+ }
937
945
  function buildClaimUrl(endpoint, siteUuid) {
938
946
  const origin = new URL(endpoint).origin;
939
947
  return `${origin}/monitor/claim?site=${encodeURIComponent(siteUuid)}`;
@@ -1004,8 +1012,212 @@ function isTimeoutError(cause) {
1004
1012
  return false;
1005
1013
  }
1006
1014
 
1015
+ // src/demo.ts
1016
+ import { readFile as readFile6 } from "fs/promises";
1017
+ import { join } from "path";
1018
+ var NODE_SERIALIZE_DEMO = {
1019
+ name: "node-serialize",
1020
+ packageName: "node-serialize",
1021
+ packageVersion: "0.0.4",
1022
+ ruleId: 18843,
1023
+ maliciousBody: {
1024
+ text: "_$$ND_FUNC$$_function(){require('child_process').exec('id')}()"
1025
+ },
1026
+ benignBody: { text: "buy milk" }
1027
+ };
1028
+ var SCENARIOS = /* @__PURE__ */ new Map([[NODE_SERIALIZE_DEMO.name, NODE_SERIALIZE_DEMO]]);
1029
+ var DemoError = class extends Error {
1030
+ constructor(message) {
1031
+ super(message);
1032
+ this.name = "DemoError";
1033
+ }
1034
+ };
1035
+ var DEMO_INSTALL_COMMANDS = {
1036
+ npm: "npm install --save-exact node-serialize@0.0.4",
1037
+ pnpm: "pnpm add --save-exact node-serialize@0.0.4",
1038
+ yarn: "yarn add --exact node-serialize@0.0.4",
1039
+ bun: "bun add --exact node-serialize@0.0.4"
1040
+ };
1041
+ var DEMO_REMOVE_COMMANDS = {
1042
+ npm: "npm uninstall node-serialize",
1043
+ pnpm: "pnpm remove node-serialize",
1044
+ yarn: "yarn remove node-serialize",
1045
+ bun: "bun remove node-serialize"
1046
+ };
1047
+ function resolveDemoScenario(name) {
1048
+ if (!name) {
1049
+ throw new DemoError(
1050
+ "A demo scenario is required. Available scenario: node-serialize.\nUsage: patchstack-connect demo node-serialize"
1051
+ );
1052
+ }
1053
+ const scenario = SCENARIOS.get(name);
1054
+ if (!scenario) {
1055
+ throw new DemoError(`Unknown demo scenario "${name}". Available scenario: node-serialize.`);
1056
+ }
1057
+ return scenario;
1058
+ }
1059
+ async function collectDemoDependency(cwd, scenario) {
1060
+ const manifest = await scanLockfile(cwd);
1061
+ const versions = [
1062
+ ...new Set(
1063
+ manifest.packages.filter((entry) => entry.name === scenario.packageName).map((entry) => entry.version)
1064
+ )
1065
+ ].sort();
1066
+ return { ready: versions.includes(scenario.packageVersion), versions };
1067
+ }
1068
+ async function inspectDemoDependency(cwd, scenario) {
1069
+ try {
1070
+ return await collectDemoDependency(cwd, scenario);
1071
+ } catch {
1072
+ return { ready: false, versions: [] };
1073
+ }
1074
+ }
1075
+ async function assertDemoDependency(cwd, scenario) {
1076
+ const { ready, versions } = await collectDemoDependency(cwd, scenario);
1077
+ if (ready) return;
1078
+ const found = versions.length > 0 ? ` Found: ${versions.join(", ")}.` : "";
1079
+ throw new DemoError(
1080
+ `${scenario.packageName}@${scenario.packageVersion} is not installed in the lockfile.${found}
1081
+ Add this deliberately vulnerable package first: npm install --save-exact ${scenario.packageName}@${scenario.packageVersion}`
1082
+ );
1083
+ }
1084
+ async function readPersistedSiteUuid(cwd) {
1085
+ try {
1086
+ const value = JSON.parse(await readFile6(join(cwd, ".patchstackrc.json"), "utf8")).siteUuid;
1087
+ return typeof value === "string" && value.length > 0 ? value : null;
1088
+ } catch {
1089
+ return null;
1090
+ }
1091
+ }
1092
+ async function assertPersistedSiteUuid(cwd, expectedUuid) {
1093
+ const persisted = await readPersistedSiteUuid(cwd);
1094
+ if (persisted === null) {
1095
+ throw new DemoError(
1096
+ "The demo requires the Host-connected .patchstackrc.json. Connect Patchstack in Bolt first, then try again."
1097
+ );
1098
+ }
1099
+ if (persisted !== expectedUuid) {
1100
+ throw new DemoError(
1101
+ `The active site UUID (${expectedUuid}) does not match .patchstackrc.json (${String(persisted)}). Remove the UUID override or reconnect this Bolt project before running the demo.`
1102
+ );
1103
+ }
1104
+ }
1105
+ function shellQuote(value) {
1106
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1107
+ }
1108
+ function renderDemoGuide(state) {
1109
+ const { scenario } = state;
1110
+ const install = DEMO_INSTALL_COMMANDS[state.packageManager].replace(
1111
+ "node-serialize@0.0.4",
1112
+ `${scenario.packageName}@${scenario.packageVersion}`
1113
+ );
1114
+ const remove = DEMO_REMOVE_COMMANDS[state.packageManager].replace(
1115
+ "node-serialize",
1116
+ scenario.packageName
1117
+ );
1118
+ const demo = state.url === "http://localhost:3000/api/tasks" ? `npx @patchstack/connect demo ${scenario.name}` : `npx @patchstack/connect demo ${scenario.name} --url ${shellQuote(state.url)}`;
1119
+ const siteReady = state.siteUuid !== null;
1120
+ const envReady = state.environment === "production";
1121
+ const dependencyDetail = state.dependency.ready ? `${scenario.packageName}@${scenario.packageVersion} is in the lockfile` : state.dependency.versions.length > 0 ? `found ${scenario.packageName}@${state.dependency.versions.join(", ")}; ${scenario.packageVersion} is required` : `${scenario.packageName}@${scenario.packageVersion} is not in the lockfile`;
1122
+ let next;
1123
+ if (!siteReady) {
1124
+ next = "Click \u201CConnect Patchstack\u201D in Bolt, then run this guide again.";
1125
+ } else if (!envReady) {
1126
+ next = "Unset PATCHSTACK_ENVIRONMENT (or set it to production), then run this guide again.";
1127
+ } else if (!state.dependency.ready) {
1128
+ next = install;
1129
+ } else {
1130
+ next = demo;
1131
+ }
1132
+ return [
1133
+ `Patchstack demo guide \u2014 ${scenario.name}`,
1134
+ "",
1135
+ "Goal: prove that a live Patchstack virtual patch blocks the vulnerable payload while normal traffic still succeeds.",
1136
+ "Deployment required: no. Keep the app running locally; the connector and generated guard contact Patchstack\u2019s production API.",
1137
+ "",
1138
+ `1. ${siteReady ? "\u2713" : "\u25CB"} Connect this project from Bolt\u2019s Host account dropdown`,
1139
+ siteReady ? ` Ready \u2014 .patchstackrc.json contains site ${state.siteUuid}.` : " Click \u201CConnect Patchstack\u201D in Bolt. This creates .patchstackrc.json; no separate CLI login is needed.",
1140
+ "",
1141
+ `2. ${state.dependency.ready ? "\u2713" : "\u25CB"} Add the deliberately vulnerable demo dependency`,
1142
+ ` ${dependencyDetail}.`,
1143
+ ` ${install}`,
1144
+ " Do not import or execute this package; its lockfile entry is enough for detection.",
1145
+ "",
1146
+ "3. \u25CB Run the production-backed demo setup",
1147
+ ` ${demo}`,
1148
+ ...envReady ? [] : [" Blocked for now: this walkthrough needs PATCHSTACK_ENVIRONMENT=production."],
1149
+ ` This scans the npm manifest, waits for live rule ${scenario.ruleId}, installs the server guard, verifies its wiring, and prints two test requests.`,
1150
+ " It does not start the server or send the requests.",
1151
+ "",
1152
+ "4. \u25CB Restart the local dev server",
1153
+ " Restart after the demo command so the application loads the generated guard.",
1154
+ "",
1155
+ "5. \u25CB Run both requests printed by the demo command",
1156
+ ` Exploit: expect HTTP 403 with Patchstack rule ${scenario.ruleId}.`,
1157
+ " Control (\u201Cbuy milk\u201D): expect HTTP 201. This proves normal traffic is not blocked.",
1158
+ "",
1159
+ "6. \u25CB Clean up the throwaway vulnerability after the demonstration",
1160
+ ` ${remove}`,
1161
+ " Re-run `npx @patchstack/connect scan` so the clean manifest is reported.",
1162
+ "",
1163
+ `Next: ${next}`
1164
+ ].join("\n");
1165
+ }
1166
+ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {}) {
1167
+ const timeoutMs = options.timeoutMs ?? 3e4;
1168
+ const pollIntervalMs = options.pollIntervalMs ?? 1e3;
1169
+ const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
1170
+ const fetchFn = options.fetchFn ?? fetch;
1171
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1172
+ const now = options.now ?? Date.now;
1173
+ const started = now();
1174
+ const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
1175
+ let lastDetail = "the rule list was empty";
1176
+ do {
1177
+ try {
1178
+ const response = await fetchFn(rulesUrl, {
1179
+ headers: { Accept: "application/json", "User-Agent": "@patchstack/connect" },
1180
+ signal: AbortSignal.timeout(requestTimeoutMs)
1181
+ });
1182
+ if (response.ok) {
1183
+ const data = await response.json();
1184
+ const rules = Array.isArray(data.firewall) ? data.firewall : [];
1185
+ const matched = rules.find((rule) => String(rule.id) === String(scenario.ruleId));
1186
+ if (matched) return matched;
1187
+ lastDetail = `${rules.length} firewall rule(s) returned without rule ${scenario.ruleId}`;
1188
+ } else {
1189
+ lastDetail = `the rules API returned HTTP ${response.status}`;
1190
+ }
1191
+ } catch (error) {
1192
+ lastDetail = `the rules request failed: ${error.message}`;
1193
+ }
1194
+ const remaining = timeoutMs - (now() - started);
1195
+ if (remaining <= 0) break;
1196
+ await sleep(Math.min(pollIntervalMs, remaining));
1197
+ } while (now() - started <= timeoutMs);
1198
+ throw new DemoError(
1199
+ `Timed out waiting for Patchstack rule ${scenario.ruleId} for site ${siteUuid}; ${lastDetail}.`
1200
+ );
1201
+ }
1202
+ function renderDemoTestCommands(url, scenario) {
1203
+ const shellUrl = shellQuote(url);
1204
+ const command = (body) => {
1205
+ const encoded = Buffer.from(JSON.stringify(body)).toString("base64");
1206
+ return `node -e "process.stdout.write(Buffer.from('${encoded}','base64'))" | curl -i -X POST --url ${shellUrl} -H "Content-Type: application/json" --data-binary @-`;
1207
+ };
1208
+ return [
1209
+ "Restart the dev server so it loads the generated guard, then run:",
1210
+ "",
1211
+ `Blocked exploit (expect HTTP 403 and rule ${scenario.ruleId}):`,
1212
+ command(scenario.maliciousBody),
1213
+ "",
1214
+ "Benign request (expect HTTP 201):",
1215
+ command(scenario.benignBody)
1216
+ ].join("\n");
1217
+ }
1218
+
1007
1219
  // src/config.ts
1008
- import { readFile as readFile6, writeFile } from "fs/promises";
1220
+ import { readFile as readFile7, writeFile } from "fs/promises";
1009
1221
  import path6 from "path";
1010
1222
  var CONFIG_FILENAME = ".patchstackrc.json";
1011
1223
  var DEFAULT_ENVIRONMENT = "production";
@@ -1057,7 +1269,7 @@ async function readConfigFile(cwd) {
1057
1269
  const target = path6.join(cwd, CONFIG_FILENAME);
1058
1270
  let raw;
1059
1271
  try {
1060
- raw = await readFile6(target, "utf8");
1272
+ raw = await readFile7(target, "utf8");
1061
1273
  } catch (err) {
1062
1274
  if (err.code === "ENOENT") {
1063
1275
  return {};
@@ -1663,13 +1875,13 @@ function renderGuideChecklist(state, useColor) {
1663
1875
 
1664
1876
  // src/protect/install/util.ts
1665
1877
  import { readFileSync as readFileSync3, existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
1666
- import { join, dirname } from "path";
1878
+ import { join as join2, dirname } from "path";
1667
1879
  import { fileURLToPath } from "url";
1668
1880
  var read = (p) => readFileSync3(p, "utf8");
1669
1881
  var log = (msg) => console.log(`patchstack protect: ${msg}`);
1670
1882
  function hasDependency(cwd, name) {
1671
1883
  try {
1672
- const pkg = JSON.parse(read(join(cwd, "package.json")));
1884
+ const pkg = JSON.parse(read(join2(cwd, "package.json")));
1673
1885
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }[name]);
1674
1886
  } catch {
1675
1887
  return false;
@@ -1678,7 +1890,7 @@ function hasDependency(cwd, name) {
1678
1890
  var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
1679
1891
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1680
1892
  function bakeSiteUuid(cwd, guardRelPath) {
1681
- const rc = join(cwd, ".patchstackrc.json");
1893
+ const rc = join2(cwd, ".patchstackrc.json");
1682
1894
  if (!existsSync4(rc)) {
1683
1895
  log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1684
1896
  return false;
@@ -1694,7 +1906,7 @@ function bakeSiteUuid(cwd, guardRelPath) {
1694
1906
  log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1695
1907
  return false;
1696
1908
  }
1697
- const guardPath = join(cwd, guardRelPath);
1909
+ const guardPath = join2(cwd, guardRelPath);
1698
1910
  if (!existsSync4(guardPath)) return false;
1699
1911
  const source = read(guardPath);
1700
1912
  if (!source.includes(SITE_UUID_PLACEHOLDER)) {
@@ -1707,19 +1919,19 @@ function bakeSiteUuid(cwd, guardRelPath) {
1707
1919
  }
1708
1920
  var HERE = dirname(fileURLToPath(import.meta.url));
1709
1921
  function templatesDir() {
1710
- const builtLayout = join(HERE, "protect", "templates");
1922
+ const builtLayout = join2(HERE, "protect", "templates");
1711
1923
  const candidates = [
1712
1924
  builtLayout,
1713
- join(HERE, "..", "templates"),
1925
+ join2(HERE, "..", "templates"),
1714
1926
  // source: src/protect/install/ → src/protect/templates
1715
- join(HERE, "templates")
1927
+ join2(HERE, "templates")
1716
1928
  ];
1717
1929
  return candidates.find((p) => existsSync4(p)) ?? builtLayout;
1718
1930
  }
1719
1931
 
1720
1932
  // src/protect/install/adapters/tanstack-supabase.ts
1721
1933
  import { writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync, copyFileSync } from "fs";
1722
- import { join as join2 } from "path";
1934
+ import { join as join3 } from "path";
1723
1935
  var CLIENT_TUNNEL = [
1724
1936
  "",
1725
1937
  " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
@@ -1788,7 +2000,7 @@ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
1788
2000
  return s.replace(insertBefore, block + "\n\n" + insertBefore);
1789
2001
  }
1790
2002
  function detect(cwd) {
1791
- const pkgPath = join2(cwd, "package.json");
2003
+ const pkgPath = join3(cwd, "package.json");
1792
2004
  if (!existsSync5(pkgPath)) return false;
1793
2005
  let pkg;
1794
2006
  try {
@@ -1797,22 +2009,22 @@ function detect(cwd) {
1797
2009
  return false;
1798
2010
  }
1799
2011
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1800
- return Boolean(deps["@tanstack/react-start"]) && existsSync5(join2(cwd, "src/start.ts")) && existsSync5(join2(cwd, "src/integrations/supabase/client.ts"));
2012
+ return Boolean(deps["@tanstack/react-start"]) && existsSync5(join3(cwd, "src/start.ts")) && existsSync5(join3(cwd, "src/integrations/supabase/client.ts"));
1801
2013
  }
1802
2014
  var GUARD_FILE = "src/integrations/patchstack/guard.ts";
1803
2015
  function scaffold(cwd, opts) {
1804
2016
  const templates = templatesDir();
1805
- const dst = join2(cwd, "src/integrations/patchstack");
2017
+ const dst = join3(cwd, "src/integrations/patchstack");
1806
2018
  mkdirSync(dst, { recursive: true });
1807
- copyFileSync(join2(templates, "guard.ts"), join2(dst, "guard.ts"));
2019
+ copyFileSync(join3(templates, "guard.ts"), join3(dst, "guard.ts"));
1808
2020
  const changed = [GUARD_FILE];
1809
- const rulesDst = join2(dst, "rules.json");
2021
+ const rulesDst = join3(dst, "rules.json");
1810
2022
  if (opts.demo) {
1811
- copyFileSync(join2(templates, "demo-rules.json"), rulesDst);
2023
+ copyFileSync(join3(templates, "demo-rules.json"), rulesDst);
1812
2024
  changed.push("src/integrations/patchstack/rules.json");
1813
2025
  log("scaffolded guard.ts + rules.json (demo sample rule set)");
1814
2026
  } else if (!existsSync5(rulesDst)) {
1815
- copyFileSync(join2(templates, "rules.json"), rulesDst);
2027
+ copyFileSync(join3(templates, "rules.json"), rulesDst);
1816
2028
  changed.push("src/integrations/patchstack/rules.json");
1817
2029
  log("scaffolded guard.ts + rules.json (starter rules)");
1818
2030
  } else {
@@ -1821,7 +2033,7 @@ function scaffold(cwd, opts) {
1821
2033
  return changed;
1822
2034
  }
1823
2035
  function patchClient(cwd) {
1824
- const p = join2(cwd, "src/integrations/supabase/client.ts");
2036
+ const p = join3(cwd, "src/integrations/supabase/client.ts");
1825
2037
  const s = read(p);
1826
2038
  if (s.includes("x-ps-target")) {
1827
2039
  log("client.ts already wired");
@@ -1837,7 +2049,7 @@ function patchClient(cwd) {
1837
2049
  return true;
1838
2050
  }
1839
2051
  function patchStart(cwd) {
1840
- const p = join2(cwd, "src/start.ts");
2052
+ const p = join3(cwd, "src/start.ts");
1841
2053
  let s = read(p);
1842
2054
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
1843
2055
  const exportAnchor = "export const startInstance";
@@ -1884,9 +2096,9 @@ function wire(cwd, opts) {
1884
2096
  return { ok: true, changed: [...new Set(changed)] };
1885
2097
  }
1886
2098
  function verify(cwd) {
1887
- const guardPath = join2(cwd, GUARD_FILE);
1888
- const clientPath = join2(cwd, "src/integrations/supabase/client.ts");
1889
- const startPath = join2(cwd, "src/start.ts");
2099
+ const guardPath = join3(cwd, GUARD_FILE);
2100
+ const clientPath = join3(cwd, "src/integrations/supabase/client.ts");
2101
+ const startPath = join3(cwd, "src/start.ts");
1890
2102
  const guard = existsSync5(guardPath) ? read(guardPath) : "";
1891
2103
  const client = existsSync5(clientPath) ? read(clientPath) : "";
1892
2104
  const start = existsSync5(startPath) ? read(startPath) : "";
@@ -1908,10 +2120,10 @@ var tanstackSupabaseAdapter = {
1908
2120
 
1909
2121
  // src/protect/install/adapters/next.ts
1910
2122
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2 } from "fs";
1911
- import { join as join3 } from "path";
2123
+ import { join as join4 } from "path";
1912
2124
  function hasNextDep(cwd) {
1913
2125
  try {
1914
- const pkg = JSON.parse(read(join3(cwd, "package.json")));
2126
+ const pkg = JSON.parse(read(join4(cwd, "package.json")));
1915
2127
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
1916
2128
  } catch {
1917
2129
  return false;
@@ -1920,9 +2132,9 @@ function hasNextDep(cwd) {
1920
2132
  function middlewareInfo(cwd) {
1921
2133
  const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
1922
2134
  for (const rel of candidates) {
1923
- if (existsSync6(join3(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
2135
+ if (existsSync6(join4(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
1924
2136
  }
1925
- const useSrc = existsSync6(join3(cwd, "src"));
2137
+ const useSrc = existsSync6(join4(cwd, "src"));
1926
2138
  return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
1927
2139
  }
1928
2140
  function detect2(cwd) {
@@ -1934,15 +2146,15 @@ function rulesFile(relDir) {
1934
2146
  function wire2(cwd, opts) {
1935
2147
  const templates = templatesDir();
1936
2148
  const mw = middlewareInfo(cwd);
1937
- const dir = join3(cwd, mw.relDir === "." ? "" : mw.relDir);
2149
+ const dir = join4(cwd, mw.relDir === "." ? "" : mw.relDir);
1938
2150
  mkdirSync2(dir, { recursive: true });
1939
- const rulesDst = join3(dir, "patchstack.rules.json");
2151
+ const rulesDst = join4(dir, "patchstack.rules.json");
1940
2152
  const changed = [];
1941
2153
  if (opts.demo || !existsSync6(rulesDst)) {
1942
- copyFileSync2(join3(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2154
+ copyFileSync2(join4(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
1943
2155
  changed.push(rulesFile(mw.relDir));
1944
2156
  }
1945
- const mwPath = join3(cwd, mw.relFile);
2157
+ const mwPath = join4(cwd, mw.relFile);
1946
2158
  const existing = mw.exists ? read(mwPath) : "";
1947
2159
  if (mw.exists && !existing.includes("patchstack-next")) {
1948
2160
  log(
@@ -1950,7 +2162,7 @@ function wire2(cwd, opts) {
1950
2162
  );
1951
2163
  return { ok: true, changed };
1952
2164
  }
1953
- copyFileSync2(join3(templates, "next-middleware.ts"), mwPath);
2165
+ copyFileSync2(join4(templates, "next-middleware.ts"), mwPath);
1954
2166
  if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
1955
2167
  changed.push(mw.relFile);
1956
2168
  log(mw.exists ? `refreshed ${mw.relFile} (Patchstack middleware)` : `scaffolded ${mw.relFile} (Patchstack middleware)`);
@@ -1958,8 +2170,8 @@ function wire2(cwd, opts) {
1958
2170
  }
1959
2171
  function verify2(cwd) {
1960
2172
  const mw = middlewareInfo(cwd);
1961
- const present = mw.exists && read(join3(cwd, mw.relFile)).includes("patchstack-next");
1962
- const rulesPresent = existsSync6(join3(cwd, rulesFile(mw.relDir)));
2173
+ const present = mw.exists && read(join4(cwd, mw.relFile)).includes("patchstack-next");
2174
+ const rulesPresent = existsSync6(join4(cwd, rulesFile(mw.relDir)));
1963
2175
  return {
1964
2176
  wired: present && rulesPresent,
1965
2177
  checks: [
@@ -1977,42 +2189,42 @@ var nextAdapter = {
1977
2189
  };
1978
2190
 
1979
2191
  // src/protect/install/adapters/sveltekit.ts
1980
- import { join as join5 } from "path";
2192
+ import { join as join6 } from "path";
1981
2193
 
1982
2194
  // src/protect/install/seam.ts
1983
2195
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, copyFileSync as copyFileSync3 } from "fs";
1984
- import { join as join4, dirname as dirname2 } from "path";
2196
+ import { join as join5, dirname as dirname2 } from "path";
1985
2197
  function rulesRel(seamRel) {
1986
2198
  const d = dirname2(seamRel);
1987
2199
  return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
1988
2200
  }
1989
2201
  function wireSeam(cwd, opts, spec) {
1990
2202
  const templates = templatesDir();
1991
- const existing = spec.candidates.find((c) => existsSync7(join4(cwd, c)));
2203
+ const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
1992
2204
  const seamRel = existing ?? spec.target;
1993
- mkdirSync3(dirname2(join4(cwd, seamRel)), { recursive: true });
1994
- const rulesDst = join4(cwd, rulesRel(seamRel));
2205
+ mkdirSync3(dirname2(join5(cwd, seamRel)), { recursive: true });
2206
+ const rulesDst = join5(cwd, rulesRel(seamRel));
1995
2207
  const changed = [];
1996
2208
  if (opts.demo || !existsSync7(rulesDst)) {
1997
- copyFileSync3(join4(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2209
+ copyFileSync3(join5(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
1998
2210
  changed.push(rulesRel(seamRel));
1999
2211
  }
2000
- const current = existing ? read(join4(cwd, existing)) : "";
2212
+ const current = existing ? read(join5(cwd, existing)) : "";
2001
2213
  if (existing && !current.includes(spec.marker)) {
2002
2214
  log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
2003
2215
  return { ok: true, changed };
2004
2216
  }
2005
- copyFileSync3(join4(templates, spec.templateName), join4(cwd, seamRel));
2217
+ copyFileSync3(join5(templates, spec.templateName), join5(cwd, seamRel));
2006
2218
  if (!opts.demo) bakeSiteUuid(cwd, seamRel);
2007
2219
  changed.push(seamRel);
2008
2220
  log(existing ? `refreshed ${seamRel}` : `scaffolded ${seamRel}`);
2009
2221
  return { ok: true, changed: [...new Set(changed)] };
2010
2222
  }
2011
2223
  function verifySeam(cwd, spec) {
2012
- const existing = spec.candidates.find((c) => existsSync7(join4(cwd, c)));
2224
+ const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
2013
2225
  const seamRel = existing ?? spec.target;
2014
- const present = existing ? read(join4(cwd, existing)).includes(spec.marker) : false;
2015
- const rulesPresent = existsSync7(join4(cwd, rulesRel(seamRel)));
2226
+ const present = existing ? read(join5(cwd, existing)).includes(spec.marker) : false;
2227
+ const rulesPresent = existsSync7(join5(cwd, rulesRel(seamRel)));
2016
2228
  return {
2017
2229
  wired: present && rulesPresent,
2018
2230
  checks: [
@@ -2033,7 +2245,7 @@ var SPEC = {
2033
2245
  };
2034
2246
  function detect3(cwd) {
2035
2247
  try {
2036
- const pkg = JSON.parse(read(join5(cwd, "package.json")));
2248
+ const pkg = JSON.parse(read(join6(cwd, "package.json")));
2037
2249
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
2038
2250
  } catch {
2039
2251
  return false;
@@ -2054,7 +2266,7 @@ var sveltekitAdapter = {
2054
2266
  };
2055
2267
 
2056
2268
  // src/protect/install/adapters/astro.ts
2057
- import { join as join6 } from "path";
2269
+ import { join as join7 } from "path";
2058
2270
  var SPEC2 = {
2059
2271
  templateName: "astro-middleware.ts",
2060
2272
  candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
@@ -2065,7 +2277,7 @@ var SPEC2 = {
2065
2277
  };
2066
2278
  function detect4(cwd) {
2067
2279
  try {
2068
- const pkg = JSON.parse(read(join6(cwd, "package.json")));
2280
+ const pkg = JSON.parse(read(join7(cwd, "package.json")));
2069
2281
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
2070
2282
  } catch {
2071
2283
  return false;
@@ -2087,9 +2299,9 @@ var astroAdapter = {
2087
2299
 
2088
2300
  // src/protect/install/find-app.ts
2089
2301
  import { readFileSync as readFileSync4, existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2090
- import { join as join7, relative, dirname as dirname3 } from "path";
2302
+ import { join as join8, relative, dirname as dirname3 } from "path";
2091
2303
  function findAppInstance(cwd, re) {
2092
- const root = existsSync8(join7(cwd, "src")) ? join7(cwd, "src") : cwd;
2304
+ const root = existsSync8(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
2093
2305
  let hit = null;
2094
2306
  const walk2 = (dir) => {
2095
2307
  if (hit) return;
@@ -2102,7 +2314,7 @@ function findAppInstance(cwd, re) {
2102
2314
  for (const name of entries) {
2103
2315
  if (hit) return;
2104
2316
  if (name === "node_modules" || name === "patchstack" || name.startsWith(".")) continue;
2105
- const p = join7(dir, name);
2317
+ const p = join8(dir, name);
2106
2318
  let st;
2107
2319
  try {
2108
2320
  st = statSync3(p);
@@ -2125,34 +2337,36 @@ function findAppInstance(cwd, re) {
2125
2337
  walk2(root);
2126
2338
  return hit;
2127
2339
  }
2128
- function importSpecifier(fromRel, toRel) {
2129
- let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/").replace(/\.(?:ts|js)$/, "");
2340
+ function importSpecifier(fromRel, toRel, preserveExtension = false) {
2341
+ let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/");
2342
+ if (!preserveExtension) spec = spec.replace(/\.(?:ts|js)$/, "");
2130
2343
  if (!spec.startsWith(".")) spec = `./${spec}`;
2131
2344
  return spec;
2132
2345
  }
2133
2346
 
2134
2347
  // src/protect/install/register.ts
2135
2348
  import { writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
2136
- import { join as join9 } from "path";
2349
+ import { join as join10 } from "path";
2137
2350
 
2138
2351
  // src/protect/install/generic.ts
2139
2352
  import { readFileSync as readFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2140
- import { join as join8 } from "path";
2353
+ import { join as join9 } from "path";
2141
2354
  var GUARD_MARKER = "patchstack/guard";
2142
2355
  function genericDir(cwd) {
2143
- return existsSync9(join8(cwd, "src")) ? "src/patchstack" : "patchstack";
2356
+ return existsSync9(join9(cwd, "src")) ? "src/patchstack" : "patchstack";
2144
2357
  }
2145
- function scaffoldGeneric(cwd, opts, guardTemplate = "generic-guard.ts") {
2358
+ function scaffoldGeneric(cwd, opts, guardTemplate = "generic-guard.ts", guardFile = "guard.ts") {
2146
2359
  const templates = templatesDir();
2147
2360
  const dir = genericDir(cwd);
2148
- const dst = join8(cwd, dir);
2361
+ const dst = join9(cwd, dir);
2149
2362
  mkdirSync4(dst, { recursive: true });
2150
- copyFileSync4(join8(templates, guardTemplate), join8(dst, "guard.ts"));
2151
- const changed = [`${dir}/guard.ts`];
2152
- if (!opts.demo) bakeSiteUuid(cwd, `${dir}/guard.ts`);
2153
- const rulesDst = join8(dst, "rules.json");
2363
+ copyFileSync4(join9(templates, guardTemplate), join9(dst, guardFile));
2364
+ const guardRel = `${dir}/${guardFile}`;
2365
+ const changed = [guardRel];
2366
+ if (!opts.demo) bakeSiteUuid(cwd, guardRel);
2367
+ const rulesDst = join9(dst, "rules.json");
2154
2368
  if (opts.demo || !existsSync9(rulesDst)) {
2155
- copyFileSync4(join8(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2369
+ copyFileSync4(join9(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2156
2370
  changed.push(`${dir}/rules.json`);
2157
2371
  }
2158
2372
  return { changed, dir };
@@ -2173,17 +2387,17 @@ function candidateEntries(cwd) {
2173
2387
  "app.ts",
2174
2388
  "app.js"
2175
2389
  ];
2176
- const hits = names.filter((n) => existsSync9(join8(cwd, n)));
2390
+ const hits = names.filter((n) => existsSync9(join9(cwd, n)));
2177
2391
  try {
2178
- const pkg = JSON.parse(read(join8(cwd, "package.json")));
2179
- if (typeof pkg.main === "string" && existsSync9(join8(cwd, pkg.main))) hits.push(pkg.main);
2392
+ const pkg = JSON.parse(read(join9(cwd, "package.json")));
2393
+ if (typeof pkg.main === "string" && existsSync9(join9(cwd, pkg.main))) hits.push(pkg.main);
2180
2394
  } catch {
2181
2395
  }
2182
2396
  return [...new Set(hits)];
2183
2397
  }
2184
2398
  function usesExpress(cwd) {
2185
2399
  try {
2186
- const pkg = JSON.parse(read(join8(cwd, "package.json")));
2400
+ const pkg = JSON.parse(read(join9(cwd, "package.json")));
2187
2401
  return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
2188
2402
  } catch {
2189
2403
  return false;
@@ -2204,8 +2418,8 @@ function wiringPlan(cwd, dir) {
2204
2418
  }
2205
2419
  function genericVerify(cwd) {
2206
2420
  const dir = genericDir(cwd);
2207
- const scaffolded = existsSync9(join8(cwd, dir, "guard.ts"));
2208
- const imported = scaffolded && guardIsImported(cwd, join8(cwd, dir, "guard.ts"));
2421
+ const scaffolded = existsSync9(join9(cwd, dir, "guard.ts"));
2422
+ const imported = scaffolded && guardIsImported(cwd, join9(cwd, dir, "guard.ts"));
2209
2423
  return {
2210
2424
  wired: scaffolded && imported,
2211
2425
  checks: [
@@ -2219,7 +2433,7 @@ function genericVerify(cwd) {
2219
2433
  };
2220
2434
  }
2221
2435
  function guardIsImported(cwd, guardPath) {
2222
- const root = existsSync9(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
2436
+ const root = existsSync9(join9(cwd, "src")) ? join9(cwd, "src") : cwd;
2223
2437
  let found = false;
2224
2438
  const walk2 = (d) => {
2225
2439
  if (found) return;
@@ -2232,7 +2446,7 @@ function guardIsImported(cwd, guardPath) {
2232
2446
  for (const name of entries) {
2233
2447
  if (found) return;
2234
2448
  if (name === "node_modules" || name.startsWith(".")) continue;
2235
- const p = join8(d, name);
2449
+ const p = join9(d, name);
2236
2450
  let st;
2237
2451
  try {
2238
2452
  st = statSync4(p);
@@ -2254,27 +2468,76 @@ function guardIsImported(cwd, guardPath) {
2254
2468
 
2255
2469
  // src/protect/install/register.ts
2256
2470
  var REGION = "// #region patchstack (managed by patchstack-connect protect \u2014 do not edit)";
2471
+ function guardTarget(cwd, entryRel, spec) {
2472
+ if (/\.cjs$/.test(entryRel) || /\.js$/.test(entryRel) && packageType(cwd) !== "module") {
2473
+ if (spec.guardTemplateCjs) {
2474
+ return {
2475
+ template: spec.guardTemplateCjs,
2476
+ file: "guard.cjs",
2477
+ importLine: (name, target) => `const { ${name} } = require("${target}");`,
2478
+ preserveExtension: true
2479
+ };
2480
+ }
2481
+ } else if (/\.(?:js|mjs)$/.test(entryRel) && spec.guardTemplateEsm) {
2482
+ return {
2483
+ template: spec.guardTemplateEsm,
2484
+ file: entryRel.endsWith(".mjs") ? "guard.mjs" : "guard.js",
2485
+ importLine: (name, target) => `import { ${name} } from "${target}";`,
2486
+ preserveExtension: true
2487
+ };
2488
+ }
2489
+ return {
2490
+ template: spec.guardTemplate,
2491
+ file: "guard.ts",
2492
+ importLine: (name, target) => `import { ${name} } from "${target}";`,
2493
+ preserveExtension: false
2494
+ };
2495
+ }
2496
+ function packageType(cwd) {
2497
+ try {
2498
+ return JSON.parse(read(join10(cwd, "package.json"))).type;
2499
+ } catch {
2500
+ return void 0;
2501
+ }
2502
+ }
2257
2503
  function wireRegister(cwd, opts, spec) {
2258
- const { changed, dir } = scaffoldGeneric(cwd, opts, spec.guardTemplate);
2259
2504
  const entry = findAppInstance(cwd, spec.appRe);
2260
2505
  if (!entry) {
2506
+ const { changed: changed2 } = scaffoldGeneric(cwd, opts, spec.guardTemplate);
2261
2507
  log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
2262
- return { ok: true, changed };
2508
+ return { ok: true, changed: changed2 };
2263
2509
  }
2264
- const p = join9(cwd, entry.relPath);
2510
+ const target = guardTarget(cwd, entry.relPath, spec);
2511
+ const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
2512
+ const p = join10(cwd, entry.relPath);
2265
2513
  const s = read(p);
2266
2514
  if (s.includes(spec.importName)) {
2267
2515
  log(`${entry.relPath} already wired`);
2268
2516
  return { ok: true, changed };
2269
2517
  }
2270
- const importLine = `import { ${spec.importName} } from "${importSpecifier(entry.relPath, `${dir}/guard`)}";`;
2518
+ const sourceLines = s.split("\n");
2519
+ const sourceAppIdx = sourceLines.findIndex((line) => spec.appRe.test(line));
2520
+ const requiredAnchor = spec.callAfter?.(entry.appVar);
2521
+ if (spec.requireCallAfter && requiredAnchor && !sourceLines.some((line, index) => index > sourceAppIdx && requiredAnchor.test(line))) {
2522
+ log(`${entry.relPath} body-parser anchor not found \u2014 scaffolded guard; ${spec.manualHint}`);
2523
+ return { ok: true, changed };
2524
+ }
2525
+ const importLine = target.importLine(
2526
+ spec.importName,
2527
+ importSpecifier(entry.relPath, `${dir}/${target.file}`, target.preserveExtension)
2528
+ );
2271
2529
  const lines = s.split("\n");
2272
2530
  let lastImport = -1;
2273
- for (let i = 0; i < lines.length; i++) if (/^\s*import\b/.test(lines[i] ?? "")) lastImport = i;
2531
+ for (let i = 0; i < lines.length; i++) {
2532
+ if (/^\s*(?:import\b|(?:const|let|var)\s+.+?=\s*require\()/.test(lines[i] ?? "")) lastImport = i;
2533
+ }
2274
2534
  lines.splice(lastImport + 1, 0, importLine);
2275
2535
  const appIdx = lines.findIndex((l) => spec.appRe.test(l));
2276
2536
  if (appIdx !== -1) {
2277
- lines.splice(appIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
2537
+ const preferred = spec.callAfter?.(entry.appVar);
2538
+ const preferredIdx = preferred ? lines.findIndex((line, index) => index > appIdx && preferred.test(line)) : -1;
2539
+ const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
2540
+ lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
2278
2541
  }
2279
2542
  writeFileSync5(p, lines.join("\n"));
2280
2543
  changed.push(entry.relPath);
@@ -2282,10 +2545,17 @@ function wireRegister(cwd, opts, spec) {
2282
2545
  return { ok: true, changed: [...new Set(changed)] };
2283
2546
  }
2284
2547
  function verifyRegister(cwd, spec) {
2285
- const dir = existsSync10(join9(cwd, "src")) ? "src/patchstack" : "patchstack";
2286
- const scaffolded = existsSync10(join9(cwd, dir, "guard.ts"));
2548
+ const dir = existsSync10(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
2287
2549
  const entry = findAppInstance(cwd, spec.appRe);
2288
- const wired = entry ? read(join9(cwd, entry.relPath)).includes(spec.importName) : false;
2550
+ const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
2551
+ const scaffolded = target ? existsSync10(join10(cwd, dir, target.file)) : false;
2552
+ const entrySource = entry ? read(join10(cwd, entry.relPath)) : "";
2553
+ const entryLines = entrySource.split("\n");
2554
+ const anchor = entry && spec.callAfter ? spec.callAfter(entry.appVar) : null;
2555
+ const anchorIndex = anchor ? entryLines.findIndex((line) => anchor.test(line)) : -1;
2556
+ const callIndex = entry ? entryLines.findIndex((line) => line.includes(spec.call(entry.appVar))) : -1;
2557
+ const ordered = anchor ? anchorIndex !== -1 && callIndex > anchorIndex : callIndex !== -1;
2558
+ const wired = entrySource.includes(spec.importName) && ordered;
2289
2559
  return {
2290
2560
  wired: scaffolded && wired,
2291
2561
  checks: [
@@ -2330,13 +2600,18 @@ var fastifyAdapter = {
2330
2600
  };
2331
2601
 
2332
2602
  // src/protect/install/adapters/express.ts
2603
+ var jsonParserRe = (appVar) => new RegExp(`^\\s*${appVar}\\.use\\(\\s*express\\.json\\(`, "m");
2333
2604
  var SPEC5 = {
2334
2605
  appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*express\(\)/,
2335
- guardTemplate: "generic-guard.ts",
2606
+ guardTemplate: "express-guard.ts",
2607
+ guardTemplateEsm: "express-guard.js",
2608
+ guardTemplateCjs: "express-guard.cjs",
2336
2609
  importName: "patchstackMiddleware",
2337
2610
  call: (v) => `${v}.use(patchstackMiddleware);`,
2611
+ callAfter: jsonParserRe,
2612
+ requireCallAfter: true,
2338
2613
  label: "Express app",
2339
- manualHint: "add `app.use(patchstackMiddleware)` right after you create your express() app"
2614
+ manualHint: "add `app.use(patchstackMiddleware)` after your JSON body parser and before the routes"
2340
2615
  };
2341
2616
  var expressAdapter = {
2342
2617
  name: "express",
@@ -2467,13 +2742,21 @@ Usage:
2467
2742
  build fingerprint, and ensure the widget
2468
2743
  tag in built pages (run as a postbuild step)
2469
2744
  patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
2470
- guard). Auto-wires known stacks (TanStack
2471
- Start + Supabase); for others it scaffolds a
2745
+ guard). Auto-wires supported server stacks;
2746
+ for others it scaffolds a
2472
2747
  generic guard + prints a wiring plan.
2473
2748
  --demo seeds a broad sample rule set (for
2474
2749
  demonstrations, not production).
2475
2750
  --check verifies the guard is wired (exit 1
2476
2751
  if not) \u2014 for the wire-then-verify loop.
2752
+ patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
2753
+ walkthrough: verify the vulnerable package,
2754
+ scan it, wait for live rule 18843, install +
2755
+ verify the guard, and print test requests.
2756
+ Does not install vulnerable dependencies.
2757
+ patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
2758
+ preparing, running, proving, and cleaning up
2759
+ the production-backed local demo.
2477
2760
  patchstack-connect guide [--full] Show this project's setup status (what's done,
2478
2761
  what's missing, with tailored commands), then
2479
2762
  print the full setup guide. --full prints the
@@ -2489,6 +2772,10 @@ Options (for mark-build):
2489
2772
  --dir <path> Build output directory (default: auto-detect
2490
2773
  dist/ build/ out/ .output/public)
2491
2774
 
2775
+ Options (for demo and demo-guide):
2776
+ --url <url> Test endpoint printed at the end
2777
+ (default: http://localhost:3000/api/tasks)
2778
+
2492
2779
  Environment:
2493
2780
  PATCHSTACK_SITE_UUID Site UUID
2494
2781
  PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
@@ -2505,8 +2792,10 @@ Examples:
2505
2792
  npx @patchstack/connect scan --dry-run
2506
2793
  npx @patchstack/connect init 550e8400-e29b-41d4-a716-446655440000
2507
2794
  npx @patchstack/connect scan --site-uuid 550e8400-...-446655440000
2795
+ npx @patchstack/connect demo node-serialize
2796
+ npx @patchstack/connect demo-guide node-serialize
2508
2797
  `;
2509
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir"]);
2798
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url"]);
2510
2799
  function parseArgs(argv) {
2511
2800
  const args = argv.slice(2);
2512
2801
  const positional = [];
@@ -2699,6 +2988,99 @@ async function runProtectCommand(args) {
2699
2988
  }
2700
2989
  return 0;
2701
2990
  }
2991
+ async function runDemoCommand(args) {
2992
+ try {
2993
+ const scenario = resolveDemoScenario(args.positional[0]);
2994
+ const cwd = process.cwd();
2995
+ const config = await resolveConfig({
2996
+ cwd,
2997
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
2998
+ cliEndpoint: getStringFlag(args.flags, "endpoint"),
2999
+ requireSiteUuid: true
3000
+ });
3001
+ if (config.environment !== "production") {
3002
+ throw new DemoError(
3003
+ "The production-backed demo requires PATCHSTACK_ENVIRONMENT=production. Unset the sandbox override and try again."
3004
+ );
3005
+ }
3006
+ await assertPersistedSiteUuid(cwd, config.siteUuid);
3007
+ await assertDemoDependency(cwd, scenario);
3008
+ console.log(`Patchstack production demo \u2014 ${scenario.packageName}@${scenario.packageVersion}`);
3009
+ console.log(` Site: ${config.siteUuid}`);
3010
+ console.log("");
3011
+ console.log("1. Report the current npm manifest");
3012
+ const scanCode = await runScan(args, { showRemainingSetup: false });
3013
+ if (scanCode !== 0) return scanCode;
3014
+ console.log("");
3015
+ console.log(`2. Wait for live virtual-patch rule ${scenario.ruleId}`);
3016
+ const rule = await waitForDemoRule(config.endpoint, config.siteUuid, scenario, {
3017
+ requestTimeoutMs: config.timeoutMs
3018
+ });
3019
+ console.log(`Rule ready: ${rule.id}${rule.title ? ` \u2014 ${rule.title}` : ""}`);
3020
+ console.log("");
3021
+ console.log("3. Install and verify the runtime guard");
3022
+ const result = runProtect(cwd);
3023
+ if (result.status === "unsupported") {
3024
+ throw new DemoError(
3025
+ `Runtime protection is not supported for this stack. Supported: ${result.supported.join(", ")}.`
3026
+ );
3027
+ }
3028
+ console.log(`Guard installer: ${result.adapter} (${result.status})`);
3029
+ const report = runVerify(cwd);
3030
+ for (const check of report.checks) {
3031
+ console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.label}${!check.ok && check.hint ? ` \u2014 ${check.hint}` : ""}`);
3032
+ }
3033
+ if (!report.wired) {
3034
+ throw new DemoError(
3035
+ `The ${report.stack} guard is not fully wired. Complete the failed check above, then run this command again.`
3036
+ );
3037
+ }
3038
+ console.log("");
3039
+ console.log("Production virtual patch is ready.");
3040
+ console.log("");
3041
+ console.log(
3042
+ renderDemoTestCommands(
3043
+ getStringFlag(args.flags, "url") ?? "http://localhost:3000/api/tasks",
3044
+ scenario
3045
+ )
3046
+ );
3047
+ return 0;
3048
+ } catch (error) {
3049
+ if (error instanceof DemoError) {
3050
+ console.error(`Demo error: ${error.message}`);
3051
+ return 1;
3052
+ }
3053
+ throw error;
3054
+ }
3055
+ }
3056
+ async function runDemoGuideCommand(args) {
3057
+ try {
3058
+ const scenario = resolveDemoScenario(args.positional[0]);
3059
+ const cwd = process.cwd();
3060
+ const config = await resolveConfig({ cwd });
3061
+ const [siteUuid, dependency] = await Promise.all([
3062
+ readPersistedSiteUuid(cwd),
3063
+ inspectDemoDependency(cwd, scenario)
3064
+ ]);
3065
+ console.log(
3066
+ renderDemoGuide({
3067
+ scenario,
3068
+ packageManager: detectPackageManager(cwd),
3069
+ siteUuid,
3070
+ dependency,
3071
+ environment: config.environment,
3072
+ url: getStringFlag(args.flags, "url") ?? "http://localhost:3000/api/tasks"
3073
+ })
3074
+ );
3075
+ return 0;
3076
+ } catch (error) {
3077
+ if (error instanceof DemoError) {
3078
+ console.error(`Demo guide error: ${error.message}`);
3079
+ return 1;
3080
+ }
3081
+ throw error;
3082
+ }
3083
+ }
2702
3084
  async function runGuide(args) {
2703
3085
  let allDone = false;
2704
3086
  try {
@@ -2871,6 +3253,10 @@ async function main() {
2871
3253
  return runMarkBuild(args);
2872
3254
  case "protect":
2873
3255
  return runProtectCommand(args);
3256
+ case "demo":
3257
+ return runDemoCommand(args);
3258
+ case "demo-guide":
3259
+ return runDemoGuideCommand(args);
2874
3260
  case "guide":
2875
3261
  return runGuide(args);
2876
3262
  case "setup":