@patchstack/connect 0.5.27 → 0.5.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENT-INSTALL.md +75 -25
  2. package/README.md +28 -14
  3. package/dist/{chunk-YG5C7HYB.js → chunk-52IV7LN7.js} +1 -1
  4. package/dist/{chunk-YG5C7HYB.js.map → chunk-52IV7LN7.js.map} +1 -1
  5. package/dist/cli.js +2749 -372
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +10 -6
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +5 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.js +10 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/protect/templates/astro-middleware.ts +5 -4
  14. package/dist/protect/templates/express-guard.cjs +5 -8
  15. package/dist/protect/templates/express-guard.js +5 -8
  16. package/dist/protect/templates/express-guard.ts +5 -8
  17. package/dist/protect/templates/fastify-plugin.cjs +52 -6
  18. package/dist/protect/templates/fastify-plugin.js +52 -6
  19. package/dist/protect/templates/fastify-plugin.ts +53 -7
  20. package/dist/protect/templates/fetch-guard.ts +52 -0
  21. package/dist/protect/templates/generic-guard.cjs +6 -5
  22. package/dist/protect/templates/generic-guard.js +6 -5
  23. package/dist/protect/templates/generic-guard.ts +5 -8
  24. package/dist/protect/templates/guard.ts +4 -7
  25. package/dist/protect/templates/next-guard.ts +5 -4
  26. package/dist/protect/templates/next-middleware.ts +5 -4
  27. package/dist/protect/templates/nuxt-middleware.ts +5 -4
  28. package/dist/protect/templates/sveltekit-hooks.ts +5 -4
  29. package/dist/protect.cjs +10 -6
  30. package/dist/protect.cjs.map +1 -1
  31. package/dist/protect.d.cts +1 -1
  32. package/dist/protect.d.ts +1 -1
  33. package/dist/protect.edge.js +2 -2
  34. package/dist/protect.edge.js.map +1 -1
  35. package/dist/protect.js +4 -4
  36. package/dist/protect.js.map +1 -1
  37. package/dist/{refresh-manifest-THITZHBD.js → refresh-manifest-EBORKM75.js} +10 -6
  38. package/dist/refresh-manifest-EBORKM75.js.map +1 -0
  39. package/package.json +5 -2
  40. package/dist/refresh-manifest-THITZHBD.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync19 } from "fs";
4
+ import { readFileSync as readFileSync20 } from "fs";
5
5
  import path16 from "path";
6
- import { createRequire as createRequire3 } from "module";
6
+ import { createRequire as createRequire6 } from "module";
7
7
 
8
8
  // src/safe-file.ts
9
9
  import {
@@ -1073,7 +1073,7 @@ async function scanLockfile(cwd) {
1073
1073
  await detectLockfile(cwd);
1074
1074
  }
1075
1075
  const declared = await readDeclaredDependencyNames(cwd);
1076
- const warnings = [];
1076
+ const warnings2 = [];
1077
1077
  let firstParsed = null;
1078
1078
  let walked = null;
1079
1079
  let walkWasCandidate = false;
@@ -1087,7 +1087,7 @@ async function scanLockfile(cwd) {
1087
1087
  continue;
1088
1088
  }
1089
1089
  const unreadable = unreadableWarning(candidate.filename, report);
1090
- if (unreadable !== null) warnings.push(unreadable);
1090
+ if (unreadable !== null) warnings2.push(unreadable);
1091
1091
  if (candidate.strategy === "node-modules-walk") {
1092
1092
  walked = packages;
1093
1093
  walkWasCandidate = true;
@@ -1098,16 +1098,16 @@ async function scanLockfile(cwd) {
1098
1098
  for (const source of parsedSources) {
1099
1099
  const missing = missingDependencies(declared, source.packages);
1100
1100
  if (missing.length > 0) {
1101
- warnings.push(staleWarning(source.filename, missing));
1101
+ warnings2.push(staleWarning(source.filename, missing));
1102
1102
  continue;
1103
1103
  }
1104
1104
  const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
1105
1105
  (other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
1106
1106
  );
1107
1107
  if (conflicts.length === 0) {
1108
- return manifestWith(source.packages, warnings);
1108
+ return manifestWith(source.packages, warnings2);
1109
1109
  }
1110
- warnings.push(conflictWarning(source.filename, conflicts));
1110
+ warnings2.push(conflictWarning(source.filename, conflicts));
1111
1111
  break;
1112
1112
  }
1113
1113
  if (walked === null) {
@@ -1120,12 +1120,12 @@ async function scanLockfile(cwd) {
1120
1120
  if (walked !== null) {
1121
1121
  const missing = missingDependencies(declared, walked);
1122
1122
  if (missing.length > 0) {
1123
- warnings.push(staleWarning("node_modules/", missing));
1123
+ warnings2.push(staleWarning("node_modules/", missing));
1124
1124
  }
1125
- warnings.push(
1125
+ warnings2.push(
1126
1126
  walkWasCandidate ? "Checked the installed packages in node_modules/, which is what the build loads." : "Checked the installed packages in node_modules/ instead. Delete the out-of-date package list file to stop this warning."
1127
1127
  );
1128
- return manifestWith(walked, warnings);
1128
+ return manifestWith(walked, warnings2);
1129
1129
  }
1130
1130
  if (firstParsed === null) {
1131
1131
  const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
@@ -1134,13 +1134,13 @@ async function scanLockfile(cwd) {
1134
1134
  "LOCKFILE_NOT_FOUND"
1135
1135
  );
1136
1136
  }
1137
- warnings.push(
1137
+ warnings2.push(
1138
1138
  `No package list matches package.json; checked ${firstParsed.filename}, so some packages may be missing. Reinstall your packages to fix this.`
1139
1139
  );
1140
- return manifestWith(firstParsed.packages, warnings);
1140
+ return manifestWith(firstParsed.packages, warnings2);
1141
1141
  }
1142
- function manifestWith(packages, warnings) {
1143
- return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
1142
+ function manifestWith(packages, warnings2) {
1143
+ return warnings2.length > 0 ? { ecosystem: "npm", packages, warnings: warnings2 } : { ecosystem: "npm", packages };
1144
1144
  }
1145
1145
  function otherCompleteSources(all, current, declared) {
1146
1146
  return all.filter(
@@ -1205,10 +1205,10 @@ function buildWirePayload(manifest, options = {}) {
1205
1205
  const byIdentity = /* @__PURE__ */ new Map();
1206
1206
  let entriesWithoutPath = 0;
1207
1207
  for (const entry of manifest.packages) {
1208
- const identity = `${entry.name}@${entry.version}`;
1208
+ const identity2 = `${entry.name}@${entry.version}`;
1209
1209
  const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
1210
1210
  if (location === void 0) entriesWithoutPath++;
1211
- const existing = byIdentity.get(identity);
1211
+ const existing = byIdentity.get(identity2);
1212
1212
  if (existing) {
1213
1213
  if (location !== void 0) addPath(existing, location);
1214
1214
  continue;
@@ -1221,7 +1221,7 @@ function buildWirePayload(manifest, options = {}) {
1221
1221
  }
1222
1222
  const wire6 = { name: entry.name, version: entry.version };
1223
1223
  if (location !== void 0) addPath(wire6, location);
1224
- byIdentity.set(identity, wire6);
1224
+ byIdentity.set(identity2, wire6);
1225
1225
  wirePackages.push(wire6);
1226
1226
  }
1227
1227
  for (const wire6 of wirePackages) wire6.paths?.sort();
@@ -1329,7 +1329,7 @@ function compareSegments(a, b) {
1329
1329
  // src/checksum.ts
1330
1330
  import { createHash } from "crypto";
1331
1331
  function computeManifestChecksum(packages) {
1332
- const canonical = packages.map((pkg) => ({ name: pkg.name, version: pkg.version })).sort((a, b) => {
1332
+ const canonical2 = packages.map((pkg) => ({ name: pkg.name, version: pkg.version })).sort((a, b) => {
1333
1333
  if (a.name !== b.name) {
1334
1334
  return a.name < b.name ? -1 : 1;
1335
1335
  }
@@ -1338,7 +1338,7 @@ function computeManifestChecksum(packages) {
1338
1338
  }
1339
1339
  return 0;
1340
1340
  });
1341
- return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 12);
1341
+ return createHash("sha256").update(JSON.stringify(canonical2)).digest("hex").slice(0, 12);
1342
1342
  }
1343
1343
 
1344
1344
  // src/hosting.ts
@@ -1379,6 +1379,18 @@ function isSafeOrigin(value) {
1379
1379
  return false;
1380
1380
  }
1381
1381
  }
1382
+ function safeBaseUrl(candidate, fallback, label) {
1383
+ if (typeof candidate !== "string" || candidate === "") return fallback;
1384
+ if (isSafeOrigin(candidate)) return candidate;
1385
+ warnOnce(label, `[patchstack] ignoring unsafe ${label} override (${candidate}): must be https (or localhost). Using the default.`);
1386
+ return fallback;
1387
+ }
1388
+ var warned = /* @__PURE__ */ new Set();
1389
+ function warnOnce(key, message) {
1390
+ if (warned.has(key)) return;
1391
+ warned.add(key);
1392
+ console.warn(message);
1393
+ }
1382
1394
 
1383
1395
  // src/endpoint-policy.ts
1384
1396
  function assertConnectableEndpoint(config, target = config.endpoint) {
@@ -1707,7 +1719,7 @@ async function postInputMap(config, map, buildId) {
1707
1719
  }
1708
1720
  const url = buildInputMapUrl(config.endpoint, config.siteUuid);
1709
1721
  try {
1710
- const canonical = canonicalBuildId(buildId);
1722
+ const canonical2 = canonicalBuildId(buildId);
1711
1723
  const response = await pulseFetch(config, url, {
1712
1724
  method: "POST",
1713
1725
  headers: {
@@ -1720,7 +1732,7 @@ async function postInputMap(config, map, buildId) {
1720
1732
  // to one of them as enforceable only to a guard carrying that same map. Omitted entirely when this
1721
1733
  // run could not bind the map into a pre-bundle guard — an absent field is a question the server
1722
1734
  // answers by withholding enforceability, which is the safe direction.
1723
- body: JSON.stringify(canonical === null ? map : { ...map, build_id: canonical }),
1735
+ body: JSON.stringify(canonical2 === null ? map : { ...map, build_id: canonical2 }),
1724
1736
  signal: AbortSignal.timeout(config.timeoutMs)
1725
1737
  });
1726
1738
  if (response.status === 404) {
@@ -2627,10 +2639,13 @@ async function resolveConfig(options) {
2627
2639
  "CONFIG_INVALID"
2628
2640
  );
2629
2641
  }
2630
- const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
2631
- const environment = environmentRaw ?? inferred.environment;
2632
- const environmentEvidence = inferred?.evidence ?? [];
2633
- const environmentSource = environmentRaw !== void 0 ? "override" : inferred.source;
2642
+ const inferred = fromEnv.environment === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
2643
+ const fileLabelOverruled = fromEnv.environment === void 0 && environmentRaw !== void 0 && environmentRaw !== "production" && inferred.environment === "production";
2644
+ const labelled = fileLabelOverruled ? void 0 : environmentRaw;
2645
+ const environment = labelled ?? inferred.environment;
2646
+ const environmentEvidence = labelled === void 0 ? inferred.evidence : [];
2647
+ const environmentSource = labelled !== void 0 ? "override" : inferred.source;
2648
+ const ignoredFileEnvironment = fileLabelOverruled ? environmentRaw : null;
2634
2649
  if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
2635
2650
  throw new PatchstackError(
2636
2651
  `Site UUID "${siteUuid}" does not look like a valid UUID.`,
@@ -2645,20 +2660,21 @@ async function resolveConfig(options) {
2645
2660
  }
2646
2661
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
2647
2662
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
2648
- const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
2663
+ const identity2 = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
2649
2664
  const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
2650
2665
  return {
2651
2666
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
2652
2667
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
2653
2668
  pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
2654
- siteUrl: identity.url,
2655
- siteName: identity.name,
2669
+ siteUrl: identity2.url,
2670
+ siteName: identity2.name,
2656
2671
  endpoint,
2657
2672
  endpointTrusted,
2658
2673
  timeoutMs,
2659
2674
  environment,
2660
2675
  environmentEvidence,
2661
2676
  environmentSource,
2677
+ ignoredFileEnvironment,
2662
2678
  widget: fromFile.widget !== false,
2663
2679
  claimToken
2664
2680
  };
@@ -2713,7 +2729,7 @@ async function writeAtomicFile(target, content, mode) {
2713
2729
  throw error;
2714
2730
  }
2715
2731
  }
2716
- async function ensureIgnored(cwd, entry) {
2732
+ async function ensureIgnored(cwd, entry, label = "Patchstack credential \u2014 never commit this") {
2717
2733
  const target = path8.join(cwd, ".gitignore");
2718
2734
  let existing = "";
2719
2735
  try {
@@ -2727,7 +2743,7 @@ async function ensureIgnored(cwd, entry) {
2727
2743
  if (ignoresEntry(existing, entry)) return { ignored: true };
2728
2744
  const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
2729
2745
  const block = `${separator}
2730
- # Patchstack credential \u2014 never commit this
2746
+ # ${label}
2731
2747
  ${entry}
2732
2748
  `;
2733
2749
  try {
@@ -3040,6 +3056,9 @@ function buildInjectionSnippet(checksum, stack) {
3040
3056
  }
3041
3057
  return `<script ${MARKER_ATTR}>${statements.join("")}</script>`;
3042
3058
  }
3059
+ function buildSandboxSnippet() {
3060
+ return `<script ${MARKER_ATTR}>window.__PATCHSTACK_ENV__="sandbox";</script>`;
3061
+ }
3043
3062
  function injectMarker(html, snippet) {
3044
3063
  const stripped = html.replace(
3045
3064
  new RegExp(`\\s*<script ${MARKER_ATTR}[^>]*>[\\s\\S]*?</script>`, "gi"),
@@ -3053,6 +3072,10 @@ function injectMarker(html, snippet) {
3053
3072
  }
3054
3073
  return stripped + snippet;
3055
3074
  }
3075
+ var HAND_MARKER_RE = /\s*<script(?![^>]*\bsrc\s*=)[^>]*>\s*(?:window\.__PATCHSTACK_(?:PROD|BUILD|STACK)__\s*=\s*[^;<]*;?\s*)+<\/script>/gi;
3076
+ function removeHandMarkers(html) {
3077
+ return html.replace(HAND_MARKER_RE, "");
3078
+ }
3056
3079
  var JSX_SHELL_FRAMEWORKS = /* @__PURE__ */ new Set([
3057
3080
  "next",
3058
3081
  "remix",
@@ -3196,7 +3219,7 @@ function ensureSourceMarker(cwd, shell, framework, checksum = null) {
3196
3219
  }
3197
3220
 
3198
3221
  // src/guide.ts
3199
- import { existsSync as existsSync12, readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
3222
+ import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
3200
3223
  import path12 from "path";
3201
3224
 
3202
3225
  // src/architecture.ts
@@ -4099,18 +4122,144 @@ function templatesDir() {
4099
4122
  }
4100
4123
 
4101
4124
  // src/protect/install/adapters/tanstack-supabase.ts
4102
- import { existsSync as existsSync4 } from "fs";
4125
+ import { existsSync as existsSync6 } from "fs";
4126
+ import { join as join9 } from "path";
4127
+
4128
+ // src/protect/install/syntax.ts
4129
+ import { createRequire } from "module";
4103
4130
  import { join as join6 } from "path";
4131
+ function sourceCompiler(cwd) {
4132
+ for (const base of [join6(cwd, "package.json"), import.meta.url]) {
4133
+ try {
4134
+ return createRequire(base)("typescript");
4135
+ } catch {
4136
+ }
4137
+ }
4138
+ return null;
4139
+ }
4140
+ function parsedSource(ts, file, source) {
4141
+ const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
4142
+ return tree.parseDiagnostics.length ? null : tree;
4143
+ }
4144
+ function statementEndLine(ts, file, source, line) {
4145
+ const tree = parsedSource(ts, file, source);
4146
+ if (!tree) return null;
4147
+ let end = null;
4148
+ const visit = (node) => {
4149
+ if ((ts.isImportDeclaration(node) || ts.isVariableStatement(node) || ts.isExpressionStatement(node)) && tree.getLineAndCharacterOfPosition(node.getStart(tree)).line === line) {
4150
+ const tail = source.slice(node.end).split("\n", 1)[0].trim();
4151
+ if (tail !== "" && !tail.startsWith("//")) return;
4152
+ end = tree.getLineAndCharacterOfPosition(node.end).line;
4153
+ return;
4154
+ }
4155
+ ts.forEachChild(node, visit);
4156
+ };
4157
+ visit(tree);
4158
+ return end;
4159
+ }
4160
+ function statementPosition(ts, file, source, line, matches) {
4161
+ const tree = parsedSource(ts, file, source);
4162
+ if (!tree) return null;
4163
+ const statements = [];
4164
+ const visit = (node) => {
4165
+ if ((ts.isVariableStatement(node) || ts.isExpressionStatement(node)) && tree.getLineAndCharacterOfPosition(node.getStart(tree)).line === line && matches.test(node.getText(tree))) statements.push({ start: node.getStart(tree), end: node.end });
4166
+ ts.forEachChild(node, visit);
4167
+ };
4168
+ visit(tree);
4169
+ return statements.length === 1 ? statements[0] : null;
4170
+ }
4171
+
4172
+ // src/protect/install/template-match.ts
4173
+ import { existsSync as existsSync4 } from "fs";
4174
+ import { join as join7 } from "path";
4175
+ function matchesGuardTemplate(cwd, file, template) {
4176
+ if (!existsSync4(join7(cwd, file))) return false;
4177
+ const ts = sourceCompiler(cwd);
4178
+ if (!ts) return false;
4179
+ const actual = canonical(ts, file, read(join7(cwd, file)));
4180
+ const expected = canonical(ts, template, read(join7(templatesDir(), template)));
4181
+ return actual !== null && expected !== null && actual === expected;
4182
+ }
4183
+ function canonical(ts, file, source) {
4184
+ const tree = parsedSource(ts, file, source);
4185
+ if (!tree) return null;
4186
+ const normalized2 = ts.transform(tree, [(context) => (root) => {
4187
+ const visit = (node) => {
4188
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "PS_SITE_UUID" && node.initializer && ts.isStringLiteral(node.initializer)) {
4189
+ return ts.factory.updateVariableDeclaration(
4190
+ node,
4191
+ node.name,
4192
+ node.exclamationToken,
4193
+ node.type,
4194
+ ts.factory.createStringLiteral("__PATCHSTACK_SITE_UUID__")
4195
+ );
4196
+ }
4197
+ if (ts.isStringLiteral(node)) return ts.factory.createStringLiteral(node.text);
4198
+ return ts.visitEachChild(node, visit, context);
4199
+ };
4200
+ return ts.visitEachChild(root, visit, context);
4201
+ }]);
4202
+ try {
4203
+ return ts.createPrinter({ removeComments: true }).printFile(normalized2.transformed[0]);
4204
+ } finally {
4205
+ normalized2.dispose();
4206
+ }
4207
+ }
4208
+
4209
+ // src/protect/install/template-upgrade.ts
4210
+ import { createHash as createHash2 } from "crypto";
4211
+ import { existsSync as existsSync5 } from "fs";
4212
+ import { join as join8 } from "path";
4213
+ var PREVIOUS = {
4214
+ "generic-guard.ts": "2c6bb8324b909e646049c36725704b474a8bfb1b6849c392eb3462eed83727e9",
4215
+ "generic-guard.js": "42357bcf0cb0541e584821d03e06ced28f98d4ef91a76a50e39008ef18c63709",
4216
+ "generic-guard.cjs": "8de21893474a3139eff9e8fc48a0c13e976e64421281db9e8e93b0c0549361b8",
4217
+ "express-guard.ts": "6c30cc82b7cbd3fee62d423fcb20b27eba1d6cc1ed054efcf9d64eb360bc7b17",
4218
+ "express-guard.js": "bce7c76a061297621791d4ebf838449e3880d05f333588fed7bf88431d1e712e",
4219
+ "express-guard.cjs": "3a6430f00fc159c28a897e700cc492c306f39b556f0f4cb4b1e718993b6c40b4",
4220
+ "fastify-plugin.ts": "5d606cb8f8f2e6fe35b9d377c5568c5a2090a81af9f0acb219ebc15a713792e9",
4221
+ "fastify-plugin.js": "03327d437513571d53554ca34c84ba170ebeebb5e89c2602ae340fb82178ab48",
4222
+ "fastify-plugin.cjs": "b3754bf023ebb5f2a22e0a95cfe74bb783034ec3db9ce61a3e7202237e09e1dd",
4223
+ "guard.ts": "270e29769f5a7b3ab70ee39970be5a4e150e1b3949877687dca1ed83596afae7",
4224
+ "sveltekit-hooks.ts": "0a68389ac9018dc0ab5805356ad78301f039cb4c701bf642e7f986cc959da990",
4225
+ "astro-middleware.ts": "f217e302fb7019de451c7538e9266a5ce38aa6a6651072b6291d4593b31f60ce",
4226
+ "nuxt-middleware.ts": "e377c48cc8c08d20c61c6119bb5f4c8991f0eed3ebd31310b8a0fc6eb5505e4e",
4227
+ "next-middleware.ts": "65c163cdb2ddda4fe53c1a3cd20aa6ab63ad7595a66985d9141d595374ecfab3",
4228
+ "next-guard.ts": "e37c4d67619df9d850a7d94b567bd76841b6ac8e774a5b442689482f5374c451"
4229
+ };
4230
+ var identity = /const PS_SITE_UUID = "([^"]*)";/;
4231
+ var normalized = (source) => source.replace(identity, 'const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";');
4232
+ function installTemplate(cwd, file, template) {
4233
+ const next = read(join8(templatesDir(), template));
4234
+ const target = join8(cwd, file);
4235
+ if (!existsSync5(target)) {
4236
+ writeProjectFileSync(cwd, target, next);
4237
+ return true;
4238
+ }
4239
+ const previous = read(target);
4240
+ if (normalized(previous) === normalized(next)) return false;
4241
+ if (createHash2("sha256").update(normalized(previous)).digest("hex") !== PREVIOUS[template]) {
4242
+ log(`${file} is customized or unrecognized \u2014 preserved; review its request/response wiring and rule-refresh settings manually.`);
4243
+ return false;
4244
+ }
4245
+ const site = identity.exec(previous)?.[1];
4246
+ writeProjectFileSync(cwd, target, site ? next.replace(identity, () => `const PS_SITE_UUID = ${JSON.stringify(site)};`) : next);
4247
+ log(`updated unchanged generated helper ${file}`);
4248
+ return true;
4249
+ }
4250
+
4251
+ // src/protect/install/adapters/tanstack-supabase.ts
4104
4252
  var CLIENT_TUNNEL = [
4105
4253
  "",
4106
4254
  " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
4107
4255
  " // (same-origin) so payloads are inspected before they reach Supabase.",
4108
4256
  " if (typeof window !== 'undefined') {",
4109
- " const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
4110
- " const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
4111
- " headers.set('x-ps-target', target);",
4257
+ " const original = new Request(input, init);",
4112
4258
  " const guardUrl = new URL('/_patchstack/guard', window.location.origin).toString();",
4113
- " return fetch(guardUrl, { ...init, method, headers });",
4259
+ " const forwarded = new Request(guardUrl, original);",
4260
+ " headers.forEach((value, key) => forwarded.headers.set(key, value));",
4261
+ " forwarded.headers.set('x-ps-target', original.url);",
4262
+ " return fetch(forwarded);",
4114
4263
  " }",
4115
4264
  ""
4116
4265
  ].join("\n");
@@ -4119,19 +4268,19 @@ var GUARD_IMPORT_RE = /import \{[^}]*\} from "@\/integrations\/patchstack\/guard
4119
4268
  var START_IMPORTS = ['import { getRequest } from "@tanstack/react-start/server";', GUARD_IMPORT].join("\n");
4120
4269
  var REQUEST_MIDDLEWARE_BLOCK = [
4121
4270
  "// #region patchstack-guard (managed by patchstack-connect protect \u2014 do not edit)",
4122
- "// Browser\u2192Supabase tunnel + response screening; optional route WAF via PATCHSTACK_ROUTE_WAF=1.",
4271
+ "// Screen native requests and responses as well as the browser\u2192Supabase tunnel.",
4123
4272
  "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
4124
4273
  " const request = getRequest();",
4125
4274
  " if (request) {",
4126
4275
  " const { pathname } = new URL(request.url);",
4127
4276
  " if (pathname === GUARD_PATH) return handleGuardRequest(request);",
4128
- ' if (process.env.PATCHSTACK_ROUTE_WAF === "1") {',
4129
- " const blocked = await guardRequest(request);",
4130
- " if (blocked) return blocked;",
4131
- " }",
4277
+ " const blocked = await guardRequest(request);",
4278
+ " if (blocked) return blocked;",
4132
4279
  " }",
4133
4280
  " // The request is passed so route/method-scoped response rules can apply their scope.",
4134
- " return screenResponse(await next(), request);",
4281
+ " const result = await next();",
4282
+ " if (result instanceof Response) return screenResponse(result, request);",
4283
+ " return { ...result, response: await screenResponse(result.response, request) };",
4135
4284
  "});",
4136
4285
  "// #endregion patchstack-guard"
4137
4286
  ].join("\n");
@@ -4170,8 +4319,8 @@ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
4170
4319
  return s.replace(insertBefore, block + "\n\n" + insertBefore);
4171
4320
  }
4172
4321
  function detect(cwd) {
4173
- const pkgPath = join6(cwd, "package.json");
4174
- if (!existsSync4(pkgPath)) return false;
4322
+ const pkgPath = join9(cwd, "package.json");
4323
+ if (!existsSync6(pkgPath)) return false;
4175
4324
  let pkg;
4176
4325
  try {
4177
4326
  pkg = JSON.parse(read(pkgPath));
@@ -4179,22 +4328,22 @@ function detect(cwd) {
4179
4328
  return false;
4180
4329
  }
4181
4330
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
4182
- return Boolean(deps["@tanstack/react-start"]) && existsSync4(join6(cwd, "src/start.ts")) && existsSync4(join6(cwd, "src/integrations/supabase/client.ts"));
4331
+ return Boolean(deps["@tanstack/react-start"]) && existsSync6(join9(cwd, "src/start.ts")) && existsSync6(join9(cwd, "src/integrations/supabase/client.ts"));
4183
4332
  }
4184
4333
  var GUARD_FILE = "src/integrations/patchstack/guard.ts";
4185
4334
  function scaffold(cwd, opts) {
4186
4335
  const templates = templatesDir();
4187
- const dst = join6(cwd, "src/integrations/patchstack");
4336
+ const dst = join9(cwd, "src/integrations/patchstack");
4188
4337
  ensureProjectDirectorySync(cwd, dst);
4189
- copyProjectFileSync(cwd, join6(templates, "guard.ts"), join6(dst, "guard.ts"));
4190
- const changed = [GUARD_FILE];
4191
- const rulesDst = join6(dst, "rules.json");
4338
+ const changed = [];
4339
+ if (installTemplate(cwd, GUARD_FILE, "guard.ts")) changed.push(GUARD_FILE);
4340
+ const rulesDst = join9(dst, "rules.json");
4192
4341
  if (opts.demo) {
4193
- copyProjectFileSync(cwd, join6(templates, "demo-rules.json"), rulesDst);
4342
+ copyProjectFileSync(cwd, join9(templates, "demo-rules.json"), rulesDst);
4194
4343
  changed.push("src/integrations/patchstack/rules.json");
4195
4344
  log("scaffolded guard.ts + rules.json (demo sample rule set)");
4196
- } else if (!existsSync4(rulesDst)) {
4197
- copyProjectFileSync(cwd, join6(templates, "rules.json"), rulesDst);
4345
+ } else if (!existsSync6(rulesDst)) {
4346
+ copyProjectFileSync(cwd, join9(templates, "rules.json"), rulesDst);
4198
4347
  changed.push("src/integrations/patchstack/rules.json");
4199
4348
  log("scaffolded guard.ts + rules.json (starter rules)");
4200
4349
  } else {
@@ -4202,37 +4351,74 @@ function scaffold(cwd, opts) {
4202
4351
  }
4203
4352
  return changed;
4204
4353
  }
4205
- function patchClient(cwd) {
4206
- const p = join6(cwd, "src/integrations/supabase/client.ts");
4207
- const s = read(p);
4354
+ function patchClient(ts, s) {
4355
+ const tree = parsedSource(ts, "client.ts", s);
4356
+ if (!tree) return null;
4357
+ const anchors = [];
4358
+ const visit = (node) => {
4359
+ if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression)) {
4360
+ const call = node.expression;
4361
+ if (call.expression.getText(tree) === "headers.set" && call.arguments[0] && ts.isStringLiteral(call.arguments[0]) && call.arguments[0].text === "apikey" && call.arguments[1]?.getText(tree) === "supabaseKey") {
4362
+ const block = node.parent;
4363
+ const fn = block.parent;
4364
+ if (ts.isBlock(block) && ts.isArrowFunction(fn) && fn.parameters.length === 2 && fn.parameters[0].name.getText(tree) === "input" && fn.parameters[1].name.getText(tree) === "init" && block.statements.some((statement) => ts.isVariableStatement(statement) && statement.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && d.name.text === "headers" && d.initializer && ts.isNewExpression(d.initializer) && d.initializer.expression.getText(tree) === "Headers"))) anchors.push(node.end);
4365
+ }
4366
+ }
4367
+ ts.forEachChild(node, visit);
4368
+ };
4369
+ visit(tree);
4370
+ if (anchors.length !== 1) return null;
4208
4371
  if (s.includes("x-ps-target")) {
4209
- log("client.ts already wired");
4210
- return false;
4211
- }
4212
- const anchor = "headers.set('apikey', supabaseKey);";
4213
- if (!s.includes(anchor)) {
4214
- log("client.ts anchor not found \u2014 skipping (template changed?)");
4215
- return false;
4372
+ const code = ts.createPrinter({ removeComments: true }).printFile(tree);
4373
+ return /\.set\(['"]x-ps-target['"],/.test(code) && /return fetch\(/.test(code) ? s : null;
4216
4374
  }
4217
- writeProjectFileSync(cwd, p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL), { encoding: "utf8" });
4218
- log("patched client.ts (tunnel Supabase through the guard)");
4219
- return true;
4375
+ return s.slice(0, anchors[0]) + "\n" + CLIENT_TUNNEL + s.slice(anchors[0]);
4220
4376
  }
4221
- function patchStart(cwd) {
4222
- const p = join6(cwd, "src/start.ts");
4223
- let s = read(p);
4377
+ function patchStart(ts, original) {
4378
+ let s = original;
4224
4379
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
4225
4380
  const exportAnchor = "export const startInstance";
4226
4381
  const rmAnchor = "requestMiddleware: [";
4382
+ const tree = parsedSource(ts, "start.ts", s);
4383
+ if (!tree) return null;
4384
+ const startImport = tree.statements.find((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@tanstack/react-start" && !node.importClause?.isTypeOnly && !node.importClause?.name && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && node.importClause.namedBindings.elements.length === 2 && node.importClause.namedBindings.elements.every((e) => !e.propertyName && !e.isTypeOnly && ["createStart", "createMiddleware"].includes(e.name.text)));
4385
+ if (!startImport) return null;
4386
+ const declaration = tree.statements.filter(ts.isVariableStatement).filter((node) => node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)).flatMap((node) => [...node.declarationList.declarations]).find((node) => ts.isIdentifier(node.name) && node.name.text === "startInstance");
4387
+ const call = declaration?.initializer;
4388
+ if (!call || !ts.isCallExpression(call) || call.expression.getText(tree) !== "createStart" || call.arguments.length !== 1) return null;
4389
+ const factory = call.arguments[0];
4390
+ if (!ts.isArrowFunction(factory) || factory.parameters.length) return null;
4391
+ let config = factory.body;
4392
+ while (ts.isParenthesizedExpression(config)) config = config.expression;
4393
+ if (!ts.isObjectLiteralExpression(config)) return null;
4394
+ if (config.properties.some((p) => !ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name))) return null;
4395
+ const edits = [{ start: startImport.getStart(tree), end: startImport.end, text: importAnchor }];
4396
+ for (const name of ["requestMiddleware", "functionMiddleware"]) {
4397
+ const properties = config.properties.filter((p) => p.name?.getText(tree) === name);
4398
+ if (properties.length > 1 || name === "requestMiddleware" && !properties.length) return null;
4399
+ const property = properties[0];
4400
+ if (!property || !ts.isPropertyAssignment(property)) continue;
4401
+ if (!ts.isArrayLiteralExpression(property.initializer)) return null;
4402
+ edits.push({ start: property.getStart(tree), end: property.initializer.getStart(tree) + 1, text: `${name}: [` });
4403
+ }
4404
+ const guardImports = tree.statements.filter((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@/integrations/patchstack/guard");
4405
+ if (guardImports.length > 1) return null;
4406
+ const guardImport = guardImports[0];
4407
+ if (guardImport) {
4408
+ if (!ts.isImportDeclaration(guardImport) || !guardImport.importClause?.namedBindings || !ts.isNamedImports(guardImport.importClause.namedBindings) || guardImport.importClause.namedBindings.elements.some((e) => e.propertyName || e.isTypeOnly || !["GUARD_PATH", "handleGuardRequest", "inspectServerFn", "screenResponse", "guardRequest"].includes(e.name.text))) return null;
4409
+ edits.push({ start: guardImport.getStart(tree), end: guardImport.end, text: GUARD_IMPORT });
4410
+ }
4411
+ for (const edit of edits.sort((a, b) => b.start - a.start)) s = s.slice(0, edit.start) + edit.text + s.slice(edit.end);
4227
4412
  if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
4228
- log("start.ts anchors not found \u2014 skipping (template changed?)");
4229
- return false;
4413
+ return null;
4230
4414
  }
4231
- const original = s;
4415
+ if (!s.includes(rmAnchor)) return null;
4232
4416
  if (GUARD_IMPORT_RE.test(s)) {
4233
4417
  s = s.replace(GUARD_IMPORT_RE, GUARD_IMPORT);
4234
4418
  } else {
4235
- s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
4419
+ const requestImport = tree.statements.some((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@tanstack/react-start/server" && !node.importClause?.isTypeOnly && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && node.importClause.namedBindings.elements.some((e) => !e.propertyName && !e.isTypeOnly && e.name.text === "getRequest"));
4420
+ if (!requestImport && /\bgetRequest\b/.test(original)) return null;
4421
+ s = s.replace(importAnchor, importAnchor + "\n" + (requestImport ? GUARD_IMPORT : START_IMPORTS));
4236
4422
  }
4237
4423
  s = reconcileBlock(s, "patchstack-guard", REQUEST_MIDDLEWARE_BLOCK, "const patchstackGuard =", exportAnchor);
4238
4424
  s = reconcileBlock(s, "patchstack-function-guard", FUNCTION_MIDDLEWARE_BLOCK, "const patchstackFunctionGuard =", exportAnchor);
@@ -4247,34 +4433,56 @@ function patchStart(cwd) {
4247
4433
  s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
4248
4434
  }
4249
4435
  }
4250
- if (s === original) {
4251
- log("start.ts already wired");
4252
- return false;
4253
- }
4254
- writeProjectFileSync(cwd, p, s, { encoding: "utf8" });
4255
- log("patched start.ts (guard registered as request + function middleware)");
4256
- return true;
4436
+ return parsedSource(ts, "start.ts", s) ? s : null;
4257
4437
  }
4258
4438
  function wire(cwd, opts) {
4439
+ const ts = sourceCompiler(cwd);
4440
+ const clientPath = join9(cwd, "src/integrations/supabase/client.ts");
4441
+ const startPath = join9(cwd, "src/start.ts");
4442
+ const oldClient = read(clientPath);
4443
+ const oldStart = read(startPath);
4444
+ const client = ts && patchClient(ts, oldClient);
4445
+ const start = ts && patchStart(ts, oldStart);
4446
+ if (!ts || !client || !start || !parsedSource(ts, "client.ts", client)) {
4447
+ log("TanStack wiring needs manual integration: client and server left untouched; no browser traffic was redirected.");
4448
+ return { ok: false, changed: [] };
4449
+ }
4259
4450
  const changed = scaffold(cwd, opts);
4451
+ if (!matchesGuardTemplate(cwd, GUARD_FILE, "guard.ts")) {
4452
+ log("Custom guard helper needs manual review; client and server entries were left untouched.");
4453
+ return { ok: false, changed };
4454
+ }
4260
4455
  if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
4261
- if (patchClient(cwd)) changed.push("src/integrations/supabase/client.ts");
4262
- if (patchStart(cwd)) changed.push("src/start.ts");
4456
+ try {
4457
+ if (start !== oldStart) {
4458
+ writeProjectFileSync(cwd, startPath, start);
4459
+ changed.push("src/start.ts");
4460
+ }
4461
+ if (client !== oldClient) {
4462
+ writeProjectFileSync(cwd, clientPath, client);
4463
+ changed.push("src/integrations/supabase/client.ts");
4464
+ }
4465
+ } catch (error) {
4466
+ if (start !== oldStart) writeProjectFileSync(cwd, startPath, oldStart);
4467
+ throw error;
4468
+ }
4263
4469
  log(
4264
4470
  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."
4265
4471
  );
4266
4472
  return { ok: true, changed: [...new Set(changed)] };
4267
4473
  }
4268
4474
  function verify(cwd) {
4269
- const guardPath = join6(cwd, GUARD_FILE);
4270
- const clientPath = join6(cwd, "src/integrations/supabase/client.ts");
4271
- const startPath = join6(cwd, "src/start.ts");
4272
- const guard = existsSync4(guardPath) ? read(guardPath) : "";
4273
- const client = existsSync4(clientPath) ? read(clientPath) : "";
4274
- const start = existsSync4(startPath) ? read(startPath) : "";
4475
+ const guardPath = join9(cwd, GUARD_FILE);
4476
+ const clientPath = join9(cwd, "src/integrations/supabase/client.ts");
4477
+ const startPath = join9(cwd, "src/start.ts");
4478
+ const guard = existsSync6(guardPath) ? read(guardPath) : "";
4479
+ const client = existsSync6(clientPath) ? read(clientPath) : "";
4480
+ const start = existsSync6(startPath) ? read(startPath) : "";
4481
+ const ts = sourceCompiler(cwd);
4275
4482
  const checks = [
4276
4483
  { label: "guard.ts scaffolded", ok: guard.length > 0, hint: "run `patchstack-connect protect`" },
4277
- { 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" },
4484
+ { label: "guard helper implementation verified", ok: matchesGuardTemplate(cwd, GUARD_FILE, "guard.ts"), hint: "preserved custom helpers require manual review before redirecting browser traffic" },
4485
+ { label: "Supabase client tunnels through the guard", ok: !!ts && client.includes("x-ps-target") && patchClient(ts, client) === client, hint: "run `patchstack-connect protect` to re-patch src/integrations/supabase/client.ts" },
4278
4486
  { 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" },
4279
4487
  { 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" }
4280
4488
  ];
@@ -4288,20 +4496,189 @@ var tanstackSupabaseAdapter = {
4288
4496
  verify
4289
4497
  };
4290
4498
 
4499
+ // src/protect/install/adapters/tanstack.ts
4500
+ import { existsSync as existsSync7 } from "fs";
4501
+ import { createRequire as createRequire2 } from "module";
4502
+ import { join as join10 } from "path";
4503
+ var ENTRY = "src/server.ts";
4504
+ var GUARD = "src/patchstack/guard.ts";
4505
+ var IMPORT = 'import { protectFetch } from "./patchstack/guard";';
4506
+ var DEFAULT = `import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
4507
+ export default createServerEntry({ fetch: handler.fetch.bind(handler) });
4508
+ `;
4509
+ var EXTENSIONS = ["ts", "js", "mts", "mjs", "tsx", "jsx", "cts", "cjs"];
4510
+ function defaultEntryConfig(ts, file, source) {
4511
+ const tree = parsedSource(ts, file, source);
4512
+ if (!tree) return false;
4513
+ const binding = (module, name) => tree.statements.filter(ts.isImportDeclaration).filter((n) => ts.isStringLiteral(n.moduleSpecifier) && n.moduleSpecifier.text === module && !n.importClause?.isTypeOnly).flatMap((n) => n.importClause?.namedBindings && ts.isNamedImports(n.importClause.namedBindings) ? n.importClause.namedBindings.elements.filter((e) => !e.isTypeOnly && (e.propertyName ?? e.name).text === name).map((e) => e.name.text) : []);
4514
+ const defineConfig = binding("vite", "defineConfig");
4515
+ const start = binding("@tanstack/react-start/plugin/vite", "tanstackStart");
4516
+ const unwrap = (expression) => {
4517
+ while (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) expression = expression.expression;
4518
+ return expression;
4519
+ };
4520
+ const exports = tree.statements.filter(ts.isExportAssignment);
4521
+ if (exports.length !== 1 || exports[0].isExportEquals) return false;
4522
+ let config = unwrap(exports[0].expression);
4523
+ if (ts.isCallExpression(config)) {
4524
+ if (!ts.isIdentifier(config.expression) || !defineConfig.includes(config.expression.text) || config.arguments.length !== 1) return false;
4525
+ config = unwrap(config.arguments[0]);
4526
+ }
4527
+ if (!ts.isObjectLiteralExpression(config)) return false;
4528
+ let unsafe = false;
4529
+ const visit = (node) => {
4530
+ if (ts.isSpreadAssignment(node) || ts.isSpreadElement(node) || ts.isComputedPropertyName(node)) unsafe = true;
4531
+ if (ts.isObjectLiteralExpression(node)) {
4532
+ const names = /* @__PURE__ */ new Set();
4533
+ for (const property of node.properties) {
4534
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name)) {
4535
+ unsafe = true;
4536
+ continue;
4537
+ }
4538
+ const name = property.name.text;
4539
+ if (names.has(name) || ["srcDirectory", "serverEntry", "entry", "server", "root"].includes(name)) unsafe = true;
4540
+ names.add(name);
4541
+ }
4542
+ }
4543
+ ts.forEachChild(node, visit);
4544
+ };
4545
+ visit(config);
4546
+ if (unsafe) return false;
4547
+ const plugins = config.properties.find((p) => ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === "plugins");
4548
+ if (!plugins || !ts.isPropertyAssignment(plugins) || !ts.isArrayLiteralExpression(plugins.initializer)) return false;
4549
+ const calls = plugins.initializer.elements.map(unwrap).filter((n) => ts.isCallExpression(n) && ts.isIdentifier(n.expression) && start.includes(n.expression.text));
4550
+ if (calls.length !== 1) return false;
4551
+ const call = calls[0];
4552
+ return ts.isCallExpression(call) && (call.arguments.length === 0 || call.arguments.length === 1 && ts.isObjectLiteralExpression(unwrap(call.arguments[0])));
4553
+ }
4554
+ function entryProblem(cwd) {
4555
+ try {
4556
+ const file = createRequire2(join10(cwd, "package.json")).resolve("@tanstack/react-start/package.json");
4557
+ if (!JSON.parse(read(file)).exports?.["./server-entry"]) return "the installed TanStack Start version does not expose server-entry";
4558
+ } catch {
4559
+ return "the installed TanStack Start version must expose its server-entry contract";
4560
+ }
4561
+ const entries = ["src/server", "server", ...EXTENSIONS.flatMap((ext) => [`src/server.${ext}`, `server.${ext}`])];
4562
+ if (entries.some((file) => file !== ENTRY && existsSync7(join10(cwd, file)))) {
4563
+ return "non-default or competing server entries require manual integration";
4564
+ }
4565
+ const configs = ["vite", "app", "rsbuild"].flatMap((name) => EXTENSIONS.map((ext) => `${name}.config.${ext}`)).filter((file) => existsSync7(join10(cwd, file)));
4566
+ if (configs.length) {
4567
+ const ts = sourceCompiler(cwd);
4568
+ if (configs.length !== 1 || !configs[0].startsWith("vite.") || !ts || !defaultEntryConfig(ts, configs[0], read(join10(cwd, configs[0])))) {
4569
+ return "the default server entry cannot be established from this configuration; integrate it manually";
4570
+ }
4571
+ }
4572
+ return null;
4573
+ }
4574
+ function composeTanstackEntry(ts, source) {
4575
+ const tree = parsedSource(ts, ENTRY, source);
4576
+ if (!tree || /\bprotectFetch\b/.test(source)) return null;
4577
+ const imported = tree.statements.some((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@tanstack/react-start/server-entry" && !node.importClause?.isTypeOnly && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && node.importClause.namedBindings.elements.some((e) => !e.propertyName && !e.isTypeOnly && e.name.text === "createServerEntry"));
4578
+ if (!imported) return null;
4579
+ const exports = tree.statements.filter(ts.isExportAssignment);
4580
+ if (exports.length !== 1 || exports[0].isExportEquals) return null;
4581
+ const call = exports[0].expression;
4582
+ if (!ts.isCallExpression(call) || call.expression.getText(tree) !== "createServerEntry" || call.arguments.length !== 1) return null;
4583
+ const object = call.arguments[0];
4584
+ if (!ts.isObjectLiteralExpression(object) || object.properties.some((p) => ts.isSpreadAssignment(p) || !p.name || ts.isComputedPropertyName(p.name))) return null;
4585
+ const properties = object.properties.filter((p) => p.name?.getText(tree) === "fetch");
4586
+ if (properties.length !== 1) return null;
4587
+ const fetch2 = properties[0];
4588
+ let replacement;
4589
+ if (ts.isPropertyAssignment(fetch2)) replacement = `fetch: protectFetch(${fetch2.initializer.getText(tree)})`;
4590
+ else if (ts.isMethodDeclaration(fetch2) && fetch2.body && !fetch2.asteriskToken) {
4591
+ const text = fetch2.getText(tree);
4592
+ replacement = `fetch: protectFetch(${text.replace(/^(async\s+)?fetch\s*\(/, "$1function (")})`;
4593
+ } else if (ts.isShorthandPropertyAssignment(fetch2)) replacement = "fetch: protectFetch(fetch)";
4594
+ else return null;
4595
+ const patched = source.slice(0, fetch2.getStart(tree)) + replacement + source.slice(fetch2.end);
4596
+ const insertion = tree.statements.filter(ts.isImportDeclaration).at(-1)?.end ?? 0;
4597
+ const result = patched.slice(0, insertion) + "\n" + IMPORT + "\n" + patched.slice(insertion);
4598
+ return parsedSource(ts, ENTRY, result) ? result : null;
4599
+ }
4600
+ function wiredEntry(ts, source) {
4601
+ const tree = parsedSource(ts, ENTRY, source);
4602
+ if (!tree) return false;
4603
+ const imported = tree.statements.filter(ts.isImportDeclaration).filter((n) => n.getText(tree) === IMPORT);
4604
+ if (imported.length !== 1) return false;
4605
+ const wrappers = [];
4606
+ const visit = (node) => {
4607
+ if (ts.isCallExpression(node) && node.expression.getText(tree) === "protectFetch") wrappers.push(node);
4608
+ ts.forEachChild(node, visit);
4609
+ };
4610
+ visit(tree);
4611
+ if (wrappers.length !== 1 || wrappers[0].arguments.length !== 1) return false;
4612
+ const wrapper = wrappers[0];
4613
+ const plain = source.slice(0, wrapper.getStart(tree)) + wrapper.arguments[0].getText(tree) + source.slice(wrapper.end);
4614
+ const composed = composeTanstackEntry(ts, plain.replace(IMPORT, ""));
4615
+ const reparsed = composed && parsedSource(ts, ENTRY, composed);
4616
+ const printer = ts.createPrinter({ removeComments: true });
4617
+ return !!reparsed && printer.printFile(reparsed) === printer.printFile(tree);
4618
+ }
4619
+ var tanstackAdapter = {
4620
+ name: "tanstack-start",
4621
+ label: "TanStack Start",
4622
+ detect: (cwd) => hasDependency(cwd, "@tanstack/react-start"),
4623
+ wire(cwd, opts) {
4624
+ const problem = entryProblem(cwd);
4625
+ const ts = sourceCompiler(cwd);
4626
+ if (problem || !ts) {
4627
+ log(`TanStack server entry left untouched: ${problem ?? "parser unavailable"}.`);
4628
+ return { ok: false, changed: [] };
4629
+ }
4630
+ const previous = existsSync7(join10(cwd, ENTRY)) ? read(join10(cwd, ENTRY)) : DEFAULT;
4631
+ const next = wiredEntry(ts, previous) ? previous : composeTanstackEntry(ts, previous);
4632
+ if (!next) {
4633
+ log("Custom TanStack server entry left untouched; wrap its final Fetch boundary manually.");
4634
+ return { ok: false, changed: [] };
4635
+ }
4636
+ const changed = [];
4637
+ ensureProjectDirectorySync(cwd, join10(cwd, "src/patchstack"));
4638
+ if (installTemplate(cwd, GUARD, "fetch-guard.ts")) changed.push(GUARD);
4639
+ if (!matchesGuardTemplate(cwd, GUARD, "fetch-guard.ts")) {
4640
+ log("Custom Fetch helper needs manual review; the server entry was left untouched.");
4641
+ return { ok: false, changed };
4642
+ }
4643
+ const rules = "src/patchstack/rules.json";
4644
+ if (opts.demo || !existsSync7(join10(cwd, rules))) {
4645
+ copyProjectFileSync(cwd, join10(templatesDir(), opts.demo ? "demo-rules.json" : "rules.json"), join10(cwd, rules));
4646
+ changed.push(rules);
4647
+ }
4648
+ if (!opts.demo && bakeSiteUuid(cwd, GUARD)) changed.push(GUARD);
4649
+ if (next !== previous || !existsSync7(join10(cwd, ENTRY))) {
4650
+ writeProjectFileSync(cwd, join10(cwd, ENTRY), next);
4651
+ changed.push(ENTRY);
4652
+ }
4653
+ log("TanStack server entry screens requests and final Responses; browser-direct backend traffic and separately deployed functions need their own protection.");
4654
+ return { ok: true, changed };
4655
+ },
4656
+ verify(cwd) {
4657
+ const ts = sourceCompiler(cwd);
4658
+ const problem = entryProblem(cwd);
4659
+ const wired = !problem && !!ts && existsSync7(join10(cwd, ENTRY)) && wiredEntry(ts, read(join10(cwd, ENTRY))) && matchesGuardTemplate(cwd, GUARD, "fetch-guard.ts") && existsSync7(join10(cwd, "src/patchstack/rules.json"));
4660
+ return { wired, checks: [
4661
+ { label: "TanStack server Fetch boundary wired", ok: wired, hint: problem ?? "run protect; manually review unsupported custom server entries" },
4662
+ { label: "browser-direct services and separately deployed functions are not covered by this entry", ok: true, unverifiable: true, hint: "install protection at each independently exposed backend; retain backend authorization and RLS" }
4663
+ ] };
4664
+ }
4665
+ };
4666
+
4291
4667
  // src/protect/install/adapters/next.ts
4292
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3 } from "fs";
4293
- import { dirname as dirname3, join as join8, relative as relative3 } from "path";
4668
+ import { existsSync as existsSync8, lstatSync as lstatSync2, readdirSync as readdirSync3 } from "fs";
4669
+ import { dirname as dirname3, join as join12, relative as relative3 } from "path";
4670
+ import { createRequire as createRequire4 } from "module";
4294
4671
 
4295
4672
  // src/protect/install/adapters/next-source.ts
4296
- import { createRequire } from "module";
4297
- import { join as join7 } from "path";
4673
+ import { createRequire as createRequire3 } from "module";
4674
+ import { join as join11 } from "path";
4298
4675
  var NEXT_MARKER = "// #region patchstack-next-composed";
4299
4676
  var ROUTE_MARKER = "// #region patchstack-next-route";
4300
4677
  var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
4301
4678
  function nextCompiler(cwd) {
4302
- for (const base of [join7(cwd, "package.json"), import.meta.url]) {
4679
+ for (const base of [join11(cwd, "package.json"), import.meta.url]) {
4303
4680
  try {
4304
- return createRequire(base)("typescript");
4681
+ return createRequire3(base)("typescript");
4305
4682
  } catch {
4306
4683
  }
4307
4684
  }
@@ -4373,7 +4750,7 @@ function standardNextRouting(ts, file, source) {
4373
4750
  return false;
4374
4751
  };
4375
4752
  if (!literal(target) || ![...objects.values()].every(literal)) return false;
4376
- return target.properties.every((p) => p.name && !["basePath", "i18n", "skipMiddlewareUrlNormalize", "pageExtensions", "__proto__"].includes(
4753
+ return target.properties.every((p) => p.name && !["basePath", "i18n", "skipMiddlewareUrlNormalize", "skipProxyUrlNormalize", "pageExtensions", "__proto__"].includes(
4377
4754
  ts.isIdentifier(p.name) || ts.isStringLiteral(p.name) ? p.name.text : ""
4378
4755
  ));
4379
4756
  }
@@ -4437,7 +4814,7 @@ function prelude(request, marker) {
4437
4814
  function composeNextMiddleware(ts, file, source, guardImport) {
4438
4815
  const sf = parse(ts, file, source);
4439
4816
  if (!sf || /\b(?:psProtection|psBlocked|psRequest|getPatchstackProtection)\b/.test(source)) return null;
4440
- const found = handlers(ts, sf, /* @__PURE__ */ new Set(["middleware"]));
4817
+ const found = handlers(ts, sf, /* @__PURE__ */ new Set([/proxy\.[jt]s$/.test(file) ? "proxy" : "middleware"]));
4441
4818
  if (found.length !== 1 || sf.statements.some((s) => ts.isExportAssignment(s) || ts.isExportDeclaration(s))) return null;
4442
4819
  const fn = found[0];
4443
4820
  const request = requestName(ts, fn);
@@ -4499,7 +4876,7 @@ function nextSourceWired(ts, file, source, guardImport, route = false) {
4499
4876
  const imported = sf.statements.some((s) => ts.isImportDeclaration(s) && ts.isStringLiteral(s.moduleSpecifier) && s.moduleSpecifier.text === guardImport && s.importClause?.namedBindings && ts.isNamedImports(s.importClause.namedBindings) && s.importClause.namedBindings.elements.some((e) => !e.propertyName && e.name.text === "getPatchstackProtection"));
4500
4877
  if (!imported) return false;
4501
4878
  if (route && !sf.statements.some((s) => ts.isImportDeclaration(s) && ts.isStringLiteral(s.moduleSpecifier) && s.moduleSpecifier.text === guardImport && s.importClause?.namedBindings && ts.isNamedImports(s.importClause.namedBindings) && s.importClause.namedBindings.elements.some((e) => !e.propertyName && e.name.text === "screenPatchstackResponse"))) return false;
4502
- const found = handlers(ts, sf, route ? METHODS : /* @__PURE__ */ new Set(["middleware"]));
4879
+ const found = handlers(ts, sf, route ? METHODS : /* @__PURE__ */ new Set([/proxy\.[jt]s$/.test(file) ? "proxy" : "middleware"]));
4503
4880
  if (!found.length || sf.statements.some((s) => ts.isExportDeclaration(s) || ts.isExportAssignment(s))) return false;
4504
4881
  if (route && methodExportCount(ts, sf) !== found.length) return false;
4505
4882
  const compact = (text) => text.replace(/\s+/g, "");
@@ -4534,17 +4911,48 @@ function nextSourceWired(ts, file, source, guardImport, route = false) {
4534
4911
 
4535
4912
  // src/protect/install/adapters/next.ts
4536
4913
  function middlewareInfo(cwd) {
4537
- const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
4538
- const relFile = candidates.find((rel) => existsSync5(join8(cwd, rel))) ?? (existsSync5(join8(cwd, "src")) ? "src/middleware.ts" : "middleware.ts");
4539
- return { relFile, relDir: dirname3(relFile), exists: existsSync5(join8(cwd, relFile)) };
4914
+ const candidates = entryFiles(cwd);
4915
+ const name = (nextMajor(cwd) ?? 0) >= 16 ? "proxy" : "middleware";
4916
+ const relFile = candidates.find((rel) => existsSync8(join12(cwd, rel))) ?? (existsSync8(join12(cwd, "src")) ? `src/${name}.ts` : `${name}.ts`);
4917
+ return { relFile, relDir: dirname3(relFile), exists: existsSync8(join12(cwd, relFile)) };
4918
+ }
4919
+ function nextMajor(cwd) {
4920
+ try {
4921
+ const file = createRequire4(join12(cwd, "package.json")).resolve("next/package.json");
4922
+ return Number(JSON.parse(read(file)).version.split(".")[0]);
4923
+ } catch {
4924
+ try {
4925
+ const pkg = JSON.parse(read(join12(cwd, "package.json")));
4926
+ const version = pkg.dependencies?.next ?? pkg.devDependencies?.next;
4927
+ const match = typeof version === "string" && /^[~^]?(\d+)\./.exec(version);
4928
+ return match ? Number(match[1]) : null;
4929
+ } catch {
4930
+ return null;
4931
+ }
4932
+ }
4540
4933
  }
4541
- function proxyFiles(cwd) {
4542
- return ["proxy.ts", "proxy.js", "src/proxy.ts", "src/proxy.js"].filter((file) => existsSync5(join8(cwd, file)));
4934
+ function entryFiles(cwd) {
4935
+ return [
4936
+ "middleware.ts",
4937
+ "middleware.js",
4938
+ "src/middleware.ts",
4939
+ "src/middleware.js",
4940
+ "proxy.ts",
4941
+ "proxy.js",
4942
+ "src/proxy.ts",
4943
+ "src/proxy.js"
4944
+ ].filter((file) => existsSync8(join12(cwd, file)));
4945
+ }
4946
+ function entryProblem2(cwd) {
4947
+ const files = entryFiles(cwd);
4948
+ if (files.length > 1) return `multiple Next.js middleware/proxy entries (${files.join(", ")}); select one manually`;
4949
+ if (files.some((file) => /proxy\.[jt]s$/.test(file)) && (nextMajor(cwd) ?? 0) < 16) return `${files.join(", ")} requires a verified Next.js 16+ installation`;
4950
+ return null;
4543
4951
  }
4544
4952
  function paths(cwd) {
4545
4953
  const mw = middlewareInfo(cwd);
4546
4954
  const ext = mw.relFile.endsWith(".js") ? "js" : "ts";
4547
- return { ...mw, rules: join8(mw.relDir, "patchstack.rules.json"), guard: join8(mw.relDir, `patchstack.next.${ext}`) };
4955
+ return { ...mw, rules: join12(mw.relDir, "patchstack.rules.json"), guard: join12(mw.relDir, `patchstack.next.${ext}`) };
4548
4956
  }
4549
4957
  function importFrom(file, guard) {
4550
4958
  const rel = relative3(dirname3(file), guard).replace(/\\/g, "/").replace(/\.ts$/, "");
@@ -4559,7 +4967,7 @@ function routes(cwd) {
4559
4967
  incomplete = true;
4560
4968
  return;
4561
4969
  }
4562
- const full = join8(cwd, rel);
4970
+ const full = join12(cwd, rel);
4563
4971
  if (lstatSync2(full).isSymbolicLink()) {
4564
4972
  incomplete = true;
4565
4973
  return;
@@ -4570,14 +4978,14 @@ function routes(cwd) {
4570
4978
  incomplete = true;
4571
4979
  continue;
4572
4980
  }
4573
- const child = join8(rel, entry.name);
4981
+ const child = join12(rel, entry.name);
4574
4982
  if (entry.isDirectory()) walk2(child);
4575
4983
  else if (entry.isFile() && /^route\.(?:tsx?|jsx?)$/.test(entry.name)) files.push(child);
4576
4984
  }
4577
4985
  };
4578
4986
  for (const root of ["app", "src/app"]) {
4579
- if (!existsSync5(join8(cwd, root))) continue;
4580
- if (root.startsWith("src/") && lstatSync2(join8(cwd, "src")).isSymbolicLink()) {
4987
+ if (!existsSync8(join12(cwd, root))) continue;
4988
+ if (root.startsWith("src/") && lstatSync2(join12(cwd, "src")).isSymbolicLink()) {
4581
4989
  incomplete = true;
4582
4990
  continue;
4583
4991
  }
@@ -4591,53 +4999,62 @@ function routes(cwd) {
4591
4999
  }
4592
5000
  function customRouting(cwd, ts) {
4593
5001
  return ["js", "mjs", "ts", "cjs"].some((ext) => {
4594
- const file = join8(cwd, `next.config.${ext}`);
4595
- return existsSync5(file) && !standardNextRouting(ts, file, read(file));
5002
+ const file = join12(cwd, `next.config.${ext}`);
5003
+ return existsSync8(file) && !standardNextRouting(ts, file, read(file));
4596
5004
  });
4597
5005
  }
4598
5006
  function sharedGuardPresent(file) {
4599
- if (!existsSync5(file)) return false;
5007
+ if (!existsSync8(file)) return false;
4600
5008
  const source = read(file);
4601
5009
  return source.includes("export async function getPatchstackProtection") && source.includes("export async function screenPatchstackResponse") && source.includes('import { createProtection } from "@patchstack/connect/protect"');
4602
5010
  }
4603
5011
  function wire2(cwd, opts) {
4604
- const proxies = proxyFiles(cwd);
4605
- if (proxies.length) {
4606
- log(`left ${proxies.join(", ")} untouched: Next.js proxy integration requires manual wiring. Do not add middleware alongside a proxy; protect --check reports this gap.`);
5012
+ const problem = entryProblem2(cwd);
5013
+ if (problem) {
5014
+ log(`Next.js entries left untouched: ${problem}. Do not add middleware alongside a proxy.`);
4607
5015
  return { ok: false, changed: [] };
4608
5016
  }
4609
5017
  const templates = templatesDir();
4610
5018
  const mw = paths(cwd);
4611
- ensureProjectDirectorySync(cwd, join8(cwd, mw.relDir));
5019
+ ensureProjectDirectorySync(cwd, join12(cwd, mw.relDir));
4612
5020
  const changed = [];
4613
- if (opts.demo || !existsSync5(join8(cwd, mw.rules))) {
4614
- copyProjectFileSync(cwd, join8(templates, opts.demo ? "demo-rules.json" : "rules.json"), join8(cwd, mw.rules));
5021
+ if (opts.demo || !existsSync8(join12(cwd, mw.rules))) {
5022
+ copyProjectFileSync(cwd, join12(templates, opts.demo ? "demo-rules.json" : "rules.json"), join12(cwd, mw.rules));
4615
5023
  changed.push(mw.rules);
4616
5024
  }
4617
5025
  const ts = nextCompiler(cwd);
4618
- const guardPath = join8(cwd, mw.guard);
4619
- const guardConflict = existsSync5(guardPath) && !sharedGuardPresent(guardPath);
5026
+ const guardPath = join12(cwd, mw.guard);
5027
+ const guardConflict = existsSync8(guardPath) && !sharedGuardPresent(guardPath);
4620
5028
  const ensureGuard = () => {
4621
- if (existsSync5(guardPath)) return;
4622
- const source = read(join8(templates, "next-guard.ts"));
5029
+ if (existsSync8(guardPath)) {
5030
+ if (mw.guard.endsWith(".ts") && installTemplate(cwd, mw.guard, "next-guard.ts")) changed.push(mw.guard);
5031
+ return;
5032
+ }
5033
+ const source = read(join12(templates, "next-guard.ts"));
4623
5034
  writeProjectFileSync(cwd, guardPath, mw.guard.endsWith(".js") ? ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText : source);
4624
5035
  if (!opts.demo) bakeSiteUuid(cwd, mw.guard);
4625
5036
  changed.push(mw.guard);
4626
5037
  };
4627
- const existing = mw.exists ? read(join8(cwd, mw.relFile)) : "";
5038
+ const existing = mw.exists ? read(join12(cwd, mw.relFile)) : "";
4628
5039
  if (!mw.exists) {
4629
- copyProjectFileSync(cwd, join8(templates, "next-middleware.ts"), join8(cwd, mw.relFile));
5040
+ if (/proxy\.[jt]s$/.test(mw.relFile)) {
5041
+ const composed = ts && !guardConflict && composeNextMiddleware(ts, mw.relFile, 'export function proxy(request: Request) {}\nexport const config = { matcher: "/:path*" };\n', importFrom(mw.relFile, mw.guard));
5042
+ if (!composed) return { ok: false, changed };
5043
+ ensureGuard();
5044
+ writeProjectFileSync(cwd, join12(cwd, mw.relFile), composed);
5045
+ } else copyProjectFileSync(cwd, join12(templates, "next-middleware.ts"), join12(cwd, mw.relFile));
4630
5046
  if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
4631
5047
  changed.push(mw.relFile);
4632
5048
  log(`scaffolded ${mw.relFile} (request-phase guard)`);
4633
5049
  } else if (existing.includes(NEXT_MARKER) || existing.includes("#region patchstack-next (")) {
5050
+ if (existing.includes("#region patchstack-next (") && installTemplate(cwd, mw.relFile, "next-middleware.ts")) changed.push(mw.relFile);
4634
5051
  log(`${mw.relFile} already has a Patchstack guard \u2014 left as-is`);
4635
5052
  if (ts && existing.includes(NEXT_MARKER)) ensureGuard();
4636
5053
  } else {
4637
5054
  const composed = ts && !guardConflict && !customRouting(cwd, ts) && composeNextMiddleware(ts, mw.relFile, existing, importFrom(mw.relFile, mw.guard));
4638
5055
  if (composed) {
4639
5056
  ensureGuard();
4640
- writeProjectFileSync(cwd, join8(cwd, mw.relFile), composed);
5057
+ writeProjectFileSync(cwd, join12(cwd, mw.relFile), composed);
4641
5058
  changed.push(mw.relFile);
4642
5059
  log(`composed ${mw.relFile}: request guard first; existing middleware keeps its original matcher scope`);
4643
5060
  } else {
@@ -4646,7 +5063,7 @@ function wire2(cwd, opts) {
4646
5063
  }
4647
5064
  const inventory = routes(cwd);
4648
5065
  for (const file of inventory.files) {
4649
- const source = read(join8(cwd, file));
5066
+ const source = read(join12(cwd, file));
4650
5067
  if (source.includes(ROUTE_MARKER)) {
4651
5068
  if (ts) ensureGuard();
4652
5069
  continue;
@@ -4654,7 +5071,7 @@ function wire2(cwd, opts) {
4654
5071
  const composed = ts && !guardConflict && /\.(?:ts|js)$/.test(file) && composeNextRoute(ts, file, source, importFrom(file, mw.guard));
4655
5072
  if (composed) {
4656
5073
  ensureGuard();
4657
- writeProjectFileSync(cwd, join8(cwd, file), composed);
5074
+ writeProjectFileSync(cwd, join12(cwd, file), composed);
4658
5075
  changed.push(file);
4659
5076
  log(`wired ${file} (request checks + response filtering)`);
4660
5077
  } else {
@@ -4667,25 +5084,21 @@ function wire2(cwd, opts) {
4667
5084
  return { ok: true, changed: [...new Set(changed)] };
4668
5085
  }
4669
5086
  function verify2(cwd) {
4670
- const proxies = proxyFiles(cwd);
4671
- if (proxies.length) return {
5087
+ const problem = entryProblem2(cwd);
5088
+ if (problem) return {
4672
5089
  wired: false,
4673
- checks: [{
4674
- label: "Next.js proxy wiring requires manual verification",
4675
- ok: false,
4676
- hint: `review ${proxies.join(", ")}; automatic proxy integration is not supported. Next.js cannot use middleware and proxy together.`
4677
- }]
5090
+ checks: [{ label: "Next.js request entry needs review", ok: false, hint: problem }]
4678
5091
  };
4679
5092
  const mw = paths(cwd);
4680
5093
  const ts = nextCompiler(cwd);
4681
- const source = mw.exists ? read(join8(cwd, mw.relFile)) : "";
4682
- const shared = sharedGuardPresent(join8(cwd, mw.guard));
5094
+ const source = mw.exists ? read(join12(cwd, mw.relFile)) : "";
5095
+ const shared = sharedGuardPresent(join12(cwd, mw.guard));
4683
5096
  const legacy = source.includes("#region patchstack-next (") && source.includes("await protection.fetchGuard()(request)") && source.includes("if (blocked) return blocked") && source.includes('matcher: "/:path*"');
4684
5097
  const composed = ts && shared && !customRouting(cwd, ts) && nextSourceWired(ts, mw.relFile, source, importFrom(mw.relFile, mw.guard));
4685
5098
  const present2 = !!(legacy || composed);
4686
- const rulesPresent = existsSync5(join8(cwd, mw.rules));
5099
+ const rulesPresent = existsSync8(join12(cwd, mw.rules));
4687
5100
  const inventory = routes(cwd);
4688
- const missing = inventory.files.filter((file) => !(ts && shared && nextSourceWired(ts, file, read(join8(cwd, file)), importFrom(file, mw.guard), true)));
5101
+ const missing = inventory.files.filter((file) => !(ts && shared && nextSourceWired(ts, file, read(join12(cwd, file)), importFrom(file, mw.guard), true)));
4689
5102
  return {
4690
5103
  wired: present2 && rulesPresent && missing.length === 0 && !inventory.incomplete,
4691
5104
  checks: [
@@ -4724,51 +5137,63 @@ var nextAdapter = {
4724
5137
  };
4725
5138
 
4726
5139
  // src/protect/install/seam.ts
4727
- import { existsSync as existsSync6 } from "fs";
4728
- import { join as join9, dirname as dirname4 } from "path";
5140
+ import { existsSync as existsSync9 } from "fs";
5141
+ import { join as join13, dirname as dirname4 } from "path";
4729
5142
  function rulesRel(seamRel) {
4730
5143
  const d = dirname4(seamRel);
4731
5144
  return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
4732
5145
  }
4733
5146
  function wireSeam(cwd, opts, spec) {
4734
5147
  const templates = templatesDir();
4735
- const existing = spec.candidates.find((c) => existsSync6(join9(cwd, c)));
5148
+ const existing = spec.candidates.find((c) => existsSync9(join13(cwd, c)));
4736
5149
  const seamRel = existing ?? spec.target;
4737
- ensureProjectDirectorySync(cwd, dirname4(join9(cwd, seamRel)));
4738
- const rulesDst = join9(cwd, rulesRel(seamRel));
5150
+ ensureProjectDirectorySync(cwd, dirname4(join13(cwd, seamRel)));
5151
+ const rulesDst = join13(cwd, rulesRel(seamRel));
4739
5152
  const changed = [];
4740
- if (opts.demo || !existsSync6(rulesDst)) {
4741
- copyProjectFileSync(cwd, join9(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
5153
+ if (opts.demo || !existsSync9(rulesDst)) {
5154
+ copyProjectFileSync(cwd, join13(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4742
5155
  changed.push(rulesRel(seamRel));
4743
5156
  }
4744
- const current = existing ? read(join9(cwd, existing)) : "";
5157
+ const current = existing ? read(join13(cwd, existing)) : "";
4745
5158
  if (existing && current.includes(spec.marker)) {
4746
- log(`${existing} already has the Patchstack guard \u2014 left as-is`);
5159
+ if (installTemplate(cwd, seamRel, spec.templateName)) changed.push(seamRel);
4747
5160
  return { ok: true, changed };
4748
5161
  }
4749
5162
  if (existing) {
4750
5163
  log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
4751
5164
  return { ok: true, changed };
4752
5165
  }
4753
- copyProjectFileSync(cwd, join9(templates, spec.templateName), join9(cwd, seamRel));
5166
+ copyProjectFileSync(cwd, join13(templates, spec.templateName), join13(cwd, seamRel));
4754
5167
  if (!opts.demo) bakeSiteUuid(cwd, seamRel);
4755
5168
  changed.push(seamRel);
4756
5169
  log(`scaffolded ${seamRel}`);
4757
5170
  return { ok: true, changed: [...new Set(changed)] };
4758
5171
  }
4759
5172
  function verifySeam(cwd, spec) {
4760
- const existing = spec.candidates.find((c) => existsSync6(join9(cwd, c)));
5173
+ const existing = spec.candidates.find((c) => existsSync9(join13(cwd, c)));
4761
5174
  const seamRel = existing ?? spec.target;
4762
- const present2 = existing ? read(join9(cwd, existing)).includes(spec.marker) : false;
4763
- const rulesPresent = existsSync6(join9(cwd, rulesRel(seamRel)));
5175
+ const present2 = existing ? templateWiringPresent(cwd, existing, spec.templateName) : false;
5176
+ const rulesPresent = existsSync9(join13(cwd, rulesRel(seamRel)));
4764
5177
  return {
4765
5178
  wired: present2 && rulesPresent,
4766
5179
  checks: [
4767
- { label: `${spec.seamLabel} present`, ok: present2, hint: `run \`patchstack-connect protect\` (writes ${spec.target})` },
5180
+ { label: `${spec.seamLabel} wiring verified`, ok: present2, hint: `run \`patchstack-connect protect\`; customized hooks need manual verification (${spec.target})` },
4768
5181
  { label: "rules co-located with the guard", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
4769
5182
  ]
4770
5183
  };
4771
5184
  }
5185
+ function templateWiringPresent(cwd, file, template) {
5186
+ const ts = sourceCompiler(cwd);
5187
+ if (!ts) return false;
5188
+ const actual = parsedSource(ts, file, read(join13(cwd, file)));
5189
+ const expected = parsedSource(ts, template, read(join13(templatesDir(), template)));
5190
+ if (!actual || !expected) return false;
5191
+ const printer = ts.createPrinter({ removeComments: true });
5192
+ const emit = (node, tree) => printer.printNode(ts.EmitHint.Unspecified, node, tree);
5193
+ const required = expected.statements.filter((node) => ts.isExportAssignment(node) || ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) || ts.isFunctionDeclaration(node) && node.name?.text === "getProtection" || ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@patchstack/connect/protect");
5194
+ const code = actual.statements.map((node) => emit(node, actual));
5195
+ return required.length >= 3 && required.every((node) => code.filter((text) => text === emit(node, expected)).length === 1);
5196
+ }
4772
5197
 
4773
5198
  // src/protect/install/adapters/sveltekit.ts
4774
5199
  var SPEC = {
@@ -4850,7 +5275,7 @@ var nuxtAdapter = {
4850
5275
 
4851
5276
  // src/protect/install/find-app.ts
4852
5277
  import { readFileSync as readFileSync8, readdirSync as readdirSync4, lstatSync as lstatSync3 } from "fs";
4853
- import { join as join10, relative as relative4, dirname as dirname5 } from "path";
5278
+ import { join as join14, relative as relative4, dirname as dirname5 } from "path";
4854
5279
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
4855
5280
  "node_modules",
4856
5281
  "patchstack",
@@ -4882,7 +5307,7 @@ function findAppInstance(cwd, re) {
4882
5307
  }
4883
5308
  for (const name of entries) {
4884
5309
  if (SKIP_DIRS2.has(name) || name.startsWith(".")) continue;
4885
- const p = join10(dir, name);
5310
+ const p = join14(dir, name);
4886
5311
  let st;
4887
5312
  try {
4888
5313
  st = lstatSync3(p);
@@ -4941,7 +5366,7 @@ function declaredEntries(cwd) {
4941
5366
  const paths2 = /* @__PURE__ */ new Set();
4942
5367
  let pkg;
4943
5368
  try {
4944
- pkg = JSON.parse(readFileSync8(join10(cwd, "package.json"), "utf8"));
5369
+ pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
4945
5370
  } catch {
4946
5371
  return paths2;
4947
5372
  }
@@ -4967,17 +5392,17 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
4967
5392
  }
4968
5393
 
4969
5394
  // src/protect/install/register.ts
4970
- import { existsSync as existsSync9 } from "fs";
4971
- import { join as join13, resolve as resolve3 } from "path";
5395
+ import { existsSync as existsSync12 } from "fs";
5396
+ import { join as join17, resolve as resolve3 } from "path";
4972
5397
 
4973
5398
  // src/protect/install/generic.ts
4974
- import { readFileSync as readFileSync9, existsSync as existsSync8, readdirSync as readdirSync5, lstatSync as lstatSync4 } from "fs";
4975
- import { join as join12, dirname as dirname7 } from "path";
5399
+ import { readFileSync as readFileSync9, existsSync as existsSync11, readdirSync as readdirSync5, lstatSync as lstatSync4 } from "fs";
5400
+ import { join as join16, dirname as dirname7 } from "path";
4976
5401
 
4977
5402
  // src/protect/install/source-scope.ts
4978
5403
  import { execFileSync } from "child_process";
4979
- import { existsSync as existsSync7 } from "fs";
4980
- import { dirname as dirname6, join as join11, resolve as resolve2 } from "path";
5404
+ import { existsSync as existsSync10 } from "fs";
5405
+ import { dirname as dirname6, join as join15, resolve as resolve2 } from "path";
4981
5406
 
4982
5407
  // src/protect/install/node-flags.ts
4983
5408
  var flagName = (token) => token.split("=")[0];
@@ -5267,8 +5692,8 @@ function moduleKindOf(filePath, packageIsModule) {
5267
5692
  }
5268
5693
  function resolvesToGuardModule(specifier, query) {
5269
5694
  const resolved = resolve2(dirname6(query.fromFile), specifier);
5270
- if (withoutModuleExtension(resolved) !== join11(query.guardDir, "guard")) return false;
5271
- const existing = GUARD_FILENAMES.filter((name) => existsSync7(join11(query.guardDir, name)));
5695
+ if (withoutModuleExtension(resolved) !== join15(query.guardDir, "guard")) return false;
5696
+ const existing = GUARD_FILENAMES.filter((name) => existsSync10(join15(query.guardDir, name)));
5272
5697
  if (existing.length === 0) return false;
5273
5698
  const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
5274
5699
  if (!query.strictExtensions) return true;
@@ -5277,7 +5702,7 @@ function resolvesToGuardModule(specifier, query) {
5277
5702
  if (query.kind === "cjs") return existing.includes("guard.js");
5278
5703
  return true;
5279
5704
  }
5280
- if (existsSync7(resolved)) return true;
5705
+ if (existsSync10(resolved)) return true;
5281
5706
  return query.kind === "ts" && extension === ".js" && existing.includes("guard.ts");
5282
5707
  }
5283
5708
  function withoutModuleExtension(filePath) {
@@ -5287,16 +5712,16 @@ function withoutModuleExtension(filePath) {
5287
5712
  // src/protect/install/generic.ts
5288
5713
  function packageIsEsm(cwd) {
5289
5714
  try {
5290
- return JSON.parse(read(join12(cwd, "package.json"))).type === "module";
5715
+ return JSON.parse(read(join16(cwd, "package.json"))).type === "module";
5291
5716
  } catch {
5292
5717
  return false;
5293
5718
  }
5294
5719
  }
5295
5720
  function genericDir(cwd) {
5296
- return existsSync8(join12(cwd, "src")) ? "src/patchstack" : "patchstack";
5721
+ return existsSync11(join16(cwd, "src")) ? "src/patchstack" : "patchstack";
5297
5722
  }
5298
5723
  function hasTypeScript(cwd) {
5299
- return existsSync8(join12(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
5724
+ return existsSync11(join16(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
5300
5725
  }
5301
5726
  function genericGuardTarget(cwd) {
5302
5727
  if (hasTypeScript(cwd)) {
@@ -5310,7 +5735,7 @@ function genericGuardTarget(cwd) {
5310
5735
  function genericScaffoldFiles(cwd) {
5311
5736
  const dir = genericDir(cwd);
5312
5737
  const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
5313
- return candidates.map((file) => `${dir}/${file}`).filter((relative9) => existsSync8(join12(cwd, relative9)));
5738
+ return candidates.map((file) => `${dir}/${file}`).filter((relative9) => existsSync11(join16(cwd, relative9)));
5314
5739
  }
5315
5740
  function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
5316
5741
  const target = genericGuardTarget(cwd);
@@ -5318,15 +5743,15 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
5318
5743
  guardFile ??= target.file;
5319
5744
  const templates = templatesDir();
5320
5745
  const dir = genericDir(cwd);
5321
- const dst = join12(cwd, dir);
5746
+ const dst = join16(cwd, dir);
5322
5747
  ensureProjectDirectorySync(cwd, dst);
5323
- copyProjectFileSync(cwd, join12(templates, guardTemplate), join12(dst, guardFile));
5324
5748
  const guardRel = `${dir}/${guardFile}`;
5325
- const changed = [guardRel];
5326
- if (!opts.demo) bakeSiteUuid(cwd, guardRel);
5327
- const rulesDst = join12(dst, "rules.json");
5328
- if (opts.demo || !existsSync8(rulesDst)) {
5329
- copyProjectFileSync(cwd, join12(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
5749
+ const changed = [];
5750
+ if (installTemplate(cwd, guardRel, guardTemplate)) changed.push(guardRel);
5751
+ if (!opts.demo && matchesGuardTemplate(cwd, guardRel, guardTemplate)) bakeSiteUuid(cwd, guardRel);
5752
+ const rulesDst = join16(dst, "rules.json");
5753
+ if (opts.demo || !existsSync11(rulesDst)) {
5754
+ copyProjectFileSync(cwd, join16(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
5330
5755
  changed.push(`${dir}/rules.json`);
5331
5756
  }
5332
5757
  return { changed, dir };
@@ -5347,10 +5772,10 @@ function candidateEntries(cwd) {
5347
5772
  "app.ts",
5348
5773
  "app.js"
5349
5774
  ];
5350
- const hits = names.filter((n) => existsSync8(join12(cwd, n)));
5775
+ const hits = names.filter((n) => existsSync11(join16(cwd, n)));
5351
5776
  try {
5352
- const pkg = JSON.parse(read(join12(cwd, "package.json")));
5353
- if (typeof pkg.main === "string" && existsSync8(join12(cwd, pkg.main))) hits.push(pkg.main);
5777
+ const pkg = JSON.parse(read(join16(cwd, "package.json")));
5778
+ if (typeof pkg.main === "string" && existsSync11(join16(cwd, pkg.main))) hits.push(pkg.main);
5354
5779
  } catch {
5355
5780
  }
5356
5781
  return [...new Set(hits)];
@@ -5453,13 +5878,16 @@ function escapeForRegExp(value) {
5453
5878
  function genericVerify(cwd) {
5454
5879
  const dir = genericDir(cwd);
5455
5880
  const target = genericGuardTarget(cwd);
5456
- const present2 = GUARD_FILENAMES.find((name) => existsSync8(join12(cwd, dir, name)));
5881
+ const present2 = GUARD_FILENAMES.find((name) => existsSync11(join16(cwd, dir, name)));
5457
5882
  const scaffolded = present2 !== void 0;
5458
- const imported = scaffolded && guardIsImported(cwd, join12(cwd, dir, present2));
5883
+ const template = present2?.endsWith(".ts") ? "generic-guard.ts" : present2?.endsWith(".cjs") ? "generic-guard.cjs" : "generic-guard.js";
5884
+ const helperVerified = !!present2 && matchesGuardTemplate(cwd, `${dir}/${present2}`, template);
5885
+ const imported = scaffolded && guardIsImported(cwd, join16(cwd, dir, present2));
5459
5886
  return {
5460
- wired: scaffolded && imported,
5887
+ wired: scaffolded && helperVerified && imported,
5461
5888
  checks: [
5462
5889
  { label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
5890
+ { label: "guard helper implementation verified", ok: helperVerified, hint: "preserved custom helpers require manual review of their exports and request/response screening" },
5463
5891
  {
5464
5892
  label: "guard imported and called in a server entry",
5465
5893
  ok: imported,
@@ -5491,7 +5919,7 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
5491
5919
  function guardIsImported(cwd, guardPath) {
5492
5920
  const guardDir = dirname7(guardPath);
5493
5921
  const packageIsModule = packageIsEsm(cwd);
5494
- const root = existsSync8(join12(cwd, "src")) ? join12(cwd, "src") : cwd;
5922
+ const root = existsSync11(join16(cwd, "src")) ? join16(cwd, "src") : cwd;
5495
5923
  let found = false;
5496
5924
  const walk2 = (d, depth) => {
5497
5925
  if (found || depth > 8) return;
@@ -5504,7 +5932,7 @@ function guardIsImported(cwd, guardPath) {
5504
5932
  for (const name of entries) {
5505
5933
  if (found) return;
5506
5934
  if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
5507
- const p = join12(d, name);
5935
+ const p = join16(d, name);
5508
5936
  let st;
5509
5937
  try {
5510
5938
  st = lstatSync4(p);
@@ -5527,7 +5955,7 @@ function guardIsImported(cwd, guardPath) {
5527
5955
 
5528
5956
  // src/protect/install/register.ts
5529
5957
  var routeRegistration = (appVar) => new RegExp(
5530
- `^\\s*${appVar}\\.(?:get|post|put|patch|delete|head|options|all|route)\\(|^\\s*${appVar}\\.use\\(\\s*['"\`]`,
5958
+ `^\\s*${appVar}\\.(?:get|post|put|patch|delete|head|options|all|route)\\(|^\\s*${appVar}\\.use\\(\\s*(?:['"\`]|[\\w$]+(?:\\.[\\w$]+)*\\s*[,\\)])`,
5531
5959
  "m"
5532
5960
  );
5533
5961
  function routesBefore(source, appVar, guardIndex) {
@@ -5563,7 +5991,7 @@ function guardTarget(cwd, entryRel, spec) {
5563
5991
  }
5564
5992
  function packageType(cwd) {
5565
5993
  try {
5566
- return JSON.parse(read(join13(cwd, "package.json"))).type;
5994
+ return JSON.parse(read(join17(cwd, "package.json"))).type;
5567
5995
  } catch {
5568
5996
  return void 0;
5569
5997
  }
@@ -5580,7 +6008,7 @@ function wiringState(source, spec, appVar, fileRel, guard) {
5580
6008
  );
5581
6009
  const appIndex = maskedLines.findIndex((line) => spec.appRe.test(line));
5582
6010
  const anchor = spec.callAfter ? spec.callAfter(appVar) : null;
5583
- const anchorIndex = anchor ? maskedLines.findIndex((line) => anchor.test(line)) : -1;
6011
+ const anchorIndex = anchor ? maskedLines.reduce((last, line, index) => anchor.test(line) ? index : last, -1) : -1;
5584
6012
  return {
5585
6013
  importAtTopLevel: importIndex !== -1 && isTopLevelLine(code, importIndex),
5586
6014
  callInAppScope: callIndex !== -1 && inSameBlockAfter(code, appIndex, callIndex),
@@ -5599,8 +6027,8 @@ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
5599
6027
  }
5600
6028
  function namesGuardModule(specifier, fileRel, guard) {
5601
6029
  return resolvesToGuardModule(specifier, {
5602
- fromFile: join13(guard.cwd, fileRel),
5603
- guardDir: resolve3(join13(guard.cwd, guard.guardDir)),
6030
+ fromFile: join17(guard.cwd, fileRel),
6031
+ guardDir: resolve3(join17(guard.cwd, guard.guardDir)),
5604
6032
  kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
5605
6033
  strictExtensions: true
5606
6034
  });
@@ -5619,8 +6047,17 @@ function wireRegister(cwd, opts, spec) {
5619
6047
  }
5620
6048
  const target = guardTarget(cwd, entry.relPath, spec);
5621
6049
  const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
5622
- const p = join13(cwd, entry.relPath);
6050
+ if (!matchesGuardTemplate(cwd, `${dir}/${target.file}`, target.template)) {
6051
+ log(`${dir}/${target.file} needs manual review; the existing helper and ${entry.relPath} were left untouched.`);
6052
+ return { ok: false, changed };
6053
+ }
6054
+ const p = join17(cwd, entry.relPath);
5623
6055
  const s = read(p);
6056
+ const compiler = sourceCompiler(cwd);
6057
+ if (!compiler || !parsedSource(compiler, entry.relPath, s)) {
6058
+ log(`${entry.relPath} left untouched: a working TypeScript parser is required for source edits; ${spec.manualHint}`);
6059
+ return { ok: false, changed };
6060
+ }
5624
6061
  const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
5625
6062
  if (state.importAtTopLevel && state.callInAppScope) {
5626
6063
  log(
@@ -5647,17 +6084,35 @@ function wireRegister(cwd, opts, spec) {
5647
6084
  const lines = s.split("\n");
5648
6085
  if (!state.importAtTopLevel) {
5649
6086
  const lastImport = lastTopLevelImportLine(s);
5650
- const importIdx = lastImport === -1 ? firstStatementLine(lines) : lastImport + 1;
6087
+ const importEnd = lastImport === -1 ? -1 : statementEndLine(compiler, entry.relPath, s, lastImport);
6088
+ if (importEnd === null) return { ok: false, changed };
6089
+ const importIdx = lastImport === -1 ? firstStatementLine(lines) : importEnd + 1;
5651
6090
  lines.splice(importIdx, 0, importLine);
5652
6091
  }
5653
6092
  const appIdx = lines.findIndex((l) => spec.appRe.test(l));
5654
6093
  if (appIdx !== -1 && !state.callInAppScope) {
5655
6094
  const preferred = spec.callAfter?.(entry.appVar);
5656
- const preferredIdx = preferred ? lines.findIndex((line, index) => index > appIdx && preferred.test(line)) : -1;
6095
+ const preferredIdx = preferred ? lines.reduce((last, line, index) => index > appIdx && preferred.test(line) && inSameBlockAfter(lines.join("\n"), appIdx, index) ? index : last, -1) : -1;
5657
6096
  const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
5658
- lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
6097
+ if (spec.requireCallAfter && preferredIdx === -1) return { ok: false, changed };
6098
+ const current = lines.join("\n");
6099
+ const statement = statementPosition(compiler, entry.relPath, current, callIdx, preferredIdx === -1 ? spec.appRe : preferred);
6100
+ if (statement === null) return { ok: false, changed };
6101
+ const { start, end } = statement;
6102
+ const tail = current.slice(end);
6103
+ const trailingComment = /^[ \t]*\/\//.test(tail);
6104
+ const insertion = trailingComment ? current.indexOf("\n", end) : end;
6105
+ const position = insertion === -1 ? current.length : insertion;
6106
+ const separator = current.slice(0, end).trimEnd().endsWith(";") ? "" : ";";
6107
+ const prefix = current.slice(current.lastIndexOf("\n", start - 1) + 1, start).trim();
6108
+ const updated = current.slice(0, start) + (prefix ? "\n" : "") + current.slice(start, end) + separator + current.slice(end, position) + "\n" + REGION + "\n" + spec.call(entry.appVar) + "\n// #endregion patchstack\n" + current.slice(position);
6109
+ lines.splice(0, lines.length, ...updated.split("\n"));
5659
6110
  }
5660
6111
  const patched = lines.join("\n");
6112
+ if (!parsedSource(compiler, entry.relPath, patched)) {
6113
+ log(`${entry.relPath} would not parse after patching \u2014 left untouched; ${spec.manualHint}`);
6114
+ return { ok: false, changed };
6115
+ }
5661
6116
  writeProjectFileSync(cwd, p, patched, { encoding: "utf8" });
5662
6117
  if (spec.callAfter) {
5663
6118
  const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
@@ -5689,26 +6144,30 @@ function firstStatementLine(lines) {
5689
6144
  return i;
5690
6145
  }
5691
6146
  function serverIsGuarded(cwd, spec, server, guardDir) {
5692
- const source = read(join13(cwd, server.relPath));
6147
+ const target = guardTarget(cwd, server.relPath, spec);
6148
+ if (!matchesGuardTemplate(cwd, `${guardDir}/${target.file}`, target.template)) return false;
6149
+ const source = read(join17(cwd, server.relPath));
5693
6150
  const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
5694
6151
  if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
5695
6152
  return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
5696
6153
  }
5697
6154
  function verifyRegister(cwd, spec) {
5698
- const dir = existsSync9(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
6155
+ const dir = existsSync12(join17(cwd, "src")) ? "src/patchstack" : "patchstack";
5699
6156
  const entry = findAppInstance(cwd, spec.appRe);
5700
6157
  const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
5701
- const scaffolded = target ? existsSync9(join13(cwd, dir, target.file)) : false;
5702
- const entrySource = entry ? read(join13(cwd, entry.relPath)) : "";
6158
+ const scaffolded = target ? existsSync12(join17(cwd, dir, target.file)) : false;
6159
+ const helperVerified = !!target && matchesGuardTemplate(cwd, `${dir}/${target.file}`, target.template);
6160
+ const entrySource = entry ? read(join17(cwd, entry.relPath)) : "";
5703
6161
  const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
5704
6162
  const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
5705
6163
  const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
5706
6164
  const noEarlyRoutes = early.length === 0;
5707
6165
  const unguarded = (entry?.others ?? []).filter((other) => !serverIsGuarded(cwd, spec, other, dir)).map((other) => other.relPath);
5708
6166
  return {
5709
- wired: scaffolded && wired && noEarlyRoutes && unguarded.length === 0,
6167
+ wired: scaffolded && helperVerified && wired && noEarlyRoutes && unguarded.length === 0,
5710
6168
  checks: [
5711
6169
  { label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
6170
+ { label: "guard helper implementation verified", ok: helperVerified, hint: "preserved custom helpers require manual review of their exports and request/response screening" },
5712
6171
  { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint },
5713
6172
  ...entry && entry.others.length > 0 ? [
5714
6173
  {
@@ -5789,8 +6248,8 @@ var expressAdapter = {
5789
6248
  };
5790
6249
 
5791
6250
  // src/protect/install/reporting.ts
5792
- import { existsSync as existsSync10 } from "fs";
5793
- import { join as join14 } from "path";
6251
+ import { existsSync as existsSync13 } from "fs";
6252
+ import { join as join18 } from "path";
5794
6253
 
5795
6254
  // src/protect/reporting-state.js
5796
6255
  var REPORTING_STATES = Object.freeze([
@@ -5848,9 +6307,9 @@ function explainReportingState(state) {
5848
6307
 
5849
6308
  // src/protect/install/reporting.ts
5850
6309
  function siteIdentity(cwd, env) {
5851
- const rc = join14(cwd, ".patchstackrc.json");
6310
+ const rc = join18(cwd, ".patchstackrc.json");
5852
6311
  let recorded;
5853
- if (existsSync10(rc)) {
6312
+ if (existsSync13(rc)) {
5854
6313
  try {
5855
6314
  recorded = JSON.parse(read(rc)).siteUuid;
5856
6315
  } catch {
@@ -5906,6 +6365,7 @@ function reportingChecks(cwd, env = process.env) {
5906
6365
  // src/protect/install/index.ts
5907
6366
  var ADAPTERS = [
5908
6367
  tanstackSupabaseAdapter,
6368
+ tanstackAdapter,
5909
6369
  nextAdapter,
5910
6370
  sveltekitAdapter,
5911
6371
  astroAdapter,
@@ -5930,7 +6390,10 @@ function runProtect(cwd, opts = {}) {
5930
6390
  adapter = ADAPTERS.find((a) => a.detect(cwd));
5931
6391
  if (adapter) {
5932
6392
  const result = adapter.wire(cwd, opts);
5933
- return { status: "wired", adapter: adapter.name, changed: result.changed };
6393
+ const verification = adapter.verify(cwd);
6394
+ if (result.ok && verification.wired) return { status: "wired", adapter: adapter.name, changed: result.changed };
6395
+ const plan = verification.checks.filter((check) => !check.ok).map((check) => check.hint ?? check.label).join("\n");
6396
+ return { status: "scaffolded", adapter: adapter.name, changed: result.changed, plan };
5934
6397
  }
5935
6398
  } catch (err) {
5936
6399
  log(`adapter ${adapter?.name ?? ""} failed (${err?.message ?? err}); falling back to a generic guard`);
@@ -5945,7 +6408,7 @@ function runProtect(cwd, opts = {}) {
5945
6408
  return { status: "scaffolded", adapter: "generic", changed: [], plan: "" };
5946
6409
  }
5947
6410
  }
5948
- var RUNTIMES_WITHOUT_CONFIG_FILE = /* @__PURE__ */ new Set(["nextjs", "sveltekit", "astro", "nuxt", "generic"]);
6411
+ var RUNTIMES_WITHOUT_CONFIG_FILE = /* @__PURE__ */ new Set(["tanstack-start", "nextjs", "sveltekit", "astro", "nuxt", "generic"]);
5949
6412
  function credentialNote(cwd, adapterName) {
5950
6413
  if (!RUNTIMES_WITHOUT_CONFIG_FILE.has(adapterName)) return [];
5951
6414
  const localHint = hasResolvableCredential(cwd) ? "a credential is configured here; set the same one in your deployment" : "no credential found here either \u2014 run `npx @patchstack/connect scan` to provision one";
@@ -5999,7 +6462,7 @@ function runVerify(cwd) {
5999
6462
  }
6000
6463
 
6001
6464
  // src/widget.ts
6002
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
6465
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
6003
6466
  import path11 from "path";
6004
6467
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
6005
6468
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
@@ -6038,7 +6501,7 @@ ${indent}</body>`;
6038
6501
  }
6039
6502
  function findSourceShell(cwd) {
6040
6503
  for (const candidate of SOURCE_SHELL_CANDIDATES) {
6041
- if (existsSync11(path11.join(cwd, candidate))) {
6504
+ if (existsSync14(path11.join(cwd, candidate))) {
6042
6505
  return candidate;
6043
6506
  }
6044
6507
  }
@@ -6050,7 +6513,7 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
6050
6513
  return { shell: null, action: "no-shell" };
6051
6514
  }
6052
6515
  const file = path11.join(cwd, shell);
6053
- if (!existsSync11(file)) {
6516
+ if (!existsSync14(file)) {
6054
6517
  return { shell: null, action: "no-shell" };
6055
6518
  }
6056
6519
  const before = readFileSync10(file, "utf8");
@@ -6276,7 +6739,7 @@ function detectPackageManager(cwd) {
6276
6739
  return declared;
6277
6740
  }
6278
6741
  for (const { filename, pm } of PM_BY_LOCKFILE) {
6279
- if (existsSync12(path12.join(cwd, filename))) {
6742
+ if (existsSync15(path12.join(cwd, filename))) {
6280
6743
  return pm;
6281
6744
  }
6282
6745
  }
@@ -6373,7 +6836,7 @@ function resolveWidgetFileHint(cwd, framework) {
6373
6836
  ...GENERIC_WIDGET_FILES
6374
6837
  ];
6375
6838
  for (const candidate of candidates) {
6376
- if (existsSync12(path12.join(cwd, candidate))) {
6839
+ if (existsSync15(path12.join(cwd, candidate))) {
6377
6840
  return candidate;
6378
6841
  }
6379
6842
  }
@@ -6453,9 +6916,9 @@ async function collectGuideState(cwd) {
6453
6916
  hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
6454
6917
  installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
6455
6918
  // The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
6456
- // must retain. Bun skips npm-style pre/post scripts, so its build chain follows the same order.
6457
- prebuildWired: /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
6458
- postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
6919
+ // must retain. Non-npm managers use explicit chains independent of lifecycle settings.
6920
+ prebuildWired: packageManager === "npm" && /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
6921
+ postbuildWired: packageManager === "npm" && (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
6459
6922
  widgetInstalled: widget.found,
6460
6923
  widgetTokenMatches: widget.uuidMatches,
6461
6924
  widgetOptOut,
@@ -6525,10 +6988,10 @@ function buildScriptLines(state) {
6525
6988
  const lines = [];
6526
6989
  if (!state.installScanWired) lines.push('"postinstall": "patchstack-connect scan"');
6527
6990
  if (state.hasBuildScript && !(state.prebuildWired && state.postbuildWired)) {
6528
- if (state.packageManager === "bun") {
6529
- lines.push('"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"');
6991
+ if (state.packageManager !== "npm") {
6992
+ lines.push('"build": "patchstack-connect scan && patchstack-connect map --upload && <existing build command> && patchstack-connect mark-build"');
6530
6993
  } else {
6531
- if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan"');
6994
+ if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan && patchstack-connect map --upload"');
6532
6995
  if (!state.postbuildWired) lines.push('"postbuild": "patchstack-connect mark-build"');
6533
6996
  }
6534
6997
  }
@@ -6646,7 +7109,7 @@ import {
6646
7109
  } from "fs";
6647
7110
  import { tmpdir } from "os";
6648
7111
  import path13 from "path";
6649
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7112
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
6650
7113
  function baseFrom(manifestEndpoint) {
6651
7114
  const url = new URL(manifestEndpoint);
6652
7115
  const p = url.pathname.replace(/\/$/, "");
@@ -6656,7 +7119,7 @@ function baseFrom(manifestEndpoint) {
6656
7119
  return url.toString().replace(/\/$/, "");
6657
7120
  }
6658
7121
  function pendingPath(siteUuid, intent) {
6659
- const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
7122
+ const key = createHash3("sha256").update(siteUuid).digest("hex").slice(0, 16);
6660
7123
  return path13.join(tmpdir(), `patchstack-${intent}-${key}.json`);
6661
7124
  }
6662
7125
  function savePending(siteUuid, intent, pending) {
@@ -6932,8 +7395,8 @@ async function claim2(config, onPrompt, deps = {}) {
6932
7395
  import { relative as relative5 } from "path";
6933
7396
 
6934
7397
  // src/protect/install/runtime/entry.ts
6935
- import { existsSync as existsSync13, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
6936
- import { join as join15, resolve as resolve4, sep as sep2 } from "path";
7398
+ import { existsSync as existsSync16, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
7399
+ import { join as join19, resolve as resolve4, sep as sep2 } from "path";
6937
7400
  var CONVENTIONAL = [
6938
7401
  "server.js",
6939
7402
  "server.mjs",
@@ -6959,7 +7422,7 @@ var CONVENTIONAL = [
6959
7422
  var LOADABLE = /\.(?:js|mjs|cjs)$/;
6960
7423
  function readPackage2(cwd) {
6961
7424
  try {
6962
- const parsed = JSON.parse(readFileSync13(join15(cwd, "package.json"), "utf8"));
7425
+ const parsed = JSON.parse(readFileSync13(join19(cwd, "package.json"), "utf8"));
6963
7426
  return parsed !== null && typeof parsed === "object" ? parsed : null;
6964
7427
  } catch {
6965
7428
  return null;
@@ -7029,14 +7492,14 @@ function resolveEntry(cwd) {
7029
7492
  }
7030
7493
  return {
7031
7494
  kind: "unavailable",
7032
- reason: existsSync13(join15(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
7495
+ reason: existsSync16(join19(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
7033
7496
  };
7034
7497
  }
7035
7498
 
7036
7499
  // src/protect/install/runtime/probe.ts
7037
7500
  import { spawn } from "child_process";
7038
- import { createHash as createHash3, randomBytes } from "crypto";
7039
- import { existsSync as existsSync14 } from "fs";
7501
+ import { createHash as createHash4, randomBytes } from "crypto";
7502
+ import { existsSync as existsSync17 } from "fs";
7040
7503
  import { request as httpRequest } from "http";
7041
7504
  import { request as httpsRequest, Agent as HttpsAgent } from "https";
7042
7505
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -7053,7 +7516,7 @@ var ANSWER_LIMIT = 8192;
7053
7516
  var LOOPBACK = ["127.0.0.1", "::1"];
7054
7517
  var ENDING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
7055
7518
  function expectedAnswerFor(challenge) {
7056
- return createHash3("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
7519
+ return createHash4("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
7057
7520
  }
7058
7521
  function preloadPath() {
7059
7522
  const candidates = [
@@ -7064,7 +7527,7 @@ function preloadPath() {
7064
7527
  ];
7065
7528
  for (const candidate of candidates) {
7066
7529
  const path17 = fileURLToPath2(candidate);
7067
- if (existsSync14(path17)) return path17;
7530
+ if (existsSync17(path17)) return path17;
7068
7531
  }
7069
7532
  return null;
7070
7533
  }
@@ -7414,12 +7877,12 @@ function formatRuntimeCheck(report) {
7414
7877
  import path14 from "path";
7415
7878
 
7416
7879
  // src/map/ts-loader.ts
7417
- import { createRequire as createRequire2 } from "module";
7880
+ import { createRequire as createRequire5 } from "module";
7418
7881
  import { pathToFileURL } from "url";
7419
- import { join as join16 } from "path";
7882
+ import { join as join20 } from "path";
7420
7883
  async function loadTypeScript(cwd) {
7421
7884
  try {
7422
- const req = createRequire2(pathToFileURL(join16(cwd, "package.json")));
7885
+ const req = createRequire5(pathToFileURL(join20(cwd, "package.json")));
7423
7886
  const resolved = req.resolve("typescript");
7424
7887
  const mod = await import(pathToFileURL(resolved).href);
7425
7888
  return mod.default ?? mod;
@@ -7435,7 +7898,7 @@ async function loadTypeScript(cwd) {
7435
7898
 
7436
7899
  // src/map/extract.ts
7437
7900
  import { readFileSync as readFileSync16, realpathSync as realpathSync6 } from "fs";
7438
- import { createHash as createHash5 } from "crypto";
7901
+ import { createHash as createHash6 } from "crypto";
7439
7902
  import { relative as relative7 } from "path";
7440
7903
 
7441
7904
  // src/map/bindings.ts
@@ -7565,7 +8028,7 @@ function npmPackageOf(spec) {
7565
8028
  }
7566
8029
 
7567
8030
  // src/map/sinks.ts
7568
- import { createHash as createHash4 } from "crypto";
8031
+ import { createHash as createHash5 } from "crypto";
7569
8032
 
7570
8033
  // src/map/capabilities.ts
7571
8034
  var PACKAGE_SINK_MODELS = [
@@ -7964,7 +8427,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
7964
8427
  return table?.[index] ?? "unknown";
7965
8428
  }
7966
8429
  function sinkId(s, owner) {
7967
- return createHash4("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
8430
+ return createHash5("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
7968
8431
  }
7969
8432
  function dedupeSinks(sinks, owner) {
7970
8433
  const seen = /* @__PURE__ */ new Set();
@@ -8986,11 +9449,11 @@ function capEndpoint(e, limits) {
8986
9449
  drop((f) => kept.has(byIndex.get(f)));
8987
9450
  }
8988
9451
  const ranked = [...flows].sort((a, b) => keepPriority(b) - keepPriority(a));
8989
- const pick = (items, limit, idOf) => {
9452
+ const pick = (items, limit, idOf2) => {
8990
9453
  if (items.length <= limit) return { kept: items, omitted: 0 };
8991
9454
  const wanted = /* @__PURE__ */ new Set();
8992
9455
  for (const f of ranked) {
8993
- const id = idOf(f);
9456
+ const id = idOf2(f);
8994
9457
  if (id !== void 0 && wanted.size < limit) wanted.add(id);
8995
9458
  }
8996
9459
  const chosen = new Set(items.filter((it) => it.id !== void 0 && wanted.has(it.id)));
@@ -9090,7 +9553,7 @@ function withNote(doc) {
9090
9553
 
9091
9554
  // src/map/module-graph.ts
9092
9555
  import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
9093
- import { dirname as dirname9, join as join17, relative as relative6, resolve as resolvePath } from "path";
9556
+ import { dirname as dirname9, join as join21, relative as relative6, resolve as resolvePath } from "path";
9094
9557
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
9095
9558
  function createModuleGraph(ts, opts) {
9096
9559
  const cache = /* @__PURE__ */ new Map();
@@ -9168,7 +9631,7 @@ function resolveRelativeModule(fromFile, spec) {
9168
9631
  }
9169
9632
  candidates.push(base);
9170
9633
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
9171
- for (const e of RESOLVE_EXTS) candidates.push(join17(base, "index" + e));
9634
+ for (const e of RESOLVE_EXTS) candidates.push(join21(base, "index" + e));
9172
9635
  for (const c of candidates) {
9173
9636
  try {
9174
9637
  if (statSync7(c).isFile()) return c;
@@ -9458,7 +9921,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
9458
9921
 
9459
9922
  // src/map/imports.ts
9460
9923
  import { readFileSync as readFileSync15 } from "fs";
9461
- import { join as join18 } from "path";
9924
+ import { join as join23 } from "path";
9462
9925
  var MAX_SITES2 = 5;
9463
9926
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
9464
9927
  function isNpmPackageName(pkg) {
@@ -9469,7 +9932,7 @@ function readPathAliases(cwd) {
9469
9932
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
9470
9933
  let paths2;
9471
9934
  try {
9472
- const raw = readFileSync15(join18(cwd, name), "utf8");
9935
+ const raw = readFileSync15(join23(cwd, name), "utf8");
9473
9936
  const parsed = JSON.parse(stripJsonComments(raw));
9474
9937
  paths2 = parsed.compilerOptions?.paths;
9475
9938
  } catch {
@@ -9736,7 +10199,7 @@ async function extractInputMap(cwd, ts, options = {}) {
9736
10199
  let sourceBytes = 0;
9737
10200
  const calls = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
9738
10201
  const startedAt = Date.now();
9739
- const entryFiles = /* @__PURE__ */ new Set();
10202
+ const entryFiles2 = /* @__PURE__ */ new Set();
9740
10203
  const hopTargets = /* @__PURE__ */ new Set();
9741
10204
  for (const file of files) {
9742
10205
  try {
@@ -9762,8 +10225,8 @@ async function extractInputMap(cwd, ts, options = {}) {
9762
10225
  continue;
9763
10226
  }
9764
10227
  parsed++;
9765
- entryFiles.add(file);
9766
- const fingerprint = createHash5("sha256").update(text).digest("hex").slice(0, 16);
10228
+ entryFiles2.add(file);
10229
+ const fingerprint = createHash6("sha256").update(text).digest("hex").slice(0, 16);
9767
10230
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
9768
10231
  const bindings = buildModuleBindings(sf, ts);
9769
10232
  for (const specifier of bindings.imports) {
@@ -9816,7 +10279,7 @@ async function extractInputMap(cwd, ts, options = {}) {
9816
10279
  let hopParsed = 0;
9817
10280
  let hopFailures = 0;
9818
10281
  for (const target of hopTargets) {
9819
- if (entryFiles.has(target)) continue;
10282
+ if (entryFiles2.has(target)) continue;
9820
10283
  try {
9821
10284
  const text = readFileSync16(target, "utf8");
9822
10285
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
@@ -9982,7 +10445,7 @@ function getStringFlag(flags, name) {
9982
10445
 
9983
10446
  // src/build-stamp.ts
9984
10447
  import { lstatSync as lstatSync5, readFileSync as readFileSync17, readdirSync as readdirSync7, realpathSync as realpathSync7 } from "fs";
9985
- import { join as join19, relative as relative8, sep as sep3 } from "path";
10448
+ import { join as join24, relative as relative8, sep as sep3 } from "path";
9986
10449
  var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
9987
10450
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
9988
10451
  var MAX_DEPTH2 = 6;
@@ -10000,7 +10463,7 @@ function guardImportsRulesFile(dir, rulesName) {
10000
10463
  }
10001
10464
  for (const name of siblings) {
10002
10465
  if (!GUARD_SOURCE.test(name)) continue;
10003
- const path17 = join19(dir, name);
10466
+ const path17 = join24(dir, name);
10004
10467
  try {
10005
10468
  const entry = lstatSync5(path17);
10006
10469
  if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
@@ -10043,7 +10506,7 @@ function findRulesFile(cwd) {
10043
10506
  }
10044
10507
  for (const name of entries) {
10045
10508
  if (name.startsWith(".")) continue;
10046
- const path17 = join19(dir, name);
10509
+ const path17 = join24(dir, name);
10047
10510
  let entry;
10048
10511
  try {
10049
10512
  entry = lstatSync5(path17);
@@ -10121,6 +10584,8 @@ function applyBuildStamp(cwd, id) {
10121
10584
  }
10122
10585
 
10123
10586
  // src/build-hook.ts
10587
+ import { readFileSync as readFileSync18 } from "fs";
10588
+ import { join as join25 } from "path";
10124
10589
  var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
10125
10590
  "preinstall",
10126
10591
  "install",
@@ -10134,11 +10599,16 @@ function isInstallOrBuildHook(env = process.env) {
10134
10599
  const event = env.npm_lifecycle_event;
10135
10600
  return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
10136
10601
  }
10137
- function isPreBundleBuildHook(env = process.env) {
10602
+ function isPreBundleBuildHook(env = process.env, cwd = process.cwd()) {
10138
10603
  const event = env.npm_lifecycle_event;
10139
10604
  if (event === "prebuild") return true;
10140
10605
  if (event !== "build") return false;
10141
- return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
10606
+ try {
10607
+ const pkg = JSON.parse(readFileSync18(join25(cwd, "package.json"), "utf8"));
10608
+ return typeof pkg.scripts?.build === "string" && /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg.scripts.build);
10609
+ } catch {
10610
+ return false;
10611
+ }
10142
10612
  }
10143
10613
  function undeliveredReportLines(err, config, cwd) {
10144
10614
  const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
@@ -10162,7 +10632,7 @@ function undeliveredReportLines(err, config, cwd) {
10162
10632
  }
10163
10633
 
10164
10634
  // src/input-map-id.ts
10165
- import { createHash as createHash6 } from "crypto";
10635
+ import { createHash as createHash7 } from "crypto";
10166
10636
  var NonCanonicalInputMap = class extends Error {
10167
10637
  };
10168
10638
  function canonicalString(value) {
@@ -10190,7 +10660,7 @@ function canonicalText(value) {
10190
10660
  if (Array.isArray(value)) return `[${value.map(canonicalText).join(",")}]`;
10191
10661
  if (value !== null && typeof value === "object") {
10192
10662
  const record = value;
10193
- const keys = Object.keys(record).sort();
10663
+ const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
10194
10664
  return `{${keys.map((key) => `${canonicalString(key)}:${canonicalText(record[key])}`).join(",")}}`;
10195
10665
  }
10196
10666
  throw new NonCanonicalInputMap(`the input map contains a value with no JSON form (${typeof value})`);
@@ -10206,27 +10676,32 @@ function inputMapBuildId(map) {
10206
10676
  material = { ...mapDocument, coverage: stableCoverage };
10207
10677
  } else material = mapDocument;
10208
10678
  }
10209
- return createHash6("sha256").update("patchstack-input-map-v1\0", "utf8").update(canonicalText(material), "utf8").digest("hex");
10679
+ return createHash7("sha256").update("patchstack-input-map-v1\0", "utf8").update(canonicalText(material), "utf8").digest("hex");
10210
10680
  }
10211
10681
 
10212
10682
  // src/map-command.ts
10213
10683
  async function runMap(flags) {
10684
+ return (await runMapDetailed(flags)).code;
10685
+ }
10686
+ async function runMapDetailed(flags, options = {}) {
10687
+ const log2 = options.log ?? console.error;
10214
10688
  const cwd = getStringFlag(flags, "dir") ?? process.cwd();
10689
+ if (options.setup) applyBuildStamp(cwd, null);
10215
10690
  const { map, error } = await buildInputMap(cwd, {
10216
10691
  followSymlinks: flags.get("follow-symlinks") === true
10217
10692
  });
10218
10693
  if (!map) {
10219
- console.error(`patchstack: ${error}`);
10220
- return 1;
10694
+ log2(`patchstack: ${error}`);
10695
+ return { code: isPreBundleBuildHook() ? 0 : 1, error: error ?? "could not analyse the project" };
10221
10696
  }
10222
10697
  const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
10223
10698
  const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
10224
10699
  const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
10225
10700
  const c = map.coverage;
10226
- console.error(
10701
+ log2(
10227
10702
  `patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
10228
10703
  );
10229
- console.error(
10704
+ log2(
10230
10705
  // All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
10231
10706
  // simply contain no server entry point (most of a project is client code). Only `skipped` is a
10232
10707
  // failure to analyse.
@@ -10239,14 +10714,14 @@ async function runMap(flags) {
10239
10714
  const ambiguous = c2.callsAmbiguous ?? 0;
10240
10715
  const denominator = dependency + ambiguous;
10241
10716
  const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
10242
- console.error(
10717
+ log2(
10243
10718
  `patchstack: ${invoked.length} dependency API call(s) resolved across ${new Set(invoked.map((i) => i.package)).size} package(s) from ${c2.callsTotal ?? 0} call site(s) \u2014 ${quality}% of dependency-candidate receivers resolved (${c2.callsLocal ?? 0} local, ${ambiguous} ambiguous). Positive evidence only: absence here never means an API is not called.`
10244
10719
  );
10245
10720
  }
10246
10721
  const imported = map.imports ?? [];
10247
10722
  if (imported.length > 0) {
10248
10723
  const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
10249
- console.error(
10724
+ log2(
10250
10725
  `patchstack: ${imported.length} package(s) imported \u2014 ${unmodelled} with no recognized sink family, so a vulnerability in those cannot be judged reachable or unreachable from this map.`
10251
10726
  );
10252
10727
  }
@@ -10254,62 +10729,75 @@ async function runMap(flags) {
10254
10729
  const out = getStringFlag(flags, "out");
10255
10730
  if (out) {
10256
10731
  atomicWriteFileSync(path14.resolve(out), json, { encoding: "utf8" });
10257
- console.error(`patchstack: wrote ${out}`);
10732
+ log2(`patchstack: wrote ${out}`);
10258
10733
  } else if (flags.get("upload") !== true) {
10259
10734
  console.log(json);
10260
10735
  }
10261
10736
  if (flags.get("upload") === true) {
10262
10737
  const problems = ingestProblems(map);
10263
10738
  if (problems.length > 0) {
10264
- console.error(`patchstack: did not upload the attack surface \u2014 it cannot be fitted to the size Patchstack accepts (${problems.join("; ")}).`);
10265
- return 0;
10739
+ log2(`patchstack: did not upload the attack surface \u2014 it cannot be fitted to the size Patchstack accepts (${problems.join("; ")}).`);
10740
+ return { code: 0, endpoints: map.endpoints.length, error: problems.join("; ") };
10266
10741
  }
10267
10742
  if (map.endpoints.length === 0) {
10268
- console.error(
10743
+ log2(
10269
10744
  "patchstack: no server entry points were recognized \u2014 uploading the import inventory and coverage notes anyway, so a vulnerability can still be judged imported or not. Nothing here can decide whether a request reaches it."
10270
10745
  );
10271
10746
  }
10272
- const config = await resolveConfig({
10747
+ const config = options.config ?? await resolveConfig({
10273
10748
  cwd,
10274
10749
  cliSiteUuid: getStringFlag(flags, "site-uuid"),
10275
10750
  cliEndpoint: getStringFlag(flags, "endpoint")
10276
10751
  });
10277
10752
  let buildId = null;
10278
- if (isPreBundleBuildHook()) {
10279
- const candidate = inputMapBuildId(map);
10280
- const stamp = applyBuildStamp(cwd, candidate);
10281
- if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
10753
+ if (options.setup || isPreBundleBuildHook()) {
10754
+ let candidate = null;
10755
+ let identityError = null;
10756
+ try {
10757
+ candidate = inputMapBuildId(map);
10758
+ } catch (err) {
10759
+ if (!(err instanceof NonCanonicalInputMap)) throw err;
10760
+ identityError = err.message;
10761
+ }
10762
+ const stamp = candidate === null ? null : applyBuildStamp(cwd, candidate);
10763
+ if (candidate === null || stamp === null) {
10764
+ log2(
10765
+ `patchstack: could not bind this map to the runtime guard \u2014 ${identityError}. Rules generated from these coordinates will detect only, not block.`
10766
+ );
10767
+ } else if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
10282
10768
  buildId = candidate;
10283
- console.error(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
10769
+ log2(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
10284
10770
  } else {
10285
10771
  const reason = stamp.kind === "skipped" ? stamp.reason : "the rules file did not retain the map identity";
10286
- console.error(
10772
+ log2(
10287
10773
  `patchstack: could not bind this map to the runtime guard \u2014 ${reason}. Rules generated from these coordinates will detect only, not block.`
10288
10774
  );
10289
10775
  }
10290
10776
  } else {
10291
- console.error(
10777
+ log2(
10292
10778
  "patchstack: no runtime binding recorded \u2014 run `map --upload` in a prebuild hook before the bundler, so rules generated from these coordinates can be tied to the runtime guard. Until then they detect only, not block."
10293
10779
  );
10294
10780
  }
10295
10781
  const outcome = await postInputMap(config, map, buildId);
10296
10782
  if (outcome.result === "stored") {
10297
- console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
10783
+ log2(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
10298
10784
  } else if (outcome.result === "unchanged") {
10299
- console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
10785
+ log2(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
10300
10786
  } else if (outcome.result === "skipped") {
10301
- console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
10787
+ log2(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
10302
10788
  } else {
10303
- console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
10789
+ log2(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
10304
10790
  }
10791
+ return { code: 0, endpoints: map.endpoints.length, buildId, upload: outcome };
10305
10792
  }
10306
- return 0;
10793
+ return { code: 0, endpoints: map.endpoints.length };
10307
10794
  }
10308
10795
 
10309
10796
  // src/setup.ts
10310
- import { readFileSync as readFileSync18 } from "fs";
10797
+ import { readFileSync as readFileSync19 } from "fs";
10311
10798
  import path15 from "path";
10312
10799
  var SCAN_COMMAND = "patchstack-connect scan";
10800
+ var MAP_COMMAND = "patchstack-connect map --upload";
10313
10801
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
10314
10802
  function setupProtection(cwd) {
10315
10803
  const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
@@ -10329,12 +10817,65 @@ function prependHook(existing, command) {
10329
10817
  if (existing === void 0 || existing.trim().length === 0) return command;
10330
10818
  const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10331
10819
  if (new RegExp(`^\\s*${escaped}\\s*(?:;|\\|\\|)`).test(existing)) return existing;
10332
- const remaining = existing.split(/\s*&&\s*/).filter((part) => part.trim() !== command && part.trim().length > 0);
10820
+ const remaining = splitAndChain(existing).filter((part) => part.trim() !== command && part.trim().length > 0);
10333
10821
  return [command, ...remaining].join(" && ");
10334
10822
  }
10823
+ function splitAndChain(source) {
10824
+ const parts = [];
10825
+ let start = 0;
10826
+ let quote = "";
10827
+ const groups = [];
10828
+ for (let i = 0; i < source.length; i++) {
10829
+ const char = source[i];
10830
+ if (char === "\\" && quote !== "'") {
10831
+ i++;
10832
+ continue;
10833
+ }
10834
+ if (quote === "'" || quote === "`") {
10835
+ if (char === quote) quote = quote === "`" ? groups.pop().quote : "";
10836
+ continue;
10837
+ }
10838
+ if (char === "`") {
10839
+ groups.push({ close: "`", quote });
10840
+ quote = "`";
10841
+ continue;
10842
+ }
10843
+ if (char === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
10844
+ groups.push({ close: source[++i] === "(" ? ")" : "}", quote });
10845
+ quote = "";
10846
+ continue;
10847
+ }
10848
+ if (quote) {
10849
+ if (char === quote) quote = "";
10850
+ continue;
10851
+ }
10852
+ if (char === '"' || char === "'") {
10853
+ quote = char;
10854
+ continue;
10855
+ }
10856
+ if (char === "#" && (i === 0 || /\s/.test(source[i - 1]))) {
10857
+ const newline = source.indexOf("\n", i);
10858
+ if (newline === -1) break;
10859
+ i = newline;
10860
+ continue;
10861
+ }
10862
+ if (char === "(" || char === "{") groups.push({ close: char === "(" ? ")" : "}", quote: "" });
10863
+ if (char === groups.at(-1)?.close) quote = groups.pop().quote;
10864
+ if (groups.length === 0 && char === "&" && source[i + 1] === "&") {
10865
+ parts.push(source.slice(start, i).trim());
10866
+ start = ++i + 1;
10867
+ }
10868
+ }
10869
+ parts.push(source.slice(start).trim());
10870
+ return parts;
10871
+ }
10872
+ function finishWithMap(existing) {
10873
+ if (/(?:^|&&|;)\s*patchstack-connect map --upload\s*$/.test(existing)) return existing;
10874
+ return `${existing} && ${MAP_COMMAND}`;
10875
+ }
10335
10876
  function wireBuildScripts(cwd, packageManager) {
10336
10877
  const target = path15.join(cwd, "package.json");
10337
- const raw = readFileSync18(target, "utf8");
10878
+ const raw = readFileSync19(target, "utf8");
10338
10879
  const pkg = JSON.parse(raw);
10339
10880
  const scripts = pkg.scripts ?? {};
10340
10881
  const build = scripts.build;
@@ -10348,8 +10889,11 @@ function wireBuildScripts(cwd, packageManager) {
10348
10889
  };
10349
10890
  }
10350
10891
  scripts.postinstall = postinstall;
10351
- } else if (packageManager === "bun") {
10892
+ } else if (packageManager !== "npm") {
10352
10893
  let nextBuild = prependHook(build, SCAN_COMMAND);
10894
+ if (!/^\s*patchstack-connect scan\s*&&\s*patchstack-connect map --upload(?:\s*(?:&&|;)|\s*$)/.test(nextBuild)) {
10895
+ nextBuild = nextBuild.replace(SCAN_COMMAND, `${SCAN_COMMAND} && ${MAP_COMMAND}`);
10896
+ }
10353
10897
  if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
10354
10898
  nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
10355
10899
  }
@@ -10363,7 +10907,7 @@ function wireBuildScripts(cwd, packageManager) {
10363
10907
  scripts.postinstall = postinstall;
10364
10908
  scripts.build = nextBuild;
10365
10909
  } else {
10366
- const prebuild = prependHook(scripts.prebuild, SCAN_COMMAND);
10910
+ const prebuild = finishWithMap(prependHook(scripts.prebuild, SCAN_COMMAND));
10367
10911
  const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
10368
10912
  if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
10369
10913
  return {
@@ -10388,98 +10932,1892 @@ function wireBuildScripts(cwd, packageManager) {
10388
10932
  detail: "added a dependency-install scan; package.json has no build script."
10389
10933
  };
10390
10934
  }
10391
- return packageManager === "bun" ? {
10935
+ return packageManager !== "npm" ? {
10392
10936
  changed: true,
10393
10937
  strategy: "build-chain",
10394
- detail: "added a dependency-install scan and chained scan/mark-build around the build."
10938
+ detail: "added a dependency-install scan and chained scan/map-upload/mark-build around the build."
10395
10939
  } : {
10396
10940
  changed: true,
10397
10941
  strategy: "lifecycle-hooks",
10398
- detail: "added scans to postinstall/prebuild and mark-build to postbuild."
10942
+ detail: "added scans to postinstall/prebuild, map upload before bundling, and mark-build to postbuild."
10399
10943
  };
10400
10944
  }
10401
10945
 
10402
- // src/cli.ts
10403
- var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
10946
+ // src/setup-sync.ts
10947
+ import { join as join26 } from "path";
10404
10948
 
10405
- Usage:
10406
- patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
10407
- If no UUID is configured, the server
10408
- provisions one and we persist it. After a
10409
- successful post it also adds/updates the
10410
- Patchstack Connector <script> tag in the root
10411
- HTML shell (index.html, public/index.html,
10412
- or src/app.html) \u2014 opt out with
10413
- "widget": false in .patchstackrc.json.
10414
- Run as a postinstall/prebuild/build hook, a
10415
- report Patchstack cannot accept is printed
10416
- and exits 0 so the build goes on; run
10417
- directly, the same failure exits 1
10418
- patchstack-connect setup [options] Finish the bounded project setup: run scan,
10419
- manage the widget, install + verify runtime
10420
- protection, and wire dependency/build scans.
10421
- Never runs the project build
10422
- patchstack-connect map [--dir <p>] [--out <f>] [--upload]
10423
- Map the app's attack surface: entry points, the
10424
- inputs each reads, the sinks it can reach, and
10425
- evidence-backed input\u2192sink flows (each labelled
10426
- with how the link was established). Best-effort static
10427
- analysis \u2014 reports the DETECTED surface, with
10428
- coverage counters. Prints JSON (--out writes a
10429
- file; a pre-bundle --upload also stamps the existing
10430
- guard rules file; --follow-symlinks leaves the project dir).
10431
- Uses the app's own TypeScript
10432
- patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
10433
- with an existing site UUID
10434
- patchstack-connect status [options] Show current configuration and whether the
10435
- site still exists on Patchstack (active /
10436
- removed)
10437
- patchstack-connect uninstall [options] Signal Patchstack that this package is being
10438
- removed from the project. An unclaimed site
10439
- record is deleted; a claimed site is flagged
10440
- for its owner to remove in the dashboard.
10441
- Does NOT touch local files \u2014 see the
10442
- "Uninstalling" steps in AGENT-INSTALL.md
10443
- patchstack-connect mark-build [options] Stamp built HTML with a production flag +
10444
- build fingerprint, and ensure the widget
10445
- tag in built pages (run as a postbuild step).
10446
- The flag marks a page as the LIVE site, so it
10447
- is stamped only on a production build; a local
10448
- or preview build gets the widget tag and no
10449
- marker (and any stale marker is removed).
10450
- Looks in the framework's own output dir, an
10451
- --output the build script names, then dist/,
10452
- build/, out/, .output/public/, _site/ \u2014 inside
10453
- the project only. --dir <path> overrides.
10454
- patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
10455
- guard). Auto-wires supported server stacks;
10456
- for others it scaffolds a
10457
- generic guard + prints a wiring plan.
10458
- --demo seeds a broad sample rule set (for
10459
- demonstrations, not production).
10460
- --check verifies the guard is wired (exit 1
10461
- if not) \u2014 for the wire-then-verify loop.
10462
- It reads the app's source and never runs it.
10463
- --check --runtime additionally STARTS THE APP
10464
- on a loopback port and sends it one request,
10465
- to establish that a request reaches the
10466
- guard. Opt-in, and the only mode that runs
10467
- the app. Exit 0 traversed, 1 a listener
10468
- answered instead of the guard, 2 could not
10469
- be established (e.g. a TypeScript or
10470
- framework entry this must not start).
10471
- patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
10472
- walkthrough: verify the vulnerable package,
10473
- scan it, wait for live rule 18843, install +
10474
- verify the guard, and print test requests.
10475
- Does not install vulnerable dependencies.
10476
- patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
10477
- preparing, running, proving, and cleaning up
10478
- the production-backed local demo.
10479
- patchstack-connect guide [--full] Show this project's setup status (what's done,
10480
- what's missing, with tailored commands), then
10481
- print the full setup guide. --full prints the
10482
- guide even when setup is complete
10949
+ // src/protect/rules/contract.js
10950
+ var SOURCES = Object.freeze({
10951
+ raw: Object.freeze({ keyed: false }),
10952
+ all: Object.freeze({ keyed: false }),
10953
+ false: Object.freeze({ keyed: false }),
10954
+ rules: Object.freeze({ keyed: false, group: true }),
10955
+ get: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10956
+ post: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10957
+ request: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10958
+ cookie: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10959
+ // A bare `files.<name>` resolves to the filename; an optional trailing attribute selects another part
10960
+ // of the upload. Optional, not required — filename-scoped rules use the bare form.
10961
+ files: Object.freeze({
10962
+ keyed: true,
10963
+ keys: "any",
10964
+ wildcard: true,
10965
+ optional_key_suffixes: Object.freeze(["content", "filename", "type"])
10966
+ }),
10967
+ server: Object.freeze({
10968
+ keyed: true,
10969
+ keys: Object.freeze([
10970
+ "REQUEST_URI",
10971
+ "REQUEST_METHOD",
10972
+ "HTTP_USER_AGENT",
10973
+ "HTTP_REFERER",
10974
+ "HTTP_HOST",
10975
+ "REMOTE_ADDR",
10976
+ "ip",
10977
+ "CONTENT_TYPE",
10978
+ "CONTENT_LENGTH"
10979
+ ]),
10980
+ key_prefixes: Object.freeze(["HTTP_"])
10981
+ }),
10982
+ response: Object.freeze({
10983
+ keyed: true,
10984
+ keys: Object.freeze(["status", "body", "headers"]),
10985
+ key_prefixes: Object.freeze(["header."])
10986
+ }),
10987
+ egress: Object.freeze({ keyed: true, keys: Object.freeze(["url", "host", "method"]) })
10988
+ });
10989
+ var MATCH_TYPES = Object.freeze([
10990
+ "array_in_array",
10991
+ "array_key_value",
10992
+ "contains",
10993
+ "cors_reflected",
10994
+ "cross_origin",
10995
+ "ctype_alnum",
10996
+ "ctype_digit",
10997
+ "ctype_special",
10998
+ "equals",
10999
+ "equals_strict",
11000
+ "hostname",
11001
+ "in_array",
11002
+ "inline_js_xss",
11003
+ "inline_xss",
11004
+ "internal_host",
11005
+ "is_numeric",
11006
+ "isset",
11007
+ "jwt_claim_equals",
11008
+ "less_than",
11009
+ "more_than",
11010
+ "not_contains",
11011
+ "not_in_array",
11012
+ "off_origin",
11013
+ "quotes",
11014
+ "regex",
11015
+ "stripos"
11016
+ ]);
11017
+ var PARAMETERLESS_MATCH_TYPES = Object.freeze(["cross_origin", "off_origin", "cors_reflected"]);
11018
+ var OPERANDLESS_MATCH_TYPES = Object.freeze([
11019
+ "isset",
11020
+ "quotes",
11021
+ "is_numeric",
11022
+ "ctype_alnum",
11023
+ "ctype_digit",
11024
+ "ctype_special",
11025
+ "inline_xss",
11026
+ "inline_js_xss",
11027
+ "internal_host",
11028
+ "cross_origin",
11029
+ "off_origin",
11030
+ "cors_reflected"
11031
+ ]);
11032
+ var OPERAND_SHAPES = Object.freeze([
11033
+ "positive-number",
11034
+ "boolean",
11035
+ "non-empty-string",
11036
+ "regex-literal",
11037
+ "string-or-string-list",
11038
+ "scalar",
11039
+ "string",
11040
+ "numeric",
11041
+ "scalar-or-non-empty-list",
11042
+ "non-empty-list",
11043
+ "object",
11044
+ "non-empty-object",
11045
+ "non-empty-string-list",
11046
+ "header-name-map",
11047
+ "header-name-list",
11048
+ "cookie-flags"
11049
+ ]);
11050
+ var COOKIE_FLAGS = Object.freeze(["httpOnly", "secure", "sameSite"]);
11051
+ var SAME_SITE_VALUES = Object.freeze(["Strict", "Lax", "None"]);
11052
+ var HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
11053
+ var REGEX_LITERAL = /^\/(.+?)\/([gimsuy]*)$/s;
11054
+ var MATCH_OPERANDS = Object.freeze({
11055
+ array_key_value: Object.freeze({
11056
+ required: Object.freeze(["key", "match"]),
11057
+ shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
11058
+ }),
11059
+ // Decides on a JWT's DECODED payload rather than its shape, and yields the matching token spans so
11060
+ // `redact` masks only those tokens. `claim` names an own, top-level claim; `value` is compared with
11061
+ // exact string equality. Anything not positively identified — undecodable, not JSON, not an object,
11062
+ // oversized, claim missing, claim not a string, or a different value — is a no-match.
11063
+ jwt_claim_equals: Object.freeze({
11064
+ required: Object.freeze(["claim", "value"]),
11065
+ shapes: Object.freeze({ claim: "non-empty-string", value: "non-empty-string" })
11066
+ }),
11067
+ regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
11068
+ contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11069
+ stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11070
+ not_contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11071
+ // A hostname is compared to `new URL(value).hostname`, so an empty one only ever equals the empty
11072
+ // hostname of an opaque-scheme URL (`file:`, `mailto:`, `data:`) — never a host, and a false positive
11073
+ // on those. The other three are the list-membership family: an empty list makes `in_array` and
11074
+ // `array_in_array` inert and `not_in_array` true for every value.
11075
+ hostname: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11076
+ in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
11077
+ not_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
11078
+ array_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-list" }) }),
11079
+ // The comparison family. `equals` coerces, so any scalar can match. `equals_strict` does not, and its
11080
+ // subject is always a string by the time it is compared — so a number operand is unconditionally
11081
+ // inert, which is the trap the engine's own note about `'1' !== 1` describes.
11082
+ equals: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar" }) }),
11083
+ equals_strict: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "string" }) }),
11084
+ less_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) }),
11085
+ more_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) })
11086
+ });
11087
+ function operandShapeProblem(shape, value) {
11088
+ const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
11089
+ const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
11090
+ const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
11091
+ const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
11092
+ const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
11093
+ const tooLong = stringLimit(value);
11094
+ if (tooLong) return tooLong;
11095
+ const tooMany = listLimit(value);
11096
+ if (tooMany) return tooMany;
11097
+ if (Array.isArray(value)) {
11098
+ const longMember = value.find((member) => stringLimit(member) !== null);
11099
+ if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
11100
+ }
11101
+ switch (shape) {
11102
+ case "positive-number":
11103
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
11104
+ case "boolean":
11105
+ return typeof value === "boolean" ? null : "must be true or false";
11106
+ case "non-empty-string":
11107
+ return isFilledString(value) ? null : "must be a non-empty string";
11108
+ case "regex-literal":
11109
+ if (!isFilledString(value)) return "must be a non-empty string";
11110
+ return REGEX_LITERAL.test(value) ? null : "must be a delimited pattern, as in /pattern/i";
11111
+ case "string-or-string-list":
11112
+ if (isFilledString(value)) return null;
11113
+ if (Array.isArray(value) && value.length > 0 && value.every(isFilledString)) return null;
11114
+ return "must be a non-empty string, or a non-empty list of them";
11115
+ case "scalar":
11116
+ return isScalar(value) ? null : "must be a string, number or boolean";
11117
+ case "string":
11118
+ return typeof value === "string" ? null : "must be a string (this comparison is type-strict)";
11119
+ case "numeric":
11120
+ return isScalar(value) && Number.isFinite(Number(value)) ? null : "must be a number";
11121
+ case "scalar-or-non-empty-list":
11122
+ if (isScalar(value)) return null;
11123
+ if (Array.isArray(value) && value.length > 0 && value.every(isScalar)) return null;
11124
+ return "must be a scalar, or a non-empty list of them";
11125
+ case "non-empty-list":
11126
+ return Array.isArray(value) && value.length > 0 && value.every(isScalar) ? null : "must be a non-empty list of scalars";
11127
+ case "header-name-map": {
11128
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
11129
+ const names = Object.keys(value);
11130
+ if (names.length === 0) return "must be an object with at least one entry";
11131
+ if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
11132
+ const bad = names.find((name) => !isHeaderName(name));
11133
+ if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
11134
+ const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
11135
+ return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
11136
+ }
11137
+ case "header-name-list": {
11138
+ if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
11139
+ const bad = value.find((name) => !isHeaderName(name));
11140
+ return bad === void 0 ? null : `names an invalid HTTP header, ${JSON.stringify(bad)}`;
11141
+ }
11142
+ case "cookie-flags": {
11143
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
11144
+ const unknown = Object.keys(value).filter((key) => !COOKIE_FLAGS.includes(key));
11145
+ if (unknown.length > 0) return `has no flag named ${unknown.map((k) => JSON.stringify(k)).join(", ")}`;
11146
+ for (const flag of ["httpOnly", "secure"]) {
11147
+ if (value[flag] !== void 0 && typeof value[flag] !== "boolean") {
11148
+ return `flag "${flag}" must be true or false`;
11149
+ }
11150
+ }
11151
+ const sameSite = value.sameSite;
11152
+ const sameSiteOff = sameSite === false;
11153
+ if (sameSite !== void 0 && !sameSiteOff) {
11154
+ if (typeof sameSite !== "string" || !SAME_SITE_VALUES.some((v) => v.toLowerCase() === sameSite.toLowerCase())) {
11155
+ return `flag "sameSite" must be false, or one of ${SAME_SITE_VALUES.join(", ")}`;
11156
+ }
11157
+ }
11158
+ if (value.httpOnly === false && value.secure === false && sameSiteOff) {
11159
+ return "turns off every flag, so it would harden nothing";
11160
+ }
11161
+ return null;
11162
+ }
11163
+ case "object":
11164
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? null : "must be an object";
11165
+ case "non-empty-object":
11166
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? null : "must be an object with at least one entry";
11167
+ case "non-empty-string-list":
11168
+ return Array.isArray(value) && value.length > 0 && value.every(isFilledString) ? null : "must be a non-empty list of non-empty strings";
11169
+ default:
11170
+ return null;
11171
+ }
11172
+ }
11173
+ var MUTATIONS = Object.freeze([
11174
+ "base64_decode",
11175
+ "getArrayValues",
11176
+ "htmlentitydecode",
11177
+ "intval",
11178
+ "json_decode",
11179
+ "json_encode",
11180
+ "urldecode"
11181
+ ]);
11182
+ var PHASES = Object.freeze(["request", "response", "egress"]);
11183
+ var ACTIONS = Object.freeze([
11184
+ "block",
11185
+ "redact",
11186
+ "encode",
11187
+ "set-header",
11188
+ "remove-header",
11189
+ "harden-cookie"
11190
+ ]);
11191
+ var RULE_PROPERTY_SHAPES = Object.freeze({
11192
+ max_bytes: "positive-number",
11193
+ bypass_limit: "boolean",
11194
+ ensure: "boolean",
11195
+ prefilter: "non-empty-string-list"
11196
+ });
11197
+ var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
11198
+ var WHEN_KEYS = Object.freeze(["path", "method"]);
11199
+ var BUILD_SCOPE_PROPERTY = "build_scope";
11200
+ var BUILD_SCOPE = Object.freeze({
11201
+ applies_to: Object.freeze(["firewall"]),
11202
+ usable: Object.freeze({
11203
+ type: "string",
11204
+ trim: true,
11205
+ pattern: "^[0-9a-fA-F]{64}$",
11206
+ canonical: "lowercase"
11207
+ }),
11208
+ unreadable: "the rule remains accepted and detects only by default",
11209
+ local_override: "trustLocalRuleScope may explicitly enforce a caller-supplied rule"
11210
+ });
11211
+ var RULE_PROPERTIES = Object.freeze([
11212
+ "id",
11213
+ "rule_id",
11214
+ "title",
11215
+ "category",
11216
+ "phase",
11217
+ "action",
11218
+ "rule_v2",
11219
+ "when",
11220
+ "message",
11221
+ "enforcement",
11222
+ "source_revision",
11223
+ "prefilter",
11224
+ "max_bytes",
11225
+ "bypass_limit",
11226
+ "set_headers",
11227
+ "remove_headers",
11228
+ "cookie_flags",
11229
+ "ensure",
11230
+ "capture",
11231
+ BUILD_SCOPE_PROPERTY
11232
+ ]);
11233
+ var CAPTURE_KEYS = Object.freeze(["version", "raw_chars"]);
11234
+ var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture", BUILD_SCOPE_PROPERTY]);
11235
+ var ACTION_PROPERTIES = Object.freeze({
11236
+ block: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["request", "response", "egress"]) }),
11237
+ redact: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
11238
+ encode: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
11239
+ "set-header": Object.freeze({
11240
+ required: Object.freeze(["set_headers"]),
11241
+ defaulted: Object.freeze(["ensure"]),
11242
+ phases: Object.freeze(["response"]),
11243
+ shapes: Object.freeze({ set_headers: "header-name-map" })
11244
+ }),
11245
+ "remove-header": Object.freeze({
11246
+ required: Object.freeze(["remove_headers"]),
11247
+ defaulted: Object.freeze([]),
11248
+ phases: Object.freeze(["response"]),
11249
+ shapes: Object.freeze({ remove_headers: "header-name-list" })
11250
+ }),
11251
+ "harden-cookie": Object.freeze({
11252
+ required: Object.freeze([]),
11253
+ defaulted: Object.freeze(["cookie_flags"]),
11254
+ phases: Object.freeze(["response"]),
11255
+ shapes: Object.freeze({ cookie_flags: "cookie-flags" })
11256
+ })
11257
+ });
11258
+ var LIMITS = Object.freeze({
11259
+ maxRules: 5e3,
11260
+ maxWhitelists: 2e3,
11261
+ maxConditionsPerRule: 250,
11262
+ maxConditionNodesPerRule: 1e3,
11263
+ maxConditionNodesPerBundle: 25e3,
11264
+ maxNestingDepth: 12,
11265
+ maxRegexLength: 1e3,
11266
+ maxValueLength: 8192,
11267
+ maxParameterLength: 512,
11268
+ maxParameterItems: 64,
11269
+ maxOperandItems: 256,
11270
+ maxMapEntries: 128,
11271
+ maxPathSegments: 64
11272
+ });
11273
+ var GROUP_PARAMETER = "rules";
11274
+ function isGroup(condition) {
11275
+ return condition?.parameter === GROUP_PARAMETER && Array.isArray(condition?.rules);
11276
+ }
11277
+ function conditionShapeProblem(condition) {
11278
+ if (!condition || typeof condition !== "object") return "condition is not an object";
11279
+ const named = condition.parameter === GROUP_PARAMETER;
11280
+ const nested = Array.isArray(condition.rules);
11281
+ if (named && !nested) {
11282
+ return `parameter "${GROUP_PARAMETER}" is a group and needs a "rules" array`;
11283
+ }
11284
+ if (nested && !named) {
11285
+ return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
11286
+ }
11287
+ if (condition.inclusive !== void 0 && typeof condition.inclusive !== "boolean") {
11288
+ return "inclusive must be true or false";
11289
+ }
11290
+ if (named && nested) {
11291
+ if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
11292
+ if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
11293
+ if (condition.rules.length === 0) return "group has no conditions (would never match)";
11294
+ }
11295
+ return null;
11296
+ }
11297
+ function parameterProblem(parameter) {
11298
+ if (parameter === void 0 || parameter === null) return null;
11299
+ if (Array.isArray(parameter)) {
11300
+ if (parameter.length === 0) return "parameter list is empty";
11301
+ if (parameter.length > LIMITS.maxParameterItems) {
11302
+ return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
11303
+ }
11304
+ for (const member of parameter) {
11305
+ if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
11306
+ if (member === void 0 || member === null) return "parameter list members must be non-empty strings";
11307
+ const problem = parameterProblem(member);
11308
+ if (problem !== null) return problem;
11309
+ }
11310
+ return null;
11311
+ }
11312
+ if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
11313
+ if (parameter.length > LIMITS.maxParameterLength) {
11314
+ return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
11315
+ }
11316
+ if (parameter === GROUP_PARAMETER) {
11317
+ return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
11318
+ }
11319
+ const dot = parameter.indexOf(".");
11320
+ const name = dot === -1 ? parameter : parameter.slice(0, dot);
11321
+ const source = SOURCES[name];
11322
+ if (source === void 0) return `unknown parameter source "${name}"`;
11323
+ if (dot === -1) {
11324
+ return source.keyed === true ? `source "${name}" needs a key, e.g. "${name}.fieldname"` : null;
11325
+ }
11326
+ if (source.keyed !== true) return `source "${name}" takes no key`;
11327
+ const key = parameter.slice(dot + 1);
11328
+ if (key === "") return `source "${name}" needs a key after the dot`;
11329
+ return keyProblem(name, source, key);
11330
+ }
11331
+ function keyProblem(name, source, key) {
11332
+ if (key.endsWith("*") && source.wildcard !== true) {
11333
+ return `"${name}" does not fan out over a wildcard key`;
11334
+ }
11335
+ const bare = key.endsWith("*") ? key.slice(0, -1) : key;
11336
+ if (source.keys === "any") return null;
11337
+ if (Array.isArray(source.keys) && source.keys.includes(bare)) return null;
11338
+ for (const prefix of source.key_prefixes ?? []) {
11339
+ if (bare.startsWith(prefix) && bare.length > prefix.length) return null;
11340
+ }
11341
+ const accepted = [
11342
+ ...Array.isArray(source.keys) ? source.keys : [],
11343
+ ...(source.key_prefixes ?? []).map((p) => `${p}\u2026`)
11344
+ ].join(", ");
11345
+ return `"${name}" has no key "${key}" (expected one of: ${accepted})`;
11346
+ }
11347
+ function matchProblem(match, parameter) {
11348
+ if (!match || typeof match !== "object") return "condition has no match object";
11349
+ if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
11350
+ if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
11351
+ for (const operand of ["value", "key", "claim"]) {
11352
+ const value = match[operand];
11353
+ if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
11354
+ return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
11355
+ }
11356
+ if (Array.isArray(value)) {
11357
+ if (value.length > LIMITS.maxOperandItems) {
11358
+ return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
11359
+ }
11360
+ if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
11361
+ return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
11362
+ }
11363
+ }
11364
+ }
11365
+ const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
11366
+ if (!parameterless && (parameter === void 0 || parameter === null)) {
11367
+ return `match type "${match.type}" needs a parameter`;
11368
+ }
11369
+ if (parameterless && parameter !== void 0 && parameter !== null) {
11370
+ return `match type "${match.type}" reads the whole request and takes no parameter`;
11371
+ }
11372
+ const operands = MATCH_OPERANDS[match.type];
11373
+ if (operands) {
11374
+ for (const required of operands.required) {
11375
+ if (match[required] === void 0 || match[required] === null) {
11376
+ return `match type "${match.type}" needs "${required}"`;
11377
+ }
11378
+ }
11379
+ for (const [operand, shape] of Object.entries(operands.shapes ?? {})) {
11380
+ if (match[operand] === void 0) continue;
11381
+ const problem = operandShapeProblem(shape, match[operand]);
11382
+ if (problem) return `match type "${match.type}" operand "${operand}" ${problem}`;
11383
+ }
11384
+ } else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
11385
+ return `match type "${match.type}" needs a value`;
11386
+ }
11387
+ if (match.type === "array_key_value") {
11388
+ const paths2 = Array.isArray(match.key) ? match.key : [match.key];
11389
+ if (paths2.some((path17) => typeof path17 === "string" && path17.split(".").length > LIMITS.maxPathSegments)) {
11390
+ return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
11391
+ }
11392
+ }
11393
+ return null;
11394
+ }
11395
+ function mutationsProblem(mutations) {
11396
+ if (mutations === void 0 || mutations === null) return null;
11397
+ if (!Array.isArray(mutations)) return "mutations must be an array";
11398
+ if (mutations.length > LIMITS.maxOperandItems) {
11399
+ return `mutations has more than ${LIMITS.maxOperandItems} entries`;
11400
+ }
11401
+ for (const mutation of mutations) {
11402
+ if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
11403
+ return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
11404
+ }
11405
+ }
11406
+ return null;
11407
+ }
11408
+ function whenProblem(when) {
11409
+ if (when === void 0 || when === null) return null;
11410
+ if (typeof when !== "object" || Array.isArray(when)) return "when must be an object";
11411
+ const keys = Object.keys(when);
11412
+ if (keys.length === 0) return "when is empty \u2014 remove it, or name a path or method";
11413
+ const unknown = keys.filter((key) => !WHEN_KEYS.includes(key));
11414
+ if (unknown.length > 0) {
11415
+ return `when names no supported key (${unknown.join(", ")}) \u2014 the engine understands ${WHEN_KEYS.join(" and ")}, and an unrecognised scope is ignored, so the rule would apply to every request`;
11416
+ }
11417
+ if ("path" in when && !isNonEmptyString(when.path)) {
11418
+ return "when.path must be a non-empty string";
11419
+ }
11420
+ if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
11421
+ return `when.path is longer than ${LIMITS.maxValueLength} chars`;
11422
+ }
11423
+ if ("method" in when) {
11424
+ const methods = Array.isArray(when.method) ? when.method : [when.method];
11425
+ if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
11426
+ if (methods.length > LIMITS.maxParameterItems) {
11427
+ return `when.method has more than ${LIMITS.maxParameterItems} entries`;
11428
+ }
11429
+ if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
11430
+ if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
11431
+ return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
11432
+ }
11433
+ }
11434
+ return null;
11435
+ }
11436
+ function isNonEmptyString(value) {
11437
+ return typeof value === "string" && value.trim() !== "";
11438
+ }
11439
+ function nullPropertyProblem(rule) {
11440
+ if (!rule || typeof rule !== "object") return null;
11441
+ for (const property of RULE_PROPERTIES) {
11442
+ if (NULL_EXEMPT_PROPERTIES.includes(property)) continue;
11443
+ if (property in rule && rule[property] === null) {
11444
+ return `"${property}" is present but null; omit it to mean the default, since a null is a value that was meant to be something and is not`;
11445
+ }
11446
+ }
11447
+ return null;
11448
+ }
11449
+ function rulePropertyProblem(rule) {
11450
+ if (!rule || typeof rule !== "object") return null;
11451
+ for (const [property, shape] of Object.entries(RULE_PROPERTY_SHAPES)) {
11452
+ if (rule[property] === void 0 || rule[property] === null) continue;
11453
+ const problem = operandShapeProblem(shape, rule[property]);
11454
+ if (problem) return `"${property}" ${problem}`;
11455
+ }
11456
+ for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
11457
+ if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
11458
+ return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
11459
+ }
11460
+ }
11461
+ return null;
11462
+ }
11463
+ function actionProblem(action, rule) {
11464
+ if (action === void 0 || action === null) return null;
11465
+ if (!ACTIONS.includes(action)) return `unknown action "${action}"`;
11466
+ const spec = ACTION_PROPERTIES[action];
11467
+ for (const required of spec?.required ?? []) {
11468
+ if (rule?.[required] === void 0 || rule?.[required] === null) {
11469
+ return `action "${action}" needs "${required}"`;
11470
+ }
11471
+ }
11472
+ for (const [property, shape] of Object.entries(spec?.shapes ?? {})) {
11473
+ if (rule?.[property] === void 0 || rule?.[property] === null) continue;
11474
+ const problem = operandShapeProblem(shape, rule[property]);
11475
+ if (problem) return `action "${action}" property "${property}" ${problem}`;
11476
+ }
11477
+ const phase = rule?.phase ?? "request";
11478
+ if (spec !== void 0 && !spec.phases.includes(phase)) {
11479
+ return `action "${action}" is carried out on the ${spec.phases.join("/")} phase; on "${phase}" the rule blocks instead`;
11480
+ }
11481
+ return null;
11482
+ }
11483
+
11484
+ // src/protect/engine/response-json.js
11485
+ var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
11486
+ async function readBoundedJson2(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
11487
+ const stated2 = Number(response.headers?.get?.("content-length"));
11488
+ if (Number.isFinite(stated2) && stated2 > maxBytes) {
11489
+ throw new Error(`rule response exceeds ${maxBytes} bytes`);
11490
+ }
11491
+ const stream = response.body;
11492
+ if (!stream || typeof stream.getReader !== "function") {
11493
+ if (typeof response.text === "function") {
11494
+ const text = await response.text();
11495
+ if (new TextEncoder().encode(text).byteLength > maxBytes) {
11496
+ throw new Error(`rule response exceeds ${maxBytes} bytes`);
11497
+ }
11498
+ return JSON.parse(text);
11499
+ }
11500
+ throw new Error("rule response body cannot be read with a size bound");
11501
+ }
11502
+ const reader = stream.getReader();
11503
+ const chunks = [];
11504
+ let total = 0;
11505
+ try {
11506
+ for (; ; ) {
11507
+ const { done, value } = await reader.read();
11508
+ if (done) break;
11509
+ total += value.byteLength;
11510
+ if (total > maxBytes) {
11511
+ void reader.cancel().catch(() => {
11512
+ });
11513
+ throw new Error(`rule response exceeds ${maxBytes} bytes`);
11514
+ }
11515
+ chunks.push(value);
11516
+ }
11517
+ } finally {
11518
+ reader.releaseLock();
11519
+ }
11520
+ const bytes = new Uint8Array(total);
11521
+ let offset = 0;
11522
+ for (const chunk of chunks) {
11523
+ bytes.set(chunk, offset);
11524
+ offset += chunk.byteLength;
11525
+ }
11526
+ return JSON.parse(new TextDecoder().decode(bytes));
11527
+ }
11528
+
11529
+ // src/protect/engine/client.js
11530
+ var DEFAULT_BASE_URL = "https://api.patchstack.com";
11531
+ var DEFAULT_CACHE_TTL = 3e5;
11532
+ var JITTER_FRACTION = 0.1;
11533
+ var PatchstackRuleClient = class {
11534
+ #token;
11535
+ #baseUrl;
11536
+ #cacheTtl;
11537
+ #cache = null;
11538
+ #cacheTime = null;
11539
+ #ttlEffective = 0;
11540
+ #etag;
11541
+ #timeoutMs;
11542
+ constructor({ token, baseUrl, cacheTtl, etag, timeoutMs } = {}) {
11543
+ this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
11544
+ this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
11545
+ this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_WAF_API_URL, DEFAULT_BASE_URL, "rule endpoint");
11546
+ this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL;
11547
+ this.#etag = etag ?? null;
11548
+ if (!this.#token) {
11549
+ throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
11550
+ }
11551
+ }
11552
+ async getRules() {
11553
+ const now = Date.now();
11554
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
11555
+ return this.#cache;
11556
+ }
11557
+ const url = `${this.#baseUrl}/api/get-rules/3`;
11558
+ try {
11559
+ const headers = {
11560
+ "Content-Type": "application/json",
11561
+ "Authorization": `Bearer ${this.#token}`,
11562
+ "LicenseID": "1"
11563
+ // Hard-coded, is never actually used but needed by the API
11564
+ };
11565
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
11566
+ const response = await fetch(url, {
11567
+ method: "POST",
11568
+ headers,
11569
+ body: JSON.stringify({}),
11570
+ signal: AbortSignal.timeout(this.#timeoutMs)
11571
+ });
11572
+ if (response.status === 304) {
11573
+ this.#touch(now);
11574
+ return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
11575
+ }
11576
+ if (!response.ok) {
11577
+ return {
11578
+ success: false,
11579
+ error: `API returned ${response.status}: ${response.statusText}`,
11580
+ firewall: [],
11581
+ whitelists: [],
11582
+ whitelist_keys: {}
11583
+ };
11584
+ }
11585
+ const data = await readBoundedJson2(response);
11586
+ if (!data || !Array.isArray(data.firewall)) {
11587
+ return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
11588
+ }
11589
+ const result = {
11590
+ success: true,
11591
+ notModified: false,
11592
+ etag: response.headers?.get?.("etag") ?? null,
11593
+ firewall: data.firewall,
11594
+ whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
11595
+ whitelist_keys: data.whitelist_keys ?? {}
11596
+ };
11597
+ this.#cache = result;
11598
+ this.#etag = result.etag;
11599
+ this.#touch(now);
11600
+ return result;
11601
+ } catch (err) {
11602
+ return {
11603
+ success: false,
11604
+ error: err.name === "TimeoutError" ? "Request timed out" : err.message,
11605
+ firewall: [],
11606
+ whitelists: [],
11607
+ whitelist_keys: {}
11608
+ };
11609
+ }
11610
+ }
11611
+ #touch(now) {
11612
+ this.#cacheTime = now;
11613
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION);
11614
+ }
11615
+ clearCache() {
11616
+ this.#cache = null;
11617
+ this.#cacheTime = null;
11618
+ this.#etag = null;
11619
+ }
11620
+ };
11621
+
11622
+ // src/protect/engine/normalizer.js
11623
+ var utf8 = new TextDecoder();
11624
+ var MAX_RAW_CHARS = 1024 * 1024;
11625
+
11626
+ // src/protect/notify.js
11627
+ var reported = /* @__PURE__ */ new WeakMap();
11628
+ var warnings = 0;
11629
+ var MAX_WARNINGS = 20;
11630
+ function notify(fn, arg, label) {
11631
+ if (typeof fn !== "function") return false;
11632
+ try {
11633
+ const result = fn(arg);
11634
+ if (result !== null && typeof result === "object" && typeof result.then === "function") {
11635
+ try {
11636
+ result.then(void 0, (err) => warnOnce2(fn, label, err));
11637
+ } catch {
11638
+ }
11639
+ }
11640
+ return true;
11641
+ } catch (err) {
11642
+ warnOnce2(fn, label, err);
11643
+ return false;
11644
+ }
11645
+ }
11646
+ function warnOnce2(fn, label, err) {
11647
+ let labels = reported.get(fn);
11648
+ if (labels?.has(label)) return;
11649
+ if (!labels) {
11650
+ labels = /* @__PURE__ */ new Set();
11651
+ reported.set(fn, labels);
11652
+ }
11653
+ labels.add(label);
11654
+ if (warnings > MAX_WARNINGS) return;
11655
+ warnings++;
11656
+ try {
11657
+ if (warnings > MAX_WARNINGS) {
11658
+ console.warn("Patchstack: further failing callbacks passed to createProtection are not reported in this process.");
11659
+ return;
11660
+ }
11661
+ console.warn(
11662
+ `Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this callback's failures are reported once. Cause: ${err && err.message ? err.message : String(err)}`
11663
+ );
11664
+ } catch {
11665
+ }
11666
+ }
11667
+
11668
+ // src/protect/engine/engine.js
11669
+ var GRP = "(?:[^()]|\\([^()]*\\))*";
11670
+ var REDOS_PATTERNS = [
11671
+ /\([^)]*[+*}][^)]*\)\s*[+*]/,
11672
+ /\([^)]*\|[^)]*\)\s*[+*]/,
11673
+ new RegExp("\\(" + GRP + "[+*}]" + GRP + "\\)\\s*[+*]"),
11674
+ // nested quantified subgroup
11675
+ new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
11676
+ // nested alternation under a quantifier
11677
+ ];
11678
+ function hasAdjacentUnbounded(body, flags) {
11679
+ let previousUnboundedAtom = null;
11680
+ for (let i = 0; i < body.length; ) {
11681
+ const start = i;
11682
+ const ch = body[i];
11683
+ let assertion = false;
11684
+ let literal = null;
11685
+ let atom = ch;
11686
+ if (ch === "\\") {
11687
+ const escaped = body[i + 1] ?? "";
11688
+ assertion = /[bBAZz]/.test(escaped);
11689
+ if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
11690
+ i += Math.min(2, body.length - i);
11691
+ atom = body.slice(start, i);
11692
+ } else if (ch === "[") {
11693
+ i++;
11694
+ while (i < body.length) {
11695
+ if (body[i] === "\\") i += 2;
11696
+ else if (body[i++] === "]") break;
11697
+ }
11698
+ atom = body.slice(start, i);
11699
+ } else if (ch === ")") {
11700
+ i++;
11701
+ atom = ".";
11702
+ } else if (ch === "(") {
11703
+ const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
11704
+ if (lookaround) {
11705
+ i = groupEnd(body, i);
11706
+ continue;
11707
+ }
11708
+ i++;
11709
+ if (body.startsWith("?:", i)) i += 2;
11710
+ else if (body.startsWith("?<", i)) {
11711
+ const nameEnd = body.indexOf(">", i + 2);
11712
+ if (nameEnd !== -1) i = nameEnd + 1;
11713
+ }
11714
+ continue;
11715
+ } else if (ch === "|") {
11716
+ previousUnboundedAtom = null;
11717
+ i++;
11718
+ continue;
11719
+ } else if ("^$".includes(ch)) {
11720
+ i++;
11721
+ continue;
11722
+ } else if ("?*+{}".includes(ch)) {
11723
+ previousUnboundedAtom = null;
11724
+ i++;
11725
+ continue;
11726
+ } else {
11727
+ if (ch !== ".") literal = ch;
11728
+ i++;
11729
+ }
11730
+ if (assertion) continue;
11731
+ let unbounded = false;
11732
+ let mayBeEmpty = false;
11733
+ if (body[i] === "*" || body[i] === "+") {
11734
+ unbounded = true;
11735
+ mayBeEmpty = body[i] === "*";
11736
+ i++;
11737
+ } else if (body[i] === "{") {
11738
+ const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
11739
+ if (quantifier) {
11740
+ unbounded = quantifier[2] === "";
11741
+ mayBeEmpty = Number(quantifier[1]) === 0;
11742
+ i += quantifier[0].length;
11743
+ }
11744
+ } else if (body[i] === "?") {
11745
+ mayBeEmpty = true;
11746
+ i++;
11747
+ }
11748
+ if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
11749
+ if (unbounded && previousUnboundedAtom !== null) return true;
11750
+ if (unbounded) {
11751
+ previousUnboundedAtom = atom;
11752
+ } else if (previousUnboundedAtom !== null && !mayBeEmpty) {
11753
+ try {
11754
+ const atomFlags = flags.replace(/[^iu]/g, "");
11755
+ const complementaryClass = previousUnboundedAtom === "\\s" && atom === "\\S" || previousUnboundedAtom === "\\S" && atom === "\\s" || previousUnboundedAtom === "\\d" && atom === "\\D" || previousUnboundedAtom === "\\D" && atom === "\\d" || previousUnboundedAtom === "\\w" && atom === "\\W" || previousUnboundedAtom === "\\W" && atom === "\\w";
11756
+ const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
11757
+ if (complementaryClass || excludedLiteral) {
11758
+ previousUnboundedAtom = null;
11759
+ }
11760
+ } catch {
11761
+ }
11762
+ }
11763
+ if (i === start) i++;
11764
+ }
11765
+ return false;
11766
+ }
11767
+ function parseRepetition(body) {
11768
+ let i = 0;
11769
+ const parseAlternation = () => {
11770
+ const branches = [parseSequence()];
11771
+ while (body[i] === "|") {
11772
+ i++;
11773
+ branches.push(parseSequence());
11774
+ }
11775
+ return branches.length === 1 ? branches[0] : { type: "alt", branches };
11776
+ };
11777
+ const parseSequence = () => {
11778
+ const items = [];
11779
+ while (i < body.length && body[i] !== "|" && body[i] !== ")") {
11780
+ const atom = parseAtom();
11781
+ if (atom === null) return null;
11782
+ const quantified = parseQuantifier(atom);
11783
+ if (quantified === null) return null;
11784
+ items.push(quantified);
11785
+ }
11786
+ return { type: "seq", items };
11787
+ };
11788
+ const parseAtom = () => {
11789
+ const ch = body[i];
11790
+ if (ch === "(") {
11791
+ const start2 = i;
11792
+ i++;
11793
+ let kind = "capture";
11794
+ if (body.startsWith("?:", i)) {
11795
+ kind = "group";
11796
+ i += 2;
11797
+ } else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
11798
+ kind = "look";
11799
+ i += 2;
11800
+ } else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
11801
+ kind = "look";
11802
+ i += 3;
11803
+ } else if (body.startsWith("?<", i)) {
11804
+ const close = body.indexOf(">", i + 2);
11805
+ if (close === -1) return null;
11806
+ i = close + 1;
11807
+ } else if (body[i] === "?") {
11808
+ return null;
11809
+ }
11810
+ const inner = parseAlternation();
11811
+ if (inner === null || body[i] !== ")") return null;
11812
+ i++;
11813
+ return { type: "group", kind, body: inner, source: body.slice(start2, i) };
11814
+ }
11815
+ const start = i;
11816
+ if (ch === "[") {
11817
+ i++;
11818
+ if (body[i] === "^") i++;
11819
+ if (body[i] === "]") i++;
11820
+ while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
11821
+ if (body[i] !== "]") return null;
11822
+ i++;
11823
+ return { type: "atom", source: body.slice(start, i) };
11824
+ }
11825
+ if (ch === "\\") {
11826
+ const next = body[i + 1];
11827
+ if (next === void 0) return null;
11828
+ if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
11829
+ const close = body.indexOf("}", i + 3);
11830
+ if (close === -1) return null;
11831
+ i = close + 1;
11832
+ } else if (next === "k" && body[i + 2] === "<") {
11833
+ const close = body.indexOf(">", i + 3);
11834
+ if (close === -1) return null;
11835
+ i = close + 1;
11836
+ } else {
11837
+ i += 2;
11838
+ }
11839
+ return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
11840
+ }
11841
+ if (ch === "^" || ch === "$") {
11842
+ i++;
11843
+ return { type: "assert" };
11844
+ }
11845
+ if (ch === "*" || ch === "+" || ch === "?") return null;
11846
+ i++;
11847
+ return { type: "atom", source: body.slice(start, i) };
11848
+ };
11849
+ const parseQuantifier = (atom) => {
11850
+ let min;
11851
+ let max;
11852
+ const ch = body[i];
11853
+ if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
11854
+ else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
11855
+ else if (ch === "?") [min, max, i] = [0, 1, i + 1];
11856
+ else if (ch === "{") {
11857
+ const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
11858
+ if (!bounds) return atom;
11859
+ min = Number(bounds[1]);
11860
+ max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
11861
+ i += bounds[0].length;
11862
+ } else {
11863
+ return atom;
11864
+ }
11865
+ if (body[i] === "?") i++;
11866
+ if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
11867
+ return { type: "quant", min, max, body: atom };
11868
+ };
11869
+ const root = parseAlternation();
11870
+ return root !== null && i === body.length ? root : null;
11871
+ }
11872
+ function* nodesOf(node) {
11873
+ if (!node) return;
11874
+ yield node;
11875
+ if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
11876
+ else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
11877
+ else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
11878
+ }
11879
+ var BOUNDED_NESTING_LIMIT = 100;
11880
+ var BOUNDED_ALTERNATION_LIMIT = 16;
11881
+ var OPTIONAL_REPEAT_LIMIT = 8;
11882
+ function hasFencedRepetitions(quant, flags) {
11883
+ let body = quant.body;
11884
+ if (body.type === "group" && body.kind !== "look") body = body.body;
11885
+ if (body.type !== "seq" || body.items.length < 2) return false;
11886
+ const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
11887
+ if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
11888
+ const atomFlags = flags.replace(/[^i]/g, "");
11889
+ const unicode = /[uv]/.test(flags);
11890
+ let insides;
11891
+ try {
11892
+ insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
11893
+ } catch {
11894
+ return false;
11895
+ }
11896
+ const literalOf = (source) => {
11897
+ if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
11898
+ if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
11899
+ return null;
11900
+ };
11901
+ const disjoint = (edge) => {
11902
+ if (edge?.type !== "atom" || edge.source === void 0) return false;
11903
+ const literal = literalOf(edge.source);
11904
+ if (literal !== null) return insides.every((re) => !re.test(literal));
11905
+ if (unicode) return false;
11906
+ let fence;
11907
+ try {
11908
+ fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
11909
+ } catch {
11910
+ return false;
11911
+ }
11912
+ for (let unit = 0; unit <= 65535; unit++) {
11913
+ const ch = String.fromCharCode(unit);
11914
+ if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
11915
+ }
11916
+ return true;
11917
+ };
11918
+ return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
11919
+ }
11920
+ function hasCatastrophicShape(body, flags) {
11921
+ const root = parseRepetition(body);
11922
+ if (root === null) return null;
11923
+ for (const node of nodesOf(root)) {
11924
+ if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
11925
+ if (node.type !== "quant" || node.max <= 1) continue;
11926
+ const fenced = hasFencedRepetitions(node, flags);
11927
+ for (const inner of nodesOf(node.body)) {
11928
+ if (inner === node.body && inner.type === "quant") continue;
11929
+ if (inner.type === "quant" && inner.max > 1 && !fenced) {
11930
+ if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
11931
+ }
11932
+ if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
11933
+ return true;
11934
+ }
11935
+ if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
11936
+ return true;
11937
+ }
11938
+ }
11939
+ }
11940
+ return false;
11941
+ }
11942
+ function lookaroundBody(source) {
11943
+ return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
11944
+ }
11945
+ function groupEnd(body, start) {
11946
+ let depth = 0;
11947
+ let inClass = false;
11948
+ for (let i = start; i < body.length; i++) {
11949
+ const ch = body[i];
11950
+ if (ch === "\\") {
11951
+ i++;
11952
+ continue;
11953
+ }
11954
+ if (ch === "[") inClass = true;
11955
+ else if (ch === "]") inClass = false;
11956
+ else if (!inClass && ch === "(") depth++;
11957
+ else if (!inClass && ch === ")" && --depth === 0) return i + 1;
11958
+ }
11959
+ return body.length;
11960
+ }
11961
+ var MAX_PATTERN_LENGTH = 1e3;
11962
+ var regexVerdicts = /* @__PURE__ */ new Map();
11963
+ var REGEX_VERDICT_LIMIT = 2048;
11964
+ function safeRegExp(pattern) {
11965
+ if (!pattern) {
11966
+ return null;
11967
+ }
11968
+ if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
11969
+ return null;
11970
+ }
11971
+ const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
11972
+ if (!match) {
11973
+ return null;
11974
+ }
11975
+ let safe = regexVerdicts.get(pattern);
11976
+ if (safe === void 0) {
11977
+ safe = isSafeBody(match[1], match[2]);
11978
+ if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
11979
+ regexVerdicts.set(pattern, safe);
11980
+ }
11981
+ if (!safe) return null;
11982
+ try {
11983
+ return new RegExp(match[1], match[2]);
11984
+ } catch {
11985
+ return null;
11986
+ }
11987
+ }
11988
+ function isSafeBody(body, flags) {
11989
+ for (const dangerous of REDOS_PATTERNS) {
11990
+ if (dangerous.test(body)) return false;
11991
+ }
11992
+ if (hasAdjacentUnbounded(body, flags)) return false;
11993
+ if (hasCatastrophicShape(body, flags) === true) return false;
11994
+ try {
11995
+ new RegExp(body, flags);
11996
+ } catch {
11997
+ return false;
11998
+ }
11999
+ return true;
12000
+ }
12001
+
12002
+ // src/protect/engine/pulse-client.js
12003
+ var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
12004
+ var DEFAULT_CACHE_TTL2 = 3e5;
12005
+ var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
12006
+ var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
12007
+ var PER_RULE_DRY_RUN_HEADER = "X-Patchstack-Per-Rule-Dry-Run";
12008
+ var JITTER_FRACTION2 = 0.1;
12009
+ var PulseRuleClient = class {
12010
+ #siteUuid;
12011
+ #timeoutMs;
12012
+ #baseUrl;
12013
+ #cacheTtl;
12014
+ #cache = null;
12015
+ #cacheTime = null;
12016
+ #ttlEffective = 0;
12017
+ #etag;
12018
+ #pulseAuth;
12019
+ #detectionState;
12020
+ #buildId;
12021
+ constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, detectionState, buildId } = {}) {
12022
+ this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
12023
+ this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
12024
+ this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
12025
+ this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
12026
+ this.#etag = etag ?? null;
12027
+ this.#pulseAuth = pulseAuth ?? null;
12028
+ this.#detectionState = typeof detectionState === "string" ? detectionState : null;
12029
+ this.#buildId = canonicalBuildId(buildId);
12030
+ if (!this.#siteUuid) {
12031
+ throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
12032
+ }
12033
+ }
12034
+ async getRules() {
12035
+ const now = Date.now();
12036
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
12037
+ return this.#cache;
12038
+ }
12039
+ const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
12040
+ try {
12041
+ const auth = await pulseAuthHeader(
12042
+ { pulseAuth: this.#pulseAuth, endpoint: this.#baseUrl, timeoutMs: this.#timeoutMs },
12043
+ fetch
12044
+ );
12045
+ const headers = { Accept: "application/json", ...auth };
12046
+ if (this.#detectionState !== null && typeof auth.Authorization === "string") {
12047
+ headers["X-Patchstack-Detections"] = this.#detectionState;
12048
+ }
12049
+ if (this.#buildId !== null && typeof auth.Authorization === "string") {
12050
+ headers["X-Patchstack-Build"] = this.#buildId;
12051
+ }
12052
+ if (typeof auth.Authorization === "string") {
12053
+ headers[PER_RULE_DRY_RUN_HEADER] = "1";
12054
+ }
12055
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
12056
+ const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
12057
+ if (response.status === 304) {
12058
+ this.#touch(now);
12059
+ const result2 = {
12060
+ ...this.#cache ?? { firewall: [], whitelists: [], whitelist_keys: {} },
12061
+ success: true,
12062
+ notModified: true,
12063
+ etag: response.headers?.get?.("etag") ?? this.#etag,
12064
+ build: buildVerdictOf(response.headers)
12065
+ };
12066
+ this.#cache = result2;
12067
+ this.#etag = result2.etag;
12068
+ return result2;
12069
+ }
12070
+ if (!response.ok) {
12071
+ return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
12072
+ }
12073
+ const data = await readBoundedJson2(response);
12074
+ if (!data || !Array.isArray(data.firewall)) {
12075
+ return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
12076
+ }
12077
+ const result = {
12078
+ success: true,
12079
+ notModified: false,
12080
+ etag: response.headers?.get?.("etag") ?? null,
12081
+ firewall: data.firewall,
12082
+ whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
12083
+ whitelist_keys: data.whitelist_keys ?? {},
12084
+ ...enforcementField(data),
12085
+ // Carried through so the caller can decide what a build-scoped rule may do. Response metadata,
12086
+ // rather than bundle content, because it varies with the build asking and is also present on 304.
12087
+ build: buildVerdictOf(response.headers)
12088
+ };
12089
+ this.#cache = result;
12090
+ this.#etag = result.etag;
12091
+ this.#touch(now);
12092
+ return result;
12093
+ } catch (err) {
12094
+ return {
12095
+ success: false,
12096
+ error: err.name === "TimeoutError" ? "Request timed out" : err.message,
12097
+ firewall: [],
12098
+ whitelists: [],
12099
+ whitelist_keys: {}
12100
+ };
12101
+ }
12102
+ }
12103
+ #touch(now) {
12104
+ this.#cacheTime = now;
12105
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
12106
+ }
12107
+ clearCache() {
12108
+ this.#cache = null;
12109
+ this.#cacheTime = null;
12110
+ this.#etag = null;
12111
+ }
12112
+ };
12113
+ function buildVerdictOf(headers) {
12114
+ const get = headers !== null && typeof headers === "object" && typeof headers.get === "function" ? (name) => headers.get(name) : () => null;
12115
+ const stated2 = get(BUILD_VERDICT_HEADER);
12116
+ const verdict = stated2 === "match" || stated2 === "missing" || stated2 === "mismatch" ? stated2 : "unknown";
12117
+ const matched = get(BUILD_IDENTITY_HEADER);
12118
+ return { verdict, matchedBuildId: typeof matched === "string" ? matched : null };
12119
+ }
12120
+ function enforcementField(data) {
12121
+ if (!data || typeof data !== "object") return {};
12122
+ const v = data.enforcement ?? data.mode;
12123
+ return v === "block" || v === "dry-run" ? { enforcement: v } : {};
12124
+ }
12125
+
12126
+ // src/protect/rules/validate.js
12127
+ var LIMITS2 = LIMITS;
12128
+ var PHASES2 = new Set(PHASES);
12129
+ var ACTIONS2 = new Set(ACTIONS);
12130
+ function validateBundle(bundle, opts = {}) {
12131
+ const rejected = [];
12132
+ let omittedRejections = 0;
12133
+ const reject = (entry) => {
12134
+ if (rejected.length < 100) rejected.push(entry);
12135
+ else omittedRejections++;
12136
+ };
12137
+ const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
12138
+ const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
12139
+ const bundleBudget = { count: 0 };
12140
+ const firewall = [];
12141
+ for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
12142
+ const reason = enforceableRuleProblem(rule, bundleBudget);
12143
+ if (reason) reject({ id: idOf(rule), reason });
12144
+ else firewall.push(rule);
12145
+ }
12146
+ if (inFirewall.length > LIMITS2.maxRules) {
12147
+ reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
12148
+ }
12149
+ const whitelists = [];
12150
+ for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
12151
+ if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
12152
+ reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
12153
+ continue;
12154
+ }
12155
+ if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
12156
+ reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
12157
+ continue;
12158
+ }
12159
+ const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
12160
+ if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
12161
+ else whitelists.push(wl);
12162
+ }
12163
+ if (inWhitelists.length > LIMITS2.maxWhitelists) {
12164
+ reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
12165
+ }
12166
+ let whitelistKeys = {};
12167
+ if (bundle?.whitelist_keys !== void 0) {
12168
+ if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
12169
+ reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
12170
+ } else {
12171
+ const entries = Object.entries(bundle.whitelist_keys);
12172
+ if (entries.length > LIMITS2.maxMapEntries) {
12173
+ reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
12174
+ } else {
12175
+ const invalid = entries.find(([key, value]) => key.length > LIMITS2.maxParameterLength || !Array.isArray(value) || value.length > LIMITS2.maxOperandItems || value.some((entry) => typeof entry !== "string" || entry.length > LIMITS2.maxValueLength));
12176
+ if (invalid) {
12177
+ reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
12178
+ } else {
12179
+ whitelistKeys = bundle.whitelist_keys;
12180
+ }
12181
+ }
12182
+ }
12183
+ }
12184
+ if (omittedRejections > 0) {
12185
+ rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
12186
+ }
12187
+ return {
12188
+ bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
12189
+ rejected
12190
+ };
12191
+ }
12192
+ function idOf(rule) {
12193
+ const id = rule?.id ?? rule?.rule_id;
12194
+ if (id === void 0 || id === null) return "(unidentified)";
12195
+ try {
12196
+ return String(id).slice(0, 128);
12197
+ } catch {
12198
+ return "(unidentified)";
12199
+ }
12200
+ }
12201
+ function enforceableRuleProblem(rule, bundleBudget = null) {
12202
+ if (!rule || typeof rule !== "object") return "not an object";
12203
+ const nullReason = nullPropertyProblem(rule);
12204
+ if (nullReason) return nullReason;
12205
+ if (rule.phase !== void 0 && !PHASES2.has(rule.phase)) return `unknown phase "${rule.phase}"`;
12206
+ const actionReason = actionProblem(rule.action, rule);
12207
+ if (actionReason) return actionReason;
12208
+ const scopeReason = whenProblem(rule.when);
12209
+ if (scopeReason) return scopeReason;
12210
+ const propertyReason = rulePropertyProblem(rule);
12211
+ if (propertyReason) return propertyReason;
12212
+ return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
12213
+ }
12214
+ function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
12215
+ if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
12216
+ if (conditions.length === 0) return "rule_v2 is empty (would never match)";
12217
+ if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
12218
+ if (conditions.length > LIMITS2.maxConditionsPerRule) {
12219
+ return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
12220
+ }
12221
+ for (const c of conditions) {
12222
+ ruleBudget.count++;
12223
+ if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
12224
+ return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
12225
+ }
12226
+ if (bundleBudget) {
12227
+ bundleBudget.count++;
12228
+ if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
12229
+ return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
12230
+ }
12231
+ }
12232
+ const shapeReason = conditionShapeProblem(c);
12233
+ if (shapeReason) return shapeReason;
12234
+ if (isGroup(c)) {
12235
+ const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
12236
+ if (nested) return nested;
12237
+ continue;
12238
+ }
12239
+ const parameterReason = parameterProblem(c.parameter);
12240
+ if (parameterReason) return parameterReason;
12241
+ const mutationReason = mutationsProblem(c.mutations);
12242
+ if (mutationReason) return mutationReason;
12243
+ const matchReason = matchProblem(c.match, c.parameter);
12244
+ if (matchReason) return matchReason;
12245
+ const m = c.match;
12246
+ if (m.type === "regex") {
12247
+ if (typeof m.value !== "string") return "regex match.value must be a string";
12248
+ if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
12249
+ if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
12250
+ } else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
12251
+ return `match.value longer than ${LIMITS2.maxValueLength} chars`;
12252
+ }
12253
+ if (m.match) {
12254
+ const nested = conditionsProblem(
12255
+ [{ parameter: c.parameter, match: m.match }],
12256
+ depth + 1,
12257
+ ruleBudget,
12258
+ bundleBudget
12259
+ );
12260
+ if (nested) return nested;
12261
+ }
12262
+ }
12263
+ return null;
12264
+ }
12265
+
12266
+ // src/protect/rules/source.js
12267
+ function liveUpdateRejections(res, options) {
12268
+ if (options.acceptPartialBundle) return [];
12269
+ const { rejected } = validateBundle(
12270
+ {
12271
+ firewall: Array.isArray(res.firewall) ? res.firewall : [],
12272
+ whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
12273
+ whitelist_keys: res.whitelist_keys
12274
+ },
12275
+ { allowGlobalWhitelists: options.allowGlobalWhitelists }
12276
+ );
12277
+ return rejected;
12278
+ }
12279
+ function reportRejections(rejected, options, label) {
12280
+ const report = options.onRuleRejected;
12281
+ for (const r of rejected) {
12282
+ if (typeof report === "function") {
12283
+ notify(report, { ...r, accepted: false }, "onRuleRejected");
12284
+ }
12285
+ }
12286
+ const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
12287
+ notify(options.onError, new Error(
12288
+ `${label}: rejected the entire update because ${rejected.length} rule(s) failed validation \u2014 keeping the previous ruleset and NOT caching this response: ${sample}${rejected.length > 3 ? ", \u2026" : ""}`
12289
+ ), "onError");
12290
+ }
12291
+ function fromSource(bundle, origin, reason) {
12292
+ return {
12293
+ ...bundle,
12294
+ source: reason === void 0 ? { ok: true, origin } : { ok: false, origin, reason }
12295
+ };
12296
+ }
12297
+ function buildIdentity(options) {
12298
+ if (options.buildId !== void 0 && options.buildId !== null) {
12299
+ const explicit = canonicalBuildId(options.buildId);
12300
+ return explicit === null ? { id: null, reason: "the buildId option is not a complete 64-hex SHA-256 identity" } : { id: explicit };
12301
+ }
12302
+ const stamped = readBuildStamp(options.rules);
12303
+ if (stamped !== null) return { id: stamped };
12304
+ return {
12305
+ id: null,
12306
+ reason: options.rules ? "the guard rules file carries no build identity \u2014 the build did not stamp one" : "this guard was configured without bundled rules, so it carries no build identity"
12307
+ };
12308
+ }
12309
+ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
12310
+ let held = 0;
12311
+ let unusable = 0;
12312
+ const firewall = bundle.firewall.map((rule) => {
12313
+ if (rule === null || typeof rule !== "object") return rule;
12314
+ if (!(BUILD_SCOPE_PROPERTY in rule)) return rule;
12315
+ const scope = rule[BUILD_SCOPE_PROPERTY];
12316
+ const target = canonicalBuildId(scope);
12317
+ if (target !== null && confirmed !== null && target === confirmed) return rule;
12318
+ if (rule.enforcement === "dry-run") return rule;
12319
+ held += 1;
12320
+ if (target === null) unusable += 1;
12321
+ return { ...rule, enforcement: "dry-run" };
12322
+ });
12323
+ if (held === 0) return bundle;
12324
+ const explanations = [
12325
+ ...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
12326
+ ...held > unusable ? [reason] : []
12327
+ ];
12328
+ const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
12329
+ if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
12330
+ else warnHeldOnce(message);
12331
+ return { ...bundle, firewall };
12332
+ }
12333
+ var warnedHeld = /* @__PURE__ */ new Set();
12334
+ function warnHeldOnce(message) {
12335
+ if (warnedHeld.has(message)) return;
12336
+ warnedHeld.add(message);
12337
+ console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
12338
+ }
12339
+ function confirmedBy(res, presented) {
12340
+ if (presented === null) return null;
12341
+ const verdict = res?.build;
12342
+ if (verdict?.verdict !== "match") return null;
12343
+ return canonicalBuildId(verdict.matchedBuildId) === presented ? presented : null;
12344
+ }
12345
+ async function resolveRules(options, store, ctx = {}) {
12346
+ const timeoutMs = ctx.timeoutMs;
12347
+ if (options.siteUuid) {
12348
+ const prior = await store.read();
12349
+ const identity2 = buildIdentity(options);
12350
+ const sameBuild = prior?.buildId === identity2.id;
12351
+ const cacheConfirmed = identity2.id !== null && sameBuild && prior?.matchedBuildId === identity2.id ? identity2.id : null;
12352
+ const unconfirmed = identity2.reason ?? "the platform did not confirm that these coordinates belong to this map";
12353
+ const served = (bundle, confirmed) => withScopedDryRun(bundle, confirmed, unconfirmed, options);
12354
+ const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options);
12355
+ const client = new PulseRuleClient({
12356
+ siteUuid: options.siteUuid,
12357
+ baseUrl: options.pulseRulesUrl,
12358
+ etag: sameBuild ? prior?.etag : null,
12359
+ timeoutMs,
12360
+ pulseAuth: ctx.pulseAuth,
12361
+ detectionState: ctx.detectionState,
12362
+ buildId: identity2.id
12363
+ });
12364
+ const res = await client.getRules();
12365
+ if (res.success && res.notModified && prior?.bundle) {
12366
+ const confirmed = confirmedBy(res, identity2.id);
12367
+ await store.write({
12368
+ bundle: prior.bundle,
12369
+ etag: res.etag ?? prior.etag ?? null,
12370
+ buildId: identity2.id,
12371
+ matchedBuildId: confirmed
12372
+ });
12373
+ return fromSource(served(normalizeBundle(prior.bundle, options), confirmed), "cache");
12374
+ }
12375
+ if (res.success && !res.notModified) {
12376
+ const confirmed = confirmedBy(res, identity2.id);
12377
+ const rejected = liveUpdateRejections(res, options);
12378
+ if (rejected.length > 0) {
12379
+ reportRejections(rejected, options, "rule update rejected");
12380
+ if (prior?.bundle) return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", "update rejected");
12381
+ if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
12382
+ return fromSource(emptyBundle(), "empty", "update rejected");
12383
+ }
12384
+ const bundle = normalizeBundle(res, options);
12385
+ await store.write({ bundle, etag: res.etag ?? null, buildId: identity2.id, matchedBuildId: confirmed });
12386
+ return fromSource(served(bundle, confirmed), "api");
12387
+ }
12388
+ if (prior?.bundle) {
12389
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
12390
+ return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", res.error ?? "no usable response");
12391
+ }
12392
+ if (options.rules) {
12393
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
12394
+ return fromSource(local(normalizeBundle(options.rules, options)), "bundled", res.error ?? "no usable response");
12395
+ }
12396
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
12397
+ return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
12398
+ }
12399
+ if (options.token) {
12400
+ const identity2 = buildIdentity(options);
12401
+ const remote = (bundle) => withScopedDryRun(
12402
+ bundle,
12403
+ null,
12404
+ "the token-authenticated rules service did not corroborate this map",
12405
+ options
12406
+ );
12407
+ const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options);
12408
+ const prior = await store.read();
12409
+ const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag, timeoutMs });
12410
+ const res = await client.getRules();
12411
+ if (res.success && res.notModified && prior?.bundle) {
12412
+ return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache");
12413
+ }
12414
+ if (res.success && !res.notModified) {
12415
+ const rejected = liveUpdateRejections(res, options);
12416
+ if (rejected.length > 0) {
12417
+ reportRejections(rejected, options, "rule update rejected");
12418
+ if (prior?.bundle) return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", "update rejected");
12419
+ if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
12420
+ return fromSource(emptyBundle(), "empty", "update rejected");
12421
+ }
12422
+ const bundle = normalizeBundle(res, options);
12423
+ await store.write({ bundle, etag: res.etag ?? null });
12424
+ return fromSource(remote(bundle), "api");
12425
+ }
12426
+ if (prior?.bundle) {
12427
+ notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
12428
+ return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", res.error ?? "no usable response");
12429
+ }
12430
+ notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
12431
+ return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
12432
+ }
12433
+ if (options.rules) {
12434
+ const identity2 = buildIdentity(options);
12435
+ const bundle = normalizeBundle(options.rules, options);
12436
+ if (options.trustLocalRuleScope === true) return fromSource(bundle, "bundled");
12437
+ return fromSource(
12438
+ withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options),
12439
+ "bundled"
12440
+ );
12441
+ }
12442
+ return fromSource(emptyBundle(), "empty");
12443
+ }
12444
+ function normalizeBundle(b, options = {}) {
12445
+ const enforcement = b?.enforcement ?? b?.mode;
12446
+ const { bundle: checked, rejected } = validateBundle(
12447
+ {
12448
+ firewall: Array.isArray(b.firewall) ? b.firewall : [],
12449
+ whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
12450
+ whitelist_keys: b.whitelist_keys
12451
+ },
12452
+ { allowGlobalWhitelists: options.allowGlobalWhitelists }
12453
+ );
12454
+ if (rejected.length > 0) {
12455
+ const report = options.onRuleRejected;
12456
+ if (typeof report === "function") {
12457
+ for (const r of rejected) {
12458
+ notify(report, r, "onRuleRejected");
12459
+ }
12460
+ } else {
12461
+ const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
12462
+ console.warn(
12463
+ `[patchstack] ${rejected.length} delivered rule(s) rejected as invalid/oversized and are NOT enforced: ${sample}` + (rejected.length > 3 ? ", \u2026" : "")
12464
+ );
12465
+ }
12466
+ }
12467
+ return {
12468
+ firewall: checked.firewall,
12469
+ whitelists: checked.whitelists,
12470
+ whitelist_keys: checked.whitelist_keys,
12471
+ ...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
12472
+ };
12473
+ }
12474
+ function emptyBundle() {
12475
+ return { firewall: [], whitelists: [], whitelist_keys: {} };
12476
+ }
12477
+
12478
+ // src/protect/rules/store.js
12479
+ var fsMod;
12480
+ async function loadFs() {
12481
+ if (fsMod !== void 0) return fsMod;
12482
+ try {
12483
+ const [fs, path17] = await Promise.all([import("fs"), import("path")]);
12484
+ fsMod = {
12485
+ chmodSync: fs.chmodSync,
12486
+ closeSync: fs.closeSync,
12487
+ constants: fs.constants,
12488
+ fstatSync: fs.fstatSync,
12489
+ fsyncSync: fs.fsyncSync,
12490
+ lstatSync: fs.lstatSync,
12491
+ openSync: fs.openSync,
12492
+ readFileSync: fs.readFileSync,
12493
+ mkdirSync: fs.mkdirSync,
12494
+ renameSync: fs.renameSync,
12495
+ unlinkSync: fs.unlinkSync,
12496
+ writeFileSync: fs.writeFileSync,
12497
+ join: path17.join
12498
+ };
12499
+ } catch {
12500
+ fsMod = null;
12501
+ }
12502
+ return fsMod;
12503
+ }
12504
+ async function sourceIdentity(options = {}) {
12505
+ if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
12506
+ const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
12507
+ return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
12508
+ }
12509
+ if (typeof options.token === "string" && options.token !== "") {
12510
+ const digest = await sha256Hex(options.token);
12511
+ if (digest === null) return null;
12512
+ const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
12513
+ return `token:${digest}@${endpoint}`;
12514
+ }
12515
+ return null;
12516
+ }
12517
+ async function sha256Hex(text) {
12518
+ try {
12519
+ const subtle = globalThis.crypto?.subtle;
12520
+ if (!subtle || typeof TextEncoder === "undefined") return null;
12521
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
12522
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
12523
+ } catch {
12524
+ return null;
12525
+ }
12526
+ }
12527
+ function makeStore(options = {}) {
12528
+ let mem = null;
12529
+ const durable = durableTier(options);
12530
+ let identity2;
12531
+ const identityOf = () => identity2 ??= sourceIdentity(options);
12532
+ return {
12533
+ async read() {
12534
+ if (mem) return mem;
12535
+ const source = await identityOf();
12536
+ if (source === null) return null;
12537
+ const env = await durable.read();
12538
+ if (!env || env.source !== source) return null;
12539
+ mem = env;
12540
+ return env;
12541
+ },
12542
+ async write(env) {
12543
+ if (!env) return;
12544
+ const stamped = { ...env, source: await identityOf() };
12545
+ mem = stamped;
12546
+ await durable.write(stamped);
12547
+ }
12548
+ };
12549
+ }
12550
+ function durableTier(options) {
12551
+ const adapter = options.ruleCache;
12552
+ if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
12553
+ return {
12554
+ read: async () => {
12555
+ try {
12556
+ return toEnvelope(await adapter.read());
12557
+ } catch {
12558
+ return null;
12559
+ }
12560
+ },
12561
+ write: async (env) => {
12562
+ try {
12563
+ await adapter.write(env);
12564
+ } catch {
12565
+ }
12566
+ }
12567
+ };
12568
+ }
12569
+ const dir = options.cacheDir;
12570
+ return {
12571
+ read: async () => cacheRead(dir),
12572
+ write: async (env) => cacheWrite(dir, env)
12573
+ };
12574
+ }
12575
+ async function cacheWrite(dir, env) {
12576
+ if (!dir) return;
12577
+ const fs = await loadFs();
12578
+ if (!fs) return;
12579
+ const target = fs.join(dir, "patchstack-rules.json");
12580
+ const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
12581
+ let descriptor = null;
12582
+ try {
12583
+ fs.mkdirSync(dir, { recursive: true });
12584
+ const directory = fs.lstatSync(dir);
12585
+ if (directory.isSymbolicLink() || !directory.isDirectory()) return;
12586
+ let mode;
12587
+ try {
12588
+ const current = fs.lstatSync(target);
12589
+ if (current.isSymbolicLink() || !current.isFile()) return;
12590
+ mode = current.mode & 4095;
12591
+ } catch (error) {
12592
+ if (error?.code !== "ENOENT") return;
12593
+ }
12594
+ descriptor = fs.openSync(temporary, "wx", mode ?? 384);
12595
+ fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
12596
+ fs.fsyncSync(descriptor);
12597
+ fs.closeSync(descriptor);
12598
+ descriptor = null;
12599
+ if (mode !== void 0) fs.chmodSync(temporary, mode);
12600
+ try {
12601
+ fs.renameSync(temporary, target);
12602
+ } catch (error) {
12603
+ if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
12604
+ try {
12605
+ fs.unlinkSync(target);
12606
+ } catch (unlinkError) {
12607
+ if (unlinkError?.code !== "ENOENT") throw unlinkError;
12608
+ }
12609
+ fs.renameSync(temporary, target);
12610
+ }
12611
+ } catch {
12612
+ } finally {
12613
+ if (descriptor !== null) {
12614
+ try {
12615
+ fs.closeSync(descriptor);
12616
+ } catch {
12617
+ }
12618
+ }
12619
+ try {
12620
+ fs.unlinkSync(temporary);
12621
+ } catch {
12622
+ }
12623
+ }
12624
+ }
12625
+ async function cacheRead(dir) {
12626
+ if (!dir) return null;
12627
+ const fs = await loadFs();
12628
+ if (!fs) return null;
12629
+ const target = fs.join(dir, "patchstack-rules.json");
12630
+ let descriptor = null;
12631
+ try {
12632
+ const entry = fs.lstatSync(target);
12633
+ if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
12634
+ const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
12635
+ descriptor = fs.openSync(target, flags);
12636
+ const opened = fs.fstatSync(descriptor);
12637
+ if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
12638
+ return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
12639
+ } catch {
12640
+ return null;
12641
+ } finally {
12642
+ if (descriptor !== null) {
12643
+ try {
12644
+ fs.closeSync(descriptor);
12645
+ } catch {
12646
+ }
12647
+ }
12648
+ }
12649
+ }
12650
+ function randomSuffix() {
12651
+ try {
12652
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
12653
+ } catch {
12654
+ }
12655
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
12656
+ }
12657
+ function toEnvelope(value) {
12658
+ if (!value || typeof value !== "object") return null;
12659
+ if (value.bundle && typeof value.bundle === "object") {
12660
+ const buildId = canonicalBuildId(value.buildId);
12661
+ const matched = canonicalBuildId(value.matchedBuildId);
12662
+ return {
12663
+ bundle: value.bundle,
12664
+ etag: value.etag ?? null,
12665
+ buildId,
12666
+ // A confirmation belongs to the presentation stored beside it. A crossed or partially written
12667
+ // envelope confirms nothing, even when one of its fields happens to name the current map.
12668
+ matchedBuildId: buildId !== null && matched === buildId ? matched : null,
12669
+ source: typeof value.source === "string" ? value.source : null
12670
+ };
12671
+ }
12672
+ if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
12673
+ return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
12674
+ }
12675
+ return null;
12676
+ }
12677
+
12678
+ // src/setup-sync.ts
12679
+ async function syncSetupProtection(cwd, config) {
12680
+ const notices = [];
12681
+ const warnings2 = [];
12682
+ let map;
12683
+ try {
12684
+ map = await runMapDetailed(/* @__PURE__ */ new Map([["dir", cwd], ["upload", true]]), {
12685
+ config,
12686
+ setup: true,
12687
+ log: (line) => notices.push(line)
12688
+ });
12689
+ } catch {
12690
+ map = { code: 1, error: "Could not analyse or bind the map; check the project files and permissions." };
12691
+ }
12692
+ const unavailable = (error) => ({
12693
+ map,
12694
+ rules: { ok: false, count: 0, origin: "empty", error },
12695
+ notices,
12696
+ warnings: warnings2
12697
+ });
12698
+ if (!isCanonicalUuid(config.siteUuid)) return unavailable("No site UUID is configured.");
12699
+ if (!config.pulseAuth) return unavailable("Set PATCHSTACK_API_KEY or run patchstack-connect login to fetch live rules.");
12700
+ try {
12701
+ const rulesUrl = buildRulesUrl(config.endpoint, config.siteUuid);
12702
+ assertConnectableEndpoint(config, rulesUrl);
12703
+ const base = rulesUrl.slice(0, rulesUrl.lastIndexOf("/rules/"));
12704
+ const defaultRules = buildRulesUrl(DEFAULT_ENDPOINT, config.siteUuid);
12705
+ if (rulesUrl !== defaultRules && process.env.PATCHSTACK_PULSE_RULES_URL !== base) {
12706
+ warnings2.push("A custom rules service was used for setup. Set PATCHSTACK_PULSE_RULES_URL to the same Pulse base in the server environment for runtime updates.");
12707
+ }
12708
+ const options = {
12709
+ siteUuid: config.siteUuid,
12710
+ // Match the ordinary guard's default cache identity; explicit endpoints remain isolated.
12711
+ pulseRulesUrl: base,
12712
+ cacheDir: join26(cwd, ".patchstack"),
12713
+ buildId: map.buildId ?? void 0,
12714
+ onError: () => notices.push("Some rules could not be refreshed or are detect-only; verify delivery after starting the app.")
12715
+ };
12716
+ const ignored = await ensureIgnored(cwd, ".patchstack/", "Patchstack local runtime cache");
12717
+ if (!ignored.ignored) warnings2.push("The local rule cache is not git-ignored. Keep .patchstack/ out of version control.");
12718
+ const store = makeStore({ ...options, pulseRulesUrl: rulesUrl === defaultRules ? void 0 : base });
12719
+ const bundle = await resolveRules(options, store, {
12720
+ pulseAuth: config.pulseAuth,
12721
+ timeoutMs: Math.min(config.timeoutMs, 1e4)
12722
+ });
12723
+ return {
12724
+ map,
12725
+ notices,
12726
+ warnings: warnings2,
12727
+ rules: {
12728
+ ok: bundle.source.ok,
12729
+ origin: bundle.source.origin,
12730
+ count: bundle.firewall.length,
12731
+ ...bundle.source.ok ? {} : { error: bundle.source.reason ?? "Live rules could not be fetched." }
12732
+ }
12733
+ };
12734
+ } catch {
12735
+ return unavailable("Live rules could not be fetched. Check the endpoint, authentication and project permissions.");
12736
+ }
12737
+ }
12738
+
12739
+ // src/cli.ts
12740
+ var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
12741
+
12742
+ Usage:
12743
+ patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
12744
+ If no UUID is configured, the server
12745
+ provisions one and we persist it. After a
12746
+ successful post it also adds/updates the
12747
+ Patchstack Connector <script> tag in the root
12748
+ HTML shell (index.html, public/index.html,
12749
+ or src/app.html) \u2014 opt out with
12750
+ "widget": false in .patchstackrc.json.
12751
+ Run as a postinstall/prebuild/build hook, a
12752
+ report Patchstack cannot accept is printed
12753
+ and exits 0 so the build goes on; run
12754
+ directly, the same failure exits 1
12755
+ patchstack-connect setup [options] Finish the bounded project setup: run scan,
12756
+ manage the widget, install + verify runtime
12757
+ protection, upload the attack-surface map, pull
12758
+ live rules, and wire dependency/build scans.
12759
+ Never runs the project build
12760
+ patchstack-connect map [--dir <p>] [--out <f>] [--upload]
12761
+ Map the app's attack surface: entry points, the
12762
+ inputs each reads, the sinks it can reach, and
12763
+ evidence-backed input\u2192sink flows (each labelled
12764
+ with how the link was established). Best-effort static
12765
+ analysis \u2014 reports the DETECTED surface, with
12766
+ coverage counters. Prints JSON (--out writes a
12767
+ file; a pre-bundle --upload also stamps the existing
12768
+ guard rules file; --follow-symlinks leaves the project dir).
12769
+ Uses the app's own TypeScript
12770
+ patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
12771
+ with an existing site UUID
12772
+ patchstack-connect status [options] Show current configuration and whether the
12773
+ site still exists on Patchstack (active /
12774
+ removed)
12775
+ patchstack-connect uninstall [options] Signal Patchstack that this package is being
12776
+ removed from the project. An unclaimed site
12777
+ record is deleted; a claimed site is flagged
12778
+ for its owner to remove in the dashboard.
12779
+ Does NOT touch local files \u2014 see the
12780
+ "Uninstalling" steps in AGENT-INSTALL.md
12781
+ patchstack-connect mark-build [options] Stamp built HTML with a production flag +
12782
+ build fingerprint, and ensure the widget
12783
+ tag in built pages (run as a postbuild step).
12784
+ The flag marks a page as the LIVE site, so it
12785
+ is stamped only on a production build; a local
12786
+ or preview build gets the widget tag and no
12787
+ marker (and any stale marker is removed).
12788
+ Looks in the framework's own output dir, an
12789
+ --output the build script names, then dist/,
12790
+ build/, out/, .output/public/, _site/ \u2014 inside
12791
+ the project only. --dir <path> overrides.
12792
+ patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
12793
+ guard). Auto-wires supported server stacks;
12794
+ for others it scaffolds a
12795
+ generic guard + prints a wiring plan.
12796
+ --demo seeds a broad sample rule set (for
12797
+ demonstrations, not production).
12798
+ --check verifies the guard is wired (exit 1
12799
+ if not) \u2014 for the wire-then-verify loop.
12800
+ It reads the app's source and never runs it.
12801
+ --check --runtime additionally STARTS THE APP
12802
+ on a loopback port and sends it one request,
12803
+ to establish that a request reaches the
12804
+ guard. Opt-in, and the only mode that runs
12805
+ the app. Exit 0 traversed, 1 a listener
12806
+ answered instead of the guard, 2 could not
12807
+ be established (e.g. a TypeScript or
12808
+ framework entry this must not start).
12809
+ patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
12810
+ walkthrough: verify the vulnerable package,
12811
+ scan it, wait for live rule 18843, install +
12812
+ verify the guard, and print test requests.
12813
+ Does not install vulnerable dependencies.
12814
+ patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
12815
+ preparing, running, proving, and cleaning up
12816
+ the production-backed local demo.
12817
+ patchstack-connect guide [--full] Show this project's setup status (what's done,
12818
+ what's missing, with tailored commands), then
12819
+ print the full setup guide. --full prints the
12820
+ guide even when setup is complete
10483
12821
  patchstack-connect claim [--wait] Attach this site to a Patchstack account from
10484
12822
  the terminal, so its reports become visible.
10485
12823
  Prints a link for the user to open and sign in
@@ -10736,7 +13074,7 @@ async function runClaim(args) {
10736
13074
  }
10737
13075
  async function runLogin(args) {
10738
13076
  if (runningInCi()) {
10739
- console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
13077
+ console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_API_KEY instead.");
10740
13078
  return 1;
10741
13079
  }
10742
13080
  const config = await resolveCliConfig(args);
@@ -10874,6 +13212,8 @@ async function runScan(args, options = {}) {
10874
13212
  const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
10875
13213
  say(`Environment: ${config.environment}${because}.`);
10876
13214
  }
13215
+ const overruled = ignoredFileEnvironmentWarning(config);
13216
+ if (overruled !== null) console.warn(`patchstack: ${overruled}`);
10877
13217
  if (config.endpoint !== DEFAULT_ENDPOINT) {
10878
13218
  say(`Endpoint override: ${config.endpoint}`);
10879
13219
  }
@@ -11069,6 +13409,12 @@ function reportSourceWidget(siteUuid, framework, report) {
11069
13409
  detail(`Widget: skipped (${err.message}).`);
11070
13410
  }
11071
13411
  }
13412
+ function ignoredFileEnvironmentWarning(config) {
13413
+ const ignored = config.ignoredFileEnvironment;
13414
+ if (ignored == null) return null;
13415
+ const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
13416
+ return `.patchstackrc.json sets "environment": "${ignored}", but this build is production${because}, so it is treated as production. Remove "environment" from .patchstackrc.json and set PATCHSTACK_ENVIRONMENT=${ignored} only in the process that needs it.`;
13417
+ }
11072
13418
  function reportSourceMarker(framework, checksum, report) {
11073
13419
  try {
11074
13420
  const shell = resolveWidgetFileHint(process.cwd(), framework);
@@ -11251,7 +13597,7 @@ async function runGuide(args) {
11251
13597
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
11252
13598
  console.log("");
11253
13599
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
11254
- console.log(readFileSync19(guidePath, "utf8"));
13600
+ console.log(readFileSync20(guidePath, "utf8"));
11255
13601
  return 0;
11256
13602
  }
11257
13603
  function reportProtection(protection, report) {
@@ -11306,11 +13652,11 @@ Run: ${installCommand(before.packageManager)}`
11306
13652
  return 1;
11307
13653
  }
11308
13654
  const report = emptyReport();
11309
- let reported = {};
13655
+ let reported2 = {};
11310
13656
  const scanCode = await runScan(args, {
11311
13657
  report,
11312
13658
  onReported: (outcome) => {
11313
- reported = outcome;
13659
+ reported2 = outcome;
11314
13660
  }
11315
13661
  });
11316
13662
  if (scanCode !== 0) {
@@ -11321,11 +13667,29 @@ Run: ${installCommand(before.packageManager)}`
11321
13667
  report.done.push(buildStepsLine(wired));
11322
13668
  detail(`Build hooks: ${wired.detail}`);
11323
13669
  const config = await resolveCliConfig(args);
13670
+ const synced = await syncSetupProtection(process.cwd(), config);
13671
+ for (const line of synced.notices) detail(line);
13672
+ for (const [index, warning] of synced.warnings.entries()) {
13673
+ report.missing.push({ key: `setup-sync-${index}`, text: warning });
13674
+ }
13675
+ const uploaded = synced.map.upload;
13676
+ if (uploaded?.result === "stored" || uploaded?.result === "unchanged") {
13677
+ report.done.push(`Attack-surface map uploaded (revision ${uploaded.revision}; ${synced.map.endpoints ?? 0} detected entry points)`);
13678
+ if (!synced.map.buildId) report.missing.push({ key: "map-binding", text: "Map is not linked to runtime protection", hint: ["Finish the reported protection integration, then rerun setup. Rules for specific inputs can only detect, not block, until linked."] });
13679
+ } else {
13680
+ report.missing.push({ key: "map-upload", text: "Attack-surface map was not uploaded", hint: [synced.map.error ?? uploaded?.message ?? "Check connectivity and rerun setup."] });
13681
+ }
13682
+ if (synced.rules.ok) {
13683
+ report.done.push(`Live rule lookup succeeded (${synced.rules.count} delivered rules; ${synced.rules.count === 0 ? "no rules currently assigned" : "not proof of runtime enforcement"})`);
13684
+ } else {
13685
+ report.missing.push({ key: "rules-pull", text: "Live rule lookup is incomplete", hint: [synced.rules.error ?? "Check authentication and rerun setup."] });
13686
+ }
13687
+ report.missing.push({ key: "runtime-restart", text: "Load the new protection in your running app", hint: ["Restart the preview/server if it is already running. Deploy a new build to update the live app. Setup does not start or deploy it."] });
11324
13688
  const after = await collectGuideState(process.cwd());
11325
- const missing = mergeMissing(report.missing, guideMissing(after, reported));
13689
+ const missing = mergeMissing(report.missing, guideMissing(after, reported2));
11326
13690
  const context = scanNextStepContext(config, after, after.siteUuid);
11327
13691
  if (verbose) console.log("");
11328
- for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported), context, {
13692
+ for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported2), context, {
11329
13693
  useColor: useColor(),
11330
13694
  verbose
11331
13695
  })) {
@@ -11399,7 +13763,7 @@ function describeStack(stack) {
11399
13763
  }
11400
13764
  function readBuildScript(cwd) {
11401
13765
  try {
11402
- const pkg = JSON.parse(readFileSync19(path16.join(cwd, "package.json"), "utf8"));
13766
+ const pkg = JSON.parse(readFileSync20(path16.join(cwd, "package.json"), "utf8"));
11403
13767
  return pkg.scripts?.build;
11404
13768
  } catch {
11405
13769
  return void 0;
@@ -11456,6 +13820,8 @@ async function runMarkBuild(args) {
11456
13820
  }
11457
13821
  environment = config.environment;
11458
13822
  environmentEvidence = config.environmentEvidence ?? [];
13823
+ const overruled = ignoredFileEnvironmentWarning(config);
13824
+ if (overruled !== null) console.warn(`mark-build: ${overruled}`);
11459
13825
  } catch (err) {
11460
13826
  console.warn(
11461
13827
  `mark-build: could not resolve the site UUID (${err.message}). Skipping the widget pass.`
@@ -11463,7 +13829,7 @@ async function runMarkBuild(args) {
11463
13829
  }
11464
13830
  const forcedProduction = args.flags.get("production") === true;
11465
13831
  const published = forcedProduction || marksPublishedBuild(environment);
11466
- const reported = config !== null && forcedProduction && config.environment !== "production" ? { ...config, environment: "production", environmentSource: "override" } : config;
13832
+ const reported2 = config !== null && forcedProduction && config.environment !== "production" ? { ...config, environment: "production", environmentSource: "override" } : config;
11467
13833
  const buildDirOptions = { framework: stack?.framework ?? null, buildScript: readBuildScript(cwd) };
11468
13834
  const dir = resolveBuildDir(cwd, getStringFlag(args.flags, "dir"), buildDirOptions);
11469
13835
  if (dir === null) {
@@ -11474,14 +13840,14 @@ async function runMarkBuild(args) {
11474
13840
  console.warn(
11475
13841
  'mark-build: if the build writes elsewhere, set it once in package.json \u2192 "postbuild": "patchstack-connect mark-build --dir <path>".'
11476
13842
  );
11477
- await reportBuildStamp(reported, wirePayload, "no-output");
13843
+ await reportBuildStamp(reported2, wirePayload, "no-output");
11478
13844
  return 0;
11479
13845
  }
11480
13846
  const files = findHtmlFiles(dir);
11481
13847
  if (files.length === 0) {
11482
13848
  console.warn(`mark-build: found ${dir} but no HTML files in it.`);
11483
13849
  console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
11484
- await reportBuildStamp(reported, wirePayload, "no-pages");
13850
+ await reportBuildStamp(reported2, wirePayload, "no-pages");
11485
13851
  return 0;
11486
13852
  }
11487
13853
  const staleBy = staleBuildAge(files, startedAt);
@@ -11490,12 +13856,18 @@ async function runMarkBuild(args) {
11490
13856
  `mark-build: nothing in ${dir} changed in the last ${Math.round(staleBy / 6e4)} minute(s). Run it right after the build (as postbuild).`
11491
13857
  );
11492
13858
  }
11493
- const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
13859
+ const snippet = published ? buildInjectionSnippet(checksum, stack) : environment === "sandbox" ? buildSandboxSnippet() : "";
11494
13860
  let marked = 0;
11495
13861
  let widgetTouched = 0;
13862
+ let handRemoved = 0;
11496
13863
  for (const file of files) {
11497
- const before = readFileSync19(file, "utf8");
13864
+ const before = readFileSync20(file, "utf8");
11498
13865
  let after = injectMarker(before, snippet);
13866
+ if (!published) {
13867
+ const cleared = removeHandMarkers(after);
13868
+ if (cleared !== after) handRemoved += 1;
13869
+ after = cleared;
13870
+ }
11499
13871
  if (widgetUuid !== null) {
11500
13872
  const ensured = ensureWidgetInHtml(after, widgetUuid);
11501
13873
  if (ensured.action === "added" || ensured.action === "updated") {
@@ -11514,7 +13886,7 @@ async function runMarkBuild(args) {
11514
13886
  detail(
11515
13887
  `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
11516
13888
  );
11517
- await reportBuildStamp(reported, wirePayload, "stamped");
13889
+ await reportBuildStamp(reported2, wirePayload, "stamped");
11518
13890
  return 0;
11519
13891
  }
11520
13892
  const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
@@ -11524,12 +13896,17 @@ async function runMarkBuild(args) {
11524
13896
  detail(
11525
13897
  `mark-build: ${environment} build${because}, so no production marker in ${files.length} HTML file(s) in ${dir}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
11526
13898
  );
11527
- await reportBuildStamp(reported, wirePayload, "withheld");
13899
+ if (handRemoved > 0) {
13900
+ console.warn(
13901
+ `mark-build: removed a hand-added __PATCHSTACK_PROD__ script from ${plural(handRemoved, "page", "pages")}. Production builds get the marker from mark-build, so it does not need to be in your source.`
13902
+ );
13903
+ }
13904
+ await reportBuildStamp(reported2, wirePayload, "withheld");
11528
13905
  return 0;
11529
13906
  }
11530
13907
  function packageVersion() {
11531
13908
  try {
11532
- const require2 = createRequire3(import.meta.url);
13909
+ const require2 = createRequire6(import.meta.url);
11533
13910
  const manifest = require2("../package.json");
11534
13911
  return typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : "unknown";
11535
13912
  } catch {