@patchstack/connect 0.3.19 → 0.3.21

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 CHANGED
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
6
11
  var __export = (target, all) => {
7
12
  for (var name in all)
8
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -13,9 +18,1185 @@ var __copyProps = (to, from, except, desc) => {
13
18
  if (!__hasOwnProp.call(to, key) && key !== except)
14
19
  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
20
  }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/types.ts
34
+ var PatchstackError;
35
+ var init_types = __esm({
36
+ "src/types.ts"() {
37
+ "use strict";
38
+ PatchstackError = class extends Error {
39
+ constructor(message, code, cause) {
40
+ super(message);
41
+ this.code = code;
42
+ this.cause = cause;
43
+ this.name = "PatchstackError";
44
+ }
45
+ code;
46
+ cause;
47
+ };
48
+ }
49
+ });
50
+
51
+ // src/client.ts
52
+ function buildEndpointUrl(base, siteUuid) {
53
+ const trimmed = base.replace(/\/$/, "");
54
+ return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
55
+ }
56
+ async function postManifest(config, payload) {
57
+ const url = buildEndpointUrl(config.endpoint, config.siteUuid);
58
+ const timeoutMs = config.timeoutMs;
59
+ let response;
60
+ try {
61
+ response = await fetch(url, {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ Accept: "application/json",
66
+ "User-Agent": "@patchstack/connect"
67
+ },
68
+ body: JSON.stringify({ ...payload, environment: config.environment }),
69
+ signal: AbortSignal.timeout(timeoutMs)
70
+ });
71
+ } catch (cause) {
72
+ if (isTimeoutError(cause)) {
73
+ throw new PatchstackError(
74
+ `Patchstack request to ${url} timed out after ${timeoutMs}ms. Override with PATCHSTACK_TIMEOUT_MS.`,
75
+ "NETWORK_TIMEOUT",
76
+ cause
77
+ );
78
+ }
79
+ throw new PatchstackError(
80
+ `Could not reach Patchstack at ${url}. Check your network connection.`,
81
+ "NETWORK_ERROR",
82
+ cause
83
+ );
84
+ }
85
+ const text = await response.text();
86
+ let body = null;
87
+ try {
88
+ body = text.length > 0 ? JSON.parse(text) : null;
89
+ } catch {
90
+ body = null;
91
+ }
92
+ if (response.status === 404) {
93
+ throw new PatchstackError(
94
+ body?.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.",
95
+ "SITE_NOT_FOUND"
96
+ );
97
+ }
98
+ if (response.status === 422) {
99
+ throw new PatchstackError(
100
+ body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
101
+ "VALIDATION_ERROR"
102
+ );
103
+ }
104
+ if (response.status < 200 || response.status >= 300) {
105
+ throw new PatchstackError(
106
+ `Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
107
+ "SERVER_ERROR"
108
+ );
109
+ }
110
+ if (body === null) {
111
+ throw new PatchstackError("Patchstack returned an empty response.", "SERVER_ERROR");
112
+ }
113
+ return body;
114
+ }
115
+ function isTimeoutError(cause) {
116
+ if (cause instanceof Error) {
117
+ return cause.name === "TimeoutError" || cause.name === "AbortError";
118
+ }
119
+ return false;
120
+ }
121
+ var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS;
122
+ var init_client = __esm({
123
+ "src/client.ts"() {
124
+ "use strict";
125
+ init_types();
126
+ DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
127
+ DEFAULT_TIMEOUT_MS = 3e4;
128
+ }
129
+ });
130
+
131
+ // src/config.ts
132
+ async function resolveConfig(options) {
133
+ const fromFile = await readConfigFile(options.cwd);
134
+ const fromEnv = readEnv();
135
+ const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
136
+ const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
137
+ const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
138
+ const environmentRaw = fromEnv.environment ?? fromFile.environment;
139
+ if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
140
+ throw new PatchstackError(
141
+ `Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
142
+ "CONFIG_INVALID"
143
+ );
144
+ }
145
+ const environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
146
+ if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
147
+ throw new PatchstackError(
148
+ `Site UUID "${siteUuid}" does not look like a valid UUID.`,
149
+ "CONFIG_INVALID"
150
+ );
151
+ }
152
+ if (options.requireSiteUuid && (siteUuid === null || siteUuid.length === 0)) {
153
+ throw new PatchstackError(
154
+ "No site UUID configured. Run `patchstack-connect scan` to provision one, or set PATCHSTACK_SITE_UUID.",
155
+ "CONFIG_MISSING"
156
+ );
157
+ }
158
+ return {
159
+ siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
160
+ endpoint,
161
+ timeoutMs,
162
+ environment,
163
+ widget: fromFile.widget !== false
164
+ };
165
+ }
166
+ async function readConfigFile(cwd) {
167
+ const target = import_node_path2.default.join(cwd, CONFIG_FILENAME);
168
+ let raw;
169
+ try {
170
+ raw = await (0, import_promises.readFile)(target, "utf8");
171
+ } catch (err) {
172
+ if (err.code === "ENOENT") {
173
+ return {};
174
+ }
175
+ throw new PatchstackError(
176
+ `Could not read ${target}: ${err.message}`,
177
+ "CONFIG_INVALID",
178
+ err
179
+ );
180
+ }
181
+ try {
182
+ return JSON.parse(raw);
183
+ } catch (err) {
184
+ throw new PatchstackError(
185
+ `Config file ${target} contains invalid JSON.`,
186
+ "CONFIG_INVALID",
187
+ err
188
+ );
189
+ }
190
+ }
191
+ function readEnv() {
192
+ const timeoutRaw = process.env.PATCHSTACK_TIMEOUT_MS;
193
+ let timeoutMs;
194
+ if (timeoutRaw !== void 0 && timeoutRaw.length > 0) {
195
+ const parsed = Number(timeoutRaw);
196
+ if (!Number.isFinite(parsed) || parsed <= 0) {
197
+ throw new PatchstackError(
198
+ `PATCHSTACK_TIMEOUT_MS must be a positive number; got "${timeoutRaw}".`,
199
+ "CONFIG_INVALID"
200
+ );
201
+ }
202
+ timeoutMs = parsed;
203
+ }
204
+ const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
205
+ return {
206
+ siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
207
+ endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
208
+ timeoutMs,
209
+ environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
210
+ };
211
+ }
212
+ function isUuid(value) {
213
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
214
+ }
215
+ function isEnvironment(value) {
216
+ return value === "production" || value === "sandbox";
217
+ }
218
+ var import_promises, import_node_path2, CONFIG_FILENAME, DEFAULT_ENVIRONMENT;
219
+ var init_config = __esm({
220
+ "src/config.ts"() {
221
+ "use strict";
222
+ import_promises = require("fs/promises");
223
+ import_node_path2 = __toESM(require("path"), 1);
224
+ init_types();
225
+ init_client();
226
+ CONFIG_FILENAME = ".patchstackrc.json";
227
+ DEFAULT_ENVIRONMENT = "production";
228
+ }
229
+ });
230
+
231
+ // src/parsers/consistency.ts
232
+ async function readDeclaredDependencyNames(cwd) {
233
+ let raw;
234
+ try {
235
+ raw = await (0, import_promises2.readFile)(import_node_path3.default.join(cwd, "package.json"), "utf8");
236
+ } catch {
237
+ return [];
238
+ }
239
+ let parsed;
240
+ try {
241
+ parsed = JSON.parse(raw);
242
+ } catch {
243
+ return [];
244
+ }
245
+ if (typeof parsed !== "object" || parsed === null) {
246
+ return [];
247
+ }
248
+ const names = /* @__PURE__ */ new Set();
249
+ for (const group of ["dependencies", "devDependencies"]) {
250
+ const deps = parsed[group];
251
+ if (typeof deps !== "object" || deps === null) {
252
+ continue;
253
+ }
254
+ for (const [name, spec] of Object.entries(deps)) {
255
+ if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
256
+ continue;
257
+ }
258
+ names.add(name);
259
+ }
260
+ }
261
+ return [...names];
262
+ }
263
+ function missingDependencies(declared, packages) {
264
+ const present = new Set(packages.map((entry) => entry.name));
265
+ return declared.filter((name) => !present.has(name));
266
+ }
267
+ var import_promises2, import_node_path3;
268
+ var init_consistency = __esm({
269
+ "src/parsers/consistency.ts"() {
270
+ "use strict";
271
+ import_promises2 = require("fs/promises");
272
+ import_node_path3 = __toESM(require("path"), 1);
273
+ }
274
+ });
275
+
276
+ // src/parsers/npm.ts
277
+ async function parseNpmLockfile(lockfilePath) {
278
+ let raw;
279
+ try {
280
+ raw = await (0, import_promises3.readFile)(lockfilePath, "utf8");
281
+ } catch (cause) {
282
+ throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
283
+ }
284
+ let parsed;
285
+ try {
286
+ parsed = JSON.parse(raw);
287
+ } catch (cause) {
288
+ throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
289
+ }
290
+ if (parsed.packages) {
291
+ return extractFromV2(parsed.packages);
292
+ }
293
+ if (parsed.dependencies) {
294
+ return extractFromV1(parsed.dependencies);
295
+ }
296
+ throw new PatchstackError(
297
+ `Lockfile at ${lockfilePath} has no "packages" or "dependencies" key`,
298
+ "LOCKFILE_PARSE_ERROR"
299
+ );
300
+ }
301
+ function extractFromV2(packages) {
302
+ const entries = [];
303
+ for (const [pkgPath, pkg] of Object.entries(packages)) {
304
+ if (pkgPath === "") {
305
+ continue;
306
+ }
307
+ if (pkg.link === true) {
308
+ continue;
309
+ }
310
+ if (typeof pkg.version !== "string" || pkg.version.length === 0) {
311
+ continue;
312
+ }
313
+ const name = pkg.name ?? extractNameFromPath(pkgPath);
314
+ if (name === null) {
315
+ continue;
316
+ }
317
+ entries.push({
318
+ name,
319
+ version: pkg.version,
320
+ path: pkgPath,
321
+ direct: isDirectV2(pkgPath)
322
+ });
323
+ }
324
+ return entries;
325
+ }
326
+ function extractFromV1(deps, acc = [], depth = 0) {
327
+ for (const [name, dep] of Object.entries(deps)) {
328
+ if (typeof dep.version === "string" && dep.version.length > 0) {
329
+ acc.push({ name, version: dep.version, direct: depth === 0 });
330
+ }
331
+ if (dep.dependencies) {
332
+ extractFromV1(dep.dependencies, acc, depth + 1);
333
+ }
334
+ }
335
+ return acc;
336
+ }
337
+ function extractNameFromPath(pkgPath) {
338
+ const segments = pkgPath.split("node_modules" + import_node_path4.default.sep === pkgPath ? import_node_path4.default.sep : "/");
339
+ const parts = pkgPath.split("/");
340
+ const nmIndex = parts.lastIndexOf("node_modules");
341
+ if (nmIndex === -1 || nmIndex >= parts.length - 1) {
342
+ return segments[segments.length - 1] ?? null;
343
+ }
344
+ const tail = parts.slice(nmIndex + 1);
345
+ if (tail.length === 0) {
346
+ return null;
347
+ }
348
+ const first = tail[0];
349
+ if (first !== void 0 && first.startsWith("@") && tail.length >= 2) {
350
+ return `${first}/${tail[1]}`;
351
+ }
352
+ return first ?? null;
353
+ }
354
+ function isDirectV2(pkgPath) {
355
+ const parts = pkgPath.split("/");
356
+ const nmCount = parts.filter((p) => p === "node_modules").length;
357
+ return nmCount === 1;
358
+ }
359
+ var import_promises3, import_node_path4;
360
+ var init_npm = __esm({
361
+ "src/parsers/npm.ts"() {
362
+ "use strict";
363
+ import_promises3 = require("fs/promises");
364
+ import_node_path4 = __toESM(require("path"), 1);
365
+ init_types();
366
+ }
367
+ });
368
+
369
+ // src/parsers/node_modules.ts
370
+ async function walkNodeModules(cwd) {
371
+ const root = import_node_path5.default.join(cwd, "node_modules");
372
+ try {
373
+ const info = await (0, import_promises4.stat)(root);
374
+ if (!info.isDirectory()) {
375
+ throw new PatchstackError(
376
+ `${root} exists but is not a directory.`,
377
+ "LOCKFILE_NOT_FOUND"
378
+ );
379
+ }
380
+ } catch (cause) {
381
+ if (cause instanceof PatchstackError) {
382
+ throw cause;
383
+ }
384
+ throw new PatchstackError(
385
+ `node_modules/ not found at ${cwd}. Install dependencies first (e.g. \`bun install\` or \`npm install\`).`,
386
+ "LOCKFILE_NOT_FOUND",
387
+ cause
388
+ );
389
+ }
390
+ const entries = [];
391
+ await walk(root, entries, 0);
392
+ return entries;
393
+ }
394
+ async function walk(dir, acc, depth) {
395
+ let names;
396
+ try {
397
+ names = await (0, import_promises4.readdir)(dir);
398
+ } catch {
399
+ return;
400
+ }
401
+ for (const name of names) {
402
+ if (name.startsWith(".")) {
403
+ continue;
404
+ }
405
+ const fullPath = import_node_path5.default.join(dir, name);
406
+ if (!await isPlainDirectory(fullPath)) {
407
+ continue;
408
+ }
409
+ if (name.startsWith("@")) {
410
+ let subNames;
411
+ try {
412
+ subNames = await (0, import_promises4.readdir)(fullPath);
413
+ } catch {
414
+ continue;
415
+ }
416
+ for (const sub of subNames) {
417
+ if (sub.startsWith(".")) {
418
+ continue;
419
+ }
420
+ const scopedDir = import_node_path5.default.join(fullPath, sub);
421
+ if (!await isPlainDirectory(scopedDir)) {
422
+ continue;
423
+ }
424
+ await readPackage(scopedDir, depth, acc);
425
+ await walkNested(scopedDir, acc, depth);
426
+ }
427
+ continue;
428
+ }
429
+ await readPackage(fullPath, depth, acc);
430
+ await walkNested(fullPath, acc, depth);
431
+ }
432
+ }
433
+ async function readPackage(pkgDir, depth, acc) {
434
+ let raw;
435
+ try {
436
+ raw = await (0, import_promises4.readFile)(import_node_path5.default.join(pkgDir, "package.json"), "utf8");
437
+ } catch {
438
+ return;
439
+ }
440
+ let parsed;
441
+ try {
442
+ parsed = JSON.parse(raw);
443
+ } catch {
444
+ return;
445
+ }
446
+ if (typeof parsed.name !== "string" || parsed.name.length === 0) {
447
+ return;
448
+ }
449
+ if (typeof parsed.version !== "string" || parsed.version.length === 0) {
450
+ return;
451
+ }
452
+ acc.push({
453
+ name: parsed.name,
454
+ version: parsed.version,
455
+ direct: depth === 0
456
+ });
457
+ }
458
+ async function walkNested(pkgDir, acc, depth) {
459
+ const nested = import_node_path5.default.join(pkgDir, "node_modules");
460
+ if (!await isPlainDirectory(nested)) {
461
+ return;
462
+ }
463
+ await walk(nested, acc, depth + 1);
464
+ }
465
+ async function isPlainDirectory(dir) {
466
+ try {
467
+ const info = await (0, import_promises4.lstat)(dir);
468
+ return info.isDirectory() && !info.isSymbolicLink();
469
+ } catch {
470
+ return false;
471
+ }
472
+ }
473
+ var import_promises4, import_node_path5;
474
+ var init_node_modules = __esm({
475
+ "src/parsers/node_modules.ts"() {
476
+ "use strict";
477
+ import_promises4 = require("fs/promises");
478
+ import_node_path5 = __toESM(require("path"), 1);
479
+ init_types();
480
+ }
481
+ });
482
+
483
+ // src/parsers/pnpm.ts
484
+ async function parsePnpmLockfile(lockfilePath) {
485
+ let raw;
486
+ try {
487
+ raw = await (0, import_promises5.readFile)(lockfilePath, "utf8");
488
+ } catch (cause) {
489
+ throw new PatchstackError(
490
+ `Could not read lockfile at ${lockfilePath}`,
491
+ "LOCKFILE_NOT_FOUND",
492
+ cause
493
+ );
494
+ }
495
+ const lines = raw.split(/\r?\n/);
496
+ const directNames = collectDirectDepNames(lines);
497
+ const packageKeys = collectPackagesBlockKeys(lines);
498
+ if (packageKeys.length === 0) {
499
+ throw new PatchstackError(
500
+ `Lockfile at ${lockfilePath} has no "packages" entries`,
501
+ "LOCKFILE_PARSE_ERROR"
502
+ );
503
+ }
504
+ const entries = [];
505
+ for (const key of packageKeys) {
506
+ const parsed = parsePackageKey(key);
507
+ if (parsed === null) {
508
+ continue;
509
+ }
510
+ entries.push({
511
+ name: parsed.name,
512
+ version: parsed.version,
513
+ direct: directNames.has(parsed.name)
514
+ });
515
+ }
516
+ return entries;
517
+ }
518
+ function parsePackageKey(rawKey) {
519
+ let k = rawKey.trim();
520
+ if (k.length === 0) {
521
+ return null;
522
+ }
523
+ if (k.startsWith("'") && k.endsWith("'") || k.startsWith('"') && k.endsWith('"')) {
524
+ k = k.slice(1, -1);
525
+ }
526
+ if (k.startsWith("/")) {
527
+ k = k.slice(1);
528
+ }
529
+ const parenIdx = k.indexOf("(");
530
+ if (parenIdx >= 0) {
531
+ k = k.slice(0, parenIdx);
532
+ }
533
+ let scopePrefix = "";
534
+ let body = k;
535
+ if (k.startsWith("@")) {
536
+ const firstSlash = k.indexOf("/");
537
+ if (firstSlash <= 0) {
538
+ return null;
539
+ }
540
+ scopePrefix = k.slice(0, firstSlash + 1);
541
+ body = k.slice(firstSlash + 1);
542
+ }
543
+ const slashIdx = body.indexOf("/");
544
+ const atIdx = body.indexOf("@");
545
+ let sepIdx;
546
+ if (slashIdx < 0 && atIdx < 0) {
547
+ return null;
548
+ } else if (slashIdx < 0) {
549
+ sepIdx = atIdx;
550
+ } else if (atIdx < 0) {
551
+ sepIdx = slashIdx;
552
+ } else {
553
+ sepIdx = Math.min(slashIdx, atIdx);
554
+ }
555
+ const name = scopePrefix + body.slice(0, sepIdx);
556
+ let version = body.slice(sepIdx + 1);
557
+ const underscoreIdx = version.indexOf("_");
558
+ if (underscoreIdx >= 0) {
559
+ version = version.slice(0, underscoreIdx);
560
+ }
561
+ if (name.length === 0 || version.length === 0) {
562
+ return null;
563
+ }
564
+ return { name, version };
565
+ }
566
+ function indentOf(line) {
567
+ let i = 0;
568
+ while (i < line.length && line[i] === " ") {
569
+ i++;
570
+ }
571
+ return i;
572
+ }
573
+ function isBlankOrComment(line) {
574
+ const trimmed = line.trim();
575
+ return trimmed.length === 0 || trimmed.startsWith("#");
576
+ }
577
+ function collectPackagesBlockKeys(lines) {
578
+ const keys = [];
579
+ let inBlock = false;
580
+ let childIndent = null;
581
+ for (const line of lines) {
582
+ if (isBlankOrComment(line)) {
583
+ continue;
584
+ }
585
+ const indent = indentOf(line);
586
+ if (!inBlock) {
587
+ if (indent === 0 && line.trim() === "packages:") {
588
+ inBlock = true;
589
+ }
590
+ continue;
591
+ }
592
+ if (indent === 0) {
593
+ break;
594
+ }
595
+ if (childIndent === null) {
596
+ childIndent = indent;
597
+ }
598
+ if (indent !== childIndent) {
599
+ continue;
600
+ }
601
+ const content = line.slice(indent);
602
+ if (!content.endsWith(":")) {
603
+ continue;
604
+ }
605
+ keys.push(content.slice(0, -1));
606
+ }
607
+ return keys;
608
+ }
609
+ function collectDirectDepNames(lines) {
610
+ const names = /* @__PURE__ */ new Set();
611
+ collectFromImporters(lines, names);
612
+ for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
613
+ collectFromTopLevelSection(lines, section, names);
614
+ }
615
+ return names;
616
+ }
617
+ function collectFromImporters(lines, out) {
618
+ let inImporters = false;
619
+ let importerIndent = null;
620
+ let inDepSection = false;
621
+ let depSectionIndent = null;
622
+ let leafIndent = null;
623
+ for (const line of lines) {
624
+ if (isBlankOrComment(line)) {
625
+ continue;
626
+ }
627
+ const indent = indentOf(line);
628
+ const trimmed = line.trim();
629
+ if (!inImporters) {
630
+ if (indent === 0 && trimmed === "importers:") {
631
+ inImporters = true;
632
+ }
633
+ continue;
634
+ }
635
+ if (indent === 0) {
636
+ break;
637
+ }
638
+ if (importerIndent === null) {
639
+ importerIndent = indent;
640
+ }
641
+ if (indent === importerIndent) {
642
+ inDepSection = false;
643
+ depSectionIndent = null;
644
+ leafIndent = null;
645
+ continue;
646
+ }
647
+ if (!inDepSection) {
648
+ const key = stripTrailingColon(trimmed);
649
+ if (key !== null && DEP_SECTIONS.has(key)) {
650
+ inDepSection = true;
651
+ depSectionIndent = indent;
652
+ }
653
+ continue;
654
+ }
655
+ if (depSectionIndent !== null && indent <= depSectionIndent) {
656
+ inDepSection = false;
657
+ depSectionIndent = null;
658
+ leafIndent = null;
659
+ const key = stripTrailingColon(trimmed);
660
+ if (key !== null && DEP_SECTIONS.has(key)) {
661
+ inDepSection = true;
662
+ depSectionIndent = indent;
663
+ }
664
+ continue;
665
+ }
666
+ if (leafIndent === null) {
667
+ leafIndent = indent;
668
+ }
669
+ if (indent !== leafIndent) {
670
+ continue;
671
+ }
672
+ const name = extractLeafName(trimmed);
673
+ if (name !== null) {
674
+ out.add(name);
675
+ }
676
+ }
677
+ }
678
+ function collectFromTopLevelSection(lines, section, out) {
679
+ let inSection = false;
680
+ let leafIndent = null;
681
+ for (const line of lines) {
682
+ if (isBlankOrComment(line)) {
683
+ continue;
684
+ }
685
+ const indent = indentOf(line);
686
+ const trimmed = line.trim();
687
+ if (!inSection) {
688
+ if (indent === 0 && trimmed === `${section}:`) {
689
+ inSection = true;
690
+ }
691
+ continue;
692
+ }
693
+ if (indent === 0) {
694
+ break;
695
+ }
696
+ if (leafIndent === null) {
697
+ leafIndent = indent;
698
+ }
699
+ if (indent !== leafIndent) {
700
+ continue;
701
+ }
702
+ const name = extractLeafName(trimmed);
703
+ if (name !== null) {
704
+ out.add(name);
705
+ }
706
+ }
707
+ }
708
+ function stripTrailingColon(s) {
709
+ if (!s.endsWith(":")) {
710
+ return null;
711
+ }
712
+ return s.slice(0, -1).trim();
713
+ }
714
+ function extractLeafName(trimmed) {
715
+ const colonIdx = trimmed.indexOf(":");
716
+ if (colonIdx < 0) {
717
+ return null;
718
+ }
719
+ let name = trimmed.slice(0, colonIdx).trim();
720
+ if (name.length === 0) {
721
+ return null;
722
+ }
723
+ if (name.startsWith("'") && name.endsWith("'") || name.startsWith('"') && name.endsWith('"')) {
724
+ name = name.slice(1, -1);
725
+ }
726
+ return name.length > 0 ? name : null;
727
+ }
728
+ var import_promises5, DEP_SECTIONS;
729
+ var init_pnpm = __esm({
730
+ "src/parsers/pnpm.ts"() {
731
+ "use strict";
732
+ import_promises5 = require("fs/promises");
733
+ init_types();
734
+ DEP_SECTIONS = /* @__PURE__ */ new Set([
735
+ "dependencies",
736
+ "devDependencies",
737
+ "optionalDependencies"
738
+ ]);
739
+ }
740
+ });
741
+
742
+ // src/parsers/yarn.ts
743
+ async function parseYarnLockfile(lockfilePath) {
744
+ let raw;
745
+ try {
746
+ raw = await (0, import_promises6.readFile)(lockfilePath, "utf8");
747
+ } catch (cause) {
748
+ throw new PatchstackError(
749
+ `Could not read lockfile at ${lockfilePath}`,
750
+ "LOCKFILE_NOT_FOUND",
751
+ cause
752
+ );
753
+ }
754
+ const blocks = parseBlocks(raw);
755
+ if (blocks.length === 0) {
756
+ throw new PatchstackError(
757
+ `Lockfile at ${lockfilePath} contains no package entries`,
758
+ "LOCKFILE_PARSE_ERROR"
759
+ );
760
+ }
761
+ const directNames = await readDirectDepNames(import_node_path6.default.dirname(lockfilePath));
762
+ const entries = [];
763
+ const seen = /* @__PURE__ */ new Set();
764
+ for (const block of blocks) {
765
+ if (block.version.length === 0 || block.names.size === 0) {
766
+ continue;
767
+ }
768
+ for (const name of block.names) {
769
+ const dedupKey = `${name}@${block.version}`;
770
+ if (seen.has(dedupKey)) {
771
+ continue;
772
+ }
773
+ seen.add(dedupKey);
774
+ entries.push({
775
+ name,
776
+ version: block.version,
777
+ direct: directNames.has(name)
778
+ });
779
+ }
780
+ }
781
+ return entries;
782
+ }
783
+ function parseBlocks(raw) {
784
+ const lines = raw.split(/\r?\n/);
785
+ const blocks = [];
786
+ let current = null;
787
+ const finalize = () => {
788
+ if (current !== null && current.version.length > 0 && current.names.size > 0) {
789
+ blocks.push(current);
790
+ }
791
+ current = null;
792
+ };
793
+ for (const line of lines) {
794
+ const trimmed = line.trim();
795
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
796
+ continue;
797
+ }
798
+ const indent = countLeadingSpaces(line);
799
+ if (indent === 0) {
800
+ finalize();
801
+ if (!trimmed.endsWith(":")) {
802
+ continue;
803
+ }
804
+ const keyLine = trimmed.slice(0, -1);
805
+ const names = /* @__PURE__ */ new Set();
806
+ for (const spec of splitDescriptors(keyLine)) {
807
+ const name = extractName(spec);
808
+ if (name !== null) {
809
+ names.add(name);
810
+ }
811
+ }
812
+ current = { names, version: "" };
813
+ continue;
814
+ }
815
+ if (current === null) {
816
+ continue;
817
+ }
818
+ const version = parseVersionField(trimmed);
819
+ if (version !== null) {
820
+ current.version = version;
821
+ }
822
+ }
823
+ finalize();
824
+ return blocks;
825
+ }
826
+ function countLeadingSpaces(line) {
827
+ let i = 0;
828
+ while (i < line.length && line[i] === " ") {
829
+ i++;
830
+ }
831
+ return i;
832
+ }
833
+ function splitDescriptors(keyLine) {
834
+ const parts = [];
835
+ let current = "";
836
+ let quote = null;
837
+ for (let i = 0; i < keyLine.length; i++) {
838
+ const c = keyLine[i];
839
+ if (quote !== null) {
840
+ current += c;
841
+ if (c === quote) {
842
+ quote = null;
843
+ }
844
+ continue;
845
+ }
846
+ if (c === '"' || c === "'") {
847
+ quote = c;
848
+ current += c;
849
+ continue;
850
+ }
851
+ if (c === ",") {
852
+ const piece = current.trim();
853
+ if (piece.length > 0) {
854
+ parts.push(piece);
855
+ }
856
+ current = "";
857
+ continue;
858
+ }
859
+ current += c;
860
+ }
861
+ const tail = current.trim();
862
+ if (tail.length > 0) {
863
+ parts.push(tail);
864
+ }
865
+ return parts;
866
+ }
867
+ function extractName(rawSpec) {
868
+ let s = rawSpec.trim();
869
+ if (s.length === 0) {
870
+ return null;
871
+ }
872
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
873
+ s = s.slice(1, -1);
874
+ }
875
+ const atIdx = s.lastIndexOf("@");
876
+ if (atIdx <= 0) {
877
+ return null;
878
+ }
879
+ const name = s.slice(0, atIdx);
880
+ return name.length > 0 ? name : null;
881
+ }
882
+ function parseVersionField(content) {
883
+ if (!content.startsWith("version")) {
884
+ return null;
885
+ }
886
+ const after = content.slice("version".length);
887
+ const firstChar = after.charAt(0);
888
+ if (firstChar !== " " && firstChar !== " " && firstChar !== ":") {
889
+ return null;
890
+ }
891
+ let rest = firstChar === ":" ? after.slice(1) : after;
892
+ rest = rest.trim();
893
+ if (rest.length === 0) {
894
+ return null;
895
+ }
896
+ if (rest.startsWith('"') && rest.endsWith('"') || rest.startsWith("'") && rest.endsWith("'")) {
897
+ rest = rest.slice(1, -1);
898
+ }
899
+ return rest.length > 0 ? rest : null;
900
+ }
901
+ async function readDirectDepNames(cwd) {
902
+ const names = /* @__PURE__ */ new Set();
903
+ let raw;
904
+ try {
905
+ raw = await (0, import_promises6.readFile)(import_node_path6.default.join(cwd, "package.json"), "utf8");
906
+ } catch {
907
+ return names;
908
+ }
909
+ let parsed;
910
+ try {
911
+ parsed = JSON.parse(raw);
912
+ } catch {
913
+ return names;
914
+ }
915
+ if (typeof parsed !== "object" || parsed === null) {
916
+ return names;
917
+ }
918
+ const obj = parsed;
919
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
920
+ const section = obj[field];
921
+ if (typeof section !== "object" || section === null) {
922
+ continue;
923
+ }
924
+ for (const name of Object.keys(section)) {
925
+ names.add(name);
926
+ }
927
+ }
928
+ return names;
929
+ }
930
+ var import_promises6, import_node_path6;
931
+ var init_yarn = __esm({
932
+ "src/parsers/yarn.ts"() {
933
+ "use strict";
934
+ import_promises6 = require("fs/promises");
935
+ import_node_path6 = __toESM(require("path"), 1);
936
+ init_types();
937
+ }
938
+ });
939
+
940
+ // src/parsers/index.ts
941
+ async function detectLockfile(cwd) {
942
+ const probed = await Promise.all(
943
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
944
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
945
+ return { ...candidate, filePath, present: await exists(filePath) };
946
+ })
947
+ );
948
+ const match = probed.find((candidate) => candidate.present);
949
+ if (match) {
950
+ return {
951
+ ecosystem: "npm",
952
+ filePath: match.filePath,
953
+ filename: match.filename,
954
+ strategy: match.strategy
955
+ };
956
+ }
957
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
958
+ throw new PatchstackError(
959
+ `No lockfile found in ${cwd}. Expected one of: ${expected}.`,
960
+ "LOCKFILE_NOT_FOUND"
961
+ );
962
+ }
963
+ async function scanLockfile(cwd) {
964
+ const candidates = await presentLockfiles(cwd);
965
+ if (candidates.length === 0) {
966
+ await detectLockfile(cwd);
967
+ }
968
+ const declared = await readDeclaredDependencyNames(cwd);
969
+ const warnings = [];
970
+ let firstParsed = null;
971
+ let walkTried = false;
972
+ for (const candidate of candidates) {
973
+ let packages;
974
+ try {
975
+ packages = await runStrategy(candidate, cwd);
976
+ } catch {
977
+ continue;
978
+ }
979
+ walkTried ||= candidate.strategy === "node-modules-walk";
980
+ firstParsed ??= { packages, filename: candidate.filename };
981
+ const missing = missingDependencies(declared, packages);
982
+ if (missing.length === 0) {
983
+ return manifestWith(packages, warnings);
984
+ }
985
+ warnings.push(staleWarning(candidate.filename, missing));
986
+ }
987
+ if (!walkTried) {
988
+ try {
989
+ const packages = await walkNodeModules(cwd);
990
+ const missing = missingDependencies(declared, packages);
991
+ if (missing.length > 0) {
992
+ warnings.push(staleWarning("node_modules/", missing));
993
+ }
994
+ warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
995
+ return manifestWith(packages, warnings);
996
+ } catch {
997
+ }
998
+ }
999
+ if (firstParsed === null) {
1000
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
1001
+ throw new PatchstackError(
1002
+ `No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
1003
+ "LOCKFILE_NOT_FOUND"
1004
+ );
1005
+ }
1006
+ warnings.push(
1007
+ `No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
1008
+ );
1009
+ return manifestWith(firstParsed.packages, warnings);
1010
+ }
1011
+ function manifestWith(packages, warnings) {
1012
+ return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
1013
+ }
1014
+ function staleWarning(source, missing) {
1015
+ const sample = missing.slice(0, 3).join(", ");
1016
+ const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
1017
+ return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
1018
+ }
1019
+ async function presentLockfiles(cwd) {
1020
+ const probed = await Promise.all(
1021
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
1022
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
1023
+ return { ...candidate, filePath, present: await exists(filePath) };
1024
+ })
1025
+ );
1026
+ return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
1027
+ }
1028
+ async function runStrategy(detected, cwd) {
1029
+ switch (detected.strategy) {
1030
+ case "npm-lockfile":
1031
+ return parseNpmLockfile(detected.filePath);
1032
+ case "pnpm-lockfile":
1033
+ return parsePnpmLockfile(detected.filePath);
1034
+ case "yarn-lockfile":
1035
+ return parseYarnLockfile(detected.filePath);
1036
+ case "node-modules-walk":
1037
+ return walkNodeModules(cwd);
1038
+ }
1039
+ }
1040
+ async function exists(filePath) {
1041
+ try {
1042
+ await (0, import_promises7.access)(filePath);
1043
+ return true;
1044
+ } catch {
1045
+ return false;
1046
+ }
1047
+ }
1048
+ var import_promises7, import_node_path7, LOCKFILE_CANDIDATES;
1049
+ var init_parsers = __esm({
1050
+ "src/parsers/index.ts"() {
1051
+ "use strict";
1052
+ import_promises7 = require("fs/promises");
1053
+ import_node_path7 = __toESM(require("path"), 1);
1054
+ init_types();
1055
+ init_consistency();
1056
+ init_npm();
1057
+ init_node_modules();
1058
+ init_pnpm();
1059
+ init_yarn();
1060
+ LOCKFILE_CANDIDATES = [
1061
+ { filename: "package-lock.json", strategy: "npm-lockfile" },
1062
+ { filename: "bun.lock", strategy: "node-modules-walk" },
1063
+ { filename: "bun.lockb", strategy: "node-modules-walk" },
1064
+ { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
1065
+ { filename: "yarn.lock", strategy: "yarn-lockfile" }
1066
+ ];
1067
+ }
1068
+ });
1069
+
1070
+ // src/normalize.ts
1071
+ function buildWirePayload(manifest) {
1072
+ const seen = /* @__PURE__ */ new Map();
1073
+ const wirePackages = [];
1074
+ for (const entry of manifest.packages) {
1075
+ const versions = seen.get(entry.name);
1076
+ if (versions) {
1077
+ if (versions.has(entry.version)) {
1078
+ continue;
1079
+ }
1080
+ versions.add(entry.version);
1081
+ } else {
1082
+ seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
1083
+ }
1084
+ wirePackages.push({ name: entry.name, version: entry.version });
1085
+ }
1086
+ wirePackages.sort((a, b) => {
1087
+ if (a.name === b.name) {
1088
+ return compareVersions(a.version, b.version);
1089
+ }
1090
+ return a.name < b.name ? -1 : 1;
1091
+ });
1092
+ const duplicateNames = [];
1093
+ for (const [name, versions] of seen) {
1094
+ if (versions.size > 1) {
1095
+ duplicateNames.push(name);
1096
+ }
1097
+ }
1098
+ return {
1099
+ payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
1100
+ stats: {
1101
+ uniqueNames: seen.size,
1102
+ duplicateNames,
1103
+ totalEntries: manifest.packages.length
1104
+ }
1105
+ };
1106
+ }
1107
+ function compareVersions(a, b) {
1108
+ if (a === b) {
1109
+ return 0;
1110
+ }
1111
+ const [aBase, aPre] = splitPrerelease(a);
1112
+ const [bBase, bPre] = splitPrerelease(b);
1113
+ const baseCmp = compareSegments(aBase.split("."), bBase.split("."));
1114
+ if (baseCmp !== 0) {
1115
+ return baseCmp;
1116
+ }
1117
+ if (aPre === null && bPre === null) {
1118
+ return 0;
1119
+ }
1120
+ if (aPre === null) {
1121
+ return 1;
1122
+ }
1123
+ if (bPre === null) {
1124
+ return -1;
1125
+ }
1126
+ return compareSegments(aPre.split("."), bPre.split("."));
1127
+ }
1128
+ function splitPrerelease(version) {
1129
+ const cleaned = version.replace(/^[v=]+/, "").split("+")[0];
1130
+ const dashIndex = cleaned.indexOf("-");
1131
+ if (dashIndex === -1) {
1132
+ return [cleaned, null];
1133
+ }
1134
+ return [cleaned.slice(0, dashIndex), cleaned.slice(dashIndex + 1)];
1135
+ }
1136
+ function compareSegments(a, b) {
1137
+ const max = Math.max(a.length, b.length);
1138
+ for (let i = 0; i < max; i++) {
1139
+ const aPart = a[i];
1140
+ const bPart = b[i];
1141
+ if (aPart === void 0) {
1142
+ return -1;
1143
+ }
1144
+ if (bPart === void 0) {
1145
+ return 1;
1146
+ }
1147
+ const aNum = /^\d+$/.test(aPart);
1148
+ const bNum = /^\d+$/.test(bPart);
1149
+ if (aNum && bNum) {
1150
+ const diff = Number(aPart) - Number(bPart);
1151
+ if (diff !== 0) {
1152
+ return diff < 0 ? -1 : 1;
1153
+ }
1154
+ continue;
1155
+ }
1156
+ if (aNum) {
1157
+ return -1;
1158
+ }
1159
+ if (bNum) {
1160
+ return 1;
1161
+ }
1162
+ if (aPart < bPart) {
1163
+ return -1;
1164
+ }
1165
+ if (aPart > bPart) {
1166
+ return 1;
1167
+ }
1168
+ }
1169
+ return 0;
1170
+ }
1171
+ var init_normalize = __esm({
1172
+ "src/normalize.ts"() {
1173
+ "use strict";
1174
+ }
1175
+ });
1176
+
1177
+ // src/protect/refresh-manifest.ts
1178
+ var refresh_manifest_exports = {};
1179
+ __export(refresh_manifest_exports, {
1180
+ reportManifest: () => reportManifest
1181
+ });
1182
+ async function reportManifest(cwd) {
1183
+ const config = await resolveConfig({ cwd });
1184
+ if (config.siteUuid === null || config.siteUuid.length === 0) {
1185
+ return;
1186
+ }
1187
+ const manifest = await scanLockfile(cwd);
1188
+ const { payload } = buildWirePayload(manifest);
1189
+ await postManifest(config, payload);
1190
+ }
1191
+ var init_refresh_manifest = __esm({
1192
+ "src/protect/refresh-manifest.ts"() {
1193
+ "use strict";
1194
+ init_config();
1195
+ init_parsers();
1196
+ init_normalize();
1197
+ init_client();
1198
+ }
1199
+ });
19
1200
 
20
1201
  // src/protect/runtime.js
21
1202
  var runtime_exports = {};
@@ -43,7 +1224,7 @@ var PatchstackRuleClient = class {
43
1224
  constructor({ token, baseUrl, cacheTtl, etag } = {}) {
44
1225
  this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
45
1226
  this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
46
- this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL;
1227
+ this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL;
47
1228
  this.#etag = etag ?? null;
48
1229
  if (!this.#token) {
49
1230
  throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
@@ -83,11 +1264,14 @@ var PatchstackRuleClient = class {
83
1264
  };
84
1265
  }
85
1266
  const data = await response.json();
1267
+ if (!data || !Array.isArray(data.firewall)) {
1268
+ return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
1269
+ }
86
1270
  const result = {
87
1271
  success: true,
88
1272
  notModified: false,
89
1273
  etag: response.headers?.get?.("etag") ?? null,
90
- firewall: Array.isArray(data.firewall) ? data.firewall : [],
1274
+ firewall: data.firewall,
91
1275
  whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
92
1276
  whitelist_keys: data.whitelist_keys ?? {}
93
1277
  };
@@ -413,13 +1597,14 @@ var RequestResolver = class {
413
1597
  if (typeof obj !== "object" || obj === null) {
414
1598
  return void 0;
415
1599
  }
416
- if (key in obj) {
1600
+ const own = (o, k) => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
1601
+ if (own(obj, key)) {
417
1602
  return obj[key];
418
1603
  }
419
1604
  const parts = key.split(".");
420
1605
  let current = obj;
421
1606
  for (const part of parts) {
422
- if (current === null || current === void 0 || typeof current !== "object") {
1607
+ if (!own(current, part)) {
423
1608
  return void 0;
424
1609
  }
425
1610
  current = current[part];
@@ -632,11 +1817,8 @@ function normalizeObject(value, options = {}) {
632
1817
 
633
1818
  // src/protect/engine/engine.js
634
1819
  var REDOS_PATTERNS = [
635
- /\(\w\+\)\+/,
636
- /\(\w\*\)\+/,
637
- /\(\w\+\)\*/,
638
- /\(\w\*\)\*/,
639
- /\(\w\|\w\)\+/
1820
+ /\([^)]*[+*}][^)]*\)\s*[+*]/,
1821
+ /\([^)]*\|[^)]*\)\s*[+*]/
640
1822
  ];
641
1823
  function safeRegExp(pattern) {
642
1824
  if (!pattern) {
@@ -664,6 +1846,25 @@ function ctypeResult(strValue, isClass, matchVal) {
664
1846
  const expected = typeof matchVal === "boolean" ? matchVal : true;
665
1847
  return isClass === expected;
666
1848
  }
1849
+ function walkLeaves(node, segments, cb) {
1850
+ if (node === null || typeof node !== "object") return;
1851
+ if (Array.isArray(node)) {
1852
+ for (const el of node) walkLeaves(el, segments, cb);
1853
+ return;
1854
+ }
1855
+ const [head, ...rest] = segments;
1856
+ if (head === void 0 || !Object.prototype.hasOwnProperty.call(node, head)) return;
1857
+ if (rest.length === 0) {
1858
+ const leaf = node[head];
1859
+ if (Array.isArray(leaf)) {
1860
+ for (let i = 0; i < leaf.length; i++) cb({ parent: leaf, key: i, value: leaf[i] });
1861
+ } else {
1862
+ cb({ parent: node, key: head, value: leaf });
1863
+ }
1864
+ } else {
1865
+ walkLeaves(node[head], rest, cb);
1866
+ }
1867
+ }
667
1868
  function arrayKeyValue(value, matchObj) {
668
1869
  if (!matchObj || !matchObj.match || value === null || typeof value !== "object") {
669
1870
  return false;
@@ -671,23 +1872,11 @@ function arrayKeyValue(value, matchObj) {
671
1872
  const keys = Array.isArray(matchObj.key) ? matchObj.key : [matchObj.key];
672
1873
  const sub = matchObj.match;
673
1874
  for (const key of keys) {
674
- let node = value;
675
- for (const part of String(key).split(".")) {
676
- if (node === null || typeof node !== "object") {
677
- node = void 0;
678
- break;
679
- }
680
- node = node[part];
681
- }
682
- if (node === void 0) {
683
- continue;
684
- }
685
- const candidates = Array.isArray(node) ? node : [node];
686
- for (const candidate of candidates) {
687
- if (matchValue(sub.type, candidate, sub.value, sub)) {
688
- return true;
689
- }
690
- }
1875
+ let matched = false;
1876
+ walkLeaves(value, String(key).split("."), ({ value: leaf }) => {
1877
+ if (!matched && matchValue(sub.type, leaf, sub.value, sub)) matched = true;
1878
+ });
1879
+ if (matched) return true;
691
1880
  }
692
1881
  return false;
693
1882
  }
@@ -709,16 +1898,68 @@ function isInternalHost(hostname) {
709
1898
  if (host === "::1" || host === "::") return true;
710
1899
  if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) return true;
711
1900
  const v4 = host.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
712
- if (v4) {
713
- const a = Number(v4[1]);
714
- const b = Number(v4[2]);
715
- if (a === 127 || a === 10 || a === 0) return true;
716
- if (a === 169 && b === 254) return true;
717
- if (a === 172 && b >= 16 && b <= 31) return true;
718
- if (a === 192 && b === 168) return true;
1901
+ if (v4 && isPrivateV4(Number(v4[1]), Number(v4[2]))) return true;
1902
+ const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
1903
+ if (mapped) {
1904
+ const g1 = parseInt(mapped[1], 16);
1905
+ const g2 = parseInt(mapped[2], 16);
1906
+ if (isPrivateV4(g1 >> 8 & 255, g1 & 255)) return true;
719
1907
  }
720
1908
  return false;
721
1909
  }
1910
+ function isPrivateV4(a, b) {
1911
+ if (a === 127 || a === 10 || a === 0) return true;
1912
+ if (a === 169 && b === 254) return true;
1913
+ if (a === 172 && b >= 16 && b <= 31) return true;
1914
+ if (a === 192 && b === 168) return true;
1915
+ return false;
1916
+ }
1917
+ function ruleAppliesTo(when, resolver) {
1918
+ try {
1919
+ if (when.method) {
1920
+ const methods = (Array.isArray(when.method) ? when.method : [when.method]).map((m) => String(m).toUpperCase());
1921
+ const actual = String(resolver.resolve("server.REQUEST_METHOD")[0] ?? "GET").toUpperCase();
1922
+ if (!methods.includes(actual)) return false;
1923
+ }
1924
+ if (when.path) {
1925
+ const uri = String(resolver.resolve("server.REQUEST_URI")[0] ?? "/");
1926
+ if (!pathMatches(String(when.path), uri.split("?")[0])) return false;
1927
+ }
1928
+ return true;
1929
+ } catch {
1930
+ return true;
1931
+ }
1932
+ }
1933
+ function pathMatches(pattern, path7) {
1934
+ if (pattern.length > 2 && pattern.startsWith("/") && pattern.endsWith("/")) {
1935
+ const re = safeRegExp(pattern);
1936
+ return re ? re.test(path7) : false;
1937
+ }
1938
+ if (pattern.includes("*")) {
1939
+ const re = safeRegExp("/^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^?]*") + "$/");
1940
+ return re ? re.test(path7) : false;
1941
+ }
1942
+ return path7 === pattern;
1943
+ }
1944
+ function isCrossOrigin(resolver) {
1945
+ try {
1946
+ const host = String(resolver.resolve("server.HTTP_HOST")[0] ?? "").toLowerCase();
1947
+ if (!host) return false;
1948
+ const src = resolver.resolve("server.HTTP_ORIGIN")[0] ?? resolver.resolve("server.HTTP_REFERER")[0];
1949
+ if (!src) return false;
1950
+ const srcHost = hostFromUrl(String(src));
1951
+ return srcHost !== null && srcHost !== host;
1952
+ } catch {
1953
+ return false;
1954
+ }
1955
+ }
1956
+ function hostFromUrl(value) {
1957
+ try {
1958
+ return new URL(value).host.toLowerCase();
1959
+ } catch {
1960
+ return null;
1961
+ }
1962
+ }
722
1963
  function matchValue(type, value, matchVal, matchObj) {
723
1964
  if (value === null || value === void 0) {
724
1965
  if (type === "isset") {
@@ -764,18 +2005,19 @@ function matchValue(type, value, matchVal, matchObj) {
764
2005
  case "isset":
765
2006
  return true;
766
2007
  case "in_array": {
767
- const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
2008
+ const arr = (Array.isArray(matchVal) ? matchVal : [matchVal]).map((x) => String(x));
768
2009
  return arr.includes(strValue);
769
2010
  }
770
2011
  case "not_in_array": {
771
- const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
2012
+ const arr = (Array.isArray(matchVal) ? matchVal : [matchVal]).map((x) => String(x));
772
2013
  return !arr.includes(strValue);
773
2014
  }
774
2015
  case "array_in_array": {
775
2016
  if (!Array.isArray(value) || !Array.isArray(matchVal)) {
776
2017
  return false;
777
2018
  }
778
- return value.some((v) => matchVal.includes(v));
2019
+ const target = matchVal.map((x) => String(x));
2020
+ return value.some((v) => target.includes(String(v)));
779
2021
  }
780
2022
  case "hostname": {
781
2023
  try {
@@ -852,6 +2094,9 @@ var RuleEngine = class {
852
2094
  }
853
2095
  for (const rule of this.#rules) {
854
2096
  try {
2097
+ if (rule.when && !ruleAppliesTo(rule.when, resolver)) {
2098
+ continue;
2099
+ }
855
2100
  const conditions = rule.rule_v2;
856
2101
  if (!Array.isArray(conditions) || conditions.length === 0) {
857
2102
  continue;
@@ -898,6 +2143,9 @@ var RuleEngine = class {
898
2143
  if (parameter === "rules" && Array.isArray(condition.rules)) {
899
2144
  return this.#evaluateRule(condition.rules, resolver);
900
2145
  }
2146
+ if (match && match.type === "cross_origin") {
2147
+ return isCrossOrigin(resolver);
2148
+ }
901
2149
  const params = Array.isArray(parameter) ? parameter : [parameter];
902
2150
  const values = params.flatMap((p) => resolver.resolve(p));
903
2151
  if (values.length === 0) {
@@ -962,7 +2210,7 @@ var PulseRuleClient = class {
962
2210
  constructor({ siteUuid, baseUrl, cacheTtl, etag } = {}) {
963
2211
  this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
964
2212
  this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL ?? DEFAULT_BASE_URL2;
965
- this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL2;
2213
+ this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
966
2214
  this.#etag = etag ?? null;
967
2215
  if (!this.#siteUuid) {
968
2216
  throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
@@ -986,11 +2234,14 @@ var PulseRuleClient = class {
986
2234
  return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
987
2235
  }
988
2236
  const data = await response.json();
2237
+ if (!data || !Array.isArray(data.firewall)) {
2238
+ return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
2239
+ }
989
2240
  const result = {
990
2241
  success: true,
991
2242
  notModified: false,
992
2243
  etag: response.headers?.get?.("etag") ?? null,
993
- firewall: Array.isArray(data.firewall) ? data.firewall : [],
2244
+ firewall: data.firewall,
994
2245
  whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
995
2246
  whitelist_keys: data.whitelist_keys ?? {}
996
2247
  };
@@ -1092,6 +2343,29 @@ async function readCappedText(request, max) {
1092
2343
  } catch {
1093
2344
  return "";
1094
2345
  }
2346
+ const body = clone.body;
2347
+ if (body && typeof body.getReader === "function") {
2348
+ const reader = body.getReader();
2349
+ const chunks = [];
2350
+ let buffered = 0;
2351
+ try {
2352
+ for (; ; ) {
2353
+ const { done, value } = await reader.read();
2354
+ if (done) break;
2355
+ if (!value || buffered >= max) continue;
2356
+ const take = Math.min(value.byteLength, max - buffered);
2357
+ chunks.push(take === value.byteLength ? value : value.subarray(0, take));
2358
+ buffered += take;
2359
+ }
2360
+ } catch {
2361
+ return "";
2362
+ }
2363
+ try {
2364
+ return new TextDecoder().decode(concatChunks(chunks, buffered));
2365
+ } catch {
2366
+ return "";
2367
+ }
2368
+ }
1095
2369
  try {
1096
2370
  const text = await clone.text();
1097
2371
  return text.length > max ? text.slice(0, max) : text;
@@ -1099,6 +2373,15 @@ async function readCappedText(request, max) {
1099
2373
  return "";
1100
2374
  }
1101
2375
  }
2376
+ function concatChunks(chunks, total) {
2377
+ const out = new Uint8Array(total);
2378
+ let offset = 0;
2379
+ for (const c of chunks) {
2380
+ out.set(c, offset);
2381
+ offset += c.byteLength;
2382
+ }
2383
+ return out;
2384
+ }
1102
2385
  function parseMultipart(rawBody, boundary) {
1103
2386
  const body = {};
1104
2387
  const files = {};
@@ -1143,7 +2426,16 @@ function fromNodeRequest(req, rawBody = "") {
1143
2426
  headers[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
1144
2427
  }
1145
2428
  const host = headers.host || "localhost";
1146
- const url = new URL(req.url || "/", `http://${host}`);
2429
+ let url;
2430
+ try {
2431
+ url = new URL(req.url || "/", `http://${host}`);
2432
+ } catch {
2433
+ try {
2434
+ url = new URL(req.url || "/", "http://localhost");
2435
+ } catch {
2436
+ url = new URL("http://localhost/");
2437
+ }
2438
+ }
1147
2439
  const query = {};
1148
2440
  for (const [key, value] of url.searchParams) {
1149
2441
  if (key in query) {
@@ -1154,6 +2446,7 @@ function fromNodeRequest(req, rawBody = "") {
1154
2446
  }
1155
2447
  const contentType = headers["content-type"] || "";
1156
2448
  let body = {};
2449
+ let files;
1157
2450
  if (rawBody) {
1158
2451
  if (contentType.includes("application/json")) {
1159
2452
  try {
@@ -1166,6 +2459,13 @@ function fromNodeRequest(req, rawBody = "") {
1166
2459
  for (const [k, v] of new URLSearchParams(rawBody)) {
1167
2460
  body[k] = k in body ? [].concat(body[k], v) : v;
1168
2461
  }
2462
+ } else if (contentType.includes("multipart/form-data")) {
2463
+ const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
2464
+ if (boundary) {
2465
+ const parsed = parseMultipart(rawBody, boundary);
2466
+ body = parsed.body;
2467
+ files = parsed.files;
2468
+ }
1169
2469
  }
1170
2470
  }
1171
2471
  const uri = url.pathname + url.search;
@@ -1176,6 +2476,7 @@ function fromNodeRequest(req, rawBody = "") {
1176
2476
  originalUrl: uri,
1177
2477
  query,
1178
2478
  body,
2479
+ files,
1179
2480
  headers,
1180
2481
  ip: forwarded.split(",")[0].trim() || req.socket && req.socket.remoteAddress || "",
1181
2482
  cookies: parseCookies2(headers.cookie),
@@ -1199,15 +2500,43 @@ function parseCookies2(header) {
1199
2500
  }
1200
2501
 
1201
2502
  // src/protect/egress.js
1202
- async function installEgressGuard({ shouldBlock, onBlock } = {}) {
2503
+ async function installEgressGuard({ shouldBlock, onBlock, dnsScreen = true, lookup, allowHosts } = {}) {
1203
2504
  const restores = [];
1204
2505
  if (typeof shouldBlock !== "function") return () => {
1205
2506
  };
2507
+ const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
1206
2508
  const block = (url, host, method) => {
1207
2509
  if (!shouldBlock(url, host, method)) return false;
1208
2510
  onBlock?.({ url, host, method });
1209
2511
  return true;
1210
2512
  };
2513
+ let screen = null;
2514
+ if (dnsScreen) {
2515
+ try {
2516
+ const resolveLookup = lookup ?? (await import("dns")).lookup;
2517
+ const { isIP } = await import("net");
2518
+ if (typeof resolveLookup === "function" && typeof isIP === "function") {
2519
+ screen = { lookup: resolveLookup, isIP, isExempt: (h) => exempt.has(String(h).toLowerCase()) };
2520
+ }
2521
+ } catch {
2522
+ screen = null;
2523
+ }
2524
+ }
2525
+ const resolvesToDisallowed = (url, host, method) => new Promise((resolve) => {
2526
+ if (!screen || !host || screen.isIP(host) !== 0 || screen.isExempt(host)) return resolve(false);
2527
+ try {
2528
+ screen.lookup(host, { all: true }, (err, addresses) => {
2529
+ if (err || !Array.isArray(addresses)) return resolve(false);
2530
+ for (const a of addresses) {
2531
+ const ip = a && typeof a === "object" ? a.address : a;
2532
+ if (ip && block(url, ip, method)) return resolve(true);
2533
+ }
2534
+ resolve(false);
2535
+ });
2536
+ } catch {
2537
+ resolve(false);
2538
+ }
2539
+ });
1211
2540
  const originalFetch = globalThis.fetch;
1212
2541
  if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
1213
2542
  const guarded = async (input, init) => {
@@ -1219,7 +2548,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
1219
2548
  host = null;
1220
2549
  }
1221
2550
  const method = init && init.method || input && input.method || "GET";
1222
- if (block(url, host, method)) {
2551
+ if (block(url, host, method) || await resolvesToDisallowed(url, host, method)) {
1223
2552
  throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
1224
2553
  }
1225
2554
  return originalFetch(input, init);
@@ -1233,7 +2562,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
1233
2562
  for (const moduleName of ["node:http", "node:https"]) {
1234
2563
  try {
1235
2564
  const mod = await import(moduleName);
1236
- const restore = patchHttpModule(mod.default ?? mod, block);
2565
+ const restore = patchHttpModule(mod.default ?? mod, block, screen);
1237
2566
  if (restore) restores.push(restore);
1238
2567
  } catch {
1239
2568
  }
@@ -1271,7 +2600,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
1271
2600
  }
1272
2601
  };
1273
2602
  }
1274
- function patchHttpModule(http, block) {
2603
+ function patchHttpModule(http, block, screen) {
1275
2604
  if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
1276
2605
  const originalRequest = http.request;
1277
2606
  const originalGet = http.get;
@@ -1280,14 +2609,25 @@ function patchHttpModule(http, block) {
1280
2609
  if (target && block(target.url, target.host, target.method)) {
1281
2610
  throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
1282
2611
  }
2612
+ if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
2613
+ try {
2614
+ args = withScreeningLookup(args, target, block, screen.lookup);
2615
+ } catch {
2616
+ }
2617
+ }
1283
2618
  return original.apply(this, args);
1284
2619
  };
1285
- http.request = wrap(originalRequest);
1286
- if (typeof originalGet === "function") http.get = wrap(originalGet);
2620
+ const guardedRequest = wrap(originalRequest);
2621
+ http.request = guardedRequest;
2622
+ let guardedGet;
2623
+ if (typeof originalGet === "function") {
2624
+ guardedGet = wrap(originalGet);
2625
+ http.get = guardedGet;
2626
+ }
1287
2627
  http.__patchstackGuarded = true;
1288
2628
  return () => {
1289
- http.request = originalRequest;
1290
- if (typeof originalGet === "function") http.get = originalGet;
2629
+ if (http.request === guardedRequest) http.request = originalRequest;
2630
+ if (guardedGet && http.get === guardedGet) http.get = originalGet;
1291
2631
  delete http.__patchstackGuarded;
1292
2632
  };
1293
2633
  }
@@ -1300,11 +2640,11 @@ function extractHttpTarget(args) {
1300
2640
  return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
1301
2641
  }
1302
2642
  if (first && typeof first === "object") {
1303
- const host = normalizeHost(first.hostname || first.host);
2643
+ const host = normalizeHost(first.hostname || first.host) || "localhost";
1304
2644
  const protocol = first.protocol || "http:";
1305
2645
  const port = first.port ? `:${first.port}` : "";
1306
- const path = first.path || "/";
1307
- return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
2646
+ const path7 = first.path || "/";
2647
+ return { url: `${protocol}//${host}${port}${path7}`, host, method: first.method || "GET" };
1308
2648
  }
1309
2649
  } catch {
1310
2650
  }
@@ -1317,6 +2657,61 @@ function normalizeHost(raw) {
1317
2657
  if ((host.match(/:/g) || []).length > 1) return host;
1318
2658
  return host.split(":")[0];
1319
2659
  }
2660
+ function screenResolved(addresses, target, block) {
2661
+ for (const a of addresses || []) {
2662
+ const ip = a && typeof a === "object" ? a.address : a;
2663
+ if (ip && block(target.url, ip, target.method)) return ip;
2664
+ }
2665
+ return null;
2666
+ }
2667
+ function withScreeningLookup(args, target, block, lookup) {
2668
+ const screeningLookup = (hostname, options, callback) => {
2669
+ let opts = options;
2670
+ let cb = callback;
2671
+ if (typeof opts === "function") {
2672
+ cb = opts;
2673
+ opts = {};
2674
+ }
2675
+ if (!opts || typeof opts !== "object") opts = {};
2676
+ try {
2677
+ lookup(hostname, { ...opts, all: true }, (err, addresses) => {
2678
+ if (err) return cb(err);
2679
+ const list = Array.isArray(addresses) ? addresses : [];
2680
+ const blocked = screenResolved(list, target, block);
2681
+ if (blocked) {
2682
+ return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
2683
+ }
2684
+ if (opts.all) return cb(null, list);
2685
+ const first = list[0];
2686
+ if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
2687
+ return cb(null, first.address, first.family);
2688
+ });
2689
+ } catch {
2690
+ try {
2691
+ lookup(hostname, opts, cb);
2692
+ } catch {
2693
+ cb(new Error(`Patchstack: lookup failed for ${hostname}`));
2694
+ }
2695
+ }
2696
+ };
2697
+ return injectLookupOption(args, screeningLookup);
2698
+ }
2699
+ function injectLookupOption(args, lookup) {
2700
+ const first = args[0];
2701
+ if (first && typeof first === "object" && !(first instanceof URL)) {
2702
+ return [{ ...first, lookup }, ...args.slice(1)];
2703
+ }
2704
+ const rest = args.slice(1);
2705
+ const optIdx = rest.findIndex((a) => a && typeof a === "object" && !(a instanceof URL));
2706
+ if (optIdx !== -1) {
2707
+ const next = [...rest];
2708
+ next[optIdx] = { ...rest[optIdx], lookup };
2709
+ return [first, ...next];
2710
+ }
2711
+ const cbIdx = rest.findIndex((a) => typeof a === "function");
2712
+ if (cbIdx === -1) return [first, { lookup }, ...rest];
2713
+ return [first, ...rest.slice(0, cbIdx), { lookup }, ...rest.slice(cbIdx)];
2714
+ }
1320
2715
 
1321
2716
  // src/protect/defaults.js
1322
2717
  var DEFAULT_RESPONSE_RULES = [
@@ -1470,20 +2865,200 @@ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
1470
2865
  navigator.clipboard.writeText(copyText.value);
1471
2866
  }
1472
2867
 
1473
- function hover(element) {
1474
- element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
2868
+ function hover(element) {
2869
+ element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
2870
+ }
2871
+
2872
+ function unhover(element) {
2873
+ element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
2874
+ }
2875
+ </script>
2876
+ </html>`;
2877
+ }
2878
+
2879
+ // src/protect/rules/store.js
2880
+ var import_node_fs = require("fs");
2881
+ var import_node_path = require("path");
2882
+ function makeStore(options = {}) {
2883
+ let mem = null;
2884
+ const durable = durableTier(options);
2885
+ return {
2886
+ async read() {
2887
+ if (mem) return mem;
2888
+ const env = await durable.read();
2889
+ if (env) mem = env;
2890
+ return env;
2891
+ },
2892
+ async write(env) {
2893
+ if (env) mem = env;
2894
+ await durable.write(env);
2895
+ }
2896
+ };
2897
+ }
2898
+ function durableTier(options) {
2899
+ const adapter = options.ruleCache;
2900
+ if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
2901
+ return {
2902
+ read: async () => {
2903
+ try {
2904
+ return toEnvelope(await adapter.read());
2905
+ } catch {
2906
+ return null;
2907
+ }
2908
+ },
2909
+ write: async (env) => {
2910
+ try {
2911
+ await adapter.write(env);
2912
+ } catch {
2913
+ }
2914
+ }
2915
+ };
2916
+ }
2917
+ const dir = options.cacheDir;
2918
+ return {
2919
+ read: async () => cacheRead(dir),
2920
+ write: async (env) => cacheWrite(dir, env)
2921
+ };
2922
+ }
2923
+ function cachePath(dir) {
2924
+ return (0, import_node_path.join)(dir, "patchstack-rules.json");
2925
+ }
2926
+ function cacheWrite(dir, env) {
2927
+ if (!dir) return;
2928
+ try {
2929
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
2930
+ (0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
2931
+ } catch {
2932
+ }
2933
+ }
2934
+ function cacheRead(dir) {
2935
+ if (!dir) return null;
2936
+ try {
2937
+ return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
2938
+ } catch {
2939
+ return null;
2940
+ }
2941
+ }
2942
+ function toEnvelope(value) {
2943
+ if (!value || typeof value !== "object") return null;
2944
+ if (value.bundle && typeof value.bundle === "object") {
2945
+ return { bundle: value.bundle, etag: value.etag ?? null };
2946
+ }
2947
+ if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
2948
+ return { bundle: value, etag: null };
2949
+ }
2950
+ return null;
2951
+ }
2952
+
2953
+ // src/protect/rules/source.js
2954
+ async function resolveRules(options, store) {
2955
+ if (options.siteUuid) {
2956
+ const prior = await store.read();
2957
+ const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
2958
+ const res = await client.getRules();
2959
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
2960
+ if (res.success && !res.notModified) {
2961
+ const bundle = normalizeBundle(res);
2962
+ await store.write({ bundle, etag: res.etag ?? null });
2963
+ return bundle;
1475
2964
  }
1476
-
1477
- function unhover(element) {
1478
- element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
2965
+ if (prior?.bundle) {
2966
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
2967
+ return normalizeBundle(prior.bundle);
1479
2968
  }
1480
- </script>
1481
- </html>`;
2969
+ if (options.rules) {
2970
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
2971
+ return normalizeBundle(options.rules);
2972
+ }
2973
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
2974
+ return emptyBundle();
2975
+ }
2976
+ if (options.token) {
2977
+ const prior = await store.read();
2978
+ const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
2979
+ const res = await client.getRules();
2980
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
2981
+ if (res.success && !res.notModified) {
2982
+ const bundle = normalizeBundle(res);
2983
+ await store.write({ bundle, etag: res.etag ?? null });
2984
+ return bundle;
2985
+ }
2986
+ if (prior?.bundle) {
2987
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
2988
+ return normalizeBundle(prior.bundle);
2989
+ }
2990
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
2991
+ return emptyBundle();
2992
+ }
2993
+ if (options.rules) {
2994
+ return normalizeBundle(options.rules);
2995
+ }
2996
+ return emptyBundle();
2997
+ }
2998
+ function normalizeBundle(b) {
2999
+ return {
3000
+ firewall: Array.isArray(b.firewall) ? b.firewall : [],
3001
+ whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
3002
+ whitelist_keys: b.whitelist_keys ?? {}
3003
+ };
3004
+ }
3005
+ function emptyBundle() {
3006
+ return { firewall: [], whitelists: [], whitelist_keys: {} };
1482
3007
  }
1483
3008
 
1484
- // src/protect/runtime.js
1485
- var import_node_fs = require("fs");
1486
- var import_node_path = require("path");
3009
+ // src/protect/rules/refresh.js
3010
+ var JITTER_FRACTION3 = 0.1;
3011
+ var MAX_BACKOFF_MULTIPLIER = 8;
3012
+ function startRefresh(tick, { refreshMs, onError } = {}) {
3013
+ let stopped = false;
3014
+ let failures = 0;
3015
+ let timer = null;
3016
+ const schedule = () => {
3017
+ if (stopped) return;
3018
+ const backoff = Math.min(2 ** failures, MAX_BACKOFF_MULTIPLIER);
3019
+ const jitter = 1 - Math.random() * JITTER_FRACTION3;
3020
+ const delay = refreshMs * backoff * jitter;
3021
+ timer = setTimeout(run, delay);
3022
+ timer?.unref?.();
3023
+ };
3024
+ const run = async () => {
3025
+ if (stopped) return;
3026
+ try {
3027
+ await tick();
3028
+ failures = 0;
3029
+ } catch (err) {
3030
+ failures++;
3031
+ onError?.(err);
3032
+ }
3033
+ schedule();
3034
+ };
3035
+ schedule();
3036
+ return {
3037
+ stop() {
3038
+ stopped = true;
3039
+ if (timer) clearTimeout(timer);
3040
+ }
3041
+ };
3042
+ }
3043
+ function makeRefreshHandler(tick, secret) {
3044
+ return async (request) => {
3045
+ if (!secret) return new Response("not found", { status: 404 });
3046
+ let provided = null;
3047
+ try {
3048
+ provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
3049
+ } catch {
3050
+ provided = null;
3051
+ }
3052
+ if (provided !== secret) return new Response("forbidden", { status: 403 });
3053
+ let refreshed = true;
3054
+ try {
3055
+ await tick();
3056
+ } catch {
3057
+ refreshed = false;
3058
+ }
3059
+ return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
3060
+ };
3061
+ }
1487
3062
 
1488
3063
  // src/protect/supabase-guard.js
1489
3064
  var GUARD_PATH = "/_patchstack/guard";
@@ -1559,23 +3134,35 @@ async function createProtection(options = {}) {
1559
3134
  const mode = options.mode === "block" ? "block" : "dry-run";
1560
3135
  const onError = options.onError;
1561
3136
  const onDetect = options.onDetect ?? defaultOnDetect;
1562
- const bundle = await resolveRules(options);
1563
- const incoming = bundle.firewall ?? [];
1564
- const requestRules = byPhase(incoming, "request");
1565
- const responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
1566
- const egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
1567
- const engine = new RuleEngine({
1568
- firewall: requestRules,
1569
- whitelists: bundle.whitelists,
1570
- whitelist_keys: bundle.whitelist_keys,
1571
- onError
1572
- });
1573
- const responseRuleSet = responseRules.map((rule) => ({
1574
- rule,
1575
- engine: new RuleEngine({ firewall: [rule], onError }),
1576
- redactors: rule.action === "redact" ? extractRedactors(rule) : null
1577
- }));
1578
- const egressEngine = new RuleEngine({ firewall: egressRules, onError });
3137
+ const store = makeStore(options);
3138
+ const bundle = await resolveRules(options, store);
3139
+ let requestRules;
3140
+ let responseRules;
3141
+ let egressRules;
3142
+ let screenCap;
3143
+ let engine;
3144
+ let responseRuleSet;
3145
+ let egressEngine;
3146
+ const applyBundle = (delivered) => {
3147
+ const incoming = delivered.firewall ?? [];
3148
+ requestRules = byPhase(incoming, "request");
3149
+ responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
3150
+ screenCap = responseScreenCap(responseRules);
3151
+ egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
3152
+ engine = new RuleEngine({
3153
+ firewall: requestRules,
3154
+ whitelists: delivered.whitelists,
3155
+ whitelist_keys: delivered.whitelist_keys,
3156
+ onError
3157
+ });
3158
+ responseRuleSet = responseRules.map((rule) => ({
3159
+ rule,
3160
+ engine: new RuleEngine({ firewall: [rule], onError }),
3161
+ redactors: rule.action === "redact" || rule.action === "encode" ? extractRedactors(rule) : null
3162
+ }));
3163
+ egressEngine = new RuleEngine({ firewall: egressRules, onError });
3164
+ };
3165
+ applyBundle(bundle);
1579
3166
  const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
1580
3167
  const decide = (phase, result, block, allow2) => {
1581
3168
  if (!result || !result.blocked) return allow2();
@@ -1609,20 +3196,26 @@ async function createProtection(options = {}) {
1609
3196
  const headers = { ...meta.headers || {} };
1610
3197
  for (const { rule, redactors } of redactions) {
1611
3198
  const mask = maskFn(rule.category);
1612
- body = applyRedactors(body, redactors, mask);
3199
+ const transform = rule.action === "encode" ? htmlEscape : null;
3200
+ const pathRedactors = redactors.filter((r) => r.jsonPath);
3201
+ const spanRedactors = redactors.filter((r) => !r.jsonPath);
3202
+ if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
3203
+ if (!spanRedactors.length) continue;
3204
+ body = applyRedactors(body, spanRedactors, mask, transform);
3205
+ if (transform) continue;
1613
3206
  for (const name of Object.keys(headers)) {
1614
3207
  const value = headers[name];
1615
3208
  if (typeof value === "string") {
1616
- headers[name] = applyRedactors(value, redactors, mask);
3209
+ headers[name] = applyRedactors(value, spanRedactors, mask);
1617
3210
  } else if (Array.isArray(value)) {
1618
- headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, redactors, mask) : item);
3211
+ headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item);
1619
3212
  }
1620
3213
  }
1621
3214
  }
1622
3215
  return { verdict: "redact", body, headers };
1623
3216
  };
1624
3217
  const screenResp = async (response) => {
1625
- const text = await readTextResponse(response);
3218
+ const text = await readTextResponse(response, screenCap);
1626
3219
  if (text == null) return response;
1627
3220
  const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
1628
3221
  if (r.verdict === "block") return leakResponse();
@@ -1635,12 +3228,15 @@ async function createProtection(options = {}) {
1635
3228
  const chunks = [];
1636
3229
  let size = 0;
1637
3230
  let overflow = false;
1638
- const MAX = 512 * 1024;
3231
+ const MAX = screenCap;
1639
3232
  const collect = (chunk, enc) => {
1640
3233
  if (chunk == null) return;
1641
3234
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
1642
3235
  size += buf.length;
1643
3236
  if (size > MAX) {
3237
+ for (const c of chunks) origWrite(c);
3238
+ chunks.length = 0;
3239
+ origWrite(buf);
1644
3240
  overflow = true;
1645
3241
  return;
1646
3242
  }
@@ -1667,6 +3263,7 @@ async function createProtection(options = {}) {
1667
3263
  return origEnd(cb);
1668
3264
  }
1669
3265
  collect(chunk, enc);
3266
+ if (overflow) return origEnd(cb);
1670
3267
  const text = Buffer.concat(chunks).toString("utf8");
1671
3268
  let ct = res.getHeader ? res.getHeader("content-type") : void 0;
1672
3269
  if (Array.isArray(ct)) ct = ct[0];
@@ -1698,6 +3295,7 @@ async function createProtection(options = {}) {
1698
3295
  if (r.headers && res.setHeader) {
1699
3296
  const current = res.getHeaders ? res.getHeaders() : {};
1700
3297
  for (const [name, value] of Object.entries(r.headers)) {
3298
+ if (name.toLowerCase() === "content-length") continue;
1701
3299
  if (Array.isArray(value)) {
1702
3300
  try {
1703
3301
  res.setHeader(name, value);
@@ -1733,7 +3331,9 @@ async function createProtection(options = {}) {
1733
3331
  };
1734
3332
  const protection = {
1735
3333
  mode,
1736
- rules: { request: requestRules, response: responseRules, egress: egressRules },
3334
+ get rules() {
3335
+ return { request: requestRules, response: responseRules, egress: egressRules };
3336
+ },
1737
3337
  // Screen a fetch Response through the response-phase rules (redact/block). Used by
1738
3338
  // .fetch(), and by the Supabase guard on its forwarded upstream response.
1739
3339
  screenResponse: (response) => screenResp(response),
@@ -1844,27 +3444,106 @@ async function createProtection(options = {}) {
1844
3444
  }
1845
3445
  };
1846
3446
  if (options.egress) {
1847
- protection.uninstallEgress = await installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
3447
+ protection.uninstallEgress = await installEgressGuard({
3448
+ shouldBlock: egressShouldBlock,
3449
+ onBlock: options.onEgressBlock,
3450
+ dnsScreen: options.screenDns !== false,
3451
+ allowHosts: options.allowHosts
3452
+ });
3453
+ }
3454
+ const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
3455
+ const live = Boolean(options.siteUuid || options.token);
3456
+ const refreshSecret = options.refreshSecret ?? (typeof process !== "undefined" ? process.env.PATCHSTACK_REFRESH_SECRET : void 0);
3457
+ const refreshable = live && (options.refreshMs > 0 || Boolean(refreshSecret));
3458
+ let reporter = null;
3459
+ if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
3460
+ try {
3461
+ ({ reportManifest: reporter } = await Promise.resolve().then(() => (init_refresh_manifest(), refresh_manifest_exports)));
3462
+ } catch (err) {
3463
+ onError?.(err);
3464
+ }
3465
+ }
3466
+ const runRefreshTick = async () => {
3467
+ if (reporter) {
3468
+ try {
3469
+ await reporter(cwd);
3470
+ } catch (err) {
3471
+ onError?.(err);
3472
+ }
3473
+ }
3474
+ applyBundle(await resolveRules(options, store));
3475
+ };
3476
+ if (live) {
3477
+ protection.refresh = () => runRefreshTick();
3478
+ protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
3479
+ }
3480
+ if (options.refreshMs > 0 && live) {
3481
+ const loop = startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError });
3482
+ protection.stopRefresh = loop.stop;
1848
3483
  }
1849
3484
  return protection;
1850
3485
  }
1851
3486
  function byPhase(rules, phase) {
1852
3487
  return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
1853
3488
  }
1854
- async function readTextResponse(response) {
3489
+ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
1855
3490
  if (!response || typeof response.clone !== "function") return null;
1856
3491
  const ct = (response.headers?.get?.("content-type") || "").toLowerCase();
1857
3492
  const isText = ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
1858
3493
  if (!isText) return null;
1859
3494
  const len = Number(response.headers?.get?.("content-length") || 0);
1860
- if (len && len > 512 * 1024) return null;
3495
+ if (len && len > cap) return null;
3496
+ let clone;
3497
+ try {
3498
+ clone = response.clone();
3499
+ } catch {
3500
+ return null;
3501
+ }
3502
+ const body = clone.body;
3503
+ if (body && typeof body.getReader === "function") {
3504
+ const reader = body.getReader();
3505
+ const chunks = [];
3506
+ let size = 0;
3507
+ let over = false;
3508
+ try {
3509
+ for (; ; ) {
3510
+ const { done, value } = await reader.read();
3511
+ if (done) break;
3512
+ if (!value) continue;
3513
+ size += value.byteLength;
3514
+ if (over) continue;
3515
+ if (size > cap) {
3516
+ over = true;
3517
+ continue;
3518
+ }
3519
+ chunks.push(value);
3520
+ }
3521
+ } catch {
3522
+ return null;
3523
+ }
3524
+ if (over) return null;
3525
+ try {
3526
+ return new TextDecoder().decode(concatBytes(chunks, size));
3527
+ } catch {
3528
+ return null;
3529
+ }
3530
+ }
1861
3531
  try {
1862
- const text = await response.clone().text();
1863
- return text.length > 512 * 1024 ? null : text;
3532
+ const text = await clone.text();
3533
+ return text.length > cap ? null : text;
1864
3534
  } catch {
1865
3535
  return null;
1866
3536
  }
1867
3537
  }
3538
+ function concatBytes(chunks, total) {
3539
+ const out = new Uint8Array(total);
3540
+ let offset = 0;
3541
+ for (const c of chunks) {
3542
+ out.set(c, offset);
3543
+ offset += c.byteLength;
3544
+ }
3545
+ return out;
3546
+ }
1868
3547
  function headerObject(headers) {
1869
3548
  const out = {};
1870
3549
  headers?.forEach?.((v, k) => {
@@ -1876,36 +3555,143 @@ function headerObject(headers) {
1876
3555
  }
1877
3556
  function extractRedactors(rule) {
1878
3557
  const out = [];
1879
- const walk = (conds) => {
3558
+ const walk2 = (conds) => {
1880
3559
  for (const c of conds ?? []) {
1881
- if (Array.isArray(c.rules)) walk(c.rules);
3560
+ if (Array.isArray(c.rules)) walk2(c.rules);
1882
3561
  const m = c.match;
1883
3562
  if (!m) continue;
1884
3563
  if (m.type === "regex" && typeof m.value === "string") {
1885
- const parsed = m.value.match(/^\/(.+)\/([a-z]*)$/is);
1886
- if (parsed) {
1887
- const flags = parsed[2].includes("g") ? parsed[2] : parsed[2] + "g";
3564
+ const safe = safeRegExp(m.value);
3565
+ if (safe) {
3566
+ const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
1888
3567
  try {
1889
- out.push({ re: new RegExp(parsed[1], flags) });
3568
+ out.push({ re: new RegExp(safe.source, flags) });
1890
3569
  } catch {
1891
3570
  }
1892
3571
  }
1893
3572
  } else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
1894
3573
  out.push({ literal: String(m.value) });
3574
+ } else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
3575
+ const keys = Array.isArray(m.key) ? m.key : [m.key];
3576
+ for (const key of keys) {
3577
+ out.push({ jsonPath: String(key).split("."), condition: m.match });
3578
+ }
1895
3579
  }
1896
3580
  }
1897
3581
  };
1898
- walk(rule.rule_v2);
3582
+ walk2(rule.rule_v2);
1899
3583
  return out;
1900
3584
  }
1901
- function applyRedactors(body, redactors, mask) {
3585
+ function htmlEscape(str) {
3586
+ return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
3587
+ }
3588
+ function applyRedactors(body, redactors, mask, transform) {
1902
3589
  let out = body;
1903
3590
  for (const r of redactors) {
1904
- if (r.re) out = out.replace(r.re, mask);
1905
- else if (r.literal) out = out.split(r.literal).join(mask);
3591
+ if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
3592
+ else if (r.literal) out = out.split(r.literal).join(transform ? transform(r.literal) : mask);
3593
+ }
3594
+ return out;
3595
+ }
3596
+ function isBodyParam(parameter) {
3597
+ return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
3598
+ }
3599
+ function conditionPredicate(condition) {
3600
+ if (!condition || !condition.type) return () => true;
3601
+ return (value) => {
3602
+ try {
3603
+ return matchValue(condition.type, value, condition.value, condition);
3604
+ } catch {
3605
+ return false;
3606
+ }
3607
+ };
3608
+ }
3609
+ var DEFAULT_SCREEN_CAP = 512 * 1024;
3610
+ function responseScreenCap(rules) {
3611
+ let cap = DEFAULT_SCREEN_CAP;
3612
+ for (const r of rules ?? []) {
3613
+ if (r && r.bypass_limit === true) return Infinity;
3614
+ const n = Number(r && r.max_bytes);
3615
+ if (Number.isFinite(n) && n > cap) cap = n;
3616
+ }
3617
+ return cap;
3618
+ }
3619
+ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
3620
+ if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
3621
+ const head = text.trimStart()[0];
3622
+ if (head !== "{" && head !== "[") return text;
3623
+ const preserved = preserveBigInts(text);
3624
+ let obj;
3625
+ try {
3626
+ obj = JSON.parse(preserved);
3627
+ } catch {
3628
+ return text;
3629
+ }
3630
+ let changed = false;
3631
+ for (const r of pathRedactors) {
3632
+ const pred = conditionPredicate(r.condition);
3633
+ walkLeaves(obj, r.jsonPath, (loc) => {
3634
+ try {
3635
+ if (pred(loc.value)) {
3636
+ loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
3637
+ changed = true;
3638
+ }
3639
+ } catch {
3640
+ }
3641
+ });
3642
+ }
3643
+ return changed ? restoreBigInts(JSON.stringify(obj)) : text;
3644
+ }
3645
+ var BIGINT_OPEN = "__PSBIGINT_9c2f__";
3646
+ var BIGINT_CLOSE = "__DNEGIB__";
3647
+ function preserveBigInts(text) {
3648
+ let out = "";
3649
+ let inStr = false;
3650
+ for (let i = 0; i < text.length; ) {
3651
+ const ch = text[i];
3652
+ if (inStr) {
3653
+ out += ch;
3654
+ if (ch === "\\") {
3655
+ out += text[i + 1] ?? "";
3656
+ i += 2;
3657
+ continue;
3658
+ }
3659
+ if (ch === '"') inStr = false;
3660
+ i++;
3661
+ continue;
3662
+ }
3663
+ if (ch === '"') {
3664
+ inStr = true;
3665
+ out += ch;
3666
+ i++;
3667
+ continue;
3668
+ }
3669
+ if (ch === "-" || ch >= "0" && ch <= "9") {
3670
+ let j = ch === "-" ? i + 1 : i;
3671
+ let digits = 0;
3672
+ while (j < text.length && text[j] >= "0" && text[j] <= "9") {
3673
+ digits++;
3674
+ j++;
3675
+ }
3676
+ const next = text[j];
3677
+ const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
3678
+ if (isIntToken && digits >= 16) {
3679
+ out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
3680
+ } else {
3681
+ out += text.slice(i, j || i + 1);
3682
+ }
3683
+ i = j > i ? j : i + 1;
3684
+ continue;
3685
+ }
3686
+ out += ch;
3687
+ i++;
1906
3688
  }
1907
3689
  return out;
1908
3690
  }
3691
+ function restoreBigInts(text) {
3692
+ if (!text.includes(BIGINT_OPEN)) return text;
3693
+ return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
3694
+ }
1909
3695
  function rebuildResponse(response, body, redactedHeaders) {
1910
3696
  const headers = new Headers(response.headers);
1911
3697
  headers.delete("content-length");
@@ -1935,115 +3721,6 @@ function leakResponse() {
1935
3721
  headers: { "content-type": "application/json" }
1936
3722
  });
1937
3723
  }
1938
- async function resolveRules(options) {
1939
- const cache = makeCache(options);
1940
- if (options.siteUuid) {
1941
- const prior = await cache.read();
1942
- const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
1943
- const res = await client.getRules();
1944
- if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
1945
- if (res.success && !res.notModified) {
1946
- const bundle = normalizeBundle(res);
1947
- await cache.write({ bundle, etag: res.etag ?? null });
1948
- return bundle;
1949
- }
1950
- if (prior?.bundle) {
1951
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
1952
- return normalizeBundle(prior.bundle);
1953
- }
1954
- if (options.rules) {
1955
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
1956
- return normalizeBundle(options.rules);
1957
- }
1958
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
1959
- return emptyBundle();
1960
- }
1961
- if (options.token) {
1962
- const prior = await cache.read();
1963
- const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
1964
- const res = await client.getRules();
1965
- if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
1966
- if (res.success && !res.notModified) {
1967
- const bundle = normalizeBundle(res);
1968
- await cache.write({ bundle, etag: res.etag ?? null });
1969
- return bundle;
1970
- }
1971
- if (prior?.bundle) {
1972
- options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
1973
- return normalizeBundle(prior.bundle);
1974
- }
1975
- options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
1976
- return emptyBundle();
1977
- }
1978
- if (options.rules) {
1979
- return normalizeBundle(options.rules);
1980
- }
1981
- return emptyBundle();
1982
- }
1983
- function normalizeBundle(b) {
1984
- return {
1985
- firewall: Array.isArray(b.firewall) ? b.firewall : [],
1986
- whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
1987
- whitelist_keys: b.whitelist_keys ?? {}
1988
- };
1989
- }
1990
- function emptyBundle() {
1991
- return { firewall: [], whitelists: [], whitelist_keys: {} };
1992
- }
1993
- function makeCache(options) {
1994
- const adapter = options.ruleCache;
1995
- if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
1996
- return {
1997
- read: async () => {
1998
- try {
1999
- return toEnvelope(await adapter.read());
2000
- } catch {
2001
- return null;
2002
- }
2003
- },
2004
- write: async (env) => {
2005
- try {
2006
- await adapter.write(env);
2007
- } catch {
2008
- }
2009
- }
2010
- };
2011
- }
2012
- const dir = options.cacheDir;
2013
- return {
2014
- read: async () => cacheRead(dir),
2015
- write: async (env) => cacheWrite(dir, env)
2016
- };
2017
- }
2018
- function cachePath(dir) {
2019
- return (0, import_node_path.join)(dir, "patchstack-rules.json");
2020
- }
2021
- function cacheWrite(dir, env) {
2022
- if (!dir) return;
2023
- try {
2024
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
2025
- (0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
2026
- } catch {
2027
- }
2028
- }
2029
- function cacheRead(dir) {
2030
- if (!dir) return null;
2031
- try {
2032
- return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
2033
- } catch {
2034
- return null;
2035
- }
2036
- }
2037
- function toEnvelope(value) {
2038
- if (!value || typeof value !== "object") return null;
2039
- if (value.bundle && typeof value.bundle === "object") {
2040
- return { bundle: value.bundle, etag: value.etag ?? null };
2041
- }
2042
- if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
2043
- return { bundle: value, etag: null };
2044
- }
2045
- return null;
2046
- }
2047
3724
  var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
2048
3725
  function blockBody(result) {
2049
3726
  return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };