@patchstack/connect 0.5.11 → 0.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/protect.cjs CHANGED
@@ -397,13 +397,13 @@ function buildEndpointUrl(base, siteUuid) {
397
397
  function authFailureMessage(status, config) {
398
398
  const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
399
399
  if (status === 401 && !hasCredential) {
400
- return "Patchstack requires an API credential for this site and none is configured. Run `npx patchstack-connect login`, or set PATCHSTACK_API_KEY.";
400
+ return "Patchstack needs this project's API key, and none is set here. Run npx @patchstack/connect login, or set PATCHSTACK_API_KEY.";
401
401
  }
402
402
  if (status === 401) {
403
- return "Patchstack rejected this API credential. It may have expired, been revoked, or the site may no longer exist. Run `npx patchstack-connect login` to issue a new one.";
403
+ return "Patchstack did not accept this project's API key: it may have expired or been revoked, or the project may no longer exist. Run npx @patchstack/connect login to get a new one.";
404
404
  }
405
405
  if (status === 403) {
406
- return "This API credential is not permitted to act on this site. Check that siteUuid in .patchstackrc.json matches the credential (a credential is issued for one site).";
406
+ return "This API key belongs to a different project. Check that siteUuid in .patchstackrc.json is the project the key was made for.";
407
407
  }
408
408
  return null;
409
409
  }
@@ -747,7 +747,7 @@ function normaliseSiteUrl(value) {
747
747
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
748
748
  if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
749
749
  const origin = `${parsed.protocol}//${parsed.host}`;
750
- return origin.length <= 191 ? origin : null;
750
+ return origin.length <= URL_MAX_LENGTH ? origin : null;
751
751
  }
752
752
  function detectSiteUrl(env = process.env) {
753
753
  for (const source of PRODUCTION_URL_SOURCES) {
@@ -758,7 +758,7 @@ function detectSiteUrl(env = process.env) {
758
758
  }
759
759
  return null;
760
760
  }
761
- var RESERVED_SUFFIXES, PRODUCTION_URL_SOURCES;
761
+ var RESERVED_SUFFIXES, URL_MAX_LENGTH, PRODUCTION_URL_SOURCES;
762
762
  var init_site_url = __esm({
763
763
  "src/site-url.ts"() {
764
764
  "use strict";
@@ -775,6 +775,7 @@ var init_site_url = __esm({
775
775
  ".in-addr.arpa",
776
776
  ".ip6.arpa"
777
777
  ];
778
+ URL_MAX_LENGTH = 191;
778
779
  PRODUCTION_URL_SOURCES = [
779
780
  {
780
781
  // VERCEL_URL is per-deployment and changes every build, so it is deliberately not read here.
@@ -1144,7 +1145,7 @@ function unreadableWarning(filename, report) {
1144
1145
  if (report.unreadable === 0) return null;
1145
1146
  const shown = report.samples.join(", ");
1146
1147
  const one = report.unreadable === 1;
1147
- return `${filename}: ${report.unreadable} entr${one ? "y was" : "ies were"} not in a form this scanner reads (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "it is" : "they are"} absent from this manifest. Anything installed only through ${one ? "that entry" : "those entries"} is not being checked.`;
1148
+ return `${filename}: ${report.unreadable} entr${one ? "y" : "ies"} could not be read (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "that package is" : "those packages are"} not being checked.`;
1148
1149
  }
1149
1150
  var MAX_SAMPLES;
1150
1151
  var init_report = __esm({
@@ -2082,7 +2083,7 @@ async function scanLockfile(cwd) {
2082
2083
  warnings.push(staleWarning("node_modules/", missing));
2083
2084
  }
2084
2085
  warnings.push(
2085
- walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
2086
+ 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."
2086
2087
  );
2087
2088
  return manifestWith(walked, warnings);
2088
2089
  }
@@ -2094,7 +2095,7 @@ async function scanLockfile(cwd) {
2094
2095
  );
2095
2096
  }
2096
2097
  warnings.push(
2097
- `No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
2098
+ `No package list matches package.json; checked ${firstParsed.filename}, so some packages may be missing. Reinstall your packages to fix this.`
2098
2099
  );
2099
2100
  return manifestWith(firstParsed.packages, warnings);
2100
2101
  }
@@ -2108,12 +2109,12 @@ function otherCompleteSources(all, current, declared) {
2108
2109
  }
2109
2110
  function conflictWarning(filename, conflicts) {
2110
2111
  const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
2111
- return `${filename} and another lockfile disagree about installed versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Scanned node_modules/ instead. Remove whichever lockfile your package manager does not maintain.`;
2112
+ return `${filename} and another package list file give different versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Checked node_modules/ instead. Delete the file your package manager does not use.`;
2112
2113
  }
2113
2114
  function staleWarning(source, missing) {
2114
2115
  const sample = missing.slice(0, 3).join(", ");
2115
2116
  const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
2116
- return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
2117
+ return `${source} is out of date: it is missing ${missing.length} package${missing.length === 1 ? "" : "s"} listed in package.json (${sample}${suffix}). Run ${INSTALL_BY_SOURCE[source] ?? "your install command"} to update it.`;
2117
2118
  }
2118
2119
  async function presentLockfiles(cwd) {
2119
2120
  const probed = await Promise.all(
@@ -2148,7 +2149,7 @@ async function exists(filePath) {
2148
2149
  return false;
2149
2150
  }
2150
2151
  }
2151
- var import_promises9, import_node_path8, LOCKFILE_CANDIDATES;
2152
+ var import_promises9, import_node_path8, LOCKFILE_CANDIDATES, INSTALL_BY_SOURCE;
2152
2153
  var init_parsers = __esm({
2153
2154
  "src/parsers/index.ts"() {
2154
2155
  "use strict";
@@ -2172,6 +2173,13 @@ var init_parsers = __esm({
2172
2173
  { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
2173
2174
  { filename: "yarn.lock", strategy: "yarn-lockfile" }
2174
2175
  ];
2176
+ INSTALL_BY_SOURCE = {
2177
+ "package-lock.json": "npm install",
2178
+ "bun.lock": "bun install",
2179
+ "bun.lockb": "bun install",
2180
+ "pnpm-lock.yaml": "pnpm install",
2181
+ "yarn.lock": "yarn install"
2182
+ };
2175
2183
  }
2176
2184
  });
2177
2185
 
@@ -3526,6 +3534,184 @@ function hasAdjacentUnbounded(body, flags) {
3526
3534
  }
3527
3535
  return false;
3528
3536
  }
3537
+ function parseRepetition(body) {
3538
+ let i = 0;
3539
+ const parseAlternation = () => {
3540
+ const branches = [parseSequence()];
3541
+ while (body[i] === "|") {
3542
+ i++;
3543
+ branches.push(parseSequence());
3544
+ }
3545
+ return branches.length === 1 ? branches[0] : { type: "alt", branches };
3546
+ };
3547
+ const parseSequence = () => {
3548
+ const items = [];
3549
+ while (i < body.length && body[i] !== "|" && body[i] !== ")") {
3550
+ const atom = parseAtom();
3551
+ if (atom === null) return null;
3552
+ const quantified = parseQuantifier(atom);
3553
+ if (quantified === null) return null;
3554
+ items.push(quantified);
3555
+ }
3556
+ return { type: "seq", items };
3557
+ };
3558
+ const parseAtom = () => {
3559
+ const ch = body[i];
3560
+ if (ch === "(") {
3561
+ const start2 = i;
3562
+ i++;
3563
+ let kind = "capture";
3564
+ if (body.startsWith("?:", i)) {
3565
+ kind = "group";
3566
+ i += 2;
3567
+ } else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
3568
+ kind = "look";
3569
+ i += 2;
3570
+ } else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
3571
+ kind = "look";
3572
+ i += 3;
3573
+ } else if (body.startsWith("?<", i)) {
3574
+ const close = body.indexOf(">", i + 2);
3575
+ if (close === -1) return null;
3576
+ i = close + 1;
3577
+ } else if (body[i] === "?") {
3578
+ return null;
3579
+ }
3580
+ const inner = parseAlternation();
3581
+ if (inner === null || body[i] !== ")") return null;
3582
+ i++;
3583
+ return { type: "group", kind, body: inner, source: body.slice(start2, i) };
3584
+ }
3585
+ const start = i;
3586
+ if (ch === "[") {
3587
+ i++;
3588
+ if (body[i] === "^") i++;
3589
+ if (body[i] === "]") i++;
3590
+ while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
3591
+ if (body[i] !== "]") return null;
3592
+ i++;
3593
+ return { type: "atom", source: body.slice(start, i) };
3594
+ }
3595
+ if (ch === "\\") {
3596
+ const next = body[i + 1];
3597
+ if (next === void 0) return null;
3598
+ if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
3599
+ const close = body.indexOf("}", i + 3);
3600
+ if (close === -1) return null;
3601
+ i = close + 1;
3602
+ } else if (next === "k" && body[i + 2] === "<") {
3603
+ const close = body.indexOf(">", i + 3);
3604
+ if (close === -1) return null;
3605
+ i = close + 1;
3606
+ } else {
3607
+ i += 2;
3608
+ }
3609
+ return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
3610
+ }
3611
+ if (ch === "^" || ch === "$") {
3612
+ i++;
3613
+ return { type: "assert" };
3614
+ }
3615
+ if (ch === "*" || ch === "+" || ch === "?") return null;
3616
+ i++;
3617
+ return { type: "atom", source: body.slice(start, i) };
3618
+ };
3619
+ const parseQuantifier = (atom) => {
3620
+ let min;
3621
+ let max;
3622
+ const ch = body[i];
3623
+ if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
3624
+ else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
3625
+ else if (ch === "?") [min, max, i] = [0, 1, i + 1];
3626
+ else if (ch === "{") {
3627
+ const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
3628
+ if (!bounds) return atom;
3629
+ min = Number(bounds[1]);
3630
+ max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
3631
+ i += bounds[0].length;
3632
+ } else {
3633
+ return atom;
3634
+ }
3635
+ if (body[i] === "?") i++;
3636
+ if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
3637
+ return { type: "quant", min, max, body: atom };
3638
+ };
3639
+ const root = parseAlternation();
3640
+ return root !== null && i === body.length ? root : null;
3641
+ }
3642
+ function* nodesOf(node) {
3643
+ if (!node) return;
3644
+ yield node;
3645
+ if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
3646
+ else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
3647
+ else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
3648
+ }
3649
+ var BOUNDED_NESTING_LIMIT = 100;
3650
+ var BOUNDED_ALTERNATION_LIMIT = 16;
3651
+ var OPTIONAL_REPEAT_LIMIT = 8;
3652
+ function hasFencedRepetitions(quant, flags) {
3653
+ let body = quant.body;
3654
+ if (body.type === "group" && body.kind !== "look") body = body.body;
3655
+ if (body.type !== "seq" || body.items.length < 2) return false;
3656
+ const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
3657
+ if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
3658
+ const atomFlags = flags.replace(/[^i]/g, "");
3659
+ const unicode = /[uv]/.test(flags);
3660
+ let insides;
3661
+ try {
3662
+ insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
3663
+ } catch {
3664
+ return false;
3665
+ }
3666
+ const literalOf = (source) => {
3667
+ if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
3668
+ if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
3669
+ return null;
3670
+ };
3671
+ const disjoint = (edge) => {
3672
+ if (edge?.type !== "atom" || edge.source === void 0) return false;
3673
+ const literal = literalOf(edge.source);
3674
+ if (literal !== null) return insides.every((re) => !re.test(literal));
3675
+ if (unicode) return false;
3676
+ let fence;
3677
+ try {
3678
+ fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
3679
+ } catch {
3680
+ return false;
3681
+ }
3682
+ for (let unit = 0; unit <= 65535; unit++) {
3683
+ const ch = String.fromCharCode(unit);
3684
+ if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
3685
+ }
3686
+ return true;
3687
+ };
3688
+ return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
3689
+ }
3690
+ function hasCatastrophicShape(body, flags) {
3691
+ const root = parseRepetition(body);
3692
+ if (root === null) return null;
3693
+ for (const node of nodesOf(root)) {
3694
+ if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
3695
+ if (node.type !== "quant" || node.max <= 1) continue;
3696
+ const fenced = hasFencedRepetitions(node, flags);
3697
+ for (const inner of nodesOf(node.body)) {
3698
+ if (inner === node.body && inner.type === "quant") continue;
3699
+ if (inner.type === "quant" && inner.max > 1 && !fenced) {
3700
+ if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
3701
+ }
3702
+ if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
3703
+ return true;
3704
+ }
3705
+ if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
3706
+ return true;
3707
+ }
3708
+ }
3709
+ }
3710
+ return false;
3711
+ }
3712
+ function lookaroundBody(source) {
3713
+ return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
3714
+ }
3529
3715
  function groupEnd(body, start) {
3530
3716
  let depth = 0;
3531
3717
  let inClass = false;
@@ -3552,6 +3738,8 @@ function warnRejectedPatternOnce(pattern) {
3552
3738
  );
3553
3739
  }
3554
3740
  var MAX_PATTERN_LENGTH = 1e3;
3741
+ var regexVerdicts = /* @__PURE__ */ new Map();
3742
+ var REGEX_VERDICT_LIMIT = 2048;
3555
3743
  function safeRegExp(pattern) {
3556
3744
  if (!pattern) {
3557
3745
  return null;
@@ -3563,16 +3751,32 @@ function safeRegExp(pattern) {
3563
3751
  if (!match) {
3564
3752
  return null;
3565
3753
  }
3566
- for (const dangerous of REDOS_PATTERNS) {
3567
- if (dangerous.test(match[1])) return null;
3754
+ let safe = regexVerdicts.get(pattern);
3755
+ if (safe === void 0) {
3756
+ safe = isSafeBody(match[1], match[2]);
3757
+ if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
3758
+ regexVerdicts.set(pattern, safe);
3568
3759
  }
3569
- if (hasAdjacentUnbounded(match[1], match[2])) return null;
3760
+ if (!safe) return null;
3570
3761
  try {
3571
3762
  return new RegExp(match[1], match[2]);
3572
3763
  } catch {
3573
3764
  return null;
3574
3765
  }
3575
3766
  }
3767
+ function isSafeBody(body, flags) {
3768
+ for (const dangerous of REDOS_PATTERNS) {
3769
+ if (dangerous.test(body)) return false;
3770
+ }
3771
+ if (hasAdjacentUnbounded(body, flags)) return false;
3772
+ if (hasCatastrophicShape(body, flags) === true) return false;
3773
+ try {
3774
+ new RegExp(body, flags);
3775
+ } catch {
3776
+ return false;
3777
+ }
3778
+ return true;
3779
+ }
3576
3780
  function ctypeResult(strValue, isClass, matchVal) {
3577
3781
  if (strValue === "") {
3578
3782
  return false;
@@ -5296,7 +5500,7 @@ function concatChunks(chunks, total) {
5296
5500
  }
5297
5501
  function parseBody(rawBody, contentType) {
5298
5502
  const ct = String(contentType || "").toLowerCase();
5299
- const isJson = ct.includes("application/json") || /\+json\b/.test(ct);
5503
+ const isJson2 = ct.includes("application/json") || /\+json\b/.test(ct);
5300
5504
  const isForm = ct.includes("application/x-www-form-urlencoded");
5301
5505
  const isMultipart = ct.includes("multipart/form-data");
5302
5506
  const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
@@ -5310,7 +5514,7 @@ function parseBody(rawBody, contentType) {
5310
5514
  for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
5311
5515
  return { body, files: void 0 };
5312
5516
  }
5313
- if (isJson || isAmbiguous) {
5517
+ if (isJson2 || isAmbiguous) {
5314
5518
  try {
5315
5519
  const parsed = JSON.parse(rawBody);
5316
5520
  if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
@@ -5326,22 +5530,68 @@ function parseMultipart(rawBody, boundary) {
5326
5530
  const sep = /\r?\n\r?\n/.exec(part);
5327
5531
  if (!sep) continue;
5328
5532
  const rawHeaders = part.slice(0, sep.index);
5329
- const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
5330
- if (!disposition) continue;
5331
- const name = /name="([^"]*)"/i.exec(disposition)?.[1];
5332
- if (name == null) continue;
5533
+ const disposition = /content-disposition:([^\r\n]*)/i.exec(rawHeaders)?.[1];
5534
+ if (disposition === void 0) continue;
5535
+ const { names, filename } = dispositionFields(disposition);
5536
+ if (names.length === 0) continue;
5333
5537
  const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
5334
- const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
5335
- if (filename !== void 0) {
5336
- const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
5337
- const file = { filename, type: partType, content };
5338
- appendOwn(files, name, file);
5339
- } else {
5340
- appendOwn(body, name, content);
5538
+ const file = filename === void 0 ? void 0 : { filename, type: /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "", content };
5539
+ for (const name of names) {
5540
+ if (file) appendOwn(files, name, file);
5541
+ else appendOwn(body, name, content);
5341
5542
  }
5342
5543
  }
5343
5544
  return { body, files };
5344
5545
  }
5546
+ function dispositionFields(value) {
5547
+ const text = String(value);
5548
+ const names = /* @__PURE__ */ new Set();
5549
+ let filename;
5550
+ let extendedFilename;
5551
+ let i = text.indexOf(";");
5552
+ while (i !== -1 && i < text.length) {
5553
+ i++;
5554
+ while (text[i] === " " || text[i] === " ") i++;
5555
+ let key = "";
5556
+ while (i < text.length && text[i] !== "=" && text[i] !== ";") key += text[i++];
5557
+ key = key.trim().toLowerCase();
5558
+ let val = null;
5559
+ if (text[i] === "=") {
5560
+ i++;
5561
+ while (text[i] === " " || text[i] === " ") i++;
5562
+ if (text[i] === '"') {
5563
+ i++;
5564
+ val = "";
5565
+ while (i < text.length && text[i] !== '"') {
5566
+ if (text[i] === "\\" && i + 1 < text.length) i++;
5567
+ val += text[i++];
5568
+ }
5569
+ i++;
5570
+ while (i < text.length && text[i] !== ";") i++;
5571
+ } else {
5572
+ let bare = "";
5573
+ while (i < text.length && text[i] !== ";") bare += text[i++];
5574
+ val = bare.trim();
5575
+ }
5576
+ }
5577
+ if (val !== null) {
5578
+ if (key === "name") names.add(val);
5579
+ else if (key === "filename") filename = val;
5580
+ else if (key === "filename*") extendedFilename = decodeExtendedValue(val);
5581
+ }
5582
+ i = i < text.length && text[i] === ";" ? i : -1;
5583
+ }
5584
+ return { names: [...names], filename: extendedFilename ?? filename };
5585
+ }
5586
+ function decodeExtendedValue(value) {
5587
+ const match = /^([^']*)'[^']*'(.*)$/.exec(value);
5588
+ if (!match) return value;
5589
+ try {
5590
+ return decodeURIComponent(match[2]);
5591
+ } catch {
5592
+ return match[2];
5593
+ }
5594
+ }
5345
5595
  function parseCookies(header) {
5346
5596
  const cookies = {};
5347
5597
  if (!header) {
@@ -5612,14 +5862,29 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
5612
5862
  if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
5613
5863
  });
5614
5864
  }
5865
+ let syncBuiltins = () => {
5866
+ };
5867
+ try {
5868
+ const { syncBuiltinESMExports } = await import("module");
5869
+ if (typeof syncBuiltinESMExports === "function") syncBuiltins = syncBuiltinESMExports;
5870
+ } catch {
5871
+ }
5872
+ let patchedAny = false;
5615
5873
  for (const moduleName of ["node:http", "node:https"]) {
5616
5874
  try {
5617
5875
  const mod = await import(moduleName);
5618
5876
  const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
5619
- if (restore) restores.push(restore);
5877
+ if (restore) {
5878
+ patchedAny = true;
5879
+ restores.push(() => {
5880
+ restore();
5881
+ syncBuiltins();
5882
+ });
5883
+ }
5620
5884
  } catch {
5621
5885
  }
5622
5886
  }
5887
+ if (patchedAny) syncBuiltins();
5623
5888
  return () => {
5624
5889
  for (const restore of restores) {
5625
5890
  try {
@@ -5633,18 +5898,22 @@ function patchHttpModule(http, block, screen, skip) {
5633
5898
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
5634
5899
  const originalRequest = http.request;
5635
5900
  const originalGet = http.get;
5636
- const wrap = (original) => function(...args) {
5901
+ const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
5902
+ const guardArgs = (args) => {
5637
5903
  const target = extractHttpTarget(args);
5638
5904
  if (target && block(target.url, target.host, target.method)) {
5639
5905
  throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
5640
5906
  }
5641
5907
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
5642
5908
  try {
5643
- args = withScreeningLookup(args, target, block, screen.lookup, skip);
5909
+ return withScreeningLookup(args, target, block, screen.lookup, skip);
5644
5910
  } catch {
5645
5911
  }
5646
5912
  }
5647
- return original.apply(this, args);
5913
+ return args;
5914
+ };
5915
+ const wrap = (original) => function(...args) {
5916
+ return original.apply(this, guardArgs(args));
5648
5917
  };
5649
5918
  const guardedRequest = wrap(originalRequest);
5650
5919
  http.request = guardedRequest;
@@ -5653,10 +5922,21 @@ function patchHttpModule(http, block, screen, skip) {
5653
5922
  guardedGet = wrap(originalGet);
5654
5923
  http.get = guardedGet;
5655
5924
  }
5925
+ let GuardedClientRequest;
5926
+ if (OriginalClientRequest) {
5927
+ GuardedClientRequest = function ClientRequest(...args) {
5928
+ const screened = guardArgs(args);
5929
+ return new.target ? Reflect.construct(OriginalClientRequest, screened, new.target) : Reflect.construct(OriginalClientRequest, screened);
5930
+ };
5931
+ GuardedClientRequest.prototype = OriginalClientRequest.prototype;
5932
+ Object.setPrototypeOf(GuardedClientRequest, OriginalClientRequest);
5933
+ http.ClientRequest = GuardedClientRequest;
5934
+ }
5656
5935
  http.__patchstackGuarded = true;
5657
5936
  return () => {
5658
5937
  if (http.request === guardedRequest) http.request = originalRequest;
5659
5938
  if (guardedGet && http.get === guardedGet) http.get = originalGet;
5939
+ if (GuardedClientRequest && http.ClientRequest === GuardedClientRequest) http.ClientRequest = OriginalClientRequest;
5660
5940
  delete http.__patchstackGuarded;
5661
5941
  };
5662
5942
  }
@@ -5666,19 +5946,27 @@ function extractHttpTarget(args) {
5666
5946
  if (typeof first === "string" || first instanceof URL) {
5667
5947
  const url = new URL(String(first));
5668
5948
  const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
5669
- return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
5670
- }
5671
- if (first && typeof first === "object") {
5672
- const host = normalizeHost(first.hostname || first.host) || "localhost";
5673
- const protocol = first.protocol || "http:";
5674
- const port = first.port ? `:${first.port}` : "";
5675
- const path9 = first.path || "/";
5676
- return { url: `${protocol}//${host}${port}${path9}`, host, method: first.method || "GET" };
5949
+ return targetOf({ ...optionsFromUrl(url), ...opts ?? {} });
5677
5950
  }
5951
+ if (first && typeof first === "object") return targetOf(first);
5678
5952
  } catch {
5679
5953
  }
5680
5954
  return null;
5681
5955
  }
5956
+ function optionsFromUrl(url) {
5957
+ const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
5958
+ const options = { protocol: url.protocol, hostname, path: `${url.pathname || "/"}${url.search}` };
5959
+ if (url.port !== "") options.port = Number(url.port);
5960
+ return options;
5961
+ }
5962
+ function targetOf(options) {
5963
+ const host = normalizeHost(options.hostname || options.host) || "localhost";
5964
+ const protocol = options.protocol || "http:";
5965
+ const port = options.port ? `:${options.port}` : "";
5966
+ const path9 = options.path || "/";
5967
+ const authority = host.includes(":") ? `[${host}]` : host;
5968
+ return { url: `${protocol}//${authority}${port}${path9}`, host, method: options.method || "GET" };
5969
+ }
5682
5970
  function normalizeHost(raw) {
5683
5971
  const host = String(raw || "").trim();
5684
5972
  const bracketed = /^\[([^\]]+)\]/.exec(host);
@@ -6060,19 +6348,49 @@ async function loadFs() {
6060
6348
  }
6061
6349
  return fsMod;
6062
6350
  }
6351
+ async function sourceIdentity(options = {}) {
6352
+ if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
6353
+ const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
6354
+ return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
6355
+ }
6356
+ if (typeof options.token === "string" && options.token !== "") {
6357
+ const digest = await sha256Hex(options.token);
6358
+ if (digest === null) return null;
6359
+ const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
6360
+ return `token:${digest}@${endpoint}`;
6361
+ }
6362
+ return null;
6363
+ }
6364
+ async function sha256Hex(text) {
6365
+ try {
6366
+ const subtle = globalThis.crypto?.subtle;
6367
+ if (!subtle || typeof TextEncoder === "undefined") return null;
6368
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
6369
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
6370
+ } catch {
6371
+ return null;
6372
+ }
6373
+ }
6063
6374
  function makeStore(options = {}) {
6064
6375
  let mem = null;
6065
6376
  const durable = durableTier(options);
6377
+ let identity;
6378
+ const identityOf = () => identity ??= sourceIdentity(options);
6066
6379
  return {
6067
6380
  async read() {
6068
6381
  if (mem) return mem;
6382
+ const source = await identityOf();
6383
+ if (source === null) return null;
6069
6384
  const env = await durable.read();
6070
- if (env) mem = env;
6385
+ if (!env || env.source !== source) return null;
6386
+ mem = env;
6071
6387
  return env;
6072
6388
  },
6073
6389
  async write(env) {
6074
- if (env) mem = env;
6075
- await durable.write(env);
6390
+ if (!env) return;
6391
+ const stamped = { ...env, source: await identityOf() };
6392
+ mem = stamped;
6393
+ await durable.write(stamped);
6076
6394
  }
6077
6395
  };
6078
6396
  }
@@ -6194,11 +6512,12 @@ function toEnvelope(value) {
6194
6512
  buildId,
6195
6513
  // A confirmation belongs to the presentation stored beside it. A crossed or partially written
6196
6514
  // envelope confirms nothing, even when one of its fields happens to name the current map.
6197
- matchedBuildId: buildId !== null && matched === buildId ? matched : null
6515
+ matchedBuildId: buildId !== null && matched === buildId ? matched : null,
6516
+ source: typeof value.source === "string" ? value.source : null
6198
6517
  };
6199
6518
  }
6200
6519
  if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
6201
- return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
6520
+ return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
6202
6521
  }
6203
6522
  return null;
6204
6523
  }
@@ -6266,13 +6585,17 @@ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
6266
6585
  ...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
6267
6586
  ...held > unusable ? [reason] : []
6268
6587
  ];
6269
- notify(
6270
- options.onError,
6271
- new Error(`${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`),
6272
- "onError"
6273
- );
6588
+ const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
6589
+ if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
6590
+ else warnHeldOnce(message);
6274
6591
  return { ...bundle, firewall };
6275
6592
  }
6593
+ var warnedHeld = /* @__PURE__ */ new Set();
6594
+ function warnHeldOnce(message) {
6595
+ if (warnedHeld.has(message)) return;
6596
+ warnedHeld.add(message);
6597
+ console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
6598
+ }
6276
6599
  function confirmedBy(res, presented) {
6277
6600
  if (presented === null) return null;
6278
6601
  const verdict = res?.build;
@@ -6447,6 +6770,60 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
6447
6770
  }
6448
6771
  };
6449
6772
  }
6773
+ var RECOVERY_DELAYS_MS = [5e3, 15e3, 45e3, 12e4, 3e5, 6e5];
6774
+ function startRecovery(tick, { onError } = {}) {
6775
+ let stopped = false;
6776
+ let attempt = 0;
6777
+ let timer = null;
6778
+ const schedule = () => {
6779
+ if (stopped) return;
6780
+ const delay = RECOVERY_DELAYS_MS[Math.min(attempt, RECOVERY_DELAYS_MS.length - 1)];
6781
+ timer = setTimeout(run, delay * (1 - Math.random() * JITTER_FRACTION3));
6782
+ timer?.unref?.();
6783
+ };
6784
+ const run = async () => {
6785
+ if (stopped) return;
6786
+ attempt++;
6787
+ try {
6788
+ const status = await tick();
6789
+ if (!status || status.ok !== false) {
6790
+ stopped = true;
6791
+ return;
6792
+ }
6793
+ } catch (err) {
6794
+ notify(onError, err, "onError");
6795
+ }
6796
+ schedule();
6797
+ };
6798
+ schedule();
6799
+ return {
6800
+ stop() {
6801
+ stopped = true;
6802
+ if (timer) clearTimeout(timer);
6803
+ }
6804
+ };
6805
+ }
6806
+ function serialise(tick) {
6807
+ let running = null;
6808
+ let next = null;
6809
+ const run = () => {
6810
+ if (!running) {
6811
+ running = Promise.resolve().then(() => tick()).finally(() => {
6812
+ running = null;
6813
+ });
6814
+ return running;
6815
+ }
6816
+ if (!next) {
6817
+ next = running.catch(() => {
6818
+ }).then(() => {
6819
+ next = null;
6820
+ return run();
6821
+ });
6822
+ }
6823
+ return next;
6824
+ };
6825
+ return run;
6826
+ }
6450
6827
  function makeRefreshHandler(tick, secret) {
6451
6828
  let inflight2 = null;
6452
6829
  const runOnce = () => {
@@ -7602,7 +7979,7 @@ function createSupabaseGuard({
7602
7979
  statusText: upstream.statusText,
7603
7980
  headers: responseHeaders(upstream.headers)
7604
7981
  });
7605
- return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
7982
+ return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
7606
7983
  };
7607
7984
  }
7608
7985
 
@@ -7696,6 +8073,7 @@ async function createProtection(options = {}) {
7696
8073
  "onDetect"
7697
8074
  );
7698
8075
  };
8076
+ const nodeHttp = await import("http").then((m) => m.default ?? m, () => null);
7699
8077
  const store = makeStore(options);
7700
8078
  const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
7701
8079
  const pulseAuth = await resolvePulseAuth(options);
@@ -7721,6 +8099,21 @@ async function createProtection(options = {}) {
7721
8099
  pulseAuth,
7722
8100
  detectionState: preFetchState
7723
8101
  });
8102
+ let ruleSource = { ...bundle.source ?? { ok: true, origin: "empty" } };
8103
+ const warnedSources = /* @__PURE__ */ new Set();
8104
+ const hasLiveSource = Boolean(options.siteUuid || options.token);
8105
+ const noteRuleSource = (source) => {
8106
+ ruleSource = { ...source ?? { ok: true, origin: ruleSource.origin } };
8107
+ if (!hasLiveSource || typeof onError === "function" || ruleSource.ok !== false) return;
8108
+ const cause = `${ruleSource.origin}:${ruleSource.reason ?? ""}`;
8109
+ if (warnedSources.has(cause)) return;
8110
+ warnedSources.add(cause);
8111
+ const running = ruleSource.origin === "cache" ? "the last rules it received" : ruleSource.origin === "bundled" ? "its bundled fallback rules" : "no rules at all";
8112
+ console.warn(
8113
+ `[patchstack] the guard's rules are not current (${ruleSource.reason ?? "the rules source did not answer"}); it is running on ${running}. Pass { onError } to handle this yourself.`
8114
+ );
8115
+ };
8116
+ noteRuleSource(bundle.source);
7724
8117
  let detectionReporting = "not-enrolled";
7725
8118
  let currentOrigin;
7726
8119
  const applyReportingState = async (origin) => {
@@ -7899,7 +8292,9 @@ async function createProtection(options = {}) {
7899
8292
  const spanRedactors = redactors.filter((r) => !r.jsonPath);
7900
8293
  if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
7901
8294
  if (!spanRedactors.length) continue;
8295
+ const beforeSpan = body;
7902
8296
  body = applyRedactors(body, spanRedactors, mask, transform);
8297
+ if (body !== beforeSpan && !preservesJsonStructure(beforeSpan, body)) return { verdict: "block" };
7903
8298
  if (transform) continue;
7904
8299
  for (const name of Object.keys(headers)) {
7905
8300
  const value = headers[name];
@@ -7914,6 +8309,7 @@ async function createProtection(options = {}) {
7914
8309
  }
7915
8310
  }
7916
8311
  }
8312
+ if (body !== text && isJson(text) && !isJson(body)) return { verdict: "block" };
7917
8313
  for (const rule of headerMutations) applyHeaderMutation(headers, rule);
7918
8314
  return { verdict: "redact", body, headers };
7919
8315
  };
@@ -8016,7 +8412,9 @@ async function createProtection(options = {}) {
8016
8412
  const screenResp = async (response, reqCtx) => {
8017
8413
  const read = await readTextResponse(response, screenCap);
8018
8414
  if (read.skip) {
8019
- if (read.skip !== "not-a-response") recordSkip("response", read.skip, { status: response?.status });
8415
+ if (read.skip !== "not-a-response") {
8416
+ recordSkip("response", read.skip, { status: response?.status, ...read.encoding ? { encoding: read.encoding } : {} });
8417
+ }
8020
8418
  if (read.skip === "not-a-response") return response;
8021
8419
  return hardenHeadersOnly(response, reqCtx);
8022
8420
  }
@@ -8097,6 +8495,11 @@ async function createProtection(options = {}) {
8097
8495
  const wrapNodeResponse = (res, reqCtx) => {
8098
8496
  const origWrite = res.write.bind(res);
8099
8497
  const origEnd = res.end.bind(res);
8498
+ const setHeaderNow = typeof res.setHeader === "function" ? res.setHeader.bind(res) : null;
8499
+ const removeHeaderNow = typeof res.removeHeader === "function" ? res.removeHeader.bind(res) : null;
8500
+ const sentAccessor = accessorOf(res, "headersSent");
8501
+ let sentValue = sentAccessor ? void 0 : res.headersSent;
8502
+ const reallySent = () => Boolean(sentAccessor ? sentAccessor.call(res) : sentValue);
8100
8503
  const chunks = [];
8101
8504
  let size = 0;
8102
8505
  let overflow = false;
@@ -8107,7 +8510,7 @@ async function createProtection(options = {}) {
8107
8510
  if (hardened) return null;
8108
8511
  hardened = true;
8109
8512
  try {
8110
- if (res.headersSent || typeof res.setHeader !== "function") return null;
8513
+ if (reallySent() || !setHeaderNow) return null;
8111
8514
  const set2 = typeof res.getHeaders === "function" ? res.getHeaders() : {};
8112
8515
  const headers = effective && effective.headers ? { ...set2, ...effective.headers } : set2;
8113
8516
  const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
@@ -8119,8 +8522,8 @@ async function createProtection(options = {}) {
8119
8522
  if (!headerValueChanged(headers[name], value)) continue;
8120
8523
  changed.set(name.toLowerCase(), value);
8121
8524
  try {
8122
- if (value === null || value === void 0) res.removeHeader?.(name);
8123
- else res.setHeader(name, value);
8525
+ if (value === null || value === void 0) removeHeaderNow?.(name);
8526
+ else setHeaderNow(name, value);
8124
8527
  } catch {
8125
8528
  }
8126
8529
  }
@@ -8131,26 +8534,139 @@ async function createProtection(options = {}) {
8131
8534
  }
8132
8535
  };
8133
8536
  const stillToAnswer = (rule) => !hardensWithoutBody(rule);
8134
- if (typeof res.writeHead === "function") {
8135
- const origWriteHead = res.writeHead.bind(res);
8537
+ let pendingHead = null;
8538
+ let sending = false;
8539
+ const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
8540
+ if (origWriteHead) {
8136
8541
  res.writeHead = function(...args) {
8137
- const at = writeHeadHeaderIndex(args);
8138
- const given = at === -1 ? null : args[at];
8139
- const changed = hardenBeforeFlush({
8140
- status: typeof args[0] === "number" ? args[0] : void 0,
8141
- headers: given ? writeHeadHeaderObject(given) : null
8142
- });
8143
- if (!changed || !given) return origWriteHead(...args);
8144
- const rewritten = [...args];
8145
- rewritten[at] = rewriteWriteHeadHeaders(given, changed);
8146
- return origWriteHead(...rewritten);
8542
+ if (sending || reallySent()) return origWriteHead(...args);
8543
+ if (pendingHead) throw headersSentError("write");
8544
+ const judged = judgeWriteHead(args);
8545
+ if (judged) {
8546
+ res.statusCode = judged.statusCode;
8547
+ res.statusMessage = judged.statusMessage;
8548
+ mirrorHeaders(judged.headers);
8549
+ pendingHead = args;
8550
+ } else {
8551
+ pendingHead = args;
8552
+ if (typeof args[0] === "number") res.statusCode = args[0];
8553
+ if (typeof args[1] === "string") res.statusMessage = args[1];
8554
+ }
8555
+ return res;
8147
8556
  };
8148
8557
  }
8558
+ function judgeWriteHead(args) {
8559
+ const ServerResponse = nodeHttp?.ServerResponse;
8560
+ if (typeof ServerResponse !== "function") return null;
8561
+ const detached = new ServerResponse({ method: res.req?.method ?? "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} });
8562
+ if (res.statusMessage !== void 0) detached.statusMessage = res.statusMessage;
8563
+ for (const [name, value] of headerEntries(res)) detached.setHeader(name, value);
8564
+ detached.writeHead(...args);
8565
+ return { statusCode: detached.statusCode, statusMessage: detached.statusMessage, headers: headerEntries(detached) };
8566
+ }
8567
+ function headerEntries(target) {
8568
+ const names = typeof target.getRawHeaderNames === "function" ? target.getRawHeaderNames() : typeof target.getHeaderNames === "function" ? target.getHeaderNames() : [];
8569
+ return names.map((name) => [name, target.getHeader(name)]);
8570
+ }
8571
+ function mirrorHeaders(entries) {
8572
+ if (!setHeaderNow) return;
8573
+ for (const [name, value] of entries) {
8574
+ const current = res.getHeader?.(name);
8575
+ if (current === void 0 || headerValueChanged(current, value)) setHeaderNow(name, value);
8576
+ }
8577
+ }
8578
+ Object.defineProperty(res, "headersSent", {
8579
+ configurable: true,
8580
+ enumerable: true,
8581
+ get: () => pendingHead !== null || reallySent(),
8582
+ // A response-like object that tracks the flag itself keeps doing so.
8583
+ set: (value) => {
8584
+ if (!sentAccessor) sentValue = value;
8585
+ }
8586
+ });
8587
+ for (const [method, verb] of [["setHeader", "set"], ["removeHeader", "remove"], ["appendHeader", "append"]]) {
8588
+ const original = typeof res[method] === "function" ? res[method].bind(res) : null;
8589
+ if (!original) continue;
8590
+ res[method] = function(...args) {
8591
+ if (pendingHead) throw headersSentError(verb);
8592
+ return original(...args);
8593
+ };
8594
+ }
8595
+ if (typeof res.flushHeaders === "function") {
8596
+ const origFlushHeaders = res.flushHeaders.bind(res);
8597
+ res.flushHeaders = function() {
8598
+ sendHead();
8599
+ return origFlushHeaders();
8600
+ };
8601
+ }
8602
+ let hardenDone = false;
8603
+ let hardenedValues = null;
8604
+ const hardenOnce = () => {
8605
+ if (hardenDone) return hardenedValues;
8606
+ hardenDone = true;
8607
+ if (!pendingHead) {
8608
+ hardenedValues = hardenBeforeFlush();
8609
+ return hardenedValues;
8610
+ }
8611
+ const at = writeHeadHeaderIndex(pendingHead);
8612
+ hardenedValues = hardenBeforeFlush({
8613
+ status: typeof pendingHead[0] === "number" ? pendingHead[0] : void 0,
8614
+ headers: at === -1 ? null : writeHeadHeaderObject(pendingHead[at])
8615
+ });
8616
+ return hardenedValues;
8617
+ };
8618
+ const effectiveHead = () => {
8619
+ const set2 = typeof res.getHeaders === "function" ? { ...res.getHeaders() } : {};
8620
+ if (typeof res.getHeaders !== "function" && typeof res.getHeader === "function") {
8621
+ for (const name of ["content-type", "content-length"]) {
8622
+ const value = res.getHeader(name);
8623
+ if (value !== void 0) set2[name] = value;
8624
+ }
8625
+ }
8626
+ if (!pendingHead) return { status: res.statusCode, headers: set2 };
8627
+ const at = writeHeadHeaderIndex(pendingHead);
8628
+ return {
8629
+ status: typeof pendingHead[0] === "number" ? pendingHead[0] : res.statusCode,
8630
+ headers: at === -1 ? set2 : { ...set2, ...writeHeadHeaderObject(pendingHead[at]) }
8631
+ };
8632
+ };
8633
+ const sendHead = (changes) => {
8634
+ if (sending) return;
8635
+ const hardened2 = hardenOnce();
8636
+ sending = true;
8637
+ if (!pendingHead) {
8638
+ if (changes) applyToResponse(changes);
8639
+ return;
8640
+ }
8641
+ const args = [...pendingHead];
8642
+ pendingHead = null;
8643
+ const merged = new Map(hardened2 ?? []);
8644
+ for (const [name, value] of changes?.headers ?? []) merged.set(name, value);
8645
+ if (changes?.status !== void 0) {
8646
+ args[0] = changes.status;
8647
+ if (typeof args[1] === "string") args.splice(1, 1);
8648
+ }
8649
+ const headerAt = writeHeadHeaderIndex(args);
8650
+ if (headerAt !== -1 && merged.size) args[headerAt] = rewriteWriteHeadHeaders(args[headerAt], merged);
8651
+ applyToResponse({ headers: merged });
8652
+ origWriteHead(...args);
8653
+ };
8654
+ const applyToResponse = (changes) => {
8655
+ if (changes.status !== void 0) res.statusCode = changes.status;
8656
+ for (const [name, value] of changes.headers ?? []) {
8657
+ try {
8658
+ if (value === null || value === void 0) removeHeaderNow?.(name);
8659
+ else setHeaderNow?.(name, value);
8660
+ } catch {
8661
+ }
8662
+ }
8663
+ };
8149
8664
  const collect = (chunk, enc) => {
8150
8665
  if (chunk == null) return;
8151
8666
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
8152
8667
  size += buf.length;
8153
8668
  if (size > MAX) {
8669
+ sendHead();
8154
8670
  for (const c of chunks) origWrite(c);
8155
8671
  chunks.length = 0;
8156
8672
  origWrite(buf);
@@ -8161,7 +8677,6 @@ async function createProtection(options = {}) {
8161
8677
  chunks.push(buf);
8162
8678
  };
8163
8679
  res.write = function(chunk, enc, cb) {
8164
- hardenBeforeFlush();
8165
8680
  if (overflow) return origWrite(chunk, enc, cb);
8166
8681
  collect(chunk, enc);
8167
8682
  if (typeof enc === "function") enc();
@@ -8169,7 +8684,6 @@ async function createProtection(options = {}) {
8169
8684
  return true;
8170
8685
  };
8171
8686
  res.end = function(chunk, enc, cb) {
8172
- hardenBeforeFlush();
8173
8687
  if (typeof chunk === "function") {
8174
8688
  cb = chunk;
8175
8689
  chunk = void 0;
@@ -8184,68 +8698,77 @@ async function createProtection(options = {}) {
8184
8698
  }
8185
8699
  collect(chunk, enc);
8186
8700
  if (overflow) return origEnd(cb);
8701
+ hardenOnce();
8702
+ const passThrough = () => {
8703
+ sendHead();
8704
+ for (const c of chunks) origWrite(c);
8705
+ return origEnd(cb);
8706
+ };
8187
8707
  const buffer = Buffer.concat(chunks);
8188
- let ct = res.getHeader ? res.getHeader("content-type") : void 0;
8708
+ const head = effectiveHead();
8709
+ let ct = head.headers["content-type"];
8189
8710
  if (Array.isArray(ct)) ct = ct[0];
8190
8711
  const kind = screenableContentType(ct);
8191
8712
  if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
8192
8713
  recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
8193
- for (const c of chunks) origWrite(c);
8194
- return origEnd(cb);
8714
+ return passThrough();
8715
+ }
8716
+ const codings = declaredCodings(head.headers["content-encoding"]);
8717
+ if (codings.length > 0 && stillEncoded(buffer)) {
8718
+ recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
8719
+ return passThrough();
8195
8720
  }
8196
8721
  const text = buffer.toString("utf8");
8197
8722
  let r;
8198
8723
  try {
8199
- r = screenText(
8200
- text,
8201
- { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} },
8202
- reqCtx,
8203
- answeredWithoutBody ? stillToAnswer : void 0
8204
- );
8724
+ r = screenText(text, head, reqCtx, answeredWithoutBody ? stillToAnswer : void 0);
8205
8725
  } catch (err) {
8206
8726
  notify(onError, err, "onError");
8207
- for (const c of chunks) origWrite(c);
8208
- return origEnd(cb);
8727
+ return passThrough();
8728
+ }
8729
+ if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
8730
+ if (reallySent() && res.chunkedEncoding !== true) {
8731
+ notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
8732
+ res.destroy?.();
8733
+ return res;
8209
8734
  }
8210
8735
  if (r.verdict === "block") {
8211
- res.statusCode = 500;
8212
- try {
8213
- res.setHeader("content-type", "application/json");
8214
- } catch {
8736
+ const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
8737
+ if (reallySent()) {
8738
+ res.destroy?.();
8739
+ return res;
8215
8740
  }
8216
- return origEnd(JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" }), cb);
8741
+ sendHead({
8742
+ status: 500,
8743
+ headers: /* @__PURE__ */ new Map([
8744
+ ["content-type", "application/json"],
8745
+ ["content-length", String(Buffer.byteLength(body))],
8746
+ ["transfer-encoding", null]
8747
+ ])
8748
+ });
8749
+ return origEnd(body, cb);
8217
8750
  }
8218
- if (r.verdict === "redact") {
8219
- try {
8220
- res.removeHeader && res.removeHeader("content-length");
8221
- } catch {
8751
+ const changed = /* @__PURE__ */ new Map();
8752
+ if (r.headers) {
8753
+ for (const [name, value] of Object.entries(r.headers)) {
8754
+ if (name.toLowerCase() === "content-length") continue;
8755
+ if (!headerValueChanged(head.headers[name], value)) continue;
8756
+ changed.set(name.toLowerCase(), value);
8222
8757
  }
8223
- if (r.headers && res.setHeader) {
8224
- const current = res.getHeaders ? res.getHeaders() : {};
8225
- for (const [name, value] of Object.entries(r.headers)) {
8226
- if (name.toLowerCase() === "content-length") continue;
8227
- if (value === null || value === void 0) {
8228
- try {
8229
- res.removeHeader && res.removeHeader(name);
8230
- } catch {
8231
- }
8232
- } else if (Array.isArray(value)) {
8233
- try {
8234
- res.setHeader(name, value);
8235
- } catch {
8236
- }
8237
- } else if (typeof value === "string" && current[name] !== value) {
8238
- try {
8239
- res.setHeader(name, value);
8240
- } catch {
8241
- }
8242
- }
8243
- }
8758
+ }
8759
+ if (!reallySent()) {
8760
+ const declared = head.headers["content-length"];
8761
+ if (declared === void 0) {
8762
+ changed.set("content-length", null);
8763
+ } else {
8764
+ changed.set("content-length", String(Buffer.byteLength(r.body)));
8765
+ changed.set("transfer-encoding", null);
8244
8766
  }
8245
- return origEnd(r.body, cb);
8767
+ sendHead({ headers: changed });
8768
+ } else if (changed.size) {
8769
+ recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
8246
8770
  }
8247
- for (const c of chunks) origWrite(c);
8248
- return origEnd(cb);
8771
+ return origEnd(r.body, cb);
8249
8772
  };
8250
8773
  };
8251
8774
  const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
@@ -8458,6 +8981,7 @@ async function createProtection(options = {}) {
8458
8981
  notify(onError, err, "onError");
8459
8982
  }
8460
8983
  }
8984
+ let recovery = null;
8461
8985
  const runRefreshTick = async () => {
8462
8986
  if (reporter) {
8463
8987
  try {
@@ -8474,23 +8998,33 @@ async function createProtection(options = {}) {
8474
8998
  });
8475
8999
  mode = resolveMode(options, next);
8476
9000
  applyBundle(next);
9001
+ noteRuleSource(next.source);
9002
+ if (next.source?.ok !== false) recovery?.stop();
8477
9003
  await applyAndAcknowledge(next.source?.origin, declaredState);
8478
9004
  if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
8479
9005
  return next.source ?? { ok: true };
8480
9006
  };
9007
+ const refreshTick = serialise(runRefreshTick);
8481
9008
  if (live) {
8482
- protection.refresh = () => runRefreshTick();
8483
- protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
9009
+ protection.refresh = () => refreshTick();
9010
+ protection.refreshHandler = () => makeRefreshHandler(refreshTick, refreshSecret);
8484
9011
  }
8485
- const loop = options.refreshMs > 0 && live ? startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError }) : null;
9012
+ const loop = options.refreshMs > 0 && live ? startRefresh(refreshTick, { refreshMs: options.refreshMs, onError }) : null;
9013
+ const canAsk = Boolean(options.token || pulseAuth);
9014
+ recovery = live && canAsk && !loop && ruleSource.ok === false ? startRecovery(refreshTick, { onError }) : null;
8486
9015
  protection.stop = () => {
8487
9016
  loop?.stop();
9017
+ recovery?.stop();
8488
9018
  const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
8489
9019
  (wait) => wait && typeof wait.then === "function"
8490
9020
  );
8491
9021
  return Promise.all(outstanding).then(() => void 0);
8492
9022
  };
8493
9023
  protection.stopRefresh = protection.stop;
9024
+ Object.defineProperty(protection, "ruleSource", {
9025
+ get: () => ({ ...ruleSource }),
9026
+ enumerable: true
9027
+ });
8494
9028
  Object.defineProperty(protection, "detectionReporting", {
8495
9029
  get: () => detectionReporting,
8496
9030
  enumerable: true
@@ -8501,6 +9035,16 @@ async function createProtection(options = {}) {
8501
9035
  });
8502
9036
  return protection;
8503
9037
  }
9038
+ function accessorOf(obj, name) {
9039
+ for (let o = obj; o; o = Object.getPrototypeOf(o)) {
9040
+ const d = Object.getOwnPropertyDescriptor(o, name);
9041
+ if (d) return typeof d.get === "function" ? d.get : null;
9042
+ }
9043
+ return null;
9044
+ }
9045
+ function headersSentError(verb) {
9046
+ return Object.assign(new Error(`Cannot ${verb} headers after they are sent to the client`), { code: "ERR_HTTP_HEADERS_SENT" });
9047
+ }
8504
9048
  function resolveMode(options, bundle) {
8505
9049
  const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
8506
9050
  if (env === "block" || env === "dry-run") return env;
@@ -8573,11 +9117,26 @@ function looksBinary(bytes) {
8573
9117
  }
8574
9118
  return n > 0 && ctrl / n > 0.1;
8575
9119
  }
9120
+ function declaredCodings(value) {
9121
+ const raw = Array.isArray(value) ? value.join(",") : String(value ?? "");
9122
+ return raw.toLowerCase().split(",").map((c) => c.trim()).filter((c) => c !== "" && c !== "identity");
9123
+ }
9124
+ function stillEncoded(bytes) {
9125
+ if (looksBinary(bytes)) return true;
9126
+ try {
9127
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
9128
+ return false;
9129
+ } catch {
9130
+ return true;
9131
+ }
9132
+ }
8576
9133
  async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
8577
9134
  if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
8578
9135
  const ct = response.headers?.get?.("content-type") || "";
8579
9136
  const kind = screenableContentType(ct);
8580
9137
  if (kind === "skip") return { skip: baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" };
9138
+ const codings = declaredCodings(response.headers?.get?.("content-encoding"));
9139
+ const encodedSkip = () => ({ skip: "encoded-body", encoding: codings.join(", ") });
8581
9140
  const sniff = kind === "sniff";
8582
9141
  const len = Number(response.headers?.get?.("content-length") || 0);
8583
9142
  if (len && len > cap) return { skip: "body-cap" };
@@ -8621,13 +9180,21 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
8621
9180
  } finally {
8622
9181
  reader.releaseLock();
8623
9182
  }
9183
+ const bytes = concatBytes(chunks, size);
9184
+ if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
8624
9185
  try {
8625
- return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
9186
+ return { text: new TextDecoder().decode(bytes) };
8626
9187
  } catch {
8627
9188
  return { skip: "decode-failed" };
8628
9189
  }
8629
9190
  }
8630
9191
  try {
9192
+ if (codings.length > 0) {
9193
+ const bytes = new Uint8Array(await clone.arrayBuffer());
9194
+ if (bytes.byteLength > cap) return { skip: "body-cap" };
9195
+ if (stillEncoded(bytes)) return encodedSkip();
9196
+ return { text: new TextDecoder().decode(bytes) };
9197
+ }
8631
9198
  const text = await clone.text();
8632
9199
  if (text.length > cap) return { skip: "body-cap" };
8633
9200
  if (sniff && looksBinary(new TextEncoder().encode(text.slice(0, 512)))) return { skip: "binary-body" };
@@ -8701,6 +9268,39 @@ function extractRedactors(rule) {
8701
9268
  function htmlEscape(str) {
8702
9269
  return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
8703
9270
  }
9271
+ function isJson(text) {
9272
+ try {
9273
+ JSON.parse(text);
9274
+ return true;
9275
+ } catch {
9276
+ return false;
9277
+ }
9278
+ }
9279
+ function preservesJsonStructure(before, after) {
9280
+ if (!isJson(before)) return true;
9281
+ if (!isJson(after)) return false;
9282
+ const expected = jsonStructure(before);
9283
+ const actual = jsonStructure(after);
9284
+ for (; ; ) {
9285
+ const left = expected.next();
9286
+ const right = actual.next();
9287
+ if (left.done || right.done) return left.done === right.done;
9288
+ if (left.value !== right.value) return false;
9289
+ }
9290
+ }
9291
+ function* jsonStructure(text) {
9292
+ const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|[^\s]/g;
9293
+ for (const match of text.matchAll(tokens)) {
9294
+ const token = match[0];
9295
+ if (token[0] !== '"') {
9296
+ yield "token:" + token;
9297
+ continue;
9298
+ }
9299
+ let next = match.index + token.length;
9300
+ while (text[next] === " " || text[next] === " " || text[next] === "\r" || text[next] === "\n") next++;
9301
+ yield text[next] === ":" ? "key:" + JSON.parse(token) : "string";
9302
+ }
9303
+ }
8704
9304
  function applyRedactors(body, redactors, mask, transform) {
8705
9305
  let out = body;
8706
9306
  for (const r of redactors) {
@@ -8743,10 +9343,12 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
8743
9343
  if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
8744
9344
  const head = text.trimStart()[0];
8745
9345
  if (head !== "{" && head !== "[") return text;
8746
- const preserved = preserveBigInts(text);
9346
+ let preserved;
8747
9347
  let obj;
8748
9348
  try {
8749
- obj = JSON.parse(preserved);
9349
+ JSON.parse(text);
9350
+ preserved = preserveBigInts(text, mask);
9351
+ obj = JSON.parse(preserved.text);
8750
9352
  } catch {
8751
9353
  return text;
8752
9354
  }
@@ -8755,65 +9357,44 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
8755
9357
  const pred = conditionPredicate(r.condition);
8756
9358
  walkLeaves(obj, r.jsonPath, (loc) => {
8757
9359
  try {
8758
- if (pred(loc.value)) {
8759
- loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
9360
+ const number = preserved.numbers.get(loc.value);
9361
+ if (pred(number === void 0 ? loc.value : Number(number))) {
9362
+ setOwn(loc.parent, loc.key, transform ? transform(number ?? String(loc.value)) : mask);
8760
9363
  changed = true;
8761
9364
  }
8762
9365
  } catch {
8763
9366
  }
8764
9367
  });
8765
9368
  }
8766
- return changed ? restoreBigInts(JSON.stringify(obj)) : text;
8767
- }
8768
- var BIGINT_OPEN = "__PSBIGINT_9c2f__";
8769
- var BIGINT_CLOSE = "__DNEGIB__";
8770
- function preserveBigInts(text) {
8771
- let out = "";
8772
- let inStr = false;
8773
- for (let i = 0; i < text.length; ) {
8774
- const ch = text[i];
8775
- if (inStr) {
8776
- out += ch;
8777
- if (ch === "\\") {
8778
- out += text[i + 1] ?? "";
8779
- i += 2;
8780
- continue;
8781
- }
8782
- if (ch === '"') inStr = false;
8783
- i++;
8784
- continue;
8785
- }
8786
- if (ch === '"') {
8787
- inStr = true;
8788
- out += ch;
8789
- i++;
8790
- continue;
8791
- }
8792
- if (ch === "-" || ch >= "0" && ch <= "9") {
8793
- let j = ch === "-" ? i + 1 : i;
8794
- let digits = 0;
8795
- while (j < text.length && text[j] >= "0" && text[j] <= "9") {
8796
- digits++;
8797
- j++;
8798
- }
8799
- const next = text[j];
8800
- const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
8801
- if (isIntToken && digits >= 16) {
8802
- out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
8803
- } else {
8804
- out += text.slice(i, j || i + 1);
8805
- }
8806
- i = j > i ? j : i + 1;
8807
- continue;
8808
- }
8809
- out += ch;
8810
- i++;
8811
- }
8812
- return out;
9369
+ return changed ? restoreBigInts(JSON.stringify(obj), preserved.numbers) : text;
8813
9370
  }
8814
- function restoreBigInts(text) {
8815
- if (!text.includes(BIGINT_OPEN)) return text;
8816
- return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
9371
+ function preserveBigInts(text, mask) {
9372
+ const occupied = /* @__PURE__ */ new Set([mask]);
9373
+ const integers = [];
9374
+ const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
9375
+ for (const match of text.matchAll(tokens)) {
9376
+ const token = match[0];
9377
+ if (token[0] === '"') occupied.add(JSON.parse(token));
9378
+ else if (/^-?\d{16,}$/.test(token)) integers.push({ start: match.index, token });
9379
+ }
9380
+ const numbers = /* @__PURE__ */ new Map();
9381
+ const chunks = [];
9382
+ let offset = 0;
9383
+ let index = 0;
9384
+ for (const { start, token } of integers) {
9385
+ let marker;
9386
+ do {
9387
+ marker = "__PSNUMBER_" + index++ + "__";
9388
+ } while (occupied.has(marker));
9389
+ numbers.set(marker, token);
9390
+ chunks.push(text.slice(offset, start), JSON.stringify(marker));
9391
+ offset = start + token.length;
9392
+ }
9393
+ chunks.push(text.slice(offset));
9394
+ return { text: chunks.join(""), numbers };
9395
+ }
9396
+ function restoreBigInts(text, numbers) {
9397
+ return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
8817
9398
  }
8818
9399
  function isHeaderMutation(action) {
8819
9400
  return action === "set-header" || action === "remove-header" || action === "harden-cookie";