@patchstack/connect 0.3.16 → 0.3.18

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
@@ -1,38 +1,48 @@
1
1
  // src/protect/engine/client.js
2
2
  var DEFAULT_BASE_URL = "https://api.patchstack.com";
3
3
  var DEFAULT_CACHE_TTL = 3e5;
4
+ var JITTER_FRACTION = 0.1;
4
5
  var PatchstackRuleClient = class {
5
6
  #token;
6
7
  #baseUrl;
7
8
  #cacheTtl;
8
9
  #cache = null;
9
10
  #cacheTime = null;
10
- constructor({ token, baseUrl, cacheTtl } = {}) {
11
+ #ttlEffective = 0;
12
+ #etag;
13
+ constructor({ token, baseUrl, cacheTtl, etag } = {}) {
11
14
  this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
12
15
  this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
13
16
  this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL;
17
+ this.#etag = etag ?? null;
14
18
  if (!this.#token) {
15
19
  throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
16
20
  }
17
21
  }
18
22
  async getRules() {
19
23
  const now = Date.now();
20
- if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#cacheTtl) {
24
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
21
25
  return this.#cache;
22
26
  }
23
27
  const url = `${this.#baseUrl}/api/get-rules/3`;
24
28
  try {
29
+ const headers = {
30
+ "Content-Type": "application/json",
31
+ "Authorization": `Bearer ${this.#token}`,
32
+ "LicenseID": "1"
33
+ // Hard-coded, is never actually used but needed by the API
34
+ };
35
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
25
36
  const response = await fetch(url, {
26
37
  method: "POST",
27
- headers: {
28
- "Content-Type": "application/json",
29
- "Authorization": `Bearer ${this.#token}`,
30
- "LicenseID": "1"
31
- // Hard-coded, is never actually used but needed by the API
32
- },
38
+ headers,
33
39
  body: JSON.stringify({}),
34
40
  signal: AbortSignal.timeout(3e4)
35
41
  });
42
+ if (response.status === 304) {
43
+ this.#touch(now);
44
+ return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
45
+ }
36
46
  if (!response.ok) {
37
47
  return {
38
48
  success: false,
@@ -45,12 +55,15 @@ var PatchstackRuleClient = class {
45
55
  const data = await response.json();
46
56
  const result = {
47
57
  success: true,
58
+ notModified: false,
59
+ etag: response.headers?.get?.("etag") ?? null,
48
60
  firewall: Array.isArray(data.firewall) ? data.firewall : [],
49
61
  whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
50
62
  whitelist_keys: data.whitelist_keys ?? {}
51
63
  };
52
64
  this.#cache = result;
53
- this.#cacheTime = now;
65
+ this.#etag = result.etag;
66
+ this.#touch(now);
54
67
  return result;
55
68
  } catch (err) {
56
69
  return {
@@ -62,9 +75,14 @@ var PatchstackRuleClient = class {
62
75
  };
63
76
  }
64
77
  }
78
+ #touch(now) {
79
+ this.#cacheTime = now;
80
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION);
81
+ }
65
82
  clearCache() {
66
83
  this.#cache = null;
67
84
  this.#cacheTime = null;
85
+ this.#etag = null;
68
86
  }
69
87
  };
70
88
 
@@ -902,44 +920,53 @@ var RuleEngine = class {
902
920
  // src/protect/engine/pulse-client.js
903
921
  var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
904
922
  var DEFAULT_CACHE_TTL2 = 3e5;
923
+ var JITTER_FRACTION2 = 0.1;
905
924
  var PulseRuleClient = class {
906
925
  #siteUuid;
907
926
  #baseUrl;
908
927
  #cacheTtl;
909
928
  #cache = null;
910
929
  #cacheTime = null;
911
- constructor({ siteUuid, baseUrl, cacheTtl } = {}) {
930
+ #ttlEffective = 0;
931
+ #etag;
932
+ constructor({ siteUuid, baseUrl, cacheTtl, etag } = {}) {
912
933
  this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
913
934
  this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL ?? DEFAULT_BASE_URL2;
914
935
  this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL2;
936
+ this.#etag = etag ?? null;
915
937
  if (!this.#siteUuid) {
916
938
  throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
917
939
  }
918
940
  }
919
941
  async getRules() {
920
942
  const now = Date.now();
921
- if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#cacheTtl) {
943
+ if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
922
944
  return this.#cache;
923
945
  }
924
946
  const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
925
947
  try {
926
- const response = await fetch(url, {
927
- method: "GET",
928
- headers: { Accept: "application/json" },
929
- signal: AbortSignal.timeout(3e4)
930
- });
948
+ const headers = { Accept: "application/json" };
949
+ if (this.#etag) headers["If-None-Match"] = this.#etag;
950
+ const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(3e4) });
951
+ if (response.status === 304) {
952
+ this.#touch(now);
953
+ return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
954
+ }
931
955
  if (!response.ok) {
932
956
  return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
933
957
  }
934
958
  const data = await response.json();
935
959
  const result = {
936
960
  success: true,
961
+ notModified: false,
962
+ etag: response.headers?.get?.("etag") ?? null,
937
963
  firewall: Array.isArray(data.firewall) ? data.firewall : [],
938
964
  whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
939
965
  whitelist_keys: data.whitelist_keys ?? {}
940
966
  };
941
967
  this.#cache = result;
942
- this.#cacheTime = now;
968
+ this.#etag = result.etag;
969
+ this.#touch(now);
943
970
  return result;
944
971
  } catch (err) {
945
972
  return {
@@ -951,14 +978,21 @@ var PulseRuleClient = class {
951
978
  };
952
979
  }
953
980
  }
981
+ #touch(now) {
982
+ this.#cacheTime = now;
983
+ this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
984
+ }
954
985
  clearCache() {
955
986
  this.#cache = null;
956
987
  this.#cacheTime = null;
988
+ this.#etag = null;
957
989
  }
958
990
  };
959
991
 
960
992
  // src/protect/engine/fetch.js
961
- async function fromFetchRequest(request) {
993
+ var MAX_BODY_BYTES = 1024 * 1024;
994
+ async function fromFetchRequest(request, options = {}) {
995
+ const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
962
996
  const url = new URL(request.url);
963
997
  const method = (request.method || "GET").toUpperCase();
964
998
  const headers = {};
@@ -975,14 +1009,11 @@ async function fromFetchRequest(request) {
975
1009
  }
976
1010
  let rawBody = "";
977
1011
  if (method !== "GET" && method !== "HEAD") {
978
- try {
979
- rawBody = await request.clone().text();
980
- } catch {
981
- rawBody = "";
982
- }
1012
+ rawBody = await readCappedText(request, maxBodyBytes);
983
1013
  }
984
1014
  const contentType = headers["content-type"] || "";
985
1015
  let body = {};
1016
+ let files;
986
1017
  if (rawBody) {
987
1018
  if (contentType.includes("application/json")) {
988
1019
  try {
@@ -995,6 +1026,13 @@ async function fromFetchRequest(request) {
995
1026
  for (const [k, v] of new URLSearchParams(rawBody)) {
996
1027
  body[k] = k in body ? [].concat(body[k], v) : v;
997
1028
  }
1029
+ } else if (contentType.includes("multipart/form-data")) {
1030
+ const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
1031
+ if (boundary) {
1032
+ const parsed = parseMultipart(rawBody, boundary);
1033
+ body = parsed.body;
1034
+ files = parsed.files;
1035
+ }
998
1036
  }
999
1037
  }
1000
1038
  const uri = url.pathname + url.search;
@@ -1005,6 +1043,7 @@ async function fromFetchRequest(request) {
1005
1043
  originalUrl: uri,
1006
1044
  query,
1007
1045
  body,
1046
+ files,
1008
1047
  headers,
1009
1048
  ip: forwarded.split(",")[0].trim(),
1010
1049
  cookies: parseCookies(headers.cookie),
@@ -1013,6 +1052,44 @@ async function fromFetchRequest(request) {
1013
1052
  _rawBody: rawBody
1014
1053
  };
1015
1054
  }
1055
+ async function readCappedText(request, max) {
1056
+ const ceiling = Math.max(max, max * 4);
1057
+ const declared = Number(request.headers?.get?.("content-length") || 0);
1058
+ if (declared && declared > ceiling) return "";
1059
+ let clone;
1060
+ try {
1061
+ clone = request.clone();
1062
+ } catch {
1063
+ return "";
1064
+ }
1065
+ try {
1066
+ const text = await clone.text();
1067
+ return text.length > max ? text.slice(0, max) : text;
1068
+ } catch {
1069
+ return "";
1070
+ }
1071
+ }
1072
+ function parseMultipart(rawBody, boundary) {
1073
+ const body = {};
1074
+ const files = {};
1075
+ for (const part of rawBody.split("--" + boundary)) {
1076
+ const sep = /\r?\n\r?\n/.exec(part);
1077
+ if (!sep) continue;
1078
+ const rawHeaders = part.slice(0, sep.index);
1079
+ const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
1080
+ if (!disposition) continue;
1081
+ const name = /name="([^"]*)"/i.exec(disposition)?.[1];
1082
+ if (name == null) continue;
1083
+ const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
1084
+ const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
1085
+ if (filename !== void 0) {
1086
+ files[name] = name in files ? [].concat(files[name], filename) : filename;
1087
+ } else {
1088
+ body[name] = name in body ? [].concat(body[name], content) : content;
1089
+ }
1090
+ }
1091
+ return { body, files };
1092
+ }
1016
1093
  function parseCookies(header) {
1017
1094
  const cookies = {};
1018
1095
  if (!header) {
@@ -1092,35 +1169,124 @@ function parseCookies2(header) {
1092
1169
  }
1093
1170
 
1094
1171
  // src/protect/egress.js
1095
- function installEgressGuard({ shouldBlock, onBlock } = {}) {
1096
- const original = globalThis.fetch;
1097
- if (typeof original !== "function" || original.__patchstackGuarded || typeof shouldBlock !== "function") {
1098
- return () => {
1172
+ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
1173
+ const restores = [];
1174
+ if (typeof shouldBlock !== "function") return () => {
1175
+ };
1176
+ const block = (url, host, method) => {
1177
+ if (!shouldBlock(url, host, method)) return false;
1178
+ onBlock?.({ url, host, method });
1179
+ return true;
1180
+ };
1181
+ const originalFetch = globalThis.fetch;
1182
+ if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
1183
+ const guarded = async (input, init) => {
1184
+ const url = typeof input === "string" ? input : input && input.url || String(input);
1185
+ let host = null;
1186
+ try {
1187
+ host = new URL(url).hostname;
1188
+ } catch {
1189
+ host = null;
1190
+ }
1191
+ const method = init && init.method || input && input.method || "GET";
1192
+ if (block(url, host, method)) {
1193
+ throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
1194
+ }
1195
+ return originalFetch(input, init);
1099
1196
  };
1197
+ guarded.__patchstackGuarded = true;
1198
+ globalThis.fetch = guarded;
1199
+ restores.push(() => {
1200
+ if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
1201
+ });
1100
1202
  }
1101
- const guarded = async (input, init) => {
1102
- const url = typeof input === "string" ? input : input && input.url || String(input);
1103
- let host = null;
1203
+ for (const moduleName of ["node:http", "node:https"]) {
1104
1204
  try {
1105
- host = new URL(url).hostname;
1205
+ const mod = await import(moduleName);
1206
+ const restore = patchHttpModule(mod.default ?? mod, block);
1207
+ if (restore) restores.push(restore);
1106
1208
  } catch {
1107
- host = null;
1108
1209
  }
1109
- const method = init && init.method || input && input.method || "GET";
1110
- if (shouldBlock(url, host, method)) {
1111
- onBlock?.({ url, host, method });
1112
- throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
1210
+ }
1211
+ const OriginalWS = globalThis.WebSocket;
1212
+ if (typeof OriginalWS === "function" && !OriginalWS.__patchstackGuarded) {
1213
+ const GuardedWS = new Proxy(OriginalWS, {
1214
+ construct(target, args, newTarget) {
1215
+ const url = String(args?.[0] ?? "");
1216
+ let host = null;
1217
+ try {
1218
+ host = new URL(url).hostname;
1219
+ } catch {
1220
+ host = null;
1221
+ }
1222
+ if (block(url, host, "WEBSOCKET")) {
1223
+ throw new Error(`Patchstack blocked an outbound WebSocket to a disallowed address: ${host ?? url}`);
1224
+ }
1225
+ return Reflect.construct(target, args, newTarget);
1226
+ }
1227
+ });
1228
+ OriginalWS.__patchstackGuarded = true;
1229
+ globalThis.WebSocket = GuardedWS;
1230
+ restores.push(() => {
1231
+ if (globalThis.WebSocket === GuardedWS) globalThis.WebSocket = OriginalWS;
1232
+ delete OriginalWS.__patchstackGuarded;
1233
+ });
1234
+ }
1235
+ return () => {
1236
+ for (const restore of restores) {
1237
+ try {
1238
+ restore();
1239
+ } catch {
1240
+ }
1113
1241
  }
1114
- return original(input, init);
1115
1242
  };
1116
- guarded.__patchstackGuarded = true;
1117
- globalThis.fetch = guarded;
1243
+ }
1244
+ function patchHttpModule(http, block) {
1245
+ if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
1246
+ const originalRequest = http.request;
1247
+ const originalGet = http.get;
1248
+ const wrap = (original) => function(...args) {
1249
+ const target = extractHttpTarget(args);
1250
+ if (target && block(target.url, target.host, target.method)) {
1251
+ throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
1252
+ }
1253
+ return original.apply(this, args);
1254
+ };
1255
+ http.request = wrap(originalRequest);
1256
+ if (typeof originalGet === "function") http.get = wrap(originalGet);
1257
+ http.__patchstackGuarded = true;
1118
1258
  return () => {
1119
- if (globalThis.fetch === guarded) {
1120
- globalThis.fetch = original;
1121
- }
1259
+ http.request = originalRequest;
1260
+ if (typeof originalGet === "function") http.get = originalGet;
1261
+ delete http.__patchstackGuarded;
1122
1262
  };
1123
1263
  }
1264
+ function extractHttpTarget(args) {
1265
+ const first = args[0];
1266
+ try {
1267
+ if (typeof first === "string" || first instanceof URL) {
1268
+ const url = new URL(String(first));
1269
+ const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
1270
+ return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
1271
+ }
1272
+ if (first && typeof first === "object") {
1273
+ const host = normalizeHost(first.hostname || first.host);
1274
+ const protocol = first.protocol || "http:";
1275
+ const port = first.port ? `:${first.port}` : "";
1276
+ const path = first.path || "/";
1277
+ return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
1278
+ }
1279
+ } catch {
1280
+ }
1281
+ return null;
1282
+ }
1283
+ function normalizeHost(raw) {
1284
+ const host = String(raw || "").trim();
1285
+ const bracketed = /^\[([^\]]+)\]/.exec(host);
1286
+ if (bracketed) return bracketed[1];
1287
+ if ((host.match(/:/g) || []).length > 1) return host;
1288
+ return host.split(":")[0];
1289
+ }
1124
1290
 
1125
1291
  // src/protect/defaults.js
1126
1292
  var DEFAULT_RESPONSE_RULES = [
@@ -1171,6 +1337,25 @@ var DEFAULT_RESPONSE_RULES = [
1171
1337
  category: "info-exposure",
1172
1338
  action: "redact",
1173
1339
  rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\n\\s+at\\s+.+\\(.+:\\d+:\\d+\\)/" } }]
1340
+ },
1341
+ {
1342
+ id: "resp-sql-error",
1343
+ title: "SQL / ORM error disclosure in response body",
1344
+ phase: "response",
1345
+ category: "info-exposure",
1346
+ action: "redact",
1347
+ 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" } }]
1348
+ },
1349
+ {
1350
+ id: "resp-exception-trace",
1351
+ title: "Backend exception / stack trace disclosure in response body",
1352
+ phase: "response",
1353
+ category: "info-exposure",
1354
+ action: "redact",
1355
+ // Multi-language exception/traceback signatures a normal API response never carries:
1356
+ // Python traceback, Java "Exception in thread", .NET System.*Exception, JVM stack frames,
1357
+ // Go goroutine dumps. (Node `at fn (file:line:col)` frames are handled by resp-stack-trace.)
1358
+ 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+ \\[)/' } }]
1174
1359
  }
1175
1360
  ];
1176
1361
  var DEFAULT_EGRESS_RULES = [
@@ -1183,6 +1368,89 @@ var DEFAULT_EGRESS_RULES = [
1183
1368
  }
1184
1369
  ];
1185
1370
 
1371
+ // src/protect/block-page.js
1372
+ function escapeHtml(s) {
1373
+ return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1374
+ }
1375
+ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
1376
+ const safeUrl = escapeHtml(url);
1377
+ const safeCode = escapeHtml(code || "patchstack");
1378
+ return `<!DOCTYPE html>
1379
+ <html lang="en">
1380
+ <head>
1381
+ <meta charset="UTF-8">
1382
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
1383
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1384
+ <style type="text/css">
1385
+ .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}}
1386
+ </style>
1387
+ <title>Access Denied</title>
1388
+ </head>
1389
+ <body>
1390
+ <div class="main-container">
1391
+ <div class="container">
1392
+ <div class="content">
1393
+ <span class="access-denied">Access Denied</span>
1394
+ <div class="site-url">
1395
+ <input id="url" class="site-url-text" type="text"
1396
+ value="${safeUrl}" readonly>
1397
+ <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')">
1398
+ </div>
1399
+ <div class="site-url">
1400
+ <input id="response" class="reason-code-text" type="text" value="${safeCode}" readonly>
1401
+ <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')">
1402
+ </div>
1403
+ <div class="descp">
1404
+ <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>
1405
+ </div>
1406
+ </div>
1407
+ </div>
1408
+ <div class="bottom-container">
1409
+ <div class="bottom-text-containers">
1410
+ <img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
1411
+ <a href="/" class="return-text">Return to homepage</a>
1412
+ </div>
1413
+ <div class="bottom-second-containers">
1414
+ <span class="secured-text">This website is secured by</span>
1415
+ <a href="https://patchstack.com" class="logoText">
1416
+ <img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
1417
+ </a>
1418
+ </div>
1419
+ </div>
1420
+
1421
+ <div class="mobile-bottom-container">
1422
+ <div class="mobile-bottom-text-containers">
1423
+ <img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
1424
+ <a href="/" class="return-text">Return to homepage</a>
1425
+ </div>
1426
+ <div class="mobile-bottom-second-containers">
1427
+ <span class="secured-text">This website is secured by</span>
1428
+ <a href="https://patchstack.com" class="logoText">
1429
+ <img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
1430
+ </a>
1431
+ </div>
1432
+ </div>
1433
+ </div>
1434
+ </body>
1435
+ <script>
1436
+ function copyText(id) {
1437
+ var copyText = document.getElementById(id)
1438
+ copyText.select();
1439
+ copyText.setSelectionRange(0, 99999);
1440
+ navigator.clipboard.writeText(copyText.value);
1441
+ }
1442
+
1443
+ function hover(element) {
1444
+ element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
1445
+ }
1446
+
1447
+ function unhover(element) {
1448
+ element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
1449
+ }
1450
+ </script>
1451
+ </html>`;
1452
+ }
1453
+
1186
1454
  // src/protect/runtime.js
1187
1455
  import { readFileSync, writeFileSync, mkdirSync } from "fs";
1188
1456
  import { join } from "path";
@@ -1308,15 +1576,27 @@ async function createProtection(options = {}) {
1308
1576
  if (mode !== "block" || !blockRule && !redactions.length) return { verdict: "pass" };
1309
1577
  if (blockRule) return { verdict: "block" };
1310
1578
  let body = text;
1311
- for (const { rule, redactors } of redactions) body = applyRedactors(body, redactors, maskFn(rule.category));
1312
- return { verdict: "redact", body };
1579
+ const headers = { ...meta.headers || {} };
1580
+ for (const { rule, redactors } of redactions) {
1581
+ const mask = maskFn(rule.category);
1582
+ body = applyRedactors(body, redactors, mask);
1583
+ for (const name of Object.keys(headers)) {
1584
+ const value = headers[name];
1585
+ if (typeof value === "string") {
1586
+ headers[name] = applyRedactors(value, redactors, mask);
1587
+ } else if (Array.isArray(value)) {
1588
+ headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, redactors, mask) : item);
1589
+ }
1590
+ }
1591
+ }
1592
+ return { verdict: "redact", body, headers };
1313
1593
  };
1314
1594
  const screenResp = async (response) => {
1315
1595
  const text = await readTextResponse(response);
1316
1596
  if (text == null) return response;
1317
1597
  const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
1318
1598
  if (r.verdict === "block") return leakResponse();
1319
- if (r.verdict === "redact") return rebuildResponse(response, r.body);
1599
+ if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
1320
1600
  return response;
1321
1601
  };
1322
1602
  const wrapNodeResponse = (res) => {
@@ -1366,7 +1646,7 @@ async function createProtection(options = {}) {
1366
1646
  }
1367
1647
  let r;
1368
1648
  try {
1369
- r = screenText(text, { status: res.statusCode, headers: {} });
1649
+ r = screenText(text, { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} });
1370
1650
  } catch (err) {
1371
1651
  onError?.(err);
1372
1652
  for (const c of chunks) origWrite(c);
@@ -1385,6 +1665,22 @@ async function createProtection(options = {}) {
1385
1665
  res.removeHeader && res.removeHeader("content-length");
1386
1666
  } catch {
1387
1667
  }
1668
+ if (r.headers && res.setHeader) {
1669
+ const current = res.getHeaders ? res.getHeaders() : {};
1670
+ for (const [name, value] of Object.entries(r.headers)) {
1671
+ if (Array.isArray(value)) {
1672
+ try {
1673
+ res.setHeader(name, value);
1674
+ } catch {
1675
+ }
1676
+ } else if (typeof value === "string" && current[name] !== value) {
1677
+ try {
1678
+ res.setHeader(name, value);
1679
+ } catch {
1680
+ }
1681
+ }
1682
+ }
1683
+ }
1388
1684
  return origEnd(r.body, cb);
1389
1685
  }
1390
1686
  for (const c of chunks) origWrite(c);
@@ -1421,7 +1717,7 @@ async function createProtection(options = {}) {
1421
1717
  onError?.(err);
1422
1718
  return null;
1423
1719
  }
1424
- return decide("request", result, () => blockResponse(result), () => null);
1720
+ return decide("request", result, () => blockResponse(result, request), () => null);
1425
1721
  };
1426
1722
  },
1427
1723
  // Wrap a fetch handler: screens the request, then the response (redact/block).
@@ -1449,7 +1745,13 @@ async function createProtection(options = {}) {
1449
1745
  decide(
1450
1746
  "request",
1451
1747
  result,
1452
- () => res.status(403).json(blockBody(result)),
1748
+ () => {
1749
+ if (isDocumentNavigation((n) => req.headers?.[n])) {
1750
+ res.status(403).type("html").send(renderBlockPage({ url: req.originalUrl || req.url || "/", code: result?.rule?.id }));
1751
+ } else {
1752
+ res.status(403).json(blockBody(result));
1753
+ }
1754
+ },
1453
1755
  () => {
1454
1756
  if (exprOptions.screenResponses) wrapNodeResponse(res);
1455
1757
  next();
@@ -1493,8 +1795,13 @@ async function createProtection(options = {}) {
1493
1795
  result,
1494
1796
  () => {
1495
1797
  res.statusCode = 403;
1496
- res.setHeader("content-type", "application/json");
1497
- res.end(JSON.stringify(blockBody(result)));
1798
+ if (isDocumentNavigation((n) => req.headers?.[n])) {
1799
+ res.setHeader("content-type", "text/html; charset=utf-8");
1800
+ res.end(renderBlockPage({ url: req.url || "/", code: result?.rule?.id }));
1801
+ } else {
1802
+ res.setHeader("content-type", "application/json");
1803
+ res.end(JSON.stringify(blockBody(result)));
1804
+ }
1498
1805
  },
1499
1806
  () => {
1500
1807
  if (req.body === void 0) req.body = shaped.body;
@@ -1507,7 +1814,7 @@ async function createProtection(options = {}) {
1507
1814
  }
1508
1815
  };
1509
1816
  if (options.egress) {
1510
- protection.uninstallEgress = installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
1817
+ protection.uninstallEgress = await installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
1511
1818
  }
1512
1819
  return protection;
1513
1820
  }
@@ -1533,6 +1840,8 @@ function headerObject(headers) {
1533
1840
  headers?.forEach?.((v, k) => {
1534
1841
  out[k.toLowerCase()] = v;
1535
1842
  });
1843
+ const setCookies = headers?.getSetCookie?.();
1844
+ if (setCookies && setCookies.length) out["set-cookie"] = setCookies;
1536
1845
  return out;
1537
1846
  }
1538
1847
  function extractRedactors(rule) {
@@ -1567,9 +1876,27 @@ function applyRedactors(body, redactors, mask) {
1567
1876
  }
1568
1877
  return out;
1569
1878
  }
1570
- function rebuildResponse(response, body) {
1879
+ function rebuildResponse(response, body, redactedHeaders) {
1571
1880
  const headers = new Headers(response.headers);
1572
1881
  headers.delete("content-length");
1882
+ if (redactedHeaders) {
1883
+ for (const [name, value] of Object.entries(redactedHeaders)) {
1884
+ if (typeof value === "string") {
1885
+ if (headers.get(name) !== value) {
1886
+ try {
1887
+ headers.set(name, value);
1888
+ } catch {
1889
+ }
1890
+ }
1891
+ } else if (Array.isArray(value)) {
1892
+ try {
1893
+ headers.delete(name);
1894
+ for (const item of value) headers.append(name, String(item));
1895
+ } catch {
1896
+ }
1897
+ }
1898
+ }
1899
+ }
1573
1900
  return new Response(body, { status: response.status, statusText: response.statusText, headers });
1574
1901
  }
1575
1902
  function leakResponse() {
@@ -1579,40 +1906,43 @@ function leakResponse() {
1579
1906
  });
1580
1907
  }
1581
1908
  async function resolveRules(options) {
1909
+ const cache = makeCache(options);
1582
1910
  if (options.siteUuid) {
1583
- const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl });
1911
+ const prior = await cache.read();
1912
+ const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
1584
1913
  const res = await client.getRules();
1585
- if (res.success) {
1914
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
1915
+ if (res.success && !res.notModified) {
1586
1916
  const bundle = normalizeBundle(res);
1587
- cacheWrite(options.cacheDir, bundle);
1917
+ await cache.write({ bundle, etag: res.etag ?? null });
1588
1918
  return bundle;
1589
1919
  }
1590
- const cached = cacheRead(options.cacheDir);
1591
- if (cached) {
1592
- options.onError?.(new Error(`pulse rule fetch failed (${res.error}); using cached bundle`));
1593
- return normalizeBundle(cached);
1920
+ if (prior?.bundle) {
1921
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
1922
+ return normalizeBundle(prior.bundle);
1594
1923
  }
1595
1924
  if (options.rules) {
1596
- options.onError?.(new Error(`pulse rule fetch failed (${res.error}); using bundled fallback`));
1925
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
1597
1926
  return normalizeBundle(options.rules);
1598
1927
  }
1599
- options.onError?.(new Error(`pulse rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
1928
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
1600
1929
  return emptyBundle();
1601
1930
  }
1602
1931
  if (options.token) {
1603
- const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl });
1932
+ const prior = await cache.read();
1933
+ const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
1604
1934
  const res = await client.getRules();
1605
- if (res.success) {
1935
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
1936
+ if (res.success && !res.notModified) {
1606
1937
  const bundle = normalizeBundle(res);
1607
- cacheWrite(options.cacheDir, bundle);
1938
+ await cache.write({ bundle, etag: res.etag ?? null });
1608
1939
  return bundle;
1609
1940
  }
1610
- const cached = cacheRead(options.cacheDir);
1611
- if (cached) {
1612
- options.onError?.(new Error(`rule fetch failed (${res.error}); using cached bundle`));
1613
- return normalizeBundle(cached);
1941
+ if (prior?.bundle) {
1942
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
1943
+ return normalizeBundle(prior.bundle);
1614
1944
  }
1615
- options.onError?.(new Error(`rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
1945
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
1616
1946
  return emptyBundle();
1617
1947
  }
1618
1948
  if (options.rules) {
@@ -1630,33 +1960,76 @@ function normalizeBundle(b) {
1630
1960
  function emptyBundle() {
1631
1961
  return { firewall: [], whitelists: [], whitelist_keys: {} };
1632
1962
  }
1963
+ function makeCache(options) {
1964
+ const adapter = options.ruleCache;
1965
+ if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
1966
+ return {
1967
+ read: async () => {
1968
+ try {
1969
+ return toEnvelope(await adapter.read());
1970
+ } catch {
1971
+ return null;
1972
+ }
1973
+ },
1974
+ write: async (env) => {
1975
+ try {
1976
+ await adapter.write(env);
1977
+ } catch {
1978
+ }
1979
+ }
1980
+ };
1981
+ }
1982
+ const dir = options.cacheDir;
1983
+ return {
1984
+ read: async () => cacheRead(dir),
1985
+ write: async (env) => cacheWrite(dir, env)
1986
+ };
1987
+ }
1633
1988
  function cachePath(dir) {
1634
1989
  return join(dir, "patchstack-rules.json");
1635
1990
  }
1636
- function cacheWrite(dir, bundle) {
1991
+ function cacheWrite(dir, env) {
1637
1992
  if (!dir) return;
1638
1993
  try {
1639
1994
  mkdirSync(dir, { recursive: true });
1640
- writeFileSync(cachePath(dir), JSON.stringify(bundle));
1995
+ writeFileSync(cachePath(dir), JSON.stringify(env));
1641
1996
  } catch {
1642
1997
  }
1643
1998
  }
1644
1999
  function cacheRead(dir) {
1645
2000
  if (!dir) return null;
1646
2001
  try {
1647
- return JSON.parse(readFileSync(cachePath(dir), "utf8"));
2002
+ return toEnvelope(JSON.parse(readFileSync(cachePath(dir), "utf8")));
1648
2003
  } catch {
1649
2004
  return null;
1650
2005
  }
1651
2006
  }
2007
+ function toEnvelope(value) {
2008
+ if (!value || typeof value !== "object") return null;
2009
+ if (value.bundle && typeof value.bundle === "object") {
2010
+ return { bundle: value.bundle, etag: value.etag ?? null };
2011
+ }
2012
+ if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
2013
+ return { bundle: value, etag: null };
2014
+ }
2015
+ return null;
2016
+ }
2017
+ var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
1652
2018
  function blockBody(result) {
1653
- return {
1654
- error: "Blocked by Patchstack",
1655
- rule: result.rule?.id,
1656
- message: result.message
1657
- };
2019
+ return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };
1658
2020
  }
1659
- function blockResponse(result) {
2021
+ function isDocumentNavigation(getHeader) {
2022
+ const dest = getHeader("sec-fetch-dest");
2023
+ if (dest) return dest === "document";
2024
+ return (getHeader("accept") || "").includes("text/html");
2025
+ }
2026
+ function blockResponse(result, request) {
2027
+ if (request && isDocumentNavigation((n) => request.headers.get(n))) {
2028
+ return new Response(renderBlockPage({ url: request.url, code: result?.rule?.id }), {
2029
+ status: 403,
2030
+ headers: { "content-type": "text/html; charset=utf-8" }
2031
+ });
2032
+ }
1660
2033
  return new Response(JSON.stringify(blockBody(result)), {
1661
2034
  status: 403,
1662
2035
  headers: { "content-type": "application/json" }