@patchstack/connect 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-INSTALL.md +2 -2
- package/README.md +4 -4
- package/dist/chunk-4OHAYSV2.js +295 -0
- package/dist/chunk-4OHAYSV2.js.map +1 -0
- package/dist/cli.js +1089 -470
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +450 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +461 -39
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +1 -1
- package/dist/protect.cjs +1226 -244
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +7 -2
- package/dist/protect.d.ts +7 -2
- package/dist/protect.edge.js +992 -179
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +846 -192
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2ALIJGZ3.js → refresh-manifest-IE6SPHG6.js} +168 -41
- package/dist/refresh-manifest-IE6SPHG6.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-IOODP4ZB.js +0 -104
- package/dist/chunk-IOODP4ZB.js.map +0 -1
- package/dist/refresh-manifest-2ALIJGZ3.js.map +0 -1
package/dist/protect.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;
|
|
61
256
|
}
|
|
62
|
-
if (
|
|
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;
|
|
262
|
+
}
|
|
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) {
|
|
@@ -185,8 +369,8 @@ var init_hosting = __esm({
|
|
|
185
369
|
RULES = [
|
|
186
370
|
{ platform: "netlify", any: ["NETLIFY", "NETLIFY_BUILD_BASE", "DEPLOY_PRIME_URL"] },
|
|
187
371
|
{ platform: "vercel", any: ["VERCEL", "VERCEL_ENV", "VERCEL_URL"] },
|
|
188
|
-
{ platform: "cloudflare", any: ["CF_PAGES", "CF_PAGES_URL", "CLOUDFLARE_ACCOUNT_ID"] },
|
|
189
|
-
{ platform: "aws", any: ["AWS_APP_ID", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV"] },
|
|
372
|
+
{ platform: "cloudflare", any: ["CF_PAGES", "CF_PAGES_URL", "WORKERS_CI", "WORKERS_CI_BRANCH", "CLOUDFLARE_ACCOUNT_ID"] },
|
|
373
|
+
{ platform: "aws", any: ["AWS_APP_ID", "AWS_BRANCH", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV"] },
|
|
190
374
|
{ platform: "render", any: ["RENDER", "RENDER_SERVICE_ID", "RENDER_EXTERNAL_URL"] },
|
|
191
375
|
{ platform: "railway", any: ["RAILWAY_ENVIRONMENT", "RAILWAY_ENVIRONMENT_NAME", "RAILWAY_PROJECT_ID"] },
|
|
192
376
|
{ platform: "fly", any: ["FLY_APP_NAME", "FLY_REGION", "FLY_ALLOC_ID"] },
|
|
@@ -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,13 +542,18 @@ 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
|
});
|
|
327
552
|
|
|
328
553
|
// src/environment.ts
|
|
554
|
+
function environmentFromBranch(variable, branch) {
|
|
555
|
+
return PRODUCTION_BRANCHES.has(branch.toLowerCase()) ? { environment: "production", evidence: `${variable}=${branch} (a production branch by name)` } : { environment: "sandbox", evidence: `${variable}=${branch} (not a production branch by name)` };
|
|
556
|
+
}
|
|
329
557
|
function detectHostedBuilder(cwd) {
|
|
330
558
|
let declared;
|
|
331
559
|
try {
|
|
@@ -358,13 +586,21 @@ function inferEnvironment(env = process.env, builder = null) {
|
|
|
358
586
|
}
|
|
359
587
|
return { environment: "local", evidence: [] };
|
|
360
588
|
}
|
|
361
|
-
var import_node_fs, import_node_path, set, DISCRIMINATORS, HOSTED_BUILDER_PACKAGES;
|
|
589
|
+
var import_node_fs, import_node_path, set, PRODUCTION_BRANCHES, DISCRIMINATORS, HOSTED_BUILDER_PACKAGES;
|
|
362
590
|
var init_environment = __esm({
|
|
363
591
|
"src/environment.ts"() {
|
|
364
592
|
"use strict";
|
|
365
593
|
import_node_fs = require("fs");
|
|
366
594
|
import_node_path = __toESM(require("path"), 1);
|
|
367
595
|
set = (value) => value !== void 0 && value !== "";
|
|
596
|
+
PRODUCTION_BRANCHES = /* @__PURE__ */ new Set([
|
|
597
|
+
"main",
|
|
598
|
+
"master",
|
|
599
|
+
"production",
|
|
600
|
+
"prod",
|
|
601
|
+
"release",
|
|
602
|
+
"live"
|
|
603
|
+
]);
|
|
368
604
|
DISCRIMINATORS = [
|
|
369
605
|
{
|
|
370
606
|
platform: "vercel",
|
|
@@ -393,6 +629,79 @@ var init_environment = __esm({
|
|
|
393
629
|
if (!set(env.RAILWAY_ENVIRONMENT_NAME)) return null;
|
|
394
630
|
return env.RAILWAY_ENVIRONMENT_NAME === "production" ? { environment: "production", evidence: "RAILWAY_ENVIRONMENT_NAME=production" } : { environment: "sandbox", evidence: `RAILWAY_ENVIRONMENT_NAME=${env.RAILWAY_ENVIRONMENT_NAME}` };
|
|
395
631
|
}
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
// Pages and Workers Builds both name the branch and neither names the tier.
|
|
635
|
+
platform: "cloudflare",
|
|
636
|
+
read: (env) => {
|
|
637
|
+
if (set(env.CF_PAGES) && set(env.CF_PAGES_BRANCH)) {
|
|
638
|
+
return environmentFromBranch("CF_PAGES_BRANCH", env.CF_PAGES_BRANCH);
|
|
639
|
+
}
|
|
640
|
+
if (set(env.WORKERS_CI) && set(env.WORKERS_CI_BRANCH)) {
|
|
641
|
+
return environmentFromBranch("WORKERS_CI_BRANCH", env.WORKERS_CI_BRANCH);
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
// Amplify names the branch, and separately whether the build is a pull-request preview.
|
|
648
|
+
platform: "aws",
|
|
649
|
+
read: (env) => {
|
|
650
|
+
if (!set(env.AWS_APP_ID) || !set(env.AWS_BRANCH)) return null;
|
|
651
|
+
return set(env.AWS_PULL_REQUEST_ID) ? { environment: "sandbox", evidence: "AWS_PULL_REQUEST_ID set (a pull-request preview)" } : environmentFromBranch("AWS_BRANCH", env.AWS_BRANCH);
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
// A Deployment build carries `REPLIT_DEPLOYMENT`; the workspace carries `REPL_ID` alone. Read here,
|
|
656
|
+
// before the hosted-builder rule, so a workspace build in a Replit project is not its publish step.
|
|
657
|
+
platform: "replit",
|
|
658
|
+
read: (env) => {
|
|
659
|
+
if (set(env.REPLIT_DEPLOYMENT)) {
|
|
660
|
+
return { environment: "production", evidence: "REPLIT_DEPLOYMENT set (a Replit Deployment build)" };
|
|
661
|
+
}
|
|
662
|
+
if (set(env.REPL_ID)) {
|
|
663
|
+
return { environment: "sandbox", evidence: "REPL_ID set without REPLIT_DEPLOYMENT (the workspace)" };
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
// GitHub Pages deploys build here. A pull request checks out a merge ref whose name says nothing, so
|
|
670
|
+
// the event is read first; a tag is a release; otherwise the branch name decides.
|
|
671
|
+
platform: "github-actions",
|
|
672
|
+
read: (env) => {
|
|
673
|
+
if (env.GITHUB_ACTIONS !== "true") return null;
|
|
674
|
+
if (set(env.GITHUB_EVENT_NAME) && env.GITHUB_EVENT_NAME.startsWith("pull_request")) {
|
|
675
|
+
return { environment: "sandbox", evidence: `GITHUB_EVENT_NAME=${env.GITHUB_EVENT_NAME} (a pull-request build)` };
|
|
676
|
+
}
|
|
677
|
+
if (env.GITHUB_REF_TYPE === "tag") {
|
|
678
|
+
return { environment: "production", evidence: "GITHUB_REF_TYPE=tag (a tag build)" };
|
|
679
|
+
}
|
|
680
|
+
if (!set(env.GITHUB_REF_NAME)) return null;
|
|
681
|
+
return environmentFromBranch("GITHUB_REF_NAME", env.GITHUB_REF_NAME);
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
// The tier, when a job declares one, is GitLab's own word for it. Without one: a merge request is a
|
|
686
|
+
// check, a tag is a release, the default branch goes live, and any other branch name decides.
|
|
687
|
+
platform: "gitlab-ci",
|
|
688
|
+
read: (env) => {
|
|
689
|
+
if (env.GITLAB_CI !== "true") return null;
|
|
690
|
+
if (set(env.CI_ENVIRONMENT_TIER)) {
|
|
691
|
+
return env.CI_ENVIRONMENT_TIER === "production" ? { environment: "production", evidence: "CI_ENVIRONMENT_TIER=production" } : { environment: "sandbox", evidence: `CI_ENVIRONMENT_TIER=${env.CI_ENVIRONMENT_TIER}` };
|
|
692
|
+
}
|
|
693
|
+
if (set(env.CI_MERGE_REQUEST_IID)) {
|
|
694
|
+
return { environment: "sandbox", evidence: "CI_MERGE_REQUEST_IID set (a merge-request build)" };
|
|
695
|
+
}
|
|
696
|
+
if (set(env.CI_COMMIT_TAG)) {
|
|
697
|
+
return { environment: "production", evidence: "CI_COMMIT_TAG set (a tag build)" };
|
|
698
|
+
}
|
|
699
|
+
if (!set(env.CI_COMMIT_BRANCH)) return null;
|
|
700
|
+
if (set(env.CI_DEFAULT_BRANCH) && env.CI_COMMIT_BRANCH === env.CI_DEFAULT_BRANCH) {
|
|
701
|
+
return { environment: "production", evidence: `CI_COMMIT_BRANCH=${env.CI_COMMIT_BRANCH} (the default branch)` };
|
|
702
|
+
}
|
|
703
|
+
return environmentFromBranch("CI_COMMIT_BRANCH", env.CI_COMMIT_BRANCH);
|
|
704
|
+
}
|
|
396
705
|
}
|
|
397
706
|
];
|
|
398
707
|
HOSTED_BUILDER_PACKAGES = [
|
|
@@ -610,8 +919,16 @@ async function resolveConfig(options) {
|
|
|
610
919
|
const fromSecretFile = await readSecretFile(options.cwd);
|
|
611
920
|
const fromEnv = readEnv();
|
|
612
921
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
613
|
-
const
|
|
614
|
-
const
|
|
922
|
+
const endpointRaw = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
923
|
+
const endpointTrusted = endpointRaw === DEFAULT_ENDPOINT || options.cliEndpoint !== void 0 || fromEnv.endpoint !== void 0 || fromFile.endpoint === void 0;
|
|
924
|
+
const endpoint = safeRemoteUrl(endpointRaw);
|
|
925
|
+
if (endpoint === null) {
|
|
926
|
+
throw new PatchstackError(
|
|
927
|
+
"Patchstack endpoints must be valid HTTPS URLs, except for localhost development endpoints.",
|
|
928
|
+
"CONFIG_INVALID"
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
615
932
|
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
616
933
|
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
617
934
|
throw new PatchstackError(
|
|
@@ -622,7 +939,7 @@ async function resolveConfig(options) {
|
|
|
622
939
|
const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
|
|
623
940
|
const environment = environmentRaw ?? inferred.environment;
|
|
624
941
|
const environmentEvidence = inferred?.evidence ?? [];
|
|
625
|
-
if (siteUuid !== null && siteUuid.length > 0 && !
|
|
942
|
+
if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
|
|
626
943
|
throw new PatchstackError(
|
|
627
944
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
628
945
|
"CONFIG_INVALID"
|
|
@@ -645,6 +962,7 @@ async function resolveConfig(options) {
|
|
|
645
962
|
siteUrl: identity.url,
|
|
646
963
|
siteName: identity.name,
|
|
647
964
|
endpoint,
|
|
965
|
+
endpointTrusted,
|
|
648
966
|
timeoutMs,
|
|
649
967
|
environment,
|
|
650
968
|
environmentEvidence,
|
|
@@ -663,8 +981,15 @@ async function readJsonFile(cwd, filename) {
|
|
|
663
981
|
const target = import_node_path3.default.join(cwd, filename);
|
|
664
982
|
let raw;
|
|
665
983
|
try {
|
|
984
|
+
if ((await (0, import_promises2.lstat)(target)).isSymbolicLink()) {
|
|
985
|
+
throw new PatchstackError(
|
|
986
|
+
`Could not read ${target}: configuration files must not be symbolic links.`,
|
|
987
|
+
"CONFIG_INVALID"
|
|
988
|
+
);
|
|
989
|
+
}
|
|
666
990
|
raw = await (0, import_promises2.readFile)(target, "utf8");
|
|
667
991
|
} catch (err) {
|
|
992
|
+
if (err instanceof PatchstackError) throw err;
|
|
668
993
|
if (err.code === "ENOENT") {
|
|
669
994
|
return {};
|
|
670
995
|
}
|
|
@@ -709,23 +1034,22 @@ function readEnv() {
|
|
|
709
1034
|
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
710
1035
|
};
|
|
711
1036
|
}
|
|
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
1037
|
function isEnvironment(value) {
|
|
716
1038
|
return value === "production" || value === "sandbox" || value === "local";
|
|
717
1039
|
}
|
|
718
|
-
var import_promises2, import_node_path3, CONFIG_FILENAME, SECRET_FILENAME;
|
|
1040
|
+
var import_promises2, import_node_path3, import_node_crypto, CONFIG_FILENAME, SECRET_FILENAME;
|
|
719
1041
|
var init_config = __esm({
|
|
720
1042
|
"src/config.ts"() {
|
|
721
1043
|
"use strict";
|
|
722
1044
|
import_promises2 = require("fs/promises");
|
|
723
1045
|
import_node_path3 = __toESM(require("path"), 1);
|
|
1046
|
+
import_node_crypto = require("crypto");
|
|
724
1047
|
init_types();
|
|
725
1048
|
init_client();
|
|
726
1049
|
init_environment();
|
|
727
1050
|
init_site_url();
|
|
728
1051
|
init_site_name();
|
|
1052
|
+
init_endpoint_policy();
|
|
729
1053
|
CONFIG_FILENAME = ".patchstackrc.json";
|
|
730
1054
|
SECRET_FILENAME = ".patchstackrc.local.json";
|
|
731
1055
|
}
|
|
@@ -2222,27 +2546,52 @@ function clientIpFields(resolved) {
|
|
|
2222
2546
|
return established && address !== null ? { client_ip: address, client_ip_source: resolved.source } : { client_ip_source: "unavailable" };
|
|
2223
2547
|
}
|
|
2224
2548
|
|
|
2225
|
-
// src/protect/
|
|
2226
|
-
|
|
2549
|
+
// src/protect/engine/client.js
|
|
2550
|
+
init_safe_origin();
|
|
2551
|
+
|
|
2552
|
+
// src/protect/engine/response-json.js
|
|
2553
|
+
var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
2554
|
+
async function readBoundedJson(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
|
|
2555
|
+
const stated2 = Number(response.headers?.get?.("content-length"));
|
|
2556
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) {
|
|
2557
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
2558
|
+
}
|
|
2559
|
+
const stream = response.body;
|
|
2560
|
+
if (!stream || typeof stream.getReader !== "function") {
|
|
2561
|
+
if (typeof response.text === "function") {
|
|
2562
|
+
const text = await response.text();
|
|
2563
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
2564
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
2565
|
+
}
|
|
2566
|
+
return JSON.parse(text);
|
|
2567
|
+
}
|
|
2568
|
+
throw new Error("rule response body cannot be read with a size bound");
|
|
2569
|
+
}
|
|
2570
|
+
const reader = stream.getReader();
|
|
2571
|
+
const chunks = [];
|
|
2572
|
+
let total = 0;
|
|
2227
2573
|
try {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2574
|
+
for (; ; ) {
|
|
2575
|
+
const { done, value } = await reader.read();
|
|
2576
|
+
if (done) break;
|
|
2577
|
+
total += value.byteLength;
|
|
2578
|
+
if (total > maxBytes) {
|
|
2579
|
+
void reader.cancel().catch(() => {
|
|
2580
|
+
});
|
|
2581
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
2582
|
+
}
|
|
2583
|
+
chunks.push(value);
|
|
2584
|
+
}
|
|
2585
|
+
} finally {
|
|
2586
|
+
reader.releaseLock();
|
|
2233
2587
|
}
|
|
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);
|
|
2588
|
+
const bytes = new Uint8Array(total);
|
|
2589
|
+
let offset = 0;
|
|
2590
|
+
for (const chunk of chunks) {
|
|
2591
|
+
bytes.set(chunk, offset);
|
|
2592
|
+
offset += chunk.byteLength;
|
|
2593
|
+
}
|
|
2594
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
2246
2595
|
}
|
|
2247
2596
|
|
|
2248
2597
|
// src/protect/engine/client.js
|
|
@@ -2301,7 +2650,7 @@ var PatchstackRuleClient = class {
|
|
|
2301
2650
|
whitelist_keys: {}
|
|
2302
2651
|
};
|
|
2303
2652
|
}
|
|
2304
|
-
const data = await response
|
|
2653
|
+
const data = await readBoundedJson(response);
|
|
2305
2654
|
if (!data || !Array.isArray(data.firewall)) {
|
|
2306
2655
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2307
2656
|
}
|
|
@@ -2338,6 +2687,22 @@ var PatchstackRuleClient = class {
|
|
|
2338
2687
|
}
|
|
2339
2688
|
};
|
|
2340
2689
|
|
|
2690
|
+
// src/protect/engine/own.js
|
|
2691
|
+
function setOwn(target, key, value) {
|
|
2692
|
+
Object.defineProperty(target, key, {
|
|
2693
|
+
configurable: true,
|
|
2694
|
+
enumerable: true,
|
|
2695
|
+
writable: true,
|
|
2696
|
+
value
|
|
2697
|
+
});
|
|
2698
|
+
return value;
|
|
2699
|
+
}
|
|
2700
|
+
function appendOwn(target, key, value) {
|
|
2701
|
+
if (!Object.hasOwn(target, key)) return setOwn(target, key, value);
|
|
2702
|
+
const previous = target[key];
|
|
2703
|
+
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2341
2706
|
// src/protect/engine/request.js
|
|
2342
2707
|
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
2343
2708
|
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
@@ -2456,8 +2821,8 @@ var RequestResolver = class {
|
|
|
2456
2821
|
}
|
|
2457
2822
|
if (key.startsWith("header.")) {
|
|
2458
2823
|
const name = key.slice("header.".length).toLowerCase();
|
|
2459
|
-
const
|
|
2460
|
-
return
|
|
2824
|
+
const headers = resp.headers ?? {};
|
|
2825
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
2461
2826
|
}
|
|
2462
2827
|
return [];
|
|
2463
2828
|
}
|
|
@@ -2552,7 +2917,7 @@ var RequestResolver = class {
|
|
|
2552
2917
|
...this.#resolveWildcard(cookies, key)
|
|
2553
2918
|
];
|
|
2554
2919
|
}
|
|
2555
|
-
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? cookies[key];
|
|
2920
|
+
const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? (Object.hasOwn(cookies, key) ? cookies[key] : void 0);
|
|
2556
2921
|
return value !== void 0 ? [value] : [];
|
|
2557
2922
|
}
|
|
2558
2923
|
#resolveCookie(key) {
|
|
@@ -2560,8 +2925,7 @@ var RequestResolver = class {
|
|
|
2560
2925
|
if (key.endsWith("*")) {
|
|
2561
2926
|
return this.#resolveWildcard(cookies, key);
|
|
2562
2927
|
}
|
|
2563
|
-
|
|
2564
|
-
return value !== void 0 ? [value] : [];
|
|
2928
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
2565
2929
|
}
|
|
2566
2930
|
#resolveServer(key) {
|
|
2567
2931
|
const req = this.#req;
|
|
@@ -2737,7 +3101,7 @@ var RequestResolver = class {
|
|
|
2737
3101
|
}
|
|
2738
3102
|
const name = pair.substring(0, eqIndex).trim();
|
|
2739
3103
|
const value = pair.substring(eqIndex + 1).trim();
|
|
2740
|
-
cookies
|
|
3104
|
+
setOwn(cookies, name, value);
|
|
2741
3105
|
}
|
|
2742
3106
|
this.#cookies = cookies;
|
|
2743
3107
|
return this.#cookies;
|
|
@@ -2894,30 +3258,113 @@ function normalizeWhitespace(value) {
|
|
|
2894
3258
|
result = result.replace(/ {2,}/g, " ");
|
|
2895
3259
|
return result;
|
|
2896
3260
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
|
|
3261
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
3262
|
+
var MAX_RAW_NODES = 25e3;
|
|
3263
|
+
var MAX_RAW_DEPTH = 200;
|
|
3264
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
3265
|
+
function serializeForRawDetection(body) {
|
|
3266
|
+
if (body === null || body === void 0) return "";
|
|
3267
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
3268
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3269
|
+
const output = [];
|
|
3270
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
3271
|
+
let chars = 0;
|
|
3272
|
+
let nodes = 0;
|
|
3273
|
+
const append = (text) => {
|
|
3274
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
3275
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
3276
|
+
output.push(piece);
|
|
3277
|
+
chars += piece.length;
|
|
3278
|
+
};
|
|
3279
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
3280
|
+
const task = stack.pop();
|
|
3281
|
+
if (task.kind === "text") {
|
|
3282
|
+
append(task.text);
|
|
3283
|
+
continue;
|
|
3284
|
+
}
|
|
3285
|
+
if (task.kind === "key") {
|
|
3286
|
+
append(boundedJsonString(task.value));
|
|
3287
|
+
append(":");
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
nodes++;
|
|
3291
|
+
if (nodes > MAX_RAW_NODES) {
|
|
3292
|
+
append('"[NodeLimit]"');
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
const value = task.value;
|
|
3296
|
+
if (value === null || value === void 0) {
|
|
3297
|
+
append(task.root ? "" : "null");
|
|
3298
|
+
continue;
|
|
3299
|
+
}
|
|
3300
|
+
if (typeof value === "string") {
|
|
3301
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
if (typeof value !== "object") {
|
|
3305
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
if (visited.has(value)) {
|
|
3309
|
+
append("[Circular]");
|
|
3310
|
+
continue;
|
|
3311
|
+
}
|
|
3312
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
3313
|
+
append('"[DepthLimit]"');
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
visited.add(value);
|
|
3317
|
+
if (Array.isArray(value)) {
|
|
3318
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
3319
|
+
const items = [];
|
|
3320
|
+
for (let i = 0; i < length2; i++) {
|
|
3321
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
3322
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
3323
|
+
}
|
|
3324
|
+
if (value.length > length2) {
|
|
3325
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
3326
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
3327
|
+
}
|
|
3328
|
+
stack.push({ kind: "text", text: "]" });
|
|
3329
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
3330
|
+
stack.push({ kind: "text", text: "[" });
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
let keys;
|
|
3334
|
+
try {
|
|
3335
|
+
keys = Object.getOwnPropertyNames(value);
|
|
3336
|
+
} catch {
|
|
3337
|
+
append('"[Unreadable]"');
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
3341
|
+
const entries = [];
|
|
3342
|
+
for (let i = 0; i < length; i++) {
|
|
3343
|
+
const key = keys[i];
|
|
3344
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
3345
|
+
entries.push({ kind: "key", value: key });
|
|
3346
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
3347
|
+
}
|
|
3348
|
+
if (keys.length > length) {
|
|
3349
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
3350
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
3351
|
+
}
|
|
3352
|
+
stack.push({ kind: "text", text: "}" });
|
|
3353
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
3354
|
+
stack.push({ kind: "text", text: "{" });
|
|
2909
3355
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3356
|
+
return output.join("");
|
|
3357
|
+
}
|
|
3358
|
+
function boundedJsonString(value) {
|
|
3359
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
3360
|
+
}
|
|
3361
|
+
function readOwnValue(value, key) {
|
|
3362
|
+
try {
|
|
3363
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3364
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
3365
|
+
} catch {
|
|
3366
|
+
return "[Unreadable]";
|
|
2914
3367
|
}
|
|
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
3368
|
}
|
|
2922
3369
|
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
2923
3370
|
function requestField(req, key) {
|
|
@@ -2959,7 +3406,7 @@ function normalizeObject(value, options = {}, depth = 0) {
|
|
|
2959
3406
|
if (typeof value === "object" && value !== null) {
|
|
2960
3407
|
const result = {};
|
|
2961
3408
|
for (const [key, val] of Object.entries(value)) {
|
|
2962
|
-
result
|
|
3409
|
+
setOwn(result, key, normalizeObject(val, options, depth + 1));
|
|
2963
3410
|
}
|
|
2964
3411
|
return result;
|
|
2965
3412
|
}
|
|
@@ -2976,6 +3423,111 @@ var REDOS_PATTERNS = [
|
|
|
2976
3423
|
new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
|
|
2977
3424
|
// nested alternation under a quantifier
|
|
2978
3425
|
];
|
|
3426
|
+
function hasAdjacentUnbounded(body, flags) {
|
|
3427
|
+
let previousUnboundedAtom = null;
|
|
3428
|
+
for (let i = 0; i < body.length; ) {
|
|
3429
|
+
const start = i;
|
|
3430
|
+
const ch = body[i];
|
|
3431
|
+
let assertion = false;
|
|
3432
|
+
let literal = null;
|
|
3433
|
+
let atom = ch;
|
|
3434
|
+
if (ch === "\\") {
|
|
3435
|
+
const escaped = body[i + 1] ?? "";
|
|
3436
|
+
assertion = /[bBAZz]/.test(escaped);
|
|
3437
|
+
if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
|
|
3438
|
+
i += Math.min(2, body.length - i);
|
|
3439
|
+
atom = body.slice(start, i);
|
|
3440
|
+
} else if (ch === "[") {
|
|
3441
|
+
i++;
|
|
3442
|
+
while (i < body.length) {
|
|
3443
|
+
if (body[i] === "\\") i += 2;
|
|
3444
|
+
else if (body[i++] === "]") break;
|
|
3445
|
+
}
|
|
3446
|
+
atom = body.slice(start, i);
|
|
3447
|
+
} else if (ch === ")") {
|
|
3448
|
+
i++;
|
|
3449
|
+
atom = ".";
|
|
3450
|
+
} else if (ch === "(") {
|
|
3451
|
+
const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
|
|
3452
|
+
if (lookaround) {
|
|
3453
|
+
i = groupEnd(body, i);
|
|
3454
|
+
continue;
|
|
3455
|
+
}
|
|
3456
|
+
i++;
|
|
3457
|
+
if (body.startsWith("?:", i)) i += 2;
|
|
3458
|
+
else if (body.startsWith("?<", i)) {
|
|
3459
|
+
const nameEnd = body.indexOf(">", i + 2);
|
|
3460
|
+
if (nameEnd !== -1) i = nameEnd + 1;
|
|
3461
|
+
}
|
|
3462
|
+
continue;
|
|
3463
|
+
} else if (ch === "|") {
|
|
3464
|
+
previousUnboundedAtom = null;
|
|
3465
|
+
i++;
|
|
3466
|
+
continue;
|
|
3467
|
+
} else if ("^$".includes(ch)) {
|
|
3468
|
+
i++;
|
|
3469
|
+
continue;
|
|
3470
|
+
} else if ("?*+{}".includes(ch)) {
|
|
3471
|
+
previousUnboundedAtom = null;
|
|
3472
|
+
i++;
|
|
3473
|
+
continue;
|
|
3474
|
+
} else {
|
|
3475
|
+
if (ch !== ".") literal = ch;
|
|
3476
|
+
i++;
|
|
3477
|
+
}
|
|
3478
|
+
if (assertion) continue;
|
|
3479
|
+
let unbounded = false;
|
|
3480
|
+
let mayBeEmpty = false;
|
|
3481
|
+
if (body[i] === "*" || body[i] === "+") {
|
|
3482
|
+
unbounded = true;
|
|
3483
|
+
mayBeEmpty = body[i] === "*";
|
|
3484
|
+
i++;
|
|
3485
|
+
} else if (body[i] === "{") {
|
|
3486
|
+
const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
|
|
3487
|
+
if (quantifier) {
|
|
3488
|
+
unbounded = quantifier[2] === "";
|
|
3489
|
+
mayBeEmpty = Number(quantifier[1]) === 0;
|
|
3490
|
+
i += quantifier[0].length;
|
|
3491
|
+
}
|
|
3492
|
+
} else if (body[i] === "?") {
|
|
3493
|
+
mayBeEmpty = true;
|
|
3494
|
+
i++;
|
|
3495
|
+
}
|
|
3496
|
+
if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
|
|
3497
|
+
if (unbounded && previousUnboundedAtom !== null) return true;
|
|
3498
|
+
if (unbounded) {
|
|
3499
|
+
previousUnboundedAtom = atom;
|
|
3500
|
+
} else if (previousUnboundedAtom !== null && !mayBeEmpty) {
|
|
3501
|
+
try {
|
|
3502
|
+
const atomFlags = flags.replace(/[^iu]/g, "");
|
|
3503
|
+
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";
|
|
3504
|
+
const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
|
|
3505
|
+
if (complementaryClass || excludedLiteral) {
|
|
3506
|
+
previousUnboundedAtom = null;
|
|
3507
|
+
}
|
|
3508
|
+
} catch {
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
if (i === start) i++;
|
|
3512
|
+
}
|
|
3513
|
+
return false;
|
|
3514
|
+
}
|
|
3515
|
+
function groupEnd(body, start) {
|
|
3516
|
+
let depth = 0;
|
|
3517
|
+
let inClass = false;
|
|
3518
|
+
for (let i = start; i < body.length; i++) {
|
|
3519
|
+
const ch = body[i];
|
|
3520
|
+
if (ch === "\\") {
|
|
3521
|
+
i++;
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
if (ch === "[") inClass = true;
|
|
3525
|
+
else if (ch === "]") inClass = false;
|
|
3526
|
+
else if (!inClass && ch === "(") depth++;
|
|
3527
|
+
else if (!inClass && ch === ")" && --depth === 0) return i + 1;
|
|
3528
|
+
}
|
|
3529
|
+
return body.length;
|
|
3530
|
+
}
|
|
2979
3531
|
var warnedRejectedPatterns = /* @__PURE__ */ new Set();
|
|
2980
3532
|
function warnRejectedPatternOnce(pattern) {
|
|
2981
3533
|
const key = String(pattern);
|
|
@@ -2993,15 +3545,14 @@ function safeRegExp(pattern) {
|
|
|
2993
3545
|
if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
|
|
2994
3546
|
return null;
|
|
2995
3547
|
}
|
|
2996
|
-
for (const dangerous of REDOS_PATTERNS) {
|
|
2997
|
-
if (dangerous.test(pattern)) {
|
|
2998
|
-
return null;
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
3548
|
const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
|
|
3002
3549
|
if (!match) {
|
|
3003
3550
|
return null;
|
|
3004
3551
|
}
|
|
3552
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
3553
|
+
if (dangerous.test(match[1])) return null;
|
|
3554
|
+
}
|
|
3555
|
+
if (hasAdjacentUnbounded(match[1], match[2])) return null;
|
|
3005
3556
|
try {
|
|
3006
3557
|
return new RegExp(match[1], match[2]);
|
|
3007
3558
|
} catch {
|
|
@@ -3745,6 +4296,16 @@ function operandShapeProblem(shape, value) {
|
|
|
3745
4296
|
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
3746
4297
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
3747
4298
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
4299
|
+
const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
|
|
4300
|
+
const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
|
|
4301
|
+
const tooLong = stringLimit(value);
|
|
4302
|
+
if (tooLong) return tooLong;
|
|
4303
|
+
const tooMany = listLimit(value);
|
|
4304
|
+
if (tooMany) return tooMany;
|
|
4305
|
+
if (Array.isArray(value)) {
|
|
4306
|
+
const longMember = value.find((member) => stringLimit(member) !== null);
|
|
4307
|
+
if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
4308
|
+
}
|
|
3748
4309
|
switch (shape) {
|
|
3749
4310
|
case "positive-number":
|
|
3750
4311
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
@@ -3775,8 +4336,11 @@ function operandShapeProblem(shape, value) {
|
|
|
3775
4336
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
3776
4337
|
const names = Object.keys(value);
|
|
3777
4338
|
if (names.length === 0) return "must be an object with at least one entry";
|
|
4339
|
+
if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
|
|
3778
4340
|
const bad = names.find((name) => !isHeaderName(name));
|
|
3779
|
-
|
|
4341
|
+
if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
4342
|
+
const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
|
|
4343
|
+
return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
|
|
3780
4344
|
}
|
|
3781
4345
|
case "header-name-list": {
|
|
3782
4346
|
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
@@ -3922,9 +4486,16 @@ var LIMITS = Object.freeze({
|
|
|
3922
4486
|
maxRules: 5e3,
|
|
3923
4487
|
maxWhitelists: 2e3,
|
|
3924
4488
|
maxConditionsPerRule: 250,
|
|
4489
|
+
maxConditionNodesPerRule: 1e3,
|
|
4490
|
+
maxConditionNodesPerBundle: 25e3,
|
|
3925
4491
|
maxNestingDepth: 12,
|
|
3926
4492
|
maxRegexLength: 1e3,
|
|
3927
|
-
maxValueLength: 8192
|
|
4493
|
+
maxValueLength: 8192,
|
|
4494
|
+
maxParameterLength: 512,
|
|
4495
|
+
maxParameterItems: 64,
|
|
4496
|
+
maxOperandItems: 256,
|
|
4497
|
+
maxMapEntries: 128,
|
|
4498
|
+
maxPathSegments: 64
|
|
3928
4499
|
});
|
|
3929
4500
|
var GROUP_PARAMETER = "rules";
|
|
3930
4501
|
function isGroup(condition) {
|
|
@@ -3951,6 +4522,9 @@ function parameterProblem(parameter) {
|
|
|
3951
4522
|
if (parameter === void 0 || parameter === null) return null;
|
|
3952
4523
|
if (Array.isArray(parameter)) {
|
|
3953
4524
|
if (parameter.length === 0) return "parameter list is empty";
|
|
4525
|
+
if (parameter.length > LIMITS.maxParameterItems) {
|
|
4526
|
+
return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
|
|
4527
|
+
}
|
|
3954
4528
|
for (const member of parameter) {
|
|
3955
4529
|
if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
|
|
3956
4530
|
const problem = parameterProblem(member);
|
|
@@ -3959,6 +4533,9 @@ function parameterProblem(parameter) {
|
|
|
3959
4533
|
return null;
|
|
3960
4534
|
}
|
|
3961
4535
|
if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
|
|
4536
|
+
if (parameter.length > LIMITS.maxParameterLength) {
|
|
4537
|
+
return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
|
|
4538
|
+
}
|
|
3962
4539
|
if (parameter === GROUP_PARAMETER) {
|
|
3963
4540
|
return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
|
|
3964
4541
|
}
|
|
@@ -3994,6 +4571,20 @@ function matchProblem(match, parameter) {
|
|
|
3994
4571
|
if (!match || typeof match !== "object") return "condition has no match object";
|
|
3995
4572
|
if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
|
|
3996
4573
|
if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
|
|
4574
|
+
for (const operand of ["value", "key", "claim"]) {
|
|
4575
|
+
const value = match[operand];
|
|
4576
|
+
if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
|
|
4577
|
+
return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
4578
|
+
}
|
|
4579
|
+
if (Array.isArray(value)) {
|
|
4580
|
+
if (value.length > LIMITS.maxOperandItems) {
|
|
4581
|
+
return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
|
|
4582
|
+
}
|
|
4583
|
+
if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
|
|
4584
|
+
return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
3997
4588
|
const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
|
|
3998
4589
|
if (!parameterless && (parameter === void 0 || parameter === null)) {
|
|
3999
4590
|
return `match type "${match.type}" needs a parameter`;
|
|
@@ -4016,11 +4607,20 @@ function matchProblem(match, parameter) {
|
|
|
4016
4607
|
} else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
|
|
4017
4608
|
return `match type "${match.type}" needs a value`;
|
|
4018
4609
|
}
|
|
4610
|
+
if (match.type === "array_key_value") {
|
|
4611
|
+
const paths = Array.isArray(match.key) ? match.key : [match.key];
|
|
4612
|
+
if (paths.some((path9) => typeof path9 === "string" && path9.split(".").length > LIMITS.maxPathSegments)) {
|
|
4613
|
+
return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4019
4616
|
return null;
|
|
4020
4617
|
}
|
|
4021
4618
|
function mutationsProblem(mutations) {
|
|
4022
4619
|
if (mutations === void 0 || mutations === null) return null;
|
|
4023
4620
|
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
4621
|
+
if (mutations.length > LIMITS.maxOperandItems) {
|
|
4622
|
+
return `mutations has more than ${LIMITS.maxOperandItems} entries`;
|
|
4623
|
+
}
|
|
4024
4624
|
for (const mutation of mutations) {
|
|
4025
4625
|
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
4026
4626
|
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
@@ -4040,10 +4640,19 @@ function whenProblem(when) {
|
|
|
4040
4640
|
if ("path" in when && !isNonEmptyString(when.path)) {
|
|
4041
4641
|
return "when.path must be a non-empty string";
|
|
4042
4642
|
}
|
|
4643
|
+
if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
|
|
4644
|
+
return `when.path is longer than ${LIMITS.maxValueLength} chars`;
|
|
4645
|
+
}
|
|
4043
4646
|
if ("method" in when) {
|
|
4044
4647
|
const methods = Array.isArray(when.method) ? when.method : [when.method];
|
|
4045
4648
|
if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
|
|
4649
|
+
if (methods.length > LIMITS.maxParameterItems) {
|
|
4650
|
+
return `when.method has more than ${LIMITS.maxParameterItems} entries`;
|
|
4651
|
+
}
|
|
4046
4652
|
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
4653
|
+
if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
|
|
4654
|
+
return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
|
|
4655
|
+
}
|
|
4047
4656
|
}
|
|
4048
4657
|
return null;
|
|
4049
4658
|
}
|
|
@@ -4067,6 +4676,11 @@ function rulePropertyProblem(rule) {
|
|
|
4067
4676
|
const problem = operandShapeProblem(shape, rule[property]);
|
|
4068
4677
|
if (problem) return `"${property}" ${problem}`;
|
|
4069
4678
|
}
|
|
4679
|
+
for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
|
|
4680
|
+
if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
|
|
4681
|
+
return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4070
4684
|
return null;
|
|
4071
4685
|
}
|
|
4072
4686
|
function actionProblem(action, rule) {
|
|
@@ -4096,46 +4710,76 @@ var PHASES2 = new Set(PHASES);
|
|
|
4096
4710
|
var ACTIONS2 = new Set(ACTIONS);
|
|
4097
4711
|
function validateBundle(bundle, opts = {}) {
|
|
4098
4712
|
const rejected = [];
|
|
4713
|
+
let omittedRejections = 0;
|
|
4714
|
+
const reject = (entry) => {
|
|
4715
|
+
if (rejected.length < 100) rejected.push(entry);
|
|
4716
|
+
else omittedRejections++;
|
|
4717
|
+
};
|
|
4099
4718
|
const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
|
|
4100
4719
|
const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
|
|
4720
|
+
const bundleBudget = { count: 0 };
|
|
4101
4721
|
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 });
|
|
4722
|
+
for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
|
|
4723
|
+
const reason = enforceableRuleProblem(rule, bundleBudget);
|
|
4724
|
+
if (reason) reject({ id: idOf(rule), reason });
|
|
4109
4725
|
else firewall.push(rule);
|
|
4110
4726
|
}
|
|
4727
|
+
if (inFirewall.length > LIMITS2.maxRules) {
|
|
4728
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
|
|
4729
|
+
}
|
|
4111
4730
|
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
|
-
}
|
|
4731
|
+
for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
|
|
4117
4732
|
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
4118
|
-
|
|
4733
|
+
reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
4119
4734
|
continue;
|
|
4120
4735
|
}
|
|
4121
4736
|
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
4122
|
-
|
|
4737
|
+
reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
4123
4738
|
continue;
|
|
4124
4739
|
}
|
|
4125
|
-
const reason = conditionsProblem(wl?.rule_v2);
|
|
4126
|
-
if (reason)
|
|
4740
|
+
const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
4741
|
+
if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
4127
4742
|
else whitelists.push(wl);
|
|
4128
4743
|
}
|
|
4744
|
+
if (inWhitelists.length > LIMITS2.maxWhitelists) {
|
|
4745
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
4746
|
+
}
|
|
4747
|
+
let whitelistKeys = {};
|
|
4748
|
+
if (bundle?.whitelist_keys !== void 0) {
|
|
4749
|
+
if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
|
|
4750
|
+
reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
|
|
4751
|
+
} else {
|
|
4752
|
+
const entries = Object.entries(bundle.whitelist_keys);
|
|
4753
|
+
if (entries.length > LIMITS2.maxMapEntries) {
|
|
4754
|
+
reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
|
|
4755
|
+
} else {
|
|
4756
|
+
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));
|
|
4757
|
+
if (invalid) {
|
|
4758
|
+
reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
|
|
4759
|
+
} else {
|
|
4760
|
+
whitelistKeys = bundle.whitelist_keys;
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
if (omittedRejections > 0) {
|
|
4766
|
+
rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
|
|
4767
|
+
}
|
|
4129
4768
|
return {
|
|
4130
|
-
bundle: { ...bundle, firewall, whitelists },
|
|
4769
|
+
bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
|
|
4131
4770
|
rejected
|
|
4132
4771
|
};
|
|
4133
4772
|
}
|
|
4134
4773
|
function idOf(rule) {
|
|
4135
4774
|
const id = rule?.id ?? rule?.rule_id;
|
|
4136
|
-
|
|
4775
|
+
if (id === void 0 || id === null) return "(unidentified)";
|
|
4776
|
+
try {
|
|
4777
|
+
return String(id).slice(0, 128);
|
|
4778
|
+
} catch {
|
|
4779
|
+
return "(unidentified)";
|
|
4780
|
+
}
|
|
4137
4781
|
}
|
|
4138
|
-
function enforceableRuleProblem(rule) {
|
|
4782
|
+
function enforceableRuleProblem(rule, bundleBudget = null) {
|
|
4139
4783
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
4140
4784
|
const nullReason = nullPropertyProblem(rule);
|
|
4141
4785
|
if (nullReason) return nullReason;
|
|
@@ -4146,9 +4790,9 @@ function enforceableRuleProblem(rule) {
|
|
|
4146
4790
|
if (scopeReason) return scopeReason;
|
|
4147
4791
|
const propertyReason = rulePropertyProblem(rule);
|
|
4148
4792
|
if (propertyReason) return propertyReason;
|
|
4149
|
-
return conditionsProblem(rule.rule_v2);
|
|
4793
|
+
return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
4150
4794
|
}
|
|
4151
|
-
function conditionsProblem(conditions, depth = 0) {
|
|
4795
|
+
function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
|
|
4152
4796
|
if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
|
|
4153
4797
|
if (conditions.length === 0) return "rule_v2 is empty (would never match)";
|
|
4154
4798
|
if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
|
|
@@ -4156,10 +4800,20 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
4156
4800
|
return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
|
|
4157
4801
|
}
|
|
4158
4802
|
for (const c of conditions) {
|
|
4803
|
+
ruleBudget.count++;
|
|
4804
|
+
if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
|
|
4805
|
+
return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
|
|
4806
|
+
}
|
|
4807
|
+
if (bundleBudget) {
|
|
4808
|
+
bundleBudget.count++;
|
|
4809
|
+
if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
|
|
4810
|
+
return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4159
4813
|
const shapeReason = conditionShapeProblem(c);
|
|
4160
4814
|
if (shapeReason) return shapeReason;
|
|
4161
4815
|
if (isGroup(c)) {
|
|
4162
|
-
const nested = conditionsProblem(c.rules, depth + 1);
|
|
4816
|
+
const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
|
|
4163
4817
|
if (nested) return nested;
|
|
4164
4818
|
continue;
|
|
4165
4819
|
}
|
|
@@ -4173,11 +4827,17 @@ function conditionsProblem(conditions, depth = 0) {
|
|
|
4173
4827
|
if (m.type === "regex") {
|
|
4174
4828
|
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
4175
4829
|
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
4830
|
+
if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
|
|
4176
4831
|
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
4177
4832
|
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
4178
4833
|
}
|
|
4179
4834
|
if (m.match) {
|
|
4180
|
-
const nested = conditionsProblem(
|
|
4835
|
+
const nested = conditionsProblem(
|
|
4836
|
+
[{ parameter: c.parameter, match: m.match }],
|
|
4837
|
+
depth + 1,
|
|
4838
|
+
ruleBudget,
|
|
4839
|
+
bundleBudget
|
|
4840
|
+
);
|
|
4181
4841
|
if (nested) return nested;
|
|
4182
4842
|
}
|
|
4183
4843
|
}
|
|
@@ -4391,12 +5051,14 @@ function asText(value) {
|
|
|
4391
5051
|
}
|
|
4392
5052
|
|
|
4393
5053
|
// src/protect/engine/pulse-client.js
|
|
5054
|
+
init_safe_origin();
|
|
4394
5055
|
init_pulse_token();
|
|
4395
5056
|
init_build_id();
|
|
4396
5057
|
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
4397
5058
|
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
4398
5059
|
var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
|
|
4399
5060
|
var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
|
|
5061
|
+
var PER_RULE_DRY_RUN_HEADER = "X-Patchstack-Per-Rule-Dry-Run";
|
|
4400
5062
|
var JITTER_FRACTION2 = 0.1;
|
|
4401
5063
|
var PulseRuleClient = class {
|
|
4402
5064
|
#siteUuid;
|
|
@@ -4441,6 +5103,9 @@ var PulseRuleClient = class {
|
|
|
4441
5103
|
if (this.#buildId !== null && typeof auth.Authorization === "string") {
|
|
4442
5104
|
headers["X-Patchstack-Build"] = this.#buildId;
|
|
4443
5105
|
}
|
|
5106
|
+
if (typeof auth.Authorization === "string") {
|
|
5107
|
+
headers[PER_RULE_DRY_RUN_HEADER] = "1";
|
|
5108
|
+
}
|
|
4444
5109
|
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
4445
5110
|
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
4446
5111
|
if (response.status === 304) {
|
|
@@ -4459,7 +5124,7 @@ var PulseRuleClient = class {
|
|
|
4459
5124
|
if (!response.ok) {
|
|
4460
5125
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
4461
5126
|
}
|
|
4462
|
-
const data = await response
|
|
5127
|
+
const data = await readBoundedJson(response);
|
|
4463
5128
|
if (!data || !Array.isArray(data.firewall)) {
|
|
4464
5129
|
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
4465
5130
|
}
|
|
@@ -4520,19 +5185,18 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
4520
5185
|
const method = (request.method || "GET").toUpperCase();
|
|
4521
5186
|
const headers = {};
|
|
4522
5187
|
request.headers.forEach((value, key) => {
|
|
4523
|
-
headers
|
|
5188
|
+
setOwn(headers, key.toLowerCase(), value);
|
|
4524
5189
|
});
|
|
4525
5190
|
const query = {};
|
|
4526
5191
|
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
|
-
}
|
|
5192
|
+
appendOwn(query, key, value);
|
|
4532
5193
|
}
|
|
4533
5194
|
let rawBody = "";
|
|
5195
|
+
let bodyInspectionSkip = null;
|
|
4534
5196
|
if (method !== "GET" && method !== "HEAD") {
|
|
4535
|
-
|
|
5197
|
+
const read = await readCappedText(request, maxBodyBytes);
|
|
5198
|
+
rawBody = read.text;
|
|
5199
|
+
bodyInspectionSkip = read.skip;
|
|
4536
5200
|
}
|
|
4537
5201
|
const contentType = headers["content-type"] || "";
|
|
4538
5202
|
let body = {};
|
|
@@ -4559,7 +5223,8 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
4559
5223
|
cookies: parseCookies(headers.cookie),
|
|
4560
5224
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
4561
5225
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
4562
|
-
_rawBody: rawBody
|
|
5226
|
+
_rawBody: rawBody,
|
|
5227
|
+
...bodyInspectionSkip === null ? {} : { _bodyInspectionSkip: bodyInspectionSkip }
|
|
4563
5228
|
};
|
|
4564
5229
|
}
|
|
4565
5230
|
async function readCappedText(request, max) {
|
|
@@ -4567,7 +5232,7 @@ async function readCappedText(request, max) {
|
|
|
4567
5232
|
try {
|
|
4568
5233
|
clone = request.clone();
|
|
4569
5234
|
} catch {
|
|
4570
|
-
return "";
|
|
5235
|
+
return { text: "", skip: "clone-failed" };
|
|
4571
5236
|
}
|
|
4572
5237
|
const body = clone.body;
|
|
4573
5238
|
if (body && typeof body.getReader === "function") {
|
|
@@ -4578,23 +5243,30 @@ async function readCappedText(request, max) {
|
|
|
4578
5243
|
for (; ; ) {
|
|
4579
5244
|
const { done, value } = await reader.read();
|
|
4580
5245
|
if (done) break;
|
|
4581
|
-
if (!value
|
|
5246
|
+
if (!value) continue;
|
|
4582
5247
|
const take = Math.min(value.byteLength, max - buffered);
|
|
4583
|
-
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
5248
|
+
if (take > 0) chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
4584
5249
|
buffered += take;
|
|
5250
|
+
if (buffered >= max) {
|
|
5251
|
+
void reader.cancel().catch(() => {
|
|
5252
|
+
});
|
|
5253
|
+
return { text: decodeChunks(chunks, buffered), skip: "body-cap" };
|
|
5254
|
+
}
|
|
4585
5255
|
}
|
|
4586
5256
|
} catch {
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
return "";
|
|
5257
|
+
void reader.cancel().catch(() => {
|
|
5258
|
+
});
|
|
5259
|
+
return { text: "", skip: "read-failed" };
|
|
5260
|
+
} finally {
|
|
5261
|
+
reader.releaseLock();
|
|
4593
5262
|
}
|
|
5263
|
+
return { text: decodeChunks(chunks, buffered), skip: null };
|
|
4594
5264
|
}
|
|
5265
|
+
return { text: "", skip: "unreadable-body" };
|
|
5266
|
+
}
|
|
5267
|
+
function decodeChunks(chunks, total) {
|
|
4595
5268
|
try {
|
|
4596
|
-
|
|
4597
|
-
return text.length > max ? text.slice(0, max) : text;
|
|
5269
|
+
return new TextDecoder().decode(concatChunks(chunks, total));
|
|
4598
5270
|
} catch {
|
|
4599
5271
|
return "";
|
|
4600
5272
|
}
|
|
@@ -4621,7 +5293,7 @@ function parseBody(rawBody, contentType) {
|
|
|
4621
5293
|
}
|
|
4622
5294
|
if (isForm) {
|
|
4623
5295
|
const body = {};
|
|
4624
|
-
for (const [k, v] of new URLSearchParams(rawBody)) body
|
|
5296
|
+
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
4625
5297
|
return { body, files: void 0 };
|
|
4626
5298
|
}
|
|
4627
5299
|
if (isJson || isAmbiguous) {
|
|
@@ -4649,9 +5321,9 @@ function parseMultipart(rawBody, boundary) {
|
|
|
4649
5321
|
if (filename !== void 0) {
|
|
4650
5322
|
const partType = /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "";
|
|
4651
5323
|
const file = { filename, type: partType, content };
|
|
4652
|
-
files
|
|
5324
|
+
appendOwn(files, name, file);
|
|
4653
5325
|
} else {
|
|
4654
|
-
body
|
|
5326
|
+
appendOwn(body, name, content);
|
|
4655
5327
|
}
|
|
4656
5328
|
}
|
|
4657
5329
|
return { body, files };
|
|
@@ -4666,7 +5338,7 @@ function parseCookies(header) {
|
|
|
4666
5338
|
if (idx === -1) {
|
|
4667
5339
|
continue;
|
|
4668
5340
|
}
|
|
4669
|
-
cookies
|
|
5341
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
4670
5342
|
}
|
|
4671
5343
|
return cookies;
|
|
4672
5344
|
}
|
|
@@ -4676,7 +5348,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
4676
5348
|
const method = (req.method || "GET").toUpperCase();
|
|
4677
5349
|
const headers = {};
|
|
4678
5350
|
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
4679
|
-
headers
|
|
5351
|
+
setOwn(headers, key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value);
|
|
4680
5352
|
}
|
|
4681
5353
|
const host = headers.host || "localhost";
|
|
4682
5354
|
let url;
|
|
@@ -4691,11 +5363,7 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
4691
5363
|
}
|
|
4692
5364
|
const query = {};
|
|
4693
5365
|
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
|
-
}
|
|
5366
|
+
appendOwn(query, key, value);
|
|
4699
5367
|
}
|
|
4700
5368
|
const contentType = headers["content-type"] || "";
|
|
4701
5369
|
let body = {};
|
|
@@ -4736,7 +5404,7 @@ function parseCookies2(header) {
|
|
|
4736
5404
|
if (idx === -1) {
|
|
4737
5405
|
continue;
|
|
4738
5406
|
}
|
|
4739
|
-
cookies
|
|
5407
|
+
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
4740
5408
|
}
|
|
4741
5409
|
return cookies;
|
|
4742
5410
|
}
|
|
@@ -4792,6 +5460,67 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
4792
5460
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
4793
5461
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
4794
5462
|
const MAX_REDIRECTS = 20;
|
|
5463
|
+
const MAX_REPLAY_BODY_BYTES = 1024 * 1024;
|
|
5464
|
+
const captureReplayBody = (request) => {
|
|
5465
|
+
if (request.method === "GET" || request.method === "HEAD" || !request.body) {
|
|
5466
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: false }), cancel() {
|
|
5467
|
+
} };
|
|
5468
|
+
}
|
|
5469
|
+
let reader;
|
|
5470
|
+
try {
|
|
5471
|
+
reader = request.clone().body?.getReader();
|
|
5472
|
+
} catch {
|
|
5473
|
+
reader = null;
|
|
5474
|
+
}
|
|
5475
|
+
if (!reader) {
|
|
5476
|
+
return { result: Promise.resolve({ body: void 0, tooLarge: true }), cancel() {
|
|
5477
|
+
} };
|
|
5478
|
+
}
|
|
5479
|
+
let cancelled = false;
|
|
5480
|
+
const result = (async () => {
|
|
5481
|
+
const chunks = [];
|
|
5482
|
+
let total = 0;
|
|
5483
|
+
try {
|
|
5484
|
+
for (; ; ) {
|
|
5485
|
+
const { done, value } = await reader.read();
|
|
5486
|
+
if (done || cancelled) break;
|
|
5487
|
+
total += value.byteLength;
|
|
5488
|
+
if (total > MAX_REPLAY_BODY_BYTES) {
|
|
5489
|
+
void reader.cancel().catch(() => {
|
|
5490
|
+
});
|
|
5491
|
+
return { body: void 0, tooLarge: true };
|
|
5492
|
+
}
|
|
5493
|
+
chunks.push(value);
|
|
5494
|
+
}
|
|
5495
|
+
if (cancelled) return { body: void 0, tooLarge: false };
|
|
5496
|
+
const body = new Uint8Array(total);
|
|
5497
|
+
let offset = 0;
|
|
5498
|
+
for (const chunk of chunks) {
|
|
5499
|
+
body.set(chunk, offset);
|
|
5500
|
+
offset += chunk.byteLength;
|
|
5501
|
+
}
|
|
5502
|
+
return { body, tooLarge: false };
|
|
5503
|
+
} catch {
|
|
5504
|
+
return { body: void 0, tooLarge: true };
|
|
5505
|
+
} finally {
|
|
5506
|
+
reader.releaseLock();
|
|
5507
|
+
}
|
|
5508
|
+
})();
|
|
5509
|
+
return {
|
|
5510
|
+
result,
|
|
5511
|
+
cancel() {
|
|
5512
|
+
cancelled = true;
|
|
5513
|
+
void reader.cancel().catch(() => {
|
|
5514
|
+
});
|
|
5515
|
+
}
|
|
5516
|
+
};
|
|
5517
|
+
};
|
|
5518
|
+
const discardResponseBody = async (response) => {
|
|
5519
|
+
try {
|
|
5520
|
+
await response?.body?.cancel?.();
|
|
5521
|
+
} catch {
|
|
5522
|
+
}
|
|
5523
|
+
};
|
|
4795
5524
|
const screenUrl = async (u, method) => {
|
|
4796
5525
|
let host = null;
|
|
4797
5526
|
try {
|
|
@@ -4817,20 +5546,43 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
4817
5546
|
if (callerRedirect !== "follow") return originalFetch(input, init);
|
|
4818
5547
|
const headers = new Headers(cur.headers);
|
|
4819
5548
|
const signal = cur.signal;
|
|
4820
|
-
|
|
5549
|
+
const replay = captureReplayBody(cur);
|
|
5550
|
+
let body;
|
|
4821
5551
|
for (let hop = 0; ; hop++) {
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
5552
|
+
let resp;
|
|
5553
|
+
try {
|
|
5554
|
+
resp = await originalFetch(
|
|
5555
|
+
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
5556
|
+
);
|
|
5557
|
+
} catch (error) {
|
|
5558
|
+
replay.cancel();
|
|
5559
|
+
throw error;
|
|
5560
|
+
}
|
|
4825
5561
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
4826
|
-
if (!location)
|
|
4827
|
-
|
|
5562
|
+
if (!location) {
|
|
5563
|
+
replay.cancel();
|
|
5564
|
+
return resp;
|
|
5565
|
+
}
|
|
5566
|
+
await discardResponseBody(resp);
|
|
5567
|
+
if (hop >= MAX_REDIRECTS) {
|
|
5568
|
+
replay.cancel();
|
|
5569
|
+
throw new Error("Patchstack blocked an outbound request: too many redirects");
|
|
5570
|
+
}
|
|
4828
5571
|
const next = new URL(location, url).href;
|
|
4829
5572
|
if (resp.status === 303 || (resp.status === 301 || resp.status === 302) && method === "POST") {
|
|
4830
5573
|
method = "GET";
|
|
4831
5574
|
body = void 0;
|
|
5575
|
+
replay.cancel();
|
|
4832
5576
|
headers.delete("content-type");
|
|
4833
5577
|
headers.delete("content-length");
|
|
5578
|
+
} else if (body === void 0 && method !== "GET" && method !== "HEAD") {
|
|
5579
|
+
const captured = await replay.result;
|
|
5580
|
+
if (captured.tooLarge) {
|
|
5581
|
+
throw new Error(
|
|
5582
|
+
`Patchstack blocked a redirect that required replaying more than ${MAX_REPLAY_BODY_BYTES} bytes`
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
body = captured.body;
|
|
4834
5586
|
}
|
|
4835
5587
|
if (new URL(next).origin !== new URL(url).origin) {
|
|
4836
5588
|
headers.delete("authorization");
|
|
@@ -5141,7 +5893,7 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
5141
5893
|
//
|
|
5142
5894
|
// Accepts a real newline and a JSON-escaped one. Most traces reach a client inside a JSON error
|
|
5143
5895
|
// 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
|
|
5896
|
+
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
5897
|
},
|
|
5146
5898
|
{
|
|
5147
5899
|
id: "resp-sql-error",
|
|
@@ -5275,9 +6027,18 @@ async function loadFs() {
|
|
|
5275
6027
|
try {
|
|
5276
6028
|
const [fs, path9] = await Promise.all([import("fs"), import("path")]);
|
|
5277
6029
|
fsMod = {
|
|
6030
|
+
chmodSync: fs.chmodSync,
|
|
6031
|
+
closeSync: fs.closeSync,
|
|
6032
|
+
constants: fs.constants,
|
|
6033
|
+
fstatSync: fs.fstatSync,
|
|
6034
|
+
fsyncSync: fs.fsyncSync,
|
|
6035
|
+
lstatSync: fs.lstatSync,
|
|
6036
|
+
openSync: fs.openSync,
|
|
5278
6037
|
readFileSync: fs.readFileSync,
|
|
5279
|
-
writeFileSync: fs.writeFileSync,
|
|
5280
6038
|
mkdirSync: fs.mkdirSync,
|
|
6039
|
+
renameSync: fs.renameSync,
|
|
6040
|
+
unlinkSync: fs.unlinkSync,
|
|
6041
|
+
writeFileSync: fs.writeFileSync,
|
|
5281
6042
|
join: path9.join
|
|
5282
6043
|
};
|
|
5283
6044
|
} catch {
|
|
@@ -5330,21 +6091,83 @@ async function cacheWrite(dir, env) {
|
|
|
5330
6091
|
if (!dir) return;
|
|
5331
6092
|
const fs = await loadFs();
|
|
5332
6093
|
if (!fs) return;
|
|
6094
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
6095
|
+
const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
|
|
6096
|
+
let descriptor = null;
|
|
5333
6097
|
try {
|
|
5334
6098
|
fs.mkdirSync(dir, { recursive: true });
|
|
5335
|
-
fs.
|
|
6099
|
+
const directory = fs.lstatSync(dir);
|
|
6100
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return;
|
|
6101
|
+
let mode;
|
|
6102
|
+
try {
|
|
6103
|
+
const current = fs.lstatSync(target);
|
|
6104
|
+
if (current.isSymbolicLink() || !current.isFile()) return;
|
|
6105
|
+
mode = current.mode & 4095;
|
|
6106
|
+
} catch (error) {
|
|
6107
|
+
if (error?.code !== "ENOENT") return;
|
|
6108
|
+
}
|
|
6109
|
+
descriptor = fs.openSync(temporary, "wx", mode ?? 384);
|
|
6110
|
+
fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
|
|
6111
|
+
fs.fsyncSync(descriptor);
|
|
6112
|
+
fs.closeSync(descriptor);
|
|
6113
|
+
descriptor = null;
|
|
6114
|
+
if (mode !== void 0) fs.chmodSync(temporary, mode);
|
|
6115
|
+
try {
|
|
6116
|
+
fs.renameSync(temporary, target);
|
|
6117
|
+
} catch (error) {
|
|
6118
|
+
if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
|
|
6119
|
+
try {
|
|
6120
|
+
fs.unlinkSync(target);
|
|
6121
|
+
} catch (unlinkError) {
|
|
6122
|
+
if (unlinkError?.code !== "ENOENT") throw unlinkError;
|
|
6123
|
+
}
|
|
6124
|
+
fs.renameSync(temporary, target);
|
|
6125
|
+
}
|
|
5336
6126
|
} catch {
|
|
6127
|
+
} finally {
|
|
6128
|
+
if (descriptor !== null) {
|
|
6129
|
+
try {
|
|
6130
|
+
fs.closeSync(descriptor);
|
|
6131
|
+
} catch {
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
try {
|
|
6135
|
+
fs.unlinkSync(temporary);
|
|
6136
|
+
} catch {
|
|
6137
|
+
}
|
|
5337
6138
|
}
|
|
5338
6139
|
}
|
|
5339
6140
|
async function cacheRead(dir) {
|
|
5340
6141
|
if (!dir) return null;
|
|
5341
6142
|
const fs = await loadFs();
|
|
5342
6143
|
if (!fs) return null;
|
|
6144
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
6145
|
+
let descriptor = null;
|
|
5343
6146
|
try {
|
|
5344
|
-
|
|
6147
|
+
const entry = fs.lstatSync(target);
|
|
6148
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
|
|
6149
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
6150
|
+
descriptor = fs.openSync(target, flags);
|
|
6151
|
+
const opened = fs.fstatSync(descriptor);
|
|
6152
|
+
if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
|
|
6153
|
+
return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
|
|
5345
6154
|
} catch {
|
|
5346
6155
|
return null;
|
|
6156
|
+
} finally {
|
|
6157
|
+
if (descriptor !== null) {
|
|
6158
|
+
try {
|
|
6159
|
+
fs.closeSync(descriptor);
|
|
6160
|
+
} catch {
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
function randomSuffix() {
|
|
6166
|
+
try {
|
|
6167
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
6168
|
+
} catch {
|
|
5347
6169
|
}
|
|
6170
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
5348
6171
|
}
|
|
5349
6172
|
function toEnvelope(value) {
|
|
5350
6173
|
if (!value || typeof value !== "object") return null;
|
|
@@ -5371,7 +6194,11 @@ init_build_id();
|
|
|
5371
6194
|
function liveUpdateRejections(res, options) {
|
|
5372
6195
|
if (options.acceptPartialBundle) return [];
|
|
5373
6196
|
const { rejected } = validateBundle(
|
|
5374
|
-
{
|
|
6197
|
+
{
|
|
6198
|
+
firewall: Array.isArray(res.firewall) ? res.firewall : [],
|
|
6199
|
+
whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
|
|
6200
|
+
whitelist_keys: res.whitelist_keys
|
|
6201
|
+
},
|
|
5375
6202
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
5376
6203
|
);
|
|
5377
6204
|
return rejected;
|
|
@@ -5542,7 +6369,8 @@ function normalizeBundle(b, options = {}) {
|
|
|
5542
6369
|
const { bundle: checked, rejected } = validateBundle(
|
|
5543
6370
|
{
|
|
5544
6371
|
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
5545
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : []
|
|
6372
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
6373
|
+
whitelist_keys: b.whitelist_keys
|
|
5546
6374
|
},
|
|
5547
6375
|
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
5548
6376
|
);
|
|
@@ -5562,7 +6390,7 @@ function normalizeBundle(b, options = {}) {
|
|
|
5562
6390
|
return {
|
|
5563
6391
|
firewall: checked.firewall,
|
|
5564
6392
|
whitelists: checked.whitelists,
|
|
5565
|
-
whitelist_keys:
|
|
6393
|
+
whitelist_keys: checked.whitelist_keys,
|
|
5566
6394
|
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
5567
6395
|
};
|
|
5568
6396
|
}
|
|
@@ -5606,18 +6434,23 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
5606
6434
|
};
|
|
5607
6435
|
}
|
|
5608
6436
|
function makeRefreshHandler(tick, secret) {
|
|
6437
|
+
let inflight2 = null;
|
|
6438
|
+
const runOnce = () => {
|
|
6439
|
+
if (inflight2) return inflight2;
|
|
6440
|
+
let current;
|
|
6441
|
+
current = Promise.resolve().then(() => tick()).finally(() => {
|
|
6442
|
+
if (inflight2 === current) inflight2 = null;
|
|
6443
|
+
});
|
|
6444
|
+
inflight2 = current;
|
|
6445
|
+
return current;
|
|
6446
|
+
};
|
|
5609
6447
|
return async (request) => {
|
|
5610
6448
|
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
|
-
}
|
|
6449
|
+
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
5617
6450
|
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
5618
6451
|
let refreshed = true;
|
|
5619
6452
|
try {
|
|
5620
|
-
const status = await
|
|
6453
|
+
const status = await runOnce();
|
|
5621
6454
|
if (status && status.ok === false) refreshed = false;
|
|
5622
6455
|
} catch {
|
|
5623
6456
|
refreshed = false;
|
|
@@ -5628,6 +6461,7 @@ function makeRefreshHandler(tick, secret) {
|
|
|
5628
6461
|
|
|
5629
6462
|
// src/protect/detections.js
|
|
5630
6463
|
init_pulse_token();
|
|
6464
|
+
init_safe_origin();
|
|
5631
6465
|
var DEFAULT_BASE_URL3 = "https://api.patchstack.com/monitor/pulse";
|
|
5632
6466
|
var DEFAULT_FLUSH_MS = 5e3;
|
|
5633
6467
|
var MAX_BATCH = 50;
|
|
@@ -6365,10 +7199,14 @@ function hardensWithoutBody(rule) {
|
|
|
6365
7199
|
}
|
|
6366
7200
|
|
|
6367
7201
|
// src/protect/firewall-log.js
|
|
7202
|
+
init_safe_origin();
|
|
7203
|
+
init_bounded_response();
|
|
6368
7204
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
6369
7205
|
var STOP_BUDGET_MS2 = 5e3;
|
|
7206
|
+
var ATTEMPT_TIMEOUT_MS2 = 1e4;
|
|
6370
7207
|
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
6371
7208
|
var MAX_BATCH2 = 50;
|
|
7209
|
+
var MAX_QUEUE2 = 500;
|
|
6372
7210
|
var TOKEN_SKEW_MS2 = 6e4;
|
|
6373
7211
|
function parseApiKey(apiKey) {
|
|
6374
7212
|
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
@@ -6390,14 +7228,16 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
6390
7228
|
}
|
|
6391
7229
|
if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
|
|
6392
7230
|
try {
|
|
6393
|
-
|
|
7231
|
+
const candidate = new URL(pulseOrManifestUrl).origin;
|
|
7232
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
6394
7233
|
} catch {
|
|
6395
7234
|
}
|
|
6396
7235
|
}
|
|
6397
7236
|
const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
|
|
6398
7237
|
if (typeof endpoint === "string" && endpoint.length > 0) {
|
|
6399
7238
|
try {
|
|
6400
|
-
|
|
7239
|
+
const candidate = new URL(endpoint).origin;
|
|
7240
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
6401
7241
|
} catch {
|
|
6402
7242
|
}
|
|
6403
7243
|
}
|
|
@@ -6409,16 +7249,16 @@ function createFirewallLogReporter(opts) {
|
|
|
6409
7249
|
return { record() {
|
|
6410
7250
|
}, flush: () => Promise.resolve(), stop: () => Promise.resolve() };
|
|
6411
7251
|
}
|
|
6412
|
-
const apiBase = (opts.apiBase
|
|
7252
|
+
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
6413
7253
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
6414
7254
|
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
|
|
6415
7255
|
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
6416
7256
|
let queue = [];
|
|
6417
7257
|
let timer = null;
|
|
6418
7258
|
let stopped = false;
|
|
6419
|
-
|
|
7259
|
+
let inFlight = null;
|
|
6420
7260
|
let drainPromise = null;
|
|
6421
|
-
|
|
7261
|
+
let activeController = null;
|
|
6422
7262
|
let ended = false;
|
|
6423
7263
|
let cachedToken = null;
|
|
6424
7264
|
let tokenInflight = null;
|
|
@@ -6444,9 +7284,9 @@ function createFirewallLogReporter(opts) {
|
|
|
6444
7284
|
})
|
|
6445
7285
|
});
|
|
6446
7286
|
if (!res || !res.ok) return null;
|
|
6447
|
-
const body = await res
|
|
7287
|
+
const body = await readBoundedJson2(res);
|
|
6448
7288
|
const token = body?.access_token;
|
|
6449
|
-
if (typeof token !== "string" || token.length === 0) return null;
|
|
7289
|
+
if (typeof token !== "string" || token.length === 0 || token.length > 8192 || /[\u0000-\u0020\u007f-\u009f]/.test(token)) return null;
|
|
6450
7290
|
const expiresIn = Number(body?.expires_in);
|
|
6451
7291
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
6452
7292
|
cachedToken = { token, expiresAt: Date.now() + ttlMs };
|
|
@@ -6464,12 +7304,18 @@ function createFirewallLogReporter(opts) {
|
|
|
6464
7304
|
clearTimeout(timer);
|
|
6465
7305
|
timer = null;
|
|
6466
7306
|
}
|
|
6467
|
-
if (ended ||
|
|
7307
|
+
if (ended || typeof fetchImpl !== "function") return Promise.resolve();
|
|
7308
|
+
if (inFlight) return inFlight;
|
|
7309
|
+
if (queue.length === 0) return Promise.resolve();
|
|
6468
7310
|
const batch = queue.splice(0, MAX_BATCH2);
|
|
7311
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
7312
|
+
activeController = controller;
|
|
7313
|
+
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
7314
|
+
attemptTimer?.unref?.();
|
|
6469
7315
|
let entry;
|
|
6470
|
-
|
|
7316
|
+
entry = (async () => {
|
|
6471
7317
|
try {
|
|
6472
|
-
const token = await fetchAccessToken(
|
|
7318
|
+
const token = await fetchAccessToken(controller?.signal);
|
|
6473
7319
|
if (!token || ended) return;
|
|
6474
7320
|
const body = new URLSearchParams();
|
|
6475
7321
|
body.set("type", "firewall");
|
|
@@ -6484,19 +7330,24 @@ function createFirewallLogReporter(opts) {
|
|
|
6484
7330
|
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
6485
7331
|
},
|
|
6486
7332
|
body,
|
|
6487
|
-
// Both phases carry
|
|
6488
|
-
...
|
|
7333
|
+
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
7334
|
+
...controller ? { signal: controller.signal } : {}
|
|
6489
7335
|
});
|
|
6490
7336
|
if (p && typeof p.then === "function") await p.catch(() => {
|
|
6491
7337
|
});
|
|
6492
7338
|
} catch {
|
|
6493
7339
|
} finally {
|
|
6494
|
-
|
|
7340
|
+
clearTimeout(attemptTimer);
|
|
7341
|
+
if (activeController === controller) activeController = null;
|
|
7342
|
+
if (inFlight === entry) inFlight = null;
|
|
7343
|
+
if (!stopped && !ended && queue.length > 0 && !timer) {
|
|
7344
|
+
timer = setTimeout(flush, queue.length >= MAX_BATCH2 ? 0 : flushMs);
|
|
7345
|
+
timer?.unref?.();
|
|
7346
|
+
}
|
|
6495
7347
|
}
|
|
6496
7348
|
})();
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
return send;
|
|
7349
|
+
inFlight = entry;
|
|
7350
|
+
return entry;
|
|
6500
7351
|
};
|
|
6501
7352
|
return {
|
|
6502
7353
|
/**
|
|
@@ -6512,6 +7363,7 @@ function createFirewallLogReporter(opts) {
|
|
|
6512
7363
|
if (stopped) return;
|
|
6513
7364
|
const fid = event?.rule?.id;
|
|
6514
7365
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
7366
|
+
if (queue.length >= MAX_QUEUE2) return;
|
|
6515
7367
|
queue.push({
|
|
6516
7368
|
fid,
|
|
6517
7369
|
method: event.method ?? null,
|
|
@@ -6543,9 +7395,10 @@ function createFirewallLogReporter(opts) {
|
|
|
6543
7395
|
}
|
|
6544
7396
|
const terminate = () => {
|
|
6545
7397
|
ended = true;
|
|
6546
|
-
|
|
7398
|
+
activeController?.abort();
|
|
7399
|
+
activeController = null;
|
|
6547
7400
|
queue = [];
|
|
6548
|
-
|
|
7401
|
+
inFlight = null;
|
|
6549
7402
|
};
|
|
6550
7403
|
let budget = null;
|
|
6551
7404
|
const spent = new Promise((resolve) => {
|
|
@@ -6557,8 +7410,8 @@ function createFirewallLogReporter(opts) {
|
|
|
6557
7410
|
});
|
|
6558
7411
|
const work = (async () => {
|
|
6559
7412
|
while (!ended) {
|
|
6560
|
-
if (
|
|
6561
|
-
await
|
|
7413
|
+
if (inFlight) {
|
|
7414
|
+
await inFlight;
|
|
6562
7415
|
continue;
|
|
6563
7416
|
}
|
|
6564
7417
|
if (queue.length === 0) break;
|
|
@@ -6582,10 +7435,107 @@ function telemetryEnabled() {
|
|
|
6582
7435
|
|
|
6583
7436
|
// src/protect/supabase-guard.js
|
|
6584
7437
|
var GUARD_PATH = "/_patchstack/guard";
|
|
6585
|
-
var
|
|
6586
|
-
|
|
7438
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7439
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7440
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7441
|
+
"connection",
|
|
7442
|
+
"cookie",
|
|
7443
|
+
"cookie2",
|
|
7444
|
+
"forwarded",
|
|
7445
|
+
"host",
|
|
7446
|
+
"keep-alive",
|
|
7447
|
+
"proxy-authenticate",
|
|
7448
|
+
"proxy-authorization",
|
|
7449
|
+
"te",
|
|
7450
|
+
"trailer",
|
|
7451
|
+
"transfer-encoding",
|
|
7452
|
+
"upgrade",
|
|
7453
|
+
"x-forwarded-for",
|
|
7454
|
+
"x-forwarded-host",
|
|
7455
|
+
"x-forwarded-port",
|
|
7456
|
+
"x-forwarded-proto",
|
|
7457
|
+
"x-ps-target",
|
|
7458
|
+
"x-real-ip"
|
|
7459
|
+
]);
|
|
7460
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7461
|
+
"connection",
|
|
7462
|
+
"content-encoding",
|
|
7463
|
+
"content-length",
|
|
7464
|
+
"keep-alive",
|
|
7465
|
+
"proxy-authenticate",
|
|
7466
|
+
"proxy-authorization",
|
|
7467
|
+
"set-cookie",
|
|
7468
|
+
"set-cookie2",
|
|
7469
|
+
"te",
|
|
7470
|
+
"trailer",
|
|
7471
|
+
"transfer-encoding",
|
|
7472
|
+
"upgrade"
|
|
7473
|
+
]);
|
|
7474
|
+
var BodyTooLargeError = class extends Error {
|
|
7475
|
+
};
|
|
7476
|
+
async function boundedBody(request, maxBytes) {
|
|
7477
|
+
const stated2 = Number(request.headers.get("content-length"));
|
|
7478
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) throw new BodyTooLargeError();
|
|
7479
|
+
if (!request.body) return void 0;
|
|
7480
|
+
const reader = request.body.getReader();
|
|
7481
|
+
const chunks = [];
|
|
7482
|
+
let total = 0;
|
|
7483
|
+
try {
|
|
7484
|
+
for (; ; ) {
|
|
7485
|
+
const { done, value } = await reader.read();
|
|
7486
|
+
if (done) break;
|
|
7487
|
+
total += value.byteLength;
|
|
7488
|
+
if (total > maxBytes) {
|
|
7489
|
+
void reader.cancel().catch(() => {
|
|
7490
|
+
});
|
|
7491
|
+
throw new BodyTooLargeError();
|
|
7492
|
+
}
|
|
7493
|
+
chunks.push(value);
|
|
7494
|
+
}
|
|
7495
|
+
} finally {
|
|
7496
|
+
reader.releaseLock();
|
|
7497
|
+
}
|
|
7498
|
+
const body = new Uint8Array(total);
|
|
7499
|
+
let offset = 0;
|
|
7500
|
+
for (const chunk of chunks) {
|
|
7501
|
+
body.set(chunk, offset);
|
|
7502
|
+
offset += chunk.byteLength;
|
|
7503
|
+
}
|
|
7504
|
+
return body;
|
|
7505
|
+
}
|
|
7506
|
+
function requestHeaders(input) {
|
|
7507
|
+
const output = new Headers();
|
|
7508
|
+
const connectionTokens = new Set(
|
|
7509
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7510
|
+
);
|
|
7511
|
+
input.forEach((value, key) => {
|
|
7512
|
+
const lower = key.toLowerCase();
|
|
7513
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7514
|
+
});
|
|
7515
|
+
return output;
|
|
7516
|
+
}
|
|
7517
|
+
function responseHeaders(input) {
|
|
7518
|
+
const output = new Headers();
|
|
7519
|
+
const connectionTokens = new Set(
|
|
7520
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7521
|
+
);
|
|
7522
|
+
input.forEach((value, key) => {
|
|
7523
|
+
const lower = key.toLowerCase();
|
|
7524
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7525
|
+
});
|
|
7526
|
+
return output;
|
|
7527
|
+
}
|
|
7528
|
+
function createSupabaseGuard({
|
|
7529
|
+
protection,
|
|
7530
|
+
supabaseUrl,
|
|
7531
|
+
fetchImpl = fetch,
|
|
7532
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
7533
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
7534
|
+
}) {
|
|
6587
7535
|
const guard = protection.fetchGuard();
|
|
6588
7536
|
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
7537
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
7538
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
6589
7539
|
return async function handleGuardRequest(request) {
|
|
6590
7540
|
const target = request.headers.get("x-ps-target");
|
|
6591
7541
|
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
@@ -6599,29 +7549,45 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
6599
7549
|
return new Response("patchstack: target not allowed", { status: 403 });
|
|
6600
7550
|
}
|
|
6601
7551
|
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
6602
|
-
|
|
7552
|
+
let body;
|
|
7553
|
+
try {
|
|
7554
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
7555
|
+
} catch (error) {
|
|
7556
|
+
if (error instanceof BodyTooLargeError) {
|
|
7557
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
7558
|
+
}
|
|
7559
|
+
throw error;
|
|
7560
|
+
}
|
|
7561
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
6603
7562
|
const evalReq = new Request(targetUrl.toString(), {
|
|
6604
7563
|
method: request.method,
|
|
7564
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
7565
|
+
// sanitized at the cross-origin boundary below.
|
|
6605
7566
|
headers: request.headers,
|
|
6606
|
-
body
|
|
7567
|
+
body
|
|
6607
7568
|
});
|
|
6608
7569
|
const blocked = await guard(evalReq);
|
|
6609
7570
|
if (blocked) return blocked;
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
7571
|
+
let upstream;
|
|
7572
|
+
try {
|
|
7573
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
7574
|
+
method: request.method,
|
|
7575
|
+
headers: forwardHeaders,
|
|
7576
|
+
body,
|
|
7577
|
+
redirect: "manual",
|
|
7578
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
7579
|
+
});
|
|
7580
|
+
} catch (error) {
|
|
7581
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
7582
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
7583
|
+
}
|
|
7584
|
+
throw error;
|
|
7585
|
+
}
|
|
7586
|
+
const forwarded = new Response(upstream.body, {
|
|
7587
|
+
status: upstream.status,
|
|
7588
|
+
statusText: upstream.statusText,
|
|
7589
|
+
headers: responseHeaders(upstream.headers)
|
|
6622
7590
|
});
|
|
6623
|
-
const buf = await upstream.arrayBuffer();
|
|
6624
|
-
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
6625
7591
|
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
6626
7592
|
};
|
|
6627
7593
|
}
|
|
@@ -6924,9 +7890,13 @@ async function createProtection(options = {}) {
|
|
|
6924
7890
|
for (const name of Object.keys(headers)) {
|
|
6925
7891
|
const value = headers[name];
|
|
6926
7892
|
if (typeof value === "string") {
|
|
6927
|
-
headers
|
|
7893
|
+
setOwn(headers, name, applyRedactors(value, spanRedactors, mask));
|
|
6928
7894
|
} else if (Array.isArray(value)) {
|
|
6929
|
-
|
|
7895
|
+
setOwn(
|
|
7896
|
+
headers,
|
|
7897
|
+
name,
|
|
7898
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item)
|
|
7899
|
+
);
|
|
6930
7900
|
}
|
|
6931
7901
|
}
|
|
6932
7902
|
}
|
|
@@ -6976,7 +7946,13 @@ async function createProtection(options = {}) {
|
|
|
6976
7946
|
let result;
|
|
6977
7947
|
let shaped;
|
|
6978
7948
|
try {
|
|
6979
|
-
shaped = await fromFetchRequest(request, {
|
|
7949
|
+
shaped = await fromFetchRequest(request, {
|
|
7950
|
+
trustedProxy: options.trustedProxy,
|
|
7951
|
+
maxBodyBytes: options.maxBodyBytes
|
|
7952
|
+
});
|
|
7953
|
+
if (shaped?._bodyInspectionSkip) {
|
|
7954
|
+
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
7955
|
+
}
|
|
6980
7956
|
result = engine.evaluate(shaped);
|
|
6981
7957
|
} catch (err) {
|
|
6982
7958
|
notify(onError, err, "onError");
|
|
@@ -7066,7 +8042,7 @@ async function createProtection(options = {}) {
|
|
|
7066
8042
|
const out = {};
|
|
7067
8043
|
for (const [name, value] of writeHeadEntries(headers)) {
|
|
7068
8044
|
const key = name.toLowerCase();
|
|
7069
|
-
|
|
8045
|
+
appendOwn(out, key, value);
|
|
7070
8046
|
}
|
|
7071
8047
|
return out;
|
|
7072
8048
|
};
|
|
@@ -7602,7 +8578,6 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
7602
8578
|
const reader = body.getReader();
|
|
7603
8579
|
const chunks = [];
|
|
7604
8580
|
let size = 0;
|
|
7605
|
-
let over = false;
|
|
7606
8581
|
let sniffed = !sniff;
|
|
7607
8582
|
try {
|
|
7608
8583
|
for (; ; ) {
|
|
@@ -7611,20 +8586,27 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
7611
8586
|
if (!value) continue;
|
|
7612
8587
|
if (!sniffed) {
|
|
7613
8588
|
sniffed = true;
|
|
7614
|
-
if (looksBinary(value))
|
|
8589
|
+
if (looksBinary(value)) {
|
|
8590
|
+
void reader.cancel().catch(() => {
|
|
8591
|
+
});
|
|
8592
|
+
return { skip: "binary-body" };
|
|
8593
|
+
}
|
|
7615
8594
|
}
|
|
7616
8595
|
size += value.byteLength;
|
|
7617
|
-
if (
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
8596
|
+
if (size >= cap) {
|
|
8597
|
+
void reader.cancel().catch(() => {
|
|
8598
|
+
});
|
|
8599
|
+
return { skip: "body-cap" };
|
|
7621
8600
|
}
|
|
7622
8601
|
chunks.push(value);
|
|
7623
8602
|
}
|
|
7624
8603
|
} catch {
|
|
8604
|
+
void reader.cancel().catch(() => {
|
|
8605
|
+
});
|
|
7625
8606
|
return { skip: "read-failed" };
|
|
8607
|
+
} finally {
|
|
8608
|
+
reader.releaseLock();
|
|
7626
8609
|
}
|
|
7627
|
-
if (over) return { skip: "body-cap" };
|
|
7628
8610
|
try {
|
|
7629
8611
|
return { text: new TextDecoder().decode(concatBytes(chunks, size)) };
|
|
7630
8612
|
} catch {
|
|
@@ -7652,10 +8634,10 @@ function concatBytes(chunks, total) {
|
|
|
7652
8634
|
function headerObject(headers) {
|
|
7653
8635
|
const out = {};
|
|
7654
8636
|
headers?.forEach?.((v, k) => {
|
|
7655
|
-
out
|
|
8637
|
+
setOwn(out, k.toLowerCase(), v);
|
|
7656
8638
|
});
|
|
7657
8639
|
const setCookies = headers?.getSetCookie?.();
|
|
7658
|
-
if (setCookies && setCookies.length) out
|
|
8640
|
+
if (setCookies && setCookies.length) setOwn(out, "set-cookie", setCookies);
|
|
7659
8641
|
return out;
|
|
7660
8642
|
}
|
|
7661
8643
|
function hasSpanMutations(rule) {
|
|
@@ -7824,7 +8806,7 @@ function isHeaderMutation(action) {
|
|
|
7824
8806
|
}
|
|
7825
8807
|
function applyHeaderMutation(headers, rule) {
|
|
7826
8808
|
if (rule.action === "remove-header") {
|
|
7827
|
-
for (const name of rule.remove_headers ?? []) headers
|
|
8809
|
+
for (const name of rule.remove_headers ?? []) setOwn(headers, String(name).toLowerCase(), null);
|
|
7828
8810
|
return;
|
|
7829
8811
|
}
|
|
7830
8812
|
if (rule.action === "set-header") {
|
|
@@ -7833,7 +8815,7 @@ function applyHeaderMutation(headers, rule) {
|
|
|
7833
8815
|
const key = String(name).toLowerCase();
|
|
7834
8816
|
const present2 = headers[key] != null && headers[key] !== "";
|
|
7835
8817
|
if (ensure && present2) continue;
|
|
7836
|
-
headers
|
|
8818
|
+
setOwn(headers, key, String(value));
|
|
7837
8819
|
}
|
|
7838
8820
|
return;
|
|
7839
8821
|
}
|