@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/AGENT-INSTALL.md +2 -0
- package/README.md +36 -5
- package/dist/cli.js +470 -84
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/express-guard.cjs +31 -0
- package/dist/protect/templates/express-guard.js +30 -0
- package/dist/protect/templates/express-guard.ts +32 -0
- package/package.json +1 -1
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1922
|
+
const builtLayout = join2(HERE, "protect", "templates");
|
|
1711
1923
|
const candidates = [
|
|
1712
1924
|
builtLayout,
|
|
1713
|
-
|
|
1925
|
+
join2(HERE, "..", "templates"),
|
|
1714
1926
|
// source: src/protect/install/ → src/protect/templates
|
|
1715
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
2017
|
+
const dst = join3(cwd, "src/integrations/patchstack");
|
|
1806
2018
|
mkdirSync(dst, { recursive: true });
|
|
1807
|
-
copyFileSync(
|
|
2019
|
+
copyFileSync(join3(templates, "guard.ts"), join3(dst, "guard.ts"));
|
|
1808
2020
|
const changed = [GUARD_FILE];
|
|
1809
|
-
const rulesDst =
|
|
2021
|
+
const rulesDst = join3(dst, "rules.json");
|
|
1810
2022
|
if (opts.demo) {
|
|
1811
|
-
copyFileSync(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1888
|
-
const clientPath =
|
|
1889
|
-
const startPath =
|
|
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
|
|
2123
|
+
import { join as join4 } from "path";
|
|
1912
2124
|
function hasNextDep(cwd) {
|
|
1913
2125
|
try {
|
|
1914
|
-
const pkg = JSON.parse(read(
|
|
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(
|
|
2135
|
+
if (existsSync6(join4(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
|
|
1924
2136
|
}
|
|
1925
|
-
const useSrc = existsSync6(
|
|
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 =
|
|
2149
|
+
const dir = join4(cwd, mw.relDir === "." ? "" : mw.relDir);
|
|
1938
2150
|
mkdirSync2(dir, { recursive: true });
|
|
1939
|
-
const rulesDst =
|
|
2151
|
+
const rulesDst = join4(dir, "patchstack.rules.json");
|
|
1940
2152
|
const changed = [];
|
|
1941
2153
|
if (opts.demo || !existsSync6(rulesDst)) {
|
|
1942
|
-
copyFileSync2(
|
|
2154
|
+
copyFileSync2(join4(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
|
|
1943
2155
|
changed.push(rulesFile(mw.relDir));
|
|
1944
2156
|
}
|
|
1945
|
-
const mwPath =
|
|
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(
|
|
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(
|
|
1962
|
-
const rulesPresent = existsSync6(
|
|
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
|
|
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
|
|
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(
|
|
2203
|
+
const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
|
|
1992
2204
|
const seamRel = existing ?? spec.target;
|
|
1993
|
-
mkdirSync3(dirname2(
|
|
1994
|
-
const rulesDst =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2224
|
+
const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
|
|
2013
2225
|
const seamRel = existing ?? spec.target;
|
|
2014
|
-
const present = existing ? read(
|
|
2015
|
-
const rulesPresent = existsSync7(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
2302
|
+
import { join as join8, relative, dirname as dirname3 } from "path";
|
|
2091
2303
|
function findAppInstance(cwd, re) {
|
|
2092
|
-
const root = existsSync8(
|
|
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 =
|
|
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, "/")
|
|
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
|
|
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
|
|
2353
|
+
import { join as join9 } from "path";
|
|
2141
2354
|
var GUARD_MARKER = "patchstack/guard";
|
|
2142
2355
|
function genericDir(cwd) {
|
|
2143
|
-
return existsSync9(
|
|
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 =
|
|
2361
|
+
const dst = join9(cwd, dir);
|
|
2149
2362
|
mkdirSync4(dst, { recursive: true });
|
|
2150
|
-
copyFileSync4(
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
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(
|
|
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(
|
|
2390
|
+
const hits = names.filter((n) => existsSync9(join9(cwd, n)));
|
|
2177
2391
|
try {
|
|
2178
|
-
const pkg = JSON.parse(read(
|
|
2179
|
-
if (typeof pkg.main === "string" && existsSync9(
|
|
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(
|
|
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(
|
|
2208
|
-
const imported = scaffolded && guardIsImported(cwd,
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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++)
|
|
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
|
-
|
|
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(
|
|
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
|
|
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: "
|
|
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)`
|
|
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
|
|
2471
|
-
|
|
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":
|