@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.edge.js
CHANGED
|
@@ -272,6 +272,51 @@ function warnOnce(key, message) {
|
|
|
272
272
|
console.warn(message);
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// src/protect/engine/response-json.js
|
|
276
|
+
var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
277
|
+
async function readBoundedJson(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
|
|
278
|
+
const stated = Number(response.headers?.get?.("content-length"));
|
|
279
|
+
if (Number.isFinite(stated) && stated > maxBytes) {
|
|
280
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
281
|
+
}
|
|
282
|
+
const stream = response.body;
|
|
283
|
+
if (!stream || typeof stream.getReader !== "function") {
|
|
284
|
+
if (typeof response.text === "function") {
|
|
285
|
+
const text = await response.text();
|
|
286
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
287
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
288
|
+
}
|
|
289
|
+
return JSON.parse(text);
|
|
290
|
+
}
|
|
291
|
+
throw new Error("rule response body cannot be read with a size bound");
|
|
292
|
+
}
|
|
293
|
+
const reader = stream.getReader();
|
|
294
|
+
const chunks = [];
|
|
295
|
+
let total = 0;
|
|
296
|
+
try {
|
|
297
|
+
for (; ; ) {
|
|
298
|
+
const { done, value } = await reader.read();
|
|
299
|
+
if (done) break;
|
|
300
|
+
total += value.byteLength;
|
|
301
|
+
if (total > maxBytes) {
|
|
302
|
+
void reader.cancel().catch(() => {
|
|
303
|
+
});
|
|
304
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
305
|
+
}
|
|
306
|
+
chunks.push(value);
|
|
307
|
+
}
|
|
308
|
+
} finally {
|
|
309
|
+
reader.releaseLock();
|
|
310
|
+
}
|
|
311
|
+
const bytes = new Uint8Array(total);
|
|
312
|
+
let offset = 0;
|
|
313
|
+
for (const chunk of chunks) {
|
|
314
|
+
bytes.set(chunk, offset);
|
|
315
|
+
offset += chunk.byteLength;
|
|
316
|
+
}
|
|
317
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
318
|
+
}
|
|
319
|
+
|
|
275
320
|
// src/protect/engine/client.js
|
|
276
321
|
var DEFAULT_BASE_URL = "https://api.patchstack.com";
|
|
277
322
|
var DEFAULT_CACHE_TTL = 3e5;
|
|
@@ -328,7 +373,7 @@ var PatchstackRuleClient = class {
|
|
|
328
373
|
whitelist_keys: {}
|
|
329
374
|
};
|
|
330
375
|
}
|
|
331
|
-
const data = await response
|
|
376
|
+
const data = await readBoundedJson(response);
|
|
332
377
|
if (!data || !Array.isArray(data.firewall)) {
|
|
333
378
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
334
379
|
}
|
|
@@ -365,6 +410,22 @@ var PatchstackRuleClient = class {
|
|
|
365
410
|
}
|
|
366
411
|
};
|
|
367
412
|
|
|
413
|
+
// src/protect/engine/own.js
|
|
414
|
+
function setOwn(target, key, value) {
|
|
415
|
+
Object.defineProperty(target, key, {
|
|
416
|
+
configurable: true,
|
|
417
|
+
enumerable: true,
|
|
418
|
+
writable: true,
|
|
419
|
+
value
|
|
420
|
+
});
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
function appendOwn(target, key, value) {
|
|
424
|
+
if (!Object.hasOwn(target, key)) return setOwn(target, key, value);
|
|
425
|
+
const previous = target[key];
|
|
426
|
+
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
427
|
+
}
|
|
428
|
+
|
|
368
429
|
// src/protect/engine/request.js
|
|
369
430
|
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
370
431
|
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
@@ -483,8 +544,8 @@ var RequestResolver = class {
|
|
|
483
544
|
}
|
|
484
545
|
if (key.startsWith("header.")) {
|
|
485
546
|
const name = key.slice("header.".length).toLowerCase();
|
|
486
|
-
const
|
|
487
|
-
return
|
|
547
|
+
const headers = resp.headers ?? {};
|
|
548
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
488
549
|
}
|
|
489
550
|
return [];
|
|
490
551
|
}
|
|
@@ -579,7 +640,7 @@ var RequestResolver = class {
|
|
|
579
640
|
...this.#resolveWildcard(cookies, key)
|
|
580
641
|
];
|
|
581
642
|
}
|
|
582
|
-
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? cookies[key];
|
|
643
|
+
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? (Object.hasOwn(cookies, key) ? cookies[key] : void 0);
|
|
583
644
|
return value !== void 0 ? [value] : [];
|
|
584
645
|
}
|
|
585
646
|
#resolveCookie(key) {
|
|
@@ -587,8 +648,7 @@ var RequestResolver = class {
|
|
|
587
648
|
if (key.endsWith("*")) {
|
|
588
649
|
return this.#resolveWildcard(cookies, key);
|
|
589
650
|
}
|
|
590
|
-
|
|
591
|
-
return value !== void 0 ? [value] : [];
|
|
651
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
592
652
|
}
|
|
593
653
|
#resolveServer(key) {
|
|
594
654
|
const req = this.#req;
|
|
@@ -764,7 +824,7 @@ var RequestResolver = class {
|
|
|
764
824
|
}
|
|
765
825
|
const name = pair.substring(0, eqIndex).trim();
|
|
766
826
|
const value = pair.substring(eqIndex + 1).trim();
|
|
767
|
-
cookies
|
|
827
|
+
setOwn(cookies, name, value);
|
|
768
828
|
}
|
|
769
829
|
this.#cookies = cookies;
|
|
770
830
|
return this.#cookies;
|
|
@@ -921,30 +981,113 @@ function normalizeWhitespace(value) {
|
|
|
921
981
|
result = result.replace(/ {2,}/g, " ");
|
|
922
982
|
return result;
|
|
923
983
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
|
|
984
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
985
|
+
var MAX_RAW_NODES = 25e3;
|
|
986
|
+
var MAX_RAW_DEPTH = 200;
|
|
987
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
988
|
+
function serializeForRawDetection(body) {
|
|
989
|
+
if (body === null || body === void 0) return "";
|
|
990
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
991
|
+
const visited = /* @__PURE__ */ new Set();
|
|
992
|
+
const output = [];
|
|
993
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
994
|
+
let chars = 0;
|
|
995
|
+
let nodes = 0;
|
|
996
|
+
const append = (text) => {
|
|
997
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
998
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
999
|
+
output.push(piece);
|
|
1000
|
+
chars += piece.length;
|
|
1001
|
+
};
|
|
1002
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
1003
|
+
const task = stack.pop();
|
|
1004
|
+
if (task.kind === "text") {
|
|
1005
|
+
append(task.text);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (task.kind === "key") {
|
|
1009
|
+
append(boundedJsonString(task.value));
|
|
1010
|
+
append(":");
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
nodes++;
|
|
1014
|
+
if (nodes > MAX_RAW_NODES) {
|
|
1015
|
+
append('"[NodeLimit]"');
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const value = task.value;
|
|
1019
|
+
if (value === null || value === void 0) {
|
|
1020
|
+
append(task.root ? "" : "null");
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (typeof value === "string") {
|
|
1024
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof value !== "object") {
|
|
1028
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (visited.has(value)) {
|
|
1032
|
+
append("[Circular]");
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
1036
|
+
append('"[DepthLimit]"');
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
visited.add(value);
|
|
1040
|
+
if (Array.isArray(value)) {
|
|
1041
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
1042
|
+
const items = [];
|
|
1043
|
+
for (let i = 0; i < length2; i++) {
|
|
1044
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
1045
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
1046
|
+
}
|
|
1047
|
+
if (value.length > length2) {
|
|
1048
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
1049
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
1050
|
+
}
|
|
1051
|
+
stack.push({ kind: "text", text: "]" });
|
|
1052
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
1053
|
+
stack.push({ kind: "text", text: "[" });
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
let keys;
|
|
1057
|
+
try {
|
|
1058
|
+
keys = Object.getOwnPropertyNames(value);
|
|
1059
|
+
} catch {
|
|
1060
|
+
append('"[Unreadable]"');
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
1064
|
+
const entries = [];
|
|
1065
|
+
for (let i = 0; i < length; i++) {
|
|
1066
|
+
const key = keys[i];
|
|
1067
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
1068
|
+
entries.push({ kind: "key", value: key });
|
|
1069
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
1070
|
+
}
|
|
1071
|
+
if (keys.length > length) {
|
|
1072
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
1073
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
1074
|
+
}
|
|
1075
|
+
stack.push({ kind: "text", text: "}" });
|
|
1076
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
1077
|
+
stack.push({ kind: "text", text: "{" });
|
|
936
1078
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1079
|
+
return output.join("");
|
|
1080
|
+
}
|
|
1081
|
+
function boundedJsonString(value) {
|
|
1082
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
1083
|
+
}
|
|
1084
|
+
function readOwnValue(value, key) {
|
|
1085
|
+
try {
|
|
1086
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1087
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
1088
|
+
} catch {
|
|
1089
|
+
return "[Unreadable]";
|
|
941
1090
|
}
|
|
942
|
-
const keys = Object.getOwnPropertyNames(body);
|
|
943
|
-
const parts = keys.map((key) => {
|
|
944
|
-
const val = serializeForRawDetection(body[key], visited, false);
|
|
945
|
-
return JSON.stringify(key) + ":" + val;
|
|
946
|
-
});
|
|
947
|
-
return "{" + parts.join(",") + "}";
|
|
948
1091
|
}
|
|
949
1092
|
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
950
1093
|
function requestField(req, key) {
|
|
@@ -986,7 +1129,7 @@ function normalizeObject(value, options = {}, depth = 0) {
|
|
|
986
1129
|
if (typeof value === "object" && value !== null) {
|
|
987
1130
|
const result = {};
|
|
988
1131
|
for (const [key, val] of Object.entries(value)) {
|
|
989
|
-
result
|
|
1132
|
+
setOwn(result, key, normalizeObject(val, options, depth + 1));
|
|
990
1133
|
}
|
|
991
1134
|
return result;
|
|
992
1135
|
}
|
|
@@ -1003,6 +1146,111 @@ var REDOS_PATTERNS = [
|
|
|
1003
1146
|
new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
|
|
1004
1147
|
// nested alternation under a quantifier
|
|
1005
1148
|
];
|
|
1149
|
+
function hasAdjacentUnbounded(body, flags) {
|
|
1150
|
+
let previousUnboundedAtom = null;
|
|
1151
|
+
for (let i = 0; i < body.length; ) {
|
|
1152
|
+
const start = i;
|
|
1153
|
+
const ch = body[i];
|
|
1154
|
+
let assertion = false;
|
|
1155
|
+
let literal = null;
|
|
1156
|
+
let atom = ch;
|
|
1157
|
+
if (ch === "\\") {
|
|
1158
|
+
const escaped = body[i + 1] ?? "";
|
|
1159
|
+
assertion = /[bBAZz]/.test(escaped);
|
|
1160
|
+
if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
|
|
1161
|
+
i += Math.min(2, body.length - i);
|
|
1162
|
+
atom = body.slice(start, i);
|
|
1163
|
+
} else if (ch === "[") {
|
|
1164
|
+
i++;
|
|
1165
|
+
while (i < body.length) {
|
|
1166
|
+
if (body[i] === "\\") i += 2;
|
|
1167
|
+
else if (body[i++] === "]") break;
|
|
1168
|
+
}
|
|
1169
|
+
atom = body.slice(start, i);
|
|
1170
|
+
} else if (ch === ")") {
|
|
1171
|
+
i++;
|
|
1172
|
+
atom = ".";
|
|
1173
|
+
} else if (ch === "(") {
|
|
1174
|
+
const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
|
|
1175
|
+
if (lookaround) {
|
|
1176
|
+
i = groupEnd(body, i);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
i++;
|
|
1180
|
+
if (body.startsWith("?:", i)) i += 2;
|
|
1181
|
+
else if (body.startsWith("?<", i)) {
|
|
1182
|
+
const nameEnd = body.indexOf(">", i + 2);
|
|
1183
|
+
if (nameEnd !== -1) i = nameEnd + 1;
|
|
1184
|
+
}
|
|
1185
|
+
continue;
|
|
1186
|
+
} else if (ch === "|") {
|
|
1187
|
+
previousUnboundedAtom = null;
|
|
1188
|
+
i++;
|
|
1189
|
+
continue;
|
|
1190
|
+
} else if ("^$".includes(ch)) {
|
|
1191
|
+
i++;
|
|
1192
|
+
continue;
|
|
1193
|
+
} else if ("?*+{}".includes(ch)) {
|
|
1194
|
+
previousUnboundedAtom = null;
|
|
1195
|
+
i++;
|
|
1196
|
+
continue;
|
|
1197
|
+
} else {
|
|
1198
|
+
if (ch !== ".") literal = ch;
|
|
1199
|
+
i++;
|
|
1200
|
+
}
|
|
1201
|
+
if (assertion) continue;
|
|
1202
|
+
let unbounded = false;
|
|
1203
|
+
let mayBeEmpty = false;
|
|
1204
|
+
if (body[i] === "*" || body[i] === "+") {
|
|
1205
|
+
unbounded = true;
|
|
1206
|
+
mayBeEmpty = body[i] === "*";
|
|
1207
|
+
i++;
|
|
1208
|
+
} else if (body[i] === "{") {
|
|
1209
|
+
const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
|
|
1210
|
+
if (quantifier) {
|
|
1211
|
+
unbounded = quantifier[2] === "";
|
|
1212
|
+
mayBeEmpty = Number(quantifier[1]) === 0;
|
|
1213
|
+
i += quantifier[0].length;
|
|
1214
|
+
}
|
|
1215
|
+
} else if (body[i] === "?") {
|
|
1216
|
+
mayBeEmpty = true;
|
|
1217
|
+
i++;
|
|
1218
|
+
}
|
|
1219
|
+
if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
|
|
1220
|
+
if (unbounded && previousUnboundedAtom !== null) return true;
|
|
1221
|
+
if (unbounded) {
|
|
1222
|
+
previousUnboundedAtom = atom;
|
|
1223
|
+
} else if (previousUnboundedAtom !== null && !mayBeEmpty) {
|
|
1224
|
+
try {
|
|
1225
|
+
const atomFlags = flags.replace(/[^iu]/g, "");
|
|
1226
|
+
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";
|
|
1227
|
+
const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
|
|
1228
|
+
if (complementaryClass || excludedLiteral) {
|
|
1229
|
+
previousUnboundedAtom = null;
|
|
1230
|
+
}
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (i === start) i++;
|
|
1235
|
+
}
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
function groupEnd(body, start) {
|
|
1239
|
+
let depth = 0;
|
|
1240
|
+
let inClass = false;
|
|
1241
|
+
for (let i = start; i < body.length; i++) {
|
|
1242
|
+
const ch = body[i];
|
|
1243
|
+
if (ch === "\\") {
|
|
1244
|
+
i++;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (ch === "[") inClass = true;
|
|
1248
|
+
else if (ch === "]") inClass = false;
|
|
1249
|
+
else if (!inClass && ch === "(") depth++;
|
|
1250
|
+
else if (!inClass && ch === ")" && --depth === 0) return i + 1;
|
|
1251
|
+
}
|
|
1252
|
+
return body.length;
|
|
1253
|
+
}
|
|
1006
1254
|
var warnedRejectedPatterns = /* @__PURE__ */ new Set();
|
|
1007
1255
|
function warnRejectedPatternOnce(pattern) {
|
|
1008
1256
|
const key = String(pattern);
|
|
@@ -1020,15 +1268,14 @@ function safeRegExp(pattern) {
|
|
|
1020
1268
|
if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
|
|
1021
1269
|
return null;
|
|
1022
1270
|
}
|
|
1023
|
-
for (const dangerous of REDOS_PATTERNS) {
|
|
1024
|
-
if (dangerous.test(pattern)) {
|
|
1025
|
-
return null;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
1271
|
const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
|
|
1029
1272
|
if (!match) {
|
|
1030
1273
|
return null;
|
|
1031
1274
|
}
|
|
1275
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
1276
|
+
if (dangerous.test(match[1])) return null;
|
|
1277
|
+
}
|
|
1278
|
+
if (hasAdjacentUnbounded(match[1], match[2])) return null;
|
|
1032
1279
|
try {
|
|
1033
1280
|
return new RegExp(match[1], match[2]);
|
|
1034
1281
|
} catch {
|
|
@@ -1772,6 +2019,16 @@ function operandShapeProblem(shape, value) {
|
|
|
1772
2019
|
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
1773
2020
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
1774
2021
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
2022
|
+
const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
|
|
2023
|
+
const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
|
|
2024
|
+
const tooLong = stringLimit(value);
|
|
2025
|
+
if (tooLong) return tooLong;
|
|
2026
|
+
const tooMany = listLimit(value);
|
|
2027
|
+
if (tooMany) return tooMany;
|
|
2028
|
+
if (Array.isArray(value)) {
|
|
2029
|
+
const longMember = value.find((member) => stringLimit(member) !== null);
|
|
2030
|
+
if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
2031
|
+
}
|
|
1775
2032
|
switch (shape) {
|
|
1776
2033
|
case "positive-number":
|
|
1777
2034
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
@@ -1802,8 +2059,11 @@ function operandShapeProblem(shape, value) {
|
|
|
1802
2059
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
1803
2060
|
const names = Object.keys(value);
|
|
1804
2061
|
if (names.length === 0) return "must be an object with at least one entry";
|
|
2062
|
+
if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
|
|
1805
2063
|
const bad = names.find((name) => !isHeaderName(name));
|
|
1806
|
-
|
|
2064
|
+
if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
2065
|
+
const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
|
|
2066
|
+
return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
|
|
1807
2067
|
}
|
|
1808
2068
|
case "header-name-list": {
|
|
1809
2069
|
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
@@ -1949,9 +2209,16 @@ var LIMITS = Object.freeze({
|
|
|
1949
2209
|
maxRules: 5e3,
|
|
1950
2210
|
maxWhitelists: 2e3,
|
|
1951
2211
|
maxConditionsPerRule: 250,
|
|
2212
|
+
maxConditionNodesPerRule: 1e3,
|
|
2213
|
+
maxConditionNodesPerBundle: 25e3,
|
|
1952
2214
|
maxNestingDepth: 12,
|
|
1953
2215
|
maxRegexLength: 1e3,
|
|
1954
|
-
maxValueLength: 8192
|
|
2216
|
+
maxValueLength: 8192,
|
|
2217
|
+
maxParameterLength: 512,
|
|
2218
|
+
maxParameterItems: 64,
|
|
2219
|
+
maxOperandItems: 256,
|
|
2220
|
+
maxMapEntries: 128,
|
|
2221
|
+
maxPathSegments: 64
|
|
1955
2222
|
});
|
|
1956
2223
|
var GROUP_PARAMETER = "rules";
|
|
1957
2224
|
function isGroup(condition) {
|
|
@@ -1978,6 +2245,9 @@ function parameterProblem(parameter) {
|
|
|
1978
2245
|
if (parameter === void 0 || parameter === null) return null;
|
|
1979
2246
|
if (Array.isArray(parameter)) {
|
|
1980
2247
|
if (parameter.length === 0) return "parameter list is empty";
|
|
2248
|
+
if (parameter.length > LIMITS.maxParameterItems) {
|
|
2249
|
+
return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
|
|
2250
|
+
}
|
|
1981
2251
|
for (const member of parameter) {
|
|
1982
2252
|
if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
|
|
1983
2253
|
const problem = parameterProblem(member);
|
|
@@ -1986,6 +2256,9 @@ function parameterProblem(parameter) {
|
|
|
1986
2256
|
return null;
|
|
1987
2257
|
}
|
|
1988
2258
|
if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
|
|
2259
|
+
if (parameter.length > LIMITS.maxParameterLength) {
|
|
2260
|
+
return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
|
|
2261
|
+
}
|
|
1989
2262
|
if (parameter === GROUP_PARAMETER) {
|
|
1990
2263
|
return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
|
|
1991
2264
|
}
|
|
@@ -2021,6 +2294,20 @@ function matchProblem(match, parameter) {
|
|
|
2021
2294
|
if (!match || typeof match !== "object") return "condition has no match object";
|
|
2022
2295
|
if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
|
|
2023
2296
|
if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
|
|
2297
|
+
for (const operand of ["value", "key", "claim"]) {
|
|
2298
|
+
const value = match[operand];
|
|
2299
|
+
if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
|
|
2300
|
+
return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
2301
|
+
}
|
|
2302
|
+
if (Array.isArray(value)) {
|
|
2303
|
+
if (value.length > LIMITS.maxOperandItems) {
|
|
2304
|
+
return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
|
|
2305
|
+
}
|
|
2306
|
+
if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
|
|
2307
|
+
return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2024
2311
|
const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
|
|
2025
2312
|
if (!parameterless && (parameter === void 0 || parameter === null)) {
|
|
2026
2313
|
return `match type "${match.type}" needs a parameter`;
|
|
@@ -2043,11 +2330,20 @@ function matchProblem(match, parameter) {
|
|
|
2043
2330
|
} else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
|
|
2044
2331
|
return `match type "${match.type}" needs a value`;
|
|
2045
2332
|
}
|
|
2333
|
+
if (match.type === "array_key_value") {
|
|
2334
|
+
const paths = Array.isArray(match.key) ? match.key : [match.key];
|
|
2335
|
+
if (paths.some((path) => typeof path === "string" && path.split(".").length > LIMITS.maxPathSegments)) {
|
|
2336
|
+
return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2046
2339
|
return null;
|
|
2047
2340
|
}
|
|
2048
2341
|
function mutationsProblem(mutations) {
|
|
2049
2342
|
if (mutations === void 0 || mutations === null) return null;
|
|
2050
2343
|
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
2344
|
+
if (mutations.length > LIMITS.maxOperandItems) {
|
|
2345
|
+
return `mutations has more than ${LIMITS.maxOperandItems} entries`;
|
|
2346
|
+
}
|
|
2051
2347
|
for (const mutation of mutations) {
|
|
2052
2348
|
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
2053
2349
|
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
@@ -2067,10 +2363,19 @@ function whenProblem(when) {
|
|
|
2067
2363
|
if ("path" in when && !isNonEmptyString(when.path)) {
|
|
2068
2364
|
return "when.path must be a non-empty string";
|
|
2069
2365
|
}
|
|
2366
|
+
if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
|
|
2367
|
+
return `when.path is longer than ${LIMITS.maxValueLength} chars`;
|
|
2368
|
+
}
|
|
2070
2369
|
if ("method" in when) {
|
|
2071
2370
|
const methods = Array.isArray(when.method) ? when.method : [when.method];
|
|
2072
2371
|
if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
|
|
2372
|
+
if (methods.length > LIMITS.maxParameterItems) {
|
|
2373
|
+
return `when.method has more than ${LIMITS.maxParameterItems} entries`;
|
|
2374
|
+
}
|
|
2073
2375
|
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
2376
|
+
if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
|
|
2377
|
+
return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
|
|
2378
|
+
}
|
|
2074
2379
|
}
|
|
2075
2380
|
return null;
|
|
2076
2381
|
}
|
|
@@ -2094,6 +2399,11 @@ function rulePropertyProblem(rule) {
|
|
|
2094
2399
|
const problem = operandShapeProblem(shape, rule[property]);
|
|
2095
2400
|
if (problem) return `"${property}" ${problem}`;
|
|
2096
2401
|
}
|
|
2402
|
+
for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
|
|
2403
|
+
if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
|
|
2404
|
+
return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2097
2407
|
return null;
|
|
2098
2408
|
}
|
|
2099
2409
|
function actionProblem(action, rule) {
|
|
@@ -2123,46 +2433,76 @@ var PHASES2 = new Set(PHASES);
|
|
|
2123
2433
|
var ACTIONS2 = new Set(ACTIONS);
|
|
2124
2434
|
function validateBundle(bundle, opts = {}) {
|
|
2125
2435
|
const rejected = [];
|
|
2436
|
+
let omittedRejections = 0;
|
|
2437
|
+
const reject = (entry) => {
|
|
2438
|
+
if (rejected.length < 100) rejected.push(entry);
|
|
2439
|
+
else omittedRejections++;
|
|
2440
|
+
};
|
|
2126
2441
|
const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
|
|
2127
2442
|
const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
|
|
2443
|
+
const bundleBudget = { count: 0 };
|
|
2128
2444
|
const firewall = [];
|
|
2129
|
-
for (const rule of inFirewall) {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
continue;
|
|
2133
|
-
}
|
|
2134
|
-
const reason = enforceableRuleProblem(rule);
|
|
2135
|
-
if (reason) rejected.push({ id: idOf(rule), reason });
|
|
2445
|
+
for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
|
|
2446
|
+
const reason = enforceableRuleProblem(rule, bundleBudget);
|
|
2447
|
+
if (reason) reject({ id: idOf(rule), reason });
|
|
2136
2448
|
else firewall.push(rule);
|
|
2137
2449
|
}
|
|
2450
|
+
if (inFirewall.length > LIMITS2.maxRules) {
|
|
2451
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
|
|
2452
|
+
}
|
|
2138
2453
|
const whitelists = [];
|
|
2139
|
-
for (const wl of inWhitelists) {
|
|
2140
|
-
if (whitelists.length >= LIMITS2.maxWhitelists) {
|
|
2141
|
-
rejected.push({ id: idOf(wl), reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
2142
|
-
continue;
|
|
2143
|
-
}
|
|
2454
|
+
for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
|
|
2144
2455
|
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
2145
|
-
|
|
2456
|
+
reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
2146
2457
|
continue;
|
|
2147
2458
|
}
|
|
2148
2459
|
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
2149
|
-
|
|
2460
|
+
reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
2150
2461
|
continue;
|
|
2151
2462
|
}
|
|
2152
|
-
const reason = conditionsProblem(wl?.rule_v2);
|
|
2153
|
-
if (reason)
|
|
2463
|
+
const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
2464
|
+
if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
2154
2465
|
else whitelists.push(wl);
|
|
2155
2466
|
}
|
|
2467
|
+
if (inWhitelists.length > LIMITS2.maxWhitelists) {
|
|
2468
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
2469
|
+
}
|
|
2470
|
+
let whitelistKeys = {};
|
|
2471
|
+
if (bundle?.whitelist_keys !== void 0) {
|
|
2472
|
+
if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
|
|
2473
|
+
reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
|
|
2474
|
+
} else {
|
|
2475
|
+
const entries = Object.entries(bundle.whitelist_keys);
|
|
2476
|
+
if (entries.length > LIMITS2.maxMapEntries) {
|
|
2477
|
+
reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
|
|
2478
|
+
} else {
|
|
2479
|
+
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));
|
|
2480
|
+
if (invalid) {
|
|
2481
|
+
reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
|
|
2482
|
+
} else {
|
|
2483
|
+
whitelistKeys = bundle.whitelist_keys;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (omittedRejections > 0) {
|
|
2489
|
+
rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
|
|
2490
|
+
}
|
|
2156
2491
|
return {
|
|
2157
|
-
bundle: { ...bundle, firewall, whitelists },
|
|
2492
|
+
bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
|
|
2158
2493
|
rejected
|
|
2159
2494
|
};
|
|
2160
2495
|
}
|
|
2161
2496
|
function idOf(rule) {
|
|
2162
2497
|
const id = rule?.id ?? rule?.rule_id;
|
|
2163
|
-
|
|
2498
|
+
if (id === void 0 || id === null) return "(unidentified)";
|
|
2499
|
+
try {
|
|
2500
|
+
return String(id).slice(0, 128);
|
|
2501
|
+
} catch {
|
|
2502
|
+
return "(unidentified)";
|
|
2503
|
+
}
|
|
2164
2504
|
}
|
|
2165
|
-
function enforceableRuleProblem(rule) {
|
|
2505
|
+
function enforceableRuleProblem(rule, bundleBudget = null) {
|
|
2166
2506
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
2167
2507
|
const nullReason = nullPropertyProblem(rule);
|
|
2168
2508
|
if (nullReason) return nullReason;
|
|
@@ -2173,9 +2513,9 @@ function enforceableRuleProblem(rule) {
|
|
|
2173
2513
|
if (scopeReason) return scopeReason;
|
|
2174
2514
|
const propertyReason = rulePropertyProblem(rule);
|
|
2175
2515
|
if (propertyReason) return propertyReason;
|
|
2176
|
-
return conditionsProblem(rule.rule_v2);
|
|
2516
|
+
return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
2177
2517
|
}
|
|
2178
|
-
function conditionsProblem(conditions, depth = 0) {
|
|
2518
|
+
function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
|
|
2179
2519
|
if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
|
|
2180
2520
|
if (conditions.length === 0) return "rule_v2 is empty (would never match)";
|
|
2181
2521
|
if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
|
|
@@ -2183,10 +2523,20 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
2183
2523
|
return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
|
|
2184
2524
|
}
|
|
2185
2525
|
for (const c of conditions) {
|
|
2526
|
+
ruleBudget.count++;
|
|
2527
|
+
if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
|
|
2528
|
+
return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
|
|
2529
|
+
}
|
|
2530
|
+
if (bundleBudget) {
|
|
2531
|
+
bundleBudget.count++;
|
|
2532
|
+
if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
|
|
2533
|
+
return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2186
2536
|
const shapeReason = conditionShapeProblem(c);
|
|
2187
2537
|
if (shapeReason) return shapeReason;
|
|
2188
2538
|
if (isGroup(c)) {
|
|
2189
|
-
const nested = conditionsProblem(c.rules, depth + 1);
|
|
2539
|
+
const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
|
|
2190
2540
|
if (nested) return nested;
|
|
2191
2541
|
continue;
|
|
2192
2542
|
}
|
|
@@ -2200,11 +2550,17 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
2200
2550
|
if (m.type === "regex") {
|
|
2201
2551
|
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
2202
2552
|
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
2553
|
+
if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
|
|
2203
2554
|
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
2204
2555
|
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
2205
2556
|
}
|
|
2206
2557
|
if (m.match) {
|
|
2207
|
-
const nested = conditionsProblem(
|
|
2558
|
+
const nested = conditionsProblem(
|
|
2559
|
+
[{ parameter: c.parameter, match: m.match }],
|
|
2560
|
+
depth + 1,
|
|
2561
|
+
ruleBudget,
|
|
2562
|
+
bundleBudget
|
|
2563
|
+
);
|
|
2208
2564
|
if (nested) return nested;
|
|
2209
2565
|
}
|
|
2210
2566
|
}
|
|
@@ -2417,6 +2773,117 @@ function asText(value) {
|
|
|
2417
2773
|
return null;
|
|
2418
2774
|
}
|
|
2419
2775
|
|
|
2776
|
+
// src/types.ts
|
|
2777
|
+
var PatchstackError = class extends Error {
|
|
2778
|
+
constructor(message, code, cause) {
|
|
2779
|
+
super(message);
|
|
2780
|
+
this.code = code;
|
|
2781
|
+
this.cause = cause;
|
|
2782
|
+
this.name = "PatchstackError";
|
|
2783
|
+
}
|
|
2784
|
+
code;
|
|
2785
|
+
cause;
|
|
2786
|
+
/**
|
|
2787
|
+
* For a `VALIDATION_ERROR`, the request fields the server refused, as it named them. Read from the
|
|
2788
|
+
* structured `errors` object a validation response carries, so a caller deciding what to do about a
|
|
2789
|
+
* refusal keys on the field rather than on the wording of a sentence.
|
|
2790
|
+
*/
|
|
2791
|
+
fields = [];
|
|
2792
|
+
};
|
|
2793
|
+
|
|
2794
|
+
// src/endpoint-policy.ts
|
|
2795
|
+
function assertConnectableEndpoint(config, target = config.endpoint) {
|
|
2796
|
+
if (config.endpointTrusted === false) {
|
|
2797
|
+
throw new PatchstackError(
|
|
2798
|
+
"The custom endpoint in .patchstackrc.json must be confirmed with --endpoint or PATCHSTACK_ENDPOINT before it can be used.",
|
|
2799
|
+
"CONFIG_INVALID"
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
if (safeRemoteUrl(config.endpoint) === null) {
|
|
2803
|
+
throw new PatchstackError(
|
|
2804
|
+
"Patchstack endpoints must use HTTPS, except for localhost development endpoints.",
|
|
2805
|
+
"CONFIG_INVALID"
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
let endpointOrigin;
|
|
2809
|
+
let targetOrigin;
|
|
2810
|
+
try {
|
|
2811
|
+
endpointOrigin = new URL(config.endpoint).origin;
|
|
2812
|
+
targetOrigin = new URL(target).origin;
|
|
2813
|
+
} catch (cause) {
|
|
2814
|
+
throw new PatchstackError("The configured Patchstack endpoint is not a valid URL.", "CONFIG_INVALID", cause);
|
|
2815
|
+
}
|
|
2816
|
+
if (safeRemoteUrl(target) === null || targetOrigin !== endpointOrigin) {
|
|
2817
|
+
throw new PatchstackError(
|
|
2818
|
+
"A Patchstack request resolved outside the configured endpoint origin.",
|
|
2819
|
+
"CONFIG_INVALID"
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
function safeRemoteUrl(value) {
|
|
2824
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048) return null;
|
|
2825
|
+
try {
|
|
2826
|
+
const parsed = new URL(value);
|
|
2827
|
+
if (!isSafeOrigin(parsed.toString()) || parsed.username !== "" || parsed.password !== "") return null;
|
|
2828
|
+
return parsed.toString();
|
|
2829
|
+
} catch {
|
|
2830
|
+
return null;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
function isHeaderValue(value, maxLength = 8192) {
|
|
2834
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) return false;
|
|
2835
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
2836
|
+
const code = value.charCodeAt(i);
|
|
2837
|
+
if (code < 33 || code > 126) return false;
|
|
2838
|
+
}
|
|
2839
|
+
return true;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// src/bounded-response.ts
|
|
2843
|
+
var MAX_CONTROL_RESPONSE_BYTES = 256 * 1024;
|
|
2844
|
+
async function readBoundedText(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
2845
|
+
const stated = Number(response.headers?.get?.("content-length"));
|
|
2846
|
+
if (Number.isFinite(stated) && stated > maxBytes) {
|
|
2847
|
+
void response.body?.cancel().catch(() => {
|
|
2848
|
+
});
|
|
2849
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
2850
|
+
}
|
|
2851
|
+
const stream = response.body;
|
|
2852
|
+
if (stream === null || typeof stream.getReader !== "function") {
|
|
2853
|
+
if (stated === 0) return "";
|
|
2854
|
+
throw new Error("response body cannot be read with a size bound");
|
|
2855
|
+
}
|
|
2856
|
+
const reader = stream.getReader();
|
|
2857
|
+
const chunks = [];
|
|
2858
|
+
let total = 0;
|
|
2859
|
+
try {
|
|
2860
|
+
for (; ; ) {
|
|
2861
|
+
const { done, value } = await reader.read();
|
|
2862
|
+
if (done) break;
|
|
2863
|
+
total += value.byteLength;
|
|
2864
|
+
if (total > maxBytes) {
|
|
2865
|
+
void reader.cancel().catch(() => {
|
|
2866
|
+
});
|
|
2867
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
2868
|
+
}
|
|
2869
|
+
chunks.push(value);
|
|
2870
|
+
}
|
|
2871
|
+
} finally {
|
|
2872
|
+
reader.releaseLock();
|
|
2873
|
+
}
|
|
2874
|
+
const bytes = new Uint8Array(total);
|
|
2875
|
+
let offset = 0;
|
|
2876
|
+
for (const chunk of chunks) {
|
|
2877
|
+
bytes.set(chunk, offset);
|
|
2878
|
+
offset += chunk.byteLength;
|
|
2879
|
+
}
|
|
2880
|
+
return new TextDecoder().decode(bytes);
|
|
2881
|
+
}
|
|
2882
|
+
async function readBoundedJson2(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
2883
|
+
const text = await readBoundedText(response, maxBytes);
|
|
2884
|
+
return JSON.parse(text);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2420
2887
|
// src/pulse-token.ts
|
|
2421
2888
|
var TOKEN_SKEW_MS = 6e4;
|
|
2422
2889
|
function buildTokenUrl(manifestEndpoint) {
|
|
@@ -2434,22 +2901,48 @@ function parsePulseAuth(credential) {
|
|
|
2434
2901
|
if (!/^\d+$/.test(clientId)) return null;
|
|
2435
2902
|
return { clientId, clientSecret: credential.slice(0, index) };
|
|
2436
2903
|
}
|
|
2437
|
-
var cached =
|
|
2438
|
-
var inflight =
|
|
2439
|
-
function
|
|
2440
|
-
|
|
2904
|
+
var cached = /* @__PURE__ */ new Map();
|
|
2905
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
2906
|
+
function cacheKey(tokenUrl, credential) {
|
|
2907
|
+
return `${tokenUrl}
|
|
2908
|
+
${credential}`;
|
|
2909
|
+
}
|
|
2910
|
+
function clearPulseToken(config) {
|
|
2911
|
+
if (!config || typeof config.pulseAuth !== "string") {
|
|
2912
|
+
cached.clear();
|
|
2913
|
+
inflight.clear();
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
try {
|
|
2917
|
+
const key = cacheKey(buildTokenUrl(config.endpoint), config.pulseAuth);
|
|
2918
|
+
cached.delete(key);
|
|
2919
|
+
inflight.delete(key);
|
|
2920
|
+
} catch {
|
|
2921
|
+
}
|
|
2441
2922
|
}
|
|
2442
2923
|
async function getPulseToken(config, fetchImpl = fetch) {
|
|
2443
2924
|
if (typeof config.pulseAuth !== "string" || config.pulseAuth.length === 0) return null;
|
|
2444
|
-
if (
|
|
2445
|
-
|
|
2925
|
+
if (config.endpointTrusted === false) return null;
|
|
2926
|
+
let tokenUrl;
|
|
2927
|
+
try {
|
|
2928
|
+
tokenUrl = buildTokenUrl(config.endpoint);
|
|
2929
|
+
} catch {
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
if (!isSafeOrigin(tokenUrl)) return null;
|
|
2933
|
+
const key = cacheKey(tokenUrl, config.pulseAuth);
|
|
2934
|
+
const existing = cached.get(key);
|
|
2935
|
+
if (existing !== void 0 && Date.now() < existing.expiresAt - TOKEN_SKEW_MS) {
|
|
2936
|
+
return existing.token;
|
|
2446
2937
|
}
|
|
2447
|
-
|
|
2938
|
+
const pending = inflight.get(key);
|
|
2939
|
+
if (pending !== void 0) return pending;
|
|
2448
2940
|
const credentials = parsePulseAuth(config.pulseAuth);
|
|
2449
2941
|
if (credentials === null) return null;
|
|
2450
|
-
|
|
2942
|
+
let exchange;
|
|
2943
|
+
exchange = (async () => {
|
|
2451
2944
|
try {
|
|
2452
|
-
const response = await fetchImpl(
|
|
2945
|
+
const response = await fetchImpl(tokenUrl, {
|
|
2453
2946
|
method: "POST",
|
|
2454
2947
|
headers: {
|
|
2455
2948
|
"Content-Type": "application/json",
|
|
@@ -2464,25 +2957,27 @@ async function getPulseToken(config, fetchImpl = fetch) {
|
|
|
2464
2957
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
2465
2958
|
});
|
|
2466
2959
|
if (!response.ok) return null;
|
|
2467
|
-
const body = await response
|
|
2468
|
-
if (
|
|
2960
|
+
const body = await readBoundedJson2(response);
|
|
2961
|
+
if (!isHeaderValue(body.access_token)) return null;
|
|
2469
2962
|
const expiresIn = Number(body.expires_in);
|
|
2470
2963
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
2471
|
-
cached
|
|
2964
|
+
cached.set(key, { token: body.access_token, expiresAt: Date.now() + ttlMs });
|
|
2472
2965
|
return body.access_token;
|
|
2473
2966
|
} catch {
|
|
2474
2967
|
return null;
|
|
2475
2968
|
} finally {
|
|
2476
|
-
inflight
|
|
2969
|
+
if (inflight.get(key) === exchange) inflight.delete(key);
|
|
2477
2970
|
}
|
|
2478
2971
|
})();
|
|
2479
|
-
|
|
2972
|
+
inflight.set(key, exchange);
|
|
2973
|
+
return exchange;
|
|
2480
2974
|
}
|
|
2481
2975
|
async function pulseAuthHeader(config, fetchImpl = fetch) {
|
|
2482
2976
|
const token = await getPulseToken(config, fetchImpl);
|
|
2483
2977
|
return token === null ? {} : { Authorization: `Bearer ${token}` };
|
|
2484
2978
|
}
|
|
2485
2979
|
async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
2980
|
+
assertConnectableEndpoint(config, url);
|
|
2486
2981
|
const send = async () => {
|
|
2487
2982
|
const auth = await pulseAuthHeader(config, fetchImpl);
|
|
2488
2983
|
const response = await fetchImpl(url, {
|
|
@@ -2493,7 +2988,7 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
2493
2988
|
};
|
|
2494
2989
|
const first = await send();
|
|
2495
2990
|
if (first.response.status === 401 && first.authenticated) {
|
|
2496
|
-
clearPulseToken();
|
|
2991
|
+
clearPulseToken(config);
|
|
2497
2992
|
return (await send()).response;
|
|
2498
2993
|
}
|
|
2499
2994
|
return first.response;
|
|
@@ -2519,6 +3014,7 @@ var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
|
2519
3014
|
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
2520
3015
|
var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
|
|
2521
3016
|
var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
|
|
3017
|
+
var PER_RULE_DRY_RUN_HEADER = "X-Patchstack-Per-Rule-Dry-Run";
|
|
2522
3018
|
var JITTER_FRACTION2 = 0.1;
|
|
2523
3019
|
var PulseRuleClient = class {
|
|
2524
3020
|
#siteUuid;
|
|
@@ -2563,6 +3059,9 @@ var PulseRuleClient = class {
|
|
|
2563
3059
|
if (this.#buildId !== null && typeof auth.Authorization === "string") {
|
|
2564
3060
|
headers["X-Patchstack-Build"] = this.#buildId;
|
|
2565
3061
|
}
|
|
3062
|
+
if (typeof auth.Authorization === "string") {
|
|
3063
|
+
headers[PER_RULE_DRY_RUN_HEADER] = "1";
|
|
3064
|
+
}
|
|
2566
3065
|
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
2567
3066
|
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
2568
3067
|
if (response.status === 304) {
|
|
@@ -2581,7 +3080,7 @@ var PulseRuleClient = class {
|
|
|
2581
3080
|
if (!response.ok) {
|
|
2582
3081
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2583
3082
|
}
|
|
2584
|
-
const data = await response
|
|
3083
|
+
const data = await readBoundedJson(response);
|
|
2585
3084
|
if (!data || !Array.isArray(data.firewall)) {
|
|
2586
3085
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2587
3086
|
}
|
|
@@ -2642,19 +3141,18 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2642
3141
|
const method = (request.method || "GET").toUpperCase();
|
|
2643
3142
|
const headers = {};
|
|
2644
3143
|
request.headers.forEach((value, key) => {
|
|
2645
|
-
headers
|
|
3144
|
+
setOwn(headers, key.toLowerCase(), value);
|
|
2646
3145
|
});
|
|
2647
3146
|
const query = {};
|
|
2648
3147
|
for (const [key, value] of url.searchParams) {
|
|
2649
|
-
|
|
2650
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
2651
|
-
} else {
|
|
2652
|
-
query[key] = value;
|
|
2653
|
-
}
|
|
3148
|
+
appendOwn(query, key, value);
|
|
2654
3149
|
}
|
|
2655
3150
|
let rawBody = "";
|
|
3151
|
+
let bodyInspectionSkip = null;
|
|
2656
3152
|
if (method !== "GET" && method !== "HEAD") {
|
|
2657
|
-
|
|
3153
|
+
const read = await readCappedText(request, maxBodyBytes);
|
|
3154
|
+
rawBody = read.text;
|
|
3155
|
+
bodyInspectionSkip = read.skip;
|
|
2658
3156
|
}
|
|
2659
3157
|
const contentType = headers["content-type"] || "";
|
|
2660
3158
|
let body = {};
|
|
@@ -2681,7 +3179,8 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2681
3179
|
cookies: parseCookies(headers.cookie),
|
|
2682
3180
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
2683
3181
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
2684
|
-
_rawBody: rawBody
|
|
3182
|
+
_rawBody: rawBody,
|
|
3183
|
+
...bodyInspectionSkip === null ? {} : { _bodyInspectionSkip: bodyInspectionSkip }
|
|
2685
3184
|
};
|
|
2686
3185
|
}
|
|
2687
3186
|
async function readCappedText(request, max) {
|
|
@@ -2689,7 +3188,7 @@ async function readCappedText(request, max) {
|
|
|
2689
3188
|
try {
|
|
2690
3189
|
clone = request.clone();
|
|
2691
3190
|
} catch {
|
|
2692
|
-
return "";
|
|
3191
|
+
return { text: "", skip: "clone-failed" };
|
|
2693
3192
|
}
|
|
2694
3193
|
const body = clone.body;
|
|
2695
3194
|
if (body && typeof body.getReader === "function") {
|
|
@@ -2700,23 +3199,30 @@ async function readCappedText(request, max) {
|
|
|
2700
3199
|
for (; ; ) {
|
|
2701
3200
|
const { done, value } = await reader.read();
|
|
2702
3201
|
if (done) break;
|
|
2703
|
-
if (!value
|
|
3202
|
+
if (!value) continue;
|
|
2704
3203
|
const take = Math.min(value.byteLength, max - buffered);
|
|
2705
|
-
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
3204
|
+
if (take > 0) chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
2706
3205
|
buffered += take;
|
|
3206
|
+
if (buffered >= max) {
|
|
3207
|
+
void reader.cancel().catch(() => {
|
|
3208
|
+
});
|
|
3209
|
+
return { text: decodeChunks(chunks, buffered), skip: "body-cap" };
|
|
3210
|
+
}
|
|
2707
3211
|
}
|
|
2708
3212
|
} catch {
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
return "";
|
|
3213
|
+
void reader.cancel().catch(() => {
|
|
3214
|
+
});
|
|
3215
|
+
return { text: "", skip: "read-failed" };
|
|
3216
|
+
} finally {
|
|
3217
|
+
reader.releaseLock();
|
|
2715
3218
|
}
|
|
3219
|
+
return { text: decodeChunks(chunks, buffered), skip: null };
|
|
2716
3220
|
}
|
|
3221
|
+
return { text: "", skip: "unreadable-body" };
|
|
3222
|
+
}
|
|
3223
|
+
function decodeChunks(chunks, total) {
|
|
2717
3224
|
try {
|
|
2718
|
-
|
|
2719
|
-
return text.length > max ? text.slice(0, max) : text;
|
|
3225
|
+
return new TextDecoder().decode(concatChunks(chunks, total));
|
|
2720
3226
|
} catch {
|
|
2721
3227
|
return "";
|
|
2722
3228
|
}
|
|
@@ -2743,7 +3249,7 @@ function parseBody(rawBody, contentType) {
|
|
|
2743
3249
|
}
|
|
2744
3250
|
if (isForm) {
|
|
2745
3251
|
const body = {};
|
|
2746
|
-
for (const [k, v] of new URLSearchParams(rawBody)) body
|
|
3252
|
+
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
2747
3253
|
return { body, files: void 0 };
|
|
2748
3254
|
}
|
|
2749
3255
|
if (isJson || isAmbiguous) {
|
|
@@ -2771,9 +3277,9 @@ function parseMultipart(rawBody, boundary) {
|
|
|
2771
3277
|
if (filename !== void 0) {
|
|
2772
3278
|
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
2773
3279
|
const file = { filename, type: partType, content };
|
|
2774
|
-
files
|
|
3280
|
+
appendOwn(files, name, file);
|
|
2775
3281
|
} else {
|
|
2776
|
-
body
|
|
3282
|
+
appendOwn(body, name, content);
|
|
2777
3283
|
}
|
|
2778
3284
|
}
|
|
2779
3285
|
return { body, files };
|
|
@@ -2788,7 +3294,7 @@ function parseCookies(header) {
|
|
|
2788
3294
|
if (idx === -1) {
|
|
2789
3295
|
continue;
|
|
2790
3296
|
}
|
|
2791
|
-
cookies
|
|
3297
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2792
3298
|
}
|
|
2793
3299
|
return cookies;
|
|
2794
3300
|
}
|
|
@@ -2798,7 +3304,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2798
3304
|
const method = (req.method || "GET").toUpperCase();
|
|
2799
3305
|
const headers = {};
|
|
2800
3306
|
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
2801
|
-
headers
|
|
3307
|
+
setOwn(headers, key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value);
|
|
2802
3308
|
}
|
|
2803
3309
|
const host = headers.host || "localhost";
|
|
2804
3310
|
let url;
|
|
@@ -2813,11 +3319,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2813
3319
|
}
|
|
2814
3320
|
const query = {};
|
|
2815
3321
|
for (const [key, value] of url.searchParams) {
|
|
2816
|
-
|
|
2817
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
2818
|
-
} else {
|
|
2819
|
-
query[key] = value;
|
|
2820
|
-
}
|
|
3322
|
+
appendOwn(query, key, value);
|
|
2821
3323
|
}
|
|
2822
3324
|
const contentType = headers["content-type"] || "";
|
|
2823
3325
|
let body = {};
|
|
@@ -2858,7 +3360,7 @@ function parseCookies2(header) {
|
|
|
2858
3360
|
if (idx === -1) {
|
|
2859
3361
|
continue;
|
|
2860
3362
|
}
|
|
2861
|
-
cookies
|
|
3363
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2862
3364
|
}
|
|
2863
3365
|
return cookies;
|
|
2864
3366
|
}
|
|
@@ -2914,6 +3416,67 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2914
3416
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
2915
3417
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
2916
3418
|
const MAX_REDIRECTS = 20;
|
|
3419
|
+
const MAX_REPLAY_BODY_BYTES = 1024 * 1024;
|
|
3420
|
+
const captureReplayBody = (request) => {
|
|
3421
|
+
if (request.method === "GET" || request.method === "HEAD" || !request.body) {
|
|
3422
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: false }), cancel() {
|
|
3423
|
+
} };
|
|
3424
|
+
}
|
|
3425
|
+
let reader;
|
|
3426
|
+
try {
|
|
3427
|
+
reader = request.clone().body?.getReader();
|
|
3428
|
+
} catch {
|
|
3429
|
+
reader = null;
|
|
3430
|
+
}
|
|
3431
|
+
if (!reader) {
|
|
3432
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: true }), cancel() {
|
|
3433
|
+
} };
|
|
3434
|
+
}
|
|
3435
|
+
let cancelled = false;
|
|
3436
|
+
const result = (async () => {
|
|
3437
|
+
const chunks = [];
|
|
3438
|
+
let total = 0;
|
|
3439
|
+
try {
|
|
3440
|
+
for (; ; ) {
|
|
3441
|
+
const { done, value } = await reader.read();
|
|
3442
|
+
if (done || cancelled) break;
|
|
3443
|
+
total += value.byteLength;
|
|
3444
|
+
if (total > MAX_REPLAY_BODY_BYTES) {
|
|
3445
|
+
void reader.cancel().catch(() => {
|
|
3446
|
+
});
|
|
3447
|
+
return { body: void 0, tooLarge: true };
|
|
3448
|
+
}
|
|
3449
|
+
chunks.push(value);
|
|
3450
|
+
}
|
|
3451
|
+
if (cancelled) return { body: void 0, tooLarge: false };
|
|
3452
|
+
const body = new Uint8Array(total);
|
|
3453
|
+
let offset = 0;
|
|
3454
|
+
for (const chunk of chunks) {
|
|
3455
|
+
body.set(chunk, offset);
|
|
3456
|
+
offset += chunk.byteLength;
|
|
3457
|
+
}
|
|
3458
|
+
return { body, tooLarge: false };
|
|
3459
|
+
} catch {
|
|
3460
|
+
return { body: void 0, tooLarge: true };
|
|
3461
|
+
} finally {
|
|
3462
|
+
reader.releaseLock();
|
|
3463
|
+
}
|
|
3464
|
+
})();
|
|
3465
|
+
return {
|
|
3466
|
+
result,
|
|
3467
|
+
cancel() {
|
|
3468
|
+
cancelled = true;
|
|
3469
|
+
void reader.cancel().catch(() => {
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
};
|
|
3473
|
+
};
|
|
3474
|
+
const discardResponseBody = async (response) => {
|
|
3475
|
+
try {
|
|
3476
|
+
await response?.body?.cancel?.();
|
|
3477
|
+
} catch {
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
2917
3480
|
const screenUrl = async (u, method) => {
|
|
2918
3481
|
let host = null;
|
|
2919
3482
|
try {
|
|
@@ -2939,20 +3502,43 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2939
3502
|
if (callerRedirect !== "follow") return originalFetch(input, init);
|
|
2940
3503
|
const headers = new Headers(cur.headers);
|
|
2941
3504
|
const signal = cur.signal;
|
|
2942
|
-
|
|
3505
|
+
const replay = captureReplayBody(cur);
|
|
3506
|
+
let body;
|
|
2943
3507
|
for (let hop = 0; ; hop++) {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
3508
|
+
let resp;
|
|
3509
|
+
try {
|
|
3510
|
+
resp = await originalFetch(
|
|
3511
|
+
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
3512
|
+
);
|
|
3513
|
+
} catch (error) {
|
|
3514
|
+
replay.cancel();
|
|
3515
|
+
throw error;
|
|
3516
|
+
}
|
|
2947
3517
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
2948
|
-
if (!location)
|
|
2949
|
-
|
|
3518
|
+
if (!location) {
|
|
3519
|
+
replay.cancel();
|
|
3520
|
+
return resp;
|
|
3521
|
+
}
|
|
3522
|
+
await discardResponseBody(resp);
|
|
3523
|
+
if (hop >= MAX_REDIRECTS) {
|
|
3524
|
+
replay.cancel();
|
|
3525
|
+
throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
3526
|
+
}
|
|
2950
3527
|
const next = new URL(location, url).href;
|
|
2951
3528
|
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
2952
3529
|
method = "GET";
|
|
2953
3530
|
body = void 0;
|
|
3531
|
+
replay.cancel();
|
|
2954
3532
|
headers.delete("content-type");
|
|
2955
3533
|
headers.delete("content-length");
|
|
3534
|
+
} else if (body === void 0 && method !== "GET" && method !== "HEAD") {
|
|
3535
|
+
const captured = await replay.result;
|
|
3536
|
+
if (captured.tooLarge) {
|
|
3537
|
+
throw new Error(
|
|
3538
|
+
`Patchstack blocked a redirect that required replaying more than ${MAX_REPLAY_BODY_BYTES} bytes`
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
body = captured.body;
|
|
2956
3542
|
}
|
|
2957
3543
|
if (new URL(next).origin !== new URL(url).origin) {
|
|
2958
3544
|
headers.delete("authorization");
|
|
@@ -3263,7 +3849,7 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
3263
3849
|
//
|
|
3264
3850
|
// Accepts a real newline and a JSON-escaped one. Most traces reach a client inside a JSON error
|
|
3265
3851
|
// body, where the newline is the two characters `\` and `n`.
|
|
3266
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(?:\\n|\\\\n)\\s*at\\s
|
|
3852
|
+
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+\\)/" } }]
|
|
3267
3853
|
},
|
|
3268
3854
|
{
|
|
3269
3855
|
id: "resp-sql-error",
|
|
@@ -3396,9 +3982,18 @@ async function loadFs() {
|
|
|
3396
3982
|
try {
|
|
3397
3983
|
const [fs, path] = await Promise.all([Promise.resolve().then(() => __toESM(require_ps_edge_stub(), 1)), Promise.resolve().then(() => __toESM(require_ps_edge_stub(), 1))]);
|
|
3398
3984
|
fsMod = {
|
|
3985
|
+
chmodSync: fs.chmodSync,
|
|
3986
|
+
closeSync: fs.closeSync,
|
|
3987
|
+
constants: fs.constants,
|
|
3988
|
+
fstatSync: fs.fstatSync,
|
|
3989
|
+
fsyncSync: fs.fsyncSync,
|
|
3990
|
+
lstatSync: fs.lstatSync,
|
|
3991
|
+
openSync: fs.openSync,
|
|
3399
3992
|
readFileSync: fs.readFileSync,
|
|
3400
|
-
writeFileSync: fs.writeFileSync,
|
|
3401
3993
|
mkdirSync: fs.mkdirSync,
|
|
3994
|
+
renameSync: fs.renameSync,
|
|
3995
|
+
unlinkSync: fs.unlinkSync,
|
|
3996
|
+
writeFileSync: fs.writeFileSync,
|
|
3402
3997
|
join: path.join
|
|
3403
3998
|
};
|
|
3404
3999
|
} catch {
|
|
@@ -3451,21 +4046,83 @@ async function cacheWrite(dir, env) {
|
|
|
3451
4046
|
if (!dir) return;
|
|
3452
4047
|
const fs = await loadFs();
|
|
3453
4048
|
if (!fs) return;
|
|
4049
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
4050
|
+
const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
|
|
4051
|
+
let descriptor = null;
|
|
3454
4052
|
try {
|
|
3455
4053
|
fs.mkdirSync(dir, { recursive: true });
|
|
3456
|
-
fs.
|
|
4054
|
+
const directory = fs.lstatSync(dir);
|
|
4055
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return;
|
|
4056
|
+
let mode;
|
|
4057
|
+
try {
|
|
4058
|
+
const current = fs.lstatSync(target);
|
|
4059
|
+
if (current.isSymbolicLink() || !current.isFile()) return;
|
|
4060
|
+
mode = current.mode & 4095;
|
|
4061
|
+
} catch (error) {
|
|
4062
|
+
if (error?.code !== "ENOENT") return;
|
|
4063
|
+
}
|
|
4064
|
+
descriptor = fs.openSync(temporary, "wx", mode ?? 384);
|
|
4065
|
+
fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
|
|
4066
|
+
fs.fsyncSync(descriptor);
|
|
4067
|
+
fs.closeSync(descriptor);
|
|
4068
|
+
descriptor = null;
|
|
4069
|
+
if (mode !== void 0) fs.chmodSync(temporary, mode);
|
|
4070
|
+
try {
|
|
4071
|
+
fs.renameSync(temporary, target);
|
|
4072
|
+
} catch (error) {
|
|
4073
|
+
if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
|
|
4074
|
+
try {
|
|
4075
|
+
fs.unlinkSync(target);
|
|
4076
|
+
} catch (unlinkError) {
|
|
4077
|
+
if (unlinkError?.code !== "ENOENT") throw unlinkError;
|
|
4078
|
+
}
|
|
4079
|
+
fs.renameSync(temporary, target);
|
|
4080
|
+
}
|
|
3457
4081
|
} catch {
|
|
4082
|
+
} finally {
|
|
4083
|
+
if (descriptor !== null) {
|
|
4084
|
+
try {
|
|
4085
|
+
fs.closeSync(descriptor);
|
|
4086
|
+
} catch {
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
try {
|
|
4090
|
+
fs.unlinkSync(temporary);
|
|
4091
|
+
} catch {
|
|
4092
|
+
}
|
|
3458
4093
|
}
|
|
3459
4094
|
}
|
|
3460
4095
|
async function cacheRead(dir) {
|
|
3461
4096
|
if (!dir) return null;
|
|
3462
4097
|
const fs = await loadFs();
|
|
3463
4098
|
if (!fs) return null;
|
|
4099
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
4100
|
+
let descriptor = null;
|
|
3464
4101
|
try {
|
|
3465
|
-
|
|
4102
|
+
const entry = fs.lstatSync(target);
|
|
4103
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
|
|
4104
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
4105
|
+
descriptor = fs.openSync(target, flags);
|
|
4106
|
+
const opened = fs.fstatSync(descriptor);
|
|
4107
|
+
if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
|
|
4108
|
+
return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
|
|
3466
4109
|
} catch {
|
|
3467
4110
|
return null;
|
|
4111
|
+
} finally {
|
|
4112
|
+
if (descriptor !== null) {
|
|
4113
|
+
try {
|
|
4114
|
+
fs.closeSync(descriptor);
|
|
4115
|
+
} catch {
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
function randomSuffix() {
|
|
4121
|
+
try {
|
|
4122
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
4123
|
+
} catch {
|
|
3468
4124
|
}
|
|
4125
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3469
4126
|
}
|
|
3470
4127
|
function toEnvelope(value) {
|
|
3471
4128
|
if (!value || typeof value !== "object") return null;
|
|
@@ -3491,7 +4148,11 @@ function toEnvelope(value) {
|
|
|
3491
4148
|
function liveUpdateRejections(res, options) {
|
|
3492
4149
|
if (options.acceptPartialBundle) return [];
|
|
3493
4150
|
const { rejected } = validateBundle(
|
|
3494
|
-
{
|
|
4151
|
+
{
|
|
4152
|
+
firewall: Array.isArray(res.firewall) ? res.firewall : [],
|
|
4153
|
+
whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
|
|
4154
|
+
whitelist_keys: res.whitelist_keys
|
|
4155
|
+
},
|
|
3495
4156
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3496
4157
|
);
|
|
3497
4158
|
return rejected;
|
|
@@ -3662,7 +4323,8 @@ function normalizeBundle(b, options = {}) {
|
|
|
3662
4323
|
const { bundle: checked, rejected } = validateBundle(
|
|
3663
4324
|
{
|
|
3664
4325
|
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
3665
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : []
|
|
4326
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
4327
|
+
whitelist_keys: b.whitelist_keys
|
|
3666
4328
|
},
|
|
3667
4329
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3668
4330
|
);
|
|
@@ -3682,7 +4344,7 @@ function normalizeBundle(b, options = {}) {
|
|
|
3682
4344
|
return {
|
|
3683
4345
|
firewall: checked.firewall,
|
|
3684
4346
|
whitelists: checked.whitelists,
|
|
3685
|
-
whitelist_keys:
|
|
4347
|
+
whitelist_keys: checked.whitelist_keys,
|
|
3686
4348
|
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
3687
4349
|
};
|
|
3688
4350
|
}
|
|
@@ -3726,18 +4388,23 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
3726
4388
|
};
|
|
3727
4389
|
}
|
|
3728
4390
|
function makeRefreshHandler(tick, secret) {
|
|
4391
|
+
let inflight2 = null;
|
|
4392
|
+
const runOnce = () => {
|
|
4393
|
+
if (inflight2) return inflight2;
|
|
4394
|
+
let current;
|
|
4395
|
+
current = Promise.resolve().then(() => tick()).finally(() => {
|
|
4396
|
+
if (inflight2 === current) inflight2 = null;
|
|
4397
|
+
});
|
|
4398
|
+
inflight2 = current;
|
|
4399
|
+
return current;
|
|
4400
|
+
};
|
|
3729
4401
|
return async (request) => {
|
|
3730
4402
|
if (!secret) return new Response("not found", { status: 404 });
|
|
3731
|
-
|
|
3732
|
-
try {
|
|
3733
|
-
provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
|
|
3734
|
-
} catch {
|
|
3735
|
-
provided = null;
|
|
3736
|
-
}
|
|
4403
|
+
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
3737
4404
|
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
3738
4405
|
let refreshed = true;
|
|
3739
4406
|
try {
|
|
3740
|
-
const status = await
|
|
4407
|
+
const status = await runOnce();
|
|
3741
4408
|
if (status && status.ok === false) refreshed = false;
|
|
3742
4409
|
} catch {
|
|
3743
4410
|
refreshed = false;
|
|
@@ -4486,8 +5153,10 @@ function hardensWithoutBody(rule) {
|
|
|
4486
5153
|
// src/protect/firewall-log.js
|
|
4487
5154
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
4488
5155
|
var STOP_BUDGET_MS2 = 5e3;
|
|
5156
|
+
var ATTEMPT_TIMEOUT_MS2 = 1e4;
|
|
4489
5157
|
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
4490
5158
|
var MAX_BATCH2 = 50;
|
|
5159
|
+
var MAX_QUEUE2 = 500;
|
|
4491
5160
|
var TOKEN_SKEW_MS2 = 6e4;
|
|
4492
5161
|
function parseApiKey(apiKey) {
|
|
4493
5162
|
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
@@ -4509,14 +5178,16 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
4509
5178
|
}
|
|
4510
5179
|
if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
|
|
4511
5180
|
try {
|
|
4512
|
-
|
|
5181
|
+
const candidate = new URL(pulseOrManifestUrl).origin;
|
|
5182
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4513
5183
|
} catch {
|
|
4514
5184
|
}
|
|
4515
5185
|
}
|
|
4516
5186
|
const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
|
|
4517
5187
|
if (typeof endpoint === "string" && endpoint.length > 0) {
|
|
4518
5188
|
try {
|
|
4519
|
-
|
|
5189
|
+
const candidate = new URL(endpoint).origin;
|
|
5190
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4520
5191
|
} catch {
|
|
4521
5192
|
}
|
|
4522
5193
|
}
|
|
@@ -4528,16 +5199,16 @@ function createFirewallLogReporter(opts) {
|
|
|
4528
5199
|
return { record() {
|
|
4529
5200
|
}, flush: () => Promise.resolve(), stop: () => Promise.resolve() };
|
|
4530
5201
|
}
|
|
4531
|
-
const apiBase = (opts.apiBase
|
|
5202
|
+
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
4532
5203
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4533
5204
|
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
|
|
4534
5205
|
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
4535
5206
|
let queue = [];
|
|
4536
5207
|
let timer = null;
|
|
4537
5208
|
let stopped = false;
|
|
4538
|
-
|
|
5209
|
+
let inFlight = null;
|
|
4539
5210
|
let drainPromise = null;
|
|
4540
|
-
|
|
5211
|
+
let activeController = null;
|
|
4541
5212
|
let ended = false;
|
|
4542
5213
|
let cachedToken = null;
|
|
4543
5214
|
let tokenInflight = null;
|
|
@@ -4563,9 +5234,9 @@ function createFirewallLogReporter(opts) {
|
|
|
4563
5234
|
})
|
|
4564
5235
|
});
|
|
4565
5236
|
if (!res || !res.ok) return null;
|
|
4566
|
-
const body = await res
|
|
5237
|
+
const body = await readBoundedJson2(res);
|
|
4567
5238
|
const token = body?.access_token;
|
|
4568
|
-
if (typeof token !== "string" || token.length === 0) return null;
|
|
5239
|
+
if (typeof token !== "string" || token.length === 0 || token.length > 8192 || /[\u0000-\u0020\u007f-\u009f]/.test(token)) return null;
|
|
4569
5240
|
const expiresIn = Number(body?.expires_in);
|
|
4570
5241
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
4571
5242
|
cachedToken = { token, expiresAt: Date.now() + ttlMs };
|
|
@@ -4583,12 +5254,18 @@ function createFirewallLogReporter(opts) {
|
|
|
4583
5254
|
clearTimeout(timer);
|
|
4584
5255
|
timer = null;
|
|
4585
5256
|
}
|
|
4586
|
-
if (ended ||
|
|
5257
|
+
if (ended || typeof fetchImpl !== "function") return Promise.resolve();
|
|
5258
|
+
if (inFlight) return inFlight;
|
|
5259
|
+
if (queue.length === 0) return Promise.resolve();
|
|
4587
5260
|
const batch = queue.splice(0, MAX_BATCH2);
|
|
5261
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
5262
|
+
activeController = controller;
|
|
5263
|
+
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
5264
|
+
attemptTimer?.unref?.();
|
|
4588
5265
|
let entry;
|
|
4589
|
-
|
|
5266
|
+
entry = (async () => {
|
|
4590
5267
|
try {
|
|
4591
|
-
const token = await fetchAccessToken(
|
|
5268
|
+
const token = await fetchAccessToken(controller?.signal);
|
|
4592
5269
|
if (!token || ended) return;
|
|
4593
5270
|
const body = new URLSearchParams();
|
|
4594
5271
|
body.set("type", "firewall");
|
|
@@ -4603,19 +5280,24 @@ function createFirewallLogReporter(opts) {
|
|
|
4603
5280
|
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
4604
5281
|
},
|
|
4605
5282
|
body,
|
|
4606
|
-
// Both phases carry
|
|
4607
|
-
...
|
|
5283
|
+
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
5284
|
+
...controller ? { signal: controller.signal } : {}
|
|
4608
5285
|
});
|
|
4609
5286
|
if (p && typeof p.then === "function") await p.catch(() => {
|
|
4610
5287
|
});
|
|
4611
5288
|
} catch {
|
|
4612
5289
|
} finally {
|
|
4613
|
-
|
|
5290
|
+
clearTimeout(attemptTimer);
|
|
5291
|
+
if (activeController === controller) activeController = null;
|
|
5292
|
+
if (inFlight === entry) inFlight = null;
|
|
5293
|
+
if (!stopped && !ended && queue.length > 0 && !timer) {
|
|
5294
|
+
timer = setTimeout(flush, queue.length >= MAX_BATCH2 ? 0 : flushMs);
|
|
5295
|
+
timer?.unref?.();
|
|
5296
|
+
}
|
|
4614
5297
|
}
|
|
4615
5298
|
})();
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
return send;
|
|
5299
|
+
inFlight = entry;
|
|
5300
|
+
return entry;
|
|
4619
5301
|
};
|
|
4620
5302
|
return {
|
|
4621
5303
|
/**
|
|
@@ -4631,6 +5313,7 @@ function createFirewallLogReporter(opts) {
|
|
|
4631
5313
|
if (stopped) return;
|
|
4632
5314
|
const fid = event?.rule?.id;
|
|
4633
5315
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
5316
|
+
if (queue.length >= MAX_QUEUE2) return;
|
|
4634
5317
|
queue.push({
|
|
4635
5318
|
fid,
|
|
4636
5319
|
method: event.method ?? null,
|
|
@@ -4662,9 +5345,10 @@ function createFirewallLogReporter(opts) {
|
|
|
4662
5345
|
}
|
|
4663
5346
|
const terminate = () => {
|
|
4664
5347
|
ended = true;
|
|
4665
|
-
|
|
5348
|
+
activeController?.abort();
|
|
5349
|
+
activeController = null;
|
|
4666
5350
|
queue = [];
|
|
4667
|
-
|
|
5351
|
+
inFlight = null;
|
|
4668
5352
|
};
|
|
4669
5353
|
let budget = null;
|
|
4670
5354
|
const spent = new Promise((resolve) => {
|
|
@@ -4676,8 +5360,8 @@ function createFirewallLogReporter(opts) {
|
|
|
4676
5360
|
});
|
|
4677
5361
|
const work = (async () => {
|
|
4678
5362
|
while (!ended) {
|
|
4679
|
-
if (
|
|
4680
|
-
await
|
|
5363
|
+
if (inFlight) {
|
|
5364
|
+
await inFlight;
|
|
4681
5365
|
continue;
|
|
4682
5366
|
}
|
|
4683
5367
|
if (queue.length === 0) break;
|
|
@@ -4701,10 +5385,107 @@ function telemetryEnabled() {
|
|
|
4701
5385
|
|
|
4702
5386
|
// src/protect/supabase-guard.js
|
|
4703
5387
|
var GUARD_PATH = "/_patchstack/guard";
|
|
4704
|
-
var
|
|
4705
|
-
|
|
5388
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5389
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5390
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5391
|
+
"connection",
|
|
5392
|
+
"cookie",
|
|
5393
|
+
"cookie2",
|
|
5394
|
+
"forwarded",
|
|
5395
|
+
"host",
|
|
5396
|
+
"keep-alive",
|
|
5397
|
+
"proxy-authenticate",
|
|
5398
|
+
"proxy-authorization",
|
|
5399
|
+
"te",
|
|
5400
|
+
"trailer",
|
|
5401
|
+
"transfer-encoding",
|
|
5402
|
+
"upgrade",
|
|
5403
|
+
"x-forwarded-for",
|
|
5404
|
+
"x-forwarded-host",
|
|
5405
|
+
"x-forwarded-port",
|
|
5406
|
+
"x-forwarded-proto",
|
|
5407
|
+
"x-ps-target",
|
|
5408
|
+
"x-real-ip"
|
|
5409
|
+
]);
|
|
5410
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5411
|
+
"connection",
|
|
5412
|
+
"content-encoding",
|
|
5413
|
+
"content-length",
|
|
5414
|
+
"keep-alive",
|
|
5415
|
+
"proxy-authenticate",
|
|
5416
|
+
"proxy-authorization",
|
|
5417
|
+
"set-cookie",
|
|
5418
|
+
"set-cookie2",
|
|
5419
|
+
"te",
|
|
5420
|
+
"trailer",
|
|
5421
|
+
"transfer-encoding",
|
|
5422
|
+
"upgrade"
|
|
5423
|
+
]);
|
|
5424
|
+
var BodyTooLargeError = class extends Error {
|
|
5425
|
+
};
|
|
5426
|
+
async function boundedBody(request, maxBytes) {
|
|
5427
|
+
const stated = Number(request.headers.get("content-length"));
|
|
5428
|
+
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5429
|
+
if (!request.body) return void 0;
|
|
5430
|
+
const reader = request.body.getReader();
|
|
5431
|
+
const chunks = [];
|
|
5432
|
+
let total = 0;
|
|
5433
|
+
try {
|
|
5434
|
+
for (; ; ) {
|
|
5435
|
+
const { done, value } = await reader.read();
|
|
5436
|
+
if (done) break;
|
|
5437
|
+
total += value.byteLength;
|
|
5438
|
+
if (total > maxBytes) {
|
|
5439
|
+
void reader.cancel().catch(() => {
|
|
5440
|
+
});
|
|
5441
|
+
throw new BodyTooLargeError();
|
|
5442
|
+
}
|
|
5443
|
+
chunks.push(value);
|
|
5444
|
+
}
|
|
5445
|
+
} finally {
|
|
5446
|
+
reader.releaseLock();
|
|
5447
|
+
}
|
|
5448
|
+
const body = new Uint8Array(total);
|
|
5449
|
+
let offset = 0;
|
|
5450
|
+
for (const chunk of chunks) {
|
|
5451
|
+
body.set(chunk, offset);
|
|
5452
|
+
offset += chunk.byteLength;
|
|
5453
|
+
}
|
|
5454
|
+
return body;
|
|
5455
|
+
}
|
|
5456
|
+
function requestHeaders(input) {
|
|
5457
|
+
const output = new Headers();
|
|
5458
|
+
const connectionTokens = new Set(
|
|
5459
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5460
|
+
);
|
|
5461
|
+
input.forEach((value, key) => {
|
|
5462
|
+
const lower = key.toLowerCase();
|
|
5463
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5464
|
+
});
|
|
5465
|
+
return output;
|
|
5466
|
+
}
|
|
5467
|
+
function responseHeaders(input) {
|
|
5468
|
+
const output = new Headers();
|
|
5469
|
+
const connectionTokens = new Set(
|
|
5470
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5471
|
+
);
|
|
5472
|
+
input.forEach((value, key) => {
|
|
5473
|
+
const lower = key.toLowerCase();
|
|
5474
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5475
|
+
});
|
|
5476
|
+
return output;
|
|
5477
|
+
}
|
|
5478
|
+
function createSupabaseGuard({
|
|
5479
|
+
protection,
|
|
5480
|
+
supabaseUrl,
|
|
5481
|
+
fetchImpl = fetch,
|
|
5482
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5483
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5484
|
+
}) {
|
|
4706
5485
|
const guard = protection.fetchGuard();
|
|
4707
5486
|
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5487
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5488
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
4708
5489
|
return async function handleGuardRequest(request) {
|
|
4709
5490
|
const target = request.headers.get("x-ps-target");
|
|
4710
5491
|
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
@@ -4718,29 +5499,45 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
4718
5499
|
return new Response("patchstack: target not allowed", { status: 403 });
|
|
4719
5500
|
}
|
|
4720
5501
|
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
4721
|
-
|
|
5502
|
+
let body;
|
|
5503
|
+
try {
|
|
5504
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5505
|
+
} catch (error) {
|
|
5506
|
+
if (error instanceof BodyTooLargeError) {
|
|
5507
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
5508
|
+
}
|
|
5509
|
+
throw error;
|
|
5510
|
+
}
|
|
5511
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
4722
5512
|
const evalReq = new Request(targetUrl.toString(), {
|
|
4723
5513
|
method: request.method,
|
|
5514
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5515
|
+
// sanitized at the cross-origin boundary below.
|
|
4724
5516
|
headers: request.headers,
|
|
4725
|
-
body
|
|
5517
|
+
body
|
|
4726
5518
|
});
|
|
4727
5519
|
const blocked = await guard(evalReq);
|
|
4728
5520
|
if (blocked) return blocked;
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
5521
|
+
let upstream;
|
|
5522
|
+
try {
|
|
5523
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5524
|
+
method: request.method,
|
|
5525
|
+
headers: forwardHeaders,
|
|
5526
|
+
body,
|
|
5527
|
+
redirect: "manual",
|
|
5528
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
5529
|
+
});
|
|
5530
|
+
} catch (error) {
|
|
5531
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
5532
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
5533
|
+
}
|
|
5534
|
+
throw error;
|
|
5535
|
+
}
|
|
5536
|
+
const forwarded = new Response(upstream.body, {
|
|
5537
|
+
status: upstream.status,
|
|
5538
|
+
statusText: upstream.statusText,
|
|
5539
|
+
headers: responseHeaders(upstream.headers)
|
|
4741
5540
|
});
|
|
4742
|
-
const buf = await upstream.arrayBuffer();
|
|
4743
|
-
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
4744
5541
|
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
4745
5542
|
};
|
|
4746
5543
|
}
|
|
@@ -5043,9 +5840,13 @@ async function createProtection(options = {}) {
|
|
|
5043
5840
|
for (const name of Object.keys(headers)) {
|
|
5044
5841
|
const value = headers[name];
|
|
5045
5842
|
if (typeof value === "string") {
|
|
5046
|
-
headers
|
|
5843
|
+
setOwn(headers, name, applyRedactors(value, spanRedactors, mask));
|
|
5047
5844
|
} else if (Array.isArray(value)) {
|
|
5048
|
-
|
|
5845
|
+
setOwn(
|
|
5846
|
+
headers,
|
|
5847
|
+
name,
|
|
5848
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item)
|
|
5849
|
+
);
|
|
5049
5850
|
}
|
|
5050
5851
|
}
|
|
5051
5852
|
}
|
|
@@ -5095,7 +5896,13 @@ async function createProtection(options = {}) {
|
|
|
5095
5896
|
let result;
|
|
5096
5897
|
let shaped;
|
|
5097
5898
|
try {
|
|
5098
|
-
shaped = await fromFetchRequest(request, {
|
|
5899
|
+
shaped = await fromFetchRequest(request, {
|
|
5900
|
+
trustedProxy: options.trustedProxy,
|
|
5901
|
+
maxBodyBytes: options.maxBodyBytes
|
|
5902
|
+
});
|
|
5903
|
+
if (shaped?._bodyInspectionSkip) {
|
|
5904
|
+
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
5905
|
+
}
|
|
5099
5906
|
result = engine.evaluate(shaped);
|
|
5100
5907
|
} catch (err) {
|
|
5101
5908
|
notify(onError, err, "onError");
|
|
@@ -5185,7 +5992,7 @@ async function createProtection(options = {}) {
|
|
|
5185
5992
|
const out = {};
|
|
5186
5993
|
for (const [name, value] of writeHeadEntries(headers)) {
|
|
5187
5994
|
const key = name.toLowerCase();
|
|
5188
|
-
|
|
5995
|
+
appendOwn(out, key, value);
|
|
5189
5996
|
}
|
|
5190
5997
|
return out;
|
|
5191
5998
|
};
|
|
@@ -5721,7 +6528,6 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5721
6528
|
const reader = body.getReader();
|
|
5722
6529
|
const chunks = [];
|
|
5723
6530
|
let size = 0;
|
|
5724
|
-
let over = false;
|
|
5725
6531
|
let sniffed = !sniff;
|
|
5726
6532
|
try {
|
|
5727
6533
|
for (; ; ) {
|
|
@@ -5730,20 +6536,27 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5730
6536
|
if (!value) continue;
|
|
5731
6537
|
if (!sniffed) {
|
|
5732
6538
|
sniffed = true;
|
|
5733
|
-
if (looksBinary(value))
|
|
6539
|
+
if (looksBinary(value)) {
|
|
6540
|
+
void reader.cancel().catch(() => {
|
|
6541
|
+
});
|
|
6542
|
+
return { skip: "binary-body" };
|
|
6543
|
+
}
|
|
5734
6544
|
}
|
|
5735
6545
|
size += value.byteLength;
|
|
5736
|
-
if (
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
6546
|
+
if (size >= cap) {
|
|
6547
|
+
void reader.cancel().catch(() => {
|
|
6548
|
+
});
|
|
6549
|
+
return { skip: "body-cap" };
|
|
5740
6550
|
}
|
|
5741
6551
|
chunks.push(value);
|
|
5742
6552
|
}
|
|
5743
6553
|
} catch {
|
|
6554
|
+
void reader.cancel().catch(() => {
|
|
6555
|
+
});
|
|
5744
6556
|
return { skip: "read-failed" };
|
|
6557
|
+
} finally {
|
|
6558
|
+
reader.releaseLock();
|
|
5745
6559
|
}
|
|
5746
|
-
if (over) return { skip: "body-cap" };
|
|
5747
6560
|
try {
|
|
5748
6561
|
return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
|
|
5749
6562
|
} catch {
|
|
@@ -5771,10 +6584,10 @@ function concatBytes(chunks, total) {
|
|
|
5771
6584
|
function headerObject(headers) {
|
|
5772
6585
|
const out = {};
|
|
5773
6586
|
headers?.forEach?.((v, k) => {
|
|
5774
|
-
out
|
|
6587
|
+
setOwn(out, k.toLowerCase(), v);
|
|
5775
6588
|
});
|
|
5776
6589
|
const setCookies = headers?.getSetCookie?.();
|
|
5777
|
-
if (setCookies && setCookies.length) out
|
|
6590
|
+
if (setCookies && setCookies.length) setOwn(out, "set-cookie", setCookies);
|
|
5778
6591
|
return out;
|
|
5779
6592
|
}
|
|
5780
6593
|
function hasSpanMutations(rule) {
|
|
@@ -5943,7 +6756,7 @@ function isHeaderMutation(action) {
|
|
|
5943
6756
|
}
|
|
5944
6757
|
function applyHeaderMutation(headers, rule) {
|
|
5945
6758
|
if (rule.action === "remove-header") {
|
|
5946
|
-
for (const name of rule.remove_headers ?? []) headers
|
|
6759
|
+
for (const name of rule.remove_headers ?? []) setOwn(headers, String(name).toLowerCase(), null);
|
|
5947
6760
|
return;
|
|
5948
6761
|
}
|
|
5949
6762
|
if (rule.action === "set-header") {
|
|
@@ -5952,7 +6765,7 @@ function applyHeaderMutation(headers, rule) {
|
|
|
5952
6765
|
const key = String(name).toLowerCase();
|
|
5953
6766
|
const present = headers[key] != null && headers[key] !== "";
|
|
5954
6767
|
if (ensure && present) continue;
|
|
5955
|
-
headers
|
|
6768
|
+
setOwn(headers, key, String(value));
|
|
5956
6769
|
}
|
|
5957
6770
|
return;
|
|
5958
6771
|
}
|