@patchstack/connect 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/protect.cjs CHANGED
@@ -31,38 +31,48 @@ module.exports = __toCommonJS(runtime_exports);
31
31
  // src/protect/engine/client.js
32
32
  var DEFAULT_BASE_URL = "https://api.patchstack.com";
33
33
  var DEFAULT_CACHE_TTL = 3e5;
34
+ var JITTER_FRACTION = 0.1;
34
35
  var PatchstackRuleClient = class {
35
36
  #token;
36
37
  #baseUrl;
37
38
  #cacheTtl;
38
39
  #cache = null;
39
40
  #cacheTime = null;
40
- constructor({ token, baseUrl, cacheTtl } = {}) {
41
+ #ttlEffective = 0;
42
+ #etag;
43
+ constructor({ token, baseUrl, cacheTtl, etag } = {}) {
41
44
  this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
42
45
  this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
43
46
  this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL;
47
+ this.#etag = etag ?? null;
44
48
  if (!this.#token) {
45
49
  throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
46
50
  }
47
51
  }
48
52
  async getRules() {
49
53
  const now = Date.now();
50
- if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#cacheTtl) {
54
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
51
55
  return this.#cache;
52
56
  }
53
57
  const url = `${this.#baseUrl}/api/get-rules/3`;
54
58
  try {
59
+ const headers = {
60
+ "Content-Type": "application/json",
61
+ "Authorization": `Bearer ${this.#token}`,
62
+ "LicenseID": "1"
63
+ // Hard-coded, is never actually used but needed by the API
64
+ };
65
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
55
66
  const response = await fetch(url, {
56
67
  method: "POST",
57
- headers: {
58
- "Content-Type": "application/json",
59
- "Authorization": `Bearer ${this.#token}`,
60
- "LicenseID": "1"
61
- // Hard-coded, is never actually used but needed by the API
62
- },
68
+ headers,
63
69
  body: JSON.stringify({}),
64
70
  signal: AbortSignal.timeout(3e4)
65
71
  });
72
+ if (response.status === 304) {
73
+ this.#touch(now);
74
+ return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
75
+ }
66
76
  if (!response.ok) {
67
77
  return {
68
78
  success: false,
@@ -75,12 +85,15 @@ var PatchstackRuleClient = class {
75
85
  const data = await response.json();
76
86
  const result = {
77
87
  success: true,
88
+ notModified: false,
89
+ etag: response.headers?.get?.("etag") ?? null,
78
90
  firewall: Array.isArray(data.firewall) ? data.firewall : [],
79
91
  whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
80
92
  whitelist_keys: data.whitelist_keys ?? {}
81
93
  };
82
94
  this.#cache = result;
83
- this.#cacheTime = now;
95
+ this.#etag = result.etag;
96
+ this.#touch(now);
84
97
  return result;
85
98
  } catch (err) {
86
99
  return {
@@ -92,9 +105,14 @@ var PatchstackRuleClient = class {
92
105
  };
93
106
  }
94
107
  }
108
+ #touch(now) {
109
+ this.#cacheTime = now;
110
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION);
111
+ }
95
112
  clearCache() {
96
113
  this.#cache = null;
97
114
  this.#cacheTime = null;
115
+ this.#etag = null;
98
116
  }
99
117
  };
100
118
 
@@ -932,44 +950,53 @@ var RuleEngine = class {
932
950
  // src/protect/engine/pulse-client.js
933
951
  var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
934
952
  var DEFAULT_CACHE_TTL2 = 3e5;
953
+ var JITTER_FRACTION2 = 0.1;
935
954
  var PulseRuleClient = class {
936
955
  #siteUuid;
937
956
  #baseUrl;
938
957
  #cacheTtl;
939
958
  #cache = null;
940
959
  #cacheTime = null;
941
- constructor({ siteUuid, baseUrl, cacheTtl } = {}) {
960
+ #ttlEffective = 0;
961
+ #etag;
962
+ constructor({ siteUuid, baseUrl, cacheTtl, etag } = {}) {
942
963
  this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
943
964
  this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL ?? DEFAULT_BASE_URL2;
944
965
  this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL2;
966
+ this.#etag = etag ?? null;
945
967
  if (!this.#siteUuid) {
946
968
  throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
947
969
  }
948
970
  }
949
971
  async getRules() {
950
972
  const now = Date.now();
951
- if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#cacheTtl) {
973
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
952
974
  return this.#cache;
953
975
  }
954
976
  const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
955
977
  try {
956
- const response = await fetch(url, {
957
- method: "GET",
958
- headers: { Accept: "application/json" },
959
- signal: AbortSignal.timeout(3e4)
960
- });
978
+ const headers = { Accept: "application/json" };
979
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
980
+ const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(3e4) });
981
+ if (response.status === 304) {
982
+ this.#touch(now);
983
+ return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
984
+ }
961
985
  if (!response.ok) {
962
986
  return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
963
987
  }
964
988
  const data = await response.json();
965
989
  const result = {
966
990
  success: true,
991
+ notModified: false,
992
+ etag: response.headers?.get?.("etag") ?? null,
967
993
  firewall: Array.isArray(data.firewall) ? data.firewall : [],
968
994
  whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
969
995
  whitelist_keys: data.whitelist_keys ?? {}
970
996
  };
971
997
  this.#cache = result;
972
- this.#cacheTime = now;
998
+ this.#etag = result.etag;
999
+ this.#touch(now);
973
1000
  return result;
974
1001
  } catch (err) {
975
1002
  return {
@@ -981,14 +1008,21 @@ var PulseRuleClient = class {
981
1008
  };
982
1009
  }
983
1010
  }
1011
+ #touch(now) {
1012
+ this.#cacheTime = now;
1013
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
1014
+ }
984
1015
  clearCache() {
985
1016
  this.#cache = null;
986
1017
  this.#cacheTime = null;
1018
+ this.#etag = null;
987
1019
  }
988
1020
  };
989
1021
 
990
1022
  // src/protect/engine/fetch.js
991
- async function fromFetchRequest(request) {
1023
+ var MAX_BODY_BYTES = 1024 * 1024;
1024
+ async function fromFetchRequest(request, options = {}) {
1025
+ const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
992
1026
  const url = new URL(request.url);
993
1027
  const method = (request.method || "GET").toUpperCase();
994
1028
  const headers = {};
@@ -1005,14 +1039,11 @@ async function fromFetchRequest(request) {
1005
1039
  }
1006
1040
  let rawBody = "";
1007
1041
  if (method !== "GET" && method !== "HEAD") {
1008
- try {
1009
- rawBody = await request.clone().text();
1010
- } catch {
1011
- rawBody = "";
1012
- }
1042
+ rawBody = await readCappedText(request, maxBodyBytes);
1013
1043
  }
1014
1044
  const contentType = headers["content-type"] || "";
1015
1045
  let body = {};
1046
+ let files;
1016
1047
  if (rawBody) {
1017
1048
  if (contentType.includes("application/json")) {
1018
1049
  try {
@@ -1025,6 +1056,13 @@ async function fromFetchRequest(request) {
1025
1056
  for (const [k, v] of new URLSearchParams(rawBody)) {
1026
1057
  body[k] = k in body ? [].concat(body[k], v) : v;
1027
1058
  }
1059
+ } else if (contentType.includes("multipart/form-data")) {
1060
+ const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
1061
+ if (boundary) {
1062
+ const parsed = parseMultipart(rawBody, boundary);
1063
+ body = parsed.body;
1064
+ files = parsed.files;
1065
+ }
1028
1066
  }
1029
1067
  }
1030
1068
  const uri = url.pathname + url.search;
@@ -1035,6 +1073,7 @@ async function fromFetchRequest(request) {
1035
1073
  originalUrl: uri,
1036
1074
  query,
1037
1075
  body,
1076
+ files,
1038
1077
  headers,
1039
1078
  ip: forwarded.split(",")[0].trim(),
1040
1079
  cookies: parseCookies(headers.cookie),
@@ -1043,6 +1082,44 @@ async function fromFetchRequest(request) {
1043
1082
  _rawBody: rawBody
1044
1083
  };
1045
1084
  }
1085
+ async function readCappedText(request, max) {
1086
+ const ceiling = Math.max(max, max * 4);
1087
+ const declared = Number(request.headers?.get?.("content-length") || 0);
1088
+ if (declared && declared > ceiling) return "";
1089
+ let clone;
1090
+ try {
1091
+ clone = request.clone();
1092
+ } catch {
1093
+ return "";
1094
+ }
1095
+ try {
1096
+ const text = await clone.text();
1097
+ return text.length > max ? text.slice(0, max) : text;
1098
+ } catch {
1099
+ return "";
1100
+ }
1101
+ }
1102
+ function parseMultipart(rawBody, boundary) {
1103
+ const body = {};
1104
+ const files = {};
1105
+ for (const part of rawBody.split("--" + boundary)) {
1106
+ const sep = /\r?\n\r?\n/.exec(part);
1107
+ if (!sep) continue;
1108
+ const rawHeaders = part.slice(0, sep.index);
1109
+ const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
1110
+ if (!disposition) continue;
1111
+ const name = /name="([^"]*)"/i.exec(disposition)?.[1];
1112
+ if (name == null) continue;
1113
+ const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
1114
+ const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
1115
+ if (filename !== void 0) {
1116
+ files[name] = name in files ? [].concat(files[name], filename) : filename;
1117
+ } else {
1118
+ body[name] = name in body ? [].concat(body[name], content) : content;
1119
+ }
1120
+ }
1121
+ return { body, files };
1122
+ }
1046
1123
  function parseCookies(header) {
1047
1124
  const cookies = {};
1048
1125
  if (!header) {
@@ -1122,35 +1199,124 @@ function parseCookies2(header) {
1122
1199
  }
1123
1200
 
1124
1201
  // src/protect/egress.js
1125
- function installEgressGuard({ shouldBlock, onBlock } = {}) {
1126
- const original = globalThis.fetch;
1127
- if (typeof original !== "function" || original.__patchstackGuarded || typeof shouldBlock !== "function") {
1128
- return () => {
1202
+ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
1203
+ const restores = [];
1204
+ if (typeof shouldBlock !== "function") return () => {
1205
+ };
1206
+ const block = (url, host, method) => {
1207
+ if (!shouldBlock(url, host, method)) return false;
1208
+ onBlock?.({ url, host, method });
1209
+ return true;
1210
+ };
1211
+ const originalFetch = globalThis.fetch;
1212
+ if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
1213
+ const guarded = async (input, init) => {
1214
+ const url = typeof input === "string" ? input : input && input.url || String(input);
1215
+ let host = null;
1216
+ try {
1217
+ host = new URL(url).hostname;
1218
+ } catch {
1219
+ host = null;
1220
+ }
1221
+ const method = init && init.method || input && input.method || "GET";
1222
+ if (block(url, host, method)) {
1223
+ throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
1224
+ }
1225
+ return originalFetch(input, init);
1129
1226
  };
1227
+ guarded.__patchstackGuarded = true;
1228
+ globalThis.fetch = guarded;
1229
+ restores.push(() => {
1230
+ if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
1231
+ });
1130
1232
  }
1131
- const guarded = async (input, init) => {
1132
- const url = typeof input === "string" ? input : input && input.url || String(input);
1133
- let host = null;
1233
+ for (const moduleName of ["node:http", "node:https"]) {
1134
1234
  try {
1135
- host = new URL(url).hostname;
1235
+ const mod = await import(moduleName);
1236
+ const restore = patchHttpModule(mod.default ?? mod, block);
1237
+ if (restore) restores.push(restore);
1136
1238
  } catch {
1137
- host = null;
1138
1239
  }
1139
- const method = init && init.method || input && input.method || "GET";
1140
- if (shouldBlock(url, host, method)) {
1141
- onBlock?.({ url, host, method });
1142
- throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
1240
+ }
1241
+ const OriginalWS = globalThis.WebSocket;
1242
+ if (typeof OriginalWS === "function" && !OriginalWS.__patchstackGuarded) {
1243
+ const GuardedWS = new Proxy(OriginalWS, {
1244
+ construct(target, args, newTarget) {
1245
+ const url = String(args?.[0] ?? "");
1246
+ let host = null;
1247
+ try {
1248
+ host = new URL(url).hostname;
1249
+ } catch {
1250
+ host = null;
1251
+ }
1252
+ if (block(url, host, "WEBSOCKET")) {
1253
+ throw new Error(`Patchstack blocked an outbound WebSocket to a disallowed address: ${host ?? url}`);
1254
+ }
1255
+ return Reflect.construct(target, args, newTarget);
1256
+ }
1257
+ });
1258
+ OriginalWS.__patchstackGuarded = true;
1259
+ globalThis.WebSocket = GuardedWS;
1260
+ restores.push(() => {
1261
+ if (globalThis.WebSocket === GuardedWS) globalThis.WebSocket = OriginalWS;
1262
+ delete OriginalWS.__patchstackGuarded;
1263
+ });
1264
+ }
1265
+ return () => {
1266
+ for (const restore of restores) {
1267
+ try {
1268
+ restore();
1269
+ } catch {
1270
+ }
1143
1271
  }
1144
- return original(input, init);
1145
1272
  };
1146
- guarded.__patchstackGuarded = true;
1147
- globalThis.fetch = guarded;
1273
+ }
1274
+ function patchHttpModule(http, block) {
1275
+ if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
1276
+ const originalRequest = http.request;
1277
+ const originalGet = http.get;
1278
+ const wrap = (original) => function(...args) {
1279
+ const target = extractHttpTarget(args);
1280
+ if (target && block(target.url, target.host, target.method)) {
1281
+ throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
1282
+ }
1283
+ return original.apply(this, args);
1284
+ };
1285
+ http.request = wrap(originalRequest);
1286
+ if (typeof originalGet === "function") http.get = wrap(originalGet);
1287
+ http.__patchstackGuarded = true;
1148
1288
  return () => {
1149
- if (globalThis.fetch === guarded) {
1150
- globalThis.fetch = original;
1151
- }
1289
+ http.request = originalRequest;
1290
+ if (typeof originalGet === "function") http.get = originalGet;
1291
+ delete http.__patchstackGuarded;
1152
1292
  };
1153
1293
  }
1294
+ function extractHttpTarget(args) {
1295
+ const first = args[0];
1296
+ try {
1297
+ if (typeof first === "string" || first instanceof URL) {
1298
+ const url = new URL(String(first));
1299
+ const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
1300
+ return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
1301
+ }
1302
+ if (first && typeof first === "object") {
1303
+ const host = normalizeHost(first.hostname || first.host);
1304
+ const protocol = first.protocol || "http:";
1305
+ const port = first.port ? `:${first.port}` : "";
1306
+ const path = first.path || "/";
1307
+ return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
1308
+ }
1309
+ } catch {
1310
+ }
1311
+ return null;
1312
+ }
1313
+ function normalizeHost(raw) {
1314
+ const host = String(raw || "").trim();
1315
+ const bracketed = /^\[([^\]]+)\]/.exec(host);
1316
+ if (bracketed) return bracketed[1];
1317
+ if ((host.match(/:/g) || []).length > 1) return host;
1318
+ return host.split(":")[0];
1319
+ }
1154
1320
 
1155
1321
  // src/protect/defaults.js
1156
1322
  var DEFAULT_RESPONSE_RULES = [
@@ -1201,6 +1367,25 @@ var DEFAULT_RESPONSE_RULES = [
1201
1367
  category: "info-exposure",
1202
1368
  action: "redact",
1203
1369
  rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\n\\s+at\\s+.+\\(.+:\\d+:\\d+\\)/" } }]
1370
+ },
1371
+ {
1372
+ id: "resp-sql-error",
1373
+ title: "SQL / ORM error disclosure in response body",
1374
+ phase: "response",
1375
+ category: "info-exposure",
1376
+ action: "redact",
1377
+ rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(SQLSTATE\\[[0-9A-Z]+\\]|SequelizeDatabaseError|ER_[A-Z_]+|ORA-\\d{5}|PG::[A-Za-z]+Error|SQLITE_ERROR|You have an error in your SQL syntax)/i" } }]
1378
+ },
1379
+ {
1380
+ id: "resp-exception-trace",
1381
+ title: "Backend exception / stack trace disclosure in response body",
1382
+ phase: "response",
1383
+ category: "info-exposure",
1384
+ action: "redact",
1385
+ // Multi-language exception/traceback signatures a normal API response never carries:
1386
+ // Python traceback, Java "Exception in thread", .NET System.*Exception, JVM stack frames,
1387
+ // Go goroutine dumps. (Node `at fn (file:line:col)` frames are handled by resp-stack-trace.)
1388
+ rule_v2: [{ parameter: "response.body", match: { type: "regex", value: '/(Traceback \\(most recent call last\\)|Exception in thread "|System\\.[A-Za-z.]+Exception|\\bat [\\w.$]+\\([\\w]+\\.(?:java|kt|scala|rb|py|cs):\\d+\\)|goroutine \\d+ \\[)/' } }]
1204
1389
  }
1205
1390
  ];
1206
1391
  var DEFAULT_EGRESS_RULES = [
@@ -1213,6 +1398,89 @@ var DEFAULT_EGRESS_RULES = [
1213
1398
  }
1214
1399
  ];
1215
1400
 
1401
+ // src/protect/block-page.js
1402
+ function escapeHtml(s) {
1403
+ return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1404
+ }
1405
+ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
1406
+ const safeUrl = escapeHtml(url);
1407
+ const safeCode = escapeHtml(code || "patchstack");
1408
+ return `<!DOCTYPE html>
1409
+ <html lang="en">
1410
+ <head>
1411
+ <meta charset="UTF-8">
1412
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
1413
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1414
+ <style type="text/css">
1415
+ .access-denied,.site-url-text{letter-spacing:.01em;color:#fff}body{background:#111216}.main-container{display:flex;justify-content:center;flex-direction:column;align-items:center}.container{width:688px;height:456px;background:#1c1e22;border-radius:16px;margin-top:120px}.content{height:330px;margin:64px}.access-denied{font-family:Verdana;font-style:normal;font-weight:600;font-size:24px;line-height:32px;display:flex;align-items:center}.descp-text,.site-url{display:flex;align-items:center}.site-url{margin-top:32px;width:560px;height:64px;background:#26282c;border-radius:4px}.reason-code-text,.site-url-text{font-family:Verdana;font-style:normal;border:none;background:#26282c}.site-url-text{text-overflow: ellipsis;font-weight:400;font-size:16px;line-height:24px;margin-left:16px;width:85%}.copy-icon{width:19px;height:22px;margin-left:20px}.copy-icon:hover{cursor:pointer}.reason-code-text{font-weight:600;font-size:24px;line-height:32px;letter-spacing:.01em;color:#AFE614;margin-left:16px;width:85%}.descp-text,.return-text{font-size:16px;line-height:24px;letter-spacing:.01em;font-weight:400}.return-text:hover{color: #AFE614}.descp-text,.logoText,.return-text,.secured-text{font-family:Verdana;font-style:normal;color:#fff}.descp{margin-top:32px;width:560px}.bottom-second-containers,.bottom-text-containers{align-items:center;display:flex}.bottom-container{margin-top:40px;display:flex;width:688px}.bottom-second-containers{margin-left:auto}.arrow-vector{margin-left:20px}.return-text{text-align:center;margin-left:12.59px;text-decoration:none}.secured-text{font-weight:400;font-size:11.2px;line-height:16px;display:flex;align-items:center;letter-spacing:.02em;margin-right:8px}.logoText{font-weight:600;font-size:16px;line-height:24px;letter-spacing:.01em;flex:none;order:1;flex-grow:0}.patch-logo{width:120px}.mobile-bottom-container{margin-top:40px;width:372px;display:none}.mobile-bottom-second-containers{margin-top:64px;display:flex;margin-left:16.5px}@media only screen and (max-width:900px){.container{background:#111216;width:370px;height:auto;margin-top:0}.content{margin-left:16.5px}.descp,.site-url{width:340px}.site-url-text{font-weight:400;font-size:14.4px;line-height:20px;letter-spacing:.02em;width:80%}.copy-icon{margin-left:20px}.reason-code-text{font-size:16px;line-height:32px;letter-spacing:.02em;margin-left:16px;width:80%}.descp-text,.return-text{font-size:14.4px;line-height:20px}.descp-text{display:flex;align-items:center;letter-spacing:.02em}.bottom-container{display:none}.mobile-bottom-container{display:block}.return-text{text-align:center;letter-spacing:.01em;color:#fff;margin-left:9.73px}}
1416
+ </style>
1417
+ <title>Access Denied</title>
1418
+ </head>
1419
+ <body>
1420
+ <div class="main-container">
1421
+ <div class="container">
1422
+ <div class="content">
1423
+ <span class="access-denied">Access Denied</span>
1424
+ <div class="site-url">
1425
+ <input id="url" class="site-url-text" type="text"
1426
+ value="${safeUrl}" readonly>
1427
+ <img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='url')">
1428
+ </div>
1429
+ <div class="site-url">
1430
+ <input id="response" class="reason-code-text" type="text" value="${safeCode}" readonly>
1431
+ <img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='response')">
1432
+ </div>
1433
+ <div class="descp">
1434
+ <span class="descp-text">This request has been blocked by Patchstack. If this message persists and you are a legitimate user, contact the site administrator with the reference above.</span>
1435
+ </div>
1436
+ </div>
1437
+ </div>
1438
+ <div class="bottom-container">
1439
+ <div class="bottom-text-containers">
1440
+ <img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
1441
+ <a href="/" class="return-text">Return to homepage</a>
1442
+ </div>
1443
+ <div class="bottom-second-containers">
1444
+ <span class="secured-text">This website is secured by</span>
1445
+ <a href="https://patchstack.com" class="logoText">
1446
+ <img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
1447
+ </a>
1448
+ </div>
1449
+ </div>
1450
+
1451
+ <div class="mobile-bottom-container">
1452
+ <div class="mobile-bottom-text-containers">
1453
+ <img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
1454
+ <a href="/" class="return-text">Return to homepage</a>
1455
+ </div>
1456
+ <div class="mobile-bottom-second-containers">
1457
+ <span class="secured-text">This website is secured by</span>
1458
+ <a href="https://patchstack.com" class="logoText">
1459
+ <img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
1460
+ </a>
1461
+ </div>
1462
+ </div>
1463
+ </div>
1464
+ </body>
1465
+ <script>
1466
+ function copyText(id) {
1467
+ var copyText = document.getElementById(id)
1468
+ copyText.select();
1469
+ copyText.setSelectionRange(0, 99999);
1470
+ navigator.clipboard.writeText(copyText.value);
1471
+ }
1472
+
1473
+ function hover(element) {
1474
+ element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
1475
+ }
1476
+
1477
+ function unhover(element) {
1478
+ element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
1479
+ }
1480
+ </script>
1481
+ </html>`;
1482
+ }
1483
+
1216
1484
  // src/protect/runtime.js
1217
1485
  var import_node_fs = require("fs");
1218
1486
  var import_node_path = require("path");
@@ -1338,15 +1606,27 @@ async function createProtection(options = {}) {
1338
1606
  if (mode !== "block" || !blockRule && !redactions.length) return { verdict: "pass" };
1339
1607
  if (blockRule) return { verdict: "block" };
1340
1608
  let body = text;
1341
- for (const { rule, redactors } of redactions) body = applyRedactors(body, redactors, maskFn(rule.category));
1342
- return { verdict: "redact", body };
1609
+ const headers = { ...meta.headers || {} };
1610
+ for (const { rule, redactors } of redactions) {
1611
+ const mask = maskFn(rule.category);
1612
+ body = applyRedactors(body, redactors, mask);
1613
+ for (const name of Object.keys(headers)) {
1614
+ const value = headers[name];
1615
+ if (typeof value === "string") {
1616
+ headers[name] = applyRedactors(value, redactors, mask);
1617
+ } else if (Array.isArray(value)) {
1618
+ headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, redactors, mask) : item);
1619
+ }
1620
+ }
1621
+ }
1622
+ return { verdict: "redact", body, headers };
1343
1623
  };
1344
1624
  const screenResp = async (response) => {
1345
1625
  const text = await readTextResponse(response);
1346
1626
  if (text == null) return response;
1347
1627
  const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
1348
1628
  if (r.verdict === "block") return leakResponse();
1349
- if (r.verdict === "redact") return rebuildResponse(response, r.body);
1629
+ if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
1350
1630
  return response;
1351
1631
  };
1352
1632
  const wrapNodeResponse = (res) => {
@@ -1396,7 +1676,7 @@ async function createProtection(options = {}) {
1396
1676
  }
1397
1677
  let r;
1398
1678
  try {
1399
- r = screenText(text, { status: res.statusCode, headers: {} });
1679
+ r = screenText(text, { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} });
1400
1680
  } catch (err) {
1401
1681
  onError?.(err);
1402
1682
  for (const c of chunks) origWrite(c);
@@ -1415,6 +1695,22 @@ async function createProtection(options = {}) {
1415
1695
  res.removeHeader && res.removeHeader("content-length");
1416
1696
  } catch {
1417
1697
  }
1698
+ if (r.headers && res.setHeader) {
1699
+ const current = res.getHeaders ? res.getHeaders() : {};
1700
+ for (const [name, value] of Object.entries(r.headers)) {
1701
+ if (Array.isArray(value)) {
1702
+ try {
1703
+ res.setHeader(name, value);
1704
+ } catch {
1705
+ }
1706
+ } else if (typeof value === "string" && current[name] !== value) {
1707
+ try {
1708
+ res.setHeader(name, value);
1709
+ } catch {
1710
+ }
1711
+ }
1712
+ }
1713
+ }
1418
1714
  return origEnd(r.body, cb);
1419
1715
  }
1420
1716
  for (const c of chunks) origWrite(c);
@@ -1451,7 +1747,7 @@ async function createProtection(options = {}) {
1451
1747
  onError?.(err);
1452
1748
  return null;
1453
1749
  }
1454
- return decide("request", result, () => blockResponse(result), () => null);
1750
+ return decide("request", result, () => blockResponse(result, request), () => null);
1455
1751
  };
1456
1752
  },
1457
1753
  // Wrap a fetch handler: screens the request, then the response (redact/block).
@@ -1479,7 +1775,13 @@ async function createProtection(options = {}) {
1479
1775
  decide(
1480
1776
  "request",
1481
1777
  result,
1482
- () => res.status(403).json(blockBody(result)),
1778
+ () => {
1779
+ if (isDocumentNavigation((n) => req.headers?.[n])) {
1780
+ res.status(403).type("html").send(renderBlockPage({ url: req.originalUrl || req.url || "/", code: result?.rule?.id }));
1781
+ } else {
1782
+ res.status(403).json(blockBody(result));
1783
+ }
1784
+ },
1483
1785
  () => {
1484
1786
  if (exprOptions.screenResponses) wrapNodeResponse(res);
1485
1787
  next();
@@ -1523,8 +1825,13 @@ async function createProtection(options = {}) {
1523
1825
  result,
1524
1826
  () => {
1525
1827
  res.statusCode = 403;
1526
- res.setHeader("content-type", "application/json");
1527
- res.end(JSON.stringify(blockBody(result)));
1828
+ if (isDocumentNavigation((n) => req.headers?.[n])) {
1829
+ res.setHeader("content-type", "text/html; charset=utf-8");
1830
+ res.end(renderBlockPage({ url: req.url || "/", code: result?.rule?.id }));
1831
+ } else {
1832
+ res.setHeader("content-type", "application/json");
1833
+ res.end(JSON.stringify(blockBody(result)));
1834
+ }
1528
1835
  },
1529
1836
  () => {
1530
1837
  if (req.body === void 0) req.body = shaped.body;
@@ -1537,7 +1844,7 @@ async function createProtection(options = {}) {
1537
1844
  }
1538
1845
  };
1539
1846
  if (options.egress) {
1540
- protection.uninstallEgress = installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
1847
+ protection.uninstallEgress = await installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
1541
1848
  }
1542
1849
  return protection;
1543
1850
  }
@@ -1563,6 +1870,8 @@ function headerObject(headers) {
1563
1870
  headers?.forEach?.((v, k) => {
1564
1871
  out[k.toLowerCase()] = v;
1565
1872
  });
1873
+ const setCookies = headers?.getSetCookie?.();
1874
+ if (setCookies && setCookies.length) out["set-cookie"] = setCookies;
1566
1875
  return out;
1567
1876
  }
1568
1877
  function extractRedactors(rule) {
@@ -1597,9 +1906,27 @@ function applyRedactors(body, redactors, mask) {
1597
1906
  }
1598
1907
  return out;
1599
1908
  }
1600
- function rebuildResponse(response, body) {
1909
+ function rebuildResponse(response, body, redactedHeaders) {
1601
1910
  const headers = new Headers(response.headers);
1602
1911
  headers.delete("content-length");
1912
+ if (redactedHeaders) {
1913
+ for (const [name, value] of Object.entries(redactedHeaders)) {
1914
+ if (typeof value === "string") {
1915
+ if (headers.get(name) !== value) {
1916
+ try {
1917
+ headers.set(name, value);
1918
+ } catch {
1919
+ }
1920
+ }
1921
+ } else if (Array.isArray(value)) {
1922
+ try {
1923
+ headers.delete(name);
1924
+ for (const item of value) headers.append(name, String(item));
1925
+ } catch {
1926
+ }
1927
+ }
1928
+ }
1929
+ }
1603
1930
  return new Response(body, { status: response.status, statusText: response.statusText, headers });
1604
1931
  }
1605
1932
  function leakResponse() {
@@ -1609,40 +1936,43 @@ function leakResponse() {
1609
1936
  });
1610
1937
  }
1611
1938
  async function resolveRules(options) {
1939
+ const cache = makeCache(options);
1612
1940
  if (options.siteUuid) {
1613
- const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl });
1941
+ const prior = await cache.read();
1942
+ const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
1614
1943
  const res = await client.getRules();
1615
- if (res.success) {
1944
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
1945
+ if (res.success && !res.notModified) {
1616
1946
  const bundle = normalizeBundle(res);
1617
- cacheWrite(options.cacheDir, bundle);
1947
+ await cache.write({ bundle, etag: res.etag ?? null });
1618
1948
  return bundle;
1619
1949
  }
1620
- const cached = cacheRead(options.cacheDir);
1621
- if (cached) {
1622
- options.onError?.(new Error(`pulse rule fetch failed (${res.error}); using cached bundle`));
1623
- return normalizeBundle(cached);
1950
+ if (prior?.bundle) {
1951
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
1952
+ return normalizeBundle(prior.bundle);
1624
1953
  }
1625
1954
  if (options.rules) {
1626
- options.onError?.(new Error(`pulse rule fetch failed (${res.error}); using bundled fallback`));
1955
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
1627
1956
  return normalizeBundle(options.rules);
1628
1957
  }
1629
- options.onError?.(new Error(`pulse rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
1958
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
1630
1959
  return emptyBundle();
1631
1960
  }
1632
1961
  if (options.token) {
1633
- const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl });
1962
+ const prior = await cache.read();
1963
+ const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
1634
1964
  const res = await client.getRules();
1635
- if (res.success) {
1965
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
1966
+ if (res.success && !res.notModified) {
1636
1967
  const bundle = normalizeBundle(res);
1637
- cacheWrite(options.cacheDir, bundle);
1968
+ await cache.write({ bundle, etag: res.etag ?? null });
1638
1969
  return bundle;
1639
1970
  }
1640
- const cached = cacheRead(options.cacheDir);
1641
- if (cached) {
1642
- options.onError?.(new Error(`rule fetch failed (${res.error}); using cached bundle`));
1643
- return normalizeBundle(cached);
1971
+ if (prior?.bundle) {
1972
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
1973
+ return normalizeBundle(prior.bundle);
1644
1974
  }
1645
- options.onError?.(new Error(`rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
1975
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
1646
1976
  return emptyBundle();
1647
1977
  }
1648
1978
  if (options.rules) {
@@ -1660,33 +1990,76 @@ function normalizeBundle(b) {
1660
1990
  function emptyBundle() {
1661
1991
  return { firewall: [], whitelists: [], whitelist_keys: {} };
1662
1992
  }
1993
+ function makeCache(options) {
1994
+ const adapter = options.ruleCache;
1995
+ if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
1996
+ return {
1997
+ read: async () => {
1998
+ try {
1999
+ return toEnvelope(await adapter.read());
2000
+ } catch {
2001
+ return null;
2002
+ }
2003
+ },
2004
+ write: async (env) => {
2005
+ try {
2006
+ await adapter.write(env);
2007
+ } catch {
2008
+ }
2009
+ }
2010
+ };
2011
+ }
2012
+ const dir = options.cacheDir;
2013
+ return {
2014
+ read: async () => cacheRead(dir),
2015
+ write: async (env) => cacheWrite(dir, env)
2016
+ };
2017
+ }
1663
2018
  function cachePath(dir) {
1664
2019
  return (0, import_node_path.join)(dir, "patchstack-rules.json");
1665
2020
  }
1666
- function cacheWrite(dir, bundle) {
2021
+ function cacheWrite(dir, env) {
1667
2022
  if (!dir) return;
1668
2023
  try {
1669
2024
  (0, import_node_fs.mkdirSync)(dir, { recursive: true });
1670
- (0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(bundle));
2025
+ (0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
1671
2026
  } catch {
1672
2027
  }
1673
2028
  }
1674
2029
  function cacheRead(dir) {
1675
2030
  if (!dir) return null;
1676
2031
  try {
1677
- return JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8"));
2032
+ return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
1678
2033
  } catch {
1679
2034
  return null;
1680
2035
  }
1681
2036
  }
2037
+ function toEnvelope(value) {
2038
+ if (!value || typeof value !== "object") return null;
2039
+ if (value.bundle && typeof value.bundle === "object") {
2040
+ return { bundle: value.bundle, etag: value.etag ?? null };
2041
+ }
2042
+ if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
2043
+ return { bundle: value, etag: null };
2044
+ }
2045
+ return null;
2046
+ }
2047
+ var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
1682
2048
  function blockBody(result) {
1683
- return {
1684
- error: "Blocked by Patchstack",
1685
- rule: result.rule?.id,
1686
- message: result.message
1687
- };
2049
+ return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };
1688
2050
  }
1689
- function blockResponse(result) {
2051
+ function isDocumentNavigation(getHeader) {
2052
+ const dest = getHeader("sec-fetch-dest");
2053
+ if (dest) return dest === "document";
2054
+ return (getHeader("accept") || "").includes("text/html");
2055
+ }
2056
+ function blockResponse(result, request) {
2057
+ if (request && isDocumentNavigation((n) => request.headers.get(n))) {
2058
+ return new Response(renderBlockPage({ url: request.url, code: result?.rule?.id }), {
2059
+ status: 403,
2060
+ headers: { "content-type": "text/html; charset=utf-8" }
2061
+ });
2062
+ }
1690
2063
  return new Response(JSON.stringify(blockBody(result)), {
1691
2064
  status: 403,
1692
2065
  headers: { "content-type": "application/json" }