@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.cjs
CHANGED
|
@@ -35,6 +35,183 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
35
35
|
));
|
|
36
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
37
|
|
|
38
|
+
// src/protect/safe-origin.js
|
|
39
|
+
function isSafeOrigin(value) {
|
|
40
|
+
try {
|
|
41
|
+
const u = new URL(value);
|
|
42
|
+
if (u.protocol === "https:") return true;
|
|
43
|
+
return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]" || u.hostname === "::1");
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function safeBaseUrl(candidate, fallback, label) {
|
|
49
|
+
if (typeof candidate !== "string" || candidate === "") return fallback;
|
|
50
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
51
|
+
warnOnce(label, `[patchstack] ignoring unsafe ${label} override (${candidate}): must be https (or localhost). Using the default.`);
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
function warnOnce(key, message) {
|
|
55
|
+
if (warned.has(key)) return;
|
|
56
|
+
warned.add(key);
|
|
57
|
+
console.warn(message);
|
|
58
|
+
}
|
|
59
|
+
var warned;
|
|
60
|
+
var init_safe_origin = __esm({
|
|
61
|
+
"src/protect/safe-origin.js"() {
|
|
62
|
+
"use strict";
|
|
63
|
+
warned = /* @__PURE__ */ new Set();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// src/types.ts
|
|
68
|
+
var PatchstackError;
|
|
69
|
+
var init_types = __esm({
|
|
70
|
+
"src/types.ts"() {
|
|
71
|
+
"use strict";
|
|
72
|
+
PatchstackError = class extends Error {
|
|
73
|
+
constructor(message, code, cause) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.code = code;
|
|
76
|
+
this.cause = cause;
|
|
77
|
+
this.name = "PatchstackError";
|
|
78
|
+
}
|
|
79
|
+
code;
|
|
80
|
+
cause;
|
|
81
|
+
/**
|
|
82
|
+
* For a `VALIDATION_ERROR`, the request fields the server refused, as it named them. Read from the
|
|
83
|
+
* structured `errors` object a validation response carries, so a caller deciding what to do about a
|
|
84
|
+
* refusal keys on the field rather than on the wording of a sentence.
|
|
85
|
+
*/
|
|
86
|
+
fields = [];
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// src/endpoint-policy.ts
|
|
92
|
+
function assertConnectableEndpoint(config, target = config.endpoint) {
|
|
93
|
+
if (config.endpointTrusted === false) {
|
|
94
|
+
throw new PatchstackError(
|
|
95
|
+
"The custom endpoint in .patchstackrc.json must be confirmed with --endpoint or PATCHSTACK_ENDPOINT before it can be used.",
|
|
96
|
+
"CONFIG_INVALID"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (safeRemoteUrl(config.endpoint) === null) {
|
|
100
|
+
throw new PatchstackError(
|
|
101
|
+
"Patchstack endpoints must use HTTPS, except for localhost development endpoints.",
|
|
102
|
+
"CONFIG_INVALID"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
let endpointOrigin;
|
|
106
|
+
let targetOrigin;
|
|
107
|
+
try {
|
|
108
|
+
endpointOrigin = new URL(config.endpoint).origin;
|
|
109
|
+
targetOrigin = new URL(target).origin;
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
throw new PatchstackError("The configured Patchstack endpoint is not a valid URL.", "CONFIG_INVALID", cause);
|
|
112
|
+
}
|
|
113
|
+
if (safeRemoteUrl(target) === null || targetOrigin !== endpointOrigin) {
|
|
114
|
+
throw new PatchstackError(
|
|
115
|
+
"A Patchstack request resolved outside the configured endpoint origin.",
|
|
116
|
+
"CONFIG_INVALID"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function safeRemoteUrl(value) {
|
|
121
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048) return null;
|
|
122
|
+
try {
|
|
123
|
+
const parsed = new URL(value);
|
|
124
|
+
if (!isSafeOrigin(parsed.toString()) || parsed.username !== "" || parsed.password !== "") return null;
|
|
125
|
+
return parsed.toString();
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function safeDisplayString(value, maxLength = 512) {
|
|
131
|
+
if (typeof value !== "string") return null;
|
|
132
|
+
const clean = value.replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
|
|
133
|
+
if (clean.length === 0) return null;
|
|
134
|
+
return clean.slice(0, maxLength);
|
|
135
|
+
}
|
|
136
|
+
function isCanonicalUuid(value) {
|
|
137
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
138
|
+
}
|
|
139
|
+
function isCredential(value) {
|
|
140
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) return false;
|
|
141
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
142
|
+
const code = value.charCodeAt(i);
|
|
143
|
+
if (code < 33 || code > 126) return false;
|
|
144
|
+
}
|
|
145
|
+
const separator = value.lastIndexOf("-");
|
|
146
|
+
return separator > 0 && separator < value.length - 1 && /^\d+$/.test(value.slice(separator + 1));
|
|
147
|
+
}
|
|
148
|
+
function isHeaderValue(value, maxLength = 8192) {
|
|
149
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) return false;
|
|
150
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
151
|
+
const code = value.charCodeAt(i);
|
|
152
|
+
if (code < 33 || code > 126) return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
var init_endpoint_policy = __esm({
|
|
157
|
+
"src/endpoint-policy.ts"() {
|
|
158
|
+
"use strict";
|
|
159
|
+
init_types();
|
|
160
|
+
init_safe_origin();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// src/bounded-response.ts
|
|
165
|
+
async function readBoundedText(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
166
|
+
const stated2 = Number(response.headers?.get?.("content-length"));
|
|
167
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) {
|
|
168
|
+
void response.body?.cancel().catch(() => {
|
|
169
|
+
});
|
|
170
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
171
|
+
}
|
|
172
|
+
const stream = response.body;
|
|
173
|
+
if (stream === null || typeof stream.getReader !== "function") {
|
|
174
|
+
if (stated2 === 0) return "";
|
|
175
|
+
throw new Error("response body cannot be read with a size bound");
|
|
176
|
+
}
|
|
177
|
+
const reader = stream.getReader();
|
|
178
|
+
const chunks = [];
|
|
179
|
+
let total = 0;
|
|
180
|
+
try {
|
|
181
|
+
for (; ; ) {
|
|
182
|
+
const { done, value } = await reader.read();
|
|
183
|
+
if (done) break;
|
|
184
|
+
total += value.byteLength;
|
|
185
|
+
if (total > maxBytes) {
|
|
186
|
+
void reader.cancel().catch(() => {
|
|
187
|
+
});
|
|
188
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
189
|
+
}
|
|
190
|
+
chunks.push(value);
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
reader.releaseLock();
|
|
194
|
+
}
|
|
195
|
+
const bytes = new Uint8Array(total);
|
|
196
|
+
let offset = 0;
|
|
197
|
+
for (const chunk of chunks) {
|
|
198
|
+
bytes.set(chunk, offset);
|
|
199
|
+
offset += chunk.byteLength;
|
|
200
|
+
}
|
|
201
|
+
return new TextDecoder().decode(bytes);
|
|
202
|
+
}
|
|
203
|
+
async function readBoundedJson2(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
204
|
+
const text = await readBoundedText(response, maxBytes);
|
|
205
|
+
return JSON.parse(text);
|
|
206
|
+
}
|
|
207
|
+
var MAX_CONTROL_RESPONSE_BYTES;
|
|
208
|
+
var init_bounded_response = __esm({
|
|
209
|
+
"src/bounded-response.ts"() {
|
|
210
|
+
"use strict";
|
|
211
|
+
MAX_CONTROL_RESPONSE_BYTES = 256 * 1024;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
38
215
|
// src/pulse-token.ts
|
|
39
216
|
function buildTokenUrl(manifestEndpoint) {
|
|
40
217
|
const url = new URL(manifestEndpoint);
|
|
@@ -51,20 +228,46 @@ function parsePulseAuth(credential) {
|
|
|
51
228
|
if (!/^\d+$/.test(clientId)) return null;
|
|
52
229
|
return { clientId, clientSecret: credential.slice(0, index) };
|
|
53
230
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
231
|
+
function cacheKey(tokenUrl, credential) {
|
|
232
|
+
return `${tokenUrl}
|
|
233
|
+
${credential}`;
|
|
234
|
+
}
|
|
235
|
+
function clearPulseToken(config) {
|
|
236
|
+
if (!config || typeof config.pulseAuth !== "string") {
|
|
237
|
+
cached.clear();
|
|
238
|
+
inflight.clear();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const key = cacheKey(buildTokenUrl(config.endpoint), config.pulseAuth);
|
|
243
|
+
cached.delete(key);
|
|
244
|
+
inflight.delete(key);
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
56
247
|
}
|
|
57
248
|
async function getPulseToken(config, fetchImpl = fetch) {
|
|
58
249
|
if (typeof config.pulseAuth !== "string" || config.pulseAuth.length === 0) return null;
|
|
59
|
-
if (
|
|
60
|
-
|
|
250
|
+
if (config.endpointTrusted === false) return null;
|
|
251
|
+
let tokenUrl;
|
|
252
|
+
try {
|
|
253
|
+
tokenUrl = buildTokenUrl(config.endpoint);
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
if (!isSafeOrigin(tokenUrl)) return null;
|
|
258
|
+
const key = cacheKey(tokenUrl, config.pulseAuth);
|
|
259
|
+
const existing = cached.get(key);
|
|
260
|
+
if (existing !== void 0 && Date.now() < existing.expiresAt - TOKEN_SKEW_MS) {
|
|
261
|
+
return existing.token;
|
|
61
262
|
}
|
|
62
|
-
|
|
263
|
+
const pending = inflight.get(key);
|
|
264
|
+
if (pending !== void 0) return pending;
|
|
63
265
|
const credentials = parsePulseAuth(config.pulseAuth);
|
|
64
266
|
if (credentials === null) return null;
|
|
65
|
-
|
|
267
|
+
let exchange;
|
|
268
|
+
exchange = (async () => {
|
|
66
269
|
try {
|
|
67
|
-
const response = await fetchImpl(
|
|
270
|
+
const response = await fetchImpl(tokenUrl, {
|
|
68
271
|
method: "POST",
|
|
69
272
|
headers: {
|
|
70
273
|
"Content-Type": "application/json",
|
|
@@ -79,25 +282,27 @@ async function getPulseToken(config, fetchImpl = fetch) {
|
|
|
79
282
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
80
283
|
});
|
|
81
284
|
if (!response.ok) return null;
|
|
82
|
-
const body = await response
|
|
83
|
-
if (
|
|
285
|
+
const body = await readBoundedJson2(response);
|
|
286
|
+
if (!isHeaderValue(body.access_token)) return null;
|
|
84
287
|
const expiresIn = Number(body.expires_in);
|
|
85
288
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
86
|
-
cached
|
|
289
|
+
cached.set(key, { token: body.access_token, expiresAt: Date.now() + ttlMs });
|
|
87
290
|
return body.access_token;
|
|
88
291
|
} catch {
|
|
89
292
|
return null;
|
|
90
293
|
} finally {
|
|
91
|
-
inflight
|
|
294
|
+
if (inflight.get(key) === exchange) inflight.delete(key);
|
|
92
295
|
}
|
|
93
296
|
})();
|
|
94
|
-
|
|
297
|
+
inflight.set(key, exchange);
|
|
298
|
+
return exchange;
|
|
95
299
|
}
|
|
96
300
|
async function pulseAuthHeader(config, fetchImpl = fetch) {
|
|
97
301
|
const token = await getPulseToken(config, fetchImpl);
|
|
98
302
|
return token === null ? {} : { Authorization: `Bearer ${token}` };
|
|
99
303
|
}
|
|
100
304
|
async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
305
|
+
assertConnectableEndpoint(config, url);
|
|
101
306
|
const send = async () => {
|
|
102
307
|
const auth = await pulseAuthHeader(config, fetchImpl);
|
|
103
308
|
const response = await fetchImpl(url, {
|
|
@@ -108,7 +313,7 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
108
313
|
};
|
|
109
314
|
const first = await send();
|
|
110
315
|
if (first.response.status === 401 && first.authenticated) {
|
|
111
|
-
clearPulseToken();
|
|
316
|
+
clearPulseToken(config);
|
|
112
317
|
return (await send()).response;
|
|
113
318
|
}
|
|
114
319
|
return first.response;
|
|
@@ -117,9 +322,12 @@ var TOKEN_SKEW_MS, cached, inflight;
|
|
|
117
322
|
var init_pulse_token = __esm({
|
|
118
323
|
"src/pulse-token.ts"() {
|
|
119
324
|
"use strict";
|
|
325
|
+
init_safe_origin();
|
|
326
|
+
init_endpoint_policy();
|
|
327
|
+
init_bounded_response();
|
|
120
328
|
TOKEN_SKEW_MS = 6e4;
|
|
121
|
-
cached =
|
|
122
|
-
inflight =
|
|
329
|
+
cached = /* @__PURE__ */ new Map();
|
|
330
|
+
inflight = /* @__PURE__ */ new Map();
|
|
123
331
|
}
|
|
124
332
|
});
|
|
125
333
|
|
|
@@ -144,30 +352,6 @@ var init_build_id = __esm({
|
|
|
144
352
|
}
|
|
145
353
|
});
|
|
146
354
|
|
|
147
|
-
// src/types.ts
|
|
148
|
-
var PatchstackError;
|
|
149
|
-
var init_types = __esm({
|
|
150
|
-
"src/types.ts"() {
|
|
151
|
-
"use strict";
|
|
152
|
-
PatchstackError = class extends Error {
|
|
153
|
-
constructor(message, code, cause) {
|
|
154
|
-
super(message);
|
|
155
|
-
this.code = code;
|
|
156
|
-
this.cause = cause;
|
|
157
|
-
this.name = "PatchstackError";
|
|
158
|
-
}
|
|
159
|
-
code;
|
|
160
|
-
cause;
|
|
161
|
-
/**
|
|
162
|
-
* For a `VALIDATION_ERROR`, the request fields the server refused, as it named them. Read from the
|
|
163
|
-
* structured `errors` object a validation response carries, so a caller deciding what to do about a
|
|
164
|
-
* refusal keys on the field rather than on the wording of a sentence.
|
|
165
|
-
*/
|
|
166
|
-
fields = [];
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
355
|
// src/hosting.ts
|
|
172
356
|
function detectHostingPlatform(env = process.env) {
|
|
173
357
|
for (const rule of RULES) {
|
|
@@ -204,7 +388,7 @@ var init_hosting = __esm({
|
|
|
204
388
|
|
|
205
389
|
// src/client.ts
|
|
206
390
|
function claimTokenHeader(config) {
|
|
207
|
-
return
|
|
391
|
+
return config.endpointTrusted !== false && isHeaderValue(config.claimToken) ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
208
392
|
}
|
|
209
393
|
function buildEndpointUrl(base, siteUuid) {
|
|
210
394
|
const trimmed = base.replace(/\/$/, "");
|
|
@@ -239,9 +423,46 @@ function validationFields(body) {
|
|
|
239
423
|
if (typeof errors !== "object" || errors === null || Array.isArray(errors)) return [];
|
|
240
424
|
return Object.keys(errors);
|
|
241
425
|
}
|
|
426
|
+
function manifestResponse(body, provisioning) {
|
|
427
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return null;
|
|
428
|
+
const value = body;
|
|
429
|
+
if (typeof value.stored !== "boolean") return null;
|
|
430
|
+
const uuid = value.uuid === void 0 ? void 0 : isCanonicalUuid(value.uuid) ? value.uuid : null;
|
|
431
|
+
if (uuid === null || provisioning && uuid === void 0) return null;
|
|
432
|
+
if (value.api_key !== void 0 && !isCredential(value.api_key)) return null;
|
|
433
|
+
if (value.manifest_id !== void 0 && (!Number.isInteger(value.manifest_id) || Number(value.manifest_id) <= 0)) return null;
|
|
434
|
+
const claimValue = value.claim;
|
|
435
|
+
let claim2;
|
|
436
|
+
if (claimValue !== void 0) {
|
|
437
|
+
if (typeof claimValue !== "object" || claimValue === null || Array.isArray(claimValue)) return null;
|
|
438
|
+
const raw = claimValue;
|
|
439
|
+
if (!["claimed", "owned-by-you", "owned-by-other", "rejected"].includes(String(raw.state))) return null;
|
|
440
|
+
const dashboardUrl = raw.dashboard_url === void 0 || raw.dashboard_url === null ? null : safeRemoteUrl(raw.dashboard_url);
|
|
441
|
+
if (raw.dashboard_url !== void 0 && raw.dashboard_url !== null && dashboardUrl === null) return null;
|
|
442
|
+
claim2 = {
|
|
443
|
+
state: raw.state,
|
|
444
|
+
...Number.isInteger(raw.site_id) ? { site_id: Number(raw.site_id) } : {},
|
|
445
|
+
...dashboardUrl !== null ? { dashboard_url: dashboardUrl } : {},
|
|
446
|
+
...raw.reason === "expired" || raw.reason === "invalid" ? { reason: raw.reason } : {}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const result = {
|
|
450
|
+
stored: value.stored,
|
|
451
|
+
...uuid !== void 0 ? { uuid } : {},
|
|
452
|
+
...typeof value.api_key === "string" ? { api_key: value.api_key } : {},
|
|
453
|
+
...Number.isInteger(value.manifest_id) ? { manifest_id: Number(value.manifest_id) } : {},
|
|
454
|
+
...safeDisplayString(value.checksum, 256) !== null ? { checksum: safeDisplayString(value.checksum, 256) } : {},
|
|
455
|
+
...safeDisplayString(value.reason) !== null ? { reason: safeDisplayString(value.reason) } : {},
|
|
456
|
+
...safeDisplayString(value.message) !== null ? { message: safeDisplayString(value.message) } : {},
|
|
457
|
+
...safeDisplayString(value.error) !== null ? { error: safeDisplayString(value.error) } : {},
|
|
458
|
+
...claim2 !== void 0 ? { claim: claim2 } : {}
|
|
459
|
+
};
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
242
462
|
async function postManifest(config, payload) {
|
|
243
463
|
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
244
464
|
const timeoutMs = config.timeoutMs;
|
|
465
|
+
assertConnectableEndpoint(config, url);
|
|
245
466
|
let response;
|
|
246
467
|
try {
|
|
247
468
|
response = await pulseFetch(config, url, {
|
|
@@ -269,25 +490,27 @@ async function postManifest(config, payload) {
|
|
|
269
490
|
cause
|
|
270
491
|
);
|
|
271
492
|
}
|
|
272
|
-
|
|
273
|
-
let
|
|
493
|
+
let text = "";
|
|
494
|
+
let parsed = null;
|
|
274
495
|
try {
|
|
275
|
-
|
|
496
|
+
text = await readBoundedText(response);
|
|
497
|
+
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
276
498
|
} catch {
|
|
277
|
-
|
|
499
|
+
parsed = null;
|
|
278
500
|
}
|
|
501
|
+
const body = manifestResponse(parsed, config.siteUuid === null);
|
|
279
502
|
if (response.status === 404) {
|
|
280
503
|
throw new PatchstackError(
|
|
281
|
-
|
|
504
|
+
safeDisplayString(parsed?.error) ?? "Site not found. Check that your site UUID is correct and that the app is registered as a Pulse app in your Patchstack dashboard.",
|
|
282
505
|
"SITE_NOT_FOUND"
|
|
283
506
|
);
|
|
284
507
|
}
|
|
285
508
|
if (response.status === 422) {
|
|
286
509
|
const refused2 = new PatchstackError(
|
|
287
|
-
|
|
510
|
+
safeDisplayString(parsed?.message) ?? "Patchstack rejected the manifest payload (validation failed).",
|
|
288
511
|
"VALIDATION_ERROR"
|
|
289
512
|
);
|
|
290
|
-
refused2.fields = validationFields(
|
|
513
|
+
refused2.fields = validationFields(parsed);
|
|
291
514
|
throw refused2;
|
|
292
515
|
}
|
|
293
516
|
const refused = authFailureMessage(response.status, config);
|
|
@@ -296,12 +519,12 @@ async function postManifest(config, payload) {
|
|
|
296
519
|
}
|
|
297
520
|
if (response.status < 200 || response.status >= 300) {
|
|
298
521
|
throw new PatchstackError(
|
|
299
|
-
`Patchstack returned ${response.status}: ${text
|
|
522
|
+
`Patchstack returned ${response.status}: ${safeDisplayString(text, 200) ?? "no response details"}`,
|
|
300
523
|
"SERVER_ERROR"
|
|
301
524
|
);
|
|
302
525
|
}
|
|
303
526
|
if (body === null) {
|
|
304
|
-
throw new PatchstackError("Patchstack returned an
|
|
527
|
+
throw new PatchstackError("Patchstack returned an unexpected response.", "SERVER_ERROR");
|
|
305
528
|
}
|
|
306
529
|
return body;
|
|
307
530
|
}
|
|
@@ -311,7 +534,7 @@ function isTimeoutError(cause) {
|
|
|
311
534
|
}
|
|
312
535
|
return false;
|
|
313
536
|
}
|
|
314
|
-
var DEFAULT_ENDPOINT,
|
|
537
|
+
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS2, CLAIM_TOKEN_HEADER;
|
|
315
538
|
var init_client = __esm({
|
|
316
539
|
"src/client.ts"() {
|
|
317
540
|
"use strict";
|
|
@@ -319,8 +542,10 @@ var init_client = __esm({
|
|
|
319
542
|
init_hosting();
|
|
320
543
|
init_pulse_token();
|
|
321
544
|
init_build_id();
|
|
545
|
+
init_bounded_response();
|
|
546
|
+
init_endpoint_policy();
|
|
322
547
|
DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
323
|
-
|
|
548
|
+
DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
324
549
|
CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
325
550
|
}
|
|
326
551
|
});
|
|
@@ -610,8 +835,16 @@ async function resolveConfig(options) {
|
|
|
610
835
|
const fromSecretFile = await readSecretFile(options.cwd);
|
|
611
836
|
const fromEnv = readEnv();
|
|
612
837
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
613
|
-
const
|
|
614
|
-
const
|
|
838
|
+
const endpointRaw = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
839
|
+
const endpointTrusted = endpointRaw === DEFAULT_ENDPOINT || options.cliEndpoint !== void 0 || fromEnv.endpoint !== void 0 || fromFile.endpoint === void 0;
|
|
840
|
+
const endpoint = safeRemoteUrl(endpointRaw);
|
|
841
|
+
if (endpoint === null) {
|
|
842
|
+
throw new PatchstackError(
|
|
843
|
+
"Patchstack endpoints must be valid HTTPS URLs, except for localhost development endpoints.",
|
|
844
|
+
"CONFIG_INVALID"
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
615
848
|
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
616
849
|
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
617
850
|
throw new PatchstackError(
|
|
@@ -622,7 +855,7 @@ async function resolveConfig(options) {
|
|
|
622
855
|
const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
|
|
623
856
|
const environment = environmentRaw ?? inferred.environment;
|
|
624
857
|
const environmentEvidence = inferred?.evidence ?? [];
|
|
625
|
-
if (siteUuid !== null && siteUuid.length > 0 && !
|
|
858
|
+
if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
|
|
626
859
|
throw new PatchstackError(
|
|
627
860
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
628
861
|
"CONFIG_INVALID"
|
|
@@ -645,6 +878,7 @@ async function resolveConfig(options) {
|
|
|
645
878
|
siteUrl: identity.url,
|
|
646
879
|
siteName: identity.name,
|
|
647
880
|
endpoint,
|
|
881
|
+
endpointTrusted,
|
|
648
882
|
timeoutMs,
|
|
649
883
|
environment,
|
|
650
884
|
environmentEvidence,
|
|
@@ -663,8 +897,15 @@ async function readJsonFile(cwd, filename) {
|
|
|
663
897
|
const target = import_node_path3.default.join(cwd, filename);
|
|
664
898
|
let raw;
|
|
665
899
|
try {
|
|
900
|
+
if ((await (0, import_promises2.lstat)(target)).isSymbolicLink()) {
|
|
901
|
+
throw new PatchstackError(
|
|
902
|
+
`Could not read ${target}: configuration files must not be symbolic links.`,
|
|
903
|
+
"CONFIG_INVALID"
|
|
904
|
+
);
|
|
905
|
+
}
|
|
666
906
|
raw = await (0, import_promises2.readFile)(target, "utf8");
|
|
667
907
|
} catch (err) {
|
|
908
|
+
if (err instanceof PatchstackError) throw err;
|
|
668
909
|
if (err.code === "ENOENT") {
|
|
669
910
|
return {};
|
|
670
911
|
}
|
|
@@ -709,23 +950,22 @@ function readEnv() {
|
|
|
709
950
|
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
710
951
|
};
|
|
711
952
|
}
|
|
712
|
-
function isUuid(value) {
|
|
713
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
714
|
-
}
|
|
715
953
|
function isEnvironment(value) {
|
|
716
954
|
return value === "production" || value === "sandbox" || value === "local";
|
|
717
955
|
}
|
|
718
|
-
var import_promises2, import_node_path3, CONFIG_FILENAME, SECRET_FILENAME;
|
|
956
|
+
var import_promises2, import_node_path3, import_node_crypto, CONFIG_FILENAME, SECRET_FILENAME;
|
|
719
957
|
var init_config = __esm({
|
|
720
958
|
"src/config.ts"() {
|
|
721
959
|
"use strict";
|
|
722
960
|
import_promises2 = require("fs/promises");
|
|
723
961
|
import_node_path3 = __toESM(require("path"), 1);
|
|
962
|
+
import_node_crypto = require("crypto");
|
|
724
963
|
init_types();
|
|
725
964
|
init_client();
|
|
726
965
|
init_environment();
|
|
727
966
|
init_site_url();
|
|
728
967
|
init_site_name();
|
|
968
|
+
init_endpoint_policy();
|
|
729
969
|
CONFIG_FILENAME = ".patchstackrc.json";
|
|
730
970
|
SECRET_FILENAME = ".patchstackrc.local.json";
|
|
731
971
|
}
|
|
@@ -2222,27 +2462,52 @@ function clientIpFields(resolved) {
|
|
|
2222
2462
|
return established && address !== null ? { client_ip: address, client_ip_source: resolved.source } : { client_ip_source: "unavailable" };
|
|
2223
2463
|
}
|
|
2224
2464
|
|
|
2225
|
-
// src/protect/
|
|
2226
|
-
|
|
2465
|
+
// src/protect/engine/client.js
|
|
2466
|
+
init_safe_origin();
|
|
2467
|
+
|
|
2468
|
+
// src/protect/engine/response-json.js
|
|
2469
|
+
var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
2470
|
+
async function readBoundedJson(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
|
|
2471
|
+
const stated2 = Number(response.headers?.get?.("content-length"));
|
|
2472
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) {
|
|
2473
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
2474
|
+
}
|
|
2475
|
+
const stream = response.body;
|
|
2476
|
+
if (!stream || typeof stream.getReader !== "function") {
|
|
2477
|
+
if (typeof response.text === "function") {
|
|
2478
|
+
const text = await response.text();
|
|
2479
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
2480
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
2481
|
+
}
|
|
2482
|
+
return JSON.parse(text);
|
|
2483
|
+
}
|
|
2484
|
+
throw new Error("rule response body cannot be read with a size bound");
|
|
2485
|
+
}
|
|
2486
|
+
const reader = stream.getReader();
|
|
2487
|
+
const chunks = [];
|
|
2488
|
+
let total = 0;
|
|
2227
2489
|
try {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2490
|
+
for (; ; ) {
|
|
2491
|
+
const { done, value } = await reader.read();
|
|
2492
|
+
if (done) break;
|
|
2493
|
+
total += value.byteLength;
|
|
2494
|
+
if (total > maxBytes) {
|
|
2495
|
+
void reader.cancel().catch(() => {
|
|
2496
|
+
});
|
|
2497
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
2498
|
+
}
|
|
2499
|
+
chunks.push(value);
|
|
2500
|
+
}
|
|
2501
|
+
} finally {
|
|
2502
|
+
reader.releaseLock();
|
|
2233
2503
|
}
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
var warned = /* @__PURE__ */ new Set();
|
|
2242
|
-
function warnOnce(key, message) {
|
|
2243
|
-
if (warned.has(key)) return;
|
|
2244
|
-
warned.add(key);
|
|
2245
|
-
console.warn(message);
|
|
2504
|
+
const bytes = new Uint8Array(total);
|
|
2505
|
+
let offset = 0;
|
|
2506
|
+
for (const chunk of chunks) {
|
|
2507
|
+
bytes.set(chunk, offset);
|
|
2508
|
+
offset += chunk.byteLength;
|
|
2509
|
+
}
|
|
2510
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
2246
2511
|
}
|
|
2247
2512
|
|
|
2248
2513
|
// src/protect/engine/client.js
|
|
@@ -2301,7 +2566,7 @@ var PatchstackRuleClient = class {
|
|
|
2301
2566
|
whitelist_keys: {}
|
|
2302
2567
|
};
|
|
2303
2568
|
}
|
|
2304
|
-
const data = await response
|
|
2569
|
+
const data = await readBoundedJson(response);
|
|
2305
2570
|
if (!data || !Array.isArray(data.firewall)) {
|
|
2306
2571
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2307
2572
|
}
|
|
@@ -2338,6 +2603,22 @@ var PatchstackRuleClient = class {
|
|
|
2338
2603
|
}
|
|
2339
2604
|
};
|
|
2340
2605
|
|
|
2606
|
+
// src/protect/engine/own.js
|
|
2607
|
+
function setOwn(target, key, value) {
|
|
2608
|
+
Object.defineProperty(target, key, {
|
|
2609
|
+
configurable: true,
|
|
2610
|
+
enumerable: true,
|
|
2611
|
+
writable: true,
|
|
2612
|
+
value
|
|
2613
|
+
});
|
|
2614
|
+
return value;
|
|
2615
|
+
}
|
|
2616
|
+
function appendOwn(target, key, value) {
|
|
2617
|
+
if (!Object.hasOwn(target, key)) return setOwn(target, key, value);
|
|
2618
|
+
const previous = target[key];
|
|
2619
|
+
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2341
2622
|
// src/protect/engine/request.js
|
|
2342
2623
|
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
2343
2624
|
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
@@ -2456,8 +2737,8 @@ var RequestResolver = class {
|
|
|
2456
2737
|
}
|
|
2457
2738
|
if (key.startsWith("header.")) {
|
|
2458
2739
|
const name = key.slice("header.".length).toLowerCase();
|
|
2459
|
-
const
|
|
2460
|
-
return
|
|
2740
|
+
const headers = resp.headers ?? {};
|
|
2741
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
2461
2742
|
}
|
|
2462
2743
|
return [];
|
|
2463
2744
|
}
|
|
@@ -2552,7 +2833,7 @@ var RequestResolver = class {
|
|
|
2552
2833
|
...this.#resolveWildcard(cookies, key)
|
|
2553
2834
|
];
|
|
2554
2835
|
}
|
|
2555
|
-
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? cookies[key];
|
|
2836
|
+
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? (Object.hasOwn(cookies, key) ? cookies[key] : void 0);
|
|
2556
2837
|
return value !== void 0 ? [value] : [];
|
|
2557
2838
|
}
|
|
2558
2839
|
#resolveCookie(key) {
|
|
@@ -2560,8 +2841,7 @@ var RequestResolver = class {
|
|
|
2560
2841
|
if (key.endsWith("*")) {
|
|
2561
2842
|
return this.#resolveWildcard(cookies, key);
|
|
2562
2843
|
}
|
|
2563
|
-
|
|
2564
|
-
return value !== void 0 ? [value] : [];
|
|
2844
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
2565
2845
|
}
|
|
2566
2846
|
#resolveServer(key) {
|
|
2567
2847
|
const req = this.#req;
|
|
@@ -2737,7 +3017,7 @@ var RequestResolver = class {
|
|
|
2737
3017
|
}
|
|
2738
3018
|
const name = pair.substring(0, eqIndex).trim();
|
|
2739
3019
|
const value = pair.substring(eqIndex + 1).trim();
|
|
2740
|
-
cookies
|
|
3020
|
+
setOwn(cookies, name, value);
|
|
2741
3021
|
}
|
|
2742
3022
|
this.#cookies = cookies;
|
|
2743
3023
|
return this.#cookies;
|
|
@@ -2894,30 +3174,113 @@ function normalizeWhitespace(value) {
|
|
|
2894
3174
|
result = result.replace(/ {2,}/g, " ");
|
|
2895
3175
|
return result;
|
|
2896
3176
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
|
|
3177
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
3178
|
+
var MAX_RAW_NODES = 25e3;
|
|
3179
|
+
var MAX_RAW_DEPTH = 200;
|
|
3180
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
3181
|
+
function serializeForRawDetection(body) {
|
|
3182
|
+
if (body === null || body === void 0) return "";
|
|
3183
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
3184
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3185
|
+
const output = [];
|
|
3186
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
3187
|
+
let chars = 0;
|
|
3188
|
+
let nodes = 0;
|
|
3189
|
+
const append = (text) => {
|
|
3190
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
3191
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
3192
|
+
output.push(piece);
|
|
3193
|
+
chars += piece.length;
|
|
3194
|
+
};
|
|
3195
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
3196
|
+
const task = stack.pop();
|
|
3197
|
+
if (task.kind === "text") {
|
|
3198
|
+
append(task.text);
|
|
3199
|
+
continue;
|
|
3200
|
+
}
|
|
3201
|
+
if (task.kind === "key") {
|
|
3202
|
+
append(boundedJsonString(task.value));
|
|
3203
|
+
append(":");
|
|
3204
|
+
continue;
|
|
3205
|
+
}
|
|
3206
|
+
nodes++;
|
|
3207
|
+
if (nodes > MAX_RAW_NODES) {
|
|
3208
|
+
append('"[NodeLimit]"');
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3211
|
+
const value = task.value;
|
|
3212
|
+
if (value === null || value === void 0) {
|
|
3213
|
+
append(task.root ? "" : "null");
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
if (typeof value === "string") {
|
|
3217
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
if (typeof value !== "object") {
|
|
3221
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
if (visited.has(value)) {
|
|
3225
|
+
append("[Circular]");
|
|
3226
|
+
continue;
|
|
3227
|
+
}
|
|
3228
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
3229
|
+
append('"[DepthLimit]"');
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
visited.add(value);
|
|
3233
|
+
if (Array.isArray(value)) {
|
|
3234
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
3235
|
+
const items = [];
|
|
3236
|
+
for (let i = 0; i < length2; i++) {
|
|
3237
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
3238
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
3239
|
+
}
|
|
3240
|
+
if (value.length > length2) {
|
|
3241
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
3242
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
3243
|
+
}
|
|
3244
|
+
stack.push({ kind: "text", text: "]" });
|
|
3245
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
3246
|
+
stack.push({ kind: "text", text: "[" });
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
let keys;
|
|
3250
|
+
try {
|
|
3251
|
+
keys = Object.getOwnPropertyNames(value);
|
|
3252
|
+
} catch {
|
|
3253
|
+
append('"[Unreadable]"');
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
3257
|
+
const entries = [];
|
|
3258
|
+
for (let i = 0; i < length; i++) {
|
|
3259
|
+
const key = keys[i];
|
|
3260
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
3261
|
+
entries.push({ kind: "key", value: key });
|
|
3262
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
3263
|
+
}
|
|
3264
|
+
if (keys.length > length) {
|
|
3265
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
3266
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
3267
|
+
}
|
|
3268
|
+
stack.push({ kind: "text", text: "}" });
|
|
3269
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
3270
|
+
stack.push({ kind: "text", text: "{" });
|
|
2909
3271
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3272
|
+
return output.join("");
|
|
3273
|
+
}
|
|
3274
|
+
function boundedJsonString(value) {
|
|
3275
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
3276
|
+
}
|
|
3277
|
+
function readOwnValue(value, key) {
|
|
3278
|
+
try {
|
|
3279
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3280
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
3281
|
+
} catch {
|
|
3282
|
+
return "[Unreadable]";
|
|
2914
3283
|
}
|
|
2915
|
-
const keys = Object.getOwnPropertyNames(body);
|
|
2916
|
-
const parts = keys.map((key) => {
|
|
2917
|
-
const val = serializeForRawDetection(body[key], visited, false);
|
|
2918
|
-
return JSON.stringify(key) + ":" + val;
|
|
2919
|
-
});
|
|
2920
|
-
return "{" + parts.join(",") + "}";
|
|
2921
3284
|
}
|
|
2922
3285
|
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
2923
3286
|
function requestField(req, key) {
|
|
@@ -2959,7 +3322,7 @@ function normalizeObject(value, options = {}, depth = 0) {
|
|
|
2959
3322
|
if (typeof value === "object" && value !== null) {
|
|
2960
3323
|
const result = {};
|
|
2961
3324
|
for (const [key, val] of Object.entries(value)) {
|
|
2962
|
-
result
|
|
3325
|
+
setOwn(result, key, normalizeObject(val, options, depth + 1));
|
|
2963
3326
|
}
|
|
2964
3327
|
return result;
|
|
2965
3328
|
}
|
|
@@ -2976,6 +3339,111 @@ var REDOS_PATTERNS = [
|
|
|
2976
3339
|
new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
|
|
2977
3340
|
// nested alternation under a quantifier
|
|
2978
3341
|
];
|
|
3342
|
+
function hasAdjacentUnbounded(body, flags) {
|
|
3343
|
+
let previousUnboundedAtom = null;
|
|
3344
|
+
for (let i = 0; i < body.length; ) {
|
|
3345
|
+
const start = i;
|
|
3346
|
+
const ch = body[i];
|
|
3347
|
+
let assertion = false;
|
|
3348
|
+
let literal = null;
|
|
3349
|
+
let atom = ch;
|
|
3350
|
+
if (ch === "\\") {
|
|
3351
|
+
const escaped = body[i + 1] ?? "";
|
|
3352
|
+
assertion = /[bBAZz]/.test(escaped);
|
|
3353
|
+
if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
|
|
3354
|
+
i += Math.min(2, body.length - i);
|
|
3355
|
+
atom = body.slice(start, i);
|
|
3356
|
+
} else if (ch === "[") {
|
|
3357
|
+
i++;
|
|
3358
|
+
while (i < body.length) {
|
|
3359
|
+
if (body[i] === "\\") i += 2;
|
|
3360
|
+
else if (body[i++] === "]") break;
|
|
3361
|
+
}
|
|
3362
|
+
atom = body.slice(start, i);
|
|
3363
|
+
} else if (ch === ")") {
|
|
3364
|
+
i++;
|
|
3365
|
+
atom = ".";
|
|
3366
|
+
} else if (ch === "(") {
|
|
3367
|
+
const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
|
|
3368
|
+
if (lookaround) {
|
|
3369
|
+
i = groupEnd(body, i);
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
i++;
|
|
3373
|
+
if (body.startsWith("?:", i)) i += 2;
|
|
3374
|
+
else if (body.startsWith("?<", i)) {
|
|
3375
|
+
const nameEnd = body.indexOf(">", i + 2);
|
|
3376
|
+
if (nameEnd !== -1) i = nameEnd + 1;
|
|
3377
|
+
}
|
|
3378
|
+
continue;
|
|
3379
|
+
} else if (ch === "|") {
|
|
3380
|
+
previousUnboundedAtom = null;
|
|
3381
|
+
i++;
|
|
3382
|
+
continue;
|
|
3383
|
+
} else if ("^$".includes(ch)) {
|
|
3384
|
+
i++;
|
|
3385
|
+
continue;
|
|
3386
|
+
} else if ("?*+{}".includes(ch)) {
|
|
3387
|
+
previousUnboundedAtom = null;
|
|
3388
|
+
i++;
|
|
3389
|
+
continue;
|
|
3390
|
+
} else {
|
|
3391
|
+
if (ch !== ".") literal = ch;
|
|
3392
|
+
i++;
|
|
3393
|
+
}
|
|
3394
|
+
if (assertion) continue;
|
|
3395
|
+
let unbounded = false;
|
|
3396
|
+
let mayBeEmpty = false;
|
|
3397
|
+
if (body[i] === "*" || body[i] === "+") {
|
|
3398
|
+
unbounded = true;
|
|
3399
|
+
mayBeEmpty = body[i] === "*";
|
|
3400
|
+
i++;
|
|
3401
|
+
} else if (body[i] === "{") {
|
|
3402
|
+
const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
|
|
3403
|
+
if (quantifier) {
|
|
3404
|
+
unbounded = quantifier[2] === "";
|
|
3405
|
+
mayBeEmpty = Number(quantifier[1]) === 0;
|
|
3406
|
+
i += quantifier[0].length;
|
|
3407
|
+
}
|
|
3408
|
+
} else if (body[i] === "?") {
|
|
3409
|
+
mayBeEmpty = true;
|
|
3410
|
+
i++;
|
|
3411
|
+
}
|
|
3412
|
+
if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
|
|
3413
|
+
if (unbounded && previousUnboundedAtom !== null) return true;
|
|
3414
|
+
if (unbounded) {
|
|
3415
|
+
previousUnboundedAtom = atom;
|
|
3416
|
+
} else if (previousUnboundedAtom !== null && !mayBeEmpty) {
|
|
3417
|
+
try {
|
|
3418
|
+
const atomFlags = flags.replace(/[^iu]/g, "");
|
|
3419
|
+
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";
|
|
3420
|
+
const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
|
|
3421
|
+
if (complementaryClass || excludedLiteral) {
|
|
3422
|
+
previousUnboundedAtom = null;
|
|
3423
|
+
}
|
|
3424
|
+
} catch {
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
if (i === start) i++;
|
|
3428
|
+
}
|
|
3429
|
+
return false;
|
|
3430
|
+
}
|
|
3431
|
+
function groupEnd(body, start) {
|
|
3432
|
+
let depth = 0;
|
|
3433
|
+
let inClass = false;
|
|
3434
|
+
for (let i = start; i < body.length; i++) {
|
|
3435
|
+
const ch = body[i];
|
|
3436
|
+
if (ch === "\\") {
|
|
3437
|
+
i++;
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
if (ch === "[") inClass = true;
|
|
3441
|
+
else if (ch === "]") inClass = false;
|
|
3442
|
+
else if (!inClass && ch === "(") depth++;
|
|
3443
|
+
else if (!inClass && ch === ")" && --depth === 0) return i + 1;
|
|
3444
|
+
}
|
|
3445
|
+
return body.length;
|
|
3446
|
+
}
|
|
2979
3447
|
var warnedRejectedPatterns = /* @__PURE__ */ new Set();
|
|
2980
3448
|
function warnRejectedPatternOnce(pattern) {
|
|
2981
3449
|
const key = String(pattern);
|
|
@@ -2993,15 +3461,14 @@ function safeRegExp(pattern) {
|
|
|
2993
3461
|
if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
|
|
2994
3462
|
return null;
|
|
2995
3463
|
}
|
|
2996
|
-
for (const dangerous of REDOS_PATTERNS) {
|
|
2997
|
-
if (dangerous.test(pattern)) {
|
|
2998
|
-
return null;
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
3464
|
const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
|
|
3002
3465
|
if (!match) {
|
|
3003
3466
|
return null;
|
|
3004
3467
|
}
|
|
3468
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
3469
|
+
if (dangerous.test(match[1])) return null;
|
|
3470
|
+
}
|
|
3471
|
+
if (hasAdjacentUnbounded(match[1], match[2])) return null;
|
|
3005
3472
|
try {
|
|
3006
3473
|
return new RegExp(match[1], match[2]);
|
|
3007
3474
|
} catch {
|
|
@@ -3745,6 +4212,16 @@ function operandShapeProblem(shape, value) {
|
|
|
3745
4212
|
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
3746
4213
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
3747
4214
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
4215
|
+
const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
|
|
4216
|
+
const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
|
|
4217
|
+
const tooLong = stringLimit(value);
|
|
4218
|
+
if (tooLong) return tooLong;
|
|
4219
|
+
const tooMany = listLimit(value);
|
|
4220
|
+
if (tooMany) return tooMany;
|
|
4221
|
+
if (Array.isArray(value)) {
|
|
4222
|
+
const longMember = value.find((member) => stringLimit(member) !== null);
|
|
4223
|
+
if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
4224
|
+
}
|
|
3748
4225
|
switch (shape) {
|
|
3749
4226
|
case "positive-number":
|
|
3750
4227
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
@@ -3775,8 +4252,11 @@ function operandShapeProblem(shape, value) {
|
|
|
3775
4252
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
3776
4253
|
const names = Object.keys(value);
|
|
3777
4254
|
if (names.length === 0) return "must be an object with at least one entry";
|
|
4255
|
+
if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
|
|
3778
4256
|
const bad = names.find((name) => !isHeaderName(name));
|
|
3779
|
-
|
|
4257
|
+
if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
4258
|
+
const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
|
|
4259
|
+
return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
|
|
3780
4260
|
}
|
|
3781
4261
|
case "header-name-list": {
|
|
3782
4262
|
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
@@ -3922,9 +4402,16 @@ var LIMITS = Object.freeze({
|
|
|
3922
4402
|
maxRules: 5e3,
|
|
3923
4403
|
maxWhitelists: 2e3,
|
|
3924
4404
|
maxConditionsPerRule: 250,
|
|
4405
|
+
maxConditionNodesPerRule: 1e3,
|
|
4406
|
+
maxConditionNodesPerBundle: 25e3,
|
|
3925
4407
|
maxNestingDepth: 12,
|
|
3926
4408
|
maxRegexLength: 1e3,
|
|
3927
|
-
maxValueLength: 8192
|
|
4409
|
+
maxValueLength: 8192,
|
|
4410
|
+
maxParameterLength: 512,
|
|
4411
|
+
maxParameterItems: 64,
|
|
4412
|
+
maxOperandItems: 256,
|
|
4413
|
+
maxMapEntries: 128,
|
|
4414
|
+
maxPathSegments: 64
|
|
3928
4415
|
});
|
|
3929
4416
|
var GROUP_PARAMETER = "rules";
|
|
3930
4417
|
function isGroup(condition) {
|
|
@@ -3951,6 +4438,9 @@ function parameterProblem(parameter) {
|
|
|
3951
4438
|
if (parameter === void 0 || parameter === null) return null;
|
|
3952
4439
|
if (Array.isArray(parameter)) {
|
|
3953
4440
|
if (parameter.length === 0) return "parameter list is empty";
|
|
4441
|
+
if (parameter.length > LIMITS.maxParameterItems) {
|
|
4442
|
+
return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
|
|
4443
|
+
}
|
|
3954
4444
|
for (const member of parameter) {
|
|
3955
4445
|
if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
|
|
3956
4446
|
const problem = parameterProblem(member);
|
|
@@ -3959,6 +4449,9 @@ function parameterProblem(parameter) {
|
|
|
3959
4449
|
return null;
|
|
3960
4450
|
}
|
|
3961
4451
|
if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
|
|
4452
|
+
if (parameter.length > LIMITS.maxParameterLength) {
|
|
4453
|
+
return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
|
|
4454
|
+
}
|
|
3962
4455
|
if (parameter === GROUP_PARAMETER) {
|
|
3963
4456
|
return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
|
|
3964
4457
|
}
|
|
@@ -3994,6 +4487,20 @@ function matchProblem(match, parameter) {
|
|
|
3994
4487
|
if (!match || typeof match !== "object") return "condition has no match object";
|
|
3995
4488
|
if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
|
|
3996
4489
|
if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
|
|
4490
|
+
for (const operand of ["value", "key", "claim"]) {
|
|
4491
|
+
const value = match[operand];
|
|
4492
|
+
if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
|
|
4493
|
+
return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
4494
|
+
}
|
|
4495
|
+
if (Array.isArray(value)) {
|
|
4496
|
+
if (value.length > LIMITS.maxOperandItems) {
|
|
4497
|
+
return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
|
|
4498
|
+
}
|
|
4499
|
+
if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
|
|
4500
|
+
return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
3997
4504
|
const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
|
|
3998
4505
|
if (!parameterless && (parameter === void 0 || parameter === null)) {
|
|
3999
4506
|
return `match type "${match.type}" needs a parameter`;
|
|
@@ -4016,11 +4523,20 @@ function matchProblem(match, parameter) {
|
|
|
4016
4523
|
} else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
|
|
4017
4524
|
return `match type "${match.type}" needs a value`;
|
|
4018
4525
|
}
|
|
4526
|
+
if (match.type === "array_key_value") {
|
|
4527
|
+
const paths = Array.isArray(match.key) ? match.key : [match.key];
|
|
4528
|
+
if (paths.some((path9) => typeof path9 === "string" && path9.split(".").length > LIMITS.maxPathSegments)) {
|
|
4529
|
+
return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4019
4532
|
return null;
|
|
4020
4533
|
}
|
|
4021
4534
|
function mutationsProblem(mutations) {
|
|
4022
4535
|
if (mutations === void 0 || mutations === null) return null;
|
|
4023
4536
|
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
4537
|
+
if (mutations.length > LIMITS.maxOperandItems) {
|
|
4538
|
+
return `mutations has more than ${LIMITS.maxOperandItems} entries`;
|
|
4539
|
+
}
|
|
4024
4540
|
for (const mutation of mutations) {
|
|
4025
4541
|
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
4026
4542
|
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
@@ -4040,10 +4556,19 @@ function whenProblem(when) {
|
|
|
4040
4556
|
if ("path" in when && !isNonEmptyString(when.path)) {
|
|
4041
4557
|
return "when.path must be a non-empty string";
|
|
4042
4558
|
}
|
|
4559
|
+
if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
|
|
4560
|
+
return `when.path is longer than ${LIMITS.maxValueLength} chars`;
|
|
4561
|
+
}
|
|
4043
4562
|
if ("method" in when) {
|
|
4044
4563
|
const methods = Array.isArray(when.method) ? when.method : [when.method];
|
|
4045
4564
|
if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
|
|
4565
|
+
if (methods.length > LIMITS.maxParameterItems) {
|
|
4566
|
+
return `when.method has more than ${LIMITS.maxParameterItems} entries`;
|
|
4567
|
+
}
|
|
4046
4568
|
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
4569
|
+
if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
|
|
4570
|
+
return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
|
|
4571
|
+
}
|
|
4047
4572
|
}
|
|
4048
4573
|
return null;
|
|
4049
4574
|
}
|
|
@@ -4067,6 +4592,11 @@ function rulePropertyProblem(rule) {
|
|
|
4067
4592
|
const problem = operandShapeProblem(shape, rule[property]);
|
|
4068
4593
|
if (problem) return `"${property}" ${problem}`;
|
|
4069
4594
|
}
|
|
4595
|
+
for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
|
|
4596
|
+
if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
|
|
4597
|
+
return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4070
4600
|
return null;
|
|
4071
4601
|
}
|
|
4072
4602
|
function actionProblem(action, rule) {
|
|
@@ -4096,46 +4626,76 @@ var PHASES2 = new Set(PHASES);
|
|
|
4096
4626
|
var ACTIONS2 = new Set(ACTIONS);
|
|
4097
4627
|
function validateBundle(bundle, opts = {}) {
|
|
4098
4628
|
const rejected = [];
|
|
4629
|
+
let omittedRejections = 0;
|
|
4630
|
+
const reject = (entry) => {
|
|
4631
|
+
if (rejected.length < 100) rejected.push(entry);
|
|
4632
|
+
else omittedRejections++;
|
|
4633
|
+
};
|
|
4099
4634
|
const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
|
|
4100
4635
|
const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
|
|
4636
|
+
const bundleBudget = { count: 0 };
|
|
4101
4637
|
const firewall = [];
|
|
4102
|
-
for (const rule of inFirewall) {
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
continue;
|
|
4106
|
-
}
|
|
4107
|
-
const reason = enforceableRuleProblem(rule);
|
|
4108
|
-
if (reason) rejected.push({ id: idOf(rule), reason });
|
|
4638
|
+
for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
|
|
4639
|
+
const reason = enforceableRuleProblem(rule, bundleBudget);
|
|
4640
|
+
if (reason) reject({ id: idOf(rule), reason });
|
|
4109
4641
|
else firewall.push(rule);
|
|
4110
4642
|
}
|
|
4643
|
+
if (inFirewall.length > LIMITS2.maxRules) {
|
|
4644
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
|
|
4645
|
+
}
|
|
4111
4646
|
const whitelists = [];
|
|
4112
|
-
for (const wl of inWhitelists) {
|
|
4113
|
-
if (whitelists.length >= LIMITS2.maxWhitelists) {
|
|
4114
|
-
rejected.push({ id: idOf(wl), reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
4115
|
-
continue;
|
|
4116
|
-
}
|
|
4647
|
+
for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
|
|
4117
4648
|
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
4118
|
-
|
|
4649
|
+
reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
4119
4650
|
continue;
|
|
4120
4651
|
}
|
|
4121
4652
|
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
4122
|
-
|
|
4653
|
+
reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
4123
4654
|
continue;
|
|
4124
4655
|
}
|
|
4125
|
-
const reason = conditionsProblem(wl?.rule_v2);
|
|
4126
|
-
if (reason)
|
|
4656
|
+
const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
4657
|
+
if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
4127
4658
|
else whitelists.push(wl);
|
|
4128
4659
|
}
|
|
4660
|
+
if (inWhitelists.length > LIMITS2.maxWhitelists) {
|
|
4661
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
4662
|
+
}
|
|
4663
|
+
let whitelistKeys = {};
|
|
4664
|
+
if (bundle?.whitelist_keys !== void 0) {
|
|
4665
|
+
if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
|
|
4666
|
+
reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
|
|
4667
|
+
} else {
|
|
4668
|
+
const entries = Object.entries(bundle.whitelist_keys);
|
|
4669
|
+
if (entries.length > LIMITS2.maxMapEntries) {
|
|
4670
|
+
reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
|
|
4671
|
+
} else {
|
|
4672
|
+
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));
|
|
4673
|
+
if (invalid) {
|
|
4674
|
+
reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
|
|
4675
|
+
} else {
|
|
4676
|
+
whitelistKeys = bundle.whitelist_keys;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
if (omittedRejections > 0) {
|
|
4682
|
+
rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
|
|
4683
|
+
}
|
|
4129
4684
|
return {
|
|
4130
|
-
bundle: { ...bundle, firewall, whitelists },
|
|
4685
|
+
bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
|
|
4131
4686
|
rejected
|
|
4132
4687
|
};
|
|
4133
4688
|
}
|
|
4134
4689
|
function idOf(rule) {
|
|
4135
4690
|
const id = rule?.id ?? rule?.rule_id;
|
|
4136
|
-
|
|
4691
|
+
if (id === void 0 || id === null) return "(unidentified)";
|
|
4692
|
+
try {
|
|
4693
|
+
return String(id).slice(0, 128);
|
|
4694
|
+
} catch {
|
|
4695
|
+
return "(unidentified)";
|
|
4696
|
+
}
|
|
4137
4697
|
}
|
|
4138
|
-
function enforceableRuleProblem(rule) {
|
|
4698
|
+
function enforceableRuleProblem(rule, bundleBudget = null) {
|
|
4139
4699
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
4140
4700
|
const nullReason = nullPropertyProblem(rule);
|
|
4141
4701
|
if (nullReason) return nullReason;
|
|
@@ -4146,9 +4706,9 @@ function enforceableRuleProblem(rule) {
|
|
|
4146
4706
|
if (scopeReason) return scopeReason;
|
|
4147
4707
|
const propertyReason = rulePropertyProblem(rule);
|
|
4148
4708
|
if (propertyReason) return propertyReason;
|
|
4149
|
-
return conditionsProblem(rule.rule_v2);
|
|
4709
|
+
return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
4150
4710
|
}
|
|
4151
|
-
function conditionsProblem(conditions, depth = 0) {
|
|
4711
|
+
function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
|
|
4152
4712
|
if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
|
|
4153
4713
|
if (conditions.length === 0) return "rule_v2 is empty (would never match)";
|
|
4154
4714
|
if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
|
|
@@ -4156,10 +4716,20 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
4156
4716
|
return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
|
|
4157
4717
|
}
|
|
4158
4718
|
for (const c of conditions) {
|
|
4719
|
+
ruleBudget.count++;
|
|
4720
|
+
if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
|
|
4721
|
+
return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
|
|
4722
|
+
}
|
|
4723
|
+
if (bundleBudget) {
|
|
4724
|
+
bundleBudget.count++;
|
|
4725
|
+
if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
|
|
4726
|
+
return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4159
4729
|
const shapeReason = conditionShapeProblem(c);
|
|
4160
4730
|
if (shapeReason) return shapeReason;
|
|
4161
4731
|
if (isGroup(c)) {
|
|
4162
|
-
const nested = conditionsProblem(c.rules, depth + 1);
|
|
4732
|
+
const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
|
|
4163
4733
|
if (nested) return nested;
|
|
4164
4734
|
continue;
|
|
4165
4735
|
}
|
|
@@ -4173,11 +4743,17 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
4173
4743
|
if (m.type === "regex") {
|
|
4174
4744
|
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
4175
4745
|
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
4746
|
+
if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
|
|
4176
4747
|
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
4177
4748
|
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
4178
4749
|
}
|
|
4179
4750
|
if (m.match) {
|
|
4180
|
-
const nested = conditionsProblem(
|
|
4751
|
+
const nested = conditionsProblem(
|
|
4752
|
+
[{ parameter: c.parameter, match: m.match }],
|
|
4753
|
+
depth + 1,
|
|
4754
|
+
ruleBudget,
|
|
4755
|
+
bundleBudget
|
|
4756
|
+
);
|
|
4181
4757
|
if (nested) return nested;
|
|
4182
4758
|
}
|
|
4183
4759
|
}
|
|
@@ -4391,6 +4967,7 @@ function asText(value) {
|
|
|
4391
4967
|
}
|
|
4392
4968
|
|
|
4393
4969
|
// src/protect/engine/pulse-client.js
|
|
4970
|
+
init_safe_origin();
|
|
4394
4971
|
init_pulse_token();
|
|
4395
4972
|
init_build_id();
|
|
4396
4973
|
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
@@ -4459,7 +5036,7 @@ var PulseRuleClient = class {
|
|
|
4459
5036
|
if (!response.ok) {
|
|
4460
5037
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
4461
5038
|
}
|
|
4462
|
-
const data = await response
|
|
5039
|
+
const data = await readBoundedJson(response);
|
|
4463
5040
|
if (!data || !Array.isArray(data.firewall)) {
|
|
4464
5041
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
4465
5042
|
}
|
|
@@ -4520,19 +5097,18 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
4520
5097
|
const method = (request.method || "GET").toUpperCase();
|
|
4521
5098
|
const headers = {};
|
|
4522
5099
|
request.headers.forEach((value, key) => {
|
|
4523
|
-
headers
|
|
5100
|
+
setOwn(headers, key.toLowerCase(), value);
|
|
4524
5101
|
});
|
|
4525
5102
|
const query = {};
|
|
4526
5103
|
for (const [key, value] of url.searchParams) {
|
|
4527
|
-
|
|
4528
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
4529
|
-
} else {
|
|
4530
|
-
query[key] = value;
|
|
4531
|
-
}
|
|
5104
|
+
appendOwn(query, key, value);
|
|
4532
5105
|
}
|
|
4533
5106
|
let rawBody = "";
|
|
5107
|
+
let bodyInspectionSkip = null;
|
|
4534
5108
|
if (method !== "GET" && method !== "HEAD") {
|
|
4535
|
-
|
|
5109
|
+
const read = await readCappedText(request, maxBodyBytes);
|
|
5110
|
+
rawBody = read.text;
|
|
5111
|
+
bodyInspectionSkip = read.skip;
|
|
4536
5112
|
}
|
|
4537
5113
|
const contentType = headers["content-type"] || "";
|
|
4538
5114
|
let body = {};
|
|
@@ -4559,7 +5135,8 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
4559
5135
|
cookies: parseCookies(headers.cookie),
|
|
4560
5136
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
4561
5137
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
4562
|
-
_rawBody: rawBody
|
|
5138
|
+
_rawBody: rawBody,
|
|
5139
|
+
...bodyInspectionSkip === null ? {} : { _bodyInspectionSkip: bodyInspectionSkip }
|
|
4563
5140
|
};
|
|
4564
5141
|
}
|
|
4565
5142
|
async function readCappedText(request, max) {
|
|
@@ -4567,7 +5144,7 @@ async function readCappedText(request, max) {
|
|
|
4567
5144
|
try {
|
|
4568
5145
|
clone = request.clone();
|
|
4569
5146
|
} catch {
|
|
4570
|
-
return "";
|
|
5147
|
+
return { text: "", skip: "clone-failed" };
|
|
4571
5148
|
}
|
|
4572
5149
|
const body = clone.body;
|
|
4573
5150
|
if (body && typeof body.getReader === "function") {
|
|
@@ -4578,23 +5155,30 @@ async function readCappedText(request, max) {
|
|
|
4578
5155
|
for (; ; ) {
|
|
4579
5156
|
const { done, value } = await reader.read();
|
|
4580
5157
|
if (done) break;
|
|
4581
|
-
if (!value
|
|
5158
|
+
if (!value) continue;
|
|
4582
5159
|
const take = Math.min(value.byteLength, max - buffered);
|
|
4583
|
-
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
5160
|
+
if (take > 0) chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
4584
5161
|
buffered += take;
|
|
5162
|
+
if (buffered >= max) {
|
|
5163
|
+
void reader.cancel().catch(() => {
|
|
5164
|
+
});
|
|
5165
|
+
return { text: decodeChunks(chunks, buffered), skip: "body-cap" };
|
|
5166
|
+
}
|
|
4585
5167
|
}
|
|
4586
5168
|
} catch {
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
return "";
|
|
5169
|
+
void reader.cancel().catch(() => {
|
|
5170
|
+
});
|
|
5171
|
+
return { text: "", skip: "read-failed" };
|
|
5172
|
+
} finally {
|
|
5173
|
+
reader.releaseLock();
|
|
4593
5174
|
}
|
|
5175
|
+
return { text: decodeChunks(chunks, buffered), skip: null };
|
|
4594
5176
|
}
|
|
5177
|
+
return { text: "", skip: "unreadable-body" };
|
|
5178
|
+
}
|
|
5179
|
+
function decodeChunks(chunks, total) {
|
|
4595
5180
|
try {
|
|
4596
|
-
|
|
4597
|
-
return text.length > max ? text.slice(0, max) : text;
|
|
5181
|
+
return new TextDecoder().decode(concatChunks(chunks, total));
|
|
4598
5182
|
} catch {
|
|
4599
5183
|
return "";
|
|
4600
5184
|
}
|
|
@@ -4621,7 +5205,7 @@ function parseBody(rawBody, contentType) {
|
|
|
4621
5205
|
}
|
|
4622
5206
|
if (isForm) {
|
|
4623
5207
|
const body = {};
|
|
4624
|
-
for (const [k, v] of new URLSearchParams(rawBody)) body
|
|
5208
|
+
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
4625
5209
|
return { body, files: void 0 };
|
|
4626
5210
|
}
|
|
4627
5211
|
if (isJson || isAmbiguous) {
|
|
@@ -4649,9 +5233,9 @@ function parseMultipart(rawBody, boundary) {
|
|
|
4649
5233
|
if (filename !== void 0) {
|
|
4650
5234
|
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
4651
5235
|
const file = { filename, type: partType, content };
|
|
4652
|
-
files
|
|
5236
|
+
appendOwn(files, name, file);
|
|
4653
5237
|
} else {
|
|
4654
|
-
body
|
|
5238
|
+
appendOwn(body, name, content);
|
|
4655
5239
|
}
|
|
4656
5240
|
}
|
|
4657
5241
|
return { body, files };
|
|
@@ -4666,7 +5250,7 @@ function parseCookies(header) {
|
|
|
4666
5250
|
if (idx === -1) {
|
|
4667
5251
|
continue;
|
|
4668
5252
|
}
|
|
4669
|
-
cookies
|
|
5253
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
4670
5254
|
}
|
|
4671
5255
|
return cookies;
|
|
4672
5256
|
}
|
|
@@ -4676,7 +5260,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
4676
5260
|
const method = (req.method || "GET").toUpperCase();
|
|
4677
5261
|
const headers = {};
|
|
4678
5262
|
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
4679
|
-
headers
|
|
5263
|
+
setOwn(headers, key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value);
|
|
4680
5264
|
}
|
|
4681
5265
|
const host = headers.host || "localhost";
|
|
4682
5266
|
let url;
|
|
@@ -4691,11 +5275,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
4691
5275
|
}
|
|
4692
5276
|
const query = {};
|
|
4693
5277
|
for (const [key, value] of url.searchParams) {
|
|
4694
|
-
|
|
4695
|
-
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
|
4696
|
-
} else {
|
|
4697
|
-
query[key] = value;
|
|
4698
|
-
}
|
|
5278
|
+
appendOwn(query, key, value);
|
|
4699
5279
|
}
|
|
4700
5280
|
const contentType = headers["content-type"] || "";
|
|
4701
5281
|
let body = {};
|
|
@@ -4736,7 +5316,7 @@ function parseCookies2(header) {
|
|
|
4736
5316
|
if (idx === -1) {
|
|
4737
5317
|
continue;
|
|
4738
5318
|
}
|
|
4739
|
-
cookies
|
|
5319
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
4740
5320
|
}
|
|
4741
5321
|
return cookies;
|
|
4742
5322
|
}
|
|
@@ -4792,6 +5372,67 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
4792
5372
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
4793
5373
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
4794
5374
|
const MAX_REDIRECTS = 20;
|
|
5375
|
+
const MAX_REPLAY_BODY_BYTES = 1024 * 1024;
|
|
5376
|
+
const captureReplayBody = (request) => {
|
|
5377
|
+
if (request.method === "GET" || request.method === "HEAD" || !request.body) {
|
|
5378
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: false }), cancel() {
|
|
5379
|
+
} };
|
|
5380
|
+
}
|
|
5381
|
+
let reader;
|
|
5382
|
+
try {
|
|
5383
|
+
reader = request.clone().body?.getReader();
|
|
5384
|
+
} catch {
|
|
5385
|
+
reader = null;
|
|
5386
|
+
}
|
|
5387
|
+
if (!reader) {
|
|
5388
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: true }), cancel() {
|
|
5389
|
+
} };
|
|
5390
|
+
}
|
|
5391
|
+
let cancelled = false;
|
|
5392
|
+
const result = (async () => {
|
|
5393
|
+
const chunks = [];
|
|
5394
|
+
let total = 0;
|
|
5395
|
+
try {
|
|
5396
|
+
for (; ; ) {
|
|
5397
|
+
const { done, value } = await reader.read();
|
|
5398
|
+
if (done || cancelled) break;
|
|
5399
|
+
total += value.byteLength;
|
|
5400
|
+
if (total > MAX_REPLAY_BODY_BYTES) {
|
|
5401
|
+
void reader.cancel().catch(() => {
|
|
5402
|
+
});
|
|
5403
|
+
return { body: void 0, tooLarge: true };
|
|
5404
|
+
}
|
|
5405
|
+
chunks.push(value);
|
|
5406
|
+
}
|
|
5407
|
+
if (cancelled) return { body: void 0, tooLarge: false };
|
|
5408
|
+
const body = new Uint8Array(total);
|
|
5409
|
+
let offset = 0;
|
|
5410
|
+
for (const chunk of chunks) {
|
|
5411
|
+
body.set(chunk, offset);
|
|
5412
|
+
offset += chunk.byteLength;
|
|
5413
|
+
}
|
|
5414
|
+
return { body, tooLarge: false };
|
|
5415
|
+
} catch {
|
|
5416
|
+
return { body: void 0, tooLarge: true };
|
|
5417
|
+
} finally {
|
|
5418
|
+
reader.releaseLock();
|
|
5419
|
+
}
|
|
5420
|
+
})();
|
|
5421
|
+
return {
|
|
5422
|
+
result,
|
|
5423
|
+
cancel() {
|
|
5424
|
+
cancelled = true;
|
|
5425
|
+
void reader.cancel().catch(() => {
|
|
5426
|
+
});
|
|
5427
|
+
}
|
|
5428
|
+
};
|
|
5429
|
+
};
|
|
5430
|
+
const discardResponseBody = async (response) => {
|
|
5431
|
+
try {
|
|
5432
|
+
await response?.body?.cancel?.();
|
|
5433
|
+
} catch {
|
|
5434
|
+
}
|
|
5435
|
+
};
|
|
4795
5436
|
const screenUrl = async (u, method) => {
|
|
4796
5437
|
let host = null;
|
|
4797
5438
|
try {
|
|
@@ -4817,20 +5458,43 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
4817
5458
|
if (callerRedirect !== "follow") return originalFetch(input, init);
|
|
4818
5459
|
const headers = new Headers(cur.headers);
|
|
4819
5460
|
const signal = cur.signal;
|
|
4820
|
-
|
|
5461
|
+
const replay = captureReplayBody(cur);
|
|
5462
|
+
let body;
|
|
4821
5463
|
for (let hop = 0; ; hop++) {
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
5464
|
+
let resp;
|
|
5465
|
+
try {
|
|
5466
|
+
resp = await originalFetch(
|
|
5467
|
+
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
5468
|
+
);
|
|
5469
|
+
} catch (error) {
|
|
5470
|
+
replay.cancel();
|
|
5471
|
+
throw error;
|
|
5472
|
+
}
|
|
4825
5473
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
4826
|
-
if (!location)
|
|
4827
|
-
|
|
5474
|
+
if (!location) {
|
|
5475
|
+
replay.cancel();
|
|
5476
|
+
return resp;
|
|
5477
|
+
}
|
|
5478
|
+
await discardResponseBody(resp);
|
|
5479
|
+
if (hop >= MAX_REDIRECTS) {
|
|
5480
|
+
replay.cancel();
|
|
5481
|
+
throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
5482
|
+
}
|
|
4828
5483
|
const next = new URL(location, url).href;
|
|
4829
5484
|
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
4830
5485
|
method = "GET";
|
|
4831
5486
|
body = void 0;
|
|
5487
|
+
replay.cancel();
|
|
4832
5488
|
headers.delete("content-type");
|
|
4833
5489
|
headers.delete("content-length");
|
|
5490
|
+
} else if (body === void 0 && method !== "GET" && method !== "HEAD") {
|
|
5491
|
+
const captured = await replay.result;
|
|
5492
|
+
if (captured.tooLarge) {
|
|
5493
|
+
throw new Error(
|
|
5494
|
+
`Patchstack blocked a redirect that required replaying more than ${MAX_REPLAY_BODY_BYTES} bytes`
|
|
5495
|
+
);
|
|
5496
|
+
}
|
|
5497
|
+
body = captured.body;
|
|
4834
5498
|
}
|
|
4835
5499
|
if (new URL(next).origin !== new URL(url).origin) {
|
|
4836
5500
|
headers.delete("authorization");
|
|
@@ -5141,7 +5805,7 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
5141
5805
|
//
|
|
5142
5806
|
// Accepts a real newline and a JSON-escaped one. Most traces reach a client inside a JSON error
|
|
5143
5807
|
// body, where the newline is the two characters `\` and `n`.
|
|
5144
|
-
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(?:\\n|\\\\n)\\s*at\\s
|
|
5808
|
+
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+\\)/" } }]
|
|
5145
5809
|
},
|
|
5146
5810
|
{
|
|
5147
5811
|
id: "resp-sql-error",
|
|
@@ -5275,9 +5939,18 @@ async function loadFs() {
|
|
|
5275
5939
|
try {
|
|
5276
5940
|
const [fs, path9] = await Promise.all([import("fs"), import("path")]);
|
|
5277
5941
|
fsMod = {
|
|
5942
|
+
chmodSync: fs.chmodSync,
|
|
5943
|
+
closeSync: fs.closeSync,
|
|
5944
|
+
constants: fs.constants,
|
|
5945
|
+
fstatSync: fs.fstatSync,
|
|
5946
|
+
fsyncSync: fs.fsyncSync,
|
|
5947
|
+
lstatSync: fs.lstatSync,
|
|
5948
|
+
openSync: fs.openSync,
|
|
5278
5949
|
readFileSync: fs.readFileSync,
|
|
5279
|
-
writeFileSync: fs.writeFileSync,
|
|
5280
5950
|
mkdirSync: fs.mkdirSync,
|
|
5951
|
+
renameSync: fs.renameSync,
|
|
5952
|
+
unlinkSync: fs.unlinkSync,
|
|
5953
|
+
writeFileSync: fs.writeFileSync,
|
|
5281
5954
|
join: path9.join
|
|
5282
5955
|
};
|
|
5283
5956
|
} catch {
|
|
@@ -5330,22 +6003,84 @@ async function cacheWrite(dir, env) {
|
|
|
5330
6003
|
if (!dir) return;
|
|
5331
6004
|
const fs = await loadFs();
|
|
5332
6005
|
if (!fs) return;
|
|
6006
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
6007
|
+
const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
|
|
6008
|
+
let descriptor = null;
|
|
5333
6009
|
try {
|
|
5334
6010
|
fs.mkdirSync(dir, { recursive: true });
|
|
5335
|
-
fs.
|
|
6011
|
+
const directory = fs.lstatSync(dir);
|
|
6012
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return;
|
|
6013
|
+
let mode;
|
|
6014
|
+
try {
|
|
6015
|
+
const current = fs.lstatSync(target);
|
|
6016
|
+
if (current.isSymbolicLink() || !current.isFile()) return;
|
|
6017
|
+
mode = current.mode & 4095;
|
|
6018
|
+
} catch (error) {
|
|
6019
|
+
if (error?.code !== "ENOENT") return;
|
|
6020
|
+
}
|
|
6021
|
+
descriptor = fs.openSync(temporary, "wx", mode ?? 384);
|
|
6022
|
+
fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
|
|
6023
|
+
fs.fsyncSync(descriptor);
|
|
6024
|
+
fs.closeSync(descriptor);
|
|
6025
|
+
descriptor = null;
|
|
6026
|
+
if (mode !== void 0) fs.chmodSync(temporary, mode);
|
|
6027
|
+
try {
|
|
6028
|
+
fs.renameSync(temporary, target);
|
|
6029
|
+
} catch (error) {
|
|
6030
|
+
if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
|
|
6031
|
+
try {
|
|
6032
|
+
fs.unlinkSync(target);
|
|
6033
|
+
} catch (unlinkError) {
|
|
6034
|
+
if (unlinkError?.code !== "ENOENT") throw unlinkError;
|
|
6035
|
+
}
|
|
6036
|
+
fs.renameSync(temporary, target);
|
|
6037
|
+
}
|
|
5336
6038
|
} catch {
|
|
6039
|
+
} finally {
|
|
6040
|
+
if (descriptor !== null) {
|
|
6041
|
+
try {
|
|
6042
|
+
fs.closeSync(descriptor);
|
|
6043
|
+
} catch {
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
try {
|
|
6047
|
+
fs.unlinkSync(temporary);
|
|
6048
|
+
} catch {
|
|
6049
|
+
}
|
|
5337
6050
|
}
|
|
5338
6051
|
}
|
|
5339
6052
|
async function cacheRead(dir) {
|
|
5340
6053
|
if (!dir) return null;
|
|
5341
6054
|
const fs = await loadFs();
|
|
5342
6055
|
if (!fs) return null;
|
|
6056
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
6057
|
+
let descriptor = null;
|
|
5343
6058
|
try {
|
|
5344
|
-
|
|
6059
|
+
const entry = fs.lstatSync(target);
|
|
6060
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
|
|
6061
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
6062
|
+
descriptor = fs.openSync(target, flags);
|
|
6063
|
+
const opened = fs.fstatSync(descriptor);
|
|
6064
|
+
if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
|
|
6065
|
+
return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
|
|
5345
6066
|
} catch {
|
|
5346
6067
|
return null;
|
|
6068
|
+
} finally {
|
|
6069
|
+
if (descriptor !== null) {
|
|
6070
|
+
try {
|
|
6071
|
+
fs.closeSync(descriptor);
|
|
6072
|
+
} catch {
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
5347
6075
|
}
|
|
5348
6076
|
}
|
|
6077
|
+
function randomSuffix() {
|
|
6078
|
+
try {
|
|
6079
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
6080
|
+
} catch {
|
|
6081
|
+
}
|
|
6082
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
6083
|
+
}
|
|
5349
6084
|
function toEnvelope(value) {
|
|
5350
6085
|
if (!value || typeof value !== "object") return null;
|
|
5351
6086
|
if (value.bundle && typeof value.bundle === "object") {
|
|
@@ -5371,7 +6106,11 @@ init_build_id();
|
|
|
5371
6106
|
function liveUpdateRejections(res, options) {
|
|
5372
6107
|
if (options.acceptPartialBundle) return [];
|
|
5373
6108
|
const { rejected } = validateBundle(
|
|
5374
|
-
{
|
|
6109
|
+
{
|
|
6110
|
+
firewall: Array.isArray(res.firewall) ? res.firewall : [],
|
|
6111
|
+
whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
|
|
6112
|
+
whitelist_keys: res.whitelist_keys
|
|
6113
|
+
},
|
|
5375
6114
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
5376
6115
|
);
|
|
5377
6116
|
return rejected;
|
|
@@ -5542,7 +6281,8 @@ function normalizeBundle(b, options = {}) {
|
|
|
5542
6281
|
const { bundle: checked, rejected } = validateBundle(
|
|
5543
6282
|
{
|
|
5544
6283
|
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
5545
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : []
|
|
6284
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
6285
|
+
whitelist_keys: b.whitelist_keys
|
|
5546
6286
|
},
|
|
5547
6287
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
5548
6288
|
);
|
|
@@ -5562,7 +6302,7 @@ function normalizeBundle(b, options = {}) {
|
|
|
5562
6302
|
return {
|
|
5563
6303
|
firewall: checked.firewall,
|
|
5564
6304
|
whitelists: checked.whitelists,
|
|
5565
|
-
whitelist_keys:
|
|
6305
|
+
whitelist_keys: checked.whitelist_keys,
|
|
5566
6306
|
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
5567
6307
|
};
|
|
5568
6308
|
}
|
|
@@ -5606,18 +6346,23 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
5606
6346
|
};
|
|
5607
6347
|
}
|
|
5608
6348
|
function makeRefreshHandler(tick, secret) {
|
|
6349
|
+
let inflight2 = null;
|
|
6350
|
+
const runOnce = () => {
|
|
6351
|
+
if (inflight2) return inflight2;
|
|
6352
|
+
let current;
|
|
6353
|
+
current = Promise.resolve().then(() => tick()).finally(() => {
|
|
6354
|
+
if (inflight2 === current) inflight2 = null;
|
|
6355
|
+
});
|
|
6356
|
+
inflight2 = current;
|
|
6357
|
+
return current;
|
|
6358
|
+
};
|
|
5609
6359
|
return async (request) => {
|
|
5610
6360
|
if (!secret) return new Response("not found", { status: 404 });
|
|
5611
|
-
|
|
5612
|
-
try {
|
|
5613
|
-
provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
|
|
5614
|
-
} catch {
|
|
5615
|
-
provided = null;
|
|
5616
|
-
}
|
|
6361
|
+
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
5617
6362
|
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
5618
6363
|
let refreshed = true;
|
|
5619
6364
|
try {
|
|
5620
|
-
const status = await
|
|
6365
|
+
const status = await runOnce();
|
|
5621
6366
|
if (status && status.ok === false) refreshed = false;
|
|
5622
6367
|
} catch {
|
|
5623
6368
|
refreshed = false;
|
|
@@ -5628,6 +6373,7 @@ function makeRefreshHandler(tick, secret) {
|
|
|
5628
6373
|
|
|
5629
6374
|
// src/protect/detections.js
|
|
5630
6375
|
init_pulse_token();
|
|
6376
|
+
init_safe_origin();
|
|
5631
6377
|
var DEFAULT_BASE_URL3 = "https://api.patchstack.com/monitor/pulse";
|
|
5632
6378
|
var DEFAULT_FLUSH_MS = 5e3;
|
|
5633
6379
|
var MAX_BATCH = 50;
|
|
@@ -6365,10 +7111,14 @@ function hardensWithoutBody(rule) {
|
|
|
6365
7111
|
}
|
|
6366
7112
|
|
|
6367
7113
|
// src/protect/firewall-log.js
|
|
7114
|
+
init_safe_origin();
|
|
7115
|
+
init_bounded_response();
|
|
6368
7116
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
6369
7117
|
var STOP_BUDGET_MS2 = 5e3;
|
|
7118
|
+
var ATTEMPT_TIMEOUT_MS2 = 1e4;
|
|
6370
7119
|
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
6371
7120
|
var MAX_BATCH2 = 50;
|
|
7121
|
+
var MAX_QUEUE2 = 500;
|
|
6372
7122
|
var TOKEN_SKEW_MS2 = 6e4;
|
|
6373
7123
|
function parseApiKey(apiKey) {
|
|
6374
7124
|
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
@@ -6390,14 +7140,16 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
6390
7140
|
}
|
|
6391
7141
|
if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
|
|
6392
7142
|
try {
|
|
6393
|
-
|
|
7143
|
+
const candidate = new URL(pulseOrManifestUrl).origin;
|
|
7144
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
6394
7145
|
} catch {
|
|
6395
7146
|
}
|
|
6396
7147
|
}
|
|
6397
7148
|
const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
|
|
6398
7149
|
if (typeof endpoint === "string" && endpoint.length > 0) {
|
|
6399
7150
|
try {
|
|
6400
|
-
|
|
7151
|
+
const candidate = new URL(endpoint).origin;
|
|
7152
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
6401
7153
|
} catch {
|
|
6402
7154
|
}
|
|
6403
7155
|
}
|
|
@@ -6409,16 +7161,16 @@ function createFirewallLogReporter(opts) {
|
|
|
6409
7161
|
return { record() {
|
|
6410
7162
|
}, flush: () => Promise.resolve(), stop: () => Promise.resolve() };
|
|
6411
7163
|
}
|
|
6412
|
-
const apiBase = (opts.apiBase
|
|
7164
|
+
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
6413
7165
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
6414
7166
|
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
|
|
6415
7167
|
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
6416
7168
|
let queue = [];
|
|
6417
7169
|
let timer = null;
|
|
6418
7170
|
let stopped = false;
|
|
6419
|
-
|
|
7171
|
+
let inFlight = null;
|
|
6420
7172
|
let drainPromise = null;
|
|
6421
|
-
|
|
7173
|
+
let activeController = null;
|
|
6422
7174
|
let ended = false;
|
|
6423
7175
|
let cachedToken = null;
|
|
6424
7176
|
let tokenInflight = null;
|
|
@@ -6444,9 +7196,9 @@ function createFirewallLogReporter(opts) {
|
|
|
6444
7196
|
})
|
|
6445
7197
|
});
|
|
6446
7198
|
if (!res || !res.ok) return null;
|
|
6447
|
-
const body = await res
|
|
7199
|
+
const body = await readBoundedJson2(res);
|
|
6448
7200
|
const token = body?.access_token;
|
|
6449
|
-
if (typeof token !== "string" || token.length === 0) return null;
|
|
7201
|
+
if (typeof token !== "string" || token.length === 0 || token.length > 8192 || /[\u0000-\u0020\u007f-\u009f]/.test(token)) return null;
|
|
6450
7202
|
const expiresIn = Number(body?.expires_in);
|
|
6451
7203
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
6452
7204
|
cachedToken = { token, expiresAt: Date.now() + ttlMs };
|
|
@@ -6464,12 +7216,18 @@ function createFirewallLogReporter(opts) {
|
|
|
6464
7216
|
clearTimeout(timer);
|
|
6465
7217
|
timer = null;
|
|
6466
7218
|
}
|
|
6467
|
-
if (ended ||
|
|
7219
|
+
if (ended || typeof fetchImpl !== "function") return Promise.resolve();
|
|
7220
|
+
if (inFlight) return inFlight;
|
|
7221
|
+
if (queue.length === 0) return Promise.resolve();
|
|
6468
7222
|
const batch = queue.splice(0, MAX_BATCH2);
|
|
7223
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
7224
|
+
activeController = controller;
|
|
7225
|
+
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
7226
|
+
attemptTimer?.unref?.();
|
|
6469
7227
|
let entry;
|
|
6470
|
-
|
|
7228
|
+
entry = (async () => {
|
|
6471
7229
|
try {
|
|
6472
|
-
const token = await fetchAccessToken(
|
|
7230
|
+
const token = await fetchAccessToken(controller?.signal);
|
|
6473
7231
|
if (!token || ended) return;
|
|
6474
7232
|
const body = new URLSearchParams();
|
|
6475
7233
|
body.set("type", "firewall");
|
|
@@ -6484,19 +7242,24 @@ function createFirewallLogReporter(opts) {
|
|
|
6484
7242
|
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
6485
7243
|
},
|
|
6486
7244
|
body,
|
|
6487
|
-
// Both phases carry
|
|
6488
|
-
...
|
|
7245
|
+
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
7246
|
+
...controller ? { signal: controller.signal } : {}
|
|
6489
7247
|
});
|
|
6490
7248
|
if (p && typeof p.then === "function") await p.catch(() => {
|
|
6491
7249
|
});
|
|
6492
7250
|
} catch {
|
|
6493
7251
|
} finally {
|
|
6494
|
-
|
|
7252
|
+
clearTimeout(attemptTimer);
|
|
7253
|
+
if (activeController === controller) activeController = null;
|
|
7254
|
+
if (inFlight === entry) inFlight = null;
|
|
7255
|
+
if (!stopped && !ended && queue.length > 0 && !timer) {
|
|
7256
|
+
timer = setTimeout(flush, queue.length >= MAX_BATCH2 ? 0 : flushMs);
|
|
7257
|
+
timer?.unref?.();
|
|
7258
|
+
}
|
|
6495
7259
|
}
|
|
6496
7260
|
})();
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
return send;
|
|
7261
|
+
inFlight = entry;
|
|
7262
|
+
return entry;
|
|
6500
7263
|
};
|
|
6501
7264
|
return {
|
|
6502
7265
|
/**
|
|
@@ -6512,6 +7275,7 @@ function createFirewallLogReporter(opts) {
|
|
|
6512
7275
|
if (stopped) return;
|
|
6513
7276
|
const fid = event?.rule?.id;
|
|
6514
7277
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
7278
|
+
if (queue.length >= MAX_QUEUE2) return;
|
|
6515
7279
|
queue.push({
|
|
6516
7280
|
fid,
|
|
6517
7281
|
method: event.method ?? null,
|
|
@@ -6543,9 +7307,10 @@ function createFirewallLogReporter(opts) {
|
|
|
6543
7307
|
}
|
|
6544
7308
|
const terminate = () => {
|
|
6545
7309
|
ended = true;
|
|
6546
|
-
|
|
7310
|
+
activeController?.abort();
|
|
7311
|
+
activeController = null;
|
|
6547
7312
|
queue = [];
|
|
6548
|
-
|
|
7313
|
+
inFlight = null;
|
|
6549
7314
|
};
|
|
6550
7315
|
let budget = null;
|
|
6551
7316
|
const spent = new Promise((resolve) => {
|
|
@@ -6557,8 +7322,8 @@ function createFirewallLogReporter(opts) {
|
|
|
6557
7322
|
});
|
|
6558
7323
|
const work = (async () => {
|
|
6559
7324
|
while (!ended) {
|
|
6560
|
-
if (
|
|
6561
|
-
await
|
|
7325
|
+
if (inFlight) {
|
|
7326
|
+
await inFlight;
|
|
6562
7327
|
continue;
|
|
6563
7328
|
}
|
|
6564
7329
|
if (queue.length === 0) break;
|
|
@@ -6582,10 +7347,107 @@ function telemetryEnabled() {
|
|
|
6582
7347
|
|
|
6583
7348
|
// src/protect/supabase-guard.js
|
|
6584
7349
|
var GUARD_PATH = "/_patchstack/guard";
|
|
6585
|
-
var
|
|
6586
|
-
|
|
7350
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7351
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7352
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7353
|
+
"connection",
|
|
7354
|
+
"cookie",
|
|
7355
|
+
"cookie2",
|
|
7356
|
+
"forwarded",
|
|
7357
|
+
"host",
|
|
7358
|
+
"keep-alive",
|
|
7359
|
+
"proxy-authenticate",
|
|
7360
|
+
"proxy-authorization",
|
|
7361
|
+
"te",
|
|
7362
|
+
"trailer",
|
|
7363
|
+
"transfer-encoding",
|
|
7364
|
+
"upgrade",
|
|
7365
|
+
"x-forwarded-for",
|
|
7366
|
+
"x-forwarded-host",
|
|
7367
|
+
"x-forwarded-port",
|
|
7368
|
+
"x-forwarded-proto",
|
|
7369
|
+
"x-ps-target",
|
|
7370
|
+
"x-real-ip"
|
|
7371
|
+
]);
|
|
7372
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7373
|
+
"connection",
|
|
7374
|
+
"content-encoding",
|
|
7375
|
+
"content-length",
|
|
7376
|
+
"keep-alive",
|
|
7377
|
+
"proxy-authenticate",
|
|
7378
|
+
"proxy-authorization",
|
|
7379
|
+
"set-cookie",
|
|
7380
|
+
"set-cookie2",
|
|
7381
|
+
"te",
|
|
7382
|
+
"trailer",
|
|
7383
|
+
"transfer-encoding",
|
|
7384
|
+
"upgrade"
|
|
7385
|
+
]);
|
|
7386
|
+
var BodyTooLargeError = class extends Error {
|
|
7387
|
+
};
|
|
7388
|
+
async function boundedBody(request, maxBytes) {
|
|
7389
|
+
const stated2 = Number(request.headers.get("content-length"));
|
|
7390
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) throw new BodyTooLargeError();
|
|
7391
|
+
if (!request.body) return void 0;
|
|
7392
|
+
const reader = request.body.getReader();
|
|
7393
|
+
const chunks = [];
|
|
7394
|
+
let total = 0;
|
|
7395
|
+
try {
|
|
7396
|
+
for (; ; ) {
|
|
7397
|
+
const { done, value } = await reader.read();
|
|
7398
|
+
if (done) break;
|
|
7399
|
+
total += value.byteLength;
|
|
7400
|
+
if (total > maxBytes) {
|
|
7401
|
+
void reader.cancel().catch(() => {
|
|
7402
|
+
});
|
|
7403
|
+
throw new BodyTooLargeError();
|
|
7404
|
+
}
|
|
7405
|
+
chunks.push(value);
|
|
7406
|
+
}
|
|
7407
|
+
} finally {
|
|
7408
|
+
reader.releaseLock();
|
|
7409
|
+
}
|
|
7410
|
+
const body = new Uint8Array(total);
|
|
7411
|
+
let offset = 0;
|
|
7412
|
+
for (const chunk of chunks) {
|
|
7413
|
+
body.set(chunk, offset);
|
|
7414
|
+
offset += chunk.byteLength;
|
|
7415
|
+
}
|
|
7416
|
+
return body;
|
|
7417
|
+
}
|
|
7418
|
+
function requestHeaders(input) {
|
|
7419
|
+
const output = new Headers();
|
|
7420
|
+
const connectionTokens = new Set(
|
|
7421
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7422
|
+
);
|
|
7423
|
+
input.forEach((value, key) => {
|
|
7424
|
+
const lower = key.toLowerCase();
|
|
7425
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7426
|
+
});
|
|
7427
|
+
return output;
|
|
7428
|
+
}
|
|
7429
|
+
function responseHeaders(input) {
|
|
7430
|
+
const output = new Headers();
|
|
7431
|
+
const connectionTokens = new Set(
|
|
7432
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7433
|
+
);
|
|
7434
|
+
input.forEach((value, key) => {
|
|
7435
|
+
const lower = key.toLowerCase();
|
|
7436
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7437
|
+
});
|
|
7438
|
+
return output;
|
|
7439
|
+
}
|
|
7440
|
+
function createSupabaseGuard({
|
|
7441
|
+
protection,
|
|
7442
|
+
supabaseUrl,
|
|
7443
|
+
fetchImpl = fetch,
|
|
7444
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
7445
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
7446
|
+
}) {
|
|
6587
7447
|
const guard = protection.fetchGuard();
|
|
6588
7448
|
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
7449
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
7450
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
6589
7451
|
return async function handleGuardRequest(request) {
|
|
6590
7452
|
const target = request.headers.get("x-ps-target");
|
|
6591
7453
|
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
@@ -6599,29 +7461,45 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
6599
7461
|
return new Response("patchstack: target not allowed", { status: 403 });
|
|
6600
7462
|
}
|
|
6601
7463
|
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
6602
|
-
|
|
7464
|
+
let body;
|
|
7465
|
+
try {
|
|
7466
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
7467
|
+
} catch (error) {
|
|
7468
|
+
if (error instanceof BodyTooLargeError) {
|
|
7469
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
7470
|
+
}
|
|
7471
|
+
throw error;
|
|
7472
|
+
}
|
|
7473
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
6603
7474
|
const evalReq = new Request(targetUrl.toString(), {
|
|
6604
7475
|
method: request.method,
|
|
7476
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
7477
|
+
// sanitized at the cross-origin boundary below.
|
|
6605
7478
|
headers: request.headers,
|
|
6606
|
-
body
|
|
7479
|
+
body
|
|
6607
7480
|
});
|
|
6608
7481
|
const blocked = await guard(evalReq);
|
|
6609
7482
|
if (blocked) return blocked;
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
7483
|
+
let upstream;
|
|
7484
|
+
try {
|
|
7485
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
7486
|
+
method: request.method,
|
|
7487
|
+
headers: forwardHeaders,
|
|
7488
|
+
body,
|
|
7489
|
+
redirect: "manual",
|
|
7490
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
7491
|
+
});
|
|
7492
|
+
} catch (error) {
|
|
7493
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
7494
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
7495
|
+
}
|
|
7496
|
+
throw error;
|
|
7497
|
+
}
|
|
7498
|
+
const forwarded = new Response(upstream.body, {
|
|
7499
|
+
status: upstream.status,
|
|
7500
|
+
statusText: upstream.statusText,
|
|
7501
|
+
headers: responseHeaders(upstream.headers)
|
|
6622
7502
|
});
|
|
6623
|
-
const buf = await upstream.arrayBuffer();
|
|
6624
|
-
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
6625
7503
|
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
6626
7504
|
};
|
|
6627
7505
|
}
|
|
@@ -6924,9 +7802,13 @@ async function createProtection(options = {}) {
|
|
|
6924
7802
|
for (const name of Object.keys(headers)) {
|
|
6925
7803
|
const value = headers[name];
|
|
6926
7804
|
if (typeof value === "string") {
|
|
6927
|
-
headers
|
|
7805
|
+
setOwn(headers, name, applyRedactors(value, spanRedactors, mask));
|
|
6928
7806
|
} else if (Array.isArray(value)) {
|
|
6929
|
-
|
|
7807
|
+
setOwn(
|
|
7808
|
+
headers,
|
|
7809
|
+
name,
|
|
7810
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item)
|
|
7811
|
+
);
|
|
6930
7812
|
}
|
|
6931
7813
|
}
|
|
6932
7814
|
}
|
|
@@ -6976,7 +7858,13 @@ async function createProtection(options = {}) {
|
|
|
6976
7858
|
let result;
|
|
6977
7859
|
let shaped;
|
|
6978
7860
|
try {
|
|
6979
|
-
shaped = await fromFetchRequest(request, {
|
|
7861
|
+
shaped = await fromFetchRequest(request, {
|
|
7862
|
+
trustedProxy: options.trustedProxy,
|
|
7863
|
+
maxBodyBytes: options.maxBodyBytes
|
|
7864
|
+
});
|
|
7865
|
+
if (shaped?._bodyInspectionSkip) {
|
|
7866
|
+
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
7867
|
+
}
|
|
6980
7868
|
result = engine.evaluate(shaped);
|
|
6981
7869
|
} catch (err) {
|
|
6982
7870
|
notify(onError, err, "onError");
|
|
@@ -7066,7 +7954,7 @@ async function createProtection(options = {}) {
|
|
|
7066
7954
|
const out = {};
|
|
7067
7955
|
for (const [name, value] of writeHeadEntries(headers)) {
|
|
7068
7956
|
const key = name.toLowerCase();
|
|
7069
|
-
|
|
7957
|
+
appendOwn(out, key, value);
|
|
7070
7958
|
}
|
|
7071
7959
|
return out;
|
|
7072
7960
|
};
|
|
@@ -7602,7 +8490,6 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
7602
8490
|
const reader = body.getReader();
|
|
7603
8491
|
const chunks = [];
|
|
7604
8492
|
let size = 0;
|
|
7605
|
-
let over = false;
|
|
7606
8493
|
let sniffed = !sniff;
|
|
7607
8494
|
try {
|
|
7608
8495
|
for (; ; ) {
|
|
@@ -7611,20 +8498,27 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
7611
8498
|
if (!value) continue;
|
|
7612
8499
|
if (!sniffed) {
|
|
7613
8500
|
sniffed = true;
|
|
7614
|
-
if (looksBinary(value))
|
|
8501
|
+
if (looksBinary(value)) {
|
|
8502
|
+
void reader.cancel().catch(() => {
|
|
8503
|
+
});
|
|
8504
|
+
return { skip: "binary-body" };
|
|
8505
|
+
}
|
|
7615
8506
|
}
|
|
7616
8507
|
size += value.byteLength;
|
|
7617
|
-
if (
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
8508
|
+
if (size >= cap) {
|
|
8509
|
+
void reader.cancel().catch(() => {
|
|
8510
|
+
});
|
|
8511
|
+
return { skip: "body-cap" };
|
|
7621
8512
|
}
|
|
7622
8513
|
chunks.push(value);
|
|
7623
8514
|
}
|
|
7624
8515
|
} catch {
|
|
8516
|
+
void reader.cancel().catch(() => {
|
|
8517
|
+
});
|
|
7625
8518
|
return { skip: "read-failed" };
|
|
8519
|
+
} finally {
|
|
8520
|
+
reader.releaseLock();
|
|
7626
8521
|
}
|
|
7627
|
-
if (over) return { skip: "body-cap" };
|
|
7628
8522
|
try {
|
|
7629
8523
|
return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
|
|
7630
8524
|
} catch {
|
|
@@ -7652,10 +8546,10 @@ function concatBytes(chunks, total) {
|
|
|
7652
8546
|
function headerObject(headers) {
|
|
7653
8547
|
const out = {};
|
|
7654
8548
|
headers?.forEach?.((v, k) => {
|
|
7655
|
-
out
|
|
8549
|
+
setOwn(out, k.toLowerCase(), v);
|
|
7656
8550
|
});
|
|
7657
8551
|
const setCookies = headers?.getSetCookie?.();
|
|
7658
|
-
if (setCookies && setCookies.length) out
|
|
8552
|
+
if (setCookies && setCookies.length) setOwn(out, "set-cookie", setCookies);
|
|
7659
8553
|
return out;
|
|
7660
8554
|
}
|
|
7661
8555
|
function hasSpanMutations(rule) {
|
|
@@ -7824,7 +8718,7 @@ function isHeaderMutation(action) {
|
|
|
7824
8718
|
}
|
|
7825
8719
|
function applyHeaderMutation(headers, rule) {
|
|
7826
8720
|
if (rule.action === "remove-header") {
|
|
7827
|
-
for (const name of rule.remove_headers ?? []) headers
|
|
8721
|
+
for (const name of rule.remove_headers ?? []) setOwn(headers, String(name).toLowerCase(), null);
|
|
7828
8722
|
return;
|
|
7829
8723
|
}
|
|
7830
8724
|
if (rule.action === "set-header") {
|
|
@@ -7833,7 +8727,7 @@ function applyHeaderMutation(headers, rule) {
|
|
|
7833
8727
|
const key = String(name).toLowerCase();
|
|
7834
8728
|
const present2 = headers[key] != null && headers[key] !== "";
|
|
7835
8729
|
if (ensure && present2) continue;
|
|
7836
|
-
headers
|
|
8730
|
+
setOwn(headers, key, String(value));
|
|
7837
8731
|
}
|
|
7838
8732
|
return;
|
|
7839
8733
|
}
|