@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.
@@ -1235,6 +1235,184 @@ function hasAdjacentUnbounded(body, flags) {
1235
1235
  }
1236
1236
  return false;
1237
1237
  }
1238
+ function parseRepetition(body) {
1239
+ let i = 0;
1240
+ const parseAlternation = () => {
1241
+ const branches = [parseSequence()];
1242
+ while (body[i] === "|") {
1243
+ i++;
1244
+ branches.push(parseSequence());
1245
+ }
1246
+ return branches.length === 1 ? branches[0] : { type: "alt", branches };
1247
+ };
1248
+ const parseSequence = () => {
1249
+ const items = [];
1250
+ while (i < body.length && body[i] !== "|" && body[i] !== ")") {
1251
+ const atom = parseAtom();
1252
+ if (atom === null) return null;
1253
+ const quantified = parseQuantifier(atom);
1254
+ if (quantified === null) return null;
1255
+ items.push(quantified);
1256
+ }
1257
+ return { type: "seq", items };
1258
+ };
1259
+ const parseAtom = () => {
1260
+ const ch = body[i];
1261
+ if (ch === "(") {
1262
+ const start2 = i;
1263
+ i++;
1264
+ let kind = "capture";
1265
+ if (body.startsWith("?:", i)) {
1266
+ kind = "group";
1267
+ i += 2;
1268
+ } else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
1269
+ kind = "look";
1270
+ i += 2;
1271
+ } else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
1272
+ kind = "look";
1273
+ i += 3;
1274
+ } else if (body.startsWith("?<", i)) {
1275
+ const close = body.indexOf(">", i + 2);
1276
+ if (close === -1) return null;
1277
+ i = close + 1;
1278
+ } else if (body[i] === "?") {
1279
+ return null;
1280
+ }
1281
+ const inner = parseAlternation();
1282
+ if (inner === null || body[i] !== ")") return null;
1283
+ i++;
1284
+ return { type: "group", kind, body: inner, source: body.slice(start2, i) };
1285
+ }
1286
+ const start = i;
1287
+ if (ch === "[") {
1288
+ i++;
1289
+ if (body[i] === "^") i++;
1290
+ if (body[i] === "]") i++;
1291
+ while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
1292
+ if (body[i] !== "]") return null;
1293
+ i++;
1294
+ return { type: "atom", source: body.slice(start, i) };
1295
+ }
1296
+ if (ch === "\\") {
1297
+ const next = body[i + 1];
1298
+ if (next === void 0) return null;
1299
+ if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
1300
+ const close = body.indexOf("}", i + 3);
1301
+ if (close === -1) return null;
1302
+ i = close + 1;
1303
+ } else if (next === "k" && body[i + 2] === "<") {
1304
+ const close = body.indexOf(">", i + 3);
1305
+ if (close === -1) return null;
1306
+ i = close + 1;
1307
+ } else {
1308
+ i += 2;
1309
+ }
1310
+ return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
1311
+ }
1312
+ if (ch === "^" || ch === "$") {
1313
+ i++;
1314
+ return { type: "assert" };
1315
+ }
1316
+ if (ch === "*" || ch === "+" || ch === "?") return null;
1317
+ i++;
1318
+ return { type: "atom", source: body.slice(start, i) };
1319
+ };
1320
+ const parseQuantifier = (atom) => {
1321
+ let min;
1322
+ let max;
1323
+ const ch = body[i];
1324
+ if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
1325
+ else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
1326
+ else if (ch === "?") [min, max, i] = [0, 1, i + 1];
1327
+ else if (ch === "{") {
1328
+ const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
1329
+ if (!bounds) return atom;
1330
+ min = Number(bounds[1]);
1331
+ max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
1332
+ i += bounds[0].length;
1333
+ } else {
1334
+ return atom;
1335
+ }
1336
+ if (body[i] === "?") i++;
1337
+ if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
1338
+ return { type: "quant", min, max, body: atom };
1339
+ };
1340
+ const root = parseAlternation();
1341
+ return root !== null && i === body.length ? root : null;
1342
+ }
1343
+ function* nodesOf(node) {
1344
+ if (!node) return;
1345
+ yield node;
1346
+ if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
1347
+ else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
1348
+ else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
1349
+ }
1350
+ var BOUNDED_NESTING_LIMIT = 100;
1351
+ var BOUNDED_ALTERNATION_LIMIT = 16;
1352
+ var OPTIONAL_REPEAT_LIMIT = 8;
1353
+ function hasFencedRepetitions(quant, flags) {
1354
+ let body = quant.body;
1355
+ if (body.type === "group" && body.kind !== "look") body = body.body;
1356
+ if (body.type !== "seq" || body.items.length < 2) return false;
1357
+ const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
1358
+ if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
1359
+ const atomFlags = flags.replace(/[^i]/g, "");
1360
+ const unicode = /[uv]/.test(flags);
1361
+ let insides;
1362
+ try {
1363
+ insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
1364
+ } catch {
1365
+ return false;
1366
+ }
1367
+ const literalOf = (source) => {
1368
+ if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
1369
+ if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
1370
+ return null;
1371
+ };
1372
+ const disjoint = (edge) => {
1373
+ if (edge?.type !== "atom" || edge.source === void 0) return false;
1374
+ const literal = literalOf(edge.source);
1375
+ if (literal !== null) return insides.every((re) => !re.test(literal));
1376
+ if (unicode) return false;
1377
+ let fence;
1378
+ try {
1379
+ fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
1380
+ } catch {
1381
+ return false;
1382
+ }
1383
+ for (let unit = 0; unit <= 65535; unit++) {
1384
+ const ch = String.fromCharCode(unit);
1385
+ if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
1386
+ }
1387
+ return true;
1388
+ };
1389
+ return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
1390
+ }
1391
+ function hasCatastrophicShape(body, flags) {
1392
+ const root = parseRepetition(body);
1393
+ if (root === null) return null;
1394
+ for (const node of nodesOf(root)) {
1395
+ if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
1396
+ if (node.type !== "quant" || node.max <= 1) continue;
1397
+ const fenced = hasFencedRepetitions(node, flags);
1398
+ for (const inner of nodesOf(node.body)) {
1399
+ if (inner === node.body && inner.type === "quant") continue;
1400
+ if (inner.type === "quant" && inner.max > 1 && !fenced) {
1401
+ if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
1402
+ }
1403
+ if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
1404
+ return true;
1405
+ }
1406
+ if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
1407
+ return true;
1408
+ }
1409
+ }
1410
+ }
1411
+ return false;
1412
+ }
1413
+ function lookaroundBody(source) {
1414
+ return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
1415
+ }
1238
1416
  function groupEnd(body, start) {
1239
1417
  let depth = 0;
1240
1418
  let inClass = false;
@@ -1261,6 +1439,8 @@ function warnRejectedPatternOnce(pattern) {
1261
1439
  );
1262
1440
  }
1263
1441
  var MAX_PATTERN_LENGTH = 1e3;
1442
+ var regexVerdicts = /* @__PURE__ */ new Map();
1443
+ var REGEX_VERDICT_LIMIT = 2048;
1264
1444
  function safeRegExp(pattern) {
1265
1445
  if (!pattern) {
1266
1446
  return null;
@@ -1272,16 +1452,32 @@ function safeRegExp(pattern) {
1272
1452
  if (!match) {
1273
1453
  return null;
1274
1454
  }
1275
- for (const dangerous of REDOS_PATTERNS) {
1276
- if (dangerous.test(match[1])) return null;
1455
+ let safe = regexVerdicts.get(pattern);
1456
+ if (safe === void 0) {
1457
+ safe = isSafeBody(match[1], match[2]);
1458
+ if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
1459
+ regexVerdicts.set(pattern, safe);
1277
1460
  }
1278
- if (hasAdjacentUnbounded(match[1], match[2])) return null;
1461
+ if (!safe) return null;
1279
1462
  try {
1280
1463
  return new RegExp(match[1], match[2]);
1281
1464
  } catch {
1282
1465
  return null;
1283
1466
  }
1284
1467
  }
1468
+ function isSafeBody(body, flags) {
1469
+ for (const dangerous of REDOS_PATTERNS) {
1470
+ if (dangerous.test(body)) return false;
1471
+ }
1472
+ if (hasAdjacentUnbounded(body, flags)) return false;
1473
+ if (hasCatastrophicShape(body, flags) === true) return false;
1474
+ try {
1475
+ new RegExp(body, flags);
1476
+ } catch {
1477
+ return false;
1478
+ }
1479
+ return true;
1480
+ }
1285
1481
  function ctypeResult(strValue, isClass, matchVal) {
1286
1482
  if (strValue === "") {
1287
1483
  return false;
@@ -3238,7 +3434,7 @@ function concatChunks(chunks, total) {
3238
3434
  }
3239
3435
  function parseBody(rawBody, contentType) {
3240
3436
  const ct = String(contentType || "").toLowerCase();
3241
- const isJson = ct.includes("application/json") || /\+json\b/.test(ct);
3437
+ const isJson2 = ct.includes("application/json") || /\+json\b/.test(ct);
3242
3438
  const isForm = ct.includes("application/x-www-form-urlencoded");
3243
3439
  const isMultipart = ct.includes("multipart/form-data");
3244
3440
  const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
@@ -3252,7 +3448,7 @@ function parseBody(rawBody, contentType) {
3252
3448
  for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
3253
3449
  return { body, files: void 0 };
3254
3450
  }
3255
- if (isJson || isAmbiguous) {
3451
+ if (isJson2 || isAmbiguous) {
3256
3452
  try {
3257
3453
  const parsed = JSON.parse(rawBody);
3258
3454
  if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
@@ -3268,22 +3464,68 @@ function parseMultipart(rawBody, boundary) {
3268
3464
  const sep = /\r?\n\r?\n/.exec(part);
3269
3465
  if (!sep) continue;
3270
3466
  const rawHeaders = part.slice(0, sep.index);
3271
- const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
3272
- if (!disposition) continue;
3273
- const name = /name="([^"]*)"/i.exec(disposition)?.[1];
3274
- if (name == null) continue;
3467
+ const disposition = /content-disposition:([^\r\n]*)/i.exec(rawHeaders)?.[1];
3468
+ if (disposition === void 0) continue;
3469
+ const { names, filename } = dispositionFields(disposition);
3470
+ if (names.length === 0) continue;
3275
3471
  const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
3276
- const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
3277
- if (filename !== void 0) {
3278
- const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
3279
- const file = { filename, type: partType, content };
3280
- appendOwn(files, name, file);
3281
- } else {
3282
- appendOwn(body, name, content);
3472
+ const file = filename === void 0 ? void 0 : { filename, type: /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "", content };
3473
+ for (const name of names) {
3474
+ if (file) appendOwn(files, name, file);
3475
+ else appendOwn(body, name, content);
3283
3476
  }
3284
3477
  }
3285
3478
  return { body, files };
3286
3479
  }
3480
+ function dispositionFields(value) {
3481
+ const text = String(value);
3482
+ const names = /* @__PURE__ */ new Set();
3483
+ let filename;
3484
+ let extendedFilename;
3485
+ let i = text.indexOf(";");
3486
+ while (i !== -1 && i < text.length) {
3487
+ i++;
3488
+ while (text[i] === " " || text[i] === " ") i++;
3489
+ let key = "";
3490
+ while (i < text.length && text[i] !== "=" && text[i] !== ";") key += text[i++];
3491
+ key = key.trim().toLowerCase();
3492
+ let val = null;
3493
+ if (text[i] === "=") {
3494
+ i++;
3495
+ while (text[i] === " " || text[i] === " ") i++;
3496
+ if (text[i] === '"') {
3497
+ i++;
3498
+ val = "";
3499
+ while (i < text.length && text[i] !== '"') {
3500
+ if (text[i] === "\\" && i + 1 < text.length) i++;
3501
+ val += text[i++];
3502
+ }
3503
+ i++;
3504
+ while (i < text.length && text[i] !== ";") i++;
3505
+ } else {
3506
+ let bare = "";
3507
+ while (i < text.length && text[i] !== ";") bare += text[i++];
3508
+ val = bare.trim();
3509
+ }
3510
+ }
3511
+ if (val !== null) {
3512
+ if (key === "name") names.add(val);
3513
+ else if (key === "filename") filename = val;
3514
+ else if (key === "filename*") extendedFilename = decodeExtendedValue(val);
3515
+ }
3516
+ i = i < text.length && text[i] === ";" ? i : -1;
3517
+ }
3518
+ return { names: [...names], filename: extendedFilename ?? filename };
3519
+ }
3520
+ function decodeExtendedValue(value) {
3521
+ const match = /^([^']*)'[^']*'(.*)$/.exec(value);
3522
+ if (!match) return value;
3523
+ try {
3524
+ return decodeURIComponent(match[2]);
3525
+ } catch {
3526
+ return match[2];
3527
+ }
3528
+ }
3287
3529
  function parseCookies(header) {
3288
3530
  const cookies = {};
3289
3531
  if (!header) {
@@ -3554,14 +3796,29 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
3554
3796
  if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
3555
3797
  });
3556
3798
  }
3799
+ let syncBuiltins = () => {
3800
+ };
3801
+ try {
3802
+ const { syncBuiltinESMExports } = await Promise.resolve().then(() => __toESM(require_ps_edge_stub(), 1));
3803
+ if (typeof syncBuiltinESMExports === "function") syncBuiltins = syncBuiltinESMExports;
3804
+ } catch {
3805
+ }
3806
+ let patchedAny = false;
3557
3807
  for (const moduleName of ["node:http", "node:https"]) {
3558
3808
  try {
3559
3809
  const mod = await import(moduleName);
3560
3810
  const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
3561
- if (restore) restores.push(restore);
3811
+ if (restore) {
3812
+ patchedAny = true;
3813
+ restores.push(() => {
3814
+ restore();
3815
+ syncBuiltins();
3816
+ });
3817
+ }
3562
3818
  } catch {
3563
3819
  }
3564
3820
  }
3821
+ if (patchedAny) syncBuiltins();
3565
3822
  return () => {
3566
3823
  for (const restore of restores) {
3567
3824
  try {
@@ -3575,18 +3832,22 @@ function patchHttpModule(http, block, screen, skip) {
3575
3832
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
3576
3833
  const originalRequest = http.request;
3577
3834
  const originalGet = http.get;
3578
- const wrap = (original) => function(...args) {
3835
+ const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
3836
+ const guardArgs = (args) => {
3579
3837
  const target = extractHttpTarget(args);
3580
3838
  if (target && block(target.url, target.host, target.method)) {
3581
3839
  throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
3582
3840
  }
3583
3841
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
3584
3842
  try {
3585
- args = withScreeningLookup(args, target, block, screen.lookup, skip);
3843
+ return withScreeningLookup(args, target, block, screen.lookup, skip);
3586
3844
  } catch {
3587
3845
  }
3588
3846
  }
3589
- return original.apply(this, args);
3847
+ return args;
3848
+ };
3849
+ const wrap = (original) => function(...args) {
3850
+ return original.apply(this, guardArgs(args));
3590
3851
  };
3591
3852
  const guardedRequest = wrap(originalRequest);
3592
3853
  http.request = guardedRequest;
@@ -3595,10 +3856,21 @@ function patchHttpModule(http, block, screen, skip) {
3595
3856
  guardedGet = wrap(originalGet);
3596
3857
  http.get = guardedGet;
3597
3858
  }
3859
+ let GuardedClientRequest;
3860
+ if (OriginalClientRequest) {
3861
+ GuardedClientRequest = function ClientRequest(...args) {
3862
+ const screened = guardArgs(args);
3863
+ return new.target ? Reflect.construct(OriginalClientRequest, screened, new.target) : Reflect.construct(OriginalClientRequest, screened);
3864
+ };
3865
+ GuardedClientRequest.prototype = OriginalClientRequest.prototype;
3866
+ Object.setPrototypeOf(GuardedClientRequest, OriginalClientRequest);
3867
+ http.ClientRequest = GuardedClientRequest;
3868
+ }
3598
3869
  http.__patchstackGuarded = true;
3599
3870
  return () => {
3600
3871
  if (http.request === guardedRequest) http.request = originalRequest;
3601
3872
  if (guardedGet && http.get === guardedGet) http.get = originalGet;
3873
+ if (GuardedClientRequest && http.ClientRequest === GuardedClientRequest) http.ClientRequest = OriginalClientRequest;
3602
3874
  delete http.__patchstackGuarded;
3603
3875
  };
3604
3876
  }
@@ -3608,19 +3880,27 @@ function extractHttpTarget(args) {
3608
3880
  if (typeof first === "string" || first instanceof URL) {
3609
3881
  const url = new URL(String(first));
3610
3882
  const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
3611
- return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
3612
- }
3613
- if (first && typeof first === "object") {
3614
- const host = normalizeHost(first.hostname || first.host) || "localhost";
3615
- const protocol = first.protocol || "http:";
3616
- const port = first.port ? `:${first.port}` : "";
3617
- const path = first.path || "/";
3618
- return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
3883
+ return targetOf({ ...optionsFromUrl(url), ...opts ?? {} });
3619
3884
  }
3885
+ if (first && typeof first === "object") return targetOf(first);
3620
3886
  } catch {
3621
3887
  }
3622
3888
  return null;
3623
3889
  }
3890
+ function optionsFromUrl(url) {
3891
+ const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
3892
+ const options = { protocol: url.protocol, hostname, path: `${url.pathname || "/"}${url.search}` };
3893
+ if (url.port !== "") options.port = Number(url.port);
3894
+ return options;
3895
+ }
3896
+ function targetOf(options) {
3897
+ const host = normalizeHost(options.hostname || options.host) || "localhost";
3898
+ const protocol = options.protocol || "http:";
3899
+ const port = options.port ? `:${options.port}` : "";
3900
+ const path = options.path || "/";
3901
+ const authority = host.includes(":") ? `[${host}]` : host;
3902
+ return { url: `${protocol}//${authority}${port}${path}`, host, method: options.method || "GET" };
3903
+ }
3624
3904
  function normalizeHost(raw) {
3625
3905
  const host = String(raw || "").trim();
3626
3906
  const bracketed = /^\[([^\]]+)\]/.exec(host);
@@ -4001,19 +4281,49 @@ async function loadFs() {
4001
4281
  }
4002
4282
  return fsMod;
4003
4283
  }
4284
+ async function sourceIdentity(options = {}) {
4285
+ if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
4286
+ const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
4287
+ return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
4288
+ }
4289
+ if (typeof options.token === "string" && options.token !== "") {
4290
+ const digest = await sha256Hex(options.token);
4291
+ if (digest === null) return null;
4292
+ const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
4293
+ return `token:${digest}@${endpoint}`;
4294
+ }
4295
+ return null;
4296
+ }
4297
+ async function sha256Hex(text) {
4298
+ try {
4299
+ const subtle = globalThis.crypto?.subtle;
4300
+ if (!subtle || typeof TextEncoder === "undefined") return null;
4301
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
4302
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
4303
+ } catch {
4304
+ return null;
4305
+ }
4306
+ }
4004
4307
  function makeStore(options = {}) {
4005
4308
  let mem = null;
4006
4309
  const durable = durableTier(options);
4310
+ let identity;
4311
+ const identityOf = () => identity ??= sourceIdentity(options);
4007
4312
  return {
4008
4313
  async read() {
4009
4314
  if (mem) return mem;
4315
+ const source = await identityOf();
4316
+ if (source === null) return null;
4010
4317
  const env = await durable.read();
4011
- if (env) mem = env;
4318
+ if (!env || env.source !== source) return null;
4319
+ mem = env;
4012
4320
  return env;
4013
4321
  },
4014
4322
  async write(env) {
4015
- if (env) mem = env;
4016
- await durable.write(env);
4323
+ if (!env) return;
4324
+ const stamped = { ...env, source: await identityOf() };
4325
+ mem = stamped;
4326
+ await durable.write(stamped);
4017
4327
  }
4018
4328
  };
4019
4329
  }
@@ -4135,11 +4445,12 @@ function toEnvelope(value) {
4135
4445
  buildId,
4136
4446
  // A confirmation belongs to the presentation stored beside it. A crossed or partially written
4137
4447
  // envelope confirms nothing, even when one of its fields happens to name the current map.
4138
- matchedBuildId: buildId !== null && matched === buildId ? matched : null
4448
+ matchedBuildId: buildId !== null && matched === buildId ? matched : null,
4449
+ source: typeof value.source === "string" ? value.source : null
4139
4450
  };
4140
4451
  }
4141
4452
  if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
4142
- return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
4453
+ return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
4143
4454
  }
4144
4455
  return null;
4145
4456
  }
@@ -4206,13 +4517,17 @@ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
4206
4517
  ...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
4207
4518
  ...held > unusable ? [reason] : []
4208
4519
  ];
4209
- notify(
4210
- options.onError,
4211
- new Error(`${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`),
4212
- "onError"
4213
- );
4520
+ const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
4521
+ if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
4522
+ else warnHeldOnce(message);
4214
4523
  return { ...bundle, firewall };
4215
4524
  }
4525
+ var warnedHeld = /* @__PURE__ */ new Set();
4526
+ function warnHeldOnce(message) {
4527
+ if (warnedHeld.has(message)) return;
4528
+ warnedHeld.add(message);
4529
+ console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
4530
+ }
4216
4531
  function confirmedBy(res, presented) {
4217
4532
  if (presented === null) return null;
4218
4533
  const verdict = res?.build;
@@ -4387,6 +4702,60 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
4387
4702
  }
4388
4703
  };
4389
4704
  }
4705
+ var RECOVERY_DELAYS_MS = [5e3, 15e3, 45e3, 12e4, 3e5, 6e5];
4706
+ function startRecovery(tick, { onError } = {}) {
4707
+ let stopped = false;
4708
+ let attempt = 0;
4709
+ let timer = null;
4710
+ const schedule = () => {
4711
+ if (stopped) return;
4712
+ const delay = RECOVERY_DELAYS_MS[Math.min(attempt, RECOVERY_DELAYS_MS.length - 1)];
4713
+ timer = setTimeout(run, delay * (1 - Math.random() * JITTER_FRACTION3));
4714
+ timer?.unref?.();
4715
+ };
4716
+ const run = async () => {
4717
+ if (stopped) return;
4718
+ attempt++;
4719
+ try {
4720
+ const status = await tick();
4721
+ if (!status || status.ok !== false) {
4722
+ stopped = true;
4723
+ return;
4724
+ }
4725
+ } catch (err) {
4726
+ notify(onError, err, "onError");
4727
+ }
4728
+ schedule();
4729
+ };
4730
+ schedule();
4731
+ return {
4732
+ stop() {
4733
+ stopped = true;
4734
+ if (timer) clearTimeout(timer);
4735
+ }
4736
+ };
4737
+ }
4738
+ function serialise(tick) {
4739
+ let running = null;
4740
+ let next = null;
4741
+ const run = () => {
4742
+ if (!running) {
4743
+ running = Promise.resolve().then(() => tick()).finally(() => {
4744
+ running = null;
4745
+ });
4746
+ return running;
4747
+ }
4748
+ if (!next) {
4749
+ next = running.catch(() => {
4750
+ }).then(() => {
4751
+ next = null;
4752
+ return run();
4753
+ });
4754
+ }
4755
+ return next;
4756
+ };
4757
+ return run;
4758
+ }
4390
4759
  function makeRefreshHandler(tick, secret) {
4391
4760
  let inflight2 = null;
4392
4761
  const runOnce = () => {
@@ -5538,7 +5907,7 @@ function createSupabaseGuard({
5538
5907
  statusText: upstream.statusText,
5539
5908
  headers: responseHeaders(upstream.headers)
5540
5909
  });
5541
- return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
5910
+ return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
5542
5911
  };
5543
5912
  }
5544
5913
 
@@ -5632,6 +6001,7 @@ async function createProtection(options = {}) {
5632
6001
  "onDetect"
5633
6002
  );
5634
6003
  };
6004
+ const nodeHttp = await Promise.resolve().then(() => __toESM(require_ps_edge_stub(), 1)).then((m) => m.default ?? m, () => null);
5635
6005
  const store = makeStore(options);
5636
6006
  const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
5637
6007
  const pulseAuth = await resolvePulseAuth(options);
@@ -5657,6 +6027,21 @@ async function createProtection(options = {}) {
5657
6027
  pulseAuth,
5658
6028
  detectionState: preFetchState
5659
6029
  });
6030
+ let ruleSource = { ...bundle.source ?? { ok: true, origin: "empty" } };
6031
+ const warnedSources = /* @__PURE__ */ new Set();
6032
+ const hasLiveSource = Boolean(options.siteUuid || options.token);
6033
+ const noteRuleSource = (source) => {
6034
+ ruleSource = { ...source ?? { ok: true, origin: ruleSource.origin } };
6035
+ if (!hasLiveSource || typeof onError === "function" || ruleSource.ok !== false) return;
6036
+ const cause = `${ruleSource.origin}:${ruleSource.reason ?? ""}`;
6037
+ if (warnedSources.has(cause)) return;
6038
+ warnedSources.add(cause);
6039
+ const running = ruleSource.origin === "cache" ? "the last rules it received" : ruleSource.origin === "bundled" ? "its bundled fallback rules" : "no rules at all";
6040
+ console.warn(
6041
+ `[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.`
6042
+ );
6043
+ };
6044
+ noteRuleSource(bundle.source);
5660
6045
  let detectionReporting = "not-enrolled";
5661
6046
  let currentOrigin;
5662
6047
  const applyReportingState = async (origin) => {
@@ -5835,7 +6220,9 @@ async function createProtection(options = {}) {
5835
6220
  const spanRedactors = redactors.filter((r) => !r.jsonPath);
5836
6221
  if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
5837
6222
  if (!spanRedactors.length) continue;
6223
+ const beforeSpan = body;
5838
6224
  body = applyRedactors(body, spanRedactors, mask, transform);
6225
+ if (body !== beforeSpan && !preservesJsonStructure(beforeSpan, body)) return { verdict: "block" };
5839
6226
  if (transform) continue;
5840
6227
  for (const name of Object.keys(headers)) {
5841
6228
  const value = headers[name];
@@ -5850,6 +6237,7 @@ async function createProtection(options = {}) {
5850
6237
  }
5851
6238
  }
5852
6239
  }
6240
+ if (body !== text && isJson(text) && !isJson(body)) return { verdict: "block" };
5853
6241
  for (const rule of headerMutations) applyHeaderMutation(headers, rule);
5854
6242
  return { verdict: "redact", body, headers };
5855
6243
  };
@@ -5952,7 +6340,9 @@ async function createProtection(options = {}) {
5952
6340
  const screenResp = async (response, reqCtx) => {
5953
6341
  const read = await readTextResponse(response, screenCap);
5954
6342
  if (read.skip) {
5955
- if (read.skip !== "not-a-response") recordSkip("response", read.skip, { status: response?.status });
6343
+ if (read.skip !== "not-a-response") {
6344
+ recordSkip("response", read.skip, { status: response?.status, ...read.encoding ? { encoding: read.encoding } : {} });
6345
+ }
5956
6346
  if (read.skip === "not-a-response") return response;
5957
6347
  return hardenHeadersOnly(response, reqCtx);
5958
6348
  }
@@ -6033,6 +6423,11 @@ async function createProtection(options = {}) {
6033
6423
  const wrapNodeResponse = (res, reqCtx) => {
6034
6424
  const origWrite = res.write.bind(res);
6035
6425
  const origEnd = res.end.bind(res);
6426
+ const setHeaderNow = typeof res.setHeader === "function" ? res.setHeader.bind(res) : null;
6427
+ const removeHeaderNow = typeof res.removeHeader === "function" ? res.removeHeader.bind(res) : null;
6428
+ const sentAccessor = accessorOf(res, "headersSent");
6429
+ let sentValue = sentAccessor ? void 0 : res.headersSent;
6430
+ const reallySent = () => Boolean(sentAccessor ? sentAccessor.call(res) : sentValue);
6036
6431
  const chunks = [];
6037
6432
  let size = 0;
6038
6433
  let overflow = false;
@@ -6043,7 +6438,7 @@ async function createProtection(options = {}) {
6043
6438
  if (hardened) return null;
6044
6439
  hardened = true;
6045
6440
  try {
6046
- if (res.headersSent || typeof res.setHeader !== "function") return null;
6441
+ if (reallySent() || !setHeaderNow) return null;
6047
6442
  const set = typeof res.getHeaders === "function" ? res.getHeaders() : {};
6048
6443
  const headers = effective && effective.headers ? { ...set, ...effective.headers } : set;
6049
6444
  const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
@@ -6055,8 +6450,8 @@ async function createProtection(options = {}) {
6055
6450
  if (!headerValueChanged(headers[name], value)) continue;
6056
6451
  changed.set(name.toLowerCase(), value);
6057
6452
  try {
6058
- if (value === null || value === void 0) res.removeHeader?.(name);
6059
- else res.setHeader(name, value);
6453
+ if (value === null || value === void 0) removeHeaderNow?.(name);
6454
+ else setHeaderNow(name, value);
6060
6455
  } catch {
6061
6456
  }
6062
6457
  }
@@ -6067,26 +6462,139 @@ async function createProtection(options = {}) {
6067
6462
  }
6068
6463
  };
6069
6464
  const stillToAnswer = (rule) => !hardensWithoutBody(rule);
6070
- if (typeof res.writeHead === "function") {
6071
- const origWriteHead = res.writeHead.bind(res);
6465
+ let pendingHead = null;
6466
+ let sending = false;
6467
+ const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
6468
+ if (origWriteHead) {
6072
6469
  res.writeHead = function(...args) {
6073
- const at = writeHeadHeaderIndex(args);
6074
- const given = at === -1 ? null : args[at];
6075
- const changed = hardenBeforeFlush({
6076
- status: typeof args[0] === "number" ? args[0] : void 0,
6077
- headers: given ? writeHeadHeaderObject(given) : null
6078
- });
6079
- if (!changed || !given) return origWriteHead(...args);
6080
- const rewritten = [...args];
6081
- rewritten[at] = rewriteWriteHeadHeaders(given, changed);
6082
- return origWriteHead(...rewritten);
6470
+ if (sending || reallySent()) return origWriteHead(...args);
6471
+ if (pendingHead) throw headersSentError("write");
6472
+ const judged = judgeWriteHead(args);
6473
+ if (judged) {
6474
+ res.statusCode = judged.statusCode;
6475
+ res.statusMessage = judged.statusMessage;
6476
+ mirrorHeaders(judged.headers);
6477
+ pendingHead = args;
6478
+ } else {
6479
+ pendingHead = args;
6480
+ if (typeof args[0] === "number") res.statusCode = args[0];
6481
+ if (typeof args[1] === "string") res.statusMessage = args[1];
6482
+ }
6483
+ return res;
6083
6484
  };
6084
6485
  }
6486
+ function judgeWriteHead(args) {
6487
+ const ServerResponse = nodeHttp?.ServerResponse;
6488
+ if (typeof ServerResponse !== "function") return null;
6489
+ const detached = new ServerResponse({ method: res.req?.method ?? "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} });
6490
+ if (res.statusMessage !== void 0) detached.statusMessage = res.statusMessage;
6491
+ for (const [name, value] of headerEntries(res)) detached.setHeader(name, value);
6492
+ detached.writeHead(...args);
6493
+ return { statusCode: detached.statusCode, statusMessage: detached.statusMessage, headers: headerEntries(detached) };
6494
+ }
6495
+ function headerEntries(target) {
6496
+ const names = typeof target.getRawHeaderNames === "function" ? target.getRawHeaderNames() : typeof target.getHeaderNames === "function" ? target.getHeaderNames() : [];
6497
+ return names.map((name) => [name, target.getHeader(name)]);
6498
+ }
6499
+ function mirrorHeaders(entries) {
6500
+ if (!setHeaderNow) return;
6501
+ for (const [name, value] of entries) {
6502
+ const current = res.getHeader?.(name);
6503
+ if (current === void 0 || headerValueChanged(current, value)) setHeaderNow(name, value);
6504
+ }
6505
+ }
6506
+ Object.defineProperty(res, "headersSent", {
6507
+ configurable: true,
6508
+ enumerable: true,
6509
+ get: () => pendingHead !== null || reallySent(),
6510
+ // A response-like object that tracks the flag itself keeps doing so.
6511
+ set: (value) => {
6512
+ if (!sentAccessor) sentValue = value;
6513
+ }
6514
+ });
6515
+ for (const [method, verb] of [["setHeader", "set"], ["removeHeader", "remove"], ["appendHeader", "append"]]) {
6516
+ const original = typeof res[method] === "function" ? res[method].bind(res) : null;
6517
+ if (!original) continue;
6518
+ res[method] = function(...args) {
6519
+ if (pendingHead) throw headersSentError(verb);
6520
+ return original(...args);
6521
+ };
6522
+ }
6523
+ if (typeof res.flushHeaders === "function") {
6524
+ const origFlushHeaders = res.flushHeaders.bind(res);
6525
+ res.flushHeaders = function() {
6526
+ sendHead();
6527
+ return origFlushHeaders();
6528
+ };
6529
+ }
6530
+ let hardenDone = false;
6531
+ let hardenedValues = null;
6532
+ const hardenOnce = () => {
6533
+ if (hardenDone) return hardenedValues;
6534
+ hardenDone = true;
6535
+ if (!pendingHead) {
6536
+ hardenedValues = hardenBeforeFlush();
6537
+ return hardenedValues;
6538
+ }
6539
+ const at = writeHeadHeaderIndex(pendingHead);
6540
+ hardenedValues = hardenBeforeFlush({
6541
+ status: typeof pendingHead[0] === "number" ? pendingHead[0] : void 0,
6542
+ headers: at === -1 ? null : writeHeadHeaderObject(pendingHead[at])
6543
+ });
6544
+ return hardenedValues;
6545
+ };
6546
+ const effectiveHead = () => {
6547
+ const set = typeof res.getHeaders === "function" ? { ...res.getHeaders() } : {};
6548
+ if (typeof res.getHeaders !== "function" && typeof res.getHeader === "function") {
6549
+ for (const name of ["content-type", "content-length"]) {
6550
+ const value = res.getHeader(name);
6551
+ if (value !== void 0) set[name] = value;
6552
+ }
6553
+ }
6554
+ if (!pendingHead) return { status: res.statusCode, headers: set };
6555
+ const at = writeHeadHeaderIndex(pendingHead);
6556
+ return {
6557
+ status: typeof pendingHead[0] === "number" ? pendingHead[0] : res.statusCode,
6558
+ headers: at === -1 ? set : { ...set, ...writeHeadHeaderObject(pendingHead[at]) }
6559
+ };
6560
+ };
6561
+ const sendHead = (changes) => {
6562
+ if (sending) return;
6563
+ const hardened2 = hardenOnce();
6564
+ sending = true;
6565
+ if (!pendingHead) {
6566
+ if (changes) applyToResponse(changes);
6567
+ return;
6568
+ }
6569
+ const args = [...pendingHead];
6570
+ pendingHead = null;
6571
+ const merged = new Map(hardened2 ?? []);
6572
+ for (const [name, value] of changes?.headers ?? []) merged.set(name, value);
6573
+ if (changes?.status !== void 0) {
6574
+ args[0] = changes.status;
6575
+ if (typeof args[1] === "string") args.splice(1, 1);
6576
+ }
6577
+ const headerAt = writeHeadHeaderIndex(args);
6578
+ if (headerAt !== -1 && merged.size) args[headerAt] = rewriteWriteHeadHeaders(args[headerAt], merged);
6579
+ applyToResponse({ headers: merged });
6580
+ origWriteHead(...args);
6581
+ };
6582
+ const applyToResponse = (changes) => {
6583
+ if (changes.status !== void 0) res.statusCode = changes.status;
6584
+ for (const [name, value] of changes.headers ?? []) {
6585
+ try {
6586
+ if (value === null || value === void 0) removeHeaderNow?.(name);
6587
+ else setHeaderNow?.(name, value);
6588
+ } catch {
6589
+ }
6590
+ }
6591
+ };
6085
6592
  const collect = (chunk, enc) => {
6086
6593
  if (chunk == null) return;
6087
6594
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
6088
6595
  size += buf.length;
6089
6596
  if (size > MAX) {
6597
+ sendHead();
6090
6598
  for (const c of chunks) origWrite(c);
6091
6599
  chunks.length = 0;
6092
6600
  origWrite(buf);
@@ -6097,7 +6605,6 @@ async function createProtection(options = {}) {
6097
6605
  chunks.push(buf);
6098
6606
  };
6099
6607
  res.write = function(chunk, enc, cb) {
6100
- hardenBeforeFlush();
6101
6608
  if (overflow) return origWrite(chunk, enc, cb);
6102
6609
  collect(chunk, enc);
6103
6610
  if (typeof enc === "function") enc();
@@ -6105,7 +6612,6 @@ async function createProtection(options = {}) {
6105
6612
  return true;
6106
6613
  };
6107
6614
  res.end = function(chunk, enc, cb) {
6108
- hardenBeforeFlush();
6109
6615
  if (typeof chunk === "function") {
6110
6616
  cb = chunk;
6111
6617
  chunk = void 0;
@@ -6120,68 +6626,77 @@ async function createProtection(options = {}) {
6120
6626
  }
6121
6627
  collect(chunk, enc);
6122
6628
  if (overflow) return origEnd(cb);
6629
+ hardenOnce();
6630
+ const passThrough = () => {
6631
+ sendHead();
6632
+ for (const c of chunks) origWrite(c);
6633
+ return origEnd(cb);
6634
+ };
6123
6635
  const buffer = Buffer.concat(chunks);
6124
- let ct = res.getHeader ? res.getHeader("content-type") : void 0;
6636
+ const head = effectiveHead();
6637
+ let ct = head.headers["content-type"];
6125
6638
  if (Array.isArray(ct)) ct = ct[0];
6126
6639
  const kind = screenableContentType(ct);
6127
6640
  if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
6128
6641
  recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
6129
- for (const c of chunks) origWrite(c);
6130
- return origEnd(cb);
6642
+ return passThrough();
6643
+ }
6644
+ const codings = declaredCodings(head.headers["content-encoding"]);
6645
+ if (codings.length > 0 && stillEncoded(buffer)) {
6646
+ recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
6647
+ return passThrough();
6131
6648
  }
6132
6649
  const text = buffer.toString("utf8");
6133
6650
  let r;
6134
6651
  try {
6135
- r = screenText(
6136
- text,
6137
- { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} },
6138
- reqCtx,
6139
- answeredWithoutBody ? stillToAnswer : void 0
6140
- );
6652
+ r = screenText(text, head, reqCtx, answeredWithoutBody ? stillToAnswer : void 0);
6141
6653
  } catch (err) {
6142
6654
  notify(onError, err, "onError");
6143
- for (const c of chunks) origWrite(c);
6144
- return origEnd(cb);
6655
+ return passThrough();
6656
+ }
6657
+ if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
6658
+ if (reallySent() && res.chunkedEncoding !== true) {
6659
+ notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
6660
+ res.destroy?.();
6661
+ return res;
6145
6662
  }
6146
6663
  if (r.verdict === "block") {
6147
- res.statusCode = 500;
6148
- try {
6149
- res.setHeader("content-type", "application/json");
6150
- } catch {
6664
+ const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
6665
+ if (reallySent()) {
6666
+ res.destroy?.();
6667
+ return res;
6151
6668
  }
6152
- return origEnd(JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" }), cb);
6669
+ sendHead({
6670
+ status: 500,
6671
+ headers: /* @__PURE__ */ new Map([
6672
+ ["content-type", "application/json"],
6673
+ ["content-length", String(Buffer.byteLength(body))],
6674
+ ["transfer-encoding", null]
6675
+ ])
6676
+ });
6677
+ return origEnd(body, cb);
6153
6678
  }
6154
- if (r.verdict === "redact") {
6155
- try {
6156
- res.removeHeader && res.removeHeader("content-length");
6157
- } catch {
6679
+ const changed = /* @__PURE__ */ new Map();
6680
+ if (r.headers) {
6681
+ for (const [name, value] of Object.entries(r.headers)) {
6682
+ if (name.toLowerCase() === "content-length") continue;
6683
+ if (!headerValueChanged(head.headers[name], value)) continue;
6684
+ changed.set(name.toLowerCase(), value);
6158
6685
  }
6159
- if (r.headers && res.setHeader) {
6160
- const current = res.getHeaders ? res.getHeaders() : {};
6161
- for (const [name, value] of Object.entries(r.headers)) {
6162
- if (name.toLowerCase() === "content-length") continue;
6163
- if (value === null || value === void 0) {
6164
- try {
6165
- res.removeHeader && res.removeHeader(name);
6166
- } catch {
6167
- }
6168
- } else if (Array.isArray(value)) {
6169
- try {
6170
- res.setHeader(name, value);
6171
- } catch {
6172
- }
6173
- } else if (typeof value === "string" && current[name] !== value) {
6174
- try {
6175
- res.setHeader(name, value);
6176
- } catch {
6177
- }
6178
- }
6179
- }
6686
+ }
6687
+ if (!reallySent()) {
6688
+ const declared = head.headers["content-length"];
6689
+ if (declared === void 0) {
6690
+ changed.set("content-length", null);
6691
+ } else {
6692
+ changed.set("content-length", String(Buffer.byteLength(r.body)));
6693
+ changed.set("transfer-encoding", null);
6180
6694
  }
6181
- return origEnd(r.body, cb);
6695
+ sendHead({ headers: changed });
6696
+ } else if (changed.size) {
6697
+ recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
6182
6698
  }
6183
- for (const c of chunks) origWrite(c);
6184
- return origEnd(cb);
6699
+ return origEnd(r.body, cb);
6185
6700
  };
6186
6701
  };
6187
6702
  const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
@@ -6394,6 +6909,7 @@ async function createProtection(options = {}) {
6394
6909
  notify(onError, err, "onError");
6395
6910
  }
6396
6911
  }
6912
+ let recovery = null;
6397
6913
  const runRefreshTick = async () => {
6398
6914
  if (reporter) {
6399
6915
  try {
@@ -6410,23 +6926,33 @@ async function createProtection(options = {}) {
6410
6926
  });
6411
6927
  mode = resolveMode(options, next);
6412
6928
  applyBundle(next);
6929
+ noteRuleSource(next.source);
6930
+ if (next.source?.ok !== false) recovery?.stop();
6413
6931
  await applyAndAcknowledge(next.source?.origin, declaredState);
6414
6932
  if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
6415
6933
  return next.source ?? { ok: true };
6416
6934
  };
6935
+ const refreshTick = serialise(runRefreshTick);
6417
6936
  if (live) {
6418
- protection.refresh = () => runRefreshTick();
6419
- protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
6937
+ protection.refresh = () => refreshTick();
6938
+ protection.refreshHandler = () => makeRefreshHandler(refreshTick, refreshSecret);
6420
6939
  }
6421
- const loop = options.refreshMs > 0 && live ? startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError }) : null;
6940
+ const loop = options.refreshMs > 0 && live ? startRefresh(refreshTick, { refreshMs: options.refreshMs, onError }) : null;
6941
+ const canAsk = Boolean(options.token || pulseAuth);
6942
+ recovery = live && canAsk && !loop && ruleSource.ok === false ? startRecovery(refreshTick, { onError }) : null;
6422
6943
  protection.stop = () => {
6423
6944
  loop?.stop();
6945
+ recovery?.stop();
6424
6946
  const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
6425
6947
  (wait) => wait && typeof wait.then === "function"
6426
6948
  );
6427
6949
  return Promise.all(outstanding).then(() => void 0);
6428
6950
  };
6429
6951
  protection.stopRefresh = protection.stop;
6952
+ Object.defineProperty(protection, "ruleSource", {
6953
+ get: () => ({ ...ruleSource }),
6954
+ enumerable: true
6955
+ });
6430
6956
  Object.defineProperty(protection, "detectionReporting", {
6431
6957
  get: () => detectionReporting,
6432
6958
  enumerable: true
@@ -6437,6 +6963,16 @@ async function createProtection(options = {}) {
6437
6963
  });
6438
6964
  return protection;
6439
6965
  }
6966
+ function accessorOf(obj, name) {
6967
+ for (let o = obj; o; o = Object.getPrototypeOf(o)) {
6968
+ const d = Object.getOwnPropertyDescriptor(o, name);
6969
+ if (d) return typeof d.get === "function" ? d.get : null;
6970
+ }
6971
+ return null;
6972
+ }
6973
+ function headersSentError(verb) {
6974
+ return Object.assign(new Error(`Cannot ${verb} headers after they are sent to the client`), { code: "ERR_HTTP_HEADERS_SENT" });
6975
+ }
6440
6976
  function resolveMode(options, bundle) {
6441
6977
  const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
6442
6978
  if (env === "block" || env === "dry-run") return env;
@@ -6509,11 +7045,26 @@ function looksBinary(bytes) {
6509
7045
  }
6510
7046
  return n > 0 && ctrl / n > 0.1;
6511
7047
  }
7048
+ function declaredCodings(value) {
7049
+ const raw = Array.isArray(value) ? value.join(",") : String(value ?? "");
7050
+ return raw.toLowerCase().split(",").map((c) => c.trim()).filter((c) => c !== "" && c !== "identity");
7051
+ }
7052
+ function stillEncoded(bytes) {
7053
+ if (looksBinary(bytes)) return true;
7054
+ try {
7055
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
7056
+ return false;
7057
+ } catch {
7058
+ return true;
7059
+ }
7060
+ }
6512
7061
  async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
6513
7062
  if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
6514
7063
  const ct = response.headers?.get?.("content-type") || "";
6515
7064
  const kind = screenableContentType(ct);
6516
7065
  if (kind === "skip") return { skip: baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" };
7066
+ const codings = declaredCodings(response.headers?.get?.("content-encoding"));
7067
+ const encodedSkip = () => ({ skip: "encoded-body", encoding: codings.join(", ") });
6517
7068
  const sniff = kind === "sniff";
6518
7069
  const len = Number(response.headers?.get?.("content-length") || 0);
6519
7070
  if (len && len > cap) return { skip: "body-cap" };
@@ -6557,13 +7108,21 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
6557
7108
  } finally {
6558
7109
  reader.releaseLock();
6559
7110
  }
7111
+ const bytes = concatBytes(chunks, size);
7112
+ if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
6560
7113
  try {
6561
- return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
7114
+ return { text: new TextDecoder().decode(bytes) };
6562
7115
  } catch {
6563
7116
  return { skip: "decode-failed" };
6564
7117
  }
6565
7118
  }
6566
7119
  try {
7120
+ if (codings.length > 0) {
7121
+ const bytes = new Uint8Array(await clone.arrayBuffer());
7122
+ if (bytes.byteLength > cap) return { skip: "body-cap" };
7123
+ if (stillEncoded(bytes)) return encodedSkip();
7124
+ return { text: new TextDecoder().decode(bytes) };
7125
+ }
6567
7126
  const text = await clone.text();
6568
7127
  if (text.length > cap) return { skip: "body-cap" };
6569
7128
  if (sniff && looksBinary(new TextEncoder().encode(text.slice(0, 512)))) return { skip: "binary-body" };
@@ -6637,6 +7196,39 @@ function extractRedactors(rule) {
6637
7196
  function htmlEscape(str) {
6638
7197
  return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6639
7198
  }
7199
+ function isJson(text) {
7200
+ try {
7201
+ JSON.parse(text);
7202
+ return true;
7203
+ } catch {
7204
+ return false;
7205
+ }
7206
+ }
7207
+ function preservesJsonStructure(before, after) {
7208
+ if (!isJson(before)) return true;
7209
+ if (!isJson(after)) return false;
7210
+ const expected = jsonStructure(before);
7211
+ const actual = jsonStructure(after);
7212
+ for (; ; ) {
7213
+ const left = expected.next();
7214
+ const right = actual.next();
7215
+ if (left.done || right.done) return left.done === right.done;
7216
+ if (left.value !== right.value) return false;
7217
+ }
7218
+ }
7219
+ function* jsonStructure(text) {
7220
+ const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|[^\s]/g;
7221
+ for (const match of text.matchAll(tokens)) {
7222
+ const token = match[0];
7223
+ if (token[0] !== '"') {
7224
+ yield "token:" + token;
7225
+ continue;
7226
+ }
7227
+ let next = match.index + token.length;
7228
+ while (text[next] === " " || text[next] === " " || text[next] === "\r" || text[next] === "\n") next++;
7229
+ yield text[next] === ":" ? "key:" + JSON.parse(token) : "string";
7230
+ }
7231
+ }
6640
7232
  function applyRedactors(body, redactors, mask, transform) {
6641
7233
  let out = body;
6642
7234
  for (const r of redactors) {
@@ -6679,10 +7271,12 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
6679
7271
  if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
6680
7272
  const head = text.trimStart()[0];
6681
7273
  if (head !== "{" && head !== "[") return text;
6682
- const preserved = preserveBigInts(text);
7274
+ let preserved;
6683
7275
  let obj;
6684
7276
  try {
6685
- obj = JSON.parse(preserved);
7277
+ JSON.parse(text);
7278
+ preserved = preserveBigInts(text, mask);
7279
+ obj = JSON.parse(preserved.text);
6686
7280
  } catch {
6687
7281
  return text;
6688
7282
  }
@@ -6691,65 +7285,44 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
6691
7285
  const pred = conditionPredicate(r.condition);
6692
7286
  walkLeaves(obj, r.jsonPath, (loc) => {
6693
7287
  try {
6694
- if (pred(loc.value)) {
6695
- loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
7288
+ const number = preserved.numbers.get(loc.value);
7289
+ if (pred(number === void 0 ? loc.value : Number(number))) {
7290
+ setOwn(loc.parent, loc.key, transform ? transform(number ?? String(loc.value)) : mask);
6696
7291
  changed = true;
6697
7292
  }
6698
7293
  } catch {
6699
7294
  }
6700
7295
  });
6701
7296
  }
6702
- return changed ? restoreBigInts(JSON.stringify(obj)) : text;
6703
- }
6704
- var BIGINT_OPEN = "__PSBIGINT_9c2f__";
6705
- var BIGINT_CLOSE = "__DNEGIB__";
6706
- function preserveBigInts(text) {
6707
- let out = "";
6708
- let inStr = false;
6709
- for (let i = 0; i < text.length; ) {
6710
- const ch = text[i];
6711
- if (inStr) {
6712
- out += ch;
6713
- if (ch === "\\") {
6714
- out += text[i + 1] ?? "";
6715
- i += 2;
6716
- continue;
6717
- }
6718
- if (ch === '"') inStr = false;
6719
- i++;
6720
- continue;
6721
- }
6722
- if (ch === '"') {
6723
- inStr = true;
6724
- out += ch;
6725
- i++;
6726
- continue;
6727
- }
6728
- if (ch === "-" || ch >= "0" && ch <= "9") {
6729
- let j = ch === "-" ? i + 1 : i;
6730
- let digits = 0;
6731
- while (j < text.length && text[j] >= "0" && text[j] <= "9") {
6732
- digits++;
6733
- j++;
6734
- }
6735
- const next = text[j];
6736
- const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
6737
- if (isIntToken && digits >= 16) {
6738
- out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
6739
- } else {
6740
- out += text.slice(i, j || i + 1);
6741
- }
6742
- i = j > i ? j : i + 1;
6743
- continue;
6744
- }
6745
- out += ch;
6746
- i++;
6747
- }
6748
- return out;
7297
+ return changed ? restoreBigInts(JSON.stringify(obj), preserved.numbers) : text;
6749
7298
  }
6750
- function restoreBigInts(text) {
6751
- if (!text.includes(BIGINT_OPEN)) return text;
6752
- return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
7299
+ function preserveBigInts(text, mask) {
7300
+ const occupied = /* @__PURE__ */ new Set([mask]);
7301
+ const integers = [];
7302
+ const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
7303
+ for (const match of text.matchAll(tokens)) {
7304
+ const token = match[0];
7305
+ if (token[0] === '"') occupied.add(JSON.parse(token));
7306
+ else if (/^-?\d{16,}$/.test(token)) integers.push({ start: match.index, token });
7307
+ }
7308
+ const numbers = /* @__PURE__ */ new Map();
7309
+ const chunks = [];
7310
+ let offset = 0;
7311
+ let index = 0;
7312
+ for (const { start, token } of integers) {
7313
+ let marker;
7314
+ do {
7315
+ marker = "__PSNUMBER_" + index++ + "__";
7316
+ } while (occupied.has(marker));
7317
+ numbers.set(marker, token);
7318
+ chunks.push(text.slice(offset, start), JSON.stringify(marker));
7319
+ offset = start + token.length;
7320
+ }
7321
+ chunks.push(text.slice(offset));
7322
+ return { text: chunks.join(""), numbers };
7323
+ }
7324
+ function restoreBigInts(text, numbers) {
7325
+ return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
6753
7326
  }
6754
7327
  function isHeaderMutation(action) {
6755
7328
  return action === "set-header" || action === "remove-header" || action === "harden-cookie";