@patchstack/connect 0.5.26 → 0.5.30

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 +34 -19
  2. package/README.md +73 -17
  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 +2755 -371
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +33 -12
  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 +33 -12
  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 +33 -12
  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-OMRDCNFD.js → refresh-manifest-EBORKM75.js} +33 -12
  38. package/dist/refresh-manifest-EBORKM75.js.map +1 -0
  39. package/package.json +5 -2
  40. package/dist/refresh-manifest-OMRDCNFD.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,13 +1338,13 @@ 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
1345
1345
  var RULES = [
1346
- { platform: "netlify", any: ["NETLIFY", "NETLIFY_BUILD_BASE", "DEPLOY_PRIME_URL"] },
1347
- { platform: "vercel", any: ["VERCEL", "VERCEL_ENV", "VERCEL_URL"] },
1346
+ { platform: "netlify", any: ["NETLIFY", "NETLIFY_PREVIEW_SERVER", "NETLIFY_DEV", "NETLIFY_BUILD_BASE", "DEPLOY_PRIME_URL"] },
1347
+ { platform: "vercel", any: ["VERCEL", "VERCEL_TARGET_ENV", "VERCEL_ENV", "VERCEL_URL"] },
1348
1348
  { platform: "cloudflare", any: ["CF_PAGES", "CF_PAGES_URL", "WORKERS_CI", "WORKERS_CI_BRANCH", "CLOUDFLARE_ACCOUNT_ID"] },
1349
1349
  { platform: "aws", any: ["AWS_APP_ID", "AWS_BRANCH", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV"] },
1350
1350
  { platform: "render", any: ["RENDER", "RENDER_SERVICE_ID", "RENDER_EXTERNAL_URL"] },
@@ -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) {
@@ -2237,7 +2249,7 @@ import { randomUUID as randomUUID2 } from "crypto";
2237
2249
  // src/environment.ts
2238
2250
  import { readFileSync as readFileSync2 } from "fs";
2239
2251
  import path6 from "path";
2240
- var set = (value) => value !== void 0 && value !== "";
2252
+ var set = (value) => value !== void 0 && value.trim() !== "";
2241
2253
  var PRODUCTION_BRANCHES = /* @__PURE__ */ new Set([
2242
2254
  "main",
2243
2255
  "master",
@@ -2253,14 +2265,31 @@ var DISCRIMINATORS = [
2253
2265
  {
2254
2266
  platform: "vercel",
2255
2267
  read: (env) => {
2256
- if (!set(env.VERCEL) || !set(env.VERCEL_ENV)) return null;
2257
- return env.VERCEL_ENV === "production" ? { environment: "production", evidence: "VERCEL_ENV=production" } : { environment: "sandbox", evidence: `VERCEL_ENV=${env.VERCEL_ENV}` };
2268
+ const variable = set(env.VERCEL_TARGET_ENV) ? "VERCEL_TARGET_ENV" : "VERCEL_ENV";
2269
+ const tier = env[variable];
2270
+ if (set(tier)) {
2271
+ return {
2272
+ environment: tier === "production" ? "production" : "sandbox",
2273
+ evidence: `${variable}=${tier}`
2274
+ };
2275
+ }
2276
+ if (env.VERCEL !== "1") return null;
2277
+ return { environment: "local", evidence: "VERCEL=1 without a deployment tier; set PATCHSTACK_ENVIRONMENT" };
2258
2278
  }
2259
2279
  },
2260
2280
  {
2261
2281
  platform: "netlify",
2262
2282
  read: (env) => {
2263
- if (env.NETLIFY !== "true" || !set(env.CONTEXT)) return null;
2283
+ if (env.NETLIFY_PREVIEW_SERVER === "true") {
2284
+ return { environment: "sandbox", evidence: "NETLIFY_PREVIEW_SERVER=true" };
2285
+ }
2286
+ if (env.NETLIFY_DEV === "true") {
2287
+ return { environment: "local", evidence: "NETLIFY_DEV=true (the local development server)" };
2288
+ }
2289
+ if (env.NETLIFY !== "true") return null;
2290
+ if (!set(env.CONTEXT)) {
2291
+ return { environment: "local", evidence: "NETLIFY=true without CONTEXT; set PATCHSTACK_ENVIRONMENT" };
2292
+ }
2264
2293
  return env.CONTEXT === "production" ? { environment: "production", evidence: "CONTEXT=production" } : { environment: "sandbox", evidence: `CONTEXT=${env.CONTEXT}` };
2265
2294
  }
2266
2295
  },
@@ -2610,10 +2639,13 @@ async function resolveConfig(options) {
2610
2639
  "CONFIG_INVALID"
2611
2640
  );
2612
2641
  }
2613
- const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
2614
- const environment = environmentRaw ?? inferred.environment;
2615
- const environmentEvidence = inferred?.evidence ?? [];
2616
- 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;
2617
2649
  if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
2618
2650
  throw new PatchstackError(
2619
2651
  `Site UUID "${siteUuid}" does not look like a valid UUID.`,
@@ -2628,20 +2660,21 @@ async function resolveConfig(options) {
2628
2660
  }
2629
2661
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
2630
2662
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
2631
- 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 };
2632
2664
  const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
2633
2665
  return {
2634
2666
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
2635
2667
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
2636
2668
  pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
2637
- siteUrl: identity.url,
2638
- siteName: identity.name,
2669
+ siteUrl: identity2.url,
2670
+ siteName: identity2.name,
2639
2671
  endpoint,
2640
2672
  endpointTrusted,
2641
2673
  timeoutMs,
2642
2674
  environment,
2643
2675
  environmentEvidence,
2644
2676
  environmentSource,
2677
+ ignoredFileEnvironment,
2645
2678
  widget: fromFile.widget !== false,
2646
2679
  claimToken
2647
2680
  };
@@ -2696,7 +2729,7 @@ async function writeAtomicFile(target, content, mode) {
2696
2729
  throw error;
2697
2730
  }
2698
2731
  }
2699
- async function ensureIgnored(cwd, entry) {
2732
+ async function ensureIgnored(cwd, entry, label = "Patchstack credential \u2014 never commit this") {
2700
2733
  const target = path8.join(cwd, ".gitignore");
2701
2734
  let existing = "";
2702
2735
  try {
@@ -2710,7 +2743,7 @@ async function ensureIgnored(cwd, entry) {
2710
2743
  if (ignoresEntry(existing, entry)) return { ignored: true };
2711
2744
  const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
2712
2745
  const block = `${separator}
2713
- # Patchstack credential \u2014 never commit this
2746
+ # ${label}
2714
2747
  ${entry}
2715
2748
  `;
2716
2749
  try {
@@ -3023,6 +3056,9 @@ function buildInjectionSnippet(checksum, stack) {
3023
3056
  }
3024
3057
  return `<script ${MARKER_ATTR}>${statements.join("")}</script>`;
3025
3058
  }
3059
+ function buildSandboxSnippet() {
3060
+ return `<script ${MARKER_ATTR}>window.__PATCHSTACK_ENV__="sandbox";</script>`;
3061
+ }
3026
3062
  function injectMarker(html, snippet) {
3027
3063
  const stripped = html.replace(
3028
3064
  new RegExp(`\\s*<script ${MARKER_ATTR}[^>]*>[\\s\\S]*?</script>`, "gi"),
@@ -3036,6 +3072,10 @@ function injectMarker(html, snippet) {
3036
3072
  }
3037
3073
  return stripped + snippet;
3038
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
+ }
3039
3079
  var JSX_SHELL_FRAMEWORKS = /* @__PURE__ */ new Set([
3040
3080
  "next",
3041
3081
  "remix",
@@ -3179,7 +3219,7 @@ function ensureSourceMarker(cwd, shell, framework, checksum = null) {
3179
3219
  }
3180
3220
 
3181
3221
  // src/guide.ts
3182
- 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";
3183
3223
  import path12 from "path";
3184
3224
 
3185
3225
  // src/architecture.ts
@@ -4082,18 +4122,144 @@ function templatesDir() {
4082
4122
  }
4083
4123
 
4084
4124
  // src/protect/install/adapters/tanstack-supabase.ts
4085
- 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";
4086
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
4087
4252
  var CLIENT_TUNNEL = [
4088
4253
  "",
4089
4254
  " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
4090
4255
  " // (same-origin) so payloads are inspected before they reach Supabase.",
4091
4256
  " if (typeof window !== 'undefined') {",
4092
- " const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
4093
- " const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
4094
- " headers.set('x-ps-target', target);",
4257
+ " const original = new Request(input, init);",
4095
4258
  " const guardUrl = new URL('/_patchstack/guard', window.location.origin).toString();",
4096
- " 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);",
4097
4263
  " }",
4098
4264
  ""
4099
4265
  ].join("\n");
@@ -4102,19 +4268,19 @@ var GUARD_IMPORT_RE = /import \{[^}]*\} from "@\/integrations\/patchstack\/guard
4102
4268
  var START_IMPORTS = ['import { getRequest } from "@tanstack/react-start/server";', GUARD_IMPORT].join("\n");
4103
4269
  var REQUEST_MIDDLEWARE_BLOCK = [
4104
4270
  "// #region patchstack-guard (managed by patchstack-connect protect \u2014 do not edit)",
4105
- "// 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.",
4106
4272
  "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
4107
4273
  " const request = getRequest();",
4108
4274
  " if (request) {",
4109
4275
  " const { pathname } = new URL(request.url);",
4110
4276
  " if (pathname === GUARD_PATH) return handleGuardRequest(request);",
4111
- ' if (process.env.PATCHSTACK_ROUTE_WAF === "1") {',
4112
- " const blocked = await guardRequest(request);",
4113
- " if (blocked) return blocked;",
4114
- " }",
4277
+ " const blocked = await guardRequest(request);",
4278
+ " if (blocked) return blocked;",
4115
4279
  " }",
4116
4280
  " // The request is passed so route/method-scoped response rules can apply their scope.",
4117
- " 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) };",
4118
4284
  "});",
4119
4285
  "// #endregion patchstack-guard"
4120
4286
  ].join("\n");
@@ -4153,8 +4319,8 @@ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
4153
4319
  return s.replace(insertBefore, block + "\n\n" + insertBefore);
4154
4320
  }
4155
4321
  function detect(cwd) {
4156
- const pkgPath = join6(cwd, "package.json");
4157
- if (!existsSync4(pkgPath)) return false;
4322
+ const pkgPath = join9(cwd, "package.json");
4323
+ if (!existsSync6(pkgPath)) return false;
4158
4324
  let pkg;
4159
4325
  try {
4160
4326
  pkg = JSON.parse(read(pkgPath));
@@ -4162,22 +4328,22 @@ function detect(cwd) {
4162
4328
  return false;
4163
4329
  }
4164
4330
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
4165
- 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"));
4166
4332
  }
4167
4333
  var GUARD_FILE = "src/integrations/patchstack/guard.ts";
4168
4334
  function scaffold(cwd, opts) {
4169
4335
  const templates = templatesDir();
4170
- const dst = join6(cwd, "src/integrations/patchstack");
4336
+ const dst = join9(cwd, "src/integrations/patchstack");
4171
4337
  ensureProjectDirectorySync(cwd, dst);
4172
- copyProjectFileSync(cwd, join6(templates, "guard.ts"), join6(dst, "guard.ts"));
4173
- const changed = [GUARD_FILE];
4174
- 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");
4175
4341
  if (opts.demo) {
4176
- copyProjectFileSync(cwd, join6(templates, "demo-rules.json"), rulesDst);
4342
+ copyProjectFileSync(cwd, join9(templates, "demo-rules.json"), rulesDst);
4177
4343
  changed.push("src/integrations/patchstack/rules.json");
4178
4344
  log("scaffolded guard.ts + rules.json (demo sample rule set)");
4179
- } else if (!existsSync4(rulesDst)) {
4180
- copyProjectFileSync(cwd, join6(templates, "rules.json"), rulesDst);
4345
+ } else if (!existsSync6(rulesDst)) {
4346
+ copyProjectFileSync(cwd, join9(templates, "rules.json"), rulesDst);
4181
4347
  changed.push("src/integrations/patchstack/rules.json");
4182
4348
  log("scaffolded guard.ts + rules.json (starter rules)");
4183
4349
  } else {
@@ -4185,37 +4351,74 @@ function scaffold(cwd, opts) {
4185
4351
  }
4186
4352
  return changed;
4187
4353
  }
4188
- function patchClient(cwd) {
4189
- const p = join6(cwd, "src/integrations/supabase/client.ts");
4190
- 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;
4191
4371
  if (s.includes("x-ps-target")) {
4192
- log("client.ts already wired");
4193
- return false;
4194
- }
4195
- const anchor = "headers.set('apikey', supabaseKey);";
4196
- if (!s.includes(anchor)) {
4197
- log("client.ts anchor not found \u2014 skipping (template changed?)");
4198
- 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;
4199
4374
  }
4200
- writeProjectFileSync(cwd, p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL), { encoding: "utf8" });
4201
- log("patched client.ts (tunnel Supabase through the guard)");
4202
- return true;
4375
+ return s.slice(0, anchors[0]) + "\n" + CLIENT_TUNNEL + s.slice(anchors[0]);
4203
4376
  }
4204
- function patchStart(cwd) {
4205
- const p = join6(cwd, "src/start.ts");
4206
- let s = read(p);
4377
+ function patchStart(ts, original) {
4378
+ let s = original;
4207
4379
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
4208
4380
  const exportAnchor = "export const startInstance";
4209
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);
4210
4412
  if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
4211
- log("start.ts anchors not found \u2014 skipping (template changed?)");
4212
- return false;
4413
+ return null;
4213
4414
  }
4214
- const original = s;
4415
+ if (!s.includes(rmAnchor)) return null;
4215
4416
  if (GUARD_IMPORT_RE.test(s)) {
4216
4417
  s = s.replace(GUARD_IMPORT_RE, GUARD_IMPORT);
4217
4418
  } else {
4218
- 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));
4219
4422
  }
4220
4423
  s = reconcileBlock(s, "patchstack-guard", REQUEST_MIDDLEWARE_BLOCK, "const patchstackGuard =", exportAnchor);
4221
4424
  s = reconcileBlock(s, "patchstack-function-guard", FUNCTION_MIDDLEWARE_BLOCK, "const patchstackFunctionGuard =", exportAnchor);
@@ -4230,34 +4433,56 @@ function patchStart(cwd) {
4230
4433
  s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
4231
4434
  }
4232
4435
  }
4233
- if (s === original) {
4234
- log("start.ts already wired");
4235
- return false;
4236
- }
4237
- writeProjectFileSync(cwd, p, s, { encoding: "utf8" });
4238
- log("patched start.ts (guard registered as request + function middleware)");
4239
- return true;
4436
+ return parsedSource(ts, "start.ts", s) ? s : null;
4240
4437
  }
4241
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
+ }
4242
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
+ }
4243
4455
  if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
4244
- if (patchClient(cwd)) changed.push("src/integrations/supabase/client.ts");
4245
- 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
+ }
4246
4469
  log(
4247
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."
4248
4471
  );
4249
4472
  return { ok: true, changed: [...new Set(changed)] };
4250
4473
  }
4251
4474
  function verify(cwd) {
4252
- const guardPath = join6(cwd, GUARD_FILE);
4253
- const clientPath = join6(cwd, "src/integrations/supabase/client.ts");
4254
- const startPath = join6(cwd, "src/start.ts");
4255
- const guard = existsSync4(guardPath) ? read(guardPath) : "";
4256
- const client = existsSync4(clientPath) ? read(clientPath) : "";
4257
- 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);
4258
4482
  const checks = [
4259
4483
  { label: "guard.ts scaffolded", ok: guard.length > 0, hint: "run `patchstack-connect protect`" },
4260
- { 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" },
4261
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" },
4262
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" }
4263
4488
  ];
@@ -4271,20 +4496,189 @@ var tanstackSupabaseAdapter = {
4271
4496
  verify
4272
4497
  };
4273
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
+
4274
4667
  // src/protect/install/adapters/next.ts
4275
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3 } from "fs";
4276
- 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";
4277
4671
 
4278
4672
  // src/protect/install/adapters/next-source.ts
4279
- import { createRequire } from "module";
4280
- import { join as join7 } from "path";
4673
+ import { createRequire as createRequire3 } from "module";
4674
+ import { join as join11 } from "path";
4281
4675
  var NEXT_MARKER = "// #region patchstack-next-composed";
4282
4676
  var ROUTE_MARKER = "// #region patchstack-next-route";
4283
4677
  var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
4284
4678
  function nextCompiler(cwd) {
4285
- for (const base of [join7(cwd, "package.json"), import.meta.url]) {
4679
+ for (const base of [join11(cwd, "package.json"), import.meta.url]) {
4286
4680
  try {
4287
- return createRequire(base)("typescript");
4681
+ return createRequire3(base)("typescript");
4288
4682
  } catch {
4289
4683
  }
4290
4684
  }
@@ -4356,7 +4750,7 @@ function standardNextRouting(ts, file, source) {
4356
4750
  return false;
4357
4751
  };
4358
4752
  if (!literal(target) || ![...objects.values()].every(literal)) return false;
4359
- 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(
4360
4754
  ts.isIdentifier(p.name) || ts.isStringLiteral(p.name) ? p.name.text : ""
4361
4755
  ));
4362
4756
  }
@@ -4420,7 +4814,7 @@ function prelude(request, marker) {
4420
4814
  function composeNextMiddleware(ts, file, source, guardImport) {
4421
4815
  const sf = parse(ts, file, source);
4422
4816
  if (!sf || /\b(?:psProtection|psBlocked|psRequest|getPatchstackProtection)\b/.test(source)) return null;
4423
- 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"]));
4424
4818
  if (found.length !== 1 || sf.statements.some((s) => ts.isExportAssignment(s) || ts.isExportDeclaration(s))) return null;
4425
4819
  const fn = found[0];
4426
4820
  const request = requestName(ts, fn);
@@ -4482,7 +4876,7 @@ function nextSourceWired(ts, file, source, guardImport, route = false) {
4482
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"));
4483
4877
  if (!imported) return false;
4484
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;
4485
- 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"]));
4486
4880
  if (!found.length || sf.statements.some((s) => ts.isExportDeclaration(s) || ts.isExportAssignment(s))) return false;
4487
4881
  if (route && methodExportCount(ts, sf) !== found.length) return false;
4488
4882
  const compact = (text) => text.replace(/\s+/g, "");
@@ -4517,17 +4911,48 @@ function nextSourceWired(ts, file, source, guardImport, route = false) {
4517
4911
 
4518
4912
  // src/protect/install/adapters/next.ts
4519
4913
  function middlewareInfo(cwd) {
4520
- const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
4521
- const relFile = candidates.find((rel) => existsSync5(join8(cwd, rel))) ?? (existsSync5(join8(cwd, "src")) ? "src/middleware.ts" : "middleware.ts");
4522
- 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
+ }
4523
4933
  }
4524
- function proxyFiles(cwd) {
4525
- 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;
4526
4951
  }
4527
4952
  function paths(cwd) {
4528
4953
  const mw = middlewareInfo(cwd);
4529
4954
  const ext = mw.relFile.endsWith(".js") ? "js" : "ts";
4530
- 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}`) };
4531
4956
  }
4532
4957
  function importFrom(file, guard) {
4533
4958
  const rel = relative3(dirname3(file), guard).replace(/\\/g, "/").replace(/\.ts$/, "");
@@ -4542,7 +4967,7 @@ function routes(cwd) {
4542
4967
  incomplete = true;
4543
4968
  return;
4544
4969
  }
4545
- const full = join8(cwd, rel);
4970
+ const full = join12(cwd, rel);
4546
4971
  if (lstatSync2(full).isSymbolicLink()) {
4547
4972
  incomplete = true;
4548
4973
  return;
@@ -4553,14 +4978,14 @@ function routes(cwd) {
4553
4978
  incomplete = true;
4554
4979
  continue;
4555
4980
  }
4556
- const child = join8(rel, entry.name);
4981
+ const child = join12(rel, entry.name);
4557
4982
  if (entry.isDirectory()) walk2(child);
4558
4983
  else if (entry.isFile() && /^route\.(?:tsx?|jsx?)$/.test(entry.name)) files.push(child);
4559
4984
  }
4560
4985
  };
4561
4986
  for (const root of ["app", "src/app"]) {
4562
- if (!existsSync5(join8(cwd, root))) continue;
4563
- 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()) {
4564
4989
  incomplete = true;
4565
4990
  continue;
4566
4991
  }
@@ -4574,53 +4999,62 @@ function routes(cwd) {
4574
4999
  }
4575
5000
  function customRouting(cwd, ts) {
4576
5001
  return ["js", "mjs", "ts", "cjs"].some((ext) => {
4577
- const file = join8(cwd, `next.config.${ext}`);
4578
- 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));
4579
5004
  });
4580
5005
  }
4581
5006
  function sharedGuardPresent(file) {
4582
- if (!existsSync5(file)) return false;
5007
+ if (!existsSync8(file)) return false;
4583
5008
  const source = read(file);
4584
5009
  return source.includes("export async function getPatchstackProtection") && source.includes("export async function screenPatchstackResponse") && source.includes('import { createProtection } from "@patchstack/connect/protect"');
4585
5010
  }
4586
5011
  function wire2(cwd, opts) {
4587
- const proxies = proxyFiles(cwd);
4588
- if (proxies.length) {
4589
- 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.`);
4590
5015
  return { ok: false, changed: [] };
4591
5016
  }
4592
5017
  const templates = templatesDir();
4593
5018
  const mw = paths(cwd);
4594
- ensureProjectDirectorySync(cwd, join8(cwd, mw.relDir));
5019
+ ensureProjectDirectorySync(cwd, join12(cwd, mw.relDir));
4595
5020
  const changed = [];
4596
- if (opts.demo || !existsSync5(join8(cwd, mw.rules))) {
4597
- 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));
4598
5023
  changed.push(mw.rules);
4599
5024
  }
4600
5025
  const ts = nextCompiler(cwd);
4601
- const guardPath = join8(cwd, mw.guard);
4602
- const guardConflict = existsSync5(guardPath) && !sharedGuardPresent(guardPath);
5026
+ const guardPath = join12(cwd, mw.guard);
5027
+ const guardConflict = existsSync8(guardPath) && !sharedGuardPresent(guardPath);
4603
5028
  const ensureGuard = () => {
4604
- if (existsSync5(guardPath)) return;
4605
- 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"));
4606
5034
  writeProjectFileSync(cwd, guardPath, mw.guard.endsWith(".js") ? ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText : source);
4607
5035
  if (!opts.demo) bakeSiteUuid(cwd, mw.guard);
4608
5036
  changed.push(mw.guard);
4609
5037
  };
4610
- const existing = mw.exists ? read(join8(cwd, mw.relFile)) : "";
5038
+ const existing = mw.exists ? read(join12(cwd, mw.relFile)) : "";
4611
5039
  if (!mw.exists) {
4612
- 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));
4613
5046
  if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
4614
5047
  changed.push(mw.relFile);
4615
5048
  log(`scaffolded ${mw.relFile} (request-phase guard)`);
4616
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);
4617
5051
  log(`${mw.relFile} already has a Patchstack guard \u2014 left as-is`);
4618
5052
  if (ts && existing.includes(NEXT_MARKER)) ensureGuard();
4619
5053
  } else {
4620
5054
  const composed = ts && !guardConflict && !customRouting(cwd, ts) && composeNextMiddleware(ts, mw.relFile, existing, importFrom(mw.relFile, mw.guard));
4621
5055
  if (composed) {
4622
5056
  ensureGuard();
4623
- writeProjectFileSync(cwd, join8(cwd, mw.relFile), composed);
5057
+ writeProjectFileSync(cwd, join12(cwd, mw.relFile), composed);
4624
5058
  changed.push(mw.relFile);
4625
5059
  log(`composed ${mw.relFile}: request guard first; existing middleware keeps its original matcher scope`);
4626
5060
  } else {
@@ -4629,7 +5063,7 @@ function wire2(cwd, opts) {
4629
5063
  }
4630
5064
  const inventory = routes(cwd);
4631
5065
  for (const file of inventory.files) {
4632
- const source = read(join8(cwd, file));
5066
+ const source = read(join12(cwd, file));
4633
5067
  if (source.includes(ROUTE_MARKER)) {
4634
5068
  if (ts) ensureGuard();
4635
5069
  continue;
@@ -4637,7 +5071,7 @@ function wire2(cwd, opts) {
4637
5071
  const composed = ts && !guardConflict && /\.(?:ts|js)$/.test(file) && composeNextRoute(ts, file, source, importFrom(file, mw.guard));
4638
5072
  if (composed) {
4639
5073
  ensureGuard();
4640
- writeProjectFileSync(cwd, join8(cwd, file), composed);
5074
+ writeProjectFileSync(cwd, join12(cwd, file), composed);
4641
5075
  changed.push(file);
4642
5076
  log(`wired ${file} (request checks + response filtering)`);
4643
5077
  } else {
@@ -4650,25 +5084,21 @@ function wire2(cwd, opts) {
4650
5084
  return { ok: true, changed: [...new Set(changed)] };
4651
5085
  }
4652
5086
  function verify2(cwd) {
4653
- const proxies = proxyFiles(cwd);
4654
- if (proxies.length) return {
5087
+ const problem = entryProblem2(cwd);
5088
+ if (problem) return {
4655
5089
  wired: false,
4656
- checks: [{
4657
- label: "Next.js proxy wiring requires manual verification",
4658
- ok: false,
4659
- hint: `review ${proxies.join(", ")}; automatic proxy integration is not supported. Next.js cannot use middleware and proxy together.`
4660
- }]
5090
+ checks: [{ label: "Next.js request entry needs review", ok: false, hint: problem }]
4661
5091
  };
4662
5092
  const mw = paths(cwd);
4663
5093
  const ts = nextCompiler(cwd);
4664
- const source = mw.exists ? read(join8(cwd, mw.relFile)) : "";
4665
- 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));
4666
5096
  const legacy = source.includes("#region patchstack-next (") && source.includes("await protection.fetchGuard()(request)") && source.includes("if (blocked) return blocked") && source.includes('matcher: "/:path*"');
4667
5097
  const composed = ts && shared && !customRouting(cwd, ts) && nextSourceWired(ts, mw.relFile, source, importFrom(mw.relFile, mw.guard));
4668
5098
  const present2 = !!(legacy || composed);
4669
- const rulesPresent = existsSync5(join8(cwd, mw.rules));
5099
+ const rulesPresent = existsSync8(join12(cwd, mw.rules));
4670
5100
  const inventory = routes(cwd);
4671
- 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)));
4672
5102
  return {
4673
5103
  wired: present2 && rulesPresent && missing.length === 0 && !inventory.incomplete,
4674
5104
  checks: [
@@ -4707,51 +5137,63 @@ var nextAdapter = {
4707
5137
  };
4708
5138
 
4709
5139
  // src/protect/install/seam.ts
4710
- import { existsSync as existsSync6 } from "fs";
4711
- 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";
4712
5142
  function rulesRel(seamRel) {
4713
5143
  const d = dirname4(seamRel);
4714
5144
  return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
4715
5145
  }
4716
5146
  function wireSeam(cwd, opts, spec) {
4717
5147
  const templates = templatesDir();
4718
- const existing = spec.candidates.find((c) => existsSync6(join9(cwd, c)));
5148
+ const existing = spec.candidates.find((c) => existsSync9(join13(cwd, c)));
4719
5149
  const seamRel = existing ?? spec.target;
4720
- ensureProjectDirectorySync(cwd, dirname4(join9(cwd, seamRel)));
4721
- const rulesDst = join9(cwd, rulesRel(seamRel));
5150
+ ensureProjectDirectorySync(cwd, dirname4(join13(cwd, seamRel)));
5151
+ const rulesDst = join13(cwd, rulesRel(seamRel));
4722
5152
  const changed = [];
4723
- if (opts.demo || !existsSync6(rulesDst)) {
4724
- 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);
4725
5155
  changed.push(rulesRel(seamRel));
4726
5156
  }
4727
- const current = existing ? read(join9(cwd, existing)) : "";
5157
+ const current = existing ? read(join13(cwd, existing)) : "";
4728
5158
  if (existing && current.includes(spec.marker)) {
4729
- log(`${existing} already has the Patchstack guard \u2014 left as-is`);
5159
+ if (installTemplate(cwd, seamRel, spec.templateName)) changed.push(seamRel);
4730
5160
  return { ok: true, changed };
4731
5161
  }
4732
5162
  if (existing) {
4733
5163
  log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
4734
5164
  return { ok: true, changed };
4735
5165
  }
4736
- copyProjectFileSync(cwd, join9(templates, spec.templateName), join9(cwd, seamRel));
5166
+ copyProjectFileSync(cwd, join13(templates, spec.templateName), join13(cwd, seamRel));
4737
5167
  if (!opts.demo) bakeSiteUuid(cwd, seamRel);
4738
5168
  changed.push(seamRel);
4739
5169
  log(`scaffolded ${seamRel}`);
4740
5170
  return { ok: true, changed: [...new Set(changed)] };
4741
5171
  }
4742
5172
  function verifySeam(cwd, spec) {
4743
- const existing = spec.candidates.find((c) => existsSync6(join9(cwd, c)));
5173
+ const existing = spec.candidates.find((c) => existsSync9(join13(cwd, c)));
4744
5174
  const seamRel = existing ?? spec.target;
4745
- const present2 = existing ? read(join9(cwd, existing)).includes(spec.marker) : false;
4746
- 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)));
4747
5177
  return {
4748
5178
  wired: present2 && rulesPresent,
4749
5179
  checks: [
4750
- { 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})` },
4751
5181
  { label: "rules co-located with the guard", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
4752
5182
  ]
4753
5183
  };
4754
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
+ }
4755
5197
 
4756
5198
  // src/protect/install/adapters/sveltekit.ts
4757
5199
  var SPEC = {
@@ -4833,7 +5275,7 @@ var nuxtAdapter = {
4833
5275
 
4834
5276
  // src/protect/install/find-app.ts
4835
5277
  import { readFileSync as readFileSync8, readdirSync as readdirSync4, lstatSync as lstatSync3 } from "fs";
4836
- 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";
4837
5279
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
4838
5280
  "node_modules",
4839
5281
  "patchstack",
@@ -4865,7 +5307,7 @@ function findAppInstance(cwd, re) {
4865
5307
  }
4866
5308
  for (const name of entries) {
4867
5309
  if (SKIP_DIRS2.has(name) || name.startsWith(".")) continue;
4868
- const p = join10(dir, name);
5310
+ const p = join14(dir, name);
4869
5311
  let st;
4870
5312
  try {
4871
5313
  st = lstatSync3(p);
@@ -4924,7 +5366,7 @@ function declaredEntries(cwd) {
4924
5366
  const paths2 = /* @__PURE__ */ new Set();
4925
5367
  let pkg;
4926
5368
  try {
4927
- pkg = JSON.parse(readFileSync8(join10(cwd, "package.json"), "utf8"));
5369
+ pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
4928
5370
  } catch {
4929
5371
  return paths2;
4930
5372
  }
@@ -4950,17 +5392,17 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
4950
5392
  }
4951
5393
 
4952
5394
  // src/protect/install/register.ts
4953
- import { existsSync as existsSync9 } from "fs";
4954
- 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";
4955
5397
 
4956
5398
  // src/protect/install/generic.ts
4957
- import { readFileSync as readFileSync9, existsSync as existsSync8, readdirSync as readdirSync5, lstatSync as lstatSync4 } from "fs";
4958
- 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";
4959
5401
 
4960
5402
  // src/protect/install/source-scope.ts
4961
5403
  import { execFileSync } from "child_process";
4962
- import { existsSync as existsSync7 } from "fs";
4963
- 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";
4964
5406
 
4965
5407
  // src/protect/install/node-flags.ts
4966
5408
  var flagName = (token) => token.split("=")[0];
@@ -5250,8 +5692,8 @@ function moduleKindOf(filePath, packageIsModule) {
5250
5692
  }
5251
5693
  function resolvesToGuardModule(specifier, query) {
5252
5694
  const resolved = resolve2(dirname6(query.fromFile), specifier);
5253
- if (withoutModuleExtension(resolved) !== join11(query.guardDir, "guard")) return false;
5254
- 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)));
5255
5697
  if (existing.length === 0) return false;
5256
5698
  const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
5257
5699
  if (!query.strictExtensions) return true;
@@ -5260,7 +5702,7 @@ function resolvesToGuardModule(specifier, query) {
5260
5702
  if (query.kind === "cjs") return existing.includes("guard.js");
5261
5703
  return true;
5262
5704
  }
5263
- if (existsSync7(resolved)) return true;
5705
+ if (existsSync10(resolved)) return true;
5264
5706
  return query.kind === "ts" && extension === ".js" && existing.includes("guard.ts");
5265
5707
  }
5266
5708
  function withoutModuleExtension(filePath) {
@@ -5270,16 +5712,16 @@ function withoutModuleExtension(filePath) {
5270
5712
  // src/protect/install/generic.ts
5271
5713
  function packageIsEsm(cwd) {
5272
5714
  try {
5273
- return JSON.parse(read(join12(cwd, "package.json"))).type === "module";
5715
+ return JSON.parse(read(join16(cwd, "package.json"))).type === "module";
5274
5716
  } catch {
5275
5717
  return false;
5276
5718
  }
5277
5719
  }
5278
5720
  function genericDir(cwd) {
5279
- return existsSync8(join12(cwd, "src")) ? "src/patchstack" : "patchstack";
5721
+ return existsSync11(join16(cwd, "src")) ? "src/patchstack" : "patchstack";
5280
5722
  }
5281
5723
  function hasTypeScript(cwd) {
5282
- 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));
5283
5725
  }
5284
5726
  function genericGuardTarget(cwd) {
5285
5727
  if (hasTypeScript(cwd)) {
@@ -5293,7 +5735,7 @@ function genericGuardTarget(cwd) {
5293
5735
  function genericScaffoldFiles(cwd) {
5294
5736
  const dir = genericDir(cwd);
5295
5737
  const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
5296
- 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)));
5297
5739
  }
5298
5740
  function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
5299
5741
  const target = genericGuardTarget(cwd);
@@ -5301,15 +5743,15 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
5301
5743
  guardFile ??= target.file;
5302
5744
  const templates = templatesDir();
5303
5745
  const dir = genericDir(cwd);
5304
- const dst = join12(cwd, dir);
5746
+ const dst = join16(cwd, dir);
5305
5747
  ensureProjectDirectorySync(cwd, dst);
5306
- copyProjectFileSync(cwd, join12(templates, guardTemplate), join12(dst, guardFile));
5307
5748
  const guardRel = `${dir}/${guardFile}`;
5308
- const changed = [guardRel];
5309
- if (!opts.demo) bakeSiteUuid(cwd, guardRel);
5310
- const rulesDst = join12(dst, "rules.json");
5311
- if (opts.demo || !existsSync8(rulesDst)) {
5312
- 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);
5313
5755
  changed.push(`${dir}/rules.json`);
5314
5756
  }
5315
5757
  return { changed, dir };
@@ -5330,10 +5772,10 @@ function candidateEntries(cwd) {
5330
5772
  "app.ts",
5331
5773
  "app.js"
5332
5774
  ];
5333
- const hits = names.filter((n) => existsSync8(join12(cwd, n)));
5775
+ const hits = names.filter((n) => existsSync11(join16(cwd, n)));
5334
5776
  try {
5335
- const pkg = JSON.parse(read(join12(cwd, "package.json")));
5336
- 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);
5337
5779
  } catch {
5338
5780
  }
5339
5781
  return [...new Set(hits)];
@@ -5436,13 +5878,16 @@ function escapeForRegExp(value) {
5436
5878
  function genericVerify(cwd) {
5437
5879
  const dir = genericDir(cwd);
5438
5880
  const target = genericGuardTarget(cwd);
5439
- const present2 = GUARD_FILENAMES.find((name) => existsSync8(join12(cwd, dir, name)));
5881
+ const present2 = GUARD_FILENAMES.find((name) => existsSync11(join16(cwd, dir, name)));
5440
5882
  const scaffolded = present2 !== void 0;
5441
- 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));
5442
5886
  return {
5443
- wired: scaffolded && imported,
5887
+ wired: scaffolded && helperVerified && imported,
5444
5888
  checks: [
5445
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" },
5446
5891
  {
5447
5892
  label: "guard imported and called in a server entry",
5448
5893
  ok: imported,
@@ -5474,7 +5919,7 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
5474
5919
  function guardIsImported(cwd, guardPath) {
5475
5920
  const guardDir = dirname7(guardPath);
5476
5921
  const packageIsModule = packageIsEsm(cwd);
5477
- const root = existsSync8(join12(cwd, "src")) ? join12(cwd, "src") : cwd;
5922
+ const root = existsSync11(join16(cwd, "src")) ? join16(cwd, "src") : cwd;
5478
5923
  let found = false;
5479
5924
  const walk2 = (d, depth) => {
5480
5925
  if (found || depth > 8) return;
@@ -5487,7 +5932,7 @@ function guardIsImported(cwd, guardPath) {
5487
5932
  for (const name of entries) {
5488
5933
  if (found) return;
5489
5934
  if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
5490
- const p = join12(d, name);
5935
+ const p = join16(d, name);
5491
5936
  let st;
5492
5937
  try {
5493
5938
  st = lstatSync4(p);
@@ -5510,7 +5955,7 @@ function guardIsImported(cwd, guardPath) {
5510
5955
 
5511
5956
  // src/protect/install/register.ts
5512
5957
  var routeRegistration = (appVar) => new RegExp(
5513
- `^\\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*[,\\)])`,
5514
5959
  "m"
5515
5960
  );
5516
5961
  function routesBefore(source, appVar, guardIndex) {
@@ -5546,7 +5991,7 @@ function guardTarget(cwd, entryRel, spec) {
5546
5991
  }
5547
5992
  function packageType(cwd) {
5548
5993
  try {
5549
- return JSON.parse(read(join13(cwd, "package.json"))).type;
5994
+ return JSON.parse(read(join17(cwd, "package.json"))).type;
5550
5995
  } catch {
5551
5996
  return void 0;
5552
5997
  }
@@ -5563,7 +6008,7 @@ function wiringState(source, spec, appVar, fileRel, guard) {
5563
6008
  );
5564
6009
  const appIndex = maskedLines.findIndex((line) => spec.appRe.test(line));
5565
6010
  const anchor = spec.callAfter ? spec.callAfter(appVar) : null;
5566
- 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;
5567
6012
  return {
5568
6013
  importAtTopLevel: importIndex !== -1 && isTopLevelLine(code, importIndex),
5569
6014
  callInAppScope: callIndex !== -1 && inSameBlockAfter(code, appIndex, callIndex),
@@ -5582,8 +6027,8 @@ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
5582
6027
  }
5583
6028
  function namesGuardModule(specifier, fileRel, guard) {
5584
6029
  return resolvesToGuardModule(specifier, {
5585
- fromFile: join13(guard.cwd, fileRel),
5586
- guardDir: resolve3(join13(guard.cwd, guard.guardDir)),
6030
+ fromFile: join17(guard.cwd, fileRel),
6031
+ guardDir: resolve3(join17(guard.cwd, guard.guardDir)),
5587
6032
  kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
5588
6033
  strictExtensions: true
5589
6034
  });
@@ -5602,8 +6047,17 @@ function wireRegister(cwd, opts, spec) {
5602
6047
  }
5603
6048
  const target = guardTarget(cwd, entry.relPath, spec);
5604
6049
  const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
5605
- 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);
5606
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
+ }
5607
6061
  const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
5608
6062
  if (state.importAtTopLevel && state.callInAppScope) {
5609
6063
  log(
@@ -5630,17 +6084,35 @@ function wireRegister(cwd, opts, spec) {
5630
6084
  const lines = s.split("\n");
5631
6085
  if (!state.importAtTopLevel) {
5632
6086
  const lastImport = lastTopLevelImportLine(s);
5633
- 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;
5634
6090
  lines.splice(importIdx, 0, importLine);
5635
6091
  }
5636
6092
  const appIdx = lines.findIndex((l) => spec.appRe.test(l));
5637
6093
  if (appIdx !== -1 && !state.callInAppScope) {
5638
6094
  const preferred = spec.callAfter?.(entry.appVar);
5639
- 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;
5640
6096
  const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
5641
- 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"));
5642
6110
  }
5643
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
+ }
5644
6116
  writeProjectFileSync(cwd, p, patched, { encoding: "utf8" });
5645
6117
  if (spec.callAfter) {
5646
6118
  const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
@@ -5672,26 +6144,30 @@ function firstStatementLine(lines) {
5672
6144
  return i;
5673
6145
  }
5674
6146
  function serverIsGuarded(cwd, spec, server, guardDir) {
5675
- 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));
5676
6150
  const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
5677
6151
  if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
5678
6152
  return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
5679
6153
  }
5680
6154
  function verifyRegister(cwd, spec) {
5681
- const dir = existsSync9(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
6155
+ const dir = existsSync12(join17(cwd, "src")) ? "src/patchstack" : "patchstack";
5682
6156
  const entry = findAppInstance(cwd, spec.appRe);
5683
6157
  const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
5684
- const scaffolded = target ? existsSync9(join13(cwd, dir, target.file)) : false;
5685
- 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)) : "";
5686
6161
  const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
5687
6162
  const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
5688
6163
  const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
5689
6164
  const noEarlyRoutes = early.length === 0;
5690
6165
  const unguarded = (entry?.others ?? []).filter((other) => !serverIsGuarded(cwd, spec, other, dir)).map((other) => other.relPath);
5691
6166
  return {
5692
- wired: scaffolded && wired && noEarlyRoutes && unguarded.length === 0,
6167
+ wired: scaffolded && helperVerified && wired && noEarlyRoutes && unguarded.length === 0,
5693
6168
  checks: [
5694
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" },
5695
6171
  { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint },
5696
6172
  ...entry && entry.others.length > 0 ? [
5697
6173
  {
@@ -5772,8 +6248,8 @@ var expressAdapter = {
5772
6248
  };
5773
6249
 
5774
6250
  // src/protect/install/reporting.ts
5775
- import { existsSync as existsSync10 } from "fs";
5776
- import { join as join14 } from "path";
6251
+ import { existsSync as existsSync13 } from "fs";
6252
+ import { join as join18 } from "path";
5777
6253
 
5778
6254
  // src/protect/reporting-state.js
5779
6255
  var REPORTING_STATES = Object.freeze([
@@ -5831,9 +6307,9 @@ function explainReportingState(state) {
5831
6307
 
5832
6308
  // src/protect/install/reporting.ts
5833
6309
  function siteIdentity(cwd, env) {
5834
- const rc = join14(cwd, ".patchstackrc.json");
6310
+ const rc = join18(cwd, ".patchstackrc.json");
5835
6311
  let recorded;
5836
- if (existsSync10(rc)) {
6312
+ if (existsSync13(rc)) {
5837
6313
  try {
5838
6314
  recorded = JSON.parse(read(rc)).siteUuid;
5839
6315
  } catch {
@@ -5889,6 +6365,7 @@ function reportingChecks(cwd, env = process.env) {
5889
6365
  // src/protect/install/index.ts
5890
6366
  var ADAPTERS = [
5891
6367
  tanstackSupabaseAdapter,
6368
+ tanstackAdapter,
5892
6369
  nextAdapter,
5893
6370
  sveltekitAdapter,
5894
6371
  astroAdapter,
@@ -5913,7 +6390,10 @@ function runProtect(cwd, opts = {}) {
5913
6390
  adapter = ADAPTERS.find((a) => a.detect(cwd));
5914
6391
  if (adapter) {
5915
6392
  const result = adapter.wire(cwd, opts);
5916
- 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 };
5917
6397
  }
5918
6398
  } catch (err) {
5919
6399
  log(`adapter ${adapter?.name ?? ""} failed (${err?.message ?? err}); falling back to a generic guard`);
@@ -5928,7 +6408,7 @@ function runProtect(cwd, opts = {}) {
5928
6408
  return { status: "scaffolded", adapter: "generic", changed: [], plan: "" };
5929
6409
  }
5930
6410
  }
5931
- 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"]);
5932
6412
  function credentialNote(cwd, adapterName) {
5933
6413
  if (!RUNTIMES_WITHOUT_CONFIG_FILE.has(adapterName)) return [];
5934
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";
@@ -5982,7 +6462,7 @@ function runVerify(cwd) {
5982
6462
  }
5983
6463
 
5984
6464
  // src/widget.ts
5985
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
6465
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
5986
6466
  import path11 from "path";
5987
6467
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
5988
6468
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
@@ -6021,7 +6501,7 @@ ${indent}</body>`;
6021
6501
  }
6022
6502
  function findSourceShell(cwd) {
6023
6503
  for (const candidate of SOURCE_SHELL_CANDIDATES) {
6024
- if (existsSync11(path11.join(cwd, candidate))) {
6504
+ if (existsSync14(path11.join(cwd, candidate))) {
6025
6505
  return candidate;
6026
6506
  }
6027
6507
  }
@@ -6033,7 +6513,7 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
6033
6513
  return { shell: null, action: "no-shell" };
6034
6514
  }
6035
6515
  const file = path11.join(cwd, shell);
6036
- if (!existsSync11(file)) {
6516
+ if (!existsSync14(file)) {
6037
6517
  return { shell: null, action: "no-shell" };
6038
6518
  }
6039
6519
  const before = readFileSync10(file, "utf8");
@@ -6259,7 +6739,7 @@ function detectPackageManager(cwd) {
6259
6739
  return declared;
6260
6740
  }
6261
6741
  for (const { filename, pm } of PM_BY_LOCKFILE) {
6262
- if (existsSync12(path12.join(cwd, filename))) {
6742
+ if (existsSync15(path12.join(cwd, filename))) {
6263
6743
  return pm;
6264
6744
  }
6265
6745
  }
@@ -6356,7 +6836,7 @@ function resolveWidgetFileHint(cwd, framework) {
6356
6836
  ...GENERIC_WIDGET_FILES
6357
6837
  ];
6358
6838
  for (const candidate of candidates) {
6359
- if (existsSync12(path12.join(cwd, candidate))) {
6839
+ if (existsSync15(path12.join(cwd, candidate))) {
6360
6840
  return candidate;
6361
6841
  }
6362
6842
  }
@@ -6436,9 +6916,9 @@ async function collectGuideState(cwd) {
6436
6916
  hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
6437
6917
  installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
6438
6918
  // The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
6439
- // must retain. Bun skips npm-style pre/post scripts, so its build chain follows the same order.
6440
- prebuildWired: /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
6441
- 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"),
6442
6922
  widgetInstalled: widget.found,
6443
6923
  widgetTokenMatches: widget.uuidMatches,
6444
6924
  widgetOptOut,
@@ -6508,10 +6988,10 @@ function buildScriptLines(state) {
6508
6988
  const lines = [];
6509
6989
  if (!state.installScanWired) lines.push('"postinstall": "patchstack-connect scan"');
6510
6990
  if (state.hasBuildScript && !(state.prebuildWired && state.postbuildWired)) {
6511
- if (state.packageManager === "bun") {
6512
- 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"');
6513
6993
  } else {
6514
- if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan"');
6994
+ if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan && patchstack-connect map --upload"');
6515
6995
  if (!state.postbuildWired) lines.push('"postbuild": "patchstack-connect mark-build"');
6516
6996
  }
6517
6997
  }
@@ -6629,7 +7109,7 @@ import {
6629
7109
  } from "fs";
6630
7110
  import { tmpdir } from "os";
6631
7111
  import path13 from "path";
6632
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7112
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
6633
7113
  function baseFrom(manifestEndpoint) {
6634
7114
  const url = new URL(manifestEndpoint);
6635
7115
  const p = url.pathname.replace(/\/$/, "");
@@ -6639,7 +7119,7 @@ function baseFrom(manifestEndpoint) {
6639
7119
  return url.toString().replace(/\/$/, "");
6640
7120
  }
6641
7121
  function pendingPath(siteUuid, intent) {
6642
- const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
7122
+ const key = createHash3("sha256").update(siteUuid).digest("hex").slice(0, 16);
6643
7123
  return path13.join(tmpdir(), `patchstack-${intent}-${key}.json`);
6644
7124
  }
6645
7125
  function savePending(siteUuid, intent, pending) {
@@ -6915,8 +7395,8 @@ async function claim2(config, onPrompt, deps = {}) {
6915
7395
  import { relative as relative5 } from "path";
6916
7396
 
6917
7397
  // src/protect/install/runtime/entry.ts
6918
- import { existsSync as existsSync13, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
6919
- 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";
6920
7400
  var CONVENTIONAL = [
6921
7401
  "server.js",
6922
7402
  "server.mjs",
@@ -6942,7 +7422,7 @@ var CONVENTIONAL = [
6942
7422
  var LOADABLE = /\.(?:js|mjs|cjs)$/;
6943
7423
  function readPackage2(cwd) {
6944
7424
  try {
6945
- const parsed = JSON.parse(readFileSync13(join15(cwd, "package.json"), "utf8"));
7425
+ const parsed = JSON.parse(readFileSync13(join19(cwd, "package.json"), "utf8"));
6946
7426
  return parsed !== null && typeof parsed === "object" ? parsed : null;
6947
7427
  } catch {
6948
7428
  return null;
@@ -7012,14 +7492,14 @@ function resolveEntry(cwd) {
7012
7492
  }
7013
7493
  return {
7014
7494
  kind: "unavailable",
7015
- 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"
7016
7496
  };
7017
7497
  }
7018
7498
 
7019
7499
  // src/protect/install/runtime/probe.ts
7020
7500
  import { spawn } from "child_process";
7021
- import { createHash as createHash3, randomBytes } from "crypto";
7022
- import { existsSync as existsSync14 } from "fs";
7501
+ import { createHash as createHash4, randomBytes } from "crypto";
7502
+ import { existsSync as existsSync17 } from "fs";
7023
7503
  import { request as httpRequest } from "http";
7024
7504
  import { request as httpsRequest, Agent as HttpsAgent } from "https";
7025
7505
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -7036,7 +7516,7 @@ var ANSWER_LIMIT = 8192;
7036
7516
  var LOOPBACK = ["127.0.0.1", "::1"];
7037
7517
  var ENDING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
7038
7518
  function expectedAnswerFor(challenge) {
7039
- return createHash3("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
7519
+ return createHash4("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
7040
7520
  }
7041
7521
  function preloadPath() {
7042
7522
  const candidates = [
@@ -7047,7 +7527,7 @@ function preloadPath() {
7047
7527
  ];
7048
7528
  for (const candidate of candidates) {
7049
7529
  const path17 = fileURLToPath2(candidate);
7050
- if (existsSync14(path17)) return path17;
7530
+ if (existsSync17(path17)) return path17;
7051
7531
  }
7052
7532
  return null;
7053
7533
  }
@@ -7397,12 +7877,12 @@ function formatRuntimeCheck(report) {
7397
7877
  import path14 from "path";
7398
7878
 
7399
7879
  // src/map/ts-loader.ts
7400
- import { createRequire as createRequire2 } from "module";
7880
+ import { createRequire as createRequire5 } from "module";
7401
7881
  import { pathToFileURL } from "url";
7402
- import { join as join16 } from "path";
7882
+ import { join as join20 } from "path";
7403
7883
  async function loadTypeScript(cwd) {
7404
7884
  try {
7405
- const req = createRequire2(pathToFileURL(join16(cwd, "package.json")));
7885
+ const req = createRequire5(pathToFileURL(join20(cwd, "package.json")));
7406
7886
  const resolved = req.resolve("typescript");
7407
7887
  const mod = await import(pathToFileURL(resolved).href);
7408
7888
  return mod.default ?? mod;
@@ -7418,7 +7898,7 @@ async function loadTypeScript(cwd) {
7418
7898
 
7419
7899
  // src/map/extract.ts
7420
7900
  import { readFileSync as readFileSync16, realpathSync as realpathSync6 } from "fs";
7421
- import { createHash as createHash5 } from "crypto";
7901
+ import { createHash as createHash6 } from "crypto";
7422
7902
  import { relative as relative7 } from "path";
7423
7903
 
7424
7904
  // src/map/bindings.ts
@@ -7548,7 +8028,7 @@ function npmPackageOf(spec) {
7548
8028
  }
7549
8029
 
7550
8030
  // src/map/sinks.ts
7551
- import { createHash as createHash4 } from "crypto";
8031
+ import { createHash as createHash5 } from "crypto";
7552
8032
 
7553
8033
  // src/map/capabilities.ts
7554
8034
  var PACKAGE_SINK_MODELS = [
@@ -7947,7 +8427,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
7947
8427
  return table?.[index] ?? "unknown";
7948
8428
  }
7949
8429
  function sinkId(s, owner) {
7950
- 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);
7951
8431
  }
7952
8432
  function dedupeSinks(sinks, owner) {
7953
8433
  const seen = /* @__PURE__ */ new Set();
@@ -8969,11 +9449,11 @@ function capEndpoint(e, limits) {
8969
9449
  drop((f) => kept.has(byIndex.get(f)));
8970
9450
  }
8971
9451
  const ranked = [...flows].sort((a, b) => keepPriority(b) - keepPriority(a));
8972
- const pick = (items, limit, idOf) => {
9452
+ const pick = (items, limit, idOf2) => {
8973
9453
  if (items.length <= limit) return { kept: items, omitted: 0 };
8974
9454
  const wanted = /* @__PURE__ */ new Set();
8975
9455
  for (const f of ranked) {
8976
- const id = idOf(f);
9456
+ const id = idOf2(f);
8977
9457
  if (id !== void 0 && wanted.size < limit) wanted.add(id);
8978
9458
  }
8979
9459
  const chosen = new Set(items.filter((it) => it.id !== void 0 && wanted.has(it.id)));
@@ -9073,7 +9553,7 @@ function withNote(doc) {
9073
9553
 
9074
9554
  // src/map/module-graph.ts
9075
9555
  import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
9076
- 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";
9077
9557
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
9078
9558
  function createModuleGraph(ts, opts) {
9079
9559
  const cache = /* @__PURE__ */ new Map();
@@ -9151,7 +9631,7 @@ function resolveRelativeModule(fromFile, spec) {
9151
9631
  }
9152
9632
  candidates.push(base);
9153
9633
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
9154
- 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));
9155
9635
  for (const c of candidates) {
9156
9636
  try {
9157
9637
  if (statSync7(c).isFile()) return c;
@@ -9441,7 +9921,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
9441
9921
 
9442
9922
  // src/map/imports.ts
9443
9923
  import { readFileSync as readFileSync15 } from "fs";
9444
- import { join as join18 } from "path";
9924
+ import { join as join23 } from "path";
9445
9925
  var MAX_SITES2 = 5;
9446
9926
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
9447
9927
  function isNpmPackageName(pkg) {
@@ -9452,7 +9932,7 @@ function readPathAliases(cwd) {
9452
9932
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
9453
9933
  let paths2;
9454
9934
  try {
9455
- const raw = readFileSync15(join18(cwd, name), "utf8");
9935
+ const raw = readFileSync15(join23(cwd, name), "utf8");
9456
9936
  const parsed = JSON.parse(stripJsonComments(raw));
9457
9937
  paths2 = parsed.compilerOptions?.paths;
9458
9938
  } catch {
@@ -9719,7 +10199,7 @@ async function extractInputMap(cwd, ts, options = {}) {
9719
10199
  let sourceBytes = 0;
9720
10200
  const calls = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
9721
10201
  const startedAt = Date.now();
9722
- const entryFiles = /* @__PURE__ */ new Set();
10202
+ const entryFiles2 = /* @__PURE__ */ new Set();
9723
10203
  const hopTargets = /* @__PURE__ */ new Set();
9724
10204
  for (const file of files) {
9725
10205
  try {
@@ -9745,8 +10225,8 @@ async function extractInputMap(cwd, ts, options = {}) {
9745
10225
  continue;
9746
10226
  }
9747
10227
  parsed++;
9748
- entryFiles.add(file);
9749
- 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);
9750
10230
  const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
9751
10231
  const bindings = buildModuleBindings(sf, ts);
9752
10232
  for (const specifier of bindings.imports) {
@@ -9799,7 +10279,7 @@ async function extractInputMap(cwd, ts, options = {}) {
9799
10279
  let hopParsed = 0;
9800
10280
  let hopFailures = 0;
9801
10281
  for (const target of hopTargets) {
9802
- if (entryFiles.has(target)) continue;
10282
+ if (entryFiles2.has(target)) continue;
9803
10283
  try {
9804
10284
  const text = readFileSync16(target, "utf8");
9805
10285
  const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
@@ -9965,7 +10445,7 @@ function getStringFlag(flags, name) {
9965
10445
 
9966
10446
  // src/build-stamp.ts
9967
10447
  import { lstatSync as lstatSync5, readFileSync as readFileSync17, readdirSync as readdirSync7, realpathSync as realpathSync7 } from "fs";
9968
- 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";
9969
10449
  var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
9970
10450
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
9971
10451
  var MAX_DEPTH2 = 6;
@@ -9983,7 +10463,7 @@ function guardImportsRulesFile(dir, rulesName) {
9983
10463
  }
9984
10464
  for (const name of siblings) {
9985
10465
  if (!GUARD_SOURCE.test(name)) continue;
9986
- const path17 = join19(dir, name);
10466
+ const path17 = join24(dir, name);
9987
10467
  try {
9988
10468
  const entry = lstatSync5(path17);
9989
10469
  if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
@@ -10026,7 +10506,7 @@ function findRulesFile(cwd) {
10026
10506
  }
10027
10507
  for (const name of entries) {
10028
10508
  if (name.startsWith(".")) continue;
10029
- const path17 = join19(dir, name);
10509
+ const path17 = join24(dir, name);
10030
10510
  let entry;
10031
10511
  try {
10032
10512
  entry = lstatSync5(path17);
@@ -10104,6 +10584,8 @@ function applyBuildStamp(cwd, id) {
10104
10584
  }
10105
10585
 
10106
10586
  // src/build-hook.ts
10587
+ import { readFileSync as readFileSync18 } from "fs";
10588
+ import { join as join25 } from "path";
10107
10589
  var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
10108
10590
  "preinstall",
10109
10591
  "install",
@@ -10117,11 +10599,16 @@ function isInstallOrBuildHook(env = process.env) {
10117
10599
  const event = env.npm_lifecycle_event;
10118
10600
  return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
10119
10601
  }
10120
- function isPreBundleBuildHook(env = process.env) {
10602
+ function isPreBundleBuildHook(env = process.env, cwd = process.cwd()) {
10121
10603
  const event = env.npm_lifecycle_event;
10122
10604
  if (event === "prebuild") return true;
10123
10605
  if (event !== "build") return false;
10124
- 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
+ }
10125
10612
  }
10126
10613
  function undeliveredReportLines(err, config, cwd) {
10127
10614
  const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
@@ -10145,7 +10632,7 @@ function undeliveredReportLines(err, config, cwd) {
10145
10632
  }
10146
10633
 
10147
10634
  // src/input-map-id.ts
10148
- import { createHash as createHash6 } from "crypto";
10635
+ import { createHash as createHash7 } from "crypto";
10149
10636
  var NonCanonicalInputMap = class extends Error {
10150
10637
  };
10151
10638
  function canonicalString(value) {
@@ -10189,27 +10676,32 @@ function inputMapBuildId(map) {
10189
10676
  material = { ...mapDocument, coverage: stableCoverage };
10190
10677
  } else material = mapDocument;
10191
10678
  }
10192
- 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");
10193
10680
  }
10194
10681
 
10195
10682
  // src/map-command.ts
10196
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;
10197
10688
  const cwd = getStringFlag(flags, "dir") ?? process.cwd();
10689
+ if (options.setup) applyBuildStamp(cwd, null);
10198
10690
  const { map, error } = await buildInputMap(cwd, {
10199
10691
  followSymlinks: flags.get("follow-symlinks") === true
10200
10692
  });
10201
10693
  if (!map) {
10202
- console.error(`patchstack: ${error}`);
10203
- return 1;
10694
+ log2(`patchstack: ${error}`);
10695
+ return { code: isPreBundleBuildHook() ? 0 : 1, error: error ?? "could not analyse the project" };
10204
10696
  }
10205
10697
  const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
10206
10698
  const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
10207
10699
  const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
10208
10700
  const c = map.coverage;
10209
- console.error(
10701
+ log2(
10210
10702
  `patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
10211
10703
  );
10212
- console.error(
10704
+ log2(
10213
10705
  // All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
10214
10706
  // simply contain no server entry point (most of a project is client code). Only `skipped` is a
10215
10707
  // failure to analyse.
@@ -10222,14 +10714,14 @@ async function runMap(flags) {
10222
10714
  const ambiguous = c2.callsAmbiguous ?? 0;
10223
10715
  const denominator = dependency + ambiguous;
10224
10716
  const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
10225
- console.error(
10717
+ log2(
10226
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.`
10227
10719
  );
10228
10720
  }
10229
10721
  const imported = map.imports ?? [];
10230
10722
  if (imported.length > 0) {
10231
10723
  const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
10232
- console.error(
10724
+ log2(
10233
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.`
10234
10726
  );
10235
10727
  }
@@ -10237,62 +10729,64 @@ async function runMap(flags) {
10237
10729
  const out = getStringFlag(flags, "out");
10238
10730
  if (out) {
10239
10731
  atomicWriteFileSync(path14.resolve(out), json, { encoding: "utf8" });
10240
- console.error(`patchstack: wrote ${out}`);
10732
+ log2(`patchstack: wrote ${out}`);
10241
10733
  } else if (flags.get("upload") !== true) {
10242
10734
  console.log(json);
10243
10735
  }
10244
10736
  if (flags.get("upload") === true) {
10245
10737
  const problems = ingestProblems(map);
10246
10738
  if (problems.length > 0) {
10247
- console.error(`patchstack: did not upload the attack surface \u2014 it cannot be fitted to the size Patchstack accepts (${problems.join("; ")}).`);
10248
- 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("; ") };
10249
10741
  }
10250
10742
  if (map.endpoints.length === 0) {
10251
- console.error(
10743
+ log2(
10252
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."
10253
10745
  );
10254
10746
  }
10255
- const config = await resolveConfig({
10747
+ const config = options.config ?? await resolveConfig({
10256
10748
  cwd,
10257
10749
  cliSiteUuid: getStringFlag(flags, "site-uuid"),
10258
10750
  cliEndpoint: getStringFlag(flags, "endpoint")
10259
10751
  });
10260
10752
  let buildId = null;
10261
- if (isPreBundleBuildHook()) {
10753
+ if (options.setup || isPreBundleBuildHook()) {
10262
10754
  const candidate = inputMapBuildId(map);
10263
10755
  const stamp = applyBuildStamp(cwd, candidate);
10264
10756
  if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
10265
10757
  buildId = candidate;
10266
- console.error(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
10758
+ log2(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
10267
10759
  } else {
10268
10760
  const reason = stamp.kind === "skipped" ? stamp.reason : "the rules file did not retain the map identity";
10269
- console.error(
10761
+ log2(
10270
10762
  `patchstack: could not bind this map to the runtime guard \u2014 ${reason}. Rules generated from these coordinates will detect only, not block.`
10271
10763
  );
10272
10764
  }
10273
10765
  } else {
10274
- console.error(
10766
+ log2(
10275
10767
  "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."
10276
10768
  );
10277
10769
  }
10278
10770
  const outcome = await postInputMap(config, map, buildId);
10279
10771
  if (outcome.result === "stored") {
10280
- console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
10772
+ log2(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
10281
10773
  } else if (outcome.result === "unchanged") {
10282
- console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
10774
+ log2(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
10283
10775
  } else if (outcome.result === "skipped") {
10284
- console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
10776
+ log2(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
10285
10777
  } else {
10286
- console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
10778
+ log2(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
10287
10779
  }
10780
+ return { code: 0, endpoints: map.endpoints.length, buildId, upload: outcome };
10288
10781
  }
10289
- return 0;
10782
+ return { code: 0, endpoints: map.endpoints.length };
10290
10783
  }
10291
10784
 
10292
10785
  // src/setup.ts
10293
- import { readFileSync as readFileSync18 } from "fs";
10786
+ import { readFileSync as readFileSync19 } from "fs";
10294
10787
  import path15 from "path";
10295
10788
  var SCAN_COMMAND = "patchstack-connect scan";
10789
+ var MAP_COMMAND = "patchstack-connect map --upload";
10296
10790
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
10297
10791
  function setupProtection(cwd) {
10298
10792
  const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
@@ -10312,12 +10806,65 @@ function prependHook(existing, command) {
10312
10806
  if (existing === void 0 || existing.trim().length === 0) return command;
10313
10807
  const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10314
10808
  if (new RegExp(`^\\s*${escaped}\\s*(?:;|\\|\\|)`).test(existing)) return existing;
10315
- const remaining = existing.split(/\s*&&\s*/).filter((part) => part.trim() !== command && part.trim().length > 0);
10809
+ const remaining = splitAndChain(existing).filter((part) => part.trim() !== command && part.trim().length > 0);
10316
10810
  return [command, ...remaining].join(" && ");
10317
10811
  }
10812
+ function splitAndChain(source) {
10813
+ const parts = [];
10814
+ let start = 0;
10815
+ let quote = "";
10816
+ const groups = [];
10817
+ for (let i = 0; i < source.length; i++) {
10818
+ const char = source[i];
10819
+ if (char === "\\" && quote !== "'") {
10820
+ i++;
10821
+ continue;
10822
+ }
10823
+ if (quote === "'" || quote === "`") {
10824
+ if (char === quote) quote = quote === "`" ? groups.pop().quote : "";
10825
+ continue;
10826
+ }
10827
+ if (char === "`") {
10828
+ groups.push({ close: "`", quote });
10829
+ quote = "`";
10830
+ continue;
10831
+ }
10832
+ if (char === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
10833
+ groups.push({ close: source[++i] === "(" ? ")" : "}", quote });
10834
+ quote = "";
10835
+ continue;
10836
+ }
10837
+ if (quote) {
10838
+ if (char === quote) quote = "";
10839
+ continue;
10840
+ }
10841
+ if (char === '"' || char === "'") {
10842
+ quote = char;
10843
+ continue;
10844
+ }
10845
+ if (char === "#" && (i === 0 || /\s/.test(source[i - 1]))) {
10846
+ const newline = source.indexOf("\n", i);
10847
+ if (newline === -1) break;
10848
+ i = newline;
10849
+ continue;
10850
+ }
10851
+ if (char === "(" || char === "{") groups.push({ close: char === "(" ? ")" : "}", quote: "" });
10852
+ if (char === groups.at(-1)?.close) quote = groups.pop().quote;
10853
+ if (groups.length === 0 && char === "&" && source[i + 1] === "&") {
10854
+ parts.push(source.slice(start, i).trim());
10855
+ start = ++i + 1;
10856
+ }
10857
+ }
10858
+ parts.push(source.slice(start).trim());
10859
+ return parts;
10860
+ }
10861
+ function finishWithMap(existing) {
10862
+ if (/(?:^|&&|;)\s*patchstack-connect map --upload\s*$/.test(existing)) return existing;
10863
+ return `${existing} && ${MAP_COMMAND}`;
10864
+ }
10318
10865
  function wireBuildScripts(cwd, packageManager) {
10319
10866
  const target = path15.join(cwd, "package.json");
10320
- const raw = readFileSync18(target, "utf8");
10867
+ const raw = readFileSync19(target, "utf8");
10321
10868
  const pkg = JSON.parse(raw);
10322
10869
  const scripts = pkg.scripts ?? {};
10323
10870
  const build = scripts.build;
@@ -10331,8 +10878,11 @@ function wireBuildScripts(cwd, packageManager) {
10331
10878
  };
10332
10879
  }
10333
10880
  scripts.postinstall = postinstall;
10334
- } else if (packageManager === "bun") {
10881
+ } else if (packageManager !== "npm") {
10335
10882
  let nextBuild = prependHook(build, SCAN_COMMAND);
10883
+ if (!/^\s*patchstack-connect scan\s*&&\s*patchstack-connect map --upload(?:\s*(?:&&|;)|\s*$)/.test(nextBuild)) {
10884
+ nextBuild = nextBuild.replace(SCAN_COMMAND, `${SCAN_COMMAND} && ${MAP_COMMAND}`);
10885
+ }
10336
10886
  if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
10337
10887
  nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
10338
10888
  }
@@ -10346,7 +10896,7 @@ function wireBuildScripts(cwd, packageManager) {
10346
10896
  scripts.postinstall = postinstall;
10347
10897
  scripts.build = nextBuild;
10348
10898
  } else {
10349
- const prebuild = prependHook(scripts.prebuild, SCAN_COMMAND);
10899
+ const prebuild = finishWithMap(prependHook(scripts.prebuild, SCAN_COMMAND));
10350
10900
  const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
10351
10901
  if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
10352
10902
  return {
@@ -10371,95 +10921,1889 @@ function wireBuildScripts(cwd, packageManager) {
10371
10921
  detail: "added a dependency-install scan; package.json has no build script."
10372
10922
  };
10373
10923
  }
10374
- return packageManager === "bun" ? {
10924
+ return packageManager !== "npm" ? {
10375
10925
  changed: true,
10376
10926
  strategy: "build-chain",
10377
- detail: "added a dependency-install scan and chained scan/mark-build around the build."
10927
+ detail: "added a dependency-install scan and chained scan/map-upload/mark-build around the build."
10378
10928
  } : {
10379
10929
  changed: true,
10380
10930
  strategy: "lifecycle-hooks",
10381
- detail: "added scans to postinstall/prebuild and mark-build to postbuild."
10931
+ detail: "added scans to postinstall/prebuild, map upload before bundling, and mark-build to postbuild."
10382
10932
  };
10383
10933
  }
10384
10934
 
10385
- // src/cli.ts
10386
- var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
10935
+ // src/setup-sync.ts
10936
+ import { join as join26 } from "path";
10387
10937
 
10388
- Usage:
10389
- patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
10390
- If no UUID is configured, the server
10391
- provisions one and we persist it. After a
10392
- successful post it also adds/updates the
10393
- Patchstack Connector <script> tag in the root
10394
- HTML shell (index.html, public/index.html,
10395
- or src/app.html) \u2014 opt out with
10396
- "widget": false in .patchstackrc.json.
10397
- Run as a postinstall/prebuild/build hook, a
10398
- report Patchstack cannot accept is printed
10399
- and exits 0 so the build goes on; run
10400
- directly, the same failure exits 1
10401
- patchstack-connect setup [options] Finish the bounded project setup: run scan,
10402
- manage the widget, install + verify runtime
10403
- protection, and wire dependency/build scans.
10404
- Never runs the project build
10405
- patchstack-connect map [--dir <p>] [--out <f>] [--upload]
10406
- Map the app's attack surface: entry points, the
10407
- inputs each reads, the sinks it can reach, and
10408
- evidence-backed input\u2192sink flows (each labelled
10409
- with how the link was established). Best-effort static
10410
- analysis \u2014 reports the DETECTED surface, with
10411
- coverage counters. Prints JSON (--out writes a
10412
- file; a pre-bundle --upload also stamps the existing
10413
- guard rules file; --follow-symlinks leaves the project dir).
10414
- Uses the app's own TypeScript
10415
- patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
10416
- with an existing site UUID
10417
- patchstack-connect status [options] Show current configuration and whether the
10418
- site still exists on Patchstack (active /
10419
- removed)
10420
- patchstack-connect uninstall [options] Signal Patchstack that this package is being
10421
- removed from the project. An unclaimed site
10422
- record is deleted; a claimed site is flagged
10423
- for its owner to remove in the dashboard.
10424
- Does NOT touch local files \u2014 see the
10425
- "Uninstalling" steps in AGENT-INSTALL.md
10426
- patchstack-connect mark-build [options] Stamp built HTML with a production flag +
10427
- build fingerprint, and ensure the widget
10428
- tag in built pages (run as a postbuild step).
10429
- The flag marks a page as the LIVE site, so it
10430
- is stamped only on a production build; a local
10431
- or preview build gets the widget tag and no
10432
- marker (and any stale marker is removed).
10433
- Looks in the framework's own output dir, an
10434
- --output the build script names, then dist/,
10435
- build/, out/, .output/public/, _site/ \u2014 inside
10436
- the project only. --dir <path> overrides.
10437
- patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
10438
- guard). Auto-wires supported server stacks;
10439
- for others it scaffolds a
10440
- generic guard + prints a wiring plan.
10441
- --demo seeds a broad sample rule set (for
10442
- demonstrations, not production).
10443
- --check verifies the guard is wired (exit 1
10444
- if not) \u2014 for the wire-then-verify loop.
10445
- It reads the app's source and never runs it.
10446
- --check --runtime additionally STARTS THE APP
10447
- on a loopback port and sends it one request,
10448
- to establish that a request reaches the
10449
- guard. Opt-in, and the only mode that runs
10450
- the app. Exit 0 traversed, 1 a listener
10451
- answered instead of the guard, 2 could not
10452
- be established (e.g. a TypeScript or
10453
- framework entry this must not start).
10454
- patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
10455
- walkthrough: verify the vulnerable package,
10456
- scan it, wait for live rule 18843, install +
10457
- verify the guard, and print test requests.
10458
- Does not install vulnerable dependencies.
10459
- patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
10460
- preparing, running, proving, and cleaning up
10461
- the production-backed local demo.
10462
- patchstack-connect guide [--full] Show this project's setup status (what's done,
10938
+ // src/protect/rules/contract.js
10939
+ var SOURCES = Object.freeze({
10940
+ raw: Object.freeze({ keyed: false }),
10941
+ all: Object.freeze({ keyed: false }),
10942
+ false: Object.freeze({ keyed: false }),
10943
+ rules: Object.freeze({ keyed: false, group: true }),
10944
+ get: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10945
+ post: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10946
+ request: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10947
+ cookie: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
10948
+ // A bare `files.<name>` resolves to the filename; an optional trailing attribute selects another part
10949
+ // of the upload. Optional, not required — filename-scoped rules use the bare form.
10950
+ files: Object.freeze({
10951
+ keyed: true,
10952
+ keys: "any",
10953
+ wildcard: true,
10954
+ optional_key_suffixes: Object.freeze(["content", "filename", "type"])
10955
+ }),
10956
+ server: Object.freeze({
10957
+ keyed: true,
10958
+ keys: Object.freeze([
10959
+ "REQUEST_URI",
10960
+ "REQUEST_METHOD",
10961
+ "HTTP_USER_AGENT",
10962
+ "HTTP_REFERER",
10963
+ "HTTP_HOST",
10964
+ "REMOTE_ADDR",
10965
+ "ip",
10966
+ "CONTENT_TYPE",
10967
+ "CONTENT_LENGTH"
10968
+ ]),
10969
+ key_prefixes: Object.freeze(["HTTP_"])
10970
+ }),
10971
+ response: Object.freeze({
10972
+ keyed: true,
10973
+ keys: Object.freeze(["status", "body", "headers"]),
10974
+ key_prefixes: Object.freeze(["header."])
10975
+ }),
10976
+ egress: Object.freeze({ keyed: true, keys: Object.freeze(["url", "host", "method"]) })
10977
+ });
10978
+ var MATCH_TYPES = Object.freeze([
10979
+ "array_in_array",
10980
+ "array_key_value",
10981
+ "contains",
10982
+ "cors_reflected",
10983
+ "cross_origin",
10984
+ "ctype_alnum",
10985
+ "ctype_digit",
10986
+ "ctype_special",
10987
+ "equals",
10988
+ "equals_strict",
10989
+ "hostname",
10990
+ "in_array",
10991
+ "inline_js_xss",
10992
+ "inline_xss",
10993
+ "internal_host",
10994
+ "is_numeric",
10995
+ "isset",
10996
+ "jwt_claim_equals",
10997
+ "less_than",
10998
+ "more_than",
10999
+ "not_contains",
11000
+ "not_in_array",
11001
+ "off_origin",
11002
+ "quotes",
11003
+ "regex",
11004
+ "stripos"
11005
+ ]);
11006
+ var PARAMETERLESS_MATCH_TYPES = Object.freeze(["cross_origin", "off_origin", "cors_reflected"]);
11007
+ var OPERANDLESS_MATCH_TYPES = Object.freeze([
11008
+ "isset",
11009
+ "quotes",
11010
+ "is_numeric",
11011
+ "ctype_alnum",
11012
+ "ctype_digit",
11013
+ "ctype_special",
11014
+ "inline_xss",
11015
+ "inline_js_xss",
11016
+ "internal_host",
11017
+ "cross_origin",
11018
+ "off_origin",
11019
+ "cors_reflected"
11020
+ ]);
11021
+ var OPERAND_SHAPES = Object.freeze([
11022
+ "positive-number",
11023
+ "boolean",
11024
+ "non-empty-string",
11025
+ "regex-literal",
11026
+ "string-or-string-list",
11027
+ "scalar",
11028
+ "string",
11029
+ "numeric",
11030
+ "scalar-or-non-empty-list",
11031
+ "non-empty-list",
11032
+ "object",
11033
+ "non-empty-object",
11034
+ "non-empty-string-list",
11035
+ "header-name-map",
11036
+ "header-name-list",
11037
+ "cookie-flags"
11038
+ ]);
11039
+ var COOKIE_FLAGS = Object.freeze(["httpOnly", "secure", "sameSite"]);
11040
+ var SAME_SITE_VALUES = Object.freeze(["Strict", "Lax", "None"]);
11041
+ var HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
11042
+ var REGEX_LITERAL = /^\/(.+?)\/([gimsuy]*)$/s;
11043
+ var MATCH_OPERANDS = Object.freeze({
11044
+ array_key_value: Object.freeze({
11045
+ required: Object.freeze(["key", "match"]),
11046
+ shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
11047
+ }),
11048
+ // Decides on a JWT's DECODED payload rather than its shape, and yields the matching token spans so
11049
+ // `redact` masks only those tokens. `claim` names an own, top-level claim; `value` is compared with
11050
+ // exact string equality. Anything not positively identified — undecodable, not JSON, not an object,
11051
+ // oversized, claim missing, claim not a string, or a different value — is a no-match.
11052
+ jwt_claim_equals: Object.freeze({
11053
+ required: Object.freeze(["claim", "value"]),
11054
+ shapes: Object.freeze({ claim: "non-empty-string", value: "non-empty-string" })
11055
+ }),
11056
+ regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
11057
+ contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11058
+ stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11059
+ not_contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11060
+ // A hostname is compared to `new URL(value).hostname`, so an empty one only ever equals the empty
11061
+ // hostname of an opaque-scheme URL (`file:`, `mailto:`, `data:`) — never a host, and a false positive
11062
+ // on those. The other three are the list-membership family: an empty list makes `in_array` and
11063
+ // `array_in_array` inert and `not_in_array` true for every value.
11064
+ hostname: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
11065
+ in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
11066
+ not_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
11067
+ array_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-list" }) }),
11068
+ // The comparison family. `equals` coerces, so any scalar can match. `equals_strict` does not, and its
11069
+ // subject is always a string by the time it is compared — so a number operand is unconditionally
11070
+ // inert, which is the trap the engine's own note about `'1' !== 1` describes.
11071
+ equals: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar" }) }),
11072
+ equals_strict: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "string" }) }),
11073
+ less_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) }),
11074
+ more_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) })
11075
+ });
11076
+ function operandShapeProblem(shape, value) {
11077
+ const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
11078
+ const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
11079
+ const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
11080
+ const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
11081
+ const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
11082
+ const tooLong = stringLimit(value);
11083
+ if (tooLong) return tooLong;
11084
+ const tooMany = listLimit(value);
11085
+ if (tooMany) return tooMany;
11086
+ if (Array.isArray(value)) {
11087
+ const longMember = value.find((member) => stringLimit(member) !== null);
11088
+ if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
11089
+ }
11090
+ switch (shape) {
11091
+ case "positive-number":
11092
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
11093
+ case "boolean":
11094
+ return typeof value === "boolean" ? null : "must be true or false";
11095
+ case "non-empty-string":
11096
+ return isFilledString(value) ? null : "must be a non-empty string";
11097
+ case "regex-literal":
11098
+ if (!isFilledString(value)) return "must be a non-empty string";
11099
+ return REGEX_LITERAL.test(value) ? null : "must be a delimited pattern, as in /pattern/i";
11100
+ case "string-or-string-list":
11101
+ if (isFilledString(value)) return null;
11102
+ if (Array.isArray(value) && value.length > 0 && value.every(isFilledString)) return null;
11103
+ return "must be a non-empty string, or a non-empty list of them";
11104
+ case "scalar":
11105
+ return isScalar(value) ? null : "must be a string, number or boolean";
11106
+ case "string":
11107
+ return typeof value === "string" ? null : "must be a string (this comparison is type-strict)";
11108
+ case "numeric":
11109
+ return isScalar(value) && Number.isFinite(Number(value)) ? null : "must be a number";
11110
+ case "scalar-or-non-empty-list":
11111
+ if (isScalar(value)) return null;
11112
+ if (Array.isArray(value) && value.length > 0 && value.every(isScalar)) return null;
11113
+ return "must be a scalar, or a non-empty list of them";
11114
+ case "non-empty-list":
11115
+ return Array.isArray(value) && value.length > 0 && value.every(isScalar) ? null : "must be a non-empty list of scalars";
11116
+ case "header-name-map": {
11117
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
11118
+ const names = Object.keys(value);
11119
+ if (names.length === 0) return "must be an object with at least one entry";
11120
+ if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
11121
+ const bad = names.find((name) => !isHeaderName(name));
11122
+ if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
11123
+ const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
11124
+ return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
11125
+ }
11126
+ case "header-name-list": {
11127
+ if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
11128
+ const bad = value.find((name) => !isHeaderName(name));
11129
+ return bad === void 0 ? null : `names an invalid HTTP header, ${JSON.stringify(bad)}`;
11130
+ }
11131
+ case "cookie-flags": {
11132
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
11133
+ const unknown = Object.keys(value).filter((key) => !COOKIE_FLAGS.includes(key));
11134
+ if (unknown.length > 0) return `has no flag named ${unknown.map((k) => JSON.stringify(k)).join(", ")}`;
11135
+ for (const flag of ["httpOnly", "secure"]) {
11136
+ if (value[flag] !== void 0 && typeof value[flag] !== "boolean") {
11137
+ return `flag "${flag}" must be true or false`;
11138
+ }
11139
+ }
11140
+ const sameSite = value.sameSite;
11141
+ const sameSiteOff = sameSite === false;
11142
+ if (sameSite !== void 0 && !sameSiteOff) {
11143
+ if (typeof sameSite !== "string" || !SAME_SITE_VALUES.some((v) => v.toLowerCase() === sameSite.toLowerCase())) {
11144
+ return `flag "sameSite" must be false, or one of ${SAME_SITE_VALUES.join(", ")}`;
11145
+ }
11146
+ }
11147
+ if (value.httpOnly === false && value.secure === false && sameSiteOff) {
11148
+ return "turns off every flag, so it would harden nothing";
11149
+ }
11150
+ return null;
11151
+ }
11152
+ case "object":
11153
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? null : "must be an object";
11154
+ case "non-empty-object":
11155
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? null : "must be an object with at least one entry";
11156
+ case "non-empty-string-list":
11157
+ return Array.isArray(value) && value.length > 0 && value.every(isFilledString) ? null : "must be a non-empty list of non-empty strings";
11158
+ default:
11159
+ return null;
11160
+ }
11161
+ }
11162
+ var MUTATIONS = Object.freeze([
11163
+ "base64_decode",
11164
+ "getArrayValues",
11165
+ "htmlentitydecode",
11166
+ "intval",
11167
+ "json_decode",
11168
+ "json_encode",
11169
+ "urldecode"
11170
+ ]);
11171
+ var PHASES = Object.freeze(["request", "response", "egress"]);
11172
+ var ACTIONS = Object.freeze([
11173
+ "block",
11174
+ "redact",
11175
+ "encode",
11176
+ "set-header",
11177
+ "remove-header",
11178
+ "harden-cookie"
11179
+ ]);
11180
+ var RULE_PROPERTY_SHAPES = Object.freeze({
11181
+ max_bytes: "positive-number",
11182
+ bypass_limit: "boolean",
11183
+ ensure: "boolean",
11184
+ prefilter: "non-empty-string-list"
11185
+ });
11186
+ var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
11187
+ var WHEN_KEYS = Object.freeze(["path", "method"]);
11188
+ var BUILD_SCOPE_PROPERTY = "build_scope";
11189
+ var BUILD_SCOPE = Object.freeze({
11190
+ applies_to: Object.freeze(["firewall"]),
11191
+ usable: Object.freeze({
11192
+ type: "string",
11193
+ trim: true,
11194
+ pattern: "^[0-9a-fA-F]{64}$",
11195
+ canonical: "lowercase"
11196
+ }),
11197
+ unreadable: "the rule remains accepted and detects only by default",
11198
+ local_override: "trustLocalRuleScope may explicitly enforce a caller-supplied rule"
11199
+ });
11200
+ var RULE_PROPERTIES = Object.freeze([
11201
+ "id",
11202
+ "rule_id",
11203
+ "title",
11204
+ "category",
11205
+ "phase",
11206
+ "action",
11207
+ "rule_v2",
11208
+ "when",
11209
+ "message",
11210
+ "enforcement",
11211
+ "source_revision",
11212
+ "prefilter",
11213
+ "max_bytes",
11214
+ "bypass_limit",
11215
+ "set_headers",
11216
+ "remove_headers",
11217
+ "cookie_flags",
11218
+ "ensure",
11219
+ "capture",
11220
+ BUILD_SCOPE_PROPERTY
11221
+ ]);
11222
+ var CAPTURE_KEYS = Object.freeze(["version", "raw_chars"]);
11223
+ var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture", BUILD_SCOPE_PROPERTY]);
11224
+ var ACTION_PROPERTIES = Object.freeze({
11225
+ block: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["request", "response", "egress"]) }),
11226
+ redact: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
11227
+ encode: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
11228
+ "set-header": Object.freeze({
11229
+ required: Object.freeze(["set_headers"]),
11230
+ defaulted: Object.freeze(["ensure"]),
11231
+ phases: Object.freeze(["response"]),
11232
+ shapes: Object.freeze({ set_headers: "header-name-map" })
11233
+ }),
11234
+ "remove-header": Object.freeze({
11235
+ required: Object.freeze(["remove_headers"]),
11236
+ defaulted: Object.freeze([]),
11237
+ phases: Object.freeze(["response"]),
11238
+ shapes: Object.freeze({ remove_headers: "header-name-list" })
11239
+ }),
11240
+ "harden-cookie": Object.freeze({
11241
+ required: Object.freeze([]),
11242
+ defaulted: Object.freeze(["cookie_flags"]),
11243
+ phases: Object.freeze(["response"]),
11244
+ shapes: Object.freeze({ cookie_flags: "cookie-flags" })
11245
+ })
11246
+ });
11247
+ var LIMITS = Object.freeze({
11248
+ maxRules: 5e3,
11249
+ maxWhitelists: 2e3,
11250
+ maxConditionsPerRule: 250,
11251
+ maxConditionNodesPerRule: 1e3,
11252
+ maxConditionNodesPerBundle: 25e3,
11253
+ maxNestingDepth: 12,
11254
+ maxRegexLength: 1e3,
11255
+ maxValueLength: 8192,
11256
+ maxParameterLength: 512,
11257
+ maxParameterItems: 64,
11258
+ maxOperandItems: 256,
11259
+ maxMapEntries: 128,
11260
+ maxPathSegments: 64
11261
+ });
11262
+ var GROUP_PARAMETER = "rules";
11263
+ function isGroup(condition) {
11264
+ return condition?.parameter === GROUP_PARAMETER && Array.isArray(condition?.rules);
11265
+ }
11266
+ function conditionShapeProblem(condition) {
11267
+ if (!condition || typeof condition !== "object") return "condition is not an object";
11268
+ const named = condition.parameter === GROUP_PARAMETER;
11269
+ const nested = Array.isArray(condition.rules);
11270
+ if (named && !nested) {
11271
+ return `parameter "${GROUP_PARAMETER}" is a group and needs a "rules" array`;
11272
+ }
11273
+ if (nested && !named) {
11274
+ return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
11275
+ }
11276
+ if (condition.inclusive !== void 0 && typeof condition.inclusive !== "boolean") {
11277
+ return "inclusive must be true or false";
11278
+ }
11279
+ if (named && nested) {
11280
+ if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
11281
+ if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
11282
+ if (condition.rules.length === 0) return "group has no conditions (would never match)";
11283
+ }
11284
+ return null;
11285
+ }
11286
+ function parameterProblem(parameter) {
11287
+ if (parameter === void 0 || parameter === null) return null;
11288
+ if (Array.isArray(parameter)) {
11289
+ if (parameter.length === 0) return "parameter list is empty";
11290
+ if (parameter.length > LIMITS.maxParameterItems) {
11291
+ return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
11292
+ }
11293
+ for (const member of parameter) {
11294
+ if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
11295
+ if (member === void 0 || member === null) return "parameter list members must be non-empty strings";
11296
+ const problem = parameterProblem(member);
11297
+ if (problem !== null) return problem;
11298
+ }
11299
+ return null;
11300
+ }
11301
+ if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
11302
+ if (parameter.length > LIMITS.maxParameterLength) {
11303
+ return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
11304
+ }
11305
+ if (parameter === GROUP_PARAMETER) {
11306
+ return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
11307
+ }
11308
+ const dot = parameter.indexOf(".");
11309
+ const name = dot === -1 ? parameter : parameter.slice(0, dot);
11310
+ const source = SOURCES[name];
11311
+ if (source === void 0) return `unknown parameter source "${name}"`;
11312
+ if (dot === -1) {
11313
+ return source.keyed === true ? `source "${name}" needs a key, e.g. "${name}.fieldname"` : null;
11314
+ }
11315
+ if (source.keyed !== true) return `source "${name}" takes no key`;
11316
+ const key = parameter.slice(dot + 1);
11317
+ if (key === "") return `source "${name}" needs a key after the dot`;
11318
+ return keyProblem(name, source, key);
11319
+ }
11320
+ function keyProblem(name, source, key) {
11321
+ if (key.endsWith("*") && source.wildcard !== true) {
11322
+ return `"${name}" does not fan out over a wildcard key`;
11323
+ }
11324
+ const bare = key.endsWith("*") ? key.slice(0, -1) : key;
11325
+ if (source.keys === "any") return null;
11326
+ if (Array.isArray(source.keys) && source.keys.includes(bare)) return null;
11327
+ for (const prefix of source.key_prefixes ?? []) {
11328
+ if (bare.startsWith(prefix) && bare.length > prefix.length) return null;
11329
+ }
11330
+ const accepted = [
11331
+ ...Array.isArray(source.keys) ? source.keys : [],
11332
+ ...(source.key_prefixes ?? []).map((p) => `${p}\u2026`)
11333
+ ].join(", ");
11334
+ return `"${name}" has no key "${key}" (expected one of: ${accepted})`;
11335
+ }
11336
+ function matchProblem(match, parameter) {
11337
+ if (!match || typeof match !== "object") return "condition has no match object";
11338
+ if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
11339
+ if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
11340
+ for (const operand of ["value", "key", "claim"]) {
11341
+ const value = match[operand];
11342
+ if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
11343
+ return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
11344
+ }
11345
+ if (Array.isArray(value)) {
11346
+ if (value.length > LIMITS.maxOperandItems) {
11347
+ return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
11348
+ }
11349
+ if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
11350
+ return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
11351
+ }
11352
+ }
11353
+ }
11354
+ const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
11355
+ if (!parameterless && (parameter === void 0 || parameter === null)) {
11356
+ return `match type "${match.type}" needs a parameter`;
11357
+ }
11358
+ if (parameterless && parameter !== void 0 && parameter !== null) {
11359
+ return `match type "${match.type}" reads the whole request and takes no parameter`;
11360
+ }
11361
+ const operands = MATCH_OPERANDS[match.type];
11362
+ if (operands) {
11363
+ for (const required of operands.required) {
11364
+ if (match[required] === void 0 || match[required] === null) {
11365
+ return `match type "${match.type}" needs "${required}"`;
11366
+ }
11367
+ }
11368
+ for (const [operand, shape] of Object.entries(operands.shapes ?? {})) {
11369
+ if (match[operand] === void 0) continue;
11370
+ const problem = operandShapeProblem(shape, match[operand]);
11371
+ if (problem) return `match type "${match.type}" operand "${operand}" ${problem}`;
11372
+ }
11373
+ } else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
11374
+ return `match type "${match.type}" needs a value`;
11375
+ }
11376
+ if (match.type === "array_key_value") {
11377
+ const paths2 = Array.isArray(match.key) ? match.key : [match.key];
11378
+ if (paths2.some((path17) => typeof path17 === "string" && path17.split(".").length > LIMITS.maxPathSegments)) {
11379
+ return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
11380
+ }
11381
+ }
11382
+ return null;
11383
+ }
11384
+ function mutationsProblem(mutations) {
11385
+ if (mutations === void 0 || mutations === null) return null;
11386
+ if (!Array.isArray(mutations)) return "mutations must be an array";
11387
+ if (mutations.length > LIMITS.maxOperandItems) {
11388
+ return `mutations has more than ${LIMITS.maxOperandItems} entries`;
11389
+ }
11390
+ for (const mutation of mutations) {
11391
+ if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
11392
+ return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
11393
+ }
11394
+ }
11395
+ return null;
11396
+ }
11397
+ function whenProblem(when) {
11398
+ if (when === void 0 || when === null) return null;
11399
+ if (typeof when !== "object" || Array.isArray(when)) return "when must be an object";
11400
+ const keys = Object.keys(when);
11401
+ if (keys.length === 0) return "when is empty \u2014 remove it, or name a path or method";
11402
+ const unknown = keys.filter((key) => !WHEN_KEYS.includes(key));
11403
+ if (unknown.length > 0) {
11404
+ 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`;
11405
+ }
11406
+ if ("path" in when && !isNonEmptyString(when.path)) {
11407
+ return "when.path must be a non-empty string";
11408
+ }
11409
+ if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
11410
+ return `when.path is longer than ${LIMITS.maxValueLength} chars`;
11411
+ }
11412
+ if ("method" in when) {
11413
+ const methods = Array.isArray(when.method) ? when.method : [when.method];
11414
+ if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
11415
+ if (methods.length > LIMITS.maxParameterItems) {
11416
+ return `when.method has more than ${LIMITS.maxParameterItems} entries`;
11417
+ }
11418
+ if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
11419
+ if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
11420
+ return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
11421
+ }
11422
+ }
11423
+ return null;
11424
+ }
11425
+ function isNonEmptyString(value) {
11426
+ return typeof value === "string" && value.trim() !== "";
11427
+ }
11428
+ function nullPropertyProblem(rule) {
11429
+ if (!rule || typeof rule !== "object") return null;
11430
+ for (const property of RULE_PROPERTIES) {
11431
+ if (NULL_EXEMPT_PROPERTIES.includes(property)) continue;
11432
+ if (property in rule && rule[property] === null) {
11433
+ 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`;
11434
+ }
11435
+ }
11436
+ return null;
11437
+ }
11438
+ function rulePropertyProblem(rule) {
11439
+ if (!rule || typeof rule !== "object") return null;
11440
+ for (const [property, shape] of Object.entries(RULE_PROPERTY_SHAPES)) {
11441
+ if (rule[property] === void 0 || rule[property] === null) continue;
11442
+ const problem = operandShapeProblem(shape, rule[property]);
11443
+ if (problem) return `"${property}" ${problem}`;
11444
+ }
11445
+ for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
11446
+ if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
11447
+ return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
11448
+ }
11449
+ }
11450
+ return null;
11451
+ }
11452
+ function actionProblem(action, rule) {
11453
+ if (action === void 0 || action === null) return null;
11454
+ if (!ACTIONS.includes(action)) return `unknown action "${action}"`;
11455
+ const spec = ACTION_PROPERTIES[action];
11456
+ for (const required of spec?.required ?? []) {
11457
+ if (rule?.[required] === void 0 || rule?.[required] === null) {
11458
+ return `action "${action}" needs "${required}"`;
11459
+ }
11460
+ }
11461
+ for (const [property, shape] of Object.entries(spec?.shapes ?? {})) {
11462
+ if (rule?.[property] === void 0 || rule?.[property] === null) continue;
11463
+ const problem = operandShapeProblem(shape, rule[property]);
11464
+ if (problem) return `action "${action}" property "${property}" ${problem}`;
11465
+ }
11466
+ const phase = rule?.phase ?? "request";
11467
+ if (spec !== void 0 && !spec.phases.includes(phase)) {
11468
+ return `action "${action}" is carried out on the ${spec.phases.join("/")} phase; on "${phase}" the rule blocks instead`;
11469
+ }
11470
+ return null;
11471
+ }
11472
+
11473
+ // src/protect/engine/response-json.js
11474
+ var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
11475
+ async function readBoundedJson2(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
11476
+ const stated2 = Number(response.headers?.get?.("content-length"));
11477
+ if (Number.isFinite(stated2) && stated2 > maxBytes) {
11478
+ throw new Error(`rule response exceeds ${maxBytes} bytes`);
11479
+ }
11480
+ const stream = response.body;
11481
+ if (!stream || typeof stream.getReader !== "function") {
11482
+ if (typeof response.text === "function") {
11483
+ const text = await response.text();
11484
+ if (new TextEncoder().encode(text).byteLength > maxBytes) {
11485
+ throw new Error(`rule response exceeds ${maxBytes} bytes`);
11486
+ }
11487
+ return JSON.parse(text);
11488
+ }
11489
+ throw new Error("rule response body cannot be read with a size bound");
11490
+ }
11491
+ const reader = stream.getReader();
11492
+ const chunks = [];
11493
+ let total = 0;
11494
+ try {
11495
+ for (; ; ) {
11496
+ const { done, value } = await reader.read();
11497
+ if (done) break;
11498
+ total += value.byteLength;
11499
+ if (total > maxBytes) {
11500
+ void reader.cancel().catch(() => {
11501
+ });
11502
+ throw new Error(`rule response exceeds ${maxBytes} bytes`);
11503
+ }
11504
+ chunks.push(value);
11505
+ }
11506
+ } finally {
11507
+ reader.releaseLock();
11508
+ }
11509
+ const bytes = new Uint8Array(total);
11510
+ let offset = 0;
11511
+ for (const chunk of chunks) {
11512
+ bytes.set(chunk, offset);
11513
+ offset += chunk.byteLength;
11514
+ }
11515
+ return JSON.parse(new TextDecoder().decode(bytes));
11516
+ }
11517
+
11518
+ // src/protect/engine/client.js
11519
+ var DEFAULT_BASE_URL = "https://api.patchstack.com";
11520
+ var DEFAULT_CACHE_TTL = 3e5;
11521
+ var JITTER_FRACTION = 0.1;
11522
+ var PatchstackRuleClient = class {
11523
+ #token;
11524
+ #baseUrl;
11525
+ #cacheTtl;
11526
+ #cache = null;
11527
+ #cacheTime = null;
11528
+ #ttlEffective = 0;
11529
+ #etag;
11530
+ #timeoutMs;
11531
+ constructor({ token, baseUrl, cacheTtl, etag, timeoutMs } = {}) {
11532
+ this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
11533
+ this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
11534
+ this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_WAF_API_URL, DEFAULT_BASE_URL, "rule endpoint");
11535
+ this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL;
11536
+ this.#etag = etag ?? null;
11537
+ if (!this.#token) {
11538
+ throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
11539
+ }
11540
+ }
11541
+ async getRules() {
11542
+ const now = Date.now();
11543
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
11544
+ return this.#cache;
11545
+ }
11546
+ const url = `${this.#baseUrl}/api/get-rules/3`;
11547
+ try {
11548
+ const headers = {
11549
+ "Content-Type": "application/json",
11550
+ "Authorization": `Bearer ${this.#token}`,
11551
+ "LicenseID": "1"
11552
+ // Hard-coded, is never actually used but needed by the API
11553
+ };
11554
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
11555
+ const response = await fetch(url, {
11556
+ method: "POST",
11557
+ headers,
11558
+ body: JSON.stringify({}),
11559
+ signal: AbortSignal.timeout(this.#timeoutMs)
11560
+ });
11561
+ if (response.status === 304) {
11562
+ this.#touch(now);
11563
+ return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
11564
+ }
11565
+ if (!response.ok) {
11566
+ return {
11567
+ success: false,
11568
+ error: `API returned ${response.status}: ${response.statusText}`,
11569
+ firewall: [],
11570
+ whitelists: [],
11571
+ whitelist_keys: {}
11572
+ };
11573
+ }
11574
+ const data = await readBoundedJson2(response);
11575
+ if (!data || !Array.isArray(data.firewall)) {
11576
+ return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
11577
+ }
11578
+ const result = {
11579
+ success: true,
11580
+ notModified: false,
11581
+ etag: response.headers?.get?.("etag") ?? null,
11582
+ firewall: data.firewall,
11583
+ whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
11584
+ whitelist_keys: data.whitelist_keys ?? {}
11585
+ };
11586
+ this.#cache = result;
11587
+ this.#etag = result.etag;
11588
+ this.#touch(now);
11589
+ return result;
11590
+ } catch (err) {
11591
+ return {
11592
+ success: false,
11593
+ error: err.name === "TimeoutError" ? "Request timed out" : err.message,
11594
+ firewall: [],
11595
+ whitelists: [],
11596
+ whitelist_keys: {}
11597
+ };
11598
+ }
11599
+ }
11600
+ #touch(now) {
11601
+ this.#cacheTime = now;
11602
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION);
11603
+ }
11604
+ clearCache() {
11605
+ this.#cache = null;
11606
+ this.#cacheTime = null;
11607
+ this.#etag = null;
11608
+ }
11609
+ };
11610
+
11611
+ // src/protect/engine/normalizer.js
11612
+ var utf8 = new TextDecoder();
11613
+ var MAX_RAW_CHARS = 1024 * 1024;
11614
+
11615
+ // src/protect/notify.js
11616
+ var reported = /* @__PURE__ */ new WeakMap();
11617
+ var warnings = 0;
11618
+ var MAX_WARNINGS = 20;
11619
+ function notify(fn, arg, label) {
11620
+ if (typeof fn !== "function") return false;
11621
+ try {
11622
+ const result = fn(arg);
11623
+ if (result !== null && typeof result === "object" && typeof result.then === "function") {
11624
+ try {
11625
+ result.then(void 0, (err) => warnOnce2(fn, label, err));
11626
+ } catch {
11627
+ }
11628
+ }
11629
+ return true;
11630
+ } catch (err) {
11631
+ warnOnce2(fn, label, err);
11632
+ return false;
11633
+ }
11634
+ }
11635
+ function warnOnce2(fn, label, err) {
11636
+ let labels = reported.get(fn);
11637
+ if (labels?.has(label)) return;
11638
+ if (!labels) {
11639
+ labels = /* @__PURE__ */ new Set();
11640
+ reported.set(fn, labels);
11641
+ }
11642
+ labels.add(label);
11643
+ if (warnings > MAX_WARNINGS) return;
11644
+ warnings++;
11645
+ try {
11646
+ if (warnings > MAX_WARNINGS) {
11647
+ console.warn("Patchstack: further failing callbacks passed to createProtection are not reported in this process.");
11648
+ return;
11649
+ }
11650
+ console.warn(
11651
+ `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)}`
11652
+ );
11653
+ } catch {
11654
+ }
11655
+ }
11656
+
11657
+ // src/protect/engine/engine.js
11658
+ var GRP = "(?:[^()]|\\([^()]*\\))*";
11659
+ var REDOS_PATTERNS = [
11660
+ /\([^)]*[+*}][^)]*\)\s*[+*]/,
11661
+ /\([^)]*\|[^)]*\)\s*[+*]/,
11662
+ new RegExp("\\(" + GRP + "[+*}]" + GRP + "\\)\\s*[+*]"),
11663
+ // nested quantified subgroup
11664
+ new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
11665
+ // nested alternation under a quantifier
11666
+ ];
11667
+ function hasAdjacentUnbounded(body, flags) {
11668
+ let previousUnboundedAtom = null;
11669
+ for (let i = 0; i < body.length; ) {
11670
+ const start = i;
11671
+ const ch = body[i];
11672
+ let assertion = false;
11673
+ let literal = null;
11674
+ let atom = ch;
11675
+ if (ch === "\\") {
11676
+ const escaped = body[i + 1] ?? "";
11677
+ assertion = /[bBAZz]/.test(escaped);
11678
+ if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
11679
+ i += Math.min(2, body.length - i);
11680
+ atom = body.slice(start, i);
11681
+ } else if (ch === "[") {
11682
+ i++;
11683
+ while (i < body.length) {
11684
+ if (body[i] === "\\") i += 2;
11685
+ else if (body[i++] === "]") break;
11686
+ }
11687
+ atom = body.slice(start, i);
11688
+ } else if (ch === ")") {
11689
+ i++;
11690
+ atom = ".";
11691
+ } else if (ch === "(") {
11692
+ const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
11693
+ if (lookaround) {
11694
+ i = groupEnd(body, i);
11695
+ continue;
11696
+ }
11697
+ i++;
11698
+ if (body.startsWith("?:", i)) i += 2;
11699
+ else if (body.startsWith("?<", i)) {
11700
+ const nameEnd = body.indexOf(">", i + 2);
11701
+ if (nameEnd !== -1) i = nameEnd + 1;
11702
+ }
11703
+ continue;
11704
+ } else if (ch === "|") {
11705
+ previousUnboundedAtom = null;
11706
+ i++;
11707
+ continue;
11708
+ } else if ("^$".includes(ch)) {
11709
+ i++;
11710
+ continue;
11711
+ } else if ("?*+{}".includes(ch)) {
11712
+ previousUnboundedAtom = null;
11713
+ i++;
11714
+ continue;
11715
+ } else {
11716
+ if (ch !== ".") literal = ch;
11717
+ i++;
11718
+ }
11719
+ if (assertion) continue;
11720
+ let unbounded = false;
11721
+ let mayBeEmpty = false;
11722
+ if (body[i] === "*" || body[i] === "+") {
11723
+ unbounded = true;
11724
+ mayBeEmpty = body[i] === "*";
11725
+ i++;
11726
+ } else if (body[i] === "{") {
11727
+ const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
11728
+ if (quantifier) {
11729
+ unbounded = quantifier[2] === "";
11730
+ mayBeEmpty = Number(quantifier[1]) === 0;
11731
+ i += quantifier[0].length;
11732
+ }
11733
+ } else if (body[i] === "?") {
11734
+ mayBeEmpty = true;
11735
+ i++;
11736
+ }
11737
+ if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
11738
+ if (unbounded && previousUnboundedAtom !== null) return true;
11739
+ if (unbounded) {
11740
+ previousUnboundedAtom = atom;
11741
+ } else if (previousUnboundedAtom !== null && !mayBeEmpty) {
11742
+ try {
11743
+ const atomFlags = flags.replace(/[^iu]/g, "");
11744
+ 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";
11745
+ const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
11746
+ if (complementaryClass || excludedLiteral) {
11747
+ previousUnboundedAtom = null;
11748
+ }
11749
+ } catch {
11750
+ }
11751
+ }
11752
+ if (i === start) i++;
11753
+ }
11754
+ return false;
11755
+ }
11756
+ function parseRepetition(body) {
11757
+ let i = 0;
11758
+ const parseAlternation = () => {
11759
+ const branches = [parseSequence()];
11760
+ while (body[i] === "|") {
11761
+ i++;
11762
+ branches.push(parseSequence());
11763
+ }
11764
+ return branches.length === 1 ? branches[0] : { type: "alt", branches };
11765
+ };
11766
+ const parseSequence = () => {
11767
+ const items = [];
11768
+ while (i < body.length && body[i] !== "|" && body[i] !== ")") {
11769
+ const atom = parseAtom();
11770
+ if (atom === null) return null;
11771
+ const quantified = parseQuantifier(atom);
11772
+ if (quantified === null) return null;
11773
+ items.push(quantified);
11774
+ }
11775
+ return { type: "seq", items };
11776
+ };
11777
+ const parseAtom = () => {
11778
+ const ch = body[i];
11779
+ if (ch === "(") {
11780
+ const start2 = i;
11781
+ i++;
11782
+ let kind = "capture";
11783
+ if (body.startsWith("?:", i)) {
11784
+ kind = "group";
11785
+ i += 2;
11786
+ } else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
11787
+ kind = "look";
11788
+ i += 2;
11789
+ } else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
11790
+ kind = "look";
11791
+ i += 3;
11792
+ } else if (body.startsWith("?<", i)) {
11793
+ const close = body.indexOf(">", i + 2);
11794
+ if (close === -1) return null;
11795
+ i = close + 1;
11796
+ } else if (body[i] === "?") {
11797
+ return null;
11798
+ }
11799
+ const inner = parseAlternation();
11800
+ if (inner === null || body[i] !== ")") return null;
11801
+ i++;
11802
+ return { type: "group", kind, body: inner, source: body.slice(start2, i) };
11803
+ }
11804
+ const start = i;
11805
+ if (ch === "[") {
11806
+ i++;
11807
+ if (body[i] === "^") i++;
11808
+ if (body[i] === "]") i++;
11809
+ while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
11810
+ if (body[i] !== "]") return null;
11811
+ i++;
11812
+ return { type: "atom", source: body.slice(start, i) };
11813
+ }
11814
+ if (ch === "\\") {
11815
+ const next = body[i + 1];
11816
+ if (next === void 0) return null;
11817
+ if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
11818
+ const close = body.indexOf("}", i + 3);
11819
+ if (close === -1) return null;
11820
+ i = close + 1;
11821
+ } else if (next === "k" && body[i + 2] === "<") {
11822
+ const close = body.indexOf(">", i + 3);
11823
+ if (close === -1) return null;
11824
+ i = close + 1;
11825
+ } else {
11826
+ i += 2;
11827
+ }
11828
+ return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
11829
+ }
11830
+ if (ch === "^" || ch === "$") {
11831
+ i++;
11832
+ return { type: "assert" };
11833
+ }
11834
+ if (ch === "*" || ch === "+" || ch === "?") return null;
11835
+ i++;
11836
+ return { type: "atom", source: body.slice(start, i) };
11837
+ };
11838
+ const parseQuantifier = (atom) => {
11839
+ let min;
11840
+ let max;
11841
+ const ch = body[i];
11842
+ if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
11843
+ else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
11844
+ else if (ch === "?") [min, max, i] = [0, 1, i + 1];
11845
+ else if (ch === "{") {
11846
+ const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
11847
+ if (!bounds) return atom;
11848
+ min = Number(bounds[1]);
11849
+ max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
11850
+ i += bounds[0].length;
11851
+ } else {
11852
+ return atom;
11853
+ }
11854
+ if (body[i] === "?") i++;
11855
+ if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
11856
+ return { type: "quant", min, max, body: atom };
11857
+ };
11858
+ const root = parseAlternation();
11859
+ return root !== null && i === body.length ? root : null;
11860
+ }
11861
+ function* nodesOf(node) {
11862
+ if (!node) return;
11863
+ yield node;
11864
+ if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
11865
+ else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
11866
+ else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
11867
+ }
11868
+ var BOUNDED_NESTING_LIMIT = 100;
11869
+ var BOUNDED_ALTERNATION_LIMIT = 16;
11870
+ var OPTIONAL_REPEAT_LIMIT = 8;
11871
+ function hasFencedRepetitions(quant, flags) {
11872
+ let body = quant.body;
11873
+ if (body.type === "group" && body.kind !== "look") body = body.body;
11874
+ if (body.type !== "seq" || body.items.length < 2) return false;
11875
+ const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
11876
+ if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
11877
+ const atomFlags = flags.replace(/[^i]/g, "");
11878
+ const unicode = /[uv]/.test(flags);
11879
+ let insides;
11880
+ try {
11881
+ insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
11882
+ } catch {
11883
+ return false;
11884
+ }
11885
+ const literalOf = (source) => {
11886
+ if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
11887
+ if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
11888
+ return null;
11889
+ };
11890
+ const disjoint = (edge) => {
11891
+ if (edge?.type !== "atom" || edge.source === void 0) return false;
11892
+ const literal = literalOf(edge.source);
11893
+ if (literal !== null) return insides.every((re) => !re.test(literal));
11894
+ if (unicode) return false;
11895
+ let fence;
11896
+ try {
11897
+ fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
11898
+ } catch {
11899
+ return false;
11900
+ }
11901
+ for (let unit = 0; unit <= 65535; unit++) {
11902
+ const ch = String.fromCharCode(unit);
11903
+ if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
11904
+ }
11905
+ return true;
11906
+ };
11907
+ return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
11908
+ }
11909
+ function hasCatastrophicShape(body, flags) {
11910
+ const root = parseRepetition(body);
11911
+ if (root === null) return null;
11912
+ for (const node of nodesOf(root)) {
11913
+ if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
11914
+ if (node.type !== "quant" || node.max <= 1) continue;
11915
+ const fenced = hasFencedRepetitions(node, flags);
11916
+ for (const inner of nodesOf(node.body)) {
11917
+ if (inner === node.body && inner.type === "quant") continue;
11918
+ if (inner.type === "quant" && inner.max > 1 && !fenced) {
11919
+ if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
11920
+ }
11921
+ if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
11922
+ return true;
11923
+ }
11924
+ if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
11925
+ return true;
11926
+ }
11927
+ }
11928
+ }
11929
+ return false;
11930
+ }
11931
+ function lookaroundBody(source) {
11932
+ return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
11933
+ }
11934
+ function groupEnd(body, start) {
11935
+ let depth = 0;
11936
+ let inClass = false;
11937
+ for (let i = start; i < body.length; i++) {
11938
+ const ch = body[i];
11939
+ if (ch === "\\") {
11940
+ i++;
11941
+ continue;
11942
+ }
11943
+ if (ch === "[") inClass = true;
11944
+ else if (ch === "]") inClass = false;
11945
+ else if (!inClass && ch === "(") depth++;
11946
+ else if (!inClass && ch === ")" && --depth === 0) return i + 1;
11947
+ }
11948
+ return body.length;
11949
+ }
11950
+ var MAX_PATTERN_LENGTH = 1e3;
11951
+ var regexVerdicts = /* @__PURE__ */ new Map();
11952
+ var REGEX_VERDICT_LIMIT = 2048;
11953
+ function safeRegExp(pattern) {
11954
+ if (!pattern) {
11955
+ return null;
11956
+ }
11957
+ if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
11958
+ return null;
11959
+ }
11960
+ const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
11961
+ if (!match) {
11962
+ return null;
11963
+ }
11964
+ let safe = regexVerdicts.get(pattern);
11965
+ if (safe === void 0) {
11966
+ safe = isSafeBody(match[1], match[2]);
11967
+ if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
11968
+ regexVerdicts.set(pattern, safe);
11969
+ }
11970
+ if (!safe) return null;
11971
+ try {
11972
+ return new RegExp(match[1], match[2]);
11973
+ } catch {
11974
+ return null;
11975
+ }
11976
+ }
11977
+ function isSafeBody(body, flags) {
11978
+ for (const dangerous of REDOS_PATTERNS) {
11979
+ if (dangerous.test(body)) return false;
11980
+ }
11981
+ if (hasAdjacentUnbounded(body, flags)) return false;
11982
+ if (hasCatastrophicShape(body, flags) === true) return false;
11983
+ try {
11984
+ new RegExp(body, flags);
11985
+ } catch {
11986
+ return false;
11987
+ }
11988
+ return true;
11989
+ }
11990
+
11991
+ // src/protect/engine/pulse-client.js
11992
+ var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
11993
+ var DEFAULT_CACHE_TTL2 = 3e5;
11994
+ var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
11995
+ var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
11996
+ var PER_RULE_DRY_RUN_HEADER = "X-Patchstack-Per-Rule-Dry-Run";
11997
+ var JITTER_FRACTION2 = 0.1;
11998
+ var PulseRuleClient = class {
11999
+ #siteUuid;
12000
+ #timeoutMs;
12001
+ #baseUrl;
12002
+ #cacheTtl;
12003
+ #cache = null;
12004
+ #cacheTime = null;
12005
+ #ttlEffective = 0;
12006
+ #etag;
12007
+ #pulseAuth;
12008
+ #detectionState;
12009
+ #buildId;
12010
+ constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, detectionState, buildId } = {}) {
12011
+ this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
12012
+ this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
12013
+ this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
12014
+ this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
12015
+ this.#etag = etag ?? null;
12016
+ this.#pulseAuth = pulseAuth ?? null;
12017
+ this.#detectionState = typeof detectionState === "string" ? detectionState : null;
12018
+ this.#buildId = canonicalBuildId(buildId);
12019
+ if (!this.#siteUuid) {
12020
+ throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
12021
+ }
12022
+ }
12023
+ async getRules() {
12024
+ const now = Date.now();
12025
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
12026
+ return this.#cache;
12027
+ }
12028
+ const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
12029
+ try {
12030
+ const auth = await pulseAuthHeader(
12031
+ { pulseAuth: this.#pulseAuth, endpoint: this.#baseUrl, timeoutMs: this.#timeoutMs },
12032
+ fetch
12033
+ );
12034
+ const headers = { Accept: "application/json", ...auth };
12035
+ if (this.#detectionState !== null && typeof auth.Authorization === "string") {
12036
+ headers["X-Patchstack-Detections"] = this.#detectionState;
12037
+ }
12038
+ if (this.#buildId !== null && typeof auth.Authorization === "string") {
12039
+ headers["X-Patchstack-Build"] = this.#buildId;
12040
+ }
12041
+ if (typeof auth.Authorization === "string") {
12042
+ headers[PER_RULE_DRY_RUN_HEADER] = "1";
12043
+ }
12044
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
12045
+ const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
12046
+ if (response.status === 304) {
12047
+ this.#touch(now);
12048
+ const result2 = {
12049
+ ...this.#cache ?? { firewall: [], whitelists: [], whitelist_keys: {} },
12050
+ success: true,
12051
+ notModified: true,
12052
+ etag: response.headers?.get?.("etag") ?? this.#etag,
12053
+ build: buildVerdictOf(response.headers)
12054
+ };
12055
+ this.#cache = result2;
12056
+ this.#etag = result2.etag;
12057
+ return result2;
12058
+ }
12059
+ if (!response.ok) {
12060
+ return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
12061
+ }
12062
+ const data = await readBoundedJson2(response);
12063
+ if (!data || !Array.isArray(data.firewall)) {
12064
+ return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
12065
+ }
12066
+ const result = {
12067
+ success: true,
12068
+ notModified: false,
12069
+ etag: response.headers?.get?.("etag") ?? null,
12070
+ firewall: data.firewall,
12071
+ whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
12072
+ whitelist_keys: data.whitelist_keys ?? {},
12073
+ ...enforcementField(data),
12074
+ // Carried through so the caller can decide what a build-scoped rule may do. Response metadata,
12075
+ // rather than bundle content, because it varies with the build asking and is also present on 304.
12076
+ build: buildVerdictOf(response.headers)
12077
+ };
12078
+ this.#cache = result;
12079
+ this.#etag = result.etag;
12080
+ this.#touch(now);
12081
+ return result;
12082
+ } catch (err) {
12083
+ return {
12084
+ success: false,
12085
+ error: err.name === "TimeoutError" ? "Request timed out" : err.message,
12086
+ firewall: [],
12087
+ whitelists: [],
12088
+ whitelist_keys: {}
12089
+ };
12090
+ }
12091
+ }
12092
+ #touch(now) {
12093
+ this.#cacheTime = now;
12094
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
12095
+ }
12096
+ clearCache() {
12097
+ this.#cache = null;
12098
+ this.#cacheTime = null;
12099
+ this.#etag = null;
12100
+ }
12101
+ };
12102
+ function buildVerdictOf(headers) {
12103
+ const get = headers !== null && typeof headers === "object" && typeof headers.get === "function" ? (name) => headers.get(name) : () => null;
12104
+ const stated2 = get(BUILD_VERDICT_HEADER);
12105
+ const verdict = stated2 === "match" || stated2 === "missing" || stated2 === "mismatch" ? stated2 : "unknown";
12106
+ const matched = get(BUILD_IDENTITY_HEADER);
12107
+ return { verdict, matchedBuildId: typeof matched === "string" ? matched : null };
12108
+ }
12109
+ function enforcementField(data) {
12110
+ if (!data || typeof data !== "object") return {};
12111
+ const v = data.enforcement ?? data.mode;
12112
+ return v === "block" || v === "dry-run" ? { enforcement: v } : {};
12113
+ }
12114
+
12115
+ // src/protect/rules/validate.js
12116
+ var LIMITS2 = LIMITS;
12117
+ var PHASES2 = new Set(PHASES);
12118
+ var ACTIONS2 = new Set(ACTIONS);
12119
+ function validateBundle(bundle, opts = {}) {
12120
+ const rejected = [];
12121
+ let omittedRejections = 0;
12122
+ const reject = (entry) => {
12123
+ if (rejected.length < 100) rejected.push(entry);
12124
+ else omittedRejections++;
12125
+ };
12126
+ const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
12127
+ const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
12128
+ const bundleBudget = { count: 0 };
12129
+ const firewall = [];
12130
+ for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
12131
+ const reason = enforceableRuleProblem(rule, bundleBudget);
12132
+ if (reason) reject({ id: idOf(rule), reason });
12133
+ else firewall.push(rule);
12134
+ }
12135
+ if (inFirewall.length > LIMITS2.maxRules) {
12136
+ reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
12137
+ }
12138
+ const whitelists = [];
12139
+ for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
12140
+ if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
12141
+ reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
12142
+ continue;
12143
+ }
12144
+ if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
12145
+ reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
12146
+ continue;
12147
+ }
12148
+ const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
12149
+ if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
12150
+ else whitelists.push(wl);
12151
+ }
12152
+ if (inWhitelists.length > LIMITS2.maxWhitelists) {
12153
+ reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
12154
+ }
12155
+ let whitelistKeys = {};
12156
+ if (bundle?.whitelist_keys !== void 0) {
12157
+ if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
12158
+ reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
12159
+ } else {
12160
+ const entries = Object.entries(bundle.whitelist_keys);
12161
+ if (entries.length > LIMITS2.maxMapEntries) {
12162
+ reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
12163
+ } else {
12164
+ 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));
12165
+ if (invalid) {
12166
+ reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
12167
+ } else {
12168
+ whitelistKeys = bundle.whitelist_keys;
12169
+ }
12170
+ }
12171
+ }
12172
+ }
12173
+ if (omittedRejections > 0) {
12174
+ rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
12175
+ }
12176
+ return {
12177
+ bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
12178
+ rejected
12179
+ };
12180
+ }
12181
+ function idOf(rule) {
12182
+ const id = rule?.id ?? rule?.rule_id;
12183
+ if (id === void 0 || id === null) return "(unidentified)";
12184
+ try {
12185
+ return String(id).slice(0, 128);
12186
+ } catch {
12187
+ return "(unidentified)";
12188
+ }
12189
+ }
12190
+ function enforceableRuleProblem(rule, bundleBudget = null) {
12191
+ if (!rule || typeof rule !== "object") return "not an object";
12192
+ const nullReason = nullPropertyProblem(rule);
12193
+ if (nullReason) return nullReason;
12194
+ if (rule.phase !== void 0 && !PHASES2.has(rule.phase)) return `unknown phase "${rule.phase}"`;
12195
+ const actionReason = actionProblem(rule.action, rule);
12196
+ if (actionReason) return actionReason;
12197
+ const scopeReason = whenProblem(rule.when);
12198
+ if (scopeReason) return scopeReason;
12199
+ const propertyReason = rulePropertyProblem(rule);
12200
+ if (propertyReason) return propertyReason;
12201
+ return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
12202
+ }
12203
+ function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
12204
+ if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
12205
+ if (conditions.length === 0) return "rule_v2 is empty (would never match)";
12206
+ if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
12207
+ if (conditions.length > LIMITS2.maxConditionsPerRule) {
12208
+ return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
12209
+ }
12210
+ for (const c of conditions) {
12211
+ ruleBudget.count++;
12212
+ if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
12213
+ return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
12214
+ }
12215
+ if (bundleBudget) {
12216
+ bundleBudget.count++;
12217
+ if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
12218
+ return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
12219
+ }
12220
+ }
12221
+ const shapeReason = conditionShapeProblem(c);
12222
+ if (shapeReason) return shapeReason;
12223
+ if (isGroup(c)) {
12224
+ const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
12225
+ if (nested) return nested;
12226
+ continue;
12227
+ }
12228
+ const parameterReason = parameterProblem(c.parameter);
12229
+ if (parameterReason) return parameterReason;
12230
+ const mutationReason = mutationsProblem(c.mutations);
12231
+ if (mutationReason) return mutationReason;
12232
+ const matchReason = matchProblem(c.match, c.parameter);
12233
+ if (matchReason) return matchReason;
12234
+ const m = c.match;
12235
+ if (m.type === "regex") {
12236
+ if (typeof m.value !== "string") return "regex match.value must be a string";
12237
+ if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
12238
+ if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
12239
+ } else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
12240
+ return `match.value longer than ${LIMITS2.maxValueLength} chars`;
12241
+ }
12242
+ if (m.match) {
12243
+ const nested = conditionsProblem(
12244
+ [{ parameter: c.parameter, match: m.match }],
12245
+ depth + 1,
12246
+ ruleBudget,
12247
+ bundleBudget
12248
+ );
12249
+ if (nested) return nested;
12250
+ }
12251
+ }
12252
+ return null;
12253
+ }
12254
+
12255
+ // src/protect/rules/source.js
12256
+ function liveUpdateRejections(res, options) {
12257
+ if (options.acceptPartialBundle) return [];
12258
+ const { rejected } = validateBundle(
12259
+ {
12260
+ firewall: Array.isArray(res.firewall) ? res.firewall : [],
12261
+ whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
12262
+ whitelist_keys: res.whitelist_keys
12263
+ },
12264
+ { allowGlobalWhitelists: options.allowGlobalWhitelists }
12265
+ );
12266
+ return rejected;
12267
+ }
12268
+ function reportRejections(rejected, options, label) {
12269
+ const report = options.onRuleRejected;
12270
+ for (const r of rejected) {
12271
+ if (typeof report === "function") {
12272
+ notify(report, { ...r, accepted: false }, "onRuleRejected");
12273
+ }
12274
+ }
12275
+ const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
12276
+ notify(options.onError, new Error(
12277
+ `${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" : ""}`
12278
+ ), "onError");
12279
+ }
12280
+ function fromSource(bundle, origin, reason) {
12281
+ return {
12282
+ ...bundle,
12283
+ source: reason === void 0 ? { ok: true, origin } : { ok: false, origin, reason }
12284
+ };
12285
+ }
12286
+ function buildIdentity(options) {
12287
+ if (options.buildId !== void 0 && options.buildId !== null) {
12288
+ const explicit = canonicalBuildId(options.buildId);
12289
+ return explicit === null ? { id: null, reason: "the buildId option is not a complete 64-hex SHA-256 identity" } : { id: explicit };
12290
+ }
12291
+ const stamped = readBuildStamp(options.rules);
12292
+ if (stamped !== null) return { id: stamped };
12293
+ return {
12294
+ id: null,
12295
+ 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"
12296
+ };
12297
+ }
12298
+ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
12299
+ let held = 0;
12300
+ let unusable = 0;
12301
+ const firewall = bundle.firewall.map((rule) => {
12302
+ if (rule === null || typeof rule !== "object") return rule;
12303
+ if (!(BUILD_SCOPE_PROPERTY in rule)) return rule;
12304
+ const scope = rule[BUILD_SCOPE_PROPERTY];
12305
+ const target = canonicalBuildId(scope);
12306
+ if (target !== null && confirmed !== null && target === confirmed) return rule;
12307
+ if (rule.enforcement === "dry-run") return rule;
12308
+ held += 1;
12309
+ if (target === null) unusable += 1;
12310
+ return { ...rule, enforcement: "dry-run" };
12311
+ });
12312
+ if (held === 0) return bundle;
12313
+ const explanations = [
12314
+ ...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
12315
+ ...held > unusable ? [reason] : []
12316
+ ];
12317
+ const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
12318
+ if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
12319
+ else warnHeldOnce(message);
12320
+ return { ...bundle, firewall };
12321
+ }
12322
+ var warnedHeld = /* @__PURE__ */ new Set();
12323
+ function warnHeldOnce(message) {
12324
+ if (warnedHeld.has(message)) return;
12325
+ warnedHeld.add(message);
12326
+ console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
12327
+ }
12328
+ function confirmedBy(res, presented) {
12329
+ if (presented === null) return null;
12330
+ const verdict = res?.build;
12331
+ if (verdict?.verdict !== "match") return null;
12332
+ return canonicalBuildId(verdict.matchedBuildId) === presented ? presented : null;
12333
+ }
12334
+ async function resolveRules(options, store, ctx = {}) {
12335
+ const timeoutMs = ctx.timeoutMs;
12336
+ if (options.siteUuid) {
12337
+ const prior = await store.read();
12338
+ const identity2 = buildIdentity(options);
12339
+ const sameBuild = prior?.buildId === identity2.id;
12340
+ const cacheConfirmed = identity2.id !== null && sameBuild && prior?.matchedBuildId === identity2.id ? identity2.id : null;
12341
+ const unconfirmed = identity2.reason ?? "the platform did not confirm that these coordinates belong to this map";
12342
+ const served = (bundle, confirmed) => withScopedDryRun(bundle, confirmed, unconfirmed, options);
12343
+ const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options);
12344
+ const client = new PulseRuleClient({
12345
+ siteUuid: options.siteUuid,
12346
+ baseUrl: options.pulseRulesUrl,
12347
+ etag: sameBuild ? prior?.etag : null,
12348
+ timeoutMs,
12349
+ pulseAuth: ctx.pulseAuth,
12350
+ detectionState: ctx.detectionState,
12351
+ buildId: identity2.id
12352
+ });
12353
+ const res = await client.getRules();
12354
+ if (res.success && res.notModified && prior?.bundle) {
12355
+ const confirmed = confirmedBy(res, identity2.id);
12356
+ await store.write({
12357
+ bundle: prior.bundle,
12358
+ etag: res.etag ?? prior.etag ?? null,
12359
+ buildId: identity2.id,
12360
+ matchedBuildId: confirmed
12361
+ });
12362
+ return fromSource(served(normalizeBundle(prior.bundle, options), confirmed), "cache");
12363
+ }
12364
+ if (res.success && !res.notModified) {
12365
+ const confirmed = confirmedBy(res, identity2.id);
12366
+ const rejected = liveUpdateRejections(res, options);
12367
+ if (rejected.length > 0) {
12368
+ reportRejections(rejected, options, "rule update rejected");
12369
+ if (prior?.bundle) return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", "update rejected");
12370
+ if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
12371
+ return fromSource(emptyBundle(), "empty", "update rejected");
12372
+ }
12373
+ const bundle = normalizeBundle(res, options);
12374
+ await store.write({ bundle, etag: res.etag ?? null, buildId: identity2.id, matchedBuildId: confirmed });
12375
+ return fromSource(served(bundle, confirmed), "api");
12376
+ }
12377
+ if (prior?.bundle) {
12378
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
12379
+ return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", res.error ?? "no usable response");
12380
+ }
12381
+ if (options.rules) {
12382
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
12383
+ return fromSource(local(normalizeBundle(options.rules, options)), "bundled", res.error ?? "no usable response");
12384
+ }
12385
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
12386
+ return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
12387
+ }
12388
+ if (options.token) {
12389
+ const identity2 = buildIdentity(options);
12390
+ const remote = (bundle) => withScopedDryRun(
12391
+ bundle,
12392
+ null,
12393
+ "the token-authenticated rules service did not corroborate this map",
12394
+ options
12395
+ );
12396
+ const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options);
12397
+ const prior = await store.read();
12398
+ const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag, timeoutMs });
12399
+ const res = await client.getRules();
12400
+ if (res.success && res.notModified && prior?.bundle) {
12401
+ return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache");
12402
+ }
12403
+ if (res.success && !res.notModified) {
12404
+ const rejected = liveUpdateRejections(res, options);
12405
+ if (rejected.length > 0) {
12406
+ reportRejections(rejected, options, "rule update rejected");
12407
+ if (prior?.bundle) return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", "update rejected");
12408
+ if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
12409
+ return fromSource(emptyBundle(), "empty", "update rejected");
12410
+ }
12411
+ const bundle = normalizeBundle(res, options);
12412
+ await store.write({ bundle, etag: res.etag ?? null });
12413
+ return fromSource(remote(bundle), "api");
12414
+ }
12415
+ if (prior?.bundle) {
12416
+ notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
12417
+ return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", res.error ?? "no usable response");
12418
+ }
12419
+ notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
12420
+ return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
12421
+ }
12422
+ if (options.rules) {
12423
+ const identity2 = buildIdentity(options);
12424
+ const bundle = normalizeBundle(options.rules, options);
12425
+ if (options.trustLocalRuleScope === true) return fromSource(bundle, "bundled");
12426
+ return fromSource(
12427
+ withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options),
12428
+ "bundled"
12429
+ );
12430
+ }
12431
+ return fromSource(emptyBundle(), "empty");
12432
+ }
12433
+ function normalizeBundle(b, options = {}) {
12434
+ const enforcement = b?.enforcement ?? b?.mode;
12435
+ const { bundle: checked, rejected } = validateBundle(
12436
+ {
12437
+ firewall: Array.isArray(b.firewall) ? b.firewall : [],
12438
+ whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
12439
+ whitelist_keys: b.whitelist_keys
12440
+ },
12441
+ { allowGlobalWhitelists: options.allowGlobalWhitelists }
12442
+ );
12443
+ if (rejected.length > 0) {
12444
+ const report = options.onRuleRejected;
12445
+ if (typeof report === "function") {
12446
+ for (const r of rejected) {
12447
+ notify(report, r, "onRuleRejected");
12448
+ }
12449
+ } else {
12450
+ const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
12451
+ console.warn(
12452
+ `[patchstack] ${rejected.length} delivered rule(s) rejected as invalid/oversized and are NOT enforced: ${sample}` + (rejected.length > 3 ? ", \u2026" : "")
12453
+ );
12454
+ }
12455
+ }
12456
+ return {
12457
+ firewall: checked.firewall,
12458
+ whitelists: checked.whitelists,
12459
+ whitelist_keys: checked.whitelist_keys,
12460
+ ...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
12461
+ };
12462
+ }
12463
+ function emptyBundle() {
12464
+ return { firewall: [], whitelists: [], whitelist_keys: {} };
12465
+ }
12466
+
12467
+ // src/protect/rules/store.js
12468
+ var fsMod;
12469
+ async function loadFs() {
12470
+ if (fsMod !== void 0) return fsMod;
12471
+ try {
12472
+ const [fs, path17] = await Promise.all([import("fs"), import("path")]);
12473
+ fsMod = {
12474
+ chmodSync: fs.chmodSync,
12475
+ closeSync: fs.closeSync,
12476
+ constants: fs.constants,
12477
+ fstatSync: fs.fstatSync,
12478
+ fsyncSync: fs.fsyncSync,
12479
+ lstatSync: fs.lstatSync,
12480
+ openSync: fs.openSync,
12481
+ readFileSync: fs.readFileSync,
12482
+ mkdirSync: fs.mkdirSync,
12483
+ renameSync: fs.renameSync,
12484
+ unlinkSync: fs.unlinkSync,
12485
+ writeFileSync: fs.writeFileSync,
12486
+ join: path17.join
12487
+ };
12488
+ } catch {
12489
+ fsMod = null;
12490
+ }
12491
+ return fsMod;
12492
+ }
12493
+ async function sourceIdentity(options = {}) {
12494
+ if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
12495
+ const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
12496
+ return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
12497
+ }
12498
+ if (typeof options.token === "string" && options.token !== "") {
12499
+ const digest = await sha256Hex(options.token);
12500
+ if (digest === null) return null;
12501
+ const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
12502
+ return `token:${digest}@${endpoint}`;
12503
+ }
12504
+ return null;
12505
+ }
12506
+ async function sha256Hex(text) {
12507
+ try {
12508
+ const subtle = globalThis.crypto?.subtle;
12509
+ if (!subtle || typeof TextEncoder === "undefined") return null;
12510
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
12511
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
12512
+ } catch {
12513
+ return null;
12514
+ }
12515
+ }
12516
+ function makeStore(options = {}) {
12517
+ let mem = null;
12518
+ const durable = durableTier(options);
12519
+ let identity2;
12520
+ const identityOf = () => identity2 ??= sourceIdentity(options);
12521
+ return {
12522
+ async read() {
12523
+ if (mem) return mem;
12524
+ const source = await identityOf();
12525
+ if (source === null) return null;
12526
+ const env = await durable.read();
12527
+ if (!env || env.source !== source) return null;
12528
+ mem = env;
12529
+ return env;
12530
+ },
12531
+ async write(env) {
12532
+ if (!env) return;
12533
+ const stamped = { ...env, source: await identityOf() };
12534
+ mem = stamped;
12535
+ await durable.write(stamped);
12536
+ }
12537
+ };
12538
+ }
12539
+ function durableTier(options) {
12540
+ const adapter = options.ruleCache;
12541
+ if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
12542
+ return {
12543
+ read: async () => {
12544
+ try {
12545
+ return toEnvelope(await adapter.read());
12546
+ } catch {
12547
+ return null;
12548
+ }
12549
+ },
12550
+ write: async (env) => {
12551
+ try {
12552
+ await adapter.write(env);
12553
+ } catch {
12554
+ }
12555
+ }
12556
+ };
12557
+ }
12558
+ const dir = options.cacheDir;
12559
+ return {
12560
+ read: async () => cacheRead(dir),
12561
+ write: async (env) => cacheWrite(dir, env)
12562
+ };
12563
+ }
12564
+ async function cacheWrite(dir, env) {
12565
+ if (!dir) return;
12566
+ const fs = await loadFs();
12567
+ if (!fs) return;
12568
+ const target = fs.join(dir, "patchstack-rules.json");
12569
+ const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
12570
+ let descriptor = null;
12571
+ try {
12572
+ fs.mkdirSync(dir, { recursive: true });
12573
+ const directory = fs.lstatSync(dir);
12574
+ if (directory.isSymbolicLink() || !directory.isDirectory()) return;
12575
+ let mode;
12576
+ try {
12577
+ const current = fs.lstatSync(target);
12578
+ if (current.isSymbolicLink() || !current.isFile()) return;
12579
+ mode = current.mode & 4095;
12580
+ } catch (error) {
12581
+ if (error?.code !== "ENOENT") return;
12582
+ }
12583
+ descriptor = fs.openSync(temporary, "wx", mode ?? 384);
12584
+ fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
12585
+ fs.fsyncSync(descriptor);
12586
+ fs.closeSync(descriptor);
12587
+ descriptor = null;
12588
+ if (mode !== void 0) fs.chmodSync(temporary, mode);
12589
+ try {
12590
+ fs.renameSync(temporary, target);
12591
+ } catch (error) {
12592
+ if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
12593
+ try {
12594
+ fs.unlinkSync(target);
12595
+ } catch (unlinkError) {
12596
+ if (unlinkError?.code !== "ENOENT") throw unlinkError;
12597
+ }
12598
+ fs.renameSync(temporary, target);
12599
+ }
12600
+ } catch {
12601
+ } finally {
12602
+ if (descriptor !== null) {
12603
+ try {
12604
+ fs.closeSync(descriptor);
12605
+ } catch {
12606
+ }
12607
+ }
12608
+ try {
12609
+ fs.unlinkSync(temporary);
12610
+ } catch {
12611
+ }
12612
+ }
12613
+ }
12614
+ async function cacheRead(dir) {
12615
+ if (!dir) return null;
12616
+ const fs = await loadFs();
12617
+ if (!fs) return null;
12618
+ const target = fs.join(dir, "patchstack-rules.json");
12619
+ let descriptor = null;
12620
+ try {
12621
+ const entry = fs.lstatSync(target);
12622
+ if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
12623
+ const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
12624
+ descriptor = fs.openSync(target, flags);
12625
+ const opened = fs.fstatSync(descriptor);
12626
+ if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
12627
+ return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
12628
+ } catch {
12629
+ return null;
12630
+ } finally {
12631
+ if (descriptor !== null) {
12632
+ try {
12633
+ fs.closeSync(descriptor);
12634
+ } catch {
12635
+ }
12636
+ }
12637
+ }
12638
+ }
12639
+ function randomSuffix() {
12640
+ try {
12641
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
12642
+ } catch {
12643
+ }
12644
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
12645
+ }
12646
+ function toEnvelope(value) {
12647
+ if (!value || typeof value !== "object") return null;
12648
+ if (value.bundle && typeof value.bundle === "object") {
12649
+ const buildId = canonicalBuildId(value.buildId);
12650
+ const matched = canonicalBuildId(value.matchedBuildId);
12651
+ return {
12652
+ bundle: value.bundle,
12653
+ etag: value.etag ?? null,
12654
+ buildId,
12655
+ // A confirmation belongs to the presentation stored beside it. A crossed or partially written
12656
+ // envelope confirms nothing, even when one of its fields happens to name the current map.
12657
+ matchedBuildId: buildId !== null && matched === buildId ? matched : null,
12658
+ source: typeof value.source === "string" ? value.source : null
12659
+ };
12660
+ }
12661
+ if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
12662
+ return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
12663
+ }
12664
+ return null;
12665
+ }
12666
+
12667
+ // src/setup-sync.ts
12668
+ async function syncSetupProtection(cwd, config) {
12669
+ const notices = [];
12670
+ const warnings2 = [];
12671
+ let map;
12672
+ try {
12673
+ map = await runMapDetailed(/* @__PURE__ */ new Map([["dir", cwd], ["upload", true]]), {
12674
+ config,
12675
+ setup: true,
12676
+ log: (line) => notices.push(line)
12677
+ });
12678
+ } catch {
12679
+ map = { code: 1, error: "Could not analyse or bind the map; check the project files and permissions." };
12680
+ }
12681
+ const unavailable = (error) => ({
12682
+ map,
12683
+ rules: { ok: false, count: 0, origin: "empty", error },
12684
+ notices,
12685
+ warnings: warnings2
12686
+ });
12687
+ if (!isCanonicalUuid(config.siteUuid)) return unavailable("No site UUID is configured.");
12688
+ if (!config.pulseAuth) return unavailable("Set PATCHSTACK_API_KEY or run patchstack-connect login to fetch live rules.");
12689
+ try {
12690
+ const rulesUrl = buildRulesUrl(config.endpoint, config.siteUuid);
12691
+ assertConnectableEndpoint(config, rulesUrl);
12692
+ const base = rulesUrl.slice(0, rulesUrl.lastIndexOf("/rules/"));
12693
+ const defaultRules = buildRulesUrl(DEFAULT_ENDPOINT, config.siteUuid);
12694
+ if (rulesUrl !== defaultRules && process.env.PATCHSTACK_PULSE_RULES_URL !== base) {
12695
+ 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.");
12696
+ }
12697
+ const options = {
12698
+ siteUuid: config.siteUuid,
12699
+ // Match the ordinary guard's default cache identity; explicit endpoints remain isolated.
12700
+ pulseRulesUrl: base,
12701
+ cacheDir: join26(cwd, ".patchstack"),
12702
+ buildId: map.buildId ?? void 0,
12703
+ onError: () => notices.push("Some rules could not be refreshed or are detect-only; verify delivery after starting the app.")
12704
+ };
12705
+ const ignored = await ensureIgnored(cwd, ".patchstack/", "Patchstack local runtime cache");
12706
+ if (!ignored.ignored) warnings2.push("The local rule cache is not git-ignored. Keep .patchstack/ out of version control.");
12707
+ const store = makeStore({ ...options, pulseRulesUrl: rulesUrl === defaultRules ? void 0 : base });
12708
+ const bundle = await resolveRules(options, store, {
12709
+ pulseAuth: config.pulseAuth,
12710
+ timeoutMs: Math.min(config.timeoutMs, 1e4)
12711
+ });
12712
+ return {
12713
+ map,
12714
+ notices,
12715
+ warnings: warnings2,
12716
+ rules: {
12717
+ ok: bundle.source.ok,
12718
+ origin: bundle.source.origin,
12719
+ count: bundle.firewall.length,
12720
+ ...bundle.source.ok ? {} : { error: bundle.source.reason ?? "Live rules could not be fetched." }
12721
+ }
12722
+ };
12723
+ } catch {
12724
+ return unavailable("Live rules could not be fetched. Check the endpoint, authentication and project permissions.");
12725
+ }
12726
+ }
12727
+
12728
+ // src/cli.ts
12729
+ var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
12730
+
12731
+ Usage:
12732
+ patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
12733
+ If no UUID is configured, the server
12734
+ provisions one and we persist it. After a
12735
+ successful post it also adds/updates the
12736
+ Patchstack Connector <script> tag in the root
12737
+ HTML shell (index.html, public/index.html,
12738
+ or src/app.html) \u2014 opt out with
12739
+ "widget": false in .patchstackrc.json.
12740
+ Run as a postinstall/prebuild/build hook, a
12741
+ report Patchstack cannot accept is printed
12742
+ and exits 0 so the build goes on; run
12743
+ directly, the same failure exits 1
12744
+ patchstack-connect setup [options] Finish the bounded project setup: run scan,
12745
+ manage the widget, install + verify runtime
12746
+ protection, upload the attack-surface map, pull
12747
+ live rules, and wire dependency/build scans.
12748
+ Never runs the project build
12749
+ patchstack-connect map [--dir <p>] [--out <f>] [--upload]
12750
+ Map the app's attack surface: entry points, the
12751
+ inputs each reads, the sinks it can reach, and
12752
+ evidence-backed input\u2192sink flows (each labelled
12753
+ with how the link was established). Best-effort static
12754
+ analysis \u2014 reports the DETECTED surface, with
12755
+ coverage counters. Prints JSON (--out writes a
12756
+ file; a pre-bundle --upload also stamps the existing
12757
+ guard rules file; --follow-symlinks leaves the project dir).
12758
+ Uses the app's own TypeScript
12759
+ patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
12760
+ with an existing site UUID
12761
+ patchstack-connect status [options] Show current configuration and whether the
12762
+ site still exists on Patchstack (active /
12763
+ removed)
12764
+ patchstack-connect uninstall [options] Signal Patchstack that this package is being
12765
+ removed from the project. An unclaimed site
12766
+ record is deleted; a claimed site is flagged
12767
+ for its owner to remove in the dashboard.
12768
+ Does NOT touch local files \u2014 see the
12769
+ "Uninstalling" steps in AGENT-INSTALL.md
12770
+ patchstack-connect mark-build [options] Stamp built HTML with a production flag +
12771
+ build fingerprint, and ensure the widget
12772
+ tag in built pages (run as a postbuild step).
12773
+ The flag marks a page as the LIVE site, so it
12774
+ is stamped only on a production build; a local
12775
+ or preview build gets the widget tag and no
12776
+ marker (and any stale marker is removed).
12777
+ Looks in the framework's own output dir, an
12778
+ --output the build script names, then dist/,
12779
+ build/, out/, .output/public/, _site/ \u2014 inside
12780
+ the project only. --dir <path> overrides.
12781
+ patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
12782
+ guard). Auto-wires supported server stacks;
12783
+ for others it scaffolds a
12784
+ generic guard + prints a wiring plan.
12785
+ --demo seeds a broad sample rule set (for
12786
+ demonstrations, not production).
12787
+ --check verifies the guard is wired (exit 1
12788
+ if not) \u2014 for the wire-then-verify loop.
12789
+ It reads the app's source and never runs it.
12790
+ --check --runtime additionally STARTS THE APP
12791
+ on a loopback port and sends it one request,
12792
+ to establish that a request reaches the
12793
+ guard. Opt-in, and the only mode that runs
12794
+ the app. Exit 0 traversed, 1 a listener
12795
+ answered instead of the guard, 2 could not
12796
+ be established (e.g. a TypeScript or
12797
+ framework entry this must not start).
12798
+ patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
12799
+ walkthrough: verify the vulnerable package,
12800
+ scan it, wait for live rule 18843, install +
12801
+ verify the guard, and print test requests.
12802
+ Does not install vulnerable dependencies.
12803
+ patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
12804
+ preparing, running, proving, and cleaning up
12805
+ the production-backed local demo.
12806
+ patchstack-connect guide [--full] Show this project's setup status (what's done,
10463
12807
  what's missing, with tailored commands), then
10464
12808
  print the full setup guide. --full prints the
10465
12809
  guide even when setup is complete
@@ -10851,11 +13195,14 @@ async function runScan(args, options = {}) {
10851
13195
  );
10852
13196
  }
10853
13197
  if (config.environment === "local") {
10854
- say("Environment: local (this machine). Set PATCHSTACK_ENVIRONMENT=production on a live build your host does not identify.");
13198
+ const because = (config.environmentEvidence ?? []).join("; ") || "this machine";
13199
+ say(`Environment: local (${because}). Set PATCHSTACK_ENVIRONMENT=production on a live build your host does not identify.`);
10855
13200
  } else {
10856
13201
  const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
10857
13202
  say(`Environment: ${config.environment}${because}.`);
10858
13203
  }
13204
+ const overruled = ignoredFileEnvironmentWarning(config);
13205
+ if (overruled !== null) console.warn(`patchstack: ${overruled}`);
10859
13206
  if (config.endpoint !== DEFAULT_ENDPOINT) {
10860
13207
  say(`Endpoint override: ${config.endpoint}`);
10861
13208
  }
@@ -11051,6 +13398,12 @@ function reportSourceWidget(siteUuid, framework, report) {
11051
13398
  detail(`Widget: skipped (${err.message}).`);
11052
13399
  }
11053
13400
  }
13401
+ function ignoredFileEnvironmentWarning(config) {
13402
+ const ignored = config.ignoredFileEnvironment;
13403
+ if (ignored == null) return null;
13404
+ const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
13405
+ 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.`;
13406
+ }
11054
13407
  function reportSourceMarker(framework, checksum, report) {
11055
13408
  try {
11056
13409
  const shell = resolveWidgetFileHint(process.cwd(), framework);
@@ -11233,7 +13586,7 @@ async function runGuide(args) {
11233
13586
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
11234
13587
  console.log("");
11235
13588
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
11236
- console.log(readFileSync19(guidePath, "utf8"));
13589
+ console.log(readFileSync20(guidePath, "utf8"));
11237
13590
  return 0;
11238
13591
  }
11239
13592
  function reportProtection(protection, report) {
@@ -11288,11 +13641,11 @@ Run: ${installCommand(before.packageManager)}`
11288
13641
  return 1;
11289
13642
  }
11290
13643
  const report = emptyReport();
11291
- let reported = {};
13644
+ let reported2 = {};
11292
13645
  const scanCode = await runScan(args, {
11293
13646
  report,
11294
13647
  onReported: (outcome) => {
11295
- reported = outcome;
13648
+ reported2 = outcome;
11296
13649
  }
11297
13650
  });
11298
13651
  if (scanCode !== 0) {
@@ -11303,11 +13656,29 @@ Run: ${installCommand(before.packageManager)}`
11303
13656
  report.done.push(buildStepsLine(wired));
11304
13657
  detail(`Build hooks: ${wired.detail}`);
11305
13658
  const config = await resolveCliConfig(args);
13659
+ const synced = await syncSetupProtection(process.cwd(), config);
13660
+ for (const line of synced.notices) detail(line);
13661
+ for (const [index, warning] of synced.warnings.entries()) {
13662
+ report.missing.push({ key: `setup-sync-${index}`, text: warning });
13663
+ }
13664
+ const uploaded = synced.map.upload;
13665
+ if (uploaded?.result === "stored" || uploaded?.result === "unchanged") {
13666
+ report.done.push(`Attack-surface map uploaded (revision ${uploaded.revision}; ${synced.map.endpoints ?? 0} detected entry points)`);
13667
+ 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."] });
13668
+ } else {
13669
+ report.missing.push({ key: "map-upload", text: "Attack-surface map was not uploaded", hint: [synced.map.error ?? uploaded?.message ?? "Check connectivity and rerun setup."] });
13670
+ }
13671
+ if (synced.rules.ok) {
13672
+ 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"})`);
13673
+ } else {
13674
+ report.missing.push({ key: "rules-pull", text: "Live rule lookup is incomplete", hint: [synced.rules.error ?? "Check authentication and rerun setup."] });
13675
+ }
13676
+ 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."] });
11306
13677
  const after = await collectGuideState(process.cwd());
11307
- const missing = mergeMissing(report.missing, guideMissing(after, reported));
13678
+ const missing = mergeMissing(report.missing, guideMissing(after, reported2));
11308
13679
  const context = scanNextStepContext(config, after, after.siteUuid);
11309
13680
  if (verbose) console.log("");
11310
- for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported), context, {
13681
+ for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported2), context, {
11311
13682
  useColor: useColor(),
11312
13683
  verbose
11313
13684
  })) {
@@ -11381,7 +13752,7 @@ function describeStack(stack) {
11381
13752
  }
11382
13753
  function readBuildScript(cwd) {
11383
13754
  try {
11384
- const pkg = JSON.parse(readFileSync19(path16.join(cwd, "package.json"), "utf8"));
13755
+ const pkg = JSON.parse(readFileSync20(path16.join(cwd, "package.json"), "utf8"));
11385
13756
  return pkg.scripts?.build;
11386
13757
  } catch {
11387
13758
  return void 0;
@@ -11438,6 +13809,8 @@ async function runMarkBuild(args) {
11438
13809
  }
11439
13810
  environment = config.environment;
11440
13811
  environmentEvidence = config.environmentEvidence ?? [];
13812
+ const overruled = ignoredFileEnvironmentWarning(config);
13813
+ if (overruled !== null) console.warn(`mark-build: ${overruled}`);
11441
13814
  } catch (err) {
11442
13815
  console.warn(
11443
13816
  `mark-build: could not resolve the site UUID (${err.message}). Skipping the widget pass.`
@@ -11445,7 +13818,7 @@ async function runMarkBuild(args) {
11445
13818
  }
11446
13819
  const forcedProduction = args.flags.get("production") === true;
11447
13820
  const published = forcedProduction || marksPublishedBuild(environment);
11448
- const reported = config !== null && forcedProduction && config.environment !== "production" ? { ...config, environment: "production", environmentSource: "override" } : config;
13821
+ const reported2 = config !== null && forcedProduction && config.environment !== "production" ? { ...config, environment: "production", environmentSource: "override" } : config;
11449
13822
  const buildDirOptions = { framework: stack?.framework ?? null, buildScript: readBuildScript(cwd) };
11450
13823
  const dir = resolveBuildDir(cwd, getStringFlag(args.flags, "dir"), buildDirOptions);
11451
13824
  if (dir === null) {
@@ -11456,14 +13829,14 @@ async function runMarkBuild(args) {
11456
13829
  console.warn(
11457
13830
  'mark-build: if the build writes elsewhere, set it once in package.json \u2192 "postbuild": "patchstack-connect mark-build --dir <path>".'
11458
13831
  );
11459
- await reportBuildStamp(reported, wirePayload, "no-output");
13832
+ await reportBuildStamp(reported2, wirePayload, "no-output");
11460
13833
  return 0;
11461
13834
  }
11462
13835
  const files = findHtmlFiles(dir);
11463
13836
  if (files.length === 0) {
11464
13837
  console.warn(`mark-build: found ${dir} but no HTML files in it.`);
11465
13838
  console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
11466
- await reportBuildStamp(reported, wirePayload, "no-pages");
13839
+ await reportBuildStamp(reported2, wirePayload, "no-pages");
11467
13840
  return 0;
11468
13841
  }
11469
13842
  const staleBy = staleBuildAge(files, startedAt);
@@ -11472,12 +13845,18 @@ async function runMarkBuild(args) {
11472
13845
  `mark-build: nothing in ${dir} changed in the last ${Math.round(staleBy / 6e4)} minute(s). Run it right after the build (as postbuild).`
11473
13846
  );
11474
13847
  }
11475
- const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
13848
+ const snippet = published ? buildInjectionSnippet(checksum, stack) : environment === "sandbox" ? buildSandboxSnippet() : "";
11476
13849
  let marked = 0;
11477
13850
  let widgetTouched = 0;
13851
+ let handRemoved = 0;
11478
13852
  for (const file of files) {
11479
- const before = readFileSync19(file, "utf8");
13853
+ const before = readFileSync20(file, "utf8");
11480
13854
  let after = injectMarker(before, snippet);
13855
+ if (!published) {
13856
+ const cleared = removeHandMarkers(after);
13857
+ if (cleared !== after) handRemoved += 1;
13858
+ after = cleared;
13859
+ }
11481
13860
  if (widgetUuid !== null) {
11482
13861
  const ensured = ensureWidgetInHtml(after, widgetUuid);
11483
13862
  if (ensured.action === "added" || ensured.action === "updated") {
@@ -11496,7 +13875,7 @@ async function runMarkBuild(args) {
11496
13875
  detail(
11497
13876
  `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
11498
13877
  );
11499
- await reportBuildStamp(reported, wirePayload, "stamped");
13878
+ await reportBuildStamp(reported2, wirePayload, "stamped");
11500
13879
  return 0;
11501
13880
  }
11502
13881
  const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
@@ -11506,12 +13885,17 @@ async function runMarkBuild(args) {
11506
13885
  detail(
11507
13886
  `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}]` : ""}.`
11508
13887
  );
11509
- await reportBuildStamp(reported, wirePayload, "withheld");
13888
+ if (handRemoved > 0) {
13889
+ console.warn(
13890
+ `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.`
13891
+ );
13892
+ }
13893
+ await reportBuildStamp(reported2, wirePayload, "withheld");
11510
13894
  return 0;
11511
13895
  }
11512
13896
  function packageVersion() {
11513
13897
  try {
11514
- const require2 = createRequire3(import.meta.url);
13898
+ const require2 = createRequire6(import.meta.url);
11515
13899
  const manifest = require2("../package.json");
11516
13900
  return typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : "unknown";
11517
13901
  } catch {