@patchstack/connect 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/protect.cjs DELETED
@@ -1,1240 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/protect/runtime.js
21
- var runtime_exports = {};
22
- __export(runtime_exports, {
23
- GUARD_PATH: () => GUARD_PATH,
24
- createProtection: () => createProtection,
25
- createServerFnGuard: () => createServerFnGuard,
26
- createSupabaseGuard: () => createSupabaseGuard
27
- });
28
- module.exports = __toCommonJS(runtime_exports);
29
-
30
- // src/protect/engine/client.js
31
- var DEFAULT_BASE_URL = "https://api.patchstack.com";
32
- var DEFAULT_CACHE_TTL = 3e5;
33
- var PatchstackRuleClient = class {
34
- #token;
35
- #baseUrl;
36
- #cacheTtl;
37
- #cache = null;
38
- #cacheTime = null;
39
- constructor({ token, baseUrl, cacheTtl } = {}) {
40
- this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
41
- this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
42
- this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL;
43
- if (!this.#token) {
44
- throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
45
- }
46
- }
47
- async getRules() {
48
- const now = Date.now();
49
- if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#cacheTtl) {
50
- return this.#cache;
51
- }
52
- const url = `${this.#baseUrl}/api/get-rules/3`;
53
- try {
54
- const response = await fetch(url, {
55
- method: "POST",
56
- headers: {
57
- "Content-Type": "application/json",
58
- "Authorization": `Bearer ${this.#token}`,
59
- "LicenseID": "1"
60
- // Hard-coded, is never actually used but needed by the API
61
- },
62
- body: JSON.stringify({}),
63
- signal: AbortSignal.timeout(3e4)
64
- });
65
- if (!response.ok) {
66
- return {
67
- success: false,
68
- error: `API returned ${response.status}: ${response.statusText}`,
69
- firewall: [],
70
- whitelists: [],
71
- whitelist_keys: {}
72
- };
73
- }
74
- const data = await response.json();
75
- const result = {
76
- success: true,
77
- firewall: Array.isArray(data.firewall) ? data.firewall : [],
78
- whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
79
- whitelist_keys: data.whitelist_keys ?? {}
80
- };
81
- this.#cache = result;
82
- this.#cacheTime = now;
83
- return result;
84
- } catch (err) {
85
- return {
86
- success: false,
87
- error: err.name === "TimeoutError" ? "Request timed out" : err.message,
88
- firewall: [],
89
- whitelists: [],
90
- whitelist_keys: {}
91
- };
92
- }
93
- }
94
- clearCache() {
95
- this.#cache = null;
96
- this.#cacheTime = null;
97
- }
98
- };
99
-
100
- // src/protect/engine/request.js
101
- function base64DecodeUtf8(value) {
102
- const str = String(value);
103
- if (typeof Buffer !== "undefined") {
104
- return Buffer.from(str, "base64").toString("utf-8");
105
- }
106
- const binary = atob(str);
107
- const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
108
- return new TextDecoder().decode(bytes);
109
- }
110
- var RequestResolver = class {
111
- #req;
112
- #cookies;
113
- constructor(req) {
114
- this.#req = req;
115
- this.#cookies = null;
116
- }
117
- resolve(parameter) {
118
- if (!parameter || parameter === "false") {
119
- return [null];
120
- }
121
- if (parameter === "rules") {
122
- return [null];
123
- }
124
- if (parameter === "raw") {
125
- return this.#resolveRaw();
126
- }
127
- if (parameter === "all") {
128
- return this.#resolveAll();
129
- }
130
- const dotIndex = parameter.indexOf(".");
131
- if (dotIndex === -1) {
132
- return [];
133
- }
134
- const source = parameter.substring(0, dotIndex);
135
- const key = parameter.substring(dotIndex + 1);
136
- switch (source) {
137
- case "get":
138
- return this.#resolveGet(key);
139
- case "post":
140
- return this.#resolvePost(key);
141
- case "request":
142
- return this.#resolveRequest(key);
143
- case "cookie":
144
- return this.#resolveCookie(key);
145
- case "server":
146
- return this.#resolveServer(key);
147
- case "files":
148
- return this.#resolveFiles(key);
149
- default:
150
- return [];
151
- }
152
- }
153
- applyMutations(mutations, value) {
154
- if (!mutations || !Array.isArray(mutations)) {
155
- return value;
156
- }
157
- let result = value;
158
- for (const mutation of mutations) {
159
- result = this.#applyMutation(mutation, result);
160
- }
161
- return result;
162
- }
163
- #applyMutation(mutation, value) {
164
- if (value === null || value === void 0) {
165
- return value;
166
- }
167
- switch (mutation) {
168
- case "base64_decode":
169
- try {
170
- return base64DecodeUtf8(value);
171
- } catch {
172
- return value;
173
- }
174
- case "json_decode":
175
- try {
176
- return JSON.parse(String(value));
177
- } catch {
178
- return value;
179
- }
180
- case "json_encode":
181
- try {
182
- return JSON.stringify(value);
183
- } catch {
184
- return value;
185
- }
186
- case "urldecode":
187
- try {
188
- return decodeURIComponent(String(value));
189
- } catch {
190
- return value;
191
- }
192
- case "intval":
193
- return parseInt(String(value), 10) || 0;
194
- case "getArrayValues":
195
- if (typeof value === "object" && value !== null) {
196
- return Object.values(value);
197
- }
198
- return value;
199
- default:
200
- return value;
201
- }
202
- }
203
- #resolveGet(key) {
204
- const query = this.#req.query ?? {};
205
- if (key.endsWith("*")) {
206
- return this.#resolveWildcard(query, key);
207
- }
208
- const value = this.#getNestedValue(query, key);
209
- return value !== void 0 ? [value] : [];
210
- }
211
- #resolvePost(key) {
212
- const body = this.#req.body ?? {};
213
- if (key.endsWith("*")) {
214
- return this.#resolveWildcard(body, key);
215
- }
216
- const value = this.#getNestedValue(body, key);
217
- return value !== void 0 ? [value] : [];
218
- }
219
- #resolveRequest(key) {
220
- const query = this.#req.query ?? {};
221
- const body = this.#req.body ?? {};
222
- const cookies = this.#parseCookies();
223
- if (key.endsWith("*")) {
224
- return [
225
- ...this.#resolveWildcard(query, key),
226
- ...this.#resolveWildcard(body, key),
227
- ...this.#resolveWildcard(cookies, key)
228
- ];
229
- }
230
- const value = this.#getNestedValue(query, key) ?? this.#getNestedValue(body, key) ?? cookies[key];
231
- return value !== void 0 ? [value] : [];
232
- }
233
- #resolveCookie(key) {
234
- const cookies = this.#parseCookies();
235
- if (key.endsWith("*")) {
236
- return this.#resolveWildcard(cookies, key);
237
- }
238
- const value = cookies[key];
239
- return value !== void 0 ? [value] : [];
240
- }
241
- #resolveServer(key) {
242
- const req = this.#req;
243
- switch (key) {
244
- case "REQUEST_URI":
245
- return [req.originalUrl ?? req.url ?? "/"];
246
- case "REQUEST_METHOD":
247
- return [req.method ?? "GET"];
248
- case "HTTP_USER_AGENT":
249
- return req.headers?.["user-agent"] ? [req.headers["user-agent"]] : [];
250
- case "HTTP_REFERER":
251
- return req.headers?.referer ? [req.headers.referer] : [];
252
- case "HTTP_HOST":
253
- return req.headers?.host ? [req.headers.host] : [];
254
- case "REMOTE_ADDR":
255
- case "ip":
256
- return [req.ip ?? req.socket?.remoteAddress ?? ""];
257
- case "CONTENT_TYPE":
258
- return req.headers?.["content-type"] ? [req.headers["content-type"]] : [];
259
- case "CONTENT_LENGTH":
260
- return req.headers?.["content-length"] ? [req.headers["content-length"]] : [];
261
- default: {
262
- if (key.startsWith("HTTP_")) {
263
- const headerName = key.substring(5).toLowerCase().replace(/_/g, "-");
264
- return req.headers?.[headerName] ? [req.headers[headerName]] : [];
265
- }
266
- return [];
267
- }
268
- }
269
- }
270
- #resolveFiles(key) {
271
- const files = this.#req.files;
272
- if (!files) {
273
- return [];
274
- }
275
- if (key.endsWith("*")) {
276
- return this.#resolveWildcard(files, key);
277
- }
278
- const value = files[key];
279
- return value !== void 0 ? [value] : [];
280
- }
281
- #resolveRaw() {
282
- if (typeof this.#req._rawBody === "string") {
283
- return this.#req._rawBody ? [this.#req._rawBody] : [];
284
- }
285
- const body = this.#req.body;
286
- if (body === void 0 || body === null) {
287
- return [];
288
- }
289
- if (typeof body === "string") {
290
- return [body];
291
- }
292
- try {
293
- return [JSON.stringify(body)];
294
- } catch {
295
- return [String(body)];
296
- }
297
- }
298
- #resolveAll() {
299
- const parts = [];
300
- const uri = this.#req.originalUrl ?? this.#req.url ?? "/";
301
- parts.push(uri);
302
- const queryString = uri.includes("?") ? uri.split("?")[1] : "";
303
- if (queryString) {
304
- parts.push(queryString);
305
- }
306
- const body = this.#req.body;
307
- if (body) {
308
- parts.push(typeof body === "string" ? body : JSON.stringify(body));
309
- }
310
- const headers = this.#req.headers ?? {};
311
- const excludedHeaders = /* @__PURE__ */ new Set([
312
- "host",
313
- "connection",
314
- "cache-control",
315
- "accept",
316
- "accept-encoding",
317
- "accept-language",
318
- "priority",
319
- "sec-ch-ua",
320
- "sec-ch-ua-mobile",
321
- "sec-ch-ua-platform",
322
- "sec-fetch-dest",
323
- "sec-fetch-mode",
324
- "sec-fetch-site",
325
- "sec-fetch-user",
326
- "upgrade-insecure-requests"
327
- ]);
328
- for (const [name, value] of Object.entries(headers)) {
329
- if (!excludedHeaders.has(name)) {
330
- parts.push(`${name}: ${value}`);
331
- }
332
- }
333
- const cookies = this.#parseCookies();
334
- const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
335
- if (cookieStr) {
336
- parts.push(cookieStr);
337
- }
338
- return [parts.join(" ")];
339
- }
340
- #resolveWildcard(obj, pattern) {
341
- if (typeof obj !== "object" || obj === null) {
342
- return [];
343
- }
344
- const prefix = pattern.slice(0, -1);
345
- const values = [];
346
- for (const [key, value] of Object.entries(obj)) {
347
- if (key.startsWith(prefix)) {
348
- values.push(value);
349
- }
350
- }
351
- return values;
352
- }
353
- #getNestedValue(obj, key) {
354
- if (typeof obj !== "object" || obj === null) {
355
- return void 0;
356
- }
357
- if (key in obj) {
358
- return obj[key];
359
- }
360
- const parts = key.split(".");
361
- let current = obj;
362
- for (const part of parts) {
363
- if (current === null || current === void 0 || typeof current !== "object") {
364
- return void 0;
365
- }
366
- current = current[part];
367
- }
368
- return current;
369
- }
370
- #parseCookies() {
371
- if (this.#cookies !== null) {
372
- return this.#cookies;
373
- }
374
- if (this.#req.cookies) {
375
- this.#cookies = this.#req.cookies;
376
- return this.#cookies;
377
- }
378
- const header = this.#req.headers?.cookie;
379
- if (!header) {
380
- this.#cookies = {};
381
- return this.#cookies;
382
- }
383
- const cookies = {};
384
- for (const pair of header.split(";")) {
385
- const eqIndex = pair.indexOf("=");
386
- if (eqIndex === -1) {
387
- continue;
388
- }
389
- const name = pair.substring(0, eqIndex).trim();
390
- const value = pair.substring(eqIndex + 1).trim();
391
- cookies[name] = value;
392
- }
393
- this.#cookies = cookies;
394
- return this.#cookies;
395
- }
396
- };
397
-
398
- // src/protect/engine/normalizer.js
399
- var HTML_ENTITIES = {
400
- "&amp;": "&",
401
- "&lt;": "<",
402
- "&gt;": ">",
403
- "&quot;": '"',
404
- "&#39;": "'",
405
- "&apos;": "'",
406
- "&nbsp;": " ",
407
- "&#x27;": "'",
408
- "&#x22;": '"',
409
- "&#x3C;": "<",
410
- "&#x3E;": ">",
411
- "&#x26;": "&",
412
- "&#34;": '"',
413
- "&#60;": "<",
414
- "&#62;": ">",
415
- "&#38;": "&"
416
- };
417
- var MAX_DECODE_ITERATIONS = 5;
418
- function normalize(value, options = {}) {
419
- if (typeof value !== "string") {
420
- return value;
421
- }
422
- const opts = {
423
- urlDecode: true,
424
- htmlDecode: true,
425
- sqlComments: true,
426
- nullBytes: true,
427
- whitespace: true,
428
- ...options
429
- };
430
- let result = value;
431
- if (opts.nullBytes) {
432
- result = removeNullBytes(result);
433
- }
434
- if (opts.urlDecode) {
435
- result = urlDecode(result);
436
- }
437
- if (opts.htmlDecode) {
438
- result = htmlEntityDecode(result);
439
- }
440
- if (opts.sqlComments) {
441
- result = removeSqlComments(result);
442
- }
443
- if (opts.whitespace) {
444
- result = normalizeWhitespace(result);
445
- }
446
- return result;
447
- }
448
- function urlDecode(value) {
449
- if (typeof value !== "string") {
450
- return value;
451
- }
452
- let result = value;
453
- let previous = "";
454
- let iterations = 0;
455
- while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
456
- previous = result;
457
- iterations++;
458
- try {
459
- result = decodeURIComponent(result);
460
- } catch {
461
- result = safeUrlDecode(result);
462
- break;
463
- }
464
- }
465
- return result;
466
- }
467
- function safeUrlDecode(value) {
468
- return value.replace(/%([0-9A-Fa-f]{2})/g, (match, hex) => {
469
- try {
470
- return String.fromCharCode(parseInt(hex, 16));
471
- } catch {
472
- return match;
473
- }
474
- });
475
- }
476
- function htmlEntityDecode(value) {
477
- if (typeof value !== "string") {
478
- return value;
479
- }
480
- let result = value;
481
- for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
482
- result = result.split(entity).join(char);
483
- }
484
- result = result.replace(/&#(\d+);/g, (match, code) => {
485
- const num = parseInt(code, 10);
486
- return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
487
- });
488
- result = result.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => {
489
- const num = parseInt(hex, 16);
490
- return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
491
- });
492
- return result;
493
- }
494
- function removeSqlComments(value) {
495
- if (typeof value !== "string") {
496
- return value;
497
- }
498
- let result = value;
499
- result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
500
- result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
501
- result = result.replace(/--[^\r\n]*/g, "");
502
- result = result.replace(/#[^\r\n]*/g, "");
503
- return result;
504
- }
505
- function removeNullBytes(value) {
506
- if (typeof value !== "string") {
507
- return value;
508
- }
509
- let result = value.replace(/\x00/g, "");
510
- result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
511
- return result;
512
- }
513
- function normalizeWhitespace(value) {
514
- if (typeof value !== "string") {
515
- return value;
516
- }
517
- let result = value.replace(/[\t\r\n\f\v]+/g, " ");
518
- result = result.replace(/ {2,}/g, " ");
519
- return result;
520
- }
521
- function serializeForRawDetection(body, visited = /* @__PURE__ */ new Set(), isRoot = true) {
522
- if (body === null || body === void 0) {
523
- return isRoot ? "" : "null";
524
- }
525
- if (typeof body === "string") {
526
- return isRoot ? body : JSON.stringify(body);
527
- }
528
- if (typeof body !== "object") {
529
- return String(body);
530
- }
531
- if (visited.has(body)) {
532
- return "[Circular]";
533
- }
534
- visited.add(body);
535
- if (Array.isArray(body)) {
536
- const items = body.map((item) => serializeForRawDetection(item, visited, false));
537
- return "[" + items.join(",") + "]";
538
- }
539
- const keys = Object.getOwnPropertyNames(body);
540
- const parts = keys.map((key) => {
541
- const val = serializeForRawDetection(body[key], visited, false);
542
- return JSON.stringify(key) + ":" + val;
543
- });
544
- return "{" + parts.join(",") + "}";
545
- }
546
- function normalizeRequest(req, options = {}) {
547
- const rawBody = typeof req._rawBody === "string" ? req._rawBody : serializeForRawDetection(req.body ?? null);
548
- return {
549
- query: normalizeObject(req.query || {}, options),
550
- body: normalizeObject(req.body || {}, options),
551
- headers: normalizeObject(req.headers || {}, options),
552
- url: normalize(req.url || "", options),
553
- originalUrl: normalize(req.originalUrl || req.url || "", options),
554
- _rawBody: rawBody
555
- };
556
- }
557
- function normalizeObject(value, options = {}) {
558
- if (typeof value === "string") {
559
- return normalize(value, options);
560
- }
561
- if (Array.isArray(value)) {
562
- return value.map((item) => normalizeObject(item, options));
563
- }
564
- if (typeof value === "object" && value !== null) {
565
- const result = {};
566
- for (const [key, val] of Object.entries(value)) {
567
- result[key] = normalizeObject(val, options);
568
- }
569
- return result;
570
- }
571
- return value;
572
- }
573
-
574
- // src/protect/engine/engine.js
575
- var REDOS_PATTERNS = [
576
- /\(\w\+\)\+/,
577
- /\(\w\*\)\+/,
578
- /\(\w\+\)\*/,
579
- /\(\w\*\)\*/,
580
- /\(\w\|\w\)\+/
581
- ];
582
- function safeRegExp(pattern) {
583
- if (!pattern) {
584
- return null;
585
- }
586
- for (const dangerous of REDOS_PATTERNS) {
587
- if (dangerous.test(pattern)) {
588
- return null;
589
- }
590
- }
591
- const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
592
- if (!match) {
593
- return null;
594
- }
595
- try {
596
- return new RegExp(match[1], match[2]);
597
- } catch {
598
- return null;
599
- }
600
- }
601
- function ctypeResult(strValue, isClass, matchVal) {
602
- if (strValue === "") {
603
- return false;
604
- }
605
- const expected = typeof matchVal === "boolean" ? matchVal : true;
606
- return isClass === expected;
607
- }
608
- function arrayKeyValue(value, matchObj) {
609
- if (!matchObj || !matchObj.match || value === null || typeof value !== "object") {
610
- return false;
611
- }
612
- const keys = Array.isArray(matchObj.key) ? matchObj.key : [matchObj.key];
613
- const sub = matchObj.match;
614
- for (const key of keys) {
615
- let node = value;
616
- for (const part of String(key).split(".")) {
617
- if (node === null || typeof node !== "object") {
618
- node = void 0;
619
- break;
620
- }
621
- node = node[part];
622
- }
623
- if (node === void 0) {
624
- continue;
625
- }
626
- const candidates = Array.isArray(node) ? node : [node];
627
- for (const candidate of candidates) {
628
- if (matchValue(sub.type, candidate, sub.value, sub)) {
629
- return true;
630
- }
631
- }
632
- }
633
- return false;
634
- }
635
- var warnedMatchTypes = /* @__PURE__ */ new Set();
636
- function warnUnsupportedMatchType(type) {
637
- if (type == null || warnedMatchTypes.has(type)) {
638
- return;
639
- }
640
- warnedMatchTypes.add(type);
641
- console.warn(
642
- `[patchstack] Unsupported rule_v2 match type "${type}" \u2014 condition treated as no-match. Rules relying on it are not enforced by this engine.`
643
- );
644
- }
645
- function matchValue(type, value, matchVal, matchObj) {
646
- if (value === null || value === void 0) {
647
- if (type === "isset") {
648
- return false;
649
- }
650
- return false;
651
- }
652
- const strValue = typeof value === "string" ? value : String(value);
653
- switch (type) {
654
- case "equals":
655
- return strValue == matchVal;
656
- case "equals_strict":
657
- return strValue === matchVal;
658
- case "contains":
659
- // engine-php exposes `stripos` as an alias of `contains`.
660
- case "stripos": {
661
- const lower = strValue.toLowerCase();
662
- const target = String(matchVal).toLowerCase();
663
- return lower.includes(target);
664
- }
665
- case "not_contains": {
666
- const lower = strValue.toLowerCase();
667
- const target = String(matchVal).toLowerCase();
668
- return !lower.includes(target);
669
- }
670
- case "regex": {
671
- const regex = safeRegExp(matchVal);
672
- if (!regex) {
673
- return false;
674
- }
675
- return regex.test(strValue);
676
- }
677
- case "more_than":
678
- return Number(strValue) > Number(matchVal);
679
- case "less_than":
680
- return Number(strValue) < Number(matchVal);
681
- case "ctype_digit":
682
- return ctypeResult(strValue, /^\d+$/.test(strValue), matchVal);
683
- case "ctype_alnum":
684
- return ctypeResult(strValue, /^[\w$\u0080-\uFFFF]+$/.test(strValue), matchVal);
685
- case "is_numeric":
686
- return ctypeResult(strValue, !isNaN(strValue) && strValue.trim() !== "", matchVal);
687
- case "isset":
688
- return true;
689
- case "in_array": {
690
- const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
691
- return arr.includes(strValue);
692
- }
693
- case "not_in_array": {
694
- const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
695
- return !arr.includes(strValue);
696
- }
697
- case "array_in_array": {
698
- if (!Array.isArray(value) || !Array.isArray(matchVal)) {
699
- return false;
700
- }
701
- return value.some((v) => matchVal.includes(v));
702
- }
703
- case "hostname": {
704
- try {
705
- const url = new URL(strValue);
706
- return url.hostname === matchVal;
707
- } catch {
708
- return false;
709
- }
710
- }
711
- case "quotes":
712
- // engine-php exposes `inline_js_xss` as an alias of `quotes`.
713
- case "inline_js_xss":
714
- return /['"]/.test(strValue);
715
- case "inline_xss":
716
- return /['"]/.test(strValue) && /[>=]/.test(strValue);
717
- case "ctype_special": {
718
- const stripped = strValue.replace(/[ _\-,]/g, "");
719
- return ctypeResult(strValue, /^[\w$€-￿]*$/.test(stripped), matchVal);
720
- }
721
- case "array_key_value":
722
- return arrayKeyValue(value, matchObj);
723
- // file_contains (uploaded-file content scanning) is NOT WordPress-specific, but is
724
- // not yet implemented in the JS engine (needs multipart body access). Kept as an
725
- // explicit, documented no-match rather than silently hitting `default`.
726
- case "file_contains":
727
- return false;
728
- // WordPress-only match types (current_user_cannot, general_xss, getShortcodeAtts,
729
- // getBlockAtts) have been removed: they depend on WP capabilities, wp_kses_post, and
730
- // the shortcode/block parsers, and have no meaning in a JS app. They now fall through
731
- // here and are reported once instead of pretending to be a no-match.
732
- default:
733
- warnUnsupportedMatchType(type);
734
- return false;
735
- }
736
- }
737
- var RuleEngine = class {
738
- #rules;
739
- #whitelists;
740
- #whitelistKeys;
741
- #onError;
742
- #reportedErrors = /* @__PURE__ */ new Set();
743
- constructor({ firewall = [], whitelists = [], whitelist_keys = {}, onError } = {}) {
744
- this.#rules = firewall;
745
- this.#whitelists = whitelists;
746
- this.#whitelistKeys = whitelist_keys;
747
- this.#onError = onError;
748
- }
749
- // A mitigation engine must never take down the app it protects: any error while
750
- // evaluating a rule is reported and the request is allowed through (fail open). A
751
- // malformed rule is skipped without aborting the rest of the ruleset.
752
- #reportError(err) {
753
- if (this.#onError) {
754
- this.#onError(err);
755
- return;
756
- }
757
- const key = err && err.message ? err.message : String(err);
758
- if (this.#reportedErrors.has(key)) {
759
- return;
760
- }
761
- this.#reportedErrors.add(key);
762
- console.error("[patchstack] rule evaluation error (rule skipped, request allowed):", err);
763
- }
764
- evaluate(req) {
765
- let normalizedReq;
766
- let resolver;
767
- try {
768
- normalizedReq = { ...req, ...normalizeRequest(req) };
769
- resolver = new RequestResolver(normalizedReq);
770
- } catch (err) {
771
- this.#reportError(err);
772
- return { blocked: false, rule: null, message: null };
773
- }
774
- for (const rule of this.#rules) {
775
- try {
776
- const conditions = rule.rule_v2;
777
- if (!Array.isArray(conditions) || conditions.length === 0) {
778
- continue;
779
- }
780
- if (this.#evaluateRule(conditions, resolver)) {
781
- if (this.#isWhitelisted(normalizedReq, rule)) {
782
- continue;
783
- }
784
- return {
785
- blocked: true,
786
- rule,
787
- message: rule.message ?? `Blocked by Patchstack WAF rule: ${rule.title ?? rule.id}`
788
- };
789
- }
790
- } catch (err) {
791
- this.#reportError(err);
792
- }
793
- }
794
- return { blocked: false, rule: null, message: null };
795
- }
796
- #evaluateRule(conditions, resolver) {
797
- const inclusiveConditions = conditions.filter((c) => c.inclusive);
798
- const nonInclusiveConditions = conditions.filter((c) => !c.inclusive);
799
- if (inclusiveConditions.length > 0) {
800
- let inclusiveHits = 0;
801
- for (const condition of inclusiveConditions) {
802
- if (this.#evaluateCondition(condition, resolver)) {
803
- inclusiveHits++;
804
- }
805
- }
806
- if (inclusiveHits === inclusiveConditions.length) {
807
- return true;
808
- }
809
- }
810
- for (const condition of nonInclusiveConditions) {
811
- if (this.#evaluateCondition(condition, resolver)) {
812
- return true;
813
- }
814
- }
815
- return false;
816
- }
817
- #evaluateCondition(condition, resolver) {
818
- const { parameter, match, mutations } = condition;
819
- if (parameter === "rules" && Array.isArray(condition.rules)) {
820
- return this.#evaluateRule(condition.rules, resolver);
821
- }
822
- const values = resolver.resolve(parameter);
823
- if (values.length === 0) {
824
- return false;
825
- }
826
- for (let value of values) {
827
- if (mutations) {
828
- value = resolver.applyMutations(mutations, value);
829
- }
830
- if (match.type === "array_key_value") {
831
- if (matchValue(match.type, value, match.value, match)) {
832
- return true;
833
- }
834
- continue;
835
- }
836
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
837
- for (const v of Object.values(value)) {
838
- if (matchValue(match.type, v, match.value, match)) {
839
- return true;
840
- }
841
- }
842
- continue;
843
- }
844
- if (matchValue(match.type, value, match.value, match)) {
845
- return true;
846
- }
847
- }
848
- return false;
849
- }
850
- #isWhitelisted(req, rule) {
851
- if (!this.#whitelists || this.#whitelists.length === 0) {
852
- return false;
853
- }
854
- for (const whitelist of this.#whitelists) {
855
- if (!Array.isArray(whitelist.rule_v2)) {
856
- continue;
857
- }
858
- if (whitelist.rule_id && whitelist.rule_id !== rule.id) {
859
- continue;
860
- }
861
- const resolver = new RequestResolver(req);
862
- if (this.#evaluateRule(whitelist.rule_v2, resolver)) {
863
- return true;
864
- }
865
- }
866
- return false;
867
- }
868
- };
869
-
870
- // src/protect/engine/fetch.js
871
- async function fromFetchRequest(request) {
872
- const url = new URL(request.url);
873
- const method = (request.method || "GET").toUpperCase();
874
- const headers = {};
875
- request.headers.forEach((value, key) => {
876
- headers[key.toLowerCase()] = value;
877
- });
878
- const query = {};
879
- for (const [key, value] of url.searchParams) {
880
- if (key in query) {
881
- query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
882
- } else {
883
- query[key] = value;
884
- }
885
- }
886
- let rawBody = "";
887
- if (method !== "GET" && method !== "HEAD") {
888
- try {
889
- rawBody = await request.clone().text();
890
- } catch {
891
- rawBody = "";
892
- }
893
- }
894
- const contentType = headers["content-type"] || "";
895
- let body = {};
896
- if (rawBody) {
897
- if (contentType.includes("application/json")) {
898
- try {
899
- body = JSON.parse(rawBody);
900
- } catch {
901
- body = {};
902
- }
903
- } else if (contentType.includes("application/x-www-form-urlencoded")) {
904
- body = {};
905
- for (const [k, v] of new URLSearchParams(rawBody)) {
906
- body[k] = k in body ? [].concat(body[k], v) : v;
907
- }
908
- }
909
- }
910
- const uri = url.pathname + url.search;
911
- const forwarded = headers["cf-connecting-ip"] || headers["x-forwarded-for"] || headers["x-real-ip"] || "";
912
- return {
913
- method,
914
- url: uri,
915
- originalUrl: uri,
916
- query,
917
- body,
918
- headers,
919
- ip: forwarded.split(",")[0].trim(),
920
- cookies: parseCookies(headers.cookie),
921
- // Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
922
- // drops, so prototype-pollution rules on `raw` are robust.
923
- _rawBody: rawBody
924
- };
925
- }
926
- function parseCookies(header) {
927
- const cookies = {};
928
- if (!header) {
929
- return cookies;
930
- }
931
- for (const pair of header.split(";")) {
932
- const idx = pair.indexOf("=");
933
- if (idx === -1) {
934
- continue;
935
- }
936
- cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
937
- }
938
- return cookies;
939
- }
940
-
941
- // src/protect/engine/node.js
942
- function fromNodeRequest(req, rawBody = "") {
943
- const method = (req.method || "GET").toUpperCase();
944
- const headers = {};
945
- for (const [key, value] of Object.entries(req.headers || {})) {
946
- headers[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
947
- }
948
- const host = headers.host || "localhost";
949
- const url = new URL(req.url || "/", `http://${host}`);
950
- const query = {};
951
- for (const [key, value] of url.searchParams) {
952
- if (key in query) {
953
- query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
954
- } else {
955
- query[key] = value;
956
- }
957
- }
958
- const contentType = headers["content-type"] || "";
959
- let body = {};
960
- if (rawBody) {
961
- if (contentType.includes("application/json")) {
962
- try {
963
- body = JSON.parse(rawBody);
964
- } catch {
965
- body = {};
966
- }
967
- } else if (contentType.includes("application/x-www-form-urlencoded")) {
968
- body = {};
969
- for (const [k, v] of new URLSearchParams(rawBody)) {
970
- body[k] = k in body ? [].concat(body[k], v) : v;
971
- }
972
- }
973
- }
974
- const uri = url.pathname + url.search;
975
- const forwarded = headers["cf-connecting-ip"] || headers["x-forwarded-for"] || headers["x-real-ip"] || "";
976
- return {
977
- method,
978
- url: uri,
979
- originalUrl: uri,
980
- query,
981
- body,
982
- headers,
983
- ip: forwarded.split(",")[0].trim() || req.socket && req.socket.remoteAddress || "",
984
- cookies: parseCookies2(headers.cookie),
985
- // Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify drops.
986
- _rawBody: rawBody
987
- };
988
- }
989
- function parseCookies2(header) {
990
- const cookies = {};
991
- if (!header) {
992
- return cookies;
993
- }
994
- for (const pair of header.split(";")) {
995
- const idx = pair.indexOf("=");
996
- if (idx === -1) {
997
- continue;
998
- }
999
- cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
1000
- }
1001
- return cookies;
1002
- }
1003
-
1004
- // src/protect/runtime.js
1005
- var import_node_fs = require("fs");
1006
- var import_node_path = require("path");
1007
-
1008
- // src/protect/supabase-guard.js
1009
- var GUARD_PATH = "/_patchstack/guard";
1010
- var HOP_BY_HOP = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection"]);
1011
- function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
1012
- const guard = protection.fetchGuard();
1013
- const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
1014
- return async function handleGuardRequest(request) {
1015
- const target = request.headers.get("x-ps-target");
1016
- if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
1017
- let targetUrl;
1018
- try {
1019
- targetUrl = new URL(target);
1020
- } catch {
1021
- return new Response("patchstack: invalid target", { status: 400 });
1022
- }
1023
- if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
1024
- return new Response("patchstack: target not allowed", { status: 403 });
1025
- }
1026
- const hasBody = request.method !== "GET" && request.method !== "HEAD";
1027
- const bodyText = hasBody ? await request.text() : "";
1028
- const evalReq = new Request(targetUrl.toString(), {
1029
- method: request.method,
1030
- headers: request.headers,
1031
- body: hasBody ? bodyText : void 0
1032
- });
1033
- const blocked = await guard(evalReq);
1034
- if (blocked) return blocked;
1035
- const forwardHeaders = new Headers(request.headers);
1036
- forwardHeaders.delete("x-ps-target");
1037
- forwardHeaders.delete("host");
1038
- const upstream = await fetchImpl(targetUrl.toString(), {
1039
- method: request.method,
1040
- headers: forwardHeaders,
1041
- body: hasBody ? bodyText : void 0,
1042
- redirect: "manual"
1043
- });
1044
- const outHeaders = new Headers();
1045
- upstream.headers.forEach((value, key) => {
1046
- if (!HOP_BY_HOP.has(key.toLowerCase())) outHeaders.set(key, value);
1047
- });
1048
- const buf = await upstream.arrayBuffer();
1049
- return new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
1050
- };
1051
- }
1052
-
1053
- // src/protect/runtime.js
1054
- function createServerFnGuard({ protection }) {
1055
- const guard = protection.fetchGuard();
1056
- return async (data) => {
1057
- let res;
1058
- try {
1059
- const req = new Request("https://patchstack.local/_serverfn", {
1060
- method: "POST",
1061
- headers: { "content-type": "application/json" },
1062
- body: JSON.stringify(data ?? {})
1063
- });
1064
- res = await guard(req);
1065
- } catch {
1066
- return null;
1067
- }
1068
- if (!res) return null;
1069
- let body = {};
1070
- try {
1071
- body = await res.clone().json();
1072
- } catch {
1073
- }
1074
- return { rule: body.rule, message: body.message || "Blocked by Patchstack" };
1075
- };
1076
- }
1077
- async function createProtection(options = {}) {
1078
- const mode = options.mode === "block" ? "block" : "dry-run";
1079
- const onError = options.onError;
1080
- const onDetect = options.onDetect ?? defaultOnDetect;
1081
- const bundle = await resolveRules(options);
1082
- const engine = new RuleEngine({ ...bundle, onError });
1083
- const decide = (result, block, allow) => {
1084
- if (!result || !result.blocked) return allow();
1085
- onDetect({ mode, rule: result.rule, message: result.message });
1086
- return mode === "block" ? block() : allow();
1087
- };
1088
- const protection = {
1089
- mode,
1090
- rules: bundle,
1091
- // (request) => Response | null (null = allow, caller proceeds)
1092
- fetchGuard() {
1093
- return async (request) => {
1094
- let result;
1095
- try {
1096
- result = engine.evaluate(await fromFetchRequest(request));
1097
- } catch (err) {
1098
- onError?.(err);
1099
- return null;
1100
- }
1101
- return decide(result, () => blockResponse(result), () => null);
1102
- };
1103
- },
1104
- // Wrap a fetch handler: export default { fetch: protection.fetch(app.fetch) }
1105
- fetch(handler) {
1106
- const guard = protection.fetchGuard();
1107
- return async (request, ...rest) => await guard(request) ?? handler(request, ...rest);
1108
- },
1109
- // Express middleware (expects express-parsed req.query/req.body).
1110
- express() {
1111
- return (req, res, next) => {
1112
- let result;
1113
- try {
1114
- result = engine.evaluate(req);
1115
- } catch (err) {
1116
- onError?.(err);
1117
- return next();
1118
- }
1119
- decide(result, () => res.status(403).json(blockBody(result)), () => next());
1120
- };
1121
- },
1122
- // Node / Connect middleware — buffers the body itself (no body-parser needed).
1123
- node(nodeOptions = {}) {
1124
- const maxBytes = nodeOptions.maxBodyBytes ?? 1024 * 1024;
1125
- return (req, res, next) => {
1126
- const chunks = [];
1127
- let size = 0;
1128
- let overflow = false;
1129
- req.on("data", (chunk) => {
1130
- size += chunk.length;
1131
- if (size > maxBytes) {
1132
- overflow = true;
1133
- return;
1134
- }
1135
- chunks.push(chunk);
1136
- });
1137
- req.on("error", (err) => {
1138
- onError?.(err);
1139
- next();
1140
- });
1141
- req.on("end", () => {
1142
- const rawBody = overflow ? "" : Buffer.concat(chunks).toString("utf8");
1143
- let result;
1144
- try {
1145
- result = engine.evaluate(fromNodeRequest(req, rawBody));
1146
- } catch (err) {
1147
- onError?.(err);
1148
- return next();
1149
- }
1150
- decide(
1151
- result,
1152
- () => {
1153
- res.statusCode = 403;
1154
- res.setHeader("content-type", "application/json");
1155
- res.end(JSON.stringify(blockBody(result)));
1156
- },
1157
- () => next()
1158
- );
1159
- });
1160
- };
1161
- }
1162
- };
1163
- return protection;
1164
- }
1165
- async function resolveRules(options) {
1166
- if (options.rules) {
1167
- return normalizeBundle(options.rules);
1168
- }
1169
- if (options.token) {
1170
- const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl });
1171
- const res = await client.getRules();
1172
- if (res.success) {
1173
- const bundle = normalizeBundle(res);
1174
- cacheWrite(options.cacheDir, bundle);
1175
- return bundle;
1176
- }
1177
- const cached = cacheRead(options.cacheDir);
1178
- if (cached) {
1179
- options.onError?.(new Error(`rule fetch failed (${res.error}); using cached bundle`));
1180
- return normalizeBundle(cached);
1181
- }
1182
- options.onError?.(new Error(`rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
1183
- return emptyBundle();
1184
- }
1185
- return emptyBundle();
1186
- }
1187
- function normalizeBundle(b) {
1188
- return {
1189
- firewall: Array.isArray(b.firewall) ? b.firewall : [],
1190
- whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
1191
- whitelist_keys: b.whitelist_keys ?? {}
1192
- };
1193
- }
1194
- function emptyBundle() {
1195
- return { firewall: [], whitelists: [], whitelist_keys: {} };
1196
- }
1197
- function cachePath(dir) {
1198
- return (0, import_node_path.join)(dir, "patchstack-rules.json");
1199
- }
1200
- function cacheWrite(dir, bundle) {
1201
- if (!dir) return;
1202
- try {
1203
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
1204
- (0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(bundle));
1205
- } catch {
1206
- }
1207
- }
1208
- function cacheRead(dir) {
1209
- if (!dir) return null;
1210
- try {
1211
- return JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8"));
1212
- } catch {
1213
- return null;
1214
- }
1215
- }
1216
- function blockBody(result) {
1217
- return {
1218
- error: "Blocked by Patchstack",
1219
- rule: result.rule?.id,
1220
- message: result.message
1221
- };
1222
- }
1223
- function blockResponse(result) {
1224
- return new Response(JSON.stringify(blockBody(result)), {
1225
- status: 403,
1226
- headers: { "content-type": "application/json" }
1227
- });
1228
- }
1229
- function defaultOnDetect({ mode, rule, message }) {
1230
- const tag = mode === "block" ? "BLOCK" : "DETECT (dry-run)";
1231
- console.warn(`[patchstack] ${tag} rule=${rule?.id ?? "?"} ${message ?? ""}`.trim());
1232
- }
1233
- // Annotate the CommonJS export names for ESM import in node:
1234
- 0 && (module.exports = {
1235
- GUARD_PATH,
1236
- createProtection,
1237
- createServerFnGuard,
1238
- createSupabaseGuard
1239
- });
1240
- //# sourceMappingURL=protect.cjs.map