@patchstack/connect 0.5.12 → 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
  }
@@ -1145,7 +1145,7 @@ function unreadableWarning(filename, report) {
1145
1145
  if (report.unreadable === 0) return null;
1146
1146
  const shown = report.samples.join(", ");
1147
1147
  const one = report.unreadable === 1;
1148
- 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.`;
1149
1149
  }
1150
1150
  var MAX_SAMPLES;
1151
1151
  var init_report = __esm({
@@ -2083,7 +2083,7 @@ async function scanLockfile(cwd) {
2083
2083
  warnings.push(staleWarning("node_modules/", missing));
2084
2084
  }
2085
2085
  warnings.push(
2086
- 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."
2087
2087
  );
2088
2088
  return manifestWith(walked, warnings);
2089
2089
  }
@@ -2095,7 +2095,7 @@ async function scanLockfile(cwd) {
2095
2095
  );
2096
2096
  }
2097
2097
  warnings.push(
2098
- `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.`
2099
2099
  );
2100
2100
  return manifestWith(firstParsed.packages, warnings);
2101
2101
  }
@@ -2109,12 +2109,12 @@ function otherCompleteSources(all, current, declared) {
2109
2109
  }
2110
2110
  function conflictWarning(filename, conflicts) {
2111
2111
  const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
2112
- 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.`;
2113
2113
  }
2114
2114
  function staleWarning(source, missing) {
2115
2115
  const sample = missing.slice(0, 3).join(", ");
2116
2116
  const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
2117
- 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.`;
2118
2118
  }
2119
2119
  async function presentLockfiles(cwd) {
2120
2120
  const probed = await Promise.all(
@@ -2149,7 +2149,7 @@ async function exists(filePath) {
2149
2149
  return false;
2150
2150
  }
2151
2151
  }
2152
- var import_promises9, import_node_path8, LOCKFILE_CANDIDATES;
2152
+ var import_promises9, import_node_path8, LOCKFILE_CANDIDATES, INSTALL_BY_SOURCE;
2153
2153
  var init_parsers = __esm({
2154
2154
  "src/parsers/index.ts"() {
2155
2155
  "use strict";
@@ -2173,6 +2173,13 @@ var init_parsers = __esm({
2173
2173
  { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
2174
2174
  { filename: "yarn.lock", strategy: "yarn-lockfile" }
2175
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
+ };
2176
2183
  }
2177
2184
  });
2178
2185
 
@@ -3527,6 +3534,184 @@ function hasAdjacentUnbounded(body, flags) {
3527
3534
  }
3528
3535
  return false;
3529
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
+ }
3530
3715
  function groupEnd(body, start) {
3531
3716
  let depth = 0;
3532
3717
  let inClass = false;
@@ -3553,6 +3738,8 @@ function warnRejectedPatternOnce(pattern) {
3553
3738
  );
3554
3739
  }
3555
3740
  var MAX_PATTERN_LENGTH = 1e3;
3741
+ var regexVerdicts = /* @__PURE__ */ new Map();
3742
+ var REGEX_VERDICT_LIMIT = 2048;
3556
3743
  function safeRegExp(pattern) {
3557
3744
  if (!pattern) {
3558
3745
  return null;
@@ -3564,16 +3751,32 @@ function safeRegExp(pattern) {
3564
3751
  if (!match) {
3565
3752
  return null;
3566
3753
  }
3567
- for (const dangerous of REDOS_PATTERNS) {
3568
- 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);
3569
3759
  }
3570
- if (hasAdjacentUnbounded(match[1], match[2])) return null;
3760
+ if (!safe) return null;
3571
3761
  try {
3572
3762
  return new RegExp(match[1], match[2]);
3573
3763
  } catch {
3574
3764
  return null;
3575
3765
  }
3576
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
+ }
3577
3780
  function ctypeResult(strValue, isClass, matchVal) {
3578
3781
  if (strValue === "") {
3579
3782
  return false;
@@ -5297,7 +5500,7 @@ function concatChunks(chunks, total) {
5297
5500
  }
5298
5501
  function parseBody(rawBody, contentType) {
5299
5502
  const ct = String(contentType || "").toLowerCase();
5300
- const isJson = ct.includes("application/json") || /\+json\b/.test(ct);
5503
+ const isJson2 = ct.includes("application/json") || /\+json\b/.test(ct);
5301
5504
  const isForm = ct.includes("application/x-www-form-urlencoded");
5302
5505
  const isMultipart = ct.includes("multipart/form-data");
5303
5506
  const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
@@ -5311,7 +5514,7 @@ function parseBody(rawBody, contentType) {
5311
5514
  for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
5312
5515
  return { body, files: void 0 };
5313
5516
  }
5314
- if (isJson || isAmbiguous) {
5517
+ if (isJson2 || isAmbiguous) {
5315
5518
  try {
5316
5519
  const parsed = JSON.parse(rawBody);
5317
5520
  if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
@@ -5327,22 +5530,68 @@ function parseMultipart(rawBody, boundary) {
5327
5530
  const sep = /\r?\n\r?\n/.exec(part);
5328
5531
  if (!sep) continue;
5329
5532
  const rawHeaders = part.slice(0, sep.index);
5330
- const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
5331
- if (!disposition) continue;
5332
- const name = /name="([^"]*)"/i.exec(disposition)?.[1];
5333
- 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;
5334
5537
  const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
5335
- const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
5336
- if (filename !== void 0) {
5337
- const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
5338
- const file = { filename, type: partType, content };
5339
- appendOwn(files, name, file);
5340
- } else {
5341
- 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);
5342
5542
  }
5343
5543
  }
5344
5544
  return { body, files };
5345
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
+ }
5346
5595
  function parseCookies(header) {
5347
5596
  const cookies = {};
5348
5597
  if (!header) {
@@ -5613,14 +5862,29 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
5613
5862
  if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
5614
5863
  });
5615
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;
5616
5873
  for (const moduleName of ["node:http", "node:https"]) {
5617
5874
  try {
5618
5875
  const mod = await import(moduleName);
5619
5876
  const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
5620
- if (restore) restores.push(restore);
5877
+ if (restore) {
5878
+ patchedAny = true;
5879
+ restores.push(() => {
5880
+ restore();
5881
+ syncBuiltins();
5882
+ });
5883
+ }
5621
5884
  } catch {
5622
5885
  }
5623
5886
  }
5887
+ if (patchedAny) syncBuiltins();
5624
5888
  return () => {
5625
5889
  for (const restore of restores) {
5626
5890
  try {
@@ -5634,18 +5898,22 @@ function patchHttpModule(http, block, screen, skip) {
5634
5898
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
5635
5899
  const originalRequest = http.request;
5636
5900
  const originalGet = http.get;
5637
- const wrap = (original) => function(...args) {
5901
+ const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
5902
+ const guardArgs = (args) => {
5638
5903
  const target = extractHttpTarget(args);
5639
5904
  if (target && block(target.url, target.host, target.method)) {
5640
5905
  throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
5641
5906
  }
5642
5907
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
5643
5908
  try {
5644
- args = withScreeningLookup(args, target, block, screen.lookup, skip);
5909
+ return withScreeningLookup(args, target, block, screen.lookup, skip);
5645
5910
  } catch {
5646
5911
  }
5647
5912
  }
5648
- return original.apply(this, args);
5913
+ return args;
5914
+ };
5915
+ const wrap = (original) => function(...args) {
5916
+ return original.apply(this, guardArgs(args));
5649
5917
  };
5650
5918
  const guardedRequest = wrap(originalRequest);
5651
5919
  http.request = guardedRequest;
@@ -5654,10 +5922,21 @@ function patchHttpModule(http, block, screen, skip) {
5654
5922
  guardedGet = wrap(originalGet);
5655
5923
  http.get = guardedGet;
5656
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
+ }
5657
5935
  http.__patchstackGuarded = true;
5658
5936
  return () => {
5659
5937
  if (http.request === guardedRequest) http.request = originalRequest;
5660
5938
  if (guardedGet && http.get === guardedGet) http.get = originalGet;
5939
+ if (GuardedClientRequest && http.ClientRequest === GuardedClientRequest) http.ClientRequest = OriginalClientRequest;
5661
5940
  delete http.__patchstackGuarded;
5662
5941
  };
5663
5942
  }
@@ -5667,19 +5946,27 @@ function extractHttpTarget(args) {
5667
5946
  if (typeof first === "string" || first instanceof URL) {
5668
5947
  const url = new URL(String(first));
5669
5948
  const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
5670
- return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
5671
- }
5672
- if (first && typeof first === "object") {
5673
- const host = normalizeHost(first.hostname || first.host) || "localhost";
5674
- const protocol = first.protocol || "http:";
5675
- const port = first.port ? `:${first.port}` : "";
5676
- const path9 = first.path || "/";
5677
- return { url: `${protocol}//${host}${port}${path9}`, host, method: first.method || "GET" };
5949
+ return targetOf({ ...optionsFromUrl(url), ...opts ?? {} });
5678
5950
  }
5951
+ if (first && typeof first === "object") return targetOf(first);
5679
5952
  } catch {
5680
5953
  }
5681
5954
  return null;
5682
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
+ }
5683
5970
  function normalizeHost(raw) {
5684
5971
  const host = String(raw || "").trim();
5685
5972
  const bracketed = /^\[([^\]]+)\]/.exec(host);
@@ -6061,19 +6348,49 @@ async function loadFs() {
6061
6348
  }
6062
6349
  return fsMod;
6063
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
+ }
6064
6374
  function makeStore(options = {}) {
6065
6375
  let mem = null;
6066
6376
  const durable = durableTier(options);
6377
+ let identity;
6378
+ const identityOf = () => identity ??= sourceIdentity(options);
6067
6379
  return {
6068
6380
  async read() {
6069
6381
  if (mem) return mem;
6382
+ const source = await identityOf();
6383
+ if (source === null) return null;
6070
6384
  const env = await durable.read();
6071
- if (env) mem = env;
6385
+ if (!env || env.source !== source) return null;
6386
+ mem = env;
6072
6387
  return env;
6073
6388
  },
6074
6389
  async write(env) {
6075
- if (env) mem = env;
6076
- await durable.write(env);
6390
+ if (!env) return;
6391
+ const stamped = { ...env, source: await identityOf() };
6392
+ mem = stamped;
6393
+ await durable.write(stamped);
6077
6394
  }
6078
6395
  };
6079
6396
  }
@@ -6195,11 +6512,12 @@ function toEnvelope(value) {
6195
6512
  buildId,
6196
6513
  // A confirmation belongs to the presentation stored beside it. A crossed or partially written
6197
6514
  // envelope confirms nothing, even when one of its fields happens to name the current map.
6198
- matchedBuildId: buildId !== null && matched === buildId ? matched : null
6515
+ matchedBuildId: buildId !== null && matched === buildId ? matched : null,
6516
+ source: typeof value.source === "string" ? value.source : null
6199
6517
  };
6200
6518
  }
6201
6519
  if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
6202
- return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
6520
+ return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
6203
6521
  }
6204
6522
  return null;
6205
6523
  }
@@ -6267,13 +6585,17 @@ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
6267
6585
  ...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
6268
6586
  ...held > unusable ? [reason] : []
6269
6587
  ];
6270
- notify(
6271
- options.onError,
6272
- new Error(`${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`),
6273
- "onError"
6274
- );
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);
6275
6591
  return { ...bundle, firewall };
6276
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
+ }
6277
6599
  function confirmedBy(res, presented) {
6278
6600
  if (presented === null) return null;
6279
6601
  const verdict = res?.build;
@@ -6448,6 +6770,60 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
6448
6770
  }
6449
6771
  };
6450
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
+ }
6451
6827
  function makeRefreshHandler(tick, secret) {
6452
6828
  let inflight2 = null;
6453
6829
  const runOnce = () => {
@@ -7603,7 +7979,7 @@ function createSupabaseGuard({
7603
7979
  statusText: upstream.statusText,
7604
7980
  headers: responseHeaders(upstream.headers)
7605
7981
  });
7606
- return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
7982
+ return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
7607
7983
  };
7608
7984
  }
7609
7985
 
@@ -7697,6 +8073,7 @@ async function createProtection(options = {}) {
7697
8073
  "onDetect"
7698
8074
  );
7699
8075
  };
8076
+ const nodeHttp = await import("http").then((m) => m.default ?? m, () => null);
7700
8077
  const store = makeStore(options);
7701
8078
  const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
7702
8079
  const pulseAuth = await resolvePulseAuth(options);
@@ -7722,6 +8099,21 @@ async function createProtection(options = {}) {
7722
8099
  pulseAuth,
7723
8100
  detectionState: preFetchState
7724
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);
7725
8117
  let detectionReporting = "not-enrolled";
7726
8118
  let currentOrigin;
7727
8119
  const applyReportingState = async (origin) => {
@@ -7900,7 +8292,9 @@ async function createProtection(options = {}) {
7900
8292
  const spanRedactors = redactors.filter((r) => !r.jsonPath);
7901
8293
  if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
7902
8294
  if (!spanRedactors.length) continue;
8295
+ const beforeSpan = body;
7903
8296
  body = applyRedactors(body, spanRedactors, mask, transform);
8297
+ if (body !== beforeSpan && !preservesJsonStructure(beforeSpan, body)) return { verdict: "block" };
7904
8298
  if (transform) continue;
7905
8299
  for (const name of Object.keys(headers)) {
7906
8300
  const value = headers[name];
@@ -7915,6 +8309,7 @@ async function createProtection(options = {}) {
7915
8309
  }
7916
8310
  }
7917
8311
  }
8312
+ if (body !== text && isJson(text) && !isJson(body)) return { verdict: "block" };
7918
8313
  for (const rule of headerMutations) applyHeaderMutation(headers, rule);
7919
8314
  return { verdict: "redact", body, headers };
7920
8315
  };
@@ -8017,7 +8412,9 @@ async function createProtection(options = {}) {
8017
8412
  const screenResp = async (response, reqCtx) => {
8018
8413
  const read = await readTextResponse(response, screenCap);
8019
8414
  if (read.skip) {
8020
- 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
+ }
8021
8418
  if (read.skip === "not-a-response") return response;
8022
8419
  return hardenHeadersOnly(response, reqCtx);
8023
8420
  }
@@ -8098,6 +8495,11 @@ async function createProtection(options = {}) {
8098
8495
  const wrapNodeResponse = (res, reqCtx) => {
8099
8496
  const origWrite = res.write.bind(res);
8100
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);
8101
8503
  const chunks = [];
8102
8504
  let size = 0;
8103
8505
  let overflow = false;
@@ -8108,7 +8510,7 @@ async function createProtection(options = {}) {
8108
8510
  if (hardened) return null;
8109
8511
  hardened = true;
8110
8512
  try {
8111
- if (res.headersSent || typeof res.setHeader !== "function") return null;
8513
+ if (reallySent() || !setHeaderNow) return null;
8112
8514
  const set2 = typeof res.getHeaders === "function" ? res.getHeaders() : {};
8113
8515
  const headers = effective && effective.headers ? { ...set2, ...effective.headers } : set2;
8114
8516
  const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
@@ -8120,8 +8522,8 @@ async function createProtection(options = {}) {
8120
8522
  if (!headerValueChanged(headers[name], value)) continue;
8121
8523
  changed.set(name.toLowerCase(), value);
8122
8524
  try {
8123
- if (value === null || value === void 0) res.removeHeader?.(name);
8124
- else res.setHeader(name, value);
8525
+ if (value === null || value === void 0) removeHeaderNow?.(name);
8526
+ else setHeaderNow(name, value);
8125
8527
  } catch {
8126
8528
  }
8127
8529
  }
@@ -8132,26 +8534,139 @@ async function createProtection(options = {}) {
8132
8534
  }
8133
8535
  };
8134
8536
  const stillToAnswer = (rule) => !hardensWithoutBody(rule);
8135
- if (typeof res.writeHead === "function") {
8136
- 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) {
8137
8541
  res.writeHead = function(...args) {
8138
- const at = writeHeadHeaderIndex(args);
8139
- const given = at === -1 ? null : args[at];
8140
- const changed = hardenBeforeFlush({
8141
- status: typeof args[0] === "number" ? args[0] : void 0,
8142
- headers: given ? writeHeadHeaderObject(given) : null
8143
- });
8144
- if (!changed || !given) return origWriteHead(...args);
8145
- const rewritten = [...args];
8146
- rewritten[at] = rewriteWriteHeadHeaders(given, changed);
8147
- 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;
8148
8556
  };
8149
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
+ };
8150
8664
  const collect = (chunk, enc) => {
8151
8665
  if (chunk == null) return;
8152
8666
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
8153
8667
  size += buf.length;
8154
8668
  if (size > MAX) {
8669
+ sendHead();
8155
8670
  for (const c of chunks) origWrite(c);
8156
8671
  chunks.length = 0;
8157
8672
  origWrite(buf);
@@ -8162,7 +8677,6 @@ async function createProtection(options = {}) {
8162
8677
  chunks.push(buf);
8163
8678
  };
8164
8679
  res.write = function(chunk, enc, cb) {
8165
- hardenBeforeFlush();
8166
8680
  if (overflow) return origWrite(chunk, enc, cb);
8167
8681
  collect(chunk, enc);
8168
8682
  if (typeof enc === "function") enc();
@@ -8170,7 +8684,6 @@ async function createProtection(options = {}) {
8170
8684
  return true;
8171
8685
  };
8172
8686
  res.end = function(chunk, enc, cb) {
8173
- hardenBeforeFlush();
8174
8687
  if (typeof chunk === "function") {
8175
8688
  cb = chunk;
8176
8689
  chunk = void 0;
@@ -8185,68 +8698,77 @@ async function createProtection(options = {}) {
8185
8698
  }
8186
8699
  collect(chunk, enc);
8187
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
+ };
8188
8707
  const buffer = Buffer.concat(chunks);
8189
- let ct = res.getHeader ? res.getHeader("content-type") : void 0;
8708
+ const head = effectiveHead();
8709
+ let ct = head.headers["content-type"];
8190
8710
  if (Array.isArray(ct)) ct = ct[0];
8191
8711
  const kind = screenableContentType(ct);
8192
8712
  if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
8193
8713
  recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
8194
- for (const c of chunks) origWrite(c);
8195
- 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();
8196
8720
  }
8197
8721
  const text = buffer.toString("utf8");
8198
8722
  let r;
8199
8723
  try {
8200
- r = screenText(
8201
- text,
8202
- { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} },
8203
- reqCtx,
8204
- answeredWithoutBody ? stillToAnswer : void 0
8205
- );
8724
+ r = screenText(text, head, reqCtx, answeredWithoutBody ? stillToAnswer : void 0);
8206
8725
  } catch (err) {
8207
8726
  notify(onError, err, "onError");
8208
- for (const c of chunks) origWrite(c);
8209
- 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;
8210
8734
  }
8211
8735
  if (r.verdict === "block") {
8212
- res.statusCode = 500;
8213
- try {
8214
- res.setHeader("content-type", "application/json");
8215
- } catch {
8736
+ const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
8737
+ if (reallySent()) {
8738
+ res.destroy?.();
8739
+ return res;
8216
8740
  }
8217
- 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);
8218
8750
  }
8219
- if (r.verdict === "redact") {
8220
- try {
8221
- res.removeHeader && res.removeHeader("content-length");
8222
- } 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);
8223
8757
  }
8224
- if (r.headers && res.setHeader) {
8225
- const current = res.getHeaders ? res.getHeaders() : {};
8226
- for (const [name, value] of Object.entries(r.headers)) {
8227
- if (name.toLowerCase() === "content-length") continue;
8228
- if (value === null || value === void 0) {
8229
- try {
8230
- res.removeHeader && res.removeHeader(name);
8231
- } catch {
8232
- }
8233
- } else if (Array.isArray(value)) {
8234
- try {
8235
- res.setHeader(name, value);
8236
- } catch {
8237
- }
8238
- } else if (typeof value === "string" && current[name] !== value) {
8239
- try {
8240
- res.setHeader(name, value);
8241
- } catch {
8242
- }
8243
- }
8244
- }
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);
8245
8766
  }
8246
- return origEnd(r.body, cb);
8767
+ sendHead({ headers: changed });
8768
+ } else if (changed.size) {
8769
+ recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
8247
8770
  }
8248
- for (const c of chunks) origWrite(c);
8249
- return origEnd(cb);
8771
+ return origEnd(r.body, cb);
8250
8772
  };
8251
8773
  };
8252
8774
  const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
@@ -8459,6 +8981,7 @@ async function createProtection(options = {}) {
8459
8981
  notify(onError, err, "onError");
8460
8982
  }
8461
8983
  }
8984
+ let recovery = null;
8462
8985
  const runRefreshTick = async () => {
8463
8986
  if (reporter) {
8464
8987
  try {
@@ -8475,23 +8998,33 @@ async function createProtection(options = {}) {
8475
8998
  });
8476
8999
  mode = resolveMode(options, next);
8477
9000
  applyBundle(next);
9001
+ noteRuleSource(next.source);
9002
+ if (next.source?.ok !== false) recovery?.stop();
8478
9003
  await applyAndAcknowledge(next.source?.origin, declaredState);
8479
9004
  if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
8480
9005
  return next.source ?? { ok: true };
8481
9006
  };
9007
+ const refreshTick = serialise(runRefreshTick);
8482
9008
  if (live) {
8483
- protection.refresh = () => runRefreshTick();
8484
- protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
9009
+ protection.refresh = () => refreshTick();
9010
+ protection.refreshHandler = () => makeRefreshHandler(refreshTick, refreshSecret);
8485
9011
  }
8486
- 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;
8487
9015
  protection.stop = () => {
8488
9016
  loop?.stop();
9017
+ recovery?.stop();
8489
9018
  const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
8490
9019
  (wait) => wait && typeof wait.then === "function"
8491
9020
  );
8492
9021
  return Promise.all(outstanding).then(() => void 0);
8493
9022
  };
8494
9023
  protection.stopRefresh = protection.stop;
9024
+ Object.defineProperty(protection, "ruleSource", {
9025
+ get: () => ({ ...ruleSource }),
9026
+ enumerable: true
9027
+ });
8495
9028
  Object.defineProperty(protection, "detectionReporting", {
8496
9029
  get: () => detectionReporting,
8497
9030
  enumerable: true
@@ -8502,6 +9035,16 @@ async function createProtection(options = {}) {
8502
9035
  });
8503
9036
  return protection;
8504
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
+ }
8505
9048
  function resolveMode(options, bundle) {
8506
9049
  const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
8507
9050
  if (env === "block" || env === "dry-run") return env;
@@ -8574,11 +9117,26 @@ function looksBinary(bytes) {
8574
9117
  }
8575
9118
  return n > 0 && ctrl / n > 0.1;
8576
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
+ }
8577
9133
  async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
8578
9134
  if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
8579
9135
  const ct = response.headers?.get?.("content-type") || "";
8580
9136
  const kind = screenableContentType(ct);
8581
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(", ") });
8582
9140
  const sniff = kind === "sniff";
8583
9141
  const len = Number(response.headers?.get?.("content-length") || 0);
8584
9142
  if (len && len > cap) return { skip: "body-cap" };
@@ -8622,13 +9180,21 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
8622
9180
  } finally {
8623
9181
  reader.releaseLock();
8624
9182
  }
9183
+ const bytes = concatBytes(chunks, size);
9184
+ if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
8625
9185
  try {
8626
- return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
9186
+ return { text: new TextDecoder().decode(bytes) };
8627
9187
  } catch {
8628
9188
  return { skip: "decode-failed" };
8629
9189
  }
8630
9190
  }
8631
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
+ }
8632
9198
  const text = await clone.text();
8633
9199
  if (text.length > cap) return { skip: "body-cap" };
8634
9200
  if (sniff && looksBinary(new TextEncoder().encode(text.slice(0, 512)))) return { skip: "binary-body" };
@@ -8702,6 +9268,39 @@ function extractRedactors(rule) {
8702
9268
  function htmlEscape(str) {
8703
9269
  return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
8704
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
+ }
8705
9304
  function applyRedactors(body, redactors, mask, transform) {
8706
9305
  let out = body;
8707
9306
  for (const r of redactors) {
@@ -8744,10 +9343,12 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
8744
9343
  if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
8745
9344
  const head = text.trimStart()[0];
8746
9345
  if (head !== "{" && head !== "[") return text;
8747
- const preserved = preserveBigInts(text);
9346
+ let preserved;
8748
9347
  let obj;
8749
9348
  try {
8750
- obj = JSON.parse(preserved);
9349
+ JSON.parse(text);
9350
+ preserved = preserveBigInts(text, mask);
9351
+ obj = JSON.parse(preserved.text);
8751
9352
  } catch {
8752
9353
  return text;
8753
9354
  }
@@ -8756,65 +9357,44 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
8756
9357
  const pred = conditionPredicate(r.condition);
8757
9358
  walkLeaves(obj, r.jsonPath, (loc) => {
8758
9359
  try {
8759
- if (pred(loc.value)) {
8760
- 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);
8761
9363
  changed = true;
8762
9364
  }
8763
9365
  } catch {
8764
9366
  }
8765
9367
  });
8766
9368
  }
8767
- return changed ? restoreBigInts(JSON.stringify(obj)) : text;
8768
- }
8769
- var BIGINT_OPEN = "__PSBIGINT_9c2f__";
8770
- var BIGINT_CLOSE = "__DNEGIB__";
8771
- function preserveBigInts(text) {
8772
- let out = "";
8773
- let inStr = false;
8774
- for (let i = 0; i < text.length; ) {
8775
- const ch = text[i];
8776
- if (inStr) {
8777
- out += ch;
8778
- if (ch === "\\") {
8779
- out += text[i + 1] ?? "";
8780
- i += 2;
8781
- continue;
8782
- }
8783
- if (ch === '"') inStr = false;
8784
- i++;
8785
- continue;
8786
- }
8787
- if (ch === '"') {
8788
- inStr = true;
8789
- out += ch;
8790
- i++;
8791
- continue;
8792
- }
8793
- if (ch === "-" || ch >= "0" && ch <= "9") {
8794
- let j = ch === "-" ? i + 1 : i;
8795
- let digits = 0;
8796
- while (j < text.length && text[j] >= "0" && text[j] <= "9") {
8797
- digits++;
8798
- j++;
8799
- }
8800
- const next = text[j];
8801
- const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
8802
- if (isIntToken && digits >= 16) {
8803
- out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
8804
- } else {
8805
- out += text.slice(i, j || i + 1);
8806
- }
8807
- i = j > i ? j : i + 1;
8808
- continue;
8809
- }
8810
- out += ch;
8811
- i++;
8812
- }
8813
- return out;
9369
+ return changed ? restoreBigInts(JSON.stringify(obj), preserved.numbers) : text;
8814
9370
  }
8815
- function restoreBigInts(text) {
8816
- if (!text.includes(BIGINT_OPEN)) return text;
8817
- 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);
8818
9398
  }
8819
9399
  function isHeaderMutation(action) {
8820
9400
  return action === "set-header" || action === "remove-header" || action === "harden-cookie";