@patchstack/connect 0.3.18 → 0.3.20

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_path.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_path, 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_path = __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_path2.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_path2;
268
+ var init_consistency = __esm({
269
+ "src/parsers/consistency.ts"() {
270
+ "use strict";
271
+ import_promises2 = require("fs/promises");
272
+ import_node_path2 = __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_path3.default.sep === pkgPath ? import_node_path3.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_path3;
360
+ var init_npm = __esm({
361
+ "src/parsers/npm.ts"() {
362
+ "use strict";
363
+ import_promises3 = require("fs/promises");
364
+ import_node_path3 = __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_path4.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_path4.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_path4.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_path4.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_path4.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_path4;
474
+ var init_node_modules = __esm({
475
+ "src/parsers/node_modules.ts"() {
476
+ "use strict";
477
+ import_promises4 = require("fs/promises");
478
+ import_node_path4 = __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_path5.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_path5.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_path5;
931
+ var init_yarn = __esm({
932
+ "src/parsers/yarn.ts"() {
933
+ "use strict";
934
+ import_promises6 = require("fs/promises");
935
+ import_node_path5 = __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_path6.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_path6.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_path6, LOCKFILE_CANDIDATES;
1049
+ var init_parsers = __esm({
1050
+ "src/parsers/index.ts"() {
1051
+ "use strict";
1052
+ import_promises7 = require("fs/promises");
1053
+ import_node_path6 = __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 = [
@@ -1483,7 +2878,7 @@ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
1483
2878
 
1484
2879
  // src/protect/runtime.js
1485
2880
  var import_node_fs = require("fs");
1486
- var import_node_path = require("path");
2881
+ var import_node_path7 = require("path");
1487
2882
 
1488
2883
  // src/protect/supabase-guard.js
1489
2884
  var GUARD_PATH = "/_patchstack/guard";
@@ -1560,22 +2955,33 @@ async function createProtection(options = {}) {
1560
2955
  const onError = options.onError;
1561
2956
  const onDetect = options.onDetect ?? defaultOnDetect;
1562
2957
  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 });
2958
+ let requestRules;
2959
+ let responseRules;
2960
+ let egressRules;
2961
+ let screenCap;
2962
+ let engine;
2963
+ let responseRuleSet;
2964
+ let egressEngine;
2965
+ const applyBundle = (delivered) => {
2966
+ const incoming = delivered.firewall ?? [];
2967
+ requestRules = byPhase(incoming, "request");
2968
+ responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
2969
+ screenCap = responseScreenCap(responseRules);
2970
+ egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
2971
+ engine = new RuleEngine({
2972
+ firewall: requestRules,
2973
+ whitelists: delivered.whitelists,
2974
+ whitelist_keys: delivered.whitelist_keys,
2975
+ onError
2976
+ });
2977
+ responseRuleSet = responseRules.map((rule) => ({
2978
+ rule,
2979
+ engine: new RuleEngine({ firewall: [rule], onError }),
2980
+ redactors: rule.action === "redact" || rule.action === "encode" ? extractRedactors(rule) : null
2981
+ }));
2982
+ egressEngine = new RuleEngine({ firewall: egressRules, onError });
2983
+ };
2984
+ applyBundle(bundle);
1579
2985
  const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
1580
2986
  const decide = (phase, result, block, allow2) => {
1581
2987
  if (!result || !result.blocked) return allow2();
@@ -1609,20 +3015,26 @@ async function createProtection(options = {}) {
1609
3015
  const headers = { ...meta.headers || {} };
1610
3016
  for (const { rule, redactors } of redactions) {
1611
3017
  const mask = maskFn(rule.category);
1612
- body = applyRedactors(body, redactors, mask);
3018
+ const transform = rule.action === "encode" ? htmlEscape : null;
3019
+ const pathRedactors = redactors.filter((r) => r.jsonPath);
3020
+ const spanRedactors = redactors.filter((r) => !r.jsonPath);
3021
+ if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
3022
+ if (!spanRedactors.length) continue;
3023
+ body = applyRedactors(body, spanRedactors, mask, transform);
3024
+ if (transform) continue;
1613
3025
  for (const name of Object.keys(headers)) {
1614
3026
  const value = headers[name];
1615
3027
  if (typeof value === "string") {
1616
- headers[name] = applyRedactors(value, redactors, mask);
3028
+ headers[name] = applyRedactors(value, spanRedactors, mask);
1617
3029
  } else if (Array.isArray(value)) {
1618
- headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, redactors, mask) : item);
3030
+ headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item);
1619
3031
  }
1620
3032
  }
1621
3033
  }
1622
3034
  return { verdict: "redact", body, headers };
1623
3035
  };
1624
3036
  const screenResp = async (response) => {
1625
- const text = await readTextResponse(response);
3037
+ const text = await readTextResponse(response, screenCap);
1626
3038
  if (text == null) return response;
1627
3039
  const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
1628
3040
  if (r.verdict === "block") return leakResponse();
@@ -1635,12 +3047,15 @@ async function createProtection(options = {}) {
1635
3047
  const chunks = [];
1636
3048
  let size = 0;
1637
3049
  let overflow = false;
1638
- const MAX = 512 * 1024;
3050
+ const MAX = screenCap;
1639
3051
  const collect = (chunk, enc) => {
1640
3052
  if (chunk == null) return;
1641
3053
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
1642
3054
  size += buf.length;
1643
3055
  if (size > MAX) {
3056
+ for (const c of chunks) origWrite(c);
3057
+ chunks.length = 0;
3058
+ origWrite(buf);
1644
3059
  overflow = true;
1645
3060
  return;
1646
3061
  }
@@ -1667,6 +3082,7 @@ async function createProtection(options = {}) {
1667
3082
  return origEnd(cb);
1668
3083
  }
1669
3084
  collect(chunk, enc);
3085
+ if (overflow) return origEnd(cb);
1670
3086
  const text = Buffer.concat(chunks).toString("utf8");
1671
3087
  let ct = res.getHeader ? res.getHeader("content-type") : void 0;
1672
3088
  if (Array.isArray(ct)) ct = ct[0];
@@ -1698,6 +3114,7 @@ async function createProtection(options = {}) {
1698
3114
  if (r.headers && res.setHeader) {
1699
3115
  const current = res.getHeaders ? res.getHeaders() : {};
1700
3116
  for (const [name, value] of Object.entries(r.headers)) {
3117
+ if (name.toLowerCase() === "content-length") continue;
1701
3118
  if (Array.isArray(value)) {
1702
3119
  try {
1703
3120
  res.setHeader(name, value);
@@ -1733,7 +3150,9 @@ async function createProtection(options = {}) {
1733
3150
  };
1734
3151
  const protection = {
1735
3152
  mode,
1736
- rules: { request: requestRules, response: responseRules, egress: egressRules },
3153
+ get rules() {
3154
+ return { request: requestRules, response: responseRules, egress: egressRules };
3155
+ },
1737
3156
  // Screen a fetch Response through the response-phase rules (redact/block). Used by
1738
3157
  // .fetch(), and by the Supabase guard on its forwarded upstream response.
1739
3158
  screenResponse: (response) => screenResp(response),
@@ -1844,27 +3263,101 @@ async function createProtection(options = {}) {
1844
3263
  }
1845
3264
  };
1846
3265
  if (options.egress) {
1847
- protection.uninstallEgress = await installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
3266
+ protection.uninstallEgress = await installEgressGuard({
3267
+ shouldBlock: egressShouldBlock,
3268
+ onBlock: options.onEgressBlock,
3269
+ dnsScreen: options.screenDns !== false,
3270
+ allowHosts: options.allowHosts
3271
+ });
3272
+ }
3273
+ if (options.refreshMs > 0 && (options.siteUuid || options.token)) {
3274
+ const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
3275
+ let reportManifest2 = null;
3276
+ if (options.siteUuid && options.reportManifest !== false && cwd) {
3277
+ try {
3278
+ ({ reportManifest: reportManifest2 } = await Promise.resolve().then(() => (init_refresh_manifest(), refresh_manifest_exports)));
3279
+ } catch (err) {
3280
+ onError?.(err);
3281
+ }
3282
+ }
3283
+ const timer = setInterval(() => {
3284
+ (async () => {
3285
+ if (reportManifest2) {
3286
+ try {
3287
+ await reportManifest2(cwd);
3288
+ } catch (err) {
3289
+ onError?.(err);
3290
+ }
3291
+ }
3292
+ applyBundle(await resolveRules(options));
3293
+ })().catch((err) => onError?.(err));
3294
+ }, options.refreshMs);
3295
+ timer.unref?.();
3296
+ protection.stopRefresh = () => clearInterval(timer);
1848
3297
  }
1849
3298
  return protection;
1850
3299
  }
1851
3300
  function byPhase(rules, phase) {
1852
3301
  return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
1853
3302
  }
1854
- async function readTextResponse(response) {
3303
+ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
1855
3304
  if (!response || typeof response.clone !== "function") return null;
1856
3305
  const ct = (response.headers?.get?.("content-type") || "").toLowerCase();
1857
3306
  const isText = ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
1858
3307
  if (!isText) return null;
1859
3308
  const len = Number(response.headers?.get?.("content-length") || 0);
1860
- if (len && len > 512 * 1024) return null;
3309
+ if (len && len > cap) return null;
3310
+ let clone;
3311
+ try {
3312
+ clone = response.clone();
3313
+ } catch {
3314
+ return null;
3315
+ }
3316
+ const body = clone.body;
3317
+ if (body && typeof body.getReader === "function") {
3318
+ const reader = body.getReader();
3319
+ const chunks = [];
3320
+ let size = 0;
3321
+ let over = false;
3322
+ try {
3323
+ for (; ; ) {
3324
+ const { done, value } = await reader.read();
3325
+ if (done) break;
3326
+ if (!value) continue;
3327
+ size += value.byteLength;
3328
+ if (over) continue;
3329
+ if (size > cap) {
3330
+ over = true;
3331
+ continue;
3332
+ }
3333
+ chunks.push(value);
3334
+ }
3335
+ } catch {
3336
+ return null;
3337
+ }
3338
+ if (over) return null;
3339
+ try {
3340
+ return new TextDecoder().decode(concatBytes(chunks, size));
3341
+ } catch {
3342
+ return null;
3343
+ }
3344
+ }
1861
3345
  try {
1862
- const text = await response.clone().text();
1863
- return text.length > 512 * 1024 ? null : text;
3346
+ const text = await clone.text();
3347
+ return text.length > cap ? null : text;
1864
3348
  } catch {
1865
3349
  return null;
1866
3350
  }
1867
3351
  }
3352
+ function concatBytes(chunks, total) {
3353
+ const out = new Uint8Array(total);
3354
+ let offset = 0;
3355
+ for (const c of chunks) {
3356
+ out.set(c, offset);
3357
+ offset += c.byteLength;
3358
+ }
3359
+ return out;
3360
+ }
1868
3361
  function headerObject(headers) {
1869
3362
  const out = {};
1870
3363
  headers?.forEach?.((v, k) => {
@@ -1876,36 +3369,143 @@ function headerObject(headers) {
1876
3369
  }
1877
3370
  function extractRedactors(rule) {
1878
3371
  const out = [];
1879
- const walk = (conds) => {
3372
+ const walk2 = (conds) => {
1880
3373
  for (const c of conds ?? []) {
1881
- if (Array.isArray(c.rules)) walk(c.rules);
3374
+ if (Array.isArray(c.rules)) walk2(c.rules);
1882
3375
  const m = c.match;
1883
3376
  if (!m) continue;
1884
3377
  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";
3378
+ const safe = safeRegExp(m.value);
3379
+ if (safe) {
3380
+ const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
1888
3381
  try {
1889
- out.push({ re: new RegExp(parsed[1], flags) });
3382
+ out.push({ re: new RegExp(safe.source, flags) });
1890
3383
  } catch {
1891
3384
  }
1892
3385
  }
1893
3386
  } else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
1894
3387
  out.push({ literal: String(m.value) });
3388
+ } else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
3389
+ const keys = Array.isArray(m.key) ? m.key : [m.key];
3390
+ for (const key of keys) {
3391
+ out.push({ jsonPath: String(key).split("."), condition: m.match });
3392
+ }
1895
3393
  }
1896
3394
  }
1897
3395
  };
1898
- walk(rule.rule_v2);
3396
+ walk2(rule.rule_v2);
1899
3397
  return out;
1900
3398
  }
1901
- function applyRedactors(body, redactors, mask) {
3399
+ function htmlEscape(str) {
3400
+ return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
3401
+ }
3402
+ function applyRedactors(body, redactors, mask, transform) {
1902
3403
  let out = body;
1903
3404
  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);
3405
+ if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
3406
+ else if (r.literal) out = out.split(r.literal).join(transform ? transform(r.literal) : mask);
1906
3407
  }
1907
3408
  return out;
1908
3409
  }
3410
+ function isBodyParam(parameter) {
3411
+ return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
3412
+ }
3413
+ function conditionPredicate(condition) {
3414
+ if (!condition || !condition.type) return () => true;
3415
+ return (value) => {
3416
+ try {
3417
+ return matchValue(condition.type, value, condition.value, condition);
3418
+ } catch {
3419
+ return false;
3420
+ }
3421
+ };
3422
+ }
3423
+ var DEFAULT_SCREEN_CAP = 512 * 1024;
3424
+ function responseScreenCap(rules) {
3425
+ let cap = DEFAULT_SCREEN_CAP;
3426
+ for (const r of rules ?? []) {
3427
+ if (r && r.bypass_limit === true) return Infinity;
3428
+ const n = Number(r && r.max_bytes);
3429
+ if (Number.isFinite(n) && n > cap) cap = n;
3430
+ }
3431
+ return cap;
3432
+ }
3433
+ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
3434
+ if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
3435
+ const head = text.trimStart()[0];
3436
+ if (head !== "{" && head !== "[") return text;
3437
+ const preserved = preserveBigInts(text);
3438
+ let obj;
3439
+ try {
3440
+ obj = JSON.parse(preserved);
3441
+ } catch {
3442
+ return text;
3443
+ }
3444
+ let changed = false;
3445
+ for (const r of pathRedactors) {
3446
+ const pred = conditionPredicate(r.condition);
3447
+ walkLeaves(obj, r.jsonPath, (loc) => {
3448
+ try {
3449
+ if (pred(loc.value)) {
3450
+ loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
3451
+ changed = true;
3452
+ }
3453
+ } catch {
3454
+ }
3455
+ });
3456
+ }
3457
+ return changed ? restoreBigInts(JSON.stringify(obj)) : text;
3458
+ }
3459
+ var BIGINT_OPEN = "__PSBIGINT_9c2f__";
3460
+ var BIGINT_CLOSE = "__DNEGIB__";
3461
+ function preserveBigInts(text) {
3462
+ let out = "";
3463
+ let inStr = false;
3464
+ for (let i = 0; i < text.length; ) {
3465
+ const ch = text[i];
3466
+ if (inStr) {
3467
+ out += ch;
3468
+ if (ch === "\\") {
3469
+ out += text[i + 1] ?? "";
3470
+ i += 2;
3471
+ continue;
3472
+ }
3473
+ if (ch === '"') inStr = false;
3474
+ i++;
3475
+ continue;
3476
+ }
3477
+ if (ch === '"') {
3478
+ inStr = true;
3479
+ out += ch;
3480
+ i++;
3481
+ continue;
3482
+ }
3483
+ if (ch === "-" || ch >= "0" && ch <= "9") {
3484
+ let j = ch === "-" ? i + 1 : i;
3485
+ let digits = 0;
3486
+ while (j < text.length && text[j] >= "0" && text[j] <= "9") {
3487
+ digits++;
3488
+ j++;
3489
+ }
3490
+ const next = text[j];
3491
+ const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
3492
+ if (isIntToken && digits >= 16) {
3493
+ out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
3494
+ } else {
3495
+ out += text.slice(i, j || i + 1);
3496
+ }
3497
+ i = j > i ? j : i + 1;
3498
+ continue;
3499
+ }
3500
+ out += ch;
3501
+ i++;
3502
+ }
3503
+ return out;
3504
+ }
3505
+ function restoreBigInts(text) {
3506
+ if (!text.includes(BIGINT_OPEN)) return text;
3507
+ return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
3508
+ }
1909
3509
  function rebuildResponse(response, body, redactedHeaders) {
1910
3510
  const headers = new Headers(response.headers);
1911
3511
  headers.delete("content-length");
@@ -2016,7 +3616,7 @@ function makeCache(options) {
2016
3616
  };
2017
3617
  }
2018
3618
  function cachePath(dir) {
2019
- return (0, import_node_path.join)(dir, "patchstack-rules.json");
3619
+ return (0, import_node_path7.join)(dir, "patchstack-rules.json");
2020
3620
  }
2021
3621
  function cacheWrite(dir, env) {
2022
3622
  if (!dir) return;