@patchstack/connect 0.3.16 → 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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -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 {};
@@ -1661,16 +1873,65 @@ function renderGuideChecklist(state, useColor) {
1661
1873
  return lines.join("\n");
1662
1874
  }
1663
1875
 
1664
- // src/protect/install.ts
1665
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync, copyFileSync } from "fs";
1666
- import { join, dirname } from "path";
1876
+ // src/protect/install/util.ts
1877
+ import { readFileSync as readFileSync3, existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
1878
+ import { join as join2, dirname } from "path";
1667
1879
  import { fileURLToPath } from "url";
1668
- var HERE = dirname(fileURLToPath(import.meta.url));
1669
- var TEMPLATES = [join(HERE, "protect", "templates"), join(HERE, "templates")].find((p) => existsSync4(p)) ?? join(HERE, "protect", "templates");
1670
- var APP = process.cwd();
1671
- var PS_DIR = join(APP, "src/integrations/patchstack");
1672
1880
  var read = (p) => readFileSync3(p, "utf8");
1673
1881
  var log = (msg) => console.log(`patchstack protect: ${msg}`);
1882
+ function hasDependency(cwd, name) {
1883
+ try {
1884
+ const pkg = JSON.parse(read(join2(cwd, "package.json")));
1885
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }[name]);
1886
+ } catch {
1887
+ return false;
1888
+ }
1889
+ }
1890
+ var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
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;
1892
+ function bakeSiteUuid(cwd, guardRelPath) {
1893
+ const rc = join2(cwd, ".patchstackrc.json");
1894
+ if (!existsSync4(rc)) {
1895
+ log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1896
+ return false;
1897
+ }
1898
+ let uuid;
1899
+ try {
1900
+ uuid = JSON.parse(read(rc)).siteUuid;
1901
+ } catch {
1902
+ log(".patchstackrc.json unreadable \u2014 skipping site-UUID bake");
1903
+ return false;
1904
+ }
1905
+ if (typeof uuid !== "string" || !UUID_RE.test(uuid)) {
1906
+ log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1907
+ return false;
1908
+ }
1909
+ const guardPath = join2(cwd, guardRelPath);
1910
+ if (!existsSync4(guardPath)) return false;
1911
+ const source = read(guardPath);
1912
+ if (!source.includes(SITE_UUID_PLACEHOLDER)) {
1913
+ log(`${guardRelPath} site UUID already baked`);
1914
+ return false;
1915
+ }
1916
+ writeFileSync2(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
1917
+ log(`baked site UUID into ${guardRelPath} \u2014 live rules from the Patchstack API`);
1918
+ return true;
1919
+ }
1920
+ var HERE = dirname(fileURLToPath(import.meta.url));
1921
+ function templatesDir() {
1922
+ const builtLayout = join2(HERE, "protect", "templates");
1923
+ const candidates = [
1924
+ builtLayout,
1925
+ join2(HERE, "..", "templates"),
1926
+ // source: src/protect/install/ → src/protect/templates
1927
+ join2(HERE, "templates")
1928
+ ];
1929
+ return candidates.find((p) => existsSync4(p)) ?? builtLayout;
1930
+ }
1931
+
1932
+ // src/protect/install/adapters/tanstack-supabase.ts
1933
+ import { writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync, copyFileSync } from "fs";
1934
+ import { join as join3 } from "path";
1674
1935
  var CLIENT_TUNNEL = [
1675
1936
  "",
1676
1937
  " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
@@ -1684,101 +1945,127 @@ var CLIENT_TUNNEL = [
1684
1945
  " }",
1685
1946
  ""
1686
1947
  ].join("\n");
1687
- var START_IMPORTS = [
1688
- 'import { getRequest } from "@tanstack/react-start/server";',
1689
- 'import { GUARD_PATH, handleGuardRequest, inspectServerFn, screenResponse } from "@/integrations/patchstack/guard";'
1690
- ].join("\n");
1691
- var REQUEST_MIDDLEWARE_DEF = [
1692
- "",
1693
- "// Patchstack guard (browser tunnel): intercept tunneled Supabase traffic before anything else,",
1694
- "// then screen the outgoing response (SSR HTML / data) for leaked secrets & PII.",
1948
+ var GUARD_IMPORT = 'import { GUARD_PATH, handleGuardRequest, inspectServerFn, screenResponse, guardRequest } from "@/integrations/patchstack/guard";';
1949
+ var GUARD_IMPORT_RE = /import \{[^}]*\} from "@\/integrations\/patchstack\/guard";/;
1950
+ var START_IMPORTS = ['import { getRequest } from "@tanstack/react-start/server";', GUARD_IMPORT].join("\n");
1951
+ var REQUEST_MIDDLEWARE_BLOCK = [
1952
+ "// #region patchstack-guard (managed by patchstack-connect protect \u2014 do not edit)",
1953
+ "// Browser\u2192Supabase tunnel + response screening; optional route WAF via PATCHSTACK_ROUTE_WAF=1.",
1695
1954
  "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
1696
1955
  " const request = getRequest();",
1697
1956
  " if (request) {",
1698
1957
  " const { pathname } = new URL(request.url);",
1699
1958
  " if (pathname === GUARD_PATH) return handleGuardRequest(request);",
1959
+ ' if (process.env.PATCHSTACK_ROUTE_WAF === "1") {',
1960
+ " const blocked = await guardRequest(request);",
1961
+ " if (blocked) return blocked;",
1962
+ " }",
1700
1963
  " }",
1701
1964
  " return screenResponse(await next());",
1702
1965
  "});",
1703
- ""
1966
+ "// #endregion patchstack-guard"
1704
1967
  ].join("\n");
1705
- var FUNCTION_MIDDLEWARE_DEF = [
1706
- "",
1707
- "// Patchstack guard (server functions): inspect server-fn args before they reach the database,",
1708
- "// covering apps that mutate via TanStack server functions (which bypass the browser tunnel).",
1968
+ var FUNCTION_MIDDLEWARE_BLOCK = [
1969
+ "// #region patchstack-function-guard (managed by patchstack-connect protect \u2014 do not edit)",
1970
+ "// Inspect server-function args before they reach the database.",
1709
1971
  'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
1710
1972
  " const blocked = await inspectServerFn(data);",
1711
1973
  " if (blocked) throw new Error(blocked.message);",
1712
1974
  " return next();",
1713
1975
  "});",
1714
- ""
1976
+ "// #endregion patchstack-function-guard"
1715
1977
  ].join("\n");
1716
- function detectSupportedStack(cwd) {
1717
- const pkgPath = join(cwd, "package.json");
1718
- if (!existsSync4(pkgPath)) return false;
1719
- const pkg = JSON.parse(read(pkgPath));
1720
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1721
- return Boolean(deps["@tanstack/react-start"]) && existsSync4(join(cwd, "src/start.ts")) && existsSync4(join(cwd, "src/integrations/supabase/client.ts"));
1722
- }
1723
- function scaffold(cwd) {
1724
- const dst = join(cwd, "src/integrations/patchstack");
1725
- mkdirSync(dst, { recursive: true });
1726
- copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
1727
- copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
1728
- log("scaffolded guard.ts + rules.json");
1729
- }
1730
- function bakeSiteUuid(cwd) {
1731
- const rc = join(cwd, ".patchstackrc.json");
1732
- if (!existsSync4(rc)) return log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1733
- let uuid;
1978
+ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
1979
+ const lines = s.split("\n");
1980
+ const startMarker = `// #region ${region} `;
1981
+ const endMarker = `// #endregion ${region}`;
1982
+ const si = lines.findIndex((l) => l.includes(startMarker));
1983
+ if (si !== -1) {
1984
+ const ei = lines.findIndex((l, i) => i > si && l.trim() === endMarker);
1985
+ if (ei !== -1) {
1986
+ lines.splice(si, ei - si + 1, ...block.split("\n"));
1987
+ return lines.join("\n");
1988
+ }
1989
+ }
1990
+ const ci = lines.findIndex((l) => l.includes(legacyConst));
1991
+ if (ci !== -1) {
1992
+ const close = lines.findIndex((l, i) => i >= ci && l.trim() === "});");
1993
+ if (close !== -1) {
1994
+ let start = ci;
1995
+ while (start > 0 && (lines[start - 1] ?? "").trim().startsWith("//")) start--;
1996
+ lines.splice(start, close - start + 1, ...block.split("\n"));
1997
+ return lines.join("\n");
1998
+ }
1999
+ }
2000
+ return s.replace(insertBefore, block + "\n\n" + insertBefore);
2001
+ }
2002
+ function detect(cwd) {
2003
+ const pkgPath = join3(cwd, "package.json");
2004
+ if (!existsSync5(pkgPath)) return false;
2005
+ let pkg;
1734
2006
  try {
1735
- uuid = JSON.parse(read(rc)).siteUuid;
2007
+ pkg = JSON.parse(read(pkgPath));
1736
2008
  } catch {
1737
- return log(".patchstackrc.json unreadable \u2014 skipping site-UUID bake");
2009
+ return false;
1738
2010
  }
1739
- if (!uuid || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
1740
- return log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
2011
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2012
+ return Boolean(deps["@tanstack/react-start"]) && existsSync5(join3(cwd, "src/start.ts")) && existsSync5(join3(cwd, "src/integrations/supabase/client.ts"));
2013
+ }
2014
+ var GUARD_FILE = "src/integrations/patchstack/guard.ts";
2015
+ function scaffold(cwd, opts) {
2016
+ const templates = templatesDir();
2017
+ const dst = join3(cwd, "src/integrations/patchstack");
2018
+ mkdirSync(dst, { recursive: true });
2019
+ copyFileSync(join3(templates, "guard.ts"), join3(dst, "guard.ts"));
2020
+ const changed = [GUARD_FILE];
2021
+ const rulesDst = join3(dst, "rules.json");
2022
+ if (opts.demo) {
2023
+ copyFileSync(join3(templates, "demo-rules.json"), rulesDst);
2024
+ changed.push("src/integrations/patchstack/rules.json");
2025
+ log("scaffolded guard.ts + rules.json (demo sample rule set)");
2026
+ } else if (!existsSync5(rulesDst)) {
2027
+ copyFileSync(join3(templates, "rules.json"), rulesDst);
2028
+ changed.push("src/integrations/patchstack/rules.json");
2029
+ log("scaffolded guard.ts + rules.json (starter rules)");
2030
+ } else {
2031
+ log("scaffolded guard.ts (kept existing rules.json)");
1741
2032
  }
1742
- const p = join(cwd, "src/integrations/patchstack/guard.ts");
1743
- if (!existsSync4(p)) return;
1744
- const s = read(p);
1745
- if (!s.includes("__PATCHSTACK_SITE_UUID__")) return log("guard.ts site UUID already baked");
1746
- writeFileSync2(p, s.replace("__PATCHSTACK_SITE_UUID__", uuid));
1747
- log("baked site UUID into guard.ts \u2014 live rules from the Patchstack API");
2033
+ return changed;
1748
2034
  }
1749
2035
  function patchClient(cwd) {
1750
- const p = join(cwd, "src/integrations/supabase/client.ts");
1751
- let s = read(p);
1752
- if (s.includes("x-ps-target")) return log("client.ts already wired");
2036
+ const p = join3(cwd, "src/integrations/supabase/client.ts");
2037
+ const s = read(p);
2038
+ if (s.includes("x-ps-target")) {
2039
+ log("client.ts already wired");
2040
+ return false;
2041
+ }
1753
2042
  const anchor = "headers.set('apikey', supabaseKey);";
1754
- if (!s.includes(anchor)) return log("client.ts anchor not found \u2014 skipping (template changed?)");
1755
- writeFileSync2(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
2043
+ if (!s.includes(anchor)) {
2044
+ log("client.ts anchor not found \u2014 skipping (template changed?)");
2045
+ return false;
2046
+ }
2047
+ writeFileSync3(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1756
2048
  log("patched client.ts (tunnel Supabase through the guard)");
2049
+ return true;
1757
2050
  }
1758
2051
  function patchStart(cwd) {
1759
- const p = join(cwd, "src/start.ts");
2052
+ const p = join3(cwd, "src/start.ts");
1760
2053
  let s = read(p);
1761
2054
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
1762
2055
  const exportAnchor = "export const startInstance";
1763
2056
  const rmAnchor = "requestMiddleware: [";
1764
2057
  if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
1765
- return log("start.ts anchors not found \u2014 skipping (template changed?)");
2058
+ log("start.ts anchors not found \u2014 skipping (template changed?)");
2059
+ return false;
1766
2060
  }
1767
2061
  const original = s;
1768
- if (!s.includes("@/integrations/patchstack/guard")) {
2062
+ if (GUARD_IMPORT_RE.test(s)) {
2063
+ s = s.replace(GUARD_IMPORT_RE, GUARD_IMPORT);
2064
+ } else {
1769
2065
  s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1770
- } else if (!s.includes("inspectServerFn")) {
1771
- s = s.replace(
1772
- 'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";',
1773
- 'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";'
1774
- );
1775
- }
1776
- if (!s.includes("const patchstackGuard =")) {
1777
- s = s.replace(exportAnchor, REQUEST_MIDDLEWARE_DEF + "\n" + exportAnchor);
1778
- }
1779
- if (!s.includes("const patchstackFunctionGuard =")) {
1780
- s = s.replace(exportAnchor, FUNCTION_MIDDLEWARE_DEF + "\n" + exportAnchor);
1781
2066
  }
2067
+ s = reconcileBlock(s, "patchstack-guard", REQUEST_MIDDLEWARE_BLOCK, "const patchstackGuard =", exportAnchor);
2068
+ s = reconcileBlock(s, "patchstack-function-guard", FUNCTION_MIDDLEWARE_BLOCK, "const patchstackFunctionGuard =", exportAnchor);
1782
2069
  if (s.includes(rmAnchor) && !s.includes("requestMiddleware: [patchstackGuard")) {
1783
2070
  s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1784
2071
  }
@@ -1790,24 +2077,579 @@ function patchStart(cwd) {
1790
2077
  s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
1791
2078
  }
1792
2079
  }
1793
- if (s === original) return log("start.ts already wired");
1794
- writeFileSync2(p, s);
2080
+ if (s === original) {
2081
+ log("start.ts already wired");
2082
+ return false;
2083
+ }
2084
+ writeFileSync3(p, s);
1795
2085
  log("patched start.ts (guard registered as request + function middleware)");
2086
+ return true;
2087
+ }
2088
+ function wire(cwd, opts) {
2089
+ const changed = scaffold(cwd, opts);
2090
+ if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
2091
+ if (patchClient(cwd)) changed.push("src/integrations/supabase/client.ts");
2092
+ if (patchStart(cwd)) changed.push("src/start.ts");
2093
+ log(
2094
+ opts.demo ? "done \u2014 guard wired with the demo sample rules (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only." : "done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only."
2095
+ );
2096
+ return { ok: true, changed: [...new Set(changed)] };
2097
+ }
2098
+ function verify(cwd) {
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");
2102
+ const guard = existsSync5(guardPath) ? read(guardPath) : "";
2103
+ const client = existsSync5(clientPath) ? read(clientPath) : "";
2104
+ const start = existsSync5(startPath) ? read(startPath) : "";
2105
+ const checks = [
2106
+ { label: "guard.ts scaffolded", ok: guard.length > 0, hint: "run `patchstack-connect protect`" },
2107
+ { label: "Supabase client tunnels through the guard", ok: client.includes("x-ps-target"), hint: "run `patchstack-connect protect` to re-patch src/integrations/supabase/client.ts" },
2108
+ { label: "request middleware defined + registered", ok: start.includes("const patchstackGuard =") && start.includes("requestMiddleware: [patchstackGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" },
2109
+ { label: "server-function middleware defined + registered", ok: start.includes("const patchstackFunctionGuard =") && start.includes("functionMiddleware: [patchstackFunctionGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" }
2110
+ ];
2111
+ return { wired: checks.every((c) => c.ok), checks };
2112
+ }
2113
+ var tanstackSupabaseAdapter = {
2114
+ name: "tanstack-supabase",
2115
+ label: "TanStack Start + Supabase",
2116
+ detect,
2117
+ wire,
2118
+ verify
2119
+ };
2120
+
2121
+ // src/protect/install/adapters/next.ts
2122
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2 } from "fs";
2123
+ import { join as join4 } from "path";
2124
+ function hasNextDep(cwd) {
2125
+ try {
2126
+ const pkg = JSON.parse(read(join4(cwd, "package.json")));
2127
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
2128
+ } catch {
2129
+ return false;
2130
+ }
1796
2131
  }
1797
- function runProtect(cwd) {
1798
- if (!detectSupportedStack(cwd)) {
1799
- log("runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping.");
1800
- return;
2132
+ function middlewareInfo(cwd) {
2133
+ const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
2134
+ for (const rel of candidates) {
2135
+ if (existsSync6(join4(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
2136
+ }
2137
+ const useSrc = existsSync6(join4(cwd, "src"));
2138
+ return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
2139
+ }
2140
+ function detect2(cwd) {
2141
+ return hasNextDep(cwd);
2142
+ }
2143
+ function rulesFile(relDir) {
2144
+ return relDir === "." ? "patchstack.rules.json" : `${relDir}/patchstack.rules.json`;
2145
+ }
2146
+ function wire2(cwd, opts) {
2147
+ const templates = templatesDir();
2148
+ const mw = middlewareInfo(cwd);
2149
+ const dir = join4(cwd, mw.relDir === "." ? "" : mw.relDir);
2150
+ mkdirSync2(dir, { recursive: true });
2151
+ const rulesDst = join4(dir, "patchstack.rules.json");
2152
+ const changed = [];
2153
+ if (opts.demo || !existsSync6(rulesDst)) {
2154
+ copyFileSync2(join4(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2155
+ changed.push(rulesFile(mw.relDir));
2156
+ }
2157
+ const mwPath = join4(cwd, mw.relFile);
2158
+ const existing = mw.exists ? read(mwPath) : "";
2159
+ if (mw.exists && !existing.includes("patchstack-next")) {
2160
+ log(
2161
+ `existing ${mw.relFile} left untouched \u2014 scaffolded ${rulesFile(mw.relDir)}; add the guard to your middleware: import { createProtection } from "@patchstack/connect/protect", run fetchGuard() on the request, and return the block Response. Then: npx patchstack-connect protect --check`
2162
+ );
2163
+ return { ok: true, changed };
2164
+ }
2165
+ copyFileSync2(join4(templates, "next-middleware.ts"), mwPath);
2166
+ if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
2167
+ changed.push(mw.relFile);
2168
+ log(mw.exists ? `refreshed ${mw.relFile} (Patchstack middleware)` : `scaffolded ${mw.relFile} (Patchstack middleware)`);
2169
+ return { ok: true, changed: [...new Set(changed)] };
2170
+ }
2171
+ function verify2(cwd) {
2172
+ const mw = middlewareInfo(cwd);
2173
+ const present = mw.exists && read(join4(cwd, mw.relFile)).includes("patchstack-next");
2174
+ const rulesPresent = existsSync6(join4(cwd, rulesFile(mw.relDir)));
2175
+ return {
2176
+ wired: present && rulesPresent,
2177
+ checks: [
2178
+ { label: "Patchstack middleware present", ok: present, hint: `run \`patchstack-connect protect\` (writes ${mw.relFile})` },
2179
+ { label: "rules co-located with the middleware", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
2180
+ ]
2181
+ };
2182
+ }
2183
+ var nextAdapter = {
2184
+ name: "nextjs",
2185
+ label: "Next.js",
2186
+ detect: detect2,
2187
+ wire: wire2,
2188
+ verify: verify2
2189
+ };
2190
+
2191
+ // src/protect/install/adapters/sveltekit.ts
2192
+ import { join as join6 } from "path";
2193
+
2194
+ // src/protect/install/seam.ts
2195
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, copyFileSync as copyFileSync3 } from "fs";
2196
+ import { join as join5, dirname as dirname2 } from "path";
2197
+ function rulesRel(seamRel) {
2198
+ const d = dirname2(seamRel);
2199
+ return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
2200
+ }
2201
+ function wireSeam(cwd, opts, spec) {
2202
+ const templates = templatesDir();
2203
+ const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
2204
+ const seamRel = existing ?? spec.target;
2205
+ mkdirSync3(dirname2(join5(cwd, seamRel)), { recursive: true });
2206
+ const rulesDst = join5(cwd, rulesRel(seamRel));
2207
+ const changed = [];
2208
+ if (opts.demo || !existsSync7(rulesDst)) {
2209
+ copyFileSync3(join5(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2210
+ changed.push(rulesRel(seamRel));
2211
+ }
2212
+ const current = existing ? read(join5(cwd, existing)) : "";
2213
+ if (existing && !current.includes(spec.marker)) {
2214
+ log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
2215
+ return { ok: true, changed };
2216
+ }
2217
+ copyFileSync3(join5(templates, spec.templateName), join5(cwd, seamRel));
2218
+ if (!opts.demo) bakeSiteUuid(cwd, seamRel);
2219
+ changed.push(seamRel);
2220
+ log(existing ? `refreshed ${seamRel}` : `scaffolded ${seamRel}`);
2221
+ return { ok: true, changed: [...new Set(changed)] };
2222
+ }
2223
+ function verifySeam(cwd, spec) {
2224
+ const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
2225
+ const seamRel = existing ?? spec.target;
2226
+ const present = existing ? read(join5(cwd, existing)).includes(spec.marker) : false;
2227
+ const rulesPresent = existsSync7(join5(cwd, rulesRel(seamRel)));
2228
+ return {
2229
+ wired: present && rulesPresent,
2230
+ checks: [
2231
+ { label: `${spec.seamLabel} present`, ok: present, hint: `run \`patchstack-connect protect\` (writes ${spec.target})` },
2232
+ { label: "rules co-located with the guard", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
2233
+ ]
2234
+ };
2235
+ }
2236
+
2237
+ // src/protect/install/adapters/sveltekit.ts
2238
+ var SPEC = {
2239
+ templateName: "sveltekit-hooks.ts",
2240
+ candidates: ["src/hooks.server.ts", "src/hooks.server.js"],
2241
+ target: "src/hooks.server.ts",
2242
+ marker: "patchstack-sveltekit",
2243
+ planHint: 'add the guard to your `handle` (compose with `sequence()` from "@sveltejs/kit/hooks"), then: npx patchstack-connect protect --check',
2244
+ seamLabel: "SvelteKit server hook"
2245
+ };
2246
+ function detect3(cwd) {
2247
+ try {
2248
+ const pkg = JSON.parse(read(join6(cwd, "package.json")));
2249
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
2250
+ } catch {
2251
+ return false;
2252
+ }
2253
+ }
2254
+ function wire3(cwd, opts) {
2255
+ return wireSeam(cwd, opts, SPEC);
2256
+ }
2257
+ function verify3(cwd) {
2258
+ return verifySeam(cwd, SPEC);
2259
+ }
2260
+ var sveltekitAdapter = {
2261
+ name: "sveltekit",
2262
+ label: "SvelteKit",
2263
+ detect: detect3,
2264
+ wire: wire3,
2265
+ verify: verify3
2266
+ };
2267
+
2268
+ // src/protect/install/adapters/astro.ts
2269
+ import { join as join7 } from "path";
2270
+ var SPEC2 = {
2271
+ templateName: "astro-middleware.ts",
2272
+ candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
2273
+ target: "src/middleware.ts",
2274
+ marker: "patchstack-astro",
2275
+ planHint: 'add the guard to your `onRequest` (compose with `sequence()` from "astro:middleware"), then: npx patchstack-connect protect --check',
2276
+ seamLabel: "Astro middleware"
2277
+ };
2278
+ function detect4(cwd) {
2279
+ try {
2280
+ const pkg = JSON.parse(read(join7(cwd, "package.json")));
2281
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
2282
+ } catch {
2283
+ return false;
2284
+ }
2285
+ }
2286
+ function wire4(cwd, opts) {
2287
+ return wireSeam(cwd, opts, SPEC2);
2288
+ }
2289
+ function verify4(cwd) {
2290
+ return verifySeam(cwd, SPEC2);
2291
+ }
2292
+ var astroAdapter = {
2293
+ name: "astro",
2294
+ label: "Astro",
2295
+ detect: detect4,
2296
+ wire: wire4,
2297
+ verify: verify4
2298
+ };
2299
+
2300
+ // src/protect/install/find-app.ts
2301
+ import { readFileSync as readFileSync4, existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2302
+ import { join as join8, relative, dirname as dirname3 } from "path";
2303
+ function findAppInstance(cwd, re) {
2304
+ const root = existsSync8(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
2305
+ let hit = null;
2306
+ const walk2 = (dir) => {
2307
+ if (hit) return;
2308
+ let entries;
2309
+ try {
2310
+ entries = readdirSync3(dir);
2311
+ } catch {
2312
+ return;
2313
+ }
2314
+ for (const name of entries) {
2315
+ if (hit) return;
2316
+ if (name === "node_modules" || name === "patchstack" || name.startsWith(".")) continue;
2317
+ const p = join8(dir, name);
2318
+ let st;
2319
+ try {
2320
+ st = statSync3(p);
2321
+ } catch {
2322
+ continue;
2323
+ }
2324
+ if (st.isDirectory()) walk2(p);
2325
+ else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) {
2326
+ let src;
2327
+ try {
2328
+ src = readFileSync4(p, "utf8");
2329
+ } catch {
2330
+ continue;
2331
+ }
2332
+ const m = re.exec(src);
2333
+ if (m) hit = { relPath: relative(cwd, p).replace(/\\/g, "/"), appVar: m[1] };
2334
+ }
2335
+ }
2336
+ };
2337
+ walk2(root);
2338
+ return hit;
2339
+ }
2340
+ function importSpecifier(fromRel, toRel, preserveExtension = false) {
2341
+ let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/");
2342
+ if (!preserveExtension) spec = spec.replace(/\.(?:ts|js)$/, "");
2343
+ if (!spec.startsWith(".")) spec = `./${spec}`;
2344
+ return spec;
2345
+ }
2346
+
2347
+ // src/protect/install/register.ts
2348
+ import { writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
2349
+ import { join as join10 } from "path";
2350
+
2351
+ // src/protect/install/generic.ts
2352
+ import { readFileSync as readFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2353
+ import { join as join9 } from "path";
2354
+ var GUARD_MARKER = "patchstack/guard";
2355
+ function genericDir(cwd) {
2356
+ return existsSync9(join9(cwd, "src")) ? "src/patchstack" : "patchstack";
2357
+ }
2358
+ function scaffoldGeneric(cwd, opts, guardTemplate = "generic-guard.ts", guardFile = "guard.ts") {
2359
+ const templates = templatesDir();
2360
+ const dir = genericDir(cwd);
2361
+ const dst = join9(cwd, dir);
2362
+ mkdirSync4(dst, { recursive: true });
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");
2368
+ if (opts.demo || !existsSync9(rulesDst)) {
2369
+ copyFileSync4(join9(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2370
+ changed.push(`${dir}/rules.json`);
2371
+ }
2372
+ return { changed, dir };
2373
+ }
2374
+ function candidateEntries(cwd) {
2375
+ const names = [
2376
+ "src/server.ts",
2377
+ "src/server.js",
2378
+ "src/index.ts",
2379
+ "src/index.js",
2380
+ "src/app.ts",
2381
+ "src/app.js",
2382
+ "src/main.ts",
2383
+ "server.ts",
2384
+ "server.js",
2385
+ "index.ts",
2386
+ "index.js",
2387
+ "app.ts",
2388
+ "app.js"
2389
+ ];
2390
+ const hits = names.filter((n) => existsSync9(join9(cwd, n)));
2391
+ try {
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);
2394
+ } catch {
1801
2395
  }
1802
- scaffold(cwd);
1803
- bakeSiteUuid(cwd);
1804
- patchClient(cwd);
1805
- patchStart(cwd);
1806
- log("done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only.");
2396
+ return [...new Set(hits)];
2397
+ }
2398
+ function usesExpress(cwd) {
2399
+ try {
2400
+ const pkg = JSON.parse(read(join9(cwd, "package.json")));
2401
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
2402
+ } catch {
2403
+ return false;
2404
+ }
2405
+ }
2406
+ function wiringPlan(cwd, dir) {
2407
+ const entries = candidateEntries(cwd);
2408
+ const express = usesExpress(cwd);
2409
+ const lines = [
2410
+ `no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/guard.ts + ${dir}/rules.json.`,
2411
+ "Finish by wiring it into your server (pick the one that fits):",
2412
+ ` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/guard"`,
2413
+ ` \u2022 Node / Express: app.use(patchstackMiddleware) // add before your routes`,
2414
+ entries.length ? `Likely server ${entries.length === 1 ? "entry" : "entries"}: ${entries.join(", ")}${express ? " (Express detected)" : ""}.` : "Could not locate a server entry \u2014 wire it wherever requests enter your app.",
2415
+ "Then confirm it is hooked up: npx patchstack-connect protect --check"
2416
+ ];
2417
+ return lines.join("\n");
2418
+ }
2419
+ function genericVerify(cwd) {
2420
+ const dir = genericDir(cwd);
2421
+ const scaffolded = existsSync9(join9(cwd, dir, "guard.ts"));
2422
+ const imported = scaffolded && guardIsImported(cwd, join9(cwd, dir, "guard.ts"));
2423
+ return {
2424
+ wired: scaffolded && imported,
2425
+ checks: [
2426
+ { label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
2427
+ {
2428
+ label: "guard imported into a server entry",
2429
+ ok: imported,
2430
+ hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}/guard" and wire it into your request path`
2431
+ }
2432
+ ]
2433
+ };
2434
+ }
2435
+ function guardIsImported(cwd, guardPath) {
2436
+ const root = existsSync9(join9(cwd, "src")) ? join9(cwd, "src") : cwd;
2437
+ let found = false;
2438
+ const walk2 = (d) => {
2439
+ if (found) return;
2440
+ let entries;
2441
+ try {
2442
+ entries = readdirSync4(d);
2443
+ } catch {
2444
+ return;
2445
+ }
2446
+ for (const name of entries) {
2447
+ if (found) return;
2448
+ if (name === "node_modules" || name.startsWith(".")) continue;
2449
+ const p = join9(d, name);
2450
+ let st;
2451
+ try {
2452
+ st = statSync4(p);
2453
+ } catch {
2454
+ continue;
2455
+ }
2456
+ if (st.isDirectory()) walk2(p);
2457
+ else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
2458
+ try {
2459
+ if (readFileSync5(p, "utf8").includes(GUARD_MARKER)) found = true;
2460
+ } catch {
2461
+ }
2462
+ }
2463
+ }
2464
+ };
2465
+ walk2(root);
2466
+ return found;
2467
+ }
2468
+
2469
+ // src/protect/install/register.ts
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
+ }
2503
+ function wireRegister(cwd, opts, spec) {
2504
+ const entry = findAppInstance(cwd, spec.appRe);
2505
+ if (!entry) {
2506
+ const { changed: changed2 } = scaffoldGeneric(cwd, opts, spec.guardTemplate);
2507
+ log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
2508
+ return { ok: true, changed: changed2 };
2509
+ }
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);
2513
+ const s = read(p);
2514
+ if (s.includes(spec.importName)) {
2515
+ log(`${entry.relPath} already wired`);
2516
+ return { ok: true, changed };
2517
+ }
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
+ );
2529
+ const lines = s.split("\n");
2530
+ let lastImport = -1;
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
+ }
2534
+ lines.splice(lastImport + 1, 0, importLine);
2535
+ const appIdx = lines.findIndex((l) => spec.appRe.test(l));
2536
+ if (appIdx !== -1) {
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");
2541
+ }
2542
+ writeFileSync5(p, lines.join("\n"));
2543
+ changed.push(entry.relPath);
2544
+ log(`patched ${entry.relPath} (${spec.call(entry.appVar).trim()})`);
2545
+ return { ok: true, changed: [...new Set(changed)] };
2546
+ }
2547
+ function verifyRegister(cwd, spec) {
2548
+ const dir = existsSync10(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
2549
+ const entry = findAppInstance(cwd, spec.appRe);
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;
2559
+ return {
2560
+ wired: scaffolded && wired,
2561
+ checks: [
2562
+ { label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
2563
+ { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint }
2564
+ ]
2565
+ };
2566
+ }
2567
+
2568
+ // src/protect/install/adapters/nestjs.ts
2569
+ var SPEC3 = {
2570
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+NestFactory\.create/,
2571
+ guardTemplate: "generic-guard.ts",
2572
+ importName: "patchstackMiddleware",
2573
+ call: (v) => `${v}.use(patchstackMiddleware);`,
2574
+ label: "NestJS app",
2575
+ manualHint: "add `app.use(patchstackMiddleware)` right after your NestFactory.create(...) call"
2576
+ };
2577
+ var nestjsAdapter = {
2578
+ name: "nestjs",
2579
+ label: "NestJS",
2580
+ detect: (cwd) => hasDependency(cwd, "@nestjs/core") && findAppInstance(cwd, SPEC3.appRe) !== null,
2581
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC3),
2582
+ verify: (cwd) => verifyRegister(cwd, SPEC3)
2583
+ };
2584
+
2585
+ // src/protect/install/adapters/fastify.ts
2586
+ var SPEC4 = {
2587
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:fastify|Fastify)\(/,
2588
+ guardTemplate: "fastify-plugin.ts",
2589
+ importName: "patchstackFastify",
2590
+ call: (v) => `${v}.register(patchstackFastify);`,
2591
+ label: "Fastify app",
2592
+ manualHint: "add `app.register(patchstackFastify)` right after you create your fastify() app"
2593
+ };
2594
+ var fastifyAdapter = {
2595
+ name: "fastify",
2596
+ label: "Fastify (Node)",
2597
+ detect: (cwd) => hasDependency(cwd, "fastify") && findAppInstance(cwd, SPEC4.appRe) !== null,
2598
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC4),
2599
+ verify: (cwd) => verifyRegister(cwd, SPEC4)
2600
+ };
2601
+
2602
+ // src/protect/install/adapters/express.ts
2603
+ var jsonParserRe = (appVar) => new RegExp(`^\\s*${appVar}\\.use\\(\\s*express\\.json\\(`, "m");
2604
+ var SPEC5 = {
2605
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*express\(\)/,
2606
+ guardTemplate: "express-guard.ts",
2607
+ guardTemplateEsm: "express-guard.js",
2608
+ guardTemplateCjs: "express-guard.cjs",
2609
+ importName: "patchstackMiddleware",
2610
+ call: (v) => `${v}.use(patchstackMiddleware);`,
2611
+ callAfter: jsonParserRe,
2612
+ requireCallAfter: true,
2613
+ label: "Express app",
2614
+ manualHint: "add `app.use(patchstackMiddleware)` after your JSON body parser and before the routes"
2615
+ };
2616
+ var expressAdapter = {
2617
+ name: "express",
2618
+ label: "Express (Node)",
2619
+ detect: (cwd) => hasDependency(cwd, "express") && findAppInstance(cwd, SPEC5.appRe) !== null,
2620
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC5),
2621
+ verify: (cwd) => verifyRegister(cwd, SPEC5)
2622
+ };
2623
+
2624
+ // src/protect/install/index.ts
2625
+ var ADAPTERS = [
2626
+ tanstackSupabaseAdapter,
2627
+ nextAdapter,
2628
+ sveltekitAdapter,
2629
+ astroAdapter,
2630
+ nestjsAdapter,
2631
+ fastifyAdapter,
2632
+ expressAdapter
2633
+ ];
2634
+ function runProtect(cwd, opts = {}) {
2635
+ const adapter = ADAPTERS.find((a) => a.detect(cwd));
2636
+ if (adapter) {
2637
+ const result = adapter.wire(cwd, opts);
2638
+ return { status: "wired", adapter: adapter.name, changed: result.changed };
2639
+ }
2640
+ const { changed, dir } = scaffoldGeneric(cwd, opts);
2641
+ const plan = wiringPlan(cwd, dir);
2642
+ log(plan);
2643
+ return { status: "scaffolded", adapter: "generic", changed, plan };
2644
+ }
2645
+ function runVerify(cwd) {
2646
+ const adapter = ADAPTERS.find((a) => a.detect(cwd));
2647
+ if (adapter) return { stack: adapter.label, ...adapter.verify(cwd) };
2648
+ return { stack: "generic", ...genericVerify(cwd) };
1807
2649
  }
1808
2650
 
1809
2651
  // src/setup.ts
1810
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2652
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
1811
2653
  import path10 from "path";
1812
2654
  var SCAN_COMMAND = "patchstack-connect scan";
1813
2655
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
@@ -1822,7 +2664,7 @@ function appendHook(existing, command) {
1822
2664
  }
1823
2665
  function wireBuildScripts(cwd, packageManager) {
1824
2666
  const target = path10.join(cwd, "package.json");
1825
- const raw = readFileSync4(target, "utf8");
2667
+ const raw = readFileSync6(target, "utf8");
1826
2668
  const pkg = JSON.parse(raw);
1827
2669
  const scripts = pkg.scripts ?? {};
1828
2670
  const build = scripts.build;
@@ -1866,7 +2708,7 @@ function wireBuildScripts(cwd, packageManager) {
1866
2708
  const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
1867
2709
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
1868
2710
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
1869
- writeFileSync3(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
2711
+ writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
1870
2712
  return packageManager === "bun" ? {
1871
2713
  changed: true,
1872
2714
  strategy: "build-chain",
@@ -1899,9 +2741,22 @@ Usage:
1899
2741
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
1900
2742
  build fingerprint, and ensure the widget
1901
2743
  tag in built pages (run as a postbuild step)
1902
- patchstack-connect protect Install always-on runtime protection (the
1903
- guard) into a TanStack Start + Supabase app.
1904
- Covers the browser + server-function paths.
2744
+ patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
2745
+ guard). Auto-wires supported server stacks;
2746
+ for others it scaffolds a
2747
+ generic guard + prints a wiring plan.
2748
+ --demo seeds a broad sample rule set (for
2749
+ demonstrations, not production).
2750
+ --check verifies the guard is wired (exit 1
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.
1905
2760
  patchstack-connect guide [--full] Show this project's setup status (what's done,
1906
2761
  what's missing, with tailored commands), then
1907
2762
  print the full setup guide. --full prints the
@@ -1917,11 +2772,17 @@ Options (for mark-build):
1917
2772
  --dir <path> Build output directory (default: auto-detect
1918
2773
  dist/ build/ out/ .output/public)
1919
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
+
1920
2779
  Environment:
1921
2780
  PATCHSTACK_SITE_UUID Site UUID
1922
2781
  PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
1923
2782
  PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
1924
2783
  PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production)
2784
+ PATCHSTACK_MODE (protect) Runtime guard mode: block (default) | dry-run
2785
+ PATCHSTACK_ROUTE_WAF (protect) Set to 1 to also screen every request at the route level (opt-in)
1925
2786
 
1926
2787
  Precedence: CLI flag > environment variable > .patchstackrc.json.
1927
2788
 
@@ -1931,8 +2792,10 @@ Examples:
1931
2792
  npx @patchstack/connect scan --dry-run
1932
2793
  npx @patchstack/connect init 550e8400-e29b-41d4-a716-446655440000
1933
2794
  npx @patchstack/connect scan --site-uuid 550e8400-...-446655440000
2795
+ npx @patchstack/connect demo node-serialize
2796
+ npx @patchstack/connect demo-guide node-serialize
1934
2797
  `;
1935
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir"]);
2798
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url"]);
1936
2799
  function parseArgs(argv) {
1937
2800
  const args = argv.slice(2);
1938
2801
  const positional = [];
@@ -2107,14 +2970,117 @@ function reportSourceWidget(siteUuid) {
2107
2970
  console.warn(`Widget: skipped (${err.message}).`);
2108
2971
  }
2109
2972
  }
2110
- async function runProtectCommand(_args) {
2973
+ async function runProtectCommand(args) {
2974
+ if (args.flags.get("check") === true) {
2975
+ const report = runVerify(process.cwd());
2976
+ console.log(`patchstack protect --check (${report.stack}):`);
2977
+ for (const c of report.checks) {
2978
+ console.log(` ${c.ok ? "\u2713" : "\u2717"} ${c.label}${!c.ok && c.hint ? ` \u2014 ${c.hint}` : ""}`);
2979
+ }
2980
+ console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
2981
+ return report.wired ? 0 : 1;
2982
+ }
2983
+ const demo = args.flags.get("demo") === true;
2111
2984
  try {
2112
- runProtect(process.cwd());
2985
+ runProtect(process.cwd(), { demo });
2113
2986
  } catch (err) {
2114
2987
  console.warn(`patchstack protect: skipped (${err.message}).`);
2115
2988
  }
2116
2989
  return 0;
2117
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
+ }
2118
3084
  async function runGuide(args) {
2119
3085
  let allDone = false;
2120
3086
  try {
@@ -2135,7 +3101,7 @@ async function runGuide(args) {
2135
3101
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
2136
3102
  console.log("");
2137
3103
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
2138
- console.log(readFileSync5(guidePath, "utf8"));
3104
+ console.log(readFileSync7(guidePath, "utf8"));
2139
3105
  return 0;
2140
3106
  }
2141
3107
  async function runSetup(args) {
@@ -2250,7 +3216,7 @@ async function runMarkBuild(args) {
2250
3216
  let marked = 0;
2251
3217
  let widgetTouched = 0;
2252
3218
  for (const file of files) {
2253
- const before = readFileSync5(file, "utf8");
3219
+ const before = readFileSync7(file, "utf8");
2254
3220
  let after = injectMarker(before, snippet);
2255
3221
  if (widgetUuid !== null) {
2256
3222
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -2260,7 +3226,7 @@ async function runMarkBuild(args) {
2260
3226
  after = ensured.html;
2261
3227
  }
2262
3228
  if (after !== before) {
2263
- writeFileSync4(file, after);
3229
+ writeFileSync7(file, after);
2264
3230
  marked += 1;
2265
3231
  }
2266
3232
  }
@@ -2287,6 +3253,10 @@ async function main() {
2287
3253
  return runMarkBuild(args);
2288
3254
  case "protect":
2289
3255
  return runProtectCommand(args);
3256
+ case "demo":
3257
+ return runDemoCommand(args);
3258
+ case "demo-guide":
3259
+ return runDemoGuideCommand(args);
2290
3260
  case "guide":
2291
3261
  return runGuide(args);
2292
3262
  case "setup":