@patchstack/connect 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2K5SHFUM.js +295 -0
- package/dist/chunk-2K5SHFUM.js.map +1 -0
- package/dist/cli.js +1001 -466
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +364 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +375 -37
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +1 -1
- package/dist/protect.cjs +1135 -241
- 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 +988 -179
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +842 -192
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2ALIJGZ3.js → refresh-manifest-3YWIYENU.js} +82 -39
- package/dist/refresh-manifest-3YWIYENU.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;
|
|
2446
2931
|
}
|
|
2447
|
-
if (
|
|
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;
|
|
2937
|
+
}
|
|
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;
|
|
@@ -2581,7 +3076,7 @@ var PulseRuleClient = class {
|
|
|
2581
3076
|
if (!response.ok) {
|
|
2582
3077
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2583
3078
|
}
|
|
2584
|
-
const data = await response
|
|
3079
|
+
const data = await readBoundedJson(response);
|
|
2585
3080
|
if (!data || !Array.isArray(data.firewall)) {
|
|
2586
3081
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2587
3082
|
}
|
|
@@ -2642,19 +3137,18 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2642
3137
|
const method = (request.method || "GET").toUpperCase();
|
|
2643
3138
|
const headers = {};
|
|
2644
3139
|
request.headers.forEach((value, key) => {
|
|
2645
|
-
headers
|
|
3140
|
+
setOwn(headers, key.toLowerCase(), value);
|
|
2646
3141
|
});
|
|
2647
3142
|
const query = {};
|
|
2648
3143
|
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
|
-
}
|
|
3144
|
+
appendOwn(query, key, value);
|
|
2654
3145
|
}
|
|
2655
3146
|
let rawBody = "";
|
|
3147
|
+
let bodyInspectionSkip = null;
|
|
2656
3148
|
if (method !== "GET" && method !== "HEAD") {
|
|
2657
|
-
|
|
3149
|
+
const read = await readCappedText(request, maxBodyBytes);
|
|
3150
|
+
rawBody = read.text;
|
|
3151
|
+
bodyInspectionSkip = read.skip;
|
|
2658
3152
|
}
|
|
2659
3153
|
const contentType = headers["content-type"] || "";
|
|
2660
3154
|
let body = {};
|
|
@@ -2681,7 +3175,8 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2681
3175
|
cookies: parseCookies(headers.cookie),
|
|
2682
3176
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
2683
3177
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
2684
|
-
_rawBody: rawBody
|
|
3178
|
+
_rawBody: rawBody,
|
|
3179
|
+
...bodyInspectionSkip === null ? {} : { _bodyInspectionSkip: bodyInspectionSkip }
|
|
2685
3180
|
};
|
|
2686
3181
|
}
|
|
2687
3182
|
async function readCappedText(request, max) {
|
|
@@ -2689,7 +3184,7 @@ async function readCappedText(request, max) {
|
|
|
2689
3184
|
try {
|
|
2690
3185
|
clone = request.clone();
|
|
2691
3186
|
} catch {
|
|
2692
|
-
return "";
|
|
3187
|
+
return { text: "", skip: "clone-failed" };
|
|
2693
3188
|
}
|
|
2694
3189
|
const body = clone.body;
|
|
2695
3190
|
if (body && typeof body.getReader === "function") {
|
|
@@ -2700,23 +3195,30 @@ async function readCappedText(request, max) {
|
|
|
2700
3195
|
for (; ; ) {
|
|
2701
3196
|
const { done, value } = await reader.read();
|
|
2702
3197
|
if (done) break;
|
|
2703
|
-
if (!value
|
|
3198
|
+
if (!value) continue;
|
|
2704
3199
|
const take = Math.min(value.byteLength, max - buffered);
|
|
2705
|
-
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
3200
|
+
if (take > 0) chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
2706
3201
|
buffered += take;
|
|
3202
|
+
if (buffered >= max) {
|
|
3203
|
+
void reader.cancel().catch(() => {
|
|
3204
|
+
});
|
|
3205
|
+
return { text: decodeChunks(chunks, buffered), skip: "body-cap" };
|
|
3206
|
+
}
|
|
2707
3207
|
}
|
|
2708
3208
|
} catch {
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
return "";
|
|
3209
|
+
void reader.cancel().catch(() => {
|
|
3210
|
+
});
|
|
3211
|
+
return { text: "", skip: "read-failed" };
|
|
3212
|
+
} finally {
|
|
3213
|
+
reader.releaseLock();
|
|
2715
3214
|
}
|
|
3215
|
+
return { text: decodeChunks(chunks, buffered), skip: null };
|
|
2716
3216
|
}
|
|
3217
|
+
return { text: "", skip: "unreadable-body" };
|
|
3218
|
+
}
|
|
3219
|
+
function decodeChunks(chunks, total) {
|
|
2717
3220
|
try {
|
|
2718
|
-
|
|
2719
|
-
return text.length > max ? text.slice(0, max) : text;
|
|
3221
|
+
return new TextDecoder().decode(concatChunks(chunks, total));
|
|
2720
3222
|
} catch {
|
|
2721
3223
|
return "";
|
|
2722
3224
|
}
|
|
@@ -2743,7 +3245,7 @@ function parseBody(rawBody, contentType) {
|
|
|
2743
3245
|
}
|
|
2744
3246
|
if (isForm) {
|
|
2745
3247
|
const body = {};
|
|
2746
|
-
for (const [k, v] of new URLSearchParams(rawBody)) body
|
|
3248
|
+
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
2747
3249
|
return { body, files: void 0 };
|
|
2748
3250
|
}
|
|
2749
3251
|
if (isJson || isAmbiguous) {
|
|
@@ -2771,9 +3273,9 @@ function parseMultipart(rawBody, boundary) {
|
|
|
2771
3273
|
if (filename !== void 0) {
|
|
2772
3274
|
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
2773
3275
|
const file = { filename, type: partType, content };
|
|
2774
|
-
files
|
|
3276
|
+
appendOwn(files, name, file);
|
|
2775
3277
|
} else {
|
|
2776
|
-
body
|
|
3278
|
+
appendOwn(body, name, content);
|
|
2777
3279
|
}
|
|
2778
3280
|
}
|
|
2779
3281
|
return { body, files };
|
|
@@ -2788,7 +3290,7 @@ function parseCookies(header) {
|
|
|
2788
3290
|
if (idx === -1) {
|
|
2789
3291
|
continue;
|
|
2790
3292
|
}
|
|
2791
|
-
cookies
|
|
3293
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2792
3294
|
}
|
|
2793
3295
|
return cookies;
|
|
2794
3296
|
}
|
|
@@ -2798,7 +3300,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2798
3300
|
const method = (req.method || "GET").toUpperCase();
|
|
2799
3301
|
const headers = {};
|
|
2800
3302
|
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
2801
|
-
headers
|
|
3303
|
+
setOwn(headers, key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value);
|
|
2802
3304
|
}
|
|
2803
3305
|
const host = headers.host || "localhost";
|
|
2804
3306
|
let url;
|
|
@@ -2813,11 +3315,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
2813
3315
|
}
|
|
2814
3316
|
const query = {};
|
|
2815
3317
|
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
|
-
}
|
|
3318
|
+
appendOwn(query, key, value);
|
|
2821
3319
|
}
|
|
2822
3320
|
const contentType = headers["content-type"] || "";
|
|
2823
3321
|
let body = {};
|
|
@@ -2858,7 +3356,7 @@ function parseCookies2(header) {
|
|
|
2858
3356
|
if (idx === -1) {
|
|
2859
3357
|
continue;
|
|
2860
3358
|
}
|
|
2861
|
-
cookies
|
|
3359
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
2862
3360
|
}
|
|
2863
3361
|
return cookies;
|
|
2864
3362
|
}
|
|
@@ -2914,6 +3412,67 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2914
3412
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
2915
3413
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
2916
3414
|
const MAX_REDIRECTS = 20;
|
|
3415
|
+
const MAX_REPLAY_BODY_BYTES = 1024 * 1024;
|
|
3416
|
+
const captureReplayBody = (request) => {
|
|
3417
|
+
if (request.method === "GET" || request.method === "HEAD" || !request.body) {
|
|
3418
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: false }), cancel() {
|
|
3419
|
+
} };
|
|
3420
|
+
}
|
|
3421
|
+
let reader;
|
|
3422
|
+
try {
|
|
3423
|
+
reader = request.clone().body?.getReader();
|
|
3424
|
+
} catch {
|
|
3425
|
+
reader = null;
|
|
3426
|
+
}
|
|
3427
|
+
if (!reader) {
|
|
3428
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: true }), cancel() {
|
|
3429
|
+
} };
|
|
3430
|
+
}
|
|
3431
|
+
let cancelled = false;
|
|
3432
|
+
const result = (async () => {
|
|
3433
|
+
const chunks = [];
|
|
3434
|
+
let total = 0;
|
|
3435
|
+
try {
|
|
3436
|
+
for (; ; ) {
|
|
3437
|
+
const { done, value } = await reader.read();
|
|
3438
|
+
if (done || cancelled) break;
|
|
3439
|
+
total += value.byteLength;
|
|
3440
|
+
if (total > MAX_REPLAY_BODY_BYTES) {
|
|
3441
|
+
void reader.cancel().catch(() => {
|
|
3442
|
+
});
|
|
3443
|
+
return { body: void 0, tooLarge: true };
|
|
3444
|
+
}
|
|
3445
|
+
chunks.push(value);
|
|
3446
|
+
}
|
|
3447
|
+
if (cancelled) return { body: void 0, tooLarge: false };
|
|
3448
|
+
const body = new Uint8Array(total);
|
|
3449
|
+
let offset = 0;
|
|
3450
|
+
for (const chunk of chunks) {
|
|
3451
|
+
body.set(chunk, offset);
|
|
3452
|
+
offset += chunk.byteLength;
|
|
3453
|
+
}
|
|
3454
|
+
return { body, tooLarge: false };
|
|
3455
|
+
} catch {
|
|
3456
|
+
return { body: void 0, tooLarge: true };
|
|
3457
|
+
} finally {
|
|
3458
|
+
reader.releaseLock();
|
|
3459
|
+
}
|
|
3460
|
+
})();
|
|
3461
|
+
return {
|
|
3462
|
+
result,
|
|
3463
|
+
cancel() {
|
|
3464
|
+
cancelled = true;
|
|
3465
|
+
void reader.cancel().catch(() => {
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
};
|
|
3470
|
+
const discardResponseBody = async (response) => {
|
|
3471
|
+
try {
|
|
3472
|
+
await response?.body?.cancel?.();
|
|
3473
|
+
} catch {
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
2917
3476
|
const screenUrl = async (u, method) => {
|
|
2918
3477
|
let host = null;
|
|
2919
3478
|
try {
|
|
@@ -2939,20 +3498,43 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2939
3498
|
if (callerRedirect !== "follow") return originalFetch(input, init);
|
|
2940
3499
|
const headers = new Headers(cur.headers);
|
|
2941
3500
|
const signal = cur.signal;
|
|
2942
|
-
|
|
3501
|
+
const replay = captureReplayBody(cur);
|
|
3502
|
+
let body;
|
|
2943
3503
|
for (let hop = 0; ; hop++) {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
3504
|
+
let resp;
|
|
3505
|
+
try {
|
|
3506
|
+
resp = await originalFetch(
|
|
3507
|
+
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
3508
|
+
);
|
|
3509
|
+
} catch (error) {
|
|
3510
|
+
replay.cancel();
|
|
3511
|
+
throw error;
|
|
3512
|
+
}
|
|
2947
3513
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
2948
|
-
if (!location)
|
|
2949
|
-
|
|
3514
|
+
if (!location) {
|
|
3515
|
+
replay.cancel();
|
|
3516
|
+
return resp;
|
|
3517
|
+
}
|
|
3518
|
+
await discardResponseBody(resp);
|
|
3519
|
+
if (hop >= MAX_REDIRECTS) {
|
|
3520
|
+
replay.cancel();
|
|
3521
|
+
throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
3522
|
+
}
|
|
2950
3523
|
const next = new URL(location, url).href;
|
|
2951
3524
|
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
2952
3525
|
method = "GET";
|
|
2953
3526
|
body = void 0;
|
|
3527
|
+
replay.cancel();
|
|
2954
3528
|
headers.delete("content-type");
|
|
2955
3529
|
headers.delete("content-length");
|
|
3530
|
+
} else if (body === void 0 && method !== "GET" && method !== "HEAD") {
|
|
3531
|
+
const captured = await replay.result;
|
|
3532
|
+
if (captured.tooLarge) {
|
|
3533
|
+
throw new Error(
|
|
3534
|
+
`Patchstack blocked a redirect that required replaying more than ${MAX_REPLAY_BODY_BYTES} bytes`
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
body = captured.body;
|
|
2956
3538
|
}
|
|
2957
3539
|
if (new URL(next).origin !== new URL(url).origin) {
|
|
2958
3540
|
headers.delete("authorization");
|
|
@@ -3263,7 +3845,7 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
3263
3845
|
//
|
|
3264
3846
|
// Accepts a real newline and a JSON-escaped one. Most traces reach a client inside a JSON error
|
|
3265
3847
|
// 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
|
|
3848
|
+
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
3849
|
},
|
|
3268
3850
|
{
|
|
3269
3851
|
id: "resp-sql-error",
|
|
@@ -3396,9 +3978,18 @@ async function loadFs() {
|
|
|
3396
3978
|
try {
|
|
3397
3979
|
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
3980
|
fsMod = {
|
|
3981
|
+
chmodSync: fs.chmodSync,
|
|
3982
|
+
closeSync: fs.closeSync,
|
|
3983
|
+
constants: fs.constants,
|
|
3984
|
+
fstatSync: fs.fstatSync,
|
|
3985
|
+
fsyncSync: fs.fsyncSync,
|
|
3986
|
+
lstatSync: fs.lstatSync,
|
|
3987
|
+
openSync: fs.openSync,
|
|
3399
3988
|
readFileSync: fs.readFileSync,
|
|
3400
|
-
writeFileSync: fs.writeFileSync,
|
|
3401
3989
|
mkdirSync: fs.mkdirSync,
|
|
3990
|
+
renameSync: fs.renameSync,
|
|
3991
|
+
unlinkSync: fs.unlinkSync,
|
|
3992
|
+
writeFileSync: fs.writeFileSync,
|
|
3402
3993
|
join: path.join
|
|
3403
3994
|
};
|
|
3404
3995
|
} catch {
|
|
@@ -3451,21 +4042,83 @@ async function cacheWrite(dir, env) {
|
|
|
3451
4042
|
if (!dir) return;
|
|
3452
4043
|
const fs = await loadFs();
|
|
3453
4044
|
if (!fs) return;
|
|
4045
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
4046
|
+
const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
|
|
4047
|
+
let descriptor = null;
|
|
3454
4048
|
try {
|
|
3455
4049
|
fs.mkdirSync(dir, { recursive: true });
|
|
3456
|
-
fs.
|
|
4050
|
+
const directory = fs.lstatSync(dir);
|
|
4051
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return;
|
|
4052
|
+
let mode;
|
|
4053
|
+
try {
|
|
4054
|
+
const current = fs.lstatSync(target);
|
|
4055
|
+
if (current.isSymbolicLink() || !current.isFile()) return;
|
|
4056
|
+
mode = current.mode & 4095;
|
|
4057
|
+
} catch (error) {
|
|
4058
|
+
if (error?.code !== "ENOENT") return;
|
|
4059
|
+
}
|
|
4060
|
+
descriptor = fs.openSync(temporary, "wx", mode ?? 384);
|
|
4061
|
+
fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
|
|
4062
|
+
fs.fsyncSync(descriptor);
|
|
4063
|
+
fs.closeSync(descriptor);
|
|
4064
|
+
descriptor = null;
|
|
4065
|
+
if (mode !== void 0) fs.chmodSync(temporary, mode);
|
|
4066
|
+
try {
|
|
4067
|
+
fs.renameSync(temporary, target);
|
|
4068
|
+
} catch (error) {
|
|
4069
|
+
if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
|
|
4070
|
+
try {
|
|
4071
|
+
fs.unlinkSync(target);
|
|
4072
|
+
} catch (unlinkError) {
|
|
4073
|
+
if (unlinkError?.code !== "ENOENT") throw unlinkError;
|
|
4074
|
+
}
|
|
4075
|
+
fs.renameSync(temporary, target);
|
|
4076
|
+
}
|
|
3457
4077
|
} catch {
|
|
4078
|
+
} finally {
|
|
4079
|
+
if (descriptor !== null) {
|
|
4080
|
+
try {
|
|
4081
|
+
fs.closeSync(descriptor);
|
|
4082
|
+
} catch {
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
try {
|
|
4086
|
+
fs.unlinkSync(temporary);
|
|
4087
|
+
} catch {
|
|
4088
|
+
}
|
|
3458
4089
|
}
|
|
3459
4090
|
}
|
|
3460
4091
|
async function cacheRead(dir) {
|
|
3461
4092
|
if (!dir) return null;
|
|
3462
4093
|
const fs = await loadFs();
|
|
3463
4094
|
if (!fs) return null;
|
|
4095
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
4096
|
+
let descriptor = null;
|
|
3464
4097
|
try {
|
|
3465
|
-
|
|
4098
|
+
const entry = fs.lstatSync(target);
|
|
4099
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
|
|
4100
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
4101
|
+
descriptor = fs.openSync(target, flags);
|
|
4102
|
+
const opened = fs.fstatSync(descriptor);
|
|
4103
|
+
if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
|
|
4104
|
+
return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
|
|
3466
4105
|
} catch {
|
|
3467
4106
|
return null;
|
|
4107
|
+
} finally {
|
|
4108
|
+
if (descriptor !== null) {
|
|
4109
|
+
try {
|
|
4110
|
+
fs.closeSync(descriptor);
|
|
4111
|
+
} catch {
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
function randomSuffix() {
|
|
4117
|
+
try {
|
|
4118
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
4119
|
+
} catch {
|
|
3468
4120
|
}
|
|
4121
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3469
4122
|
}
|
|
3470
4123
|
function toEnvelope(value) {
|
|
3471
4124
|
if (!value || typeof value !== "object") return null;
|
|
@@ -3491,7 +4144,11 @@ function toEnvelope(value) {
|
|
|
3491
4144
|
function liveUpdateRejections(res, options) {
|
|
3492
4145
|
if (options.acceptPartialBundle) return [];
|
|
3493
4146
|
const { rejected } = validateBundle(
|
|
3494
|
-
{
|
|
4147
|
+
{
|
|
4148
|
+
firewall: Array.isArray(res.firewall) ? res.firewall : [],
|
|
4149
|
+
whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
|
|
4150
|
+
whitelist_keys: res.whitelist_keys
|
|
4151
|
+
},
|
|
3495
4152
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3496
4153
|
);
|
|
3497
4154
|
return rejected;
|
|
@@ -3662,7 +4319,8 @@ function normalizeBundle(b, options = {}) {
|
|
|
3662
4319
|
const { bundle: checked, rejected } = validateBundle(
|
|
3663
4320
|
{
|
|
3664
4321
|
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
3665
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : []
|
|
4322
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
4323
|
+
whitelist_keys: b.whitelist_keys
|
|
3666
4324
|
},
|
|
3667
4325
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
3668
4326
|
);
|
|
@@ -3682,7 +4340,7 @@ function normalizeBundle(b, options = {}) {
|
|
|
3682
4340
|
return {
|
|
3683
4341
|
firewall: checked.firewall,
|
|
3684
4342
|
whitelists: checked.whitelists,
|
|
3685
|
-
whitelist_keys:
|
|
4343
|
+
whitelist_keys: checked.whitelist_keys,
|
|
3686
4344
|
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
3687
4345
|
};
|
|
3688
4346
|
}
|
|
@@ -3726,18 +4384,23 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
3726
4384
|
};
|
|
3727
4385
|
}
|
|
3728
4386
|
function makeRefreshHandler(tick, secret) {
|
|
4387
|
+
let inflight2 = null;
|
|
4388
|
+
const runOnce = () => {
|
|
4389
|
+
if (inflight2) return inflight2;
|
|
4390
|
+
let current;
|
|
4391
|
+
current = Promise.resolve().then(() => tick()).finally(() => {
|
|
4392
|
+
if (inflight2 === current) inflight2 = null;
|
|
4393
|
+
});
|
|
4394
|
+
inflight2 = current;
|
|
4395
|
+
return current;
|
|
4396
|
+
};
|
|
3729
4397
|
return async (request) => {
|
|
3730
4398
|
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
|
-
}
|
|
4399
|
+
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
3737
4400
|
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
3738
4401
|
let refreshed = true;
|
|
3739
4402
|
try {
|
|
3740
|
-
const status = await
|
|
4403
|
+
const status = await runOnce();
|
|
3741
4404
|
if (status && status.ok === false) refreshed = false;
|
|
3742
4405
|
} catch {
|
|
3743
4406
|
refreshed = false;
|
|
@@ -4486,8 +5149,10 @@ function hardensWithoutBody(rule) {
|
|
|
4486
5149
|
// src/protect/firewall-log.js
|
|
4487
5150
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
4488
5151
|
var STOP_BUDGET_MS2 = 5e3;
|
|
5152
|
+
var ATTEMPT_TIMEOUT_MS2 = 1e4;
|
|
4489
5153
|
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
4490
5154
|
var MAX_BATCH2 = 50;
|
|
5155
|
+
var MAX_QUEUE2 = 500;
|
|
4491
5156
|
var TOKEN_SKEW_MS2 = 6e4;
|
|
4492
5157
|
function parseApiKey(apiKey) {
|
|
4493
5158
|
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
@@ -4509,14 +5174,16 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
4509
5174
|
}
|
|
4510
5175
|
if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
|
|
4511
5176
|
try {
|
|
4512
|
-
|
|
5177
|
+
const candidate = new URL(pulseOrManifestUrl).origin;
|
|
5178
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4513
5179
|
} catch {
|
|
4514
5180
|
}
|
|
4515
5181
|
}
|
|
4516
5182
|
const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
|
|
4517
5183
|
if (typeof endpoint === "string" && endpoint.length > 0) {
|
|
4518
5184
|
try {
|
|
4519
|
-
|
|
5185
|
+
const candidate = new URL(endpoint).origin;
|
|
5186
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
4520
5187
|
} catch {
|
|
4521
5188
|
}
|
|
4522
5189
|
}
|
|
@@ -4528,16 +5195,16 @@ function createFirewallLogReporter(opts) {
|
|
|
4528
5195
|
return { record() {
|
|
4529
5196
|
}, flush: () => Promise.resolve(), stop: () => Promise.resolve() };
|
|
4530
5197
|
}
|
|
4531
|
-
const apiBase = (opts.apiBase
|
|
5198
|
+
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
4532
5199
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4533
5200
|
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
|
|
4534
5201
|
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
4535
5202
|
let queue = [];
|
|
4536
5203
|
let timer = null;
|
|
4537
5204
|
let stopped = false;
|
|
4538
|
-
|
|
5205
|
+
let inFlight = null;
|
|
4539
5206
|
let drainPromise = null;
|
|
4540
|
-
|
|
5207
|
+
let activeController = null;
|
|
4541
5208
|
let ended = false;
|
|
4542
5209
|
let cachedToken = null;
|
|
4543
5210
|
let tokenInflight = null;
|
|
@@ -4563,9 +5230,9 @@ function createFirewallLogReporter(opts) {
|
|
|
4563
5230
|
})
|
|
4564
5231
|
});
|
|
4565
5232
|
if (!res || !res.ok) return null;
|
|
4566
|
-
const body = await res
|
|
5233
|
+
const body = await readBoundedJson2(res);
|
|
4567
5234
|
const token = body?.access_token;
|
|
4568
|
-
if (typeof token !== "string" || token.length === 0) return null;
|
|
5235
|
+
if (typeof token !== "string" || token.length === 0 || token.length > 8192 || /[\u0000-\u0020\u007f-\u009f]/.test(token)) return null;
|
|
4569
5236
|
const expiresIn = Number(body?.expires_in);
|
|
4570
5237
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
4571
5238
|
cachedToken = { token, expiresAt: Date.now() + ttlMs };
|
|
@@ -4583,12 +5250,18 @@ function createFirewallLogReporter(opts) {
|
|
|
4583
5250
|
clearTimeout(timer);
|
|
4584
5251
|
timer = null;
|
|
4585
5252
|
}
|
|
4586
|
-
if (ended ||
|
|
5253
|
+
if (ended || typeof fetchImpl !== "function") return Promise.resolve();
|
|
5254
|
+
if (inFlight) return inFlight;
|
|
5255
|
+
if (queue.length === 0) return Promise.resolve();
|
|
4587
5256
|
const batch = queue.splice(0, MAX_BATCH2);
|
|
5257
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
5258
|
+
activeController = controller;
|
|
5259
|
+
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
5260
|
+
attemptTimer?.unref?.();
|
|
4588
5261
|
let entry;
|
|
4589
|
-
|
|
5262
|
+
entry = (async () => {
|
|
4590
5263
|
try {
|
|
4591
|
-
const token = await fetchAccessToken(
|
|
5264
|
+
const token = await fetchAccessToken(controller?.signal);
|
|
4592
5265
|
if (!token || ended) return;
|
|
4593
5266
|
const body = new URLSearchParams();
|
|
4594
5267
|
body.set("type", "firewall");
|
|
@@ -4603,19 +5276,24 @@ function createFirewallLogReporter(opts) {
|
|
|
4603
5276
|
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
4604
5277
|
},
|
|
4605
5278
|
body,
|
|
4606
|
-
// Both phases carry
|
|
4607
|
-
...
|
|
5279
|
+
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
5280
|
+
...controller ? { signal: controller.signal } : {}
|
|
4608
5281
|
});
|
|
4609
5282
|
if (p && typeof p.then === "function") await p.catch(() => {
|
|
4610
5283
|
});
|
|
4611
5284
|
} catch {
|
|
4612
5285
|
} finally {
|
|
4613
|
-
|
|
5286
|
+
clearTimeout(attemptTimer);
|
|
5287
|
+
if (activeController === controller) activeController = null;
|
|
5288
|
+
if (inFlight === entry) inFlight = null;
|
|
5289
|
+
if (!stopped && !ended && queue.length > 0 && !timer) {
|
|
5290
|
+
timer = setTimeout(flush, queue.length >= MAX_BATCH2 ? 0 : flushMs);
|
|
5291
|
+
timer?.unref?.();
|
|
5292
|
+
}
|
|
4614
5293
|
}
|
|
4615
5294
|
})();
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
return send;
|
|
5295
|
+
inFlight = entry;
|
|
5296
|
+
return entry;
|
|
4619
5297
|
};
|
|
4620
5298
|
return {
|
|
4621
5299
|
/**
|
|
@@ -4631,6 +5309,7 @@ function createFirewallLogReporter(opts) {
|
|
|
4631
5309
|
if (stopped) return;
|
|
4632
5310
|
const fid = event?.rule?.id;
|
|
4633
5311
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
5312
|
+
if (queue.length >= MAX_QUEUE2) return;
|
|
4634
5313
|
queue.push({
|
|
4635
5314
|
fid,
|
|
4636
5315
|
method: event.method ?? null,
|
|
@@ -4662,9 +5341,10 @@ function createFirewallLogReporter(opts) {
|
|
|
4662
5341
|
}
|
|
4663
5342
|
const terminate = () => {
|
|
4664
5343
|
ended = true;
|
|
4665
|
-
|
|
5344
|
+
activeController?.abort();
|
|
5345
|
+
activeController = null;
|
|
4666
5346
|
queue = [];
|
|
4667
|
-
|
|
5347
|
+
inFlight = null;
|
|
4668
5348
|
};
|
|
4669
5349
|
let budget = null;
|
|
4670
5350
|
const spent = new Promise((resolve) => {
|
|
@@ -4676,8 +5356,8 @@ function createFirewallLogReporter(opts) {
|
|
|
4676
5356
|
});
|
|
4677
5357
|
const work = (async () => {
|
|
4678
5358
|
while (!ended) {
|
|
4679
|
-
if (
|
|
4680
|
-
await
|
|
5359
|
+
if (inFlight) {
|
|
5360
|
+
await inFlight;
|
|
4681
5361
|
continue;
|
|
4682
5362
|
}
|
|
4683
5363
|
if (queue.length === 0) break;
|
|
@@ -4701,10 +5381,107 @@ function telemetryEnabled() {
|
|
|
4701
5381
|
|
|
4702
5382
|
// src/protect/supabase-guard.js
|
|
4703
5383
|
var GUARD_PATH = "/_patchstack/guard";
|
|
4704
|
-
var
|
|
4705
|
-
|
|
5384
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5385
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5386
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5387
|
+
"connection",
|
|
5388
|
+
"cookie",
|
|
5389
|
+
"cookie2",
|
|
5390
|
+
"forwarded",
|
|
5391
|
+
"host",
|
|
5392
|
+
"keep-alive",
|
|
5393
|
+
"proxy-authenticate",
|
|
5394
|
+
"proxy-authorization",
|
|
5395
|
+
"te",
|
|
5396
|
+
"trailer",
|
|
5397
|
+
"transfer-encoding",
|
|
5398
|
+
"upgrade",
|
|
5399
|
+
"x-forwarded-for",
|
|
5400
|
+
"x-forwarded-host",
|
|
5401
|
+
"x-forwarded-port",
|
|
5402
|
+
"x-forwarded-proto",
|
|
5403
|
+
"x-ps-target",
|
|
5404
|
+
"x-real-ip"
|
|
5405
|
+
]);
|
|
5406
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5407
|
+
"connection",
|
|
5408
|
+
"content-encoding",
|
|
5409
|
+
"content-length",
|
|
5410
|
+
"keep-alive",
|
|
5411
|
+
"proxy-authenticate",
|
|
5412
|
+
"proxy-authorization",
|
|
5413
|
+
"set-cookie",
|
|
5414
|
+
"set-cookie2",
|
|
5415
|
+
"te",
|
|
5416
|
+
"trailer",
|
|
5417
|
+
"transfer-encoding",
|
|
5418
|
+
"upgrade"
|
|
5419
|
+
]);
|
|
5420
|
+
var BodyTooLargeError = class extends Error {
|
|
5421
|
+
};
|
|
5422
|
+
async function boundedBody(request, maxBytes) {
|
|
5423
|
+
const stated = Number(request.headers.get("content-length"));
|
|
5424
|
+
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5425
|
+
if (!request.body) return void 0;
|
|
5426
|
+
const reader = request.body.getReader();
|
|
5427
|
+
const chunks = [];
|
|
5428
|
+
let total = 0;
|
|
5429
|
+
try {
|
|
5430
|
+
for (; ; ) {
|
|
5431
|
+
const { done, value } = await reader.read();
|
|
5432
|
+
if (done) break;
|
|
5433
|
+
total += value.byteLength;
|
|
5434
|
+
if (total > maxBytes) {
|
|
5435
|
+
void reader.cancel().catch(() => {
|
|
5436
|
+
});
|
|
5437
|
+
throw new BodyTooLargeError();
|
|
5438
|
+
}
|
|
5439
|
+
chunks.push(value);
|
|
5440
|
+
}
|
|
5441
|
+
} finally {
|
|
5442
|
+
reader.releaseLock();
|
|
5443
|
+
}
|
|
5444
|
+
const body = new Uint8Array(total);
|
|
5445
|
+
let offset = 0;
|
|
5446
|
+
for (const chunk of chunks) {
|
|
5447
|
+
body.set(chunk, offset);
|
|
5448
|
+
offset += chunk.byteLength;
|
|
5449
|
+
}
|
|
5450
|
+
return body;
|
|
5451
|
+
}
|
|
5452
|
+
function requestHeaders(input) {
|
|
5453
|
+
const output = new Headers();
|
|
5454
|
+
const connectionTokens = new Set(
|
|
5455
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5456
|
+
);
|
|
5457
|
+
input.forEach((value, key) => {
|
|
5458
|
+
const lower = key.toLowerCase();
|
|
5459
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5460
|
+
});
|
|
5461
|
+
return output;
|
|
5462
|
+
}
|
|
5463
|
+
function responseHeaders(input) {
|
|
5464
|
+
const output = new Headers();
|
|
5465
|
+
const connectionTokens = new Set(
|
|
5466
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5467
|
+
);
|
|
5468
|
+
input.forEach((value, key) => {
|
|
5469
|
+
const lower = key.toLowerCase();
|
|
5470
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5471
|
+
});
|
|
5472
|
+
return output;
|
|
5473
|
+
}
|
|
5474
|
+
function createSupabaseGuard({
|
|
5475
|
+
protection,
|
|
5476
|
+
supabaseUrl,
|
|
5477
|
+
fetchImpl = fetch,
|
|
5478
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5479
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5480
|
+
}) {
|
|
4706
5481
|
const guard = protection.fetchGuard();
|
|
4707
5482
|
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5483
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5484
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
4708
5485
|
return async function handleGuardRequest(request) {
|
|
4709
5486
|
const target = request.headers.get("x-ps-target");
|
|
4710
5487
|
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
@@ -4718,29 +5495,45 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
4718
5495
|
return new Response("patchstack: target not allowed", { status: 403 });
|
|
4719
5496
|
}
|
|
4720
5497
|
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
4721
|
-
|
|
5498
|
+
let body;
|
|
5499
|
+
try {
|
|
5500
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5501
|
+
} catch (error) {
|
|
5502
|
+
if (error instanceof BodyTooLargeError) {
|
|
5503
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
5504
|
+
}
|
|
5505
|
+
throw error;
|
|
5506
|
+
}
|
|
5507
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
4722
5508
|
const evalReq = new Request(targetUrl.toString(), {
|
|
4723
5509
|
method: request.method,
|
|
5510
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5511
|
+
// sanitized at the cross-origin boundary below.
|
|
4724
5512
|
headers: request.headers,
|
|
4725
|
-
body
|
|
5513
|
+
body
|
|
4726
5514
|
});
|
|
4727
5515
|
const blocked = await guard(evalReq);
|
|
4728
5516
|
if (blocked) return blocked;
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
5517
|
+
let upstream;
|
|
5518
|
+
try {
|
|
5519
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5520
|
+
method: request.method,
|
|
5521
|
+
headers: forwardHeaders,
|
|
5522
|
+
body,
|
|
5523
|
+
redirect: "manual",
|
|
5524
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
5525
|
+
});
|
|
5526
|
+
} catch (error) {
|
|
5527
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
5528
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
5529
|
+
}
|
|
5530
|
+
throw error;
|
|
5531
|
+
}
|
|
5532
|
+
const forwarded = new Response(upstream.body, {
|
|
5533
|
+
status: upstream.status,
|
|
5534
|
+
statusText: upstream.statusText,
|
|
5535
|
+
headers: responseHeaders(upstream.headers)
|
|
4741
5536
|
});
|
|
4742
|
-
const buf = await upstream.arrayBuffer();
|
|
4743
|
-
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
4744
5537
|
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
4745
5538
|
};
|
|
4746
5539
|
}
|
|
@@ -5043,9 +5836,13 @@ async function createProtection(options = {}) {
|
|
|
5043
5836
|
for (const name of Object.keys(headers)) {
|
|
5044
5837
|
const value = headers[name];
|
|
5045
5838
|
if (typeof value === "string") {
|
|
5046
|
-
headers
|
|
5839
|
+
setOwn(headers, name, applyRedactors(value, spanRedactors, mask));
|
|
5047
5840
|
} else if (Array.isArray(value)) {
|
|
5048
|
-
|
|
5841
|
+
setOwn(
|
|
5842
|
+
headers,
|
|
5843
|
+
name,
|
|
5844
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item)
|
|
5845
|
+
);
|
|
5049
5846
|
}
|
|
5050
5847
|
}
|
|
5051
5848
|
}
|
|
@@ -5095,7 +5892,13 @@ async function createProtection(options = {}) {
|
|
|
5095
5892
|
let result;
|
|
5096
5893
|
let shaped;
|
|
5097
5894
|
try {
|
|
5098
|
-
shaped = await fromFetchRequest(request, {
|
|
5895
|
+
shaped = await fromFetchRequest(request, {
|
|
5896
|
+
trustedProxy: options.trustedProxy,
|
|
5897
|
+
maxBodyBytes: options.maxBodyBytes
|
|
5898
|
+
});
|
|
5899
|
+
if (shaped?._bodyInspectionSkip) {
|
|
5900
|
+
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
5901
|
+
}
|
|
5099
5902
|
result = engine.evaluate(shaped);
|
|
5100
5903
|
} catch (err) {
|
|
5101
5904
|
notify(onError, err, "onError");
|
|
@@ -5185,7 +5988,7 @@ async function createProtection(options = {}) {
|
|
|
5185
5988
|
const out = {};
|
|
5186
5989
|
for (const [name, value] of writeHeadEntries(headers)) {
|
|
5187
5990
|
const key = name.toLowerCase();
|
|
5188
|
-
|
|
5991
|
+
appendOwn(out, key, value);
|
|
5189
5992
|
}
|
|
5190
5993
|
return out;
|
|
5191
5994
|
};
|
|
@@ -5721,7 +6524,6 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5721
6524
|
const reader = body.getReader();
|
|
5722
6525
|
const chunks = [];
|
|
5723
6526
|
let size = 0;
|
|
5724
|
-
let over = false;
|
|
5725
6527
|
let sniffed = !sniff;
|
|
5726
6528
|
try {
|
|
5727
6529
|
for (; ; ) {
|
|
@@ -5730,20 +6532,27 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
5730
6532
|
if (!value) continue;
|
|
5731
6533
|
if (!sniffed) {
|
|
5732
6534
|
sniffed = true;
|
|
5733
|
-
if (looksBinary(value))
|
|
6535
|
+
if (looksBinary(value)) {
|
|
6536
|
+
void reader.cancel().catch(() => {
|
|
6537
|
+
});
|
|
6538
|
+
return { skip: "binary-body" };
|
|
6539
|
+
}
|
|
5734
6540
|
}
|
|
5735
6541
|
size += value.byteLength;
|
|
5736
|
-
if (
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
6542
|
+
if (size >= cap) {
|
|
6543
|
+
void reader.cancel().catch(() => {
|
|
6544
|
+
});
|
|
6545
|
+
return { skip: "body-cap" };
|
|
5740
6546
|
}
|
|
5741
6547
|
chunks.push(value);
|
|
5742
6548
|
}
|
|
5743
6549
|
} catch {
|
|
6550
|
+
void reader.cancel().catch(() => {
|
|
6551
|
+
});
|
|
5744
6552
|
return { skip: "read-failed" };
|
|
6553
|
+
} finally {
|
|
6554
|
+
reader.releaseLock();
|
|
5745
6555
|
}
|
|
5746
|
-
if (over) return { skip: "body-cap" };
|
|
5747
6556
|
try {
|
|
5748
6557
|
return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
|
|
5749
6558
|
} catch {
|
|
@@ -5771,10 +6580,10 @@ function concatBytes(chunks, total) {
|
|
|
5771
6580
|
function headerObject(headers) {
|
|
5772
6581
|
const out = {};
|
|
5773
6582
|
headers?.forEach?.((v, k) => {
|
|
5774
|
-
out
|
|
6583
|
+
setOwn(out, k.toLowerCase(), v);
|
|
5775
6584
|
});
|
|
5776
6585
|
const setCookies = headers?.getSetCookie?.();
|
|
5777
|
-
if (setCookies && setCookies.length) out
|
|
6586
|
+
if (setCookies && setCookies.length) setOwn(out, "set-cookie", setCookies);
|
|
5778
6587
|
return out;
|
|
5779
6588
|
}
|
|
5780
6589
|
function hasSpanMutations(rule) {
|
|
@@ -5943,7 +6752,7 @@ function isHeaderMutation(action) {
|
|
|
5943
6752
|
}
|
|
5944
6753
|
function applyHeaderMutation(headers, rule) {
|
|
5945
6754
|
if (rule.action === "remove-header") {
|
|
5946
|
-
for (const name of rule.remove_headers ?? []) headers
|
|
6755
|
+
for (const name of rule.remove_headers ?? []) setOwn(headers, String(name).toLowerCase(), null);
|
|
5947
6756
|
return;
|
|
5948
6757
|
}
|
|
5949
6758
|
if (rule.action === "set-header") {
|
|
@@ -5952,7 +6761,7 @@ function applyHeaderMutation(headers, rule) {
|
|
|
5952
6761
|
const key = String(name).toLowerCase();
|
|
5953
6762
|
const present = headers[key] != null && headers[key] !== "";
|
|
5954
6763
|
if (ensure && present) continue;
|
|
5955
|
-
headers
|
|
6764
|
+
setOwn(headers, key, String(value));
|
|
5956
6765
|
}
|
|
5957
6766
|
return;
|
|
5958
6767
|
}
|