@patchstack/connect 0.5.6 → 0.5.8
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/AGENT-INSTALL.md +2 -2
- package/README.md +4 -4
- package/dist/chunk-4OHAYSV2.js +295 -0
- package/dist/chunk-4OHAYSV2.js.map +1 -0
- package/dist/cli.js +1089 -470
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +450 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +461 -39
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +1 -1
- package/dist/protect.cjs +1226 -244
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +7 -2
- package/dist/protect.d.ts +7 -2
- package/dist/protect.edge.js +992 -179
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +846 -192
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2ALIJGZ3.js → refresh-manifest-IE6SPHG6.js} +168 -41
- package/dist/refresh-manifest-IE6SPHG6.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-IOODP4ZB.js +0 -104
- package/dist/chunk-IOODP4ZB.js.map +0 -1
- package/dist/refresh-manifest-2ALIJGZ3.js.map +0 -1
package/dist/protect.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canonicalBuildId,
|
|
3
|
+
isSafeOrigin,
|
|
3
4
|
pulseAuthHeader,
|
|
4
5
|
pulseFetch,
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
readBoundedJson,
|
|
7
|
+
readBuildStamp,
|
|
8
|
+
safeBaseUrl
|
|
9
|
+
} from "./chunk-4OHAYSV2.js";
|
|
7
10
|
|
|
8
11
|
// src/protect/client-ip.js
|
|
9
12
|
var ZONE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
@@ -219,27 +222,49 @@ function clientIpFields(resolved) {
|
|
|
219
222
|
return established && address !== null ? { client_ip: address, client_ip_source: resolved.source } : { client_ip_source: "unavailable" };
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
// src/protect/
|
|
223
|
-
|
|
225
|
+
// src/protect/engine/response-json.js
|
|
226
|
+
var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
227
|
+
async function readBoundedJson2(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
|
|
228
|
+
const stated = Number(response.headers?.get?.("content-length"));
|
|
229
|
+
if (Number.isFinite(stated) && stated > maxBytes) {
|
|
230
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
231
|
+
}
|
|
232
|
+
const stream = response.body;
|
|
233
|
+
if (!stream || typeof stream.getReader !== "function") {
|
|
234
|
+
if (typeof response.text === "function") {
|
|
235
|
+
const text = await response.text();
|
|
236
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
237
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
238
|
+
}
|
|
239
|
+
return JSON.parse(text);
|
|
240
|
+
}
|
|
241
|
+
throw new Error("rule response body cannot be read with a size bound");
|
|
242
|
+
}
|
|
243
|
+
const reader = stream.getReader();
|
|
244
|
+
const chunks = [];
|
|
245
|
+
let total = 0;
|
|
224
246
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
for (; ; ) {
|
|
248
|
+
const { done, value } = await reader.read();
|
|
249
|
+
if (done) break;
|
|
250
|
+
total += value.byteLength;
|
|
251
|
+
if (total > maxBytes) {
|
|
252
|
+
void reader.cancel().catch(() => {
|
|
253
|
+
});
|
|
254
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
255
|
+
}
|
|
256
|
+
chunks.push(value);
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
reader.releaseLock();
|
|
230
260
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
var warned = /* @__PURE__ */ new Set();
|
|
239
|
-
function warnOnce(key, message) {
|
|
240
|
-
if (warned.has(key)) return;
|
|
241
|
-
warned.add(key);
|
|
242
|
-
console.warn(message);
|
|
261
|
+
const bytes = new Uint8Array(total);
|
|
262
|
+
let offset = 0;
|
|
263
|
+
for (const chunk of chunks) {
|
|
264
|
+
bytes.set(chunk, offset);
|
|
265
|
+
offset += chunk.byteLength;
|
|
266
|
+
}
|
|
267
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
// src/protect/engine/client.js
|
|
@@ -298,7 +323,7 @@ var PatchstackRuleClient = class {
|
|
|
298
323
|
whitelist_keys: {}
|
|
299
324
|
};
|
|
300
325
|
}
|
|
301
|
-
const data = await response
|
|
326
|
+
const data = await readBoundedJson2(response);
|
|
302
327
|
if (!data || !Array.isArray(data.firewall)) {
|
|
303
328
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
304
329
|
}
|
|
@@ -335,6 +360,22 @@ var PatchstackRuleClient = class {
|
|
|
335
360
|
}
|
|
336
361
|
};
|
|
337
362
|
|
|
363
|
+
// src/protect/engine/own.js
|
|
364
|
+
function setOwn(target, key, value) {
|
|
365
|
+
Object.defineProperty(target, key, {
|
|
366
|
+
configurable: true,
|
|
367
|
+
enumerable: true,
|
|
368
|
+
writable: true,
|
|
369
|
+
value
|
|
370
|
+
});
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
function appendOwn(target, key, value) {
|
|
374
|
+
if (!Object.hasOwn(target, key)) return setOwn(target, key, value);
|
|
375
|
+
const previous = target[key];
|
|
376
|
+
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
377
|
+
}
|
|
378
|
+
|
|
338
379
|
// src/protect/engine/request.js
|
|
339
380
|
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
340
381
|
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
@@ -453,8 +494,8 @@ var RequestResolver = class {
|
|
|
453
494
|
}
|
|
454
495
|
if (key.startsWith("header.")) {
|
|
455
496
|
const name = key.slice("header.".length).toLowerCase();
|
|
456
|
-
const
|
|
457
|
-
return
|
|
497
|
+
const headers = resp.headers ?? {};
|
|
498
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
458
499
|
}
|
|
459
500
|
return [];
|
|
460
501
|
}
|
|
@@ -549,7 +590,7 @@ var RequestResolver = class {
|
|
|
549
590
|
...this.#resolveWildcard(cookies, key)
|
|
550
591
|
];
|
|
551
592
|
}
|
|
552
|
-
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? cookies[key];
|
|
593
|
+
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? (Object.hasOwn(cookies, key) ? cookies[key] : void 0);
|
|
553
594
|
return value !== void 0 ? [value] : [];
|
|
554
595
|
}
|
|
555
596
|
#resolveCookie(key) {
|
|
@@ -557,8 +598,7 @@ var RequestResolver = class {
|
|
|
557
598
|
if (key.endsWith("*")) {
|
|
558
599
|
return this.#resolveWildcard(cookies, key);
|
|
559
600
|
}
|
|
560
|
-
|
|
561
|
-
return value !== void 0 ? [value] : [];
|
|
601
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
562
602
|
}
|
|
563
603
|
#resolveServer(key) {
|
|
564
604
|
const req = this.#req;
|
|
@@ -734,7 +774,7 @@ var RequestResolver = class {
|
|
|
734
774
|
}
|
|
735
775
|
const name = pair.substring(0, eqIndex).trim();
|
|
736
776
|
const value = pair.substring(eqIndex + 1).trim();
|
|
737
|
-
cookies
|
|
777
|
+
setOwn(cookies, name, value);
|
|
738
778
|
}
|
|
739
779
|
this.#cookies = cookies;
|
|
740
780
|
return this.#cookies;
|
|
@@ -749,17 +789,17 @@ function notify(fn, arg, label) {
|
|
|
749
789
|
const result = fn(arg);
|
|
750
790
|
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
751
791
|
try {
|
|
752
|
-
result.then(void 0, (err) =>
|
|
792
|
+
result.then(void 0, (err) => warnOnce(label, err));
|
|
753
793
|
} catch {
|
|
754
794
|
}
|
|
755
795
|
}
|
|
756
796
|
return true;
|
|
757
797
|
} catch (err) {
|
|
758
|
-
|
|
798
|
+
warnOnce(label, err);
|
|
759
799
|
return false;
|
|
760
800
|
}
|
|
761
801
|
}
|
|
762
|
-
function
|
|
802
|
+
function warnOnce(label, err) {
|
|
763
803
|
if (reported.has(label)) return;
|
|
764
804
|
reported.add(label);
|
|
765
805
|
try {
|
|
@@ -891,30 +931,113 @@ function normalizeWhitespace(value) {
|
|
|
891
931
|
result = result.replace(/ {2,}/g, " ");
|
|
892
932
|
return result;
|
|
893
933
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
|
|
934
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
935
|
+
var MAX_RAW_NODES = 25e3;
|
|
936
|
+
var MAX_RAW_DEPTH = 200;
|
|
937
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
938
|
+
function serializeForRawDetection(body) {
|
|
939
|
+
if (body === null || body === void 0) return "";
|
|
940
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
941
|
+
const visited = /* @__PURE__ */ new Set();
|
|
942
|
+
const output = [];
|
|
943
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
944
|
+
let chars = 0;
|
|
945
|
+
let nodes = 0;
|
|
946
|
+
const append = (text) => {
|
|
947
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
948
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
949
|
+
output.push(piece);
|
|
950
|
+
chars += piece.length;
|
|
951
|
+
};
|
|
952
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
953
|
+
const task = stack.pop();
|
|
954
|
+
if (task.kind === "text") {
|
|
955
|
+
append(task.text);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (task.kind === "key") {
|
|
959
|
+
append(boundedJsonString(task.value));
|
|
960
|
+
append(":");
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
nodes++;
|
|
964
|
+
if (nodes > MAX_RAW_NODES) {
|
|
965
|
+
append('"[NodeLimit]"');
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const value = task.value;
|
|
969
|
+
if (value === null || value === void 0) {
|
|
970
|
+
append(task.root ? "" : "null");
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (typeof value === "string") {
|
|
974
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
if (typeof value !== "object") {
|
|
978
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (visited.has(value)) {
|
|
982
|
+
append("[Circular]");
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
986
|
+
append('"[DepthLimit]"');
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
visited.add(value);
|
|
990
|
+
if (Array.isArray(value)) {
|
|
991
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
992
|
+
const items = [];
|
|
993
|
+
for (let i = 0; i < length2; i++) {
|
|
994
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
995
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
996
|
+
}
|
|
997
|
+
if (value.length > length2) {
|
|
998
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
999
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
1000
|
+
}
|
|
1001
|
+
stack.push({ kind: "text", text: "]" });
|
|
1002
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
1003
|
+
stack.push({ kind: "text", text: "[" });
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
let keys;
|
|
1007
|
+
try {
|
|
1008
|
+
keys = Object.getOwnPropertyNames(value);
|
|
1009
|
+
} catch {
|
|
1010
|
+
append('"[Unreadable]"');
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
1014
|
+
const entries = [];
|
|
1015
|
+
for (let i = 0; i < length; i++) {
|
|
1016
|
+
const key = keys[i];
|
|
1017
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
1018
|
+
entries.push({ kind: "key", value: key });
|
|
1019
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
1020
|
+
}
|
|
1021
|
+
if (keys.length > length) {
|
|
1022
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
1023
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
1024
|
+
}
|
|
1025
|
+
stack.push({ kind: "text", text: "}" });
|
|
1026
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
1027
|
+
stack.push({ kind: "text", text: "{" });
|
|
906
1028
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1029
|
+
return output.join("");
|
|
1030
|
+
}
|
|
1031
|
+
function boundedJsonString(value) {
|
|
1032
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
1033
|
+
}
|
|
1034
|
+
function readOwnValue(value, key) {
|
|
1035
|
+
try {
|
|
1036
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1037
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
1038
|
+
} catch {
|
|
1039
|
+
return "[Unreadable]";
|
|
911
1040
|
}
|
|
912
|
-
const keys = Object.getOwnPropertyNames(body);
|
|
913
|
-
const parts = keys.map((key) => {
|
|
914
|
-
const val = serializeForRawDetection(body[key], visited, false);
|
|
915
|
-
return JSON.stringify(key) + ":" + val;
|
|
916
|
-
});
|
|
917
|
-
return "{" + parts.join(",") + "}";
|
|
918
1041
|
}
|
|
919
1042
|
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
920
1043
|
function requestField(req, key) {
|
|
@@ -956,7 +1079,7 @@ function normalizeObject(value, options = {}, depth = 0) {
|
|
|
956
1079
|
if (typeof value === "object" && value !== null) {
|
|
957
1080
|
const result = {};
|
|
958
1081
|
for (const [key, val] of Object.entries(value)) {
|
|
959
|
-
result
|
|
1082
|
+
setOwn(result, key, normalizeObject(val, options, depth + 1));
|
|
960
1083
|
}
|
|
961
1084
|
return result;
|
|
962
1085
|
}
|
|
@@ -973,6 +1096,111 @@ var REDOS_PATTERNS = [
|
|
|
973
1096
|
new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
|
|
974
1097
|
// nested alternation under a quantifier
|
|
975
1098
|
];
|
|
1099
|
+
function hasAdjacentUnbounded(body, flags) {
|
|
1100
|
+
let previousUnboundedAtom = null;
|
|
1101
|
+
for (let i = 0; i < body.length; ) {
|
|
1102
|
+
const start = i;
|
|
1103
|
+
const ch = body[i];
|
|
1104
|
+
let assertion = false;
|
|
1105
|
+
let literal = null;
|
|
1106
|
+
let atom = ch;
|
|
1107
|
+
if (ch === "\\") {
|
|
1108
|
+
const escaped = body[i + 1] ?? "";
|
|
1109
|
+
assertion = /[bBAZz]/.test(escaped);
|
|
1110
|
+
if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
|
|
1111
|
+
i += Math.min(2, body.length - i);
|
|
1112
|
+
atom = body.slice(start, i);
|
|
1113
|
+
} else if (ch === "[") {
|
|
1114
|
+
i++;
|
|
1115
|
+
while (i < body.length) {
|
|
1116
|
+
if (body[i] === "\\") i += 2;
|
|
1117
|
+
else if (body[i++] === "]") break;
|
|
1118
|
+
}
|
|
1119
|
+
atom = body.slice(start, i);
|
|
1120
|
+
} else if (ch === ")") {
|
|
1121
|
+
i++;
|
|
1122
|
+
atom = ".";
|
|
1123
|
+
} else if (ch === "(") {
|
|
1124
|
+
const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
|
|
1125
|
+
if (lookaround) {
|
|
1126
|
+
i = groupEnd(body, i);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
i++;
|
|
1130
|
+
if (body.startsWith("?:", i)) i += 2;
|
|
1131
|
+
else if (body.startsWith("?<", i)) {
|
|
1132
|
+
const nameEnd = body.indexOf(">", i + 2);
|
|
1133
|
+
if (nameEnd !== -1) i = nameEnd + 1;
|
|
1134
|
+
}
|
|
1135
|
+
continue;
|
|
1136
|
+
} else if (ch === "|") {
|
|
1137
|
+
previousUnboundedAtom = null;
|
|
1138
|
+
i++;
|
|
1139
|
+
continue;
|
|
1140
|
+
} else if ("^$".includes(ch)) {
|
|
1141
|
+
i++;
|
|
1142
|
+
continue;
|
|
1143
|
+
} else if ("?*+{}".includes(ch)) {
|
|
1144
|
+
previousUnboundedAtom = null;
|
|
1145
|
+
i++;
|
|
1146
|
+
continue;
|
|
1147
|
+
} else {
|
|
1148
|
+
if (ch !== ".") literal = ch;
|
|
1149
|
+
i++;
|
|
1150
|
+
}
|
|
1151
|
+
if (assertion) continue;
|
|
1152
|
+
let unbounded = false;
|
|
1153
|
+
let mayBeEmpty = false;
|
|
1154
|
+
if (body[i] === "*" || body[i] === "+") {
|
|
1155
|
+
unbounded = true;
|
|
1156
|
+
mayBeEmpty = body[i] === "*";
|
|
1157
|
+
i++;
|
|
1158
|
+
} else if (body[i] === "{") {
|
|
1159
|
+
const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
|
|
1160
|
+
if (quantifier) {
|
|
1161
|
+
unbounded = quantifier[2] === "";
|
|
1162
|
+
mayBeEmpty = Number(quantifier[1]) === 0;
|
|
1163
|
+
i += quantifier[0].length;
|
|
1164
|
+
}
|
|
1165
|
+
} else if (body[i] === "?") {
|
|
1166
|
+
mayBeEmpty = true;
|
|
1167
|
+
i++;
|
|
1168
|
+
}
|
|
1169
|
+
if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
|
|
1170
|
+
if (unbounded && previousUnboundedAtom !== null) return true;
|
|
1171
|
+
if (unbounded) {
|
|
1172
|
+
previousUnboundedAtom = atom;
|
|
1173
|
+
} else if (previousUnboundedAtom !== null && !mayBeEmpty) {
|
|
1174
|
+
try {
|
|
1175
|
+
const atomFlags = flags.replace(/[^iu]/g, "");
|
|
1176
|
+
const complementaryClass = previousUnboundedAtom === "\\s" && atom === "\\S" || previousUnboundedAtom === "\\S" && atom === "\\s" || previousUnboundedAtom === "\\d" && atom === "\\D" || previousUnboundedAtom === "\\D" && atom === "\\d" || previousUnboundedAtom === "\\w" && atom === "\\W" || previousUnboundedAtom === "\\W" && atom === "\\w";
|
|
1177
|
+
const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
|
|
1178
|
+
if (complementaryClass || excludedLiteral) {
|
|
1179
|
+
previousUnboundedAtom = null;
|
|
1180
|
+
}
|
|
1181
|
+
} catch {
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (i === start) i++;
|
|
1185
|
+
}
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
function groupEnd(body, start) {
|
|
1189
|
+
let depth = 0;
|
|
1190
|
+
let inClass = false;
|
|
1191
|
+
for (let i = start; i < body.length; i++) {
|
|
1192
|
+
const ch = body[i];
|
|
1193
|
+
if (ch === "\\") {
|
|
1194
|
+
i++;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (ch === "[") inClass = true;
|
|
1198
|
+
else if (ch === "]") inClass = false;
|
|
1199
|
+
else if (!inClass && ch === "(") depth++;
|
|
1200
|
+
else if (!inClass && ch === ")" && --depth === 0) return i + 1;
|
|
1201
|
+
}
|
|
1202
|
+
return body.length;
|
|
1203
|
+
}
|
|
976
1204
|
var warnedRejectedPatterns = /* @__PURE__ */ new Set();
|
|
977
1205
|
function warnRejectedPatternOnce(pattern) {
|
|
978
1206
|
const key = String(pattern);
|
|
@@ -990,15 +1218,14 @@ function safeRegExp(pattern) {
|
|
|
990
1218
|
if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
|
|
991
1219
|
return null;
|
|
992
1220
|
}
|
|
993
|
-
for (const dangerous of REDOS_PATTERNS) {
|
|
994
|
-
if (dangerous.test(pattern)) {
|
|
995
|
-
return null;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
1221
|
const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
|
|
999
1222
|
if (!match) {
|
|
1000
1223
|
return null;
|
|
1001
1224
|
}
|
|
1225
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
1226
|
+
if (dangerous.test(match[1])) return null;
|
|
1227
|
+
}
|
|
1228
|
+
if (hasAdjacentUnbounded(match[1], match[2])) return null;
|
|
1002
1229
|
try {
|
|
1003
1230
|
return new RegExp(match[1], match[2]);
|
|
1004
1231
|
} catch {
|
|
@@ -1069,7 +1296,7 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
|
1069
1296
|
return out;
|
|
1070
1297
|
}
|
|
1071
1298
|
var warnedKeys = /* @__PURE__ */ new Set();
|
|
1072
|
-
function
|
|
1299
|
+
function warnOnce2(key, message) {
|
|
1073
1300
|
if (warnedKeys.has(key)) return;
|
|
1074
1301
|
warnedKeys.add(key);
|
|
1075
1302
|
console.warn(message);
|
|
@@ -1451,13 +1678,13 @@ var RuleEngine = class {
|
|
|
1451
1678
|
this.#whitelistKeys = whitelist_keys;
|
|
1452
1679
|
this.#onError = onError;
|
|
1453
1680
|
if (Array.isArray(whitelists) && whitelists.some((w) => w && Array.isArray(w.rule_v2) && !w.rule_id)) {
|
|
1454
|
-
|
|
1681
|
+
warnOnce2(
|
|
1455
1682
|
"whitelist-no-rule-id",
|
|
1456
1683
|
"[patchstack] a whitelist has no `rule_id` \u2014 it suppresses ALL rules when it matches. Scope each whitelist to a specific rule_id, and key it only on values an attacker cannot set."
|
|
1457
1684
|
);
|
|
1458
1685
|
}
|
|
1459
1686
|
if (whitelist_keys && typeof whitelist_keys === "object" && Object.keys(whitelist_keys).length > 0) {
|
|
1460
|
-
|
|
1687
|
+
warnOnce2("whitelist-keys-unimplemented", "[patchstack] `whitelist_keys` is not implemented and has no effect.");
|
|
1461
1688
|
}
|
|
1462
1689
|
}
|
|
1463
1690
|
// A mitigation engine must never take down the app it protects: any error while
|
|
@@ -1742,6 +1969,16 @@ function operandShapeProblem(shape, value) {
|
|
|
1742
1969
|
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
1743
1970
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
1744
1971
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
1972
|
+
const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
|
|
1973
|
+
const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
|
|
1974
|
+
const tooLong = stringLimit(value);
|
|
1975
|
+
if (tooLong) return tooLong;
|
|
1976
|
+
const tooMany = listLimit(value);
|
|
1977
|
+
if (tooMany) return tooMany;
|
|
1978
|
+
if (Array.isArray(value)) {
|
|
1979
|
+
const longMember = value.find((member) => stringLimit(member) !== null);
|
|
1980
|
+
if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
1981
|
+
}
|
|
1745
1982
|
switch (shape) {
|
|
1746
1983
|
case "positive-number":
|
|
1747
1984
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
@@ -1772,8 +2009,11 @@ function operandShapeProblem(shape, value) {
|
|
|
1772
2009
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
1773
2010
|
const names = Object.keys(value);
|
|
1774
2011
|
if (names.length === 0) return "must be an object with at least one entry";
|
|
2012
|
+
if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
|
|
1775
2013
|
const bad = names.find((name) => !isHeaderName(name));
|
|
1776
|
-
|
|
2014
|
+
if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
2015
|
+
const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
|
|
2016
|
+
return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
|
|
1777
2017
|
}
|
|
1778
2018
|
case "header-name-list": {
|
|
1779
2019
|
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
@@ -1919,9 +2159,16 @@ var LIMITS = Object.freeze({
|
|
|
1919
2159
|
maxRules: 5e3,
|
|
1920
2160
|
maxWhitelists: 2e3,
|
|
1921
2161
|
maxConditionsPerRule: 250,
|
|
2162
|
+
maxConditionNodesPerRule: 1e3,
|
|
2163
|
+
maxConditionNodesPerBundle: 25e3,
|
|
1922
2164
|
maxNestingDepth: 12,
|
|
1923
2165
|
maxRegexLength: 1e3,
|
|
1924
|
-
maxValueLength: 8192
|
|
2166
|
+
maxValueLength: 8192,
|
|
2167
|
+
maxParameterLength: 512,
|
|
2168
|
+
maxParameterItems: 64,
|
|
2169
|
+
maxOperandItems: 256,
|
|
2170
|
+
maxMapEntries: 128,
|
|
2171
|
+
maxPathSegments: 64
|
|
1925
2172
|
});
|
|
1926
2173
|
var GROUP_PARAMETER = "rules";
|
|
1927
2174
|
function isGroup(condition) {
|
|
@@ -1948,6 +2195,9 @@ function parameterProblem(parameter) {
|
|
|
1948
2195
|
if (parameter === void 0 || parameter === null) return null;
|
|
1949
2196
|
if (Array.isArray(parameter)) {
|
|
1950
2197
|
if (parameter.length === 0) return "parameter list is empty";
|
|
2198
|
+
if (parameter.length > LIMITS.maxParameterItems) {
|
|
2199
|
+
return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
|
|
2200
|
+
}
|
|
1951
2201
|
for (const member of parameter) {
|
|
1952
2202
|
if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
|
|
1953
2203
|
const problem = parameterProblem(member);
|
|
@@ -1956,6 +2206,9 @@ function parameterProblem(parameter) {
|
|
|
1956
2206
|
return null;
|
|
1957
2207
|
}
|
|
1958
2208
|
if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
|
|
2209
|
+
if (parameter.length > LIMITS.maxParameterLength) {
|
|
2210
|
+
return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
|
|
2211
|
+
}
|
|
1959
2212
|
if (parameter === GROUP_PARAMETER) {
|
|
1960
2213
|
return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
|
|
1961
2214
|
}
|
|
@@ -1991,6 +2244,20 @@ function matchProblem(match, parameter) {
|
|
|
1991
2244
|
if (!match || typeof match !== "object") return "condition has no match object";
|
|
1992
2245
|
if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
|
|
1993
2246
|
if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
|
|
2247
|
+
for (const operand of ["value", "key", "claim"]) {
|
|
2248
|
+
const value = match[operand];
|
|
2249
|
+
if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
|
|
2250
|
+
return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
2251
|
+
}
|
|
2252
|
+
if (Array.isArray(value)) {
|
|
2253
|
+
if (value.length > LIMITS.maxOperandItems) {
|
|
2254
|
+
return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
|
|
2255
|
+
}
|
|
2256
|
+
if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
|
|
2257
|
+
return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
1994
2261
|
const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
|
|
1995
2262
|
if (!parameterless && (parameter === void 0 || parameter === null)) {
|
|
1996
2263
|
return `match type "${match.type}" needs a parameter`;
|
|
@@ -2013,11 +2280,20 @@ function matchProblem(match, parameter) {
|
|
|
2013
2280
|
} else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
|
|
2014
2281
|
return `match type "${match.type}" needs a value`;
|
|
2015
2282
|
}
|
|
2283
|
+
if (match.type === "array_key_value") {
|
|
2284
|
+
const paths = Array.isArray(match.key) ? match.key : [match.key];
|
|
2285
|
+
if (paths.some((path) => typeof path === "string" && path.split(".").length > LIMITS.maxPathSegments)) {
|
|
2286
|
+
return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2016
2289
|
return null;
|
|
2017
2290
|
}
|
|
2018
2291
|
function mutationsProblem(mutations) {
|
|
2019
2292
|
if (mutations === void 0 || mutations === null) return null;
|
|
2020
2293
|
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
2294
|
+
if (mutations.length > LIMITS.maxOperandItems) {
|
|
2295
|
+
return `mutations has more than ${LIMITS.maxOperandItems} entries`;
|
|
2296
|
+
}
|
|
2021
2297
|
for (const mutation of mutations) {
|
|
2022
2298
|
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
2023
2299
|
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
@@ -2037,10 +2313,19 @@ function whenProblem(when) {
|
|
|
2037
2313
|
if ("path" in when && !isNonEmptyString(when.path)) {
|
|
2038
2314
|
return "when.path must be a non-empty string";
|
|
2039
2315
|
}
|
|
2316
|
+
if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
|
|
2317
|
+
return `when.path is longer than ${LIMITS.maxValueLength} chars`;
|
|
2318
|
+
}
|
|
2040
2319
|
if ("method" in when) {
|
|
2041
2320
|
const methods = Array.isArray(when.method) ? when.method : [when.method];
|
|
2042
2321
|
if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
|
|
2322
|
+
if (methods.length > LIMITS.maxParameterItems) {
|
|
2323
|
+
return `when.method has more than ${LIMITS.maxParameterItems} entries`;
|
|
2324
|
+
}
|
|
2043
2325
|
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
2326
|
+
if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
|
|
2327
|
+
return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
|
|
2328
|
+
}
|
|
2044
2329
|
}
|
|
2045
2330
|
return null;
|
|
2046
2331
|
}
|
|
@@ -2064,6 +2349,11 @@ function rulePropertyProblem(rule) {
|
|
|
2064
2349
|
const problem = operandShapeProblem(shape, rule[property]);
|
|
2065
2350
|
if (problem) return `"${property}" ${problem}`;
|
|
2066
2351
|
}
|
|
2352
|
+
for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
|
|
2353
|
+
if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
|
|
2354
|
+
return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2067
2357
|
return null;
|
|
2068
2358
|
}
|
|
2069
2359
|
function actionProblem(action, rule) {
|
|
@@ -2093,46 +2383,76 @@ var PHASES2 = new Set(PHASES);
|
|
|
2093
2383
|
var ACTIONS2 = new Set(ACTIONS);
|
|
2094
2384
|
function validateBundle(bundle, opts = {}) {
|
|
2095
2385
|
const rejected = [];
|
|
2386
|
+
let omittedRejections = 0;
|
|
2387
|
+
const reject = (entry) => {
|
|
2388
|
+
if (rejected.length < 100) rejected.push(entry);
|
|
2389
|
+
else omittedRejections++;
|
|
2390
|
+
};
|
|
2096
2391
|
const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
|
|
2097
2392
|
const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
|
|
2393
|
+
const bundleBudget = { count: 0 };
|
|
2098
2394
|
const firewall = [];
|
|
2099
|
-
for (const rule of inFirewall) {
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
continue;
|
|
2103
|
-
}
|
|
2104
|
-
const reason = enforceableRuleProblem(rule);
|
|
2105
|
-
if (reason) rejected.push({ id: idOf(rule), reason });
|
|
2395
|
+
for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
|
|
2396
|
+
const reason = enforceableRuleProblem(rule, bundleBudget);
|
|
2397
|
+
if (reason) reject({ id: idOf(rule), reason });
|
|
2106
2398
|
else firewall.push(rule);
|
|
2107
2399
|
}
|
|
2400
|
+
if (inFirewall.length > LIMITS2.maxRules) {
|
|
2401
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
|
|
2402
|
+
}
|
|
2108
2403
|
const whitelists = [];
|
|
2109
|
-
for (const wl of inWhitelists) {
|
|
2110
|
-
if (whitelists.length >= LIMITS2.maxWhitelists) {
|
|
2111
|
-
rejected.push({ id: idOf(wl), reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
2112
|
-
continue;
|
|
2113
|
-
}
|
|
2404
|
+
for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
|
|
2114
2405
|
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
2115
|
-
|
|
2406
|
+
reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
2116
2407
|
continue;
|
|
2117
2408
|
}
|
|
2118
2409
|
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
2119
|
-
|
|
2410
|
+
reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
2120
2411
|
continue;
|
|
2121
2412
|
}
|
|
2122
|
-
const reason = conditionsProblem(wl?.rule_v2);
|
|
2123
|
-
if (reason)
|
|
2413
|
+
const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
2414
|
+
if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
2124
2415
|
else whitelists.push(wl);
|
|
2125
2416
|
}
|
|
2417
|
+
if (inWhitelists.length > LIMITS2.maxWhitelists) {
|
|
2418
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
2419
|
+
}
|
|
2420
|
+
let whitelistKeys = {};
|
|
2421
|
+
if (bundle?.whitelist_keys !== void 0) {
|
|
2422
|
+
if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
|
|
2423
|
+
reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
|
|
2424
|
+
} else {
|
|
2425
|
+
const entries = Object.entries(bundle.whitelist_keys);
|
|
2426
|
+
if (entries.length > LIMITS2.maxMapEntries) {
|
|
2427
|
+
reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
|
|
2428
|
+
} else {
|
|
2429
|
+
const invalid = entries.find(([key, value]) => key.length > LIMITS2.maxParameterLength || !Array.isArray(value) || value.length > LIMITS2.maxOperandItems || value.some((entry) => typeof entry !== "string" || entry.length > LIMITS2.maxValueLength));
|
|
2430
|
+
if (invalid) {
|
|
2431
|
+
reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
|
|
2432
|
+
} else {
|
|
2433
|
+
whitelistKeys = bundle.whitelist_keys;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
if (omittedRejections > 0) {
|
|
2439
|
+
rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
|
|
2440
|
+
}
|
|
2126
2441
|
return {
|
|
2127
|
-
bundle: { ...bundle, firewall, whitelists },
|
|
2442
|
+
bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
|
|
2128
2443
|
rejected
|
|
2129
2444
|
};
|
|
2130
2445
|
}
|
|
2131
2446
|
function idOf(rule) {
|
|
2132
2447
|
const id = rule?.id ?? rule?.rule_id;
|
|
2133
|
-
|
|
2448
|
+
if (id === void 0 || id === null) return "(unidentified)";
|
|
2449
|
+
try {
|
|
2450
|
+
return String(id).slice(0, 128);
|
|
2451
|
+
} catch {
|
|
2452
|
+
return "(unidentified)";
|
|
2453
|
+
}
|
|
2134
2454
|
}
|
|
2135
|
-
function enforceableRuleProblem(rule) {
|
|
2455
|
+
function enforceableRuleProblem(rule, bundleBudget = null) {
|
|
2136
2456
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
2137
2457
|
const nullReason = nullPropertyProblem(rule);
|
|
2138
2458
|
if (nullReason) return nullReason;
|
|
@@ -2143,9 +2463,9 @@ function enforceableRuleProblem(rule) {
|
|
|
2143
2463
|
if (scopeReason) return scopeReason;
|
|
2144
2464
|
const propertyReason = rulePropertyProblem(rule);
|
|
2145
2465
|
if (propertyReason) return propertyReason;
|
|
2146
|
-
return conditionsProblem(rule.rule_v2);
|
|
2466
|
+
return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
2147
2467
|
}
|
|
2148
|
-
function conditionsProblem(conditions, depth = 0) {
|
|
2468
|
+
function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
|
|
2149
2469
|
if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
|
|
2150
2470
|
if (conditions.length === 0) return "rule_v2 is empty (would never match)";
|
|
2151
2471
|
if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
|
|
@@ -2153,10 +2473,20 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
2153
2473
|
return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
|
|
2154
2474
|
}
|
|
2155
2475
|
for (const c of conditions) {
|
|
2476
|
+
ruleBudget.count++;
|
|
2477
|
+
if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
|
|
2478
|
+
return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
|
|
2479
|
+
}
|
|
2480
|
+
if (bundleBudget) {
|
|
2481
|
+
bundleBudget.count++;
|
|
2482
|
+
if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
|
|
2483
|
+
return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2156
2486
|
const shapeReason = conditionShapeProblem(c);
|
|
2157
2487
|
if (shapeReason) return shapeReason;
|
|
2158
2488
|
if (isGroup(c)) {
|
|
2159
|
-
const nested = conditionsProblem(c.rules, depth + 1);
|
|
2489
|
+
const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
|
|
2160
2490
|
if (nested) return nested;
|
|
2161
2491
|
continue;
|
|
2162
2492
|
}
|
|
@@ -2170,11 +2500,17 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
2170
2500
|
if (m.type === "regex") {
|
|
2171
2501
|
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
2172
2502
|
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
2503
|
+
if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
|
|
2173
2504
|
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
2174
2505
|
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
2175
2506
|
}
|
|
2176
2507
|
if (m.match) {
|
|
2177
|
-
const nested = conditionsProblem(
|
|
2508
|
+
const nested = conditionsProblem(
|
|
2509
|
+
[{ parameter: c.parameter, match: m.match }],
|
|
2510
|
+
depth + 1,
|
|
2511
|
+
ruleBudget,
|
|
2512
|
+
bundleBudget
|
|
2513
|
+
);
|
|
2178
2514
|
if (nested) return nested;
|
|
2179
2515
|
}
|
|
2180
2516
|
}
|
|
@@ -2392,6 +2728,7 @@ var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
|
2392
2728
|
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
2393
2729
|
var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
|
|
2394
2730
|
var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
|
|
2731
|
+
var PER_RULE_DRY_RUN_HEADER = "X-Patchstack-Per-Rule-Dry-Run";
|
|
2395
2732
|
var JITTER_FRACTION2 = 0.1;
|
|
2396
2733
|
var PulseRuleClient = class {
|
|
2397
2734
|
#siteUuid;
|
|
@@ -2436,6 +2773,9 @@ var PulseRuleClient = class {
|
|
|
2436
2773
|
if (this.#buildId !== null && typeof auth.Authorization === "string") {
|
|
2437
2774
|
headers["X-Patchstack-Build"] = this.#buildId;
|
|
2438
2775
|
}
|
|
2776
|
+
if (typeof auth.Authorization === "string") {
|
|
2777
|
+
headers[PER_RULE_DRY_RUN_HEADER] = "1";
|
|
2778
|
+
}
|
|
2439
2779
|
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
2440
2780
|
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
2441
2781
|
if (response.status === 304) {
|
|
@@ -2454,7 +2794,7 @@ var PulseRuleClient = class {
|
|
|
2454
2794
|
if (!response.ok) {
|
|
2455
2795
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2456
2796
|
}
|
|
2457
|
-
const data = await response
|
|
2797
|
+
const data = await readBoundedJson2(response);
|
|
2458
2798
|
if (!data || !Array.isArray(data.firewall)) {
|
|
2459
2799
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2460
2800
|
}
|
|
@@ -2515,19 +2855,18 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2515
2855
|
const method = (request.method || "GET").toUpperCase();
|
|
2516
2856
|
const headers = {};
|
|
2517
2857
|
request.headers.forEach((value, key) => {
|
|
2518
|
-
headers
|
|
2858
|
+
setOwn(headers, key.toLowerCase(), value);
|
|
2519
2859
|
});
|
|
2520
2860
|
const query = {};
|
|
2521
2861
|
for (const [key, value] of url.searchParams) {
|
|
2522
|
-
|
|
2523
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
2524
|
-
} else {
|
|
2525
|
-
query[key] = value;
|
|
2526
|
-
}
|
|
2862
|
+
appendOwn(query, key, value);
|
|
2527
2863
|
}
|
|
2528
2864
|
let rawBody = "";
|
|
2865
|
+
let bodyInspectionSkip = null;
|
|
2529
2866
|
if (method !== "GET" && method !== "HEAD") {
|
|
2530
|
-
|
|
2867
|
+
const read = await readCappedText(request, maxBodyBytes);
|
|
2868
|
+
rawBody = read.text;
|
|
2869
|
+
bodyInspectionSkip = read.skip;
|
|
2531
2870
|
}
|
|
2532
2871
|
const contentType = headers["content-type"] || "";
|
|
2533
2872
|
let body = {};
|
|
@@ -2554,7 +2893,8 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2554
2893
|
cookies: parseCookies(headers.cookie),
|
|
2555
2894
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
2556
2895
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
2557
|
-
_rawBody: rawBody
|
|
2896
|
+
_rawBody: rawBody,
|
|
2897
|
+
...bodyInspectionSkip === null ? {} : { _bodyInspectionSkip: bodyInspectionSkip }
|
|
2558
2898
|
};
|
|
2559
2899
|
}
|
|
2560
2900
|
async function readCappedText(request, max) {
|
|
@@ -2562,7 +2902,7 @@ async function readCappedText(request, max) {
|
|
|
2562
2902
|
try {
|
|
2563
2903
|
clone = request.clone();
|
|
2564
2904
|
} catch {
|
|
2565
|
-
return "";
|
|
2905
|
+
return { text: "", skip: "clone-failed" };
|
|
2566
2906
|
}
|
|
2567
2907
|
const body = clone.body;
|
|
2568
2908
|
if (body && typeof body.getReader === "function") {
|
|
@@ -2573,23 +2913,30 @@ async function readCappedText(request, max) {
|
|
|
2573
2913
|
for (; ; ) {
|
|
2574
2914
|
const { done, value } = await reader.read();
|
|
2575
2915
|
if (done) break;
|
|
2576
|
-
if (!value
|
|
2916
|
+
if (!value) continue;
|
|
2577
2917
|
const take = Math.min(value.byteLength, max - buffered);
|
|
2578
|
-
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
2918
|
+
if (take > 0) chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
2579
2919
|
buffered += take;
|
|
2920
|
+
if (buffered >= max) {
|
|
2921
|
+
void reader.cancel().catch(() => {
|
|
2922
|
+
});
|
|
2923
|
+
return { text: decodeChunks(chunks, buffered), skip: "body-cap" };
|
|
2924
|
+
}
|
|
2580
2925
|
}
|
|
2581
2926
|
} catch {
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
return "";
|
|
2927
|
+
void reader.cancel().catch(() => {
|
|
2928
|
+
});
|
|
2929
|
+
return { text: "", skip: "read-failed" };
|
|
2930
|
+
} finally {
|
|
2931
|
+
reader.releaseLock();
|
|
2588
2932
|
}
|
|
2933
|
+
return { text: decodeChunks(chunks, buffered), skip: null };
|
|
2589
2934
|
}
|
|
2935
|
+
return { text: "", skip: "unreadable-body" };
|
|
2936
|
+
}
|
|
2937
|
+
function decodeChunks(chunks, total) {
|
|
2590
2938
|
try {
|
|
2591
|
-
|
|
2592
|
-
return text.length > max ? text.slice(0, max) : text;
|
|
2939
|
+
return new TextDecoder().decode(concatChunks(chunks, total));
|
|
2593
2940
|
} catch {
|
|
2594
2941
|
return "";
|
|
2595
2942
|
}
|
|
@@ -2616,7 +2963,7 @@ function parseBody(rawBody, contentType) {
|
|
|
2616
2963
|
}
|
|
2617
2964
|
if (isForm) {
|
|
2618
2965
|
const body = {};
|
|
2619
|
-
for (const [k, v] of new URLSearchParams(rawBody)) body
|
|
2966
|
+
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
2620
2967
|
return { body, files: void 0 };
|
|
2621
2968
|
}
|
|
2622
2969
|
if (isJson || isAmbiguous) {
|
|
@@ -2644,9 +2991,9 @@ function parseMultipart(rawBody, boundary) {
|
|
|
2644
2991
|
if (filename !== void 0) {
|
|
2645
2992
|
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
2646
2993
|
const file = { filename, type: partType, content };
|
|
2647
|
-
files
|
|
2994
|
+
appendOwn(files, name, file);
|
|
2648
2995
|
} else {
|
|
2649
|
-
body
|
|
2996
|
+
appendOwn(body, name, content);
|
|
2650
2997
|
}
|
|
2651
2998
|
}
|
|
2652
2999
|
return { body, files };
|
|
@@ -2661,7 +3008,7 @@ function parseCookies(header) {
|
|
|
2661
3008
|
if (idx === -1) {
|
|
2662
3009
|
continue;
|
|
2663
3010
|
}
|
|
2664
|
-
cookies
|
|
3011
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2665
3012
|
}
|
|
2666
3013
|
return cookies;
|
|
2667
3014
|
}
|
|
@@ -2671,7 +3018,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2671
3018
|
const method = (req.method || "GET").toUpperCase();
|
|
2672
3019
|
const headers = {};
|
|
2673
3020
|
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
2674
|
-
headers
|
|
3021
|
+
setOwn(headers, key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value);
|
|
2675
3022
|
}
|
|
2676
3023
|
const host = headers.host || "localhost";
|
|
2677
3024
|
let url;
|
|
@@ -2686,11 +3033,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2686
3033
|
}
|
|
2687
3034
|
const query = {};
|
|
2688
3035
|
for (const [key, value] of url.searchParams) {
|
|
2689
|
-
|
|
2690
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
2691
|
-
} else {
|
|
2692
|
-
query[key] = value;
|
|
2693
|
-
}
|
|
3036
|
+
appendOwn(query, key, value);
|
|
2694
3037
|
}
|
|
2695
3038
|
const contentType = headers["content-type"] || "";
|
|
2696
3039
|
let body = {};
|
|
@@ -2731,7 +3074,7 @@ function parseCookies2(header) {
|
|
|
2731
3074
|
if (idx === -1) {
|
|
2732
3075
|
continue;
|
|
2733
3076
|
}
|
|
2734
|
-
cookies
|
|
3077
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2735
3078
|
}
|
|
2736
3079
|
return cookies;
|
|
2737
3080
|
}
|
|
@@ -2787,6 +3130,67 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2787
3130
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
2788
3131
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
2789
3132
|
const MAX_REDIRECTS = 20;
|
|
3133
|
+
const MAX_REPLAY_BODY_BYTES = 1024 * 1024;
|
|
3134
|
+
const captureReplayBody = (request) => {
|
|
3135
|
+
if (request.method === "GET" || request.method === "HEAD" || !request.body) {
|
|
3136
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: false }), cancel() {
|
|
3137
|
+
} };
|
|
3138
|
+
}
|
|
3139
|
+
let reader;
|
|
3140
|
+
try {
|
|
3141
|
+
reader = request.clone().body?.getReader();
|
|
3142
|
+
} catch {
|
|
3143
|
+
reader = null;
|
|
3144
|
+
}
|
|
3145
|
+
if (!reader) {
|
|
3146
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: true }), cancel() {
|
|
3147
|
+
} };
|
|
3148
|
+
}
|
|
3149
|
+
let cancelled = false;
|
|
3150
|
+
const result = (async () => {
|
|
3151
|
+
const chunks = [];
|
|
3152
|
+
let total = 0;
|
|
3153
|
+
try {
|
|
3154
|
+
for (; ; ) {
|
|
3155
|
+
const { done, value } = await reader.read();
|
|
3156
|
+
if (done || cancelled) break;
|
|
3157
|
+
total += value.byteLength;
|
|
3158
|
+
if (total > MAX_REPLAY_BODY_BYTES) {
|
|
3159
|
+
void reader.cancel().catch(() => {
|
|
3160
|
+
});
|
|
3161
|
+
return { body: void 0, tooLarge: true };
|
|
3162
|
+
}
|
|
3163
|
+
chunks.push(value);
|
|
3164
|
+
}
|
|
3165
|
+
if (cancelled) return { body: void 0, tooLarge: false };
|
|
3166
|
+
const body = new Uint8Array(total);
|
|
3167
|
+
let offset = 0;
|
|
3168
|
+
for (const chunk of chunks) {
|
|
3169
|
+
body.set(chunk, offset);
|
|
3170
|
+
offset += chunk.byteLength;
|
|
3171
|
+
}
|
|
3172
|
+
return { body, tooLarge: false };
|
|
3173
|
+
} catch {
|
|
3174
|
+
return { body: void 0, tooLarge: true };
|
|
3175
|
+
} finally {
|
|
3176
|
+
reader.releaseLock();
|
|
3177
|
+
}
|
|
3178
|
+
})();
|
|
3179
|
+
return {
|
|
3180
|
+
result,
|
|
3181
|
+
cancel() {
|
|
3182
|
+
cancelled = true;
|
|
3183
|
+
void reader.cancel().catch(() => {
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
};
|
|
3187
|
+
};
|
|
3188
|
+
const discardResponseBody = async (response) => {
|
|
3189
|
+
try {
|
|
3190
|
+
await response?.body?.cancel?.();
|
|
3191
|
+
} catch {
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
2790
3194
|
const screenUrl = async (u, method) => {
|
|
2791
3195
|
let host = null;
|
|
2792
3196
|
try {
|
|
@@ -2812,20 +3216,43 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2812
3216
|
if (callerRedirect !== "follow") return originalFetch(input, init);
|
|
2813
3217
|
const headers = new Headers(cur.headers);
|
|
2814
3218
|
const signal = cur.signal;
|
|
2815
|
-
|
|
3219
|
+
const replay = captureReplayBody(cur);
|
|
3220
|
+
let body;
|
|
2816
3221
|
for (let hop = 0; ; hop++) {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3222
|
+
let resp;
|
|
3223
|
+
try {
|
|
3224
|
+
resp = await originalFetch(
|
|
3225
|
+
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
3226
|
+
);
|
|
3227
|
+
} catch (error) {
|
|
3228
|
+
replay.cancel();
|
|
3229
|
+
throw error;
|
|
3230
|
+
}
|
|
2820
3231
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
2821
|
-
if (!location)
|
|
2822
|
-
|
|
3232
|
+
if (!location) {
|
|
3233
|
+
replay.cancel();
|
|
3234
|
+
return resp;
|
|
3235
|
+
}
|
|
3236
|
+
await discardResponseBody(resp);
|
|
3237
|
+
if (hop >= MAX_REDIRECTS) {
|
|
3238
|
+
replay.cancel();
|
|
3239
|
+
throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
3240
|
+
}
|
|
2823
3241
|
const next = new URL(location, url).href;
|
|
2824
3242
|
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
2825
3243
|
method = "GET";
|
|
2826
3244
|
body = void 0;
|
|
3245
|
+
replay.cancel();
|
|
2827
3246
|
headers.delete("content-type");
|
|
2828
3247
|
headers.delete("content-length");
|
|
3248
|
+
} else if (body === void 0 && method !== "GET" && method !== "HEAD") {
|
|
3249
|
+
const captured = await replay.result;
|
|
3250
|
+
if (captured.tooLarge) {
|
|
3251
|
+
throw new Error(
|
|
3252
|
+
`Patchstack blocked a redirect that required replaying more than ${MAX_REPLAY_BODY_BYTES} bytes`
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
body = captured.body;
|
|
2829
3256
|
}
|
|
2830
3257
|
if (new URL(next).origin !== new URL(url).origin) {
|
|
2831
3258
|
headers.delete("authorization");
|
|
@@ -3136,7 +3563,7 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
3136
3563
|
//
|
|
3137
3564
|
// Accepts a real newline and a JSON-escaped one. Most traces reach a client inside a JSON error
|
|
3138
3565
|
// body, where the newline is the two characters `\` and `n`.
|
|
3139
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(?:\\n|\\\\n)\\s*at\\s
|
|
3566
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(?:\\n|\\\\n)\\s*at\\s+\\S[^\\r\\n(]*\\((?:node:|[A-Za-z]:[\\\\/])?[^:\\r\\n]+:\\d+:\\d+\\)/" } }]
|
|
3140
3567
|
},
|
|
3141
3568
|
{
|
|
3142
3569
|
id: "resp-sql-error",
|
|
@@ -3269,9 +3696,18 @@ async function loadFs() {
|
|
|
3269
3696
|
try {
|
|
3270
3697
|
const [fs, path] = await Promise.all([import("fs"), import("path")]);
|
|
3271
3698
|
fsMod = {
|
|
3699
|
+
chmodSync: fs.chmodSync,
|
|
3700
|
+
closeSync: fs.closeSync,
|
|
3701
|
+
constants: fs.constants,
|
|
3702
|
+
fstatSync: fs.fstatSync,
|
|
3703
|
+
fsyncSync: fs.fsyncSync,
|
|
3704
|
+
lstatSync: fs.lstatSync,
|
|
3705
|
+
openSync: fs.openSync,
|
|
3272
3706
|
readFileSync: fs.readFileSync,
|
|
3273
|
-
writeFileSync: fs.writeFileSync,
|
|
3274
3707
|
mkdirSync: fs.mkdirSync,
|
|
3708
|
+
renameSync: fs.renameSync,
|
|
3709
|
+
unlinkSync: fs.unlinkSync,
|
|
3710
|
+
writeFileSync: fs.writeFileSync,
|
|
3275
3711
|
join: path.join
|
|
3276
3712
|
};
|
|
3277
3713
|
} catch {
|
|
@@ -3324,22 +3760,84 @@ async function cacheWrite(dir, env) {
|
|
|
3324
3760
|
if (!dir) return;
|
|
3325
3761
|
const fs = await loadFs();
|
|
3326
3762
|
if (!fs) return;
|
|
3763
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
3764
|
+
const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
|
|
3765
|
+
let descriptor = null;
|
|
3327
3766
|
try {
|
|
3328
3767
|
fs.mkdirSync(dir, { recursive: true });
|
|
3329
|
-
fs.
|
|
3768
|
+
const directory = fs.lstatSync(dir);
|
|
3769
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return;
|
|
3770
|
+
let mode;
|
|
3771
|
+
try {
|
|
3772
|
+
const current = fs.lstatSync(target);
|
|
3773
|
+
if (current.isSymbolicLink() || !current.isFile()) return;
|
|
3774
|
+
mode = current.mode & 4095;
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
if (error?.code !== "ENOENT") return;
|
|
3777
|
+
}
|
|
3778
|
+
descriptor = fs.openSync(temporary, "wx", mode ?? 384);
|
|
3779
|
+
fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
|
|
3780
|
+
fs.fsyncSync(descriptor);
|
|
3781
|
+
fs.closeSync(descriptor);
|
|
3782
|
+
descriptor = null;
|
|
3783
|
+
if (mode !== void 0) fs.chmodSync(temporary, mode);
|
|
3784
|
+
try {
|
|
3785
|
+
fs.renameSync(temporary, target);
|
|
3786
|
+
} catch (error) {
|
|
3787
|
+
if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
|
|
3788
|
+
try {
|
|
3789
|
+
fs.unlinkSync(target);
|
|
3790
|
+
} catch (unlinkError) {
|
|
3791
|
+
if (unlinkError?.code !== "ENOENT") throw unlinkError;
|
|
3792
|
+
}
|
|
3793
|
+
fs.renameSync(temporary, target);
|
|
3794
|
+
}
|
|
3330
3795
|
} catch {
|
|
3796
|
+
} finally {
|
|
3797
|
+
if (descriptor !== null) {
|
|
3798
|
+
try {
|
|
3799
|
+
fs.closeSync(descriptor);
|
|
3800
|
+
} catch {
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
try {
|
|
3804
|
+
fs.unlinkSync(temporary);
|
|
3805
|
+
} catch {
|
|
3806
|
+
}
|
|
3331
3807
|
}
|
|
3332
3808
|
}
|
|
3333
3809
|
async function cacheRead(dir) {
|
|
3334
3810
|
if (!dir) return null;
|
|
3335
3811
|
const fs = await loadFs();
|
|
3336
3812
|
if (!fs) return null;
|
|
3813
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
3814
|
+
let descriptor = null;
|
|
3337
3815
|
try {
|
|
3338
|
-
|
|
3816
|
+
const entry = fs.lstatSync(target);
|
|
3817
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
|
|
3818
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
3819
|
+
descriptor = fs.openSync(target, flags);
|
|
3820
|
+
const opened = fs.fstatSync(descriptor);
|
|
3821
|
+
if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
|
|
3822
|
+
return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
|
|
3339
3823
|
} catch {
|
|
3340
3824
|
return null;
|
|
3825
|
+
} finally {
|
|
3826
|
+
if (descriptor !== null) {
|
|
3827
|
+
try {
|
|
3828
|
+
fs.closeSync(descriptor);
|
|
3829
|
+
} catch {
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3341
3832
|
}
|
|
3342
3833
|
}
|
|
3834
|
+
function randomSuffix() {
|
|
3835
|
+
try {
|
|
3836
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
3837
|
+
} catch {
|
|
3838
|
+
}
|
|
3839
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3840
|
+
}
|
|
3343
3841
|
function toEnvelope(value) {
|
|
3344
3842
|
if (!value || typeof value !== "object") return null;
|
|
3345
3843
|
if (value.bundle && typeof value.bundle === "object") {
|
|
@@ -3364,7 +3862,11 @@ function toEnvelope(value) {
|
|
|
3364
3862
|
function liveUpdateRejections(res, options) {
|
|
3365
3863
|
if (options.acceptPartialBundle) return [];
|
|
3366
3864
|
const { rejected } = validateBundle(
|
|
3367
|
-
{
|
|
3865
|
+
{
|
|
3866
|
+
firewall: Array.isArray(res.firewall) ? res.firewall : [],
|
|
3867
|
+
whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
|
|
3868
|
+
whitelist_keys: res.whitelist_keys
|
|
3869
|
+
},
|
|
3368
3870
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3369
3871
|
);
|
|
3370
3872
|
return rejected;
|
|
@@ -3535,7 +4037,8 @@ function normalizeBundle(b, options = {}) {
|
|
|
3535
4037
|
const { bundle: checked, rejected } = validateBundle(
|
|
3536
4038
|
{
|
|
3537
4039
|
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
3538
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : []
|
|
4040
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
4041
|
+
whitelist_keys: b.whitelist_keys
|
|
3539
4042
|
},
|
|
3540
4043
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3541
4044
|
);
|
|
@@ -3555,7 +4058,7 @@ function normalizeBundle(b, options = {}) {
|
|
|
3555
4058
|
return {
|
|
3556
4059
|
firewall: checked.firewall,
|
|
3557
4060
|
whitelists: checked.whitelists,
|
|
3558
|
-
whitelist_keys:
|
|
4061
|
+
whitelist_keys: checked.whitelist_keys,
|
|
3559
4062
|
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
3560
4063
|
};
|
|
3561
4064
|
}
|
|
@@ -3599,18 +4102,23 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
3599
4102
|
};
|
|
3600
4103
|
}
|
|
3601
4104
|
function makeRefreshHandler(tick, secret) {
|
|
4105
|
+
let inflight = null;
|
|
4106
|
+
const runOnce = () => {
|
|
4107
|
+
if (inflight) return inflight;
|
|
4108
|
+
let current;
|
|
4109
|
+
current = Promise.resolve().then(() => tick()).finally(() => {
|
|
4110
|
+
if (inflight === current) inflight = null;
|
|
4111
|
+
});
|
|
4112
|
+
inflight = current;
|
|
4113
|
+
return current;
|
|
4114
|
+
};
|
|
3602
4115
|
return async (request) => {
|
|
3603
4116
|
if (!secret) return new Response("not found", { status: 404 });
|
|
3604
|
-
|
|
3605
|
-
try {
|
|
3606
|
-
provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
|
|
3607
|
-
} catch {
|
|
3608
|
-
provided = null;
|
|
3609
|
-
}
|
|
4117
|
+
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
3610
4118
|
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
3611
4119
|
let refreshed = true;
|
|
3612
4120
|
try {
|
|
3613
|
-
const status = await
|
|
4121
|
+
const status = await runOnce();
|
|
3614
4122
|
if (status && status.ok === false) refreshed = false;
|
|
3615
4123
|
} catch {
|
|
3616
4124
|
refreshed = false;
|
|
@@ -4359,8 +4867,10 @@ function hardensWithoutBody(rule) {
|
|
|
4359
4867
|
// src/protect/firewall-log.js
|
|
4360
4868
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
4361
4869
|
var STOP_BUDGET_MS2 = 5e3;
|
|
4870
|
+
var ATTEMPT_TIMEOUT_MS2 = 1e4;
|
|
4362
4871
|
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
4363
4872
|
var MAX_BATCH2 = 50;
|
|
4873
|
+
var MAX_QUEUE2 = 500;
|
|
4364
4874
|
var TOKEN_SKEW_MS = 6e4;
|
|
4365
4875
|
function parseApiKey(apiKey) {
|
|
4366
4876
|
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
@@ -4382,14 +4892,16 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
4382
4892
|
}
|
|
4383
4893
|
if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
|
|
4384
4894
|
try {
|
|
4385
|
-
|
|
4895
|
+
const candidate = new URL(pulseOrManifestUrl).origin;
|
|
4896
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4386
4897
|
} catch {
|
|
4387
4898
|
}
|
|
4388
4899
|
}
|
|
4389
4900
|
const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
|
|
4390
4901
|
if (typeof endpoint === "string" && endpoint.length > 0) {
|
|
4391
4902
|
try {
|
|
4392
|
-
|
|
4903
|
+
const candidate = new URL(endpoint).origin;
|
|
4904
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4393
4905
|
} catch {
|
|
4394
4906
|
}
|
|
4395
4907
|
}
|
|
@@ -4401,16 +4913,16 @@ function createFirewallLogReporter(opts) {
|
|
|
4401
4913
|
return { record() {
|
|
4402
4914
|
}, flush: () => Promise.resolve(), stop: () => Promise.resolve() };
|
|
4403
4915
|
}
|
|
4404
|
-
const apiBase = (opts.apiBase
|
|
4916
|
+
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
4405
4917
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4406
4918
|
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
|
|
4407
4919
|
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
4408
4920
|
let queue = [];
|
|
4409
4921
|
let timer = null;
|
|
4410
4922
|
let stopped = false;
|
|
4411
|
-
|
|
4923
|
+
let inFlight = null;
|
|
4412
4924
|
let drainPromise = null;
|
|
4413
|
-
|
|
4925
|
+
let activeController = null;
|
|
4414
4926
|
let ended = false;
|
|
4415
4927
|
let cachedToken = null;
|
|
4416
4928
|
let tokenInflight = null;
|
|
@@ -4436,9 +4948,9 @@ function createFirewallLogReporter(opts) {
|
|
|
4436
4948
|
})
|
|
4437
4949
|
});
|
|
4438
4950
|
if (!res || !res.ok) return null;
|
|
4439
|
-
const body = await res
|
|
4951
|
+
const body = await readBoundedJson(res);
|
|
4440
4952
|
const token = body?.access_token;
|
|
4441
|
-
if (typeof token !== "string" || token.length === 0) return null;
|
|
4953
|
+
if (typeof token !== "string" || token.length === 0 || token.length > 8192 || /[\u0000-\u0020\u007f-\u009f]/.test(token)) return null;
|
|
4442
4954
|
const expiresIn = Number(body?.expires_in);
|
|
4443
4955
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
4444
4956
|
cachedToken = { token, expiresAt: Date.now() + ttlMs };
|
|
@@ -4456,12 +4968,18 @@ function createFirewallLogReporter(opts) {
|
|
|
4456
4968
|
clearTimeout(timer);
|
|
4457
4969
|
timer = null;
|
|
4458
4970
|
}
|
|
4459
|
-
if (ended ||
|
|
4971
|
+
if (ended || typeof fetchImpl !== "function") return Promise.resolve();
|
|
4972
|
+
if (inFlight) return inFlight;
|
|
4973
|
+
if (queue.length === 0) return Promise.resolve();
|
|
4460
4974
|
const batch = queue.splice(0, MAX_BATCH2);
|
|
4975
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
4976
|
+
activeController = controller;
|
|
4977
|
+
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
4978
|
+
attemptTimer?.unref?.();
|
|
4461
4979
|
let entry;
|
|
4462
|
-
|
|
4980
|
+
entry = (async () => {
|
|
4463
4981
|
try {
|
|
4464
|
-
const token = await fetchAccessToken(
|
|
4982
|
+
const token = await fetchAccessToken(controller?.signal);
|
|
4465
4983
|
if (!token || ended) return;
|
|
4466
4984
|
const body = new URLSearchParams();
|
|
4467
4985
|
body.set("type", "firewall");
|
|
@@ -4476,19 +4994,24 @@ function createFirewallLogReporter(opts) {
|
|
|
4476
4994
|
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
4477
4995
|
},
|
|
4478
4996
|
body,
|
|
4479
|
-
// Both phases carry
|
|
4480
|
-
...
|
|
4997
|
+
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
4998
|
+
...controller ? { signal: controller.signal } : {}
|
|
4481
4999
|
});
|
|
4482
5000
|
if (p && typeof p.then === "function") await p.catch(() => {
|
|
4483
5001
|
});
|
|
4484
5002
|
} catch {
|
|
4485
5003
|
} finally {
|
|
4486
|
-
|
|
5004
|
+
clearTimeout(attemptTimer);
|
|
5005
|
+
if (activeController === controller) activeController = null;
|
|
5006
|
+
if (inFlight === entry) inFlight = null;
|
|
5007
|
+
if (!stopped && !ended && queue.length > 0 && !timer) {
|
|
5008
|
+
timer = setTimeout(flush, queue.length >= MAX_BATCH2 ? 0 : flushMs);
|
|
5009
|
+
timer?.unref?.();
|
|
5010
|
+
}
|
|
4487
5011
|
}
|
|
4488
5012
|
})();
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
return send;
|
|
5013
|
+
inFlight = entry;
|
|
5014
|
+
return entry;
|
|
4492
5015
|
};
|
|
4493
5016
|
return {
|
|
4494
5017
|
/**
|
|
@@ -4504,6 +5027,7 @@ function createFirewallLogReporter(opts) {
|
|
|
4504
5027
|
if (stopped) return;
|
|
4505
5028
|
const fid = event?.rule?.id;
|
|
4506
5029
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
5030
|
+
if (queue.length >= MAX_QUEUE2) return;
|
|
4507
5031
|
queue.push({
|
|
4508
5032
|
fid,
|
|
4509
5033
|
method: event.method ?? null,
|
|
@@ -4535,9 +5059,10 @@ function createFirewallLogReporter(opts) {
|
|
|
4535
5059
|
}
|
|
4536
5060
|
const terminate = () => {
|
|
4537
5061
|
ended = true;
|
|
4538
|
-
|
|
5062
|
+
activeController?.abort();
|
|
5063
|
+
activeController = null;
|
|
4539
5064
|
queue = [];
|
|
4540
|
-
|
|
5065
|
+
inFlight = null;
|
|
4541
5066
|
};
|
|
4542
5067
|
let budget = null;
|
|
4543
5068
|
const spent = new Promise((resolve) => {
|
|
@@ -4549,8 +5074,8 @@ function createFirewallLogReporter(opts) {
|
|
|
4549
5074
|
});
|
|
4550
5075
|
const work = (async () => {
|
|
4551
5076
|
while (!ended) {
|
|
4552
|
-
if (
|
|
4553
|
-
await
|
|
5077
|
+
if (inFlight) {
|
|
5078
|
+
await inFlight;
|
|
4554
5079
|
continue;
|
|
4555
5080
|
}
|
|
4556
5081
|
if (queue.length === 0) break;
|
|
@@ -4574,10 +5099,107 @@ function telemetryEnabled() {
|
|
|
4574
5099
|
|
|
4575
5100
|
// src/protect/supabase-guard.js
|
|
4576
5101
|
var GUARD_PATH = "/_patchstack/guard";
|
|
4577
|
-
var
|
|
4578
|
-
|
|
5102
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5103
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5104
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5105
|
+
"connection",
|
|
5106
|
+
"cookie",
|
|
5107
|
+
"cookie2",
|
|
5108
|
+
"forwarded",
|
|
5109
|
+
"host",
|
|
5110
|
+
"keep-alive",
|
|
5111
|
+
"proxy-authenticate",
|
|
5112
|
+
"proxy-authorization",
|
|
5113
|
+
"te",
|
|
5114
|
+
"trailer",
|
|
5115
|
+
"transfer-encoding",
|
|
5116
|
+
"upgrade",
|
|
5117
|
+
"x-forwarded-for",
|
|
5118
|
+
"x-forwarded-host",
|
|
5119
|
+
"x-forwarded-port",
|
|
5120
|
+
"x-forwarded-proto",
|
|
5121
|
+
"x-ps-target",
|
|
5122
|
+
"x-real-ip"
|
|
5123
|
+
]);
|
|
5124
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5125
|
+
"connection",
|
|
5126
|
+
"content-encoding",
|
|
5127
|
+
"content-length",
|
|
5128
|
+
"keep-alive",
|
|
5129
|
+
"proxy-authenticate",
|
|
5130
|
+
"proxy-authorization",
|
|
5131
|
+
"set-cookie",
|
|
5132
|
+
"set-cookie2",
|
|
5133
|
+
"te",
|
|
5134
|
+
"trailer",
|
|
5135
|
+
"transfer-encoding",
|
|
5136
|
+
"upgrade"
|
|
5137
|
+
]);
|
|
5138
|
+
var BodyTooLargeError = class extends Error {
|
|
5139
|
+
};
|
|
5140
|
+
async function boundedBody(request, maxBytes) {
|
|
5141
|
+
const stated = Number(request.headers.get("content-length"));
|
|
5142
|
+
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5143
|
+
if (!request.body) return void 0;
|
|
5144
|
+
const reader = request.body.getReader();
|
|
5145
|
+
const chunks = [];
|
|
5146
|
+
let total = 0;
|
|
5147
|
+
try {
|
|
5148
|
+
for (; ; ) {
|
|
5149
|
+
const { done, value } = await reader.read();
|
|
5150
|
+
if (done) break;
|
|
5151
|
+
total += value.byteLength;
|
|
5152
|
+
if (total > maxBytes) {
|
|
5153
|
+
void reader.cancel().catch(() => {
|
|
5154
|
+
});
|
|
5155
|
+
throw new BodyTooLargeError();
|
|
5156
|
+
}
|
|
5157
|
+
chunks.push(value);
|
|
5158
|
+
}
|
|
5159
|
+
} finally {
|
|
5160
|
+
reader.releaseLock();
|
|
5161
|
+
}
|
|
5162
|
+
const body = new Uint8Array(total);
|
|
5163
|
+
let offset = 0;
|
|
5164
|
+
for (const chunk of chunks) {
|
|
5165
|
+
body.set(chunk, offset);
|
|
5166
|
+
offset += chunk.byteLength;
|
|
5167
|
+
}
|
|
5168
|
+
return body;
|
|
5169
|
+
}
|
|
5170
|
+
function requestHeaders(input) {
|
|
5171
|
+
const output = new Headers();
|
|
5172
|
+
const connectionTokens = new Set(
|
|
5173
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5174
|
+
);
|
|
5175
|
+
input.forEach((value, key) => {
|
|
5176
|
+
const lower = key.toLowerCase();
|
|
5177
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5178
|
+
});
|
|
5179
|
+
return output;
|
|
5180
|
+
}
|
|
5181
|
+
function responseHeaders(input) {
|
|
5182
|
+
const output = new Headers();
|
|
5183
|
+
const connectionTokens = new Set(
|
|
5184
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5185
|
+
);
|
|
5186
|
+
input.forEach((value, key) => {
|
|
5187
|
+
const lower = key.toLowerCase();
|
|
5188
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5189
|
+
});
|
|
5190
|
+
return output;
|
|
5191
|
+
}
|
|
5192
|
+
function createSupabaseGuard({
|
|
5193
|
+
protection,
|
|
5194
|
+
supabaseUrl,
|
|
5195
|
+
fetchImpl = fetch,
|
|
5196
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5197
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5198
|
+
}) {
|
|
4579
5199
|
const guard = protection.fetchGuard();
|
|
4580
5200
|
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5201
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5202
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
4581
5203
|
return async function handleGuardRequest(request) {
|
|
4582
5204
|
const target = request.headers.get("x-ps-target");
|
|
4583
5205
|
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
@@ -4591,29 +5213,45 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
4591
5213
|
return new Response("patchstack: target not allowed", { status: 403 });
|
|
4592
5214
|
}
|
|
4593
5215
|
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
4594
|
-
|
|
5216
|
+
let body;
|
|
5217
|
+
try {
|
|
5218
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5219
|
+
} catch (error) {
|
|
5220
|
+
if (error instanceof BodyTooLargeError) {
|
|
5221
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
5222
|
+
}
|
|
5223
|
+
throw error;
|
|
5224
|
+
}
|
|
5225
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
4595
5226
|
const evalReq = new Request(targetUrl.toString(), {
|
|
4596
5227
|
method: request.method,
|
|
5228
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5229
|
+
// sanitized at the cross-origin boundary below.
|
|
4597
5230
|
headers: request.headers,
|
|
4598
|
-
body
|
|
5231
|
+
body
|
|
4599
5232
|
});
|
|
4600
5233
|
const blocked = await guard(evalReq);
|
|
4601
5234
|
if (blocked) return blocked;
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
5235
|
+
let upstream;
|
|
5236
|
+
try {
|
|
5237
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5238
|
+
method: request.method,
|
|
5239
|
+
headers: forwardHeaders,
|
|
5240
|
+
body,
|
|
5241
|
+
redirect: "manual",
|
|
5242
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
5243
|
+
});
|
|
5244
|
+
} catch (error) {
|
|
5245
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
5246
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
5247
|
+
}
|
|
5248
|
+
throw error;
|
|
5249
|
+
}
|
|
5250
|
+
const forwarded = new Response(upstream.body, {
|
|
5251
|
+
status: upstream.status,
|
|
5252
|
+
statusText: upstream.statusText,
|
|
5253
|
+
headers: responseHeaders(upstream.headers)
|
|
4614
5254
|
});
|
|
4615
|
-
const buf = await upstream.arrayBuffer();
|
|
4616
|
-
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
4617
5255
|
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
4618
5256
|
};
|
|
4619
5257
|
}
|
|
@@ -4916,9 +5554,13 @@ async function createProtection(options = {}) {
|
|
|
4916
5554
|
for (const name of Object.keys(headers)) {
|
|
4917
5555
|
const value = headers[name];
|
|
4918
5556
|
if (typeof value === "string") {
|
|
4919
|
-
headers
|
|
5557
|
+
setOwn(headers, name, applyRedactors(value, spanRedactors, mask));
|
|
4920
5558
|
} else if (Array.isArray(value)) {
|
|
4921
|
-
|
|
5559
|
+
setOwn(
|
|
5560
|
+
headers,
|
|
5561
|
+
name,
|
|
5562
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item)
|
|
5563
|
+
);
|
|
4922
5564
|
}
|
|
4923
5565
|
}
|
|
4924
5566
|
}
|
|
@@ -4968,7 +5610,13 @@ async function createProtection(options = {}) {
|
|
|
4968
5610
|
let result;
|
|
4969
5611
|
let shaped;
|
|
4970
5612
|
try {
|
|
4971
|
-
shaped = await fromFetchRequest(request, {
|
|
5613
|
+
shaped = await fromFetchRequest(request, {
|
|
5614
|
+
trustedProxy: options.trustedProxy,
|
|
5615
|
+
maxBodyBytes: options.maxBodyBytes
|
|
5616
|
+
});
|
|
5617
|
+
if (shaped?._bodyInspectionSkip) {
|
|
5618
|
+
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
5619
|
+
}
|
|
4972
5620
|
result = engine.evaluate(shaped);
|
|
4973
5621
|
} catch (err) {
|
|
4974
5622
|
notify(onError, err, "onError");
|
|
@@ -5058,7 +5706,7 @@ async function createProtection(options = {}) {
|
|
|
5058
5706
|
const out = {};
|
|
5059
5707
|
for (const [name, value] of writeHeadEntries(headers)) {
|
|
5060
5708
|
const key = name.toLowerCase();
|
|
5061
|
-
|
|
5709
|
+
appendOwn(out, key, value);
|
|
5062
5710
|
}
|
|
5063
5711
|
return out;
|
|
5064
5712
|
};
|
|
@@ -5455,7 +6103,7 @@ async function createProtection(options = {}) {
|
|
|
5455
6103
|
let reporter = null;
|
|
5456
6104
|
if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
|
|
5457
6105
|
try {
|
|
5458
|
-
({ reportManifest: reporter } = await import("./refresh-manifest-
|
|
6106
|
+
({ reportManifest: reporter } = await import("./refresh-manifest-IE6SPHG6.js"));
|
|
5459
6107
|
} catch (err) {
|
|
5460
6108
|
notify(onError, err, "onError");
|
|
5461
6109
|
}
|
|
@@ -5594,7 +6242,6 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5594
6242
|
const reader = body.getReader();
|
|
5595
6243
|
const chunks = [];
|
|
5596
6244
|
let size = 0;
|
|
5597
|
-
let over = false;
|
|
5598
6245
|
let sniffed = !sniff;
|
|
5599
6246
|
try {
|
|
5600
6247
|
for (; ; ) {
|
|
@@ -5603,20 +6250,27 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5603
6250
|
if (!value) continue;
|
|
5604
6251
|
if (!sniffed) {
|
|
5605
6252
|
sniffed = true;
|
|
5606
|
-
if (looksBinary(value))
|
|
6253
|
+
if (looksBinary(value)) {
|
|
6254
|
+
void reader.cancel().catch(() => {
|
|
6255
|
+
});
|
|
6256
|
+
return { skip: "binary-body" };
|
|
6257
|
+
}
|
|
5607
6258
|
}
|
|
5608
6259
|
size += value.byteLength;
|
|
5609
|
-
if (
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
6260
|
+
if (size >= cap) {
|
|
6261
|
+
void reader.cancel().catch(() => {
|
|
6262
|
+
});
|
|
6263
|
+
return { skip: "body-cap" };
|
|
5613
6264
|
}
|
|
5614
6265
|
chunks.push(value);
|
|
5615
6266
|
}
|
|
5616
6267
|
} catch {
|
|
6268
|
+
void reader.cancel().catch(() => {
|
|
6269
|
+
});
|
|
5617
6270
|
return { skip: "read-failed" };
|
|
6271
|
+
} finally {
|
|
6272
|
+
reader.releaseLock();
|
|
5618
6273
|
}
|
|
5619
|
-
if (over) return { skip: "body-cap" };
|
|
5620
6274
|
try {
|
|
5621
6275
|
return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
|
|
5622
6276
|
} catch {
|
|
@@ -5644,10 +6298,10 @@ function concatBytes(chunks, total) {
|
|
|
5644
6298
|
function headerObject(headers) {
|
|
5645
6299
|
const out = {};
|
|
5646
6300
|
headers?.forEach?.((v, k) => {
|
|
5647
|
-
out
|
|
6301
|
+
setOwn(out, k.toLowerCase(), v);
|
|
5648
6302
|
});
|
|
5649
6303
|
const setCookies = headers?.getSetCookie?.();
|
|
5650
|
-
if (setCookies && setCookies.length) out
|
|
6304
|
+
if (setCookies && setCookies.length) setOwn(out, "set-cookie", setCookies);
|
|
5651
6305
|
return out;
|
|
5652
6306
|
}
|
|
5653
6307
|
function hasSpanMutations(rule) {
|
|
@@ -5816,7 +6470,7 @@ function isHeaderMutation(action) {
|
|
|
5816
6470
|
}
|
|
5817
6471
|
function applyHeaderMutation(headers, rule) {
|
|
5818
6472
|
if (rule.action === "remove-header") {
|
|
5819
|
-
for (const name of rule.remove_headers ?? []) headers
|
|
6473
|
+
for (const name of rule.remove_headers ?? []) setOwn(headers, String(name).toLowerCase(), null);
|
|
5820
6474
|
return;
|
|
5821
6475
|
}
|
|
5822
6476
|
if (rule.action === "set-header") {
|
|
@@ -5825,7 +6479,7 @@ function applyHeaderMutation(headers, rule) {
|
|
|
5825
6479
|
const key = String(name).toLowerCase();
|
|
5826
6480
|
const present = headers[key] != null && headers[key] !== "";
|
|
5827
6481
|
if (ensure && present) continue;
|
|
5828
|
-
headers
|
|
6482
|
+
setOwn(headers, key, String(value));
|
|
5829
6483
|
}
|
|
5830
6484
|
return;
|
|
5831
6485
|
}
|