@patchstack/connect 0.5.11 → 0.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/protect.js CHANGED
@@ -1185,6 +1185,184 @@ function hasAdjacentUnbounded(body, flags) {
1185
1185
  }
1186
1186
  return false;
1187
1187
  }
1188
+ function parseRepetition(body) {
1189
+ let i = 0;
1190
+ const parseAlternation = () => {
1191
+ const branches = [parseSequence()];
1192
+ while (body[i] === "|") {
1193
+ i++;
1194
+ branches.push(parseSequence());
1195
+ }
1196
+ return branches.length === 1 ? branches[0] : { type: "alt", branches };
1197
+ };
1198
+ const parseSequence = () => {
1199
+ const items = [];
1200
+ while (i < body.length && body[i] !== "|" && body[i] !== ")") {
1201
+ const atom = parseAtom();
1202
+ if (atom === null) return null;
1203
+ const quantified = parseQuantifier(atom);
1204
+ if (quantified === null) return null;
1205
+ items.push(quantified);
1206
+ }
1207
+ return { type: "seq", items };
1208
+ };
1209
+ const parseAtom = () => {
1210
+ const ch = body[i];
1211
+ if (ch === "(") {
1212
+ const start2 = i;
1213
+ i++;
1214
+ let kind = "capture";
1215
+ if (body.startsWith("?:", i)) {
1216
+ kind = "group";
1217
+ i += 2;
1218
+ } else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
1219
+ kind = "look";
1220
+ i += 2;
1221
+ } else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
1222
+ kind = "look";
1223
+ i += 3;
1224
+ } else if (body.startsWith("?<", i)) {
1225
+ const close = body.indexOf(">", i + 2);
1226
+ if (close === -1) return null;
1227
+ i = close + 1;
1228
+ } else if (body[i] === "?") {
1229
+ return null;
1230
+ }
1231
+ const inner = parseAlternation();
1232
+ if (inner === null || body[i] !== ")") return null;
1233
+ i++;
1234
+ return { type: "group", kind, body: inner, source: body.slice(start2, i) };
1235
+ }
1236
+ const start = i;
1237
+ if (ch === "[") {
1238
+ i++;
1239
+ if (body[i] === "^") i++;
1240
+ if (body[i] === "]") i++;
1241
+ while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
1242
+ if (body[i] !== "]") return null;
1243
+ i++;
1244
+ return { type: "atom", source: body.slice(start, i) };
1245
+ }
1246
+ if (ch === "\\") {
1247
+ const next = body[i + 1];
1248
+ if (next === void 0) return null;
1249
+ if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
1250
+ const close = body.indexOf("}", i + 3);
1251
+ if (close === -1) return null;
1252
+ i = close + 1;
1253
+ } else if (next === "k" && body[i + 2] === "<") {
1254
+ const close = body.indexOf(">", i + 3);
1255
+ if (close === -1) return null;
1256
+ i = close + 1;
1257
+ } else {
1258
+ i += 2;
1259
+ }
1260
+ return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
1261
+ }
1262
+ if (ch === "^" || ch === "$") {
1263
+ i++;
1264
+ return { type: "assert" };
1265
+ }
1266
+ if (ch === "*" || ch === "+" || ch === "?") return null;
1267
+ i++;
1268
+ return { type: "atom", source: body.slice(start, i) };
1269
+ };
1270
+ const parseQuantifier = (atom) => {
1271
+ let min;
1272
+ let max;
1273
+ const ch = body[i];
1274
+ if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
1275
+ else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
1276
+ else if (ch === "?") [min, max, i] = [0, 1, i + 1];
1277
+ else if (ch === "{") {
1278
+ const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
1279
+ if (!bounds) return atom;
1280
+ min = Number(bounds[1]);
1281
+ max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
1282
+ i += bounds[0].length;
1283
+ } else {
1284
+ return atom;
1285
+ }
1286
+ if (body[i] === "?") i++;
1287
+ if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
1288
+ return { type: "quant", min, max, body: atom };
1289
+ };
1290
+ const root = parseAlternation();
1291
+ return root !== null && i === body.length ? root : null;
1292
+ }
1293
+ function* nodesOf(node) {
1294
+ if (!node) return;
1295
+ yield node;
1296
+ if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
1297
+ else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
1298
+ else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
1299
+ }
1300
+ var BOUNDED_NESTING_LIMIT = 100;
1301
+ var BOUNDED_ALTERNATION_LIMIT = 16;
1302
+ var OPTIONAL_REPEAT_LIMIT = 8;
1303
+ function hasFencedRepetitions(quant, flags) {
1304
+ let body = quant.body;
1305
+ if (body.type === "group" && body.kind !== "look") body = body.body;
1306
+ if (body.type !== "seq" || body.items.length < 2) return false;
1307
+ const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
1308
+ if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
1309
+ const atomFlags = flags.replace(/[^i]/g, "");
1310
+ const unicode = /[uv]/.test(flags);
1311
+ let insides;
1312
+ try {
1313
+ insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
1314
+ } catch {
1315
+ return false;
1316
+ }
1317
+ const literalOf = (source) => {
1318
+ if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
1319
+ if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
1320
+ return null;
1321
+ };
1322
+ const disjoint = (edge) => {
1323
+ if (edge?.type !== "atom" || edge.source === void 0) return false;
1324
+ const literal = literalOf(edge.source);
1325
+ if (literal !== null) return insides.every((re) => !re.test(literal));
1326
+ if (unicode) return false;
1327
+ let fence;
1328
+ try {
1329
+ fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
1330
+ } catch {
1331
+ return false;
1332
+ }
1333
+ for (let unit = 0; unit <= 65535; unit++) {
1334
+ const ch = String.fromCharCode(unit);
1335
+ if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
1336
+ }
1337
+ return true;
1338
+ };
1339
+ return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
1340
+ }
1341
+ function hasCatastrophicShape(body, flags) {
1342
+ const root = parseRepetition(body);
1343
+ if (root === null) return null;
1344
+ for (const node of nodesOf(root)) {
1345
+ if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
1346
+ if (node.type !== "quant" || node.max <= 1) continue;
1347
+ const fenced = hasFencedRepetitions(node, flags);
1348
+ for (const inner of nodesOf(node.body)) {
1349
+ if (inner === node.body && inner.type === "quant") continue;
1350
+ if (inner.type === "quant" && inner.max > 1 && !fenced) {
1351
+ if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
1352
+ }
1353
+ if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
1354
+ return true;
1355
+ }
1356
+ if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
1357
+ return true;
1358
+ }
1359
+ }
1360
+ }
1361
+ return false;
1362
+ }
1363
+ function lookaroundBody(source) {
1364
+ return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
1365
+ }
1188
1366
  function groupEnd(body, start) {
1189
1367
  let depth = 0;
1190
1368
  let inClass = false;
@@ -1211,6 +1389,8 @@ function warnRejectedPatternOnce(pattern) {
1211
1389
  );
1212
1390
  }
1213
1391
  var MAX_PATTERN_LENGTH = 1e3;
1392
+ var regexVerdicts = /* @__PURE__ */ new Map();
1393
+ var REGEX_VERDICT_LIMIT = 2048;
1214
1394
  function safeRegExp(pattern) {
1215
1395
  if (!pattern) {
1216
1396
  return null;
@@ -1222,16 +1402,32 @@ function safeRegExp(pattern) {
1222
1402
  if (!match) {
1223
1403
  return null;
1224
1404
  }
1225
- for (const dangerous of REDOS_PATTERNS) {
1226
- if (dangerous.test(match[1])) return null;
1405
+ let safe = regexVerdicts.get(pattern);
1406
+ if (safe === void 0) {
1407
+ safe = isSafeBody(match[1], match[2]);
1408
+ if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
1409
+ regexVerdicts.set(pattern, safe);
1227
1410
  }
1228
- if (hasAdjacentUnbounded(match[1], match[2])) return null;
1411
+ if (!safe) return null;
1229
1412
  try {
1230
1413
  return new RegExp(match[1], match[2]);
1231
1414
  } catch {
1232
1415
  return null;
1233
1416
  }
1234
1417
  }
1418
+ function isSafeBody(body, flags) {
1419
+ for (const dangerous of REDOS_PATTERNS) {
1420
+ if (dangerous.test(body)) return false;
1421
+ }
1422
+ if (hasAdjacentUnbounded(body, flags)) return false;
1423
+ if (hasCatastrophicShape(body, flags) === true) return false;
1424
+ try {
1425
+ new RegExp(body, flags);
1426
+ } catch {
1427
+ return false;
1428
+ }
1429
+ return true;
1430
+ }
1235
1431
  function ctypeResult(strValue, isClass, matchVal) {
1236
1432
  if (strValue === "") {
1237
1433
  return false;
@@ -2952,7 +3148,7 @@ function concatChunks(chunks, total) {
2952
3148
  }
2953
3149
  function parseBody(rawBody, contentType) {
2954
3150
  const ct = String(contentType || "").toLowerCase();
2955
- const isJson = ct.includes("application/json") || /\+json\b/.test(ct);
3151
+ const isJson2 = ct.includes("application/json") || /\+json\b/.test(ct);
2956
3152
  const isForm = ct.includes("application/x-www-form-urlencoded");
2957
3153
  const isMultipart = ct.includes("multipart/form-data");
2958
3154
  const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
@@ -2966,7 +3162,7 @@ function parseBody(rawBody, contentType) {
2966
3162
  for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
2967
3163
  return { body, files: void 0 };
2968
3164
  }
2969
- if (isJson || isAmbiguous) {
3165
+ if (isJson2 || isAmbiguous) {
2970
3166
  try {
2971
3167
  const parsed = JSON.parse(rawBody);
2972
3168
  if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
@@ -2982,22 +3178,68 @@ function parseMultipart(rawBody, boundary) {
2982
3178
  const sep = /\r?\n\r?\n/.exec(part);
2983
3179
  if (!sep) continue;
2984
3180
  const rawHeaders = part.slice(0, sep.index);
2985
- const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
2986
- if (!disposition) continue;
2987
- const name = /name="([^"]*)"/i.exec(disposition)?.[1];
2988
- if (name == null) continue;
3181
+ const disposition = /content-disposition:([^\r\n]*)/i.exec(rawHeaders)?.[1];
3182
+ if (disposition === void 0) continue;
3183
+ const { names, filename } = dispositionFields(disposition);
3184
+ if (names.length === 0) continue;
2989
3185
  const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
2990
- const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
2991
- if (filename !== void 0) {
2992
- const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
2993
- const file = { filename, type: partType, content };
2994
- appendOwn(files, name, file);
2995
- } else {
2996
- appendOwn(body, name, content);
3186
+ const file = filename === void 0 ? void 0 : { filename, type: /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "", content };
3187
+ for (const name of names) {
3188
+ if (file) appendOwn(files, name, file);
3189
+ else appendOwn(body, name, content);
2997
3190
  }
2998
3191
  }
2999
3192
  return { body, files };
3000
3193
  }
3194
+ function dispositionFields(value) {
3195
+ const text = String(value);
3196
+ const names = /* @__PURE__ */ new Set();
3197
+ let filename;
3198
+ let extendedFilename;
3199
+ let i = text.indexOf(";");
3200
+ while (i !== -1 && i < text.length) {
3201
+ i++;
3202
+ while (text[i] === " " || text[i] === " ") i++;
3203
+ let key = "";
3204
+ while (i < text.length && text[i] !== "=" && text[i] !== ";") key += text[i++];
3205
+ key = key.trim().toLowerCase();
3206
+ let val = null;
3207
+ if (text[i] === "=") {
3208
+ i++;
3209
+ while (text[i] === " " || text[i] === " ") i++;
3210
+ if (text[i] === '"') {
3211
+ i++;
3212
+ val = "";
3213
+ while (i < text.length && text[i] !== '"') {
3214
+ if (text[i] === "\\" && i + 1 < text.length) i++;
3215
+ val += text[i++];
3216
+ }
3217
+ i++;
3218
+ while (i < text.length && text[i] !== ";") i++;
3219
+ } else {
3220
+ let bare = "";
3221
+ while (i < text.length && text[i] !== ";") bare += text[i++];
3222
+ val = bare.trim();
3223
+ }
3224
+ }
3225
+ if (val !== null) {
3226
+ if (key === "name") names.add(val);
3227
+ else if (key === "filename") filename = val;
3228
+ else if (key === "filename*") extendedFilename = decodeExtendedValue(val);
3229
+ }
3230
+ i = i < text.length && text[i] === ";" ? i : -1;
3231
+ }
3232
+ return { names: [...names], filename: extendedFilename ?? filename };
3233
+ }
3234
+ function decodeExtendedValue(value) {
3235
+ const match = /^([^']*)'[^']*'(.*)$/.exec(value);
3236
+ if (!match) return value;
3237
+ try {
3238
+ return decodeURIComponent(match[2]);
3239
+ } catch {
3240
+ return match[2];
3241
+ }
3242
+ }
3001
3243
  function parseCookies(header) {
3002
3244
  const cookies = {};
3003
3245
  if (!header) {
@@ -3268,14 +3510,29 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
3268
3510
  if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
3269
3511
  });
3270
3512
  }
3513
+ let syncBuiltins = () => {
3514
+ };
3515
+ try {
3516
+ const { syncBuiltinESMExports } = await import("module");
3517
+ if (typeof syncBuiltinESMExports === "function") syncBuiltins = syncBuiltinESMExports;
3518
+ } catch {
3519
+ }
3520
+ let patchedAny = false;
3271
3521
  for (const moduleName of ["node:http", "node:https"]) {
3272
3522
  try {
3273
3523
  const mod = await import(moduleName);
3274
3524
  const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
3275
- if (restore) restores.push(restore);
3525
+ if (restore) {
3526
+ patchedAny = true;
3527
+ restores.push(() => {
3528
+ restore();
3529
+ syncBuiltins();
3530
+ });
3531
+ }
3276
3532
  } catch {
3277
3533
  }
3278
3534
  }
3535
+ if (patchedAny) syncBuiltins();
3279
3536
  return () => {
3280
3537
  for (const restore of restores) {
3281
3538
  try {
@@ -3289,18 +3546,22 @@ function patchHttpModule(http, block, screen, skip) {
3289
3546
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
3290
3547
  const originalRequest = http.request;
3291
3548
  const originalGet = http.get;
3292
- const wrap = (original) => function(...args) {
3549
+ const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
3550
+ const guardArgs = (args) => {
3293
3551
  const target = extractHttpTarget(args);
3294
3552
  if (target && block(target.url, target.host, target.method)) {
3295
3553
  throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
3296
3554
  }
3297
3555
  if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
3298
3556
  try {
3299
- args = withScreeningLookup(args, target, block, screen.lookup, skip);
3557
+ return withScreeningLookup(args, target, block, screen.lookup, skip);
3300
3558
  } catch {
3301
3559
  }
3302
3560
  }
3303
- return original.apply(this, args);
3561
+ return args;
3562
+ };
3563
+ const wrap = (original) => function(...args) {
3564
+ return original.apply(this, guardArgs(args));
3304
3565
  };
3305
3566
  const guardedRequest = wrap(originalRequest);
3306
3567
  http.request = guardedRequest;
@@ -3309,10 +3570,21 @@ function patchHttpModule(http, block, screen, skip) {
3309
3570
  guardedGet = wrap(originalGet);
3310
3571
  http.get = guardedGet;
3311
3572
  }
3573
+ let GuardedClientRequest;
3574
+ if (OriginalClientRequest) {
3575
+ GuardedClientRequest = function ClientRequest(...args) {
3576
+ const screened = guardArgs(args);
3577
+ return new.target ? Reflect.construct(OriginalClientRequest, screened, new.target) : Reflect.construct(OriginalClientRequest, screened);
3578
+ };
3579
+ GuardedClientRequest.prototype = OriginalClientRequest.prototype;
3580
+ Object.setPrototypeOf(GuardedClientRequest, OriginalClientRequest);
3581
+ http.ClientRequest = GuardedClientRequest;
3582
+ }
3312
3583
  http.__patchstackGuarded = true;
3313
3584
  return () => {
3314
3585
  if (http.request === guardedRequest) http.request = originalRequest;
3315
3586
  if (guardedGet && http.get === guardedGet) http.get = originalGet;
3587
+ if (GuardedClientRequest && http.ClientRequest === GuardedClientRequest) http.ClientRequest = OriginalClientRequest;
3316
3588
  delete http.__patchstackGuarded;
3317
3589
  };
3318
3590
  }
@@ -3322,19 +3594,27 @@ function extractHttpTarget(args) {
3322
3594
  if (typeof first === "string" || first instanceof URL) {
3323
3595
  const url = new URL(String(first));
3324
3596
  const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
3325
- return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
3326
- }
3327
- if (first && typeof first === "object") {
3328
- const host = normalizeHost(first.hostname || first.host) || "localhost";
3329
- const protocol = first.protocol || "http:";
3330
- const port = first.port ? `:${first.port}` : "";
3331
- const path = first.path || "/";
3332
- return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
3597
+ return targetOf({ ...optionsFromUrl(url), ...opts ?? {} });
3333
3598
  }
3599
+ if (first && typeof first === "object") return targetOf(first);
3334
3600
  } catch {
3335
3601
  }
3336
3602
  return null;
3337
3603
  }
3604
+ function optionsFromUrl(url) {
3605
+ const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
3606
+ const options = { protocol: url.protocol, hostname, path: `${url.pathname || "/"}${url.search}` };
3607
+ if (url.port !== "") options.port = Number(url.port);
3608
+ return options;
3609
+ }
3610
+ function targetOf(options) {
3611
+ const host = normalizeHost(options.hostname || options.host) || "localhost";
3612
+ const protocol = options.protocol || "http:";
3613
+ const port = options.port ? `:${options.port}` : "";
3614
+ const path = options.path || "/";
3615
+ const authority = host.includes(":") ? `[${host}]` : host;
3616
+ return { url: `${protocol}//${authority}${port}${path}`, host, method: options.method || "GET" };
3617
+ }
3338
3618
  function normalizeHost(raw) {
3339
3619
  const host = String(raw || "").trim();
3340
3620
  const bracketed = /^\[([^\]]+)\]/.exec(host);
@@ -3715,19 +3995,49 @@ async function loadFs() {
3715
3995
  }
3716
3996
  return fsMod;
3717
3997
  }
3998
+ async function sourceIdentity(options = {}) {
3999
+ if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
4000
+ const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
4001
+ return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
4002
+ }
4003
+ if (typeof options.token === "string" && options.token !== "") {
4004
+ const digest = await sha256Hex(options.token);
4005
+ if (digest === null) return null;
4006
+ const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
4007
+ return `token:${digest}@${endpoint}`;
4008
+ }
4009
+ return null;
4010
+ }
4011
+ async function sha256Hex(text) {
4012
+ try {
4013
+ const subtle = globalThis.crypto?.subtle;
4014
+ if (!subtle || typeof TextEncoder === "undefined") return null;
4015
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
4016
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
4017
+ } catch {
4018
+ return null;
4019
+ }
4020
+ }
3718
4021
  function makeStore(options = {}) {
3719
4022
  let mem = null;
3720
4023
  const durable = durableTier(options);
4024
+ let identity;
4025
+ const identityOf = () => identity ??= sourceIdentity(options);
3721
4026
  return {
3722
4027
  async read() {
3723
4028
  if (mem) return mem;
4029
+ const source = await identityOf();
4030
+ if (source === null) return null;
3724
4031
  const env = await durable.read();
3725
- if (env) mem = env;
4032
+ if (!env || env.source !== source) return null;
4033
+ mem = env;
3726
4034
  return env;
3727
4035
  },
3728
4036
  async write(env) {
3729
- if (env) mem = env;
3730
- await durable.write(env);
4037
+ if (!env) return;
4038
+ const stamped = { ...env, source: await identityOf() };
4039
+ mem = stamped;
4040
+ await durable.write(stamped);
3731
4041
  }
3732
4042
  };
3733
4043
  }
@@ -3849,11 +4159,12 @@ function toEnvelope(value) {
3849
4159
  buildId,
3850
4160
  // A confirmation belongs to the presentation stored beside it. A crossed or partially written
3851
4161
  // envelope confirms nothing, even when one of its fields happens to name the current map.
3852
- matchedBuildId: buildId !== null && matched === buildId ? matched : null
4162
+ matchedBuildId: buildId !== null && matched === buildId ? matched : null,
4163
+ source: typeof value.source === "string" ? value.source : null
3853
4164
  };
3854
4165
  }
3855
4166
  if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
3856
- return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
4167
+ return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
3857
4168
  }
3858
4169
  return null;
3859
4170
  }
@@ -3920,13 +4231,17 @@ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
3920
4231
  ...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
3921
4232
  ...held > unusable ? [reason] : []
3922
4233
  ];
3923
- notify(
3924
- options.onError,
3925
- new Error(`${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`),
3926
- "onError"
3927
- );
4234
+ const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
4235
+ if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
4236
+ else warnHeldOnce(message);
3928
4237
  return { ...bundle, firewall };
3929
4238
  }
4239
+ var warnedHeld = /* @__PURE__ */ new Set();
4240
+ function warnHeldOnce(message) {
4241
+ if (warnedHeld.has(message)) return;
4242
+ warnedHeld.add(message);
4243
+ console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
4244
+ }
3930
4245
  function confirmedBy(res, presented) {
3931
4246
  if (presented === null) return null;
3932
4247
  const verdict = res?.build;
@@ -4101,6 +4416,60 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
4101
4416
  }
4102
4417
  };
4103
4418
  }
4419
+ var RECOVERY_DELAYS_MS = [5e3, 15e3, 45e3, 12e4, 3e5, 6e5];
4420
+ function startRecovery(tick, { onError } = {}) {
4421
+ let stopped = false;
4422
+ let attempt = 0;
4423
+ let timer = null;
4424
+ const schedule = () => {
4425
+ if (stopped) return;
4426
+ const delay = RECOVERY_DELAYS_MS[Math.min(attempt, RECOVERY_DELAYS_MS.length - 1)];
4427
+ timer = setTimeout(run, delay * (1 - Math.random() * JITTER_FRACTION3));
4428
+ timer?.unref?.();
4429
+ };
4430
+ const run = async () => {
4431
+ if (stopped) return;
4432
+ attempt++;
4433
+ try {
4434
+ const status = await tick();
4435
+ if (!status || status.ok !== false) {
4436
+ stopped = true;
4437
+ return;
4438
+ }
4439
+ } catch (err) {
4440
+ notify(onError, err, "onError");
4441
+ }
4442
+ schedule();
4443
+ };
4444
+ schedule();
4445
+ return {
4446
+ stop() {
4447
+ stopped = true;
4448
+ if (timer) clearTimeout(timer);
4449
+ }
4450
+ };
4451
+ }
4452
+ function serialise(tick) {
4453
+ let running = null;
4454
+ let next = null;
4455
+ const run = () => {
4456
+ if (!running) {
4457
+ running = Promise.resolve().then(() => tick()).finally(() => {
4458
+ running = null;
4459
+ });
4460
+ return running;
4461
+ }
4462
+ if (!next) {
4463
+ next = running.catch(() => {
4464
+ }).then(() => {
4465
+ next = null;
4466
+ return run();
4467
+ });
4468
+ }
4469
+ return next;
4470
+ };
4471
+ return run;
4472
+ }
4104
4473
  function makeRefreshHandler(tick, secret) {
4105
4474
  let inflight = null;
4106
4475
  const runOnce = () => {
@@ -5252,7 +5621,7 @@ function createSupabaseGuard({
5252
5621
  statusText: upstream.statusText,
5253
5622
  headers: responseHeaders(upstream.headers)
5254
5623
  });
5255
- return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
5624
+ return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
5256
5625
  };
5257
5626
  }
5258
5627
 
@@ -5346,6 +5715,7 @@ async function createProtection(options = {}) {
5346
5715
  "onDetect"
5347
5716
  );
5348
5717
  };
5718
+ const nodeHttp = await import("http").then((m) => m.default ?? m, () => null);
5349
5719
  const store = makeStore(options);
5350
5720
  const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
5351
5721
  const pulseAuth = await resolvePulseAuth(options);
@@ -5371,6 +5741,21 @@ async function createProtection(options = {}) {
5371
5741
  pulseAuth,
5372
5742
  detectionState: preFetchState
5373
5743
  });
5744
+ let ruleSource = { ...bundle.source ?? { ok: true, origin: "empty" } };
5745
+ const warnedSources = /* @__PURE__ */ new Set();
5746
+ const hasLiveSource = Boolean(options.siteUuid || options.token);
5747
+ const noteRuleSource = (source) => {
5748
+ ruleSource = { ...source ?? { ok: true, origin: ruleSource.origin } };
5749
+ if (!hasLiveSource || typeof onError === "function" || ruleSource.ok !== false) return;
5750
+ const cause = `${ruleSource.origin}:${ruleSource.reason ?? ""}`;
5751
+ if (warnedSources.has(cause)) return;
5752
+ warnedSources.add(cause);
5753
+ const running = ruleSource.origin === "cache" ? "the last rules it received" : ruleSource.origin === "bundled" ? "its bundled fallback rules" : "no rules at all";
5754
+ console.warn(
5755
+ `[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.`
5756
+ );
5757
+ };
5758
+ noteRuleSource(bundle.source);
5374
5759
  let detectionReporting = "not-enrolled";
5375
5760
  let currentOrigin;
5376
5761
  const applyReportingState = async (origin) => {
@@ -5549,7 +5934,9 @@ async function createProtection(options = {}) {
5549
5934
  const spanRedactors = redactors.filter((r) => !r.jsonPath);
5550
5935
  if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
5551
5936
  if (!spanRedactors.length) continue;
5937
+ const beforeSpan = body;
5552
5938
  body = applyRedactors(body, spanRedactors, mask, transform);
5939
+ if (body !== beforeSpan && !preservesJsonStructure(beforeSpan, body)) return { verdict: "block" };
5553
5940
  if (transform) continue;
5554
5941
  for (const name of Object.keys(headers)) {
5555
5942
  const value = headers[name];
@@ -5564,6 +5951,7 @@ async function createProtection(options = {}) {
5564
5951
  }
5565
5952
  }
5566
5953
  }
5954
+ if (body !== text && isJson(text) && !isJson(body)) return { verdict: "block" };
5567
5955
  for (const rule of headerMutations) applyHeaderMutation(headers, rule);
5568
5956
  return { verdict: "redact", body, headers };
5569
5957
  };
@@ -5666,7 +6054,9 @@ async function createProtection(options = {}) {
5666
6054
  const screenResp = async (response, reqCtx) => {
5667
6055
  const read = await readTextResponse(response, screenCap);
5668
6056
  if (read.skip) {
5669
- if (read.skip !== "not-a-response") recordSkip("response", read.skip, { status: response?.status });
6057
+ if (read.skip !== "not-a-response") {
6058
+ recordSkip("response", read.skip, { status: response?.status, ...read.encoding ? { encoding: read.encoding } : {} });
6059
+ }
5670
6060
  if (read.skip === "not-a-response") return response;
5671
6061
  return hardenHeadersOnly(response, reqCtx);
5672
6062
  }
@@ -5747,6 +6137,11 @@ async function createProtection(options = {}) {
5747
6137
  const wrapNodeResponse = (res, reqCtx) => {
5748
6138
  const origWrite = res.write.bind(res);
5749
6139
  const origEnd = res.end.bind(res);
6140
+ const setHeaderNow = typeof res.setHeader === "function" ? res.setHeader.bind(res) : null;
6141
+ const removeHeaderNow = typeof res.removeHeader === "function" ? res.removeHeader.bind(res) : null;
6142
+ const sentAccessor = accessorOf(res, "headersSent");
6143
+ let sentValue = sentAccessor ? void 0 : res.headersSent;
6144
+ const reallySent = () => Boolean(sentAccessor ? sentAccessor.call(res) : sentValue);
5750
6145
  const chunks = [];
5751
6146
  let size = 0;
5752
6147
  let overflow = false;
@@ -5757,7 +6152,7 @@ async function createProtection(options = {}) {
5757
6152
  if (hardened) return null;
5758
6153
  hardened = true;
5759
6154
  try {
5760
- if (res.headersSent || typeof res.setHeader !== "function") return null;
6155
+ if (reallySent() || !setHeaderNow) return null;
5761
6156
  const set = typeof res.getHeaders === "function" ? res.getHeaders() : {};
5762
6157
  const headers = effective && effective.headers ? { ...set, ...effective.headers } : set;
5763
6158
  const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
@@ -5769,8 +6164,8 @@ async function createProtection(options = {}) {
5769
6164
  if (!headerValueChanged(headers[name], value)) continue;
5770
6165
  changed.set(name.toLowerCase(), value);
5771
6166
  try {
5772
- if (value === null || value === void 0) res.removeHeader?.(name);
5773
- else res.setHeader(name, value);
6167
+ if (value === null || value === void 0) removeHeaderNow?.(name);
6168
+ else setHeaderNow(name, value);
5774
6169
  } catch {
5775
6170
  }
5776
6171
  }
@@ -5781,26 +6176,139 @@ async function createProtection(options = {}) {
5781
6176
  }
5782
6177
  };
5783
6178
  const stillToAnswer = (rule) => !hardensWithoutBody(rule);
5784
- if (typeof res.writeHead === "function") {
5785
- const origWriteHead = res.writeHead.bind(res);
6179
+ let pendingHead = null;
6180
+ let sending = false;
6181
+ const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
6182
+ if (origWriteHead) {
5786
6183
  res.writeHead = function(...args) {
5787
- const at = writeHeadHeaderIndex(args);
5788
- const given = at === -1 ? null : args[at];
5789
- const changed = hardenBeforeFlush({
5790
- status: typeof args[0] === "number" ? args[0] : void 0,
5791
- headers: given ? writeHeadHeaderObject(given) : null
5792
- });
5793
- if (!changed || !given) return origWriteHead(...args);
5794
- const rewritten = [...args];
5795
- rewritten[at] = rewriteWriteHeadHeaders(given, changed);
5796
- return origWriteHead(...rewritten);
6184
+ if (sending || reallySent()) return origWriteHead(...args);
6185
+ if (pendingHead) throw headersSentError("write");
6186
+ const judged = judgeWriteHead(args);
6187
+ if (judged) {
6188
+ res.statusCode = judged.statusCode;
6189
+ res.statusMessage = judged.statusMessage;
6190
+ mirrorHeaders(judged.headers);
6191
+ pendingHead = args;
6192
+ } else {
6193
+ pendingHead = args;
6194
+ if (typeof args[0] === "number") res.statusCode = args[0];
6195
+ if (typeof args[1] === "string") res.statusMessage = args[1];
6196
+ }
6197
+ return res;
5797
6198
  };
5798
6199
  }
6200
+ function judgeWriteHead(args) {
6201
+ const ServerResponse = nodeHttp?.ServerResponse;
6202
+ if (typeof ServerResponse !== "function") return null;
6203
+ const detached = new ServerResponse({ method: res.req?.method ?? "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} });
6204
+ if (res.statusMessage !== void 0) detached.statusMessage = res.statusMessage;
6205
+ for (const [name, value] of headerEntries(res)) detached.setHeader(name, value);
6206
+ detached.writeHead(...args);
6207
+ return { statusCode: detached.statusCode, statusMessage: detached.statusMessage, headers: headerEntries(detached) };
6208
+ }
6209
+ function headerEntries(target) {
6210
+ const names = typeof target.getRawHeaderNames === "function" ? target.getRawHeaderNames() : typeof target.getHeaderNames === "function" ? target.getHeaderNames() : [];
6211
+ return names.map((name) => [name, target.getHeader(name)]);
6212
+ }
6213
+ function mirrorHeaders(entries) {
6214
+ if (!setHeaderNow) return;
6215
+ for (const [name, value] of entries) {
6216
+ const current = res.getHeader?.(name);
6217
+ if (current === void 0 || headerValueChanged(current, value)) setHeaderNow(name, value);
6218
+ }
6219
+ }
6220
+ Object.defineProperty(res, "headersSent", {
6221
+ configurable: true,
6222
+ enumerable: true,
6223
+ get: () => pendingHead !== null || reallySent(),
6224
+ // A response-like object that tracks the flag itself keeps doing so.
6225
+ set: (value) => {
6226
+ if (!sentAccessor) sentValue = value;
6227
+ }
6228
+ });
6229
+ for (const [method, verb] of [["setHeader", "set"], ["removeHeader", "remove"], ["appendHeader", "append"]]) {
6230
+ const original = typeof res[method] === "function" ? res[method].bind(res) : null;
6231
+ if (!original) continue;
6232
+ res[method] = function(...args) {
6233
+ if (pendingHead) throw headersSentError(verb);
6234
+ return original(...args);
6235
+ };
6236
+ }
6237
+ if (typeof res.flushHeaders === "function") {
6238
+ const origFlushHeaders = res.flushHeaders.bind(res);
6239
+ res.flushHeaders = function() {
6240
+ sendHead();
6241
+ return origFlushHeaders();
6242
+ };
6243
+ }
6244
+ let hardenDone = false;
6245
+ let hardenedValues = null;
6246
+ const hardenOnce = () => {
6247
+ if (hardenDone) return hardenedValues;
6248
+ hardenDone = true;
6249
+ if (!pendingHead) {
6250
+ hardenedValues = hardenBeforeFlush();
6251
+ return hardenedValues;
6252
+ }
6253
+ const at = writeHeadHeaderIndex(pendingHead);
6254
+ hardenedValues = hardenBeforeFlush({
6255
+ status: typeof pendingHead[0] === "number" ? pendingHead[0] : void 0,
6256
+ headers: at === -1 ? null : writeHeadHeaderObject(pendingHead[at])
6257
+ });
6258
+ return hardenedValues;
6259
+ };
6260
+ const effectiveHead = () => {
6261
+ const set = typeof res.getHeaders === "function" ? { ...res.getHeaders() } : {};
6262
+ if (typeof res.getHeaders !== "function" && typeof res.getHeader === "function") {
6263
+ for (const name of ["content-type", "content-length"]) {
6264
+ const value = res.getHeader(name);
6265
+ if (value !== void 0) set[name] = value;
6266
+ }
6267
+ }
6268
+ if (!pendingHead) return { status: res.statusCode, headers: set };
6269
+ const at = writeHeadHeaderIndex(pendingHead);
6270
+ return {
6271
+ status: typeof pendingHead[0] === "number" ? pendingHead[0] : res.statusCode,
6272
+ headers: at === -1 ? set : { ...set, ...writeHeadHeaderObject(pendingHead[at]) }
6273
+ };
6274
+ };
6275
+ const sendHead = (changes) => {
6276
+ if (sending) return;
6277
+ const hardened2 = hardenOnce();
6278
+ sending = true;
6279
+ if (!pendingHead) {
6280
+ if (changes) applyToResponse(changes);
6281
+ return;
6282
+ }
6283
+ const args = [...pendingHead];
6284
+ pendingHead = null;
6285
+ const merged = new Map(hardened2 ?? []);
6286
+ for (const [name, value] of changes?.headers ?? []) merged.set(name, value);
6287
+ if (changes?.status !== void 0) {
6288
+ args[0] = changes.status;
6289
+ if (typeof args[1] === "string") args.splice(1, 1);
6290
+ }
6291
+ const headerAt = writeHeadHeaderIndex(args);
6292
+ if (headerAt !== -1 && merged.size) args[headerAt] = rewriteWriteHeadHeaders(args[headerAt], merged);
6293
+ applyToResponse({ headers: merged });
6294
+ origWriteHead(...args);
6295
+ };
6296
+ const applyToResponse = (changes) => {
6297
+ if (changes.status !== void 0) res.statusCode = changes.status;
6298
+ for (const [name, value] of changes.headers ?? []) {
6299
+ try {
6300
+ if (value === null || value === void 0) removeHeaderNow?.(name);
6301
+ else setHeaderNow?.(name, value);
6302
+ } catch {
6303
+ }
6304
+ }
6305
+ };
5799
6306
  const collect = (chunk, enc) => {
5800
6307
  if (chunk == null) return;
5801
6308
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
5802
6309
  size += buf.length;
5803
6310
  if (size > MAX) {
6311
+ sendHead();
5804
6312
  for (const c of chunks) origWrite(c);
5805
6313
  chunks.length = 0;
5806
6314
  origWrite(buf);
@@ -5811,7 +6319,6 @@ async function createProtection(options = {}) {
5811
6319
  chunks.push(buf);
5812
6320
  };
5813
6321
  res.write = function(chunk, enc, cb) {
5814
- hardenBeforeFlush();
5815
6322
  if (overflow) return origWrite(chunk, enc, cb);
5816
6323
  collect(chunk, enc);
5817
6324
  if (typeof enc === "function") enc();
@@ -5819,7 +6326,6 @@ async function createProtection(options = {}) {
5819
6326
  return true;
5820
6327
  };
5821
6328
  res.end = function(chunk, enc, cb) {
5822
- hardenBeforeFlush();
5823
6329
  if (typeof chunk === "function") {
5824
6330
  cb = chunk;
5825
6331
  chunk = void 0;
@@ -5834,68 +6340,77 @@ async function createProtection(options = {}) {
5834
6340
  }
5835
6341
  collect(chunk, enc);
5836
6342
  if (overflow) return origEnd(cb);
6343
+ hardenOnce();
6344
+ const passThrough = () => {
6345
+ sendHead();
6346
+ for (const c of chunks) origWrite(c);
6347
+ return origEnd(cb);
6348
+ };
5837
6349
  const buffer = Buffer.concat(chunks);
5838
- let ct = res.getHeader ? res.getHeader("content-type") : void 0;
6350
+ const head = effectiveHead();
6351
+ let ct = head.headers["content-type"];
5839
6352
  if (Array.isArray(ct)) ct = ct[0];
5840
6353
  const kind = screenableContentType(ct);
5841
6354
  if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
5842
6355
  recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
5843
- for (const c of chunks) origWrite(c);
5844
- return origEnd(cb);
6356
+ return passThrough();
6357
+ }
6358
+ const codings = declaredCodings(head.headers["content-encoding"]);
6359
+ if (codings.length > 0 && stillEncoded(buffer)) {
6360
+ recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
6361
+ return passThrough();
5845
6362
  }
5846
6363
  const text = buffer.toString("utf8");
5847
6364
  let r;
5848
6365
  try {
5849
- r = screenText(
5850
- text,
5851
- { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} },
5852
- reqCtx,
5853
- answeredWithoutBody ? stillToAnswer : void 0
5854
- );
6366
+ r = screenText(text, head, reqCtx, answeredWithoutBody ? stillToAnswer : void 0);
5855
6367
  } catch (err) {
5856
6368
  notify(onError, err, "onError");
5857
- for (const c of chunks) origWrite(c);
5858
- return origEnd(cb);
6369
+ return passThrough();
6370
+ }
6371
+ if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
6372
+ if (reallySent() && res.chunkedEncoding !== true) {
6373
+ notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
6374
+ res.destroy?.();
6375
+ return res;
5859
6376
  }
5860
6377
  if (r.verdict === "block") {
5861
- res.statusCode = 500;
5862
- try {
5863
- res.setHeader("content-type", "application/json");
5864
- } catch {
6378
+ const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
6379
+ if (reallySent()) {
6380
+ res.destroy?.();
6381
+ return res;
5865
6382
  }
5866
- return origEnd(JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" }), cb);
6383
+ sendHead({
6384
+ status: 500,
6385
+ headers: /* @__PURE__ */ new Map([
6386
+ ["content-type", "application/json"],
6387
+ ["content-length", String(Buffer.byteLength(body))],
6388
+ ["transfer-encoding", null]
6389
+ ])
6390
+ });
6391
+ return origEnd(body, cb);
5867
6392
  }
5868
- if (r.verdict === "redact") {
5869
- try {
5870
- res.removeHeader && res.removeHeader("content-length");
5871
- } catch {
6393
+ const changed = /* @__PURE__ */ new Map();
6394
+ if (r.headers) {
6395
+ for (const [name, value] of Object.entries(r.headers)) {
6396
+ if (name.toLowerCase() === "content-length") continue;
6397
+ if (!headerValueChanged(head.headers[name], value)) continue;
6398
+ changed.set(name.toLowerCase(), value);
5872
6399
  }
5873
- if (r.headers && res.setHeader) {
5874
- const current = res.getHeaders ? res.getHeaders() : {};
5875
- for (const [name, value] of Object.entries(r.headers)) {
5876
- if (name.toLowerCase() === "content-length") continue;
5877
- if (value === null || value === void 0) {
5878
- try {
5879
- res.removeHeader && res.removeHeader(name);
5880
- } catch {
5881
- }
5882
- } else if (Array.isArray(value)) {
5883
- try {
5884
- res.setHeader(name, value);
5885
- } catch {
5886
- }
5887
- } else if (typeof value === "string" && current[name] !== value) {
5888
- try {
5889
- res.setHeader(name, value);
5890
- } catch {
5891
- }
5892
- }
5893
- }
6400
+ }
6401
+ if (!reallySent()) {
6402
+ const declared = head.headers["content-length"];
6403
+ if (declared === void 0) {
6404
+ changed.set("content-length", null);
6405
+ } else {
6406
+ changed.set("content-length", String(Buffer.byteLength(r.body)));
6407
+ changed.set("transfer-encoding", null);
5894
6408
  }
5895
- return origEnd(r.body, cb);
6409
+ sendHead({ headers: changed });
6410
+ } else if (changed.size) {
6411
+ recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
5896
6412
  }
5897
- for (const c of chunks) origWrite(c);
5898
- return origEnd(cb);
6413
+ return origEnd(r.body, cb);
5899
6414
  };
5900
6415
  };
5901
6416
  const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
@@ -6103,11 +6618,12 @@ async function createProtection(options = {}) {
6103
6618
  let reporter = null;
6104
6619
  if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
6105
6620
  try {
6106
- ({ reportManifest: reporter } = await import("./refresh-manifest-PPV22Q2E.js"));
6621
+ ({ reportManifest: reporter } = await import("./refresh-manifest-DZQOEO3Q.js"));
6107
6622
  } catch (err) {
6108
6623
  notify(onError, err, "onError");
6109
6624
  }
6110
6625
  }
6626
+ let recovery = null;
6111
6627
  const runRefreshTick = async () => {
6112
6628
  if (reporter) {
6113
6629
  try {
@@ -6124,23 +6640,33 @@ async function createProtection(options = {}) {
6124
6640
  });
6125
6641
  mode = resolveMode(options, next);
6126
6642
  applyBundle(next);
6643
+ noteRuleSource(next.source);
6644
+ if (next.source?.ok !== false) recovery?.stop();
6127
6645
  await applyAndAcknowledge(next.source?.origin, declaredState);
6128
6646
  if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
6129
6647
  return next.source ?? { ok: true };
6130
6648
  };
6649
+ const refreshTick = serialise(runRefreshTick);
6131
6650
  if (live) {
6132
- protection.refresh = () => runRefreshTick();
6133
- protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
6651
+ protection.refresh = () => refreshTick();
6652
+ protection.refreshHandler = () => makeRefreshHandler(refreshTick, refreshSecret);
6134
6653
  }
6135
- const loop = options.refreshMs > 0 && live ? startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError }) : null;
6654
+ const loop = options.refreshMs > 0 && live ? startRefresh(refreshTick, { refreshMs: options.refreshMs, onError }) : null;
6655
+ const canAsk = Boolean(options.token || pulseAuth);
6656
+ recovery = live && canAsk && !loop && ruleSource.ok === false ? startRecovery(refreshTick, { onError }) : null;
6136
6657
  protection.stop = () => {
6137
6658
  loop?.stop();
6659
+ recovery?.stop();
6138
6660
  const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
6139
6661
  (wait) => wait && typeof wait.then === "function"
6140
6662
  );
6141
6663
  return Promise.all(outstanding).then(() => void 0);
6142
6664
  };
6143
6665
  protection.stopRefresh = protection.stop;
6666
+ Object.defineProperty(protection, "ruleSource", {
6667
+ get: () => ({ ...ruleSource }),
6668
+ enumerable: true
6669
+ });
6144
6670
  Object.defineProperty(protection, "detectionReporting", {
6145
6671
  get: () => detectionReporting,
6146
6672
  enumerable: true
@@ -6151,6 +6677,16 @@ async function createProtection(options = {}) {
6151
6677
  });
6152
6678
  return protection;
6153
6679
  }
6680
+ function accessorOf(obj, name) {
6681
+ for (let o = obj; o; o = Object.getPrototypeOf(o)) {
6682
+ const d = Object.getOwnPropertyDescriptor(o, name);
6683
+ if (d) return typeof d.get === "function" ? d.get : null;
6684
+ }
6685
+ return null;
6686
+ }
6687
+ function headersSentError(verb) {
6688
+ return Object.assign(new Error(`Cannot ${verb} headers after they are sent to the client`), { code: "ERR_HTTP_HEADERS_SENT" });
6689
+ }
6154
6690
  function resolveMode(options, bundle) {
6155
6691
  const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
6156
6692
  if (env === "block" || env === "dry-run") return env;
@@ -6223,11 +6759,26 @@ function looksBinary(bytes) {
6223
6759
  }
6224
6760
  return n > 0 && ctrl / n > 0.1;
6225
6761
  }
6762
+ function declaredCodings(value) {
6763
+ const raw = Array.isArray(value) ? value.join(",") : String(value ?? "");
6764
+ return raw.toLowerCase().split(",").map((c) => c.trim()).filter((c) => c !== "" && c !== "identity");
6765
+ }
6766
+ function stillEncoded(bytes) {
6767
+ if (looksBinary(bytes)) return true;
6768
+ try {
6769
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
6770
+ return false;
6771
+ } catch {
6772
+ return true;
6773
+ }
6774
+ }
6226
6775
  async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
6227
6776
  if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
6228
6777
  const ct = response.headers?.get?.("content-type") || "";
6229
6778
  const kind = screenableContentType(ct);
6230
6779
  if (kind === "skip") return { skip: baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" };
6780
+ const codings = declaredCodings(response.headers?.get?.("content-encoding"));
6781
+ const encodedSkip = () => ({ skip: "encoded-body", encoding: codings.join(", ") });
6231
6782
  const sniff = kind === "sniff";
6232
6783
  const len = Number(response.headers?.get?.("content-length") || 0);
6233
6784
  if (len && len > cap) return { skip: "body-cap" };
@@ -6271,13 +6822,21 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
6271
6822
  } finally {
6272
6823
  reader.releaseLock();
6273
6824
  }
6825
+ const bytes = concatBytes(chunks, size);
6826
+ if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
6274
6827
  try {
6275
- return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
6828
+ return { text: new TextDecoder().decode(bytes) };
6276
6829
  } catch {
6277
6830
  return { skip: "decode-failed" };
6278
6831
  }
6279
6832
  }
6280
6833
  try {
6834
+ if (codings.length > 0) {
6835
+ const bytes = new Uint8Array(await clone.arrayBuffer());
6836
+ if (bytes.byteLength > cap) return { skip: "body-cap" };
6837
+ if (stillEncoded(bytes)) return encodedSkip();
6838
+ return { text: new TextDecoder().decode(bytes) };
6839
+ }
6281
6840
  const text = await clone.text();
6282
6841
  if (text.length > cap) return { skip: "body-cap" };
6283
6842
  if (sniff && looksBinary(new TextEncoder().encode(text.slice(0, 512)))) return { skip: "binary-body" };
@@ -6351,6 +6910,39 @@ function extractRedactors(rule) {
6351
6910
  function htmlEscape(str) {
6352
6911
  return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6353
6912
  }
6913
+ function isJson(text) {
6914
+ try {
6915
+ JSON.parse(text);
6916
+ return true;
6917
+ } catch {
6918
+ return false;
6919
+ }
6920
+ }
6921
+ function preservesJsonStructure(before, after) {
6922
+ if (!isJson(before)) return true;
6923
+ if (!isJson(after)) return false;
6924
+ const expected = jsonStructure(before);
6925
+ const actual = jsonStructure(after);
6926
+ for (; ; ) {
6927
+ const left = expected.next();
6928
+ const right = actual.next();
6929
+ if (left.done || right.done) return left.done === right.done;
6930
+ if (left.value !== right.value) return false;
6931
+ }
6932
+ }
6933
+ function* jsonStructure(text) {
6934
+ const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|[^\s]/g;
6935
+ for (const match of text.matchAll(tokens)) {
6936
+ const token = match[0];
6937
+ if (token[0] !== '"') {
6938
+ yield "token:" + token;
6939
+ continue;
6940
+ }
6941
+ let next = match.index + token.length;
6942
+ while (text[next] === " " || text[next] === " " || text[next] === "\r" || text[next] === "\n") next++;
6943
+ yield text[next] === ":" ? "key:" + JSON.parse(token) : "string";
6944
+ }
6945
+ }
6354
6946
  function applyRedactors(body, redactors, mask, transform) {
6355
6947
  let out = body;
6356
6948
  for (const r of redactors) {
@@ -6393,10 +6985,12 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
6393
6985
  if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
6394
6986
  const head = text.trimStart()[0];
6395
6987
  if (head !== "{" && head !== "[") return text;
6396
- const preserved = preserveBigInts(text);
6988
+ let preserved;
6397
6989
  let obj;
6398
6990
  try {
6399
- obj = JSON.parse(preserved);
6991
+ JSON.parse(text);
6992
+ preserved = preserveBigInts(text, mask);
6993
+ obj = JSON.parse(preserved.text);
6400
6994
  } catch {
6401
6995
  return text;
6402
6996
  }
@@ -6405,65 +6999,44 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
6405
6999
  const pred = conditionPredicate(r.condition);
6406
7000
  walkLeaves(obj, r.jsonPath, (loc) => {
6407
7001
  try {
6408
- if (pred(loc.value)) {
6409
- loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
7002
+ const number = preserved.numbers.get(loc.value);
7003
+ if (pred(number === void 0 ? loc.value : Number(number))) {
7004
+ setOwn(loc.parent, loc.key, transform ? transform(number ?? String(loc.value)) : mask);
6410
7005
  changed = true;
6411
7006
  }
6412
7007
  } catch {
6413
7008
  }
6414
7009
  });
6415
7010
  }
6416
- return changed ? restoreBigInts(JSON.stringify(obj)) : text;
6417
- }
6418
- var BIGINT_OPEN = "__PSBIGINT_9c2f__";
6419
- var BIGINT_CLOSE = "__DNEGIB__";
6420
- function preserveBigInts(text) {
6421
- let out = "";
6422
- let inStr = false;
6423
- for (let i = 0; i < text.length; ) {
6424
- const ch = text[i];
6425
- if (inStr) {
6426
- out += ch;
6427
- if (ch === "\\") {
6428
- out += text[i + 1] ?? "";
6429
- i += 2;
6430
- continue;
6431
- }
6432
- if (ch === '"') inStr = false;
6433
- i++;
6434
- continue;
6435
- }
6436
- if (ch === '"') {
6437
- inStr = true;
6438
- out += ch;
6439
- i++;
6440
- continue;
6441
- }
6442
- if (ch === "-" || ch >= "0" && ch <= "9") {
6443
- let j = ch === "-" ? i + 1 : i;
6444
- let digits = 0;
6445
- while (j < text.length && text[j] >= "0" && text[j] <= "9") {
6446
- digits++;
6447
- j++;
6448
- }
6449
- const next = text[j];
6450
- const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
6451
- if (isIntToken && digits >= 16) {
6452
- out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
6453
- } else {
6454
- out += text.slice(i, j || i + 1);
6455
- }
6456
- i = j > i ? j : i + 1;
6457
- continue;
6458
- }
6459
- out += ch;
6460
- i++;
6461
- }
6462
- return out;
7011
+ return changed ? restoreBigInts(JSON.stringify(obj), preserved.numbers) : text;
6463
7012
  }
6464
- function restoreBigInts(text) {
6465
- if (!text.includes(BIGINT_OPEN)) return text;
6466
- return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
7013
+ function preserveBigInts(text, mask) {
7014
+ const occupied = /* @__PURE__ */ new Set([mask]);
7015
+ const integers = [];
7016
+ const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
7017
+ for (const match of text.matchAll(tokens)) {
7018
+ const token = match[0];
7019
+ if (token[0] === '"') occupied.add(JSON.parse(token));
7020
+ else if (/^-?\d{16,}$/.test(token)) integers.push({ start: match.index, token });
7021
+ }
7022
+ const numbers = /* @__PURE__ */ new Map();
7023
+ const chunks = [];
7024
+ let offset = 0;
7025
+ let index = 0;
7026
+ for (const { start, token } of integers) {
7027
+ let marker;
7028
+ do {
7029
+ marker = "__PSNUMBER_" + index++ + "__";
7030
+ } while (occupied.has(marker));
7031
+ numbers.set(marker, token);
7032
+ chunks.push(text.slice(offset, start), JSON.stringify(marker));
7033
+ offset = start + token.length;
7034
+ }
7035
+ chunks.push(text.slice(offset));
7036
+ return { text: chunks.join(""), numbers };
7037
+ }
7038
+ function restoreBigInts(text, numbers) {
7039
+ return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
6467
7040
  }
6468
7041
  function isHeaderMutation(action) {
6469
7042
  return action === "set-header" || action === "remove-header" || action === "harden-cookie";