@patchstack/connect 0.3.20 → 0.3.22

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
@@ -164,7 +164,7 @@ async function resolveConfig(options) {
164
164
  };
165
165
  }
166
166
  async function readConfigFile(cwd) {
167
- const target = import_node_path.default.join(cwd, CONFIG_FILENAME);
167
+ const target = import_node_path2.default.join(cwd, CONFIG_FILENAME);
168
168
  let raw;
169
169
  try {
170
170
  raw = await (0, import_promises.readFile)(target, "utf8");
@@ -215,12 +215,12 @@ function isUuid(value) {
215
215
  function isEnvironment(value) {
216
216
  return value === "production" || value === "sandbox";
217
217
  }
218
- var import_promises, import_node_path, CONFIG_FILENAME, DEFAULT_ENVIRONMENT;
218
+ var import_promises, import_node_path2, CONFIG_FILENAME, DEFAULT_ENVIRONMENT;
219
219
  var init_config = __esm({
220
220
  "src/config.ts"() {
221
221
  "use strict";
222
222
  import_promises = require("fs/promises");
223
- import_node_path = __toESM(require("path"), 1);
223
+ import_node_path2 = __toESM(require("path"), 1);
224
224
  init_types();
225
225
  init_client();
226
226
  CONFIG_FILENAME = ".patchstackrc.json";
@@ -232,7 +232,7 @@ var init_config = __esm({
232
232
  async function readDeclaredDependencyNames(cwd) {
233
233
  let raw;
234
234
  try {
235
- raw = await (0, import_promises2.readFile)(import_node_path2.default.join(cwd, "package.json"), "utf8");
235
+ raw = await (0, import_promises2.readFile)(import_node_path3.default.join(cwd, "package.json"), "utf8");
236
236
  } catch {
237
237
  return [];
238
238
  }
@@ -264,12 +264,12 @@ function missingDependencies(declared, packages) {
264
264
  const present = new Set(packages.map((entry) => entry.name));
265
265
  return declared.filter((name) => !present.has(name));
266
266
  }
267
- var import_promises2, import_node_path2;
267
+ var import_promises2, import_node_path3;
268
268
  var init_consistency = __esm({
269
269
  "src/parsers/consistency.ts"() {
270
270
  "use strict";
271
271
  import_promises2 = require("fs/promises");
272
- import_node_path2 = __toESM(require("path"), 1);
272
+ import_node_path3 = __toESM(require("path"), 1);
273
273
  }
274
274
  });
275
275
 
@@ -335,7 +335,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
335
335
  return acc;
336
336
  }
337
337
  function extractNameFromPath(pkgPath) {
338
- const segments = pkgPath.split("node_modules" + import_node_path3.default.sep === pkgPath ? import_node_path3.default.sep : "/");
338
+ const segments = pkgPath.split("node_modules" + import_node_path4.default.sep === pkgPath ? import_node_path4.default.sep : "/");
339
339
  const parts = pkgPath.split("/");
340
340
  const nmIndex = parts.lastIndexOf("node_modules");
341
341
  if (nmIndex === -1 || nmIndex >= parts.length - 1) {
@@ -356,19 +356,19 @@ function isDirectV2(pkgPath) {
356
356
  const nmCount = parts.filter((p) => p === "node_modules").length;
357
357
  return nmCount === 1;
358
358
  }
359
- var import_promises3, import_node_path3;
359
+ var import_promises3, import_node_path4;
360
360
  var init_npm = __esm({
361
361
  "src/parsers/npm.ts"() {
362
362
  "use strict";
363
363
  import_promises3 = require("fs/promises");
364
- import_node_path3 = __toESM(require("path"), 1);
364
+ import_node_path4 = __toESM(require("path"), 1);
365
365
  init_types();
366
366
  }
367
367
  });
368
368
 
369
369
  // src/parsers/node_modules.ts
370
370
  async function walkNodeModules(cwd) {
371
- const root = import_node_path4.default.join(cwd, "node_modules");
371
+ const root = import_node_path5.default.join(cwd, "node_modules");
372
372
  try {
373
373
  const info = await (0, import_promises4.stat)(root);
374
374
  if (!info.isDirectory()) {
@@ -402,7 +402,7 @@ async function walk(dir, acc, depth) {
402
402
  if (name.startsWith(".")) {
403
403
  continue;
404
404
  }
405
- const fullPath = import_node_path4.default.join(dir, name);
405
+ const fullPath = import_node_path5.default.join(dir, name);
406
406
  if (!await isPlainDirectory(fullPath)) {
407
407
  continue;
408
408
  }
@@ -417,7 +417,7 @@ async function walk(dir, acc, depth) {
417
417
  if (sub.startsWith(".")) {
418
418
  continue;
419
419
  }
420
- const scopedDir = import_node_path4.default.join(fullPath, sub);
420
+ const scopedDir = import_node_path5.default.join(fullPath, sub);
421
421
  if (!await isPlainDirectory(scopedDir)) {
422
422
  continue;
423
423
  }
@@ -433,7 +433,7 @@ async function walk(dir, acc, depth) {
433
433
  async function readPackage(pkgDir, depth, acc) {
434
434
  let raw;
435
435
  try {
436
- raw = await (0, import_promises4.readFile)(import_node_path4.default.join(pkgDir, "package.json"), "utf8");
436
+ raw = await (0, import_promises4.readFile)(import_node_path5.default.join(pkgDir, "package.json"), "utf8");
437
437
  } catch {
438
438
  return;
439
439
  }
@@ -456,7 +456,7 @@ async function readPackage(pkgDir, depth, acc) {
456
456
  });
457
457
  }
458
458
  async function walkNested(pkgDir, acc, depth) {
459
- const nested = import_node_path4.default.join(pkgDir, "node_modules");
459
+ const nested = import_node_path5.default.join(pkgDir, "node_modules");
460
460
  if (!await isPlainDirectory(nested)) {
461
461
  return;
462
462
  }
@@ -470,12 +470,12 @@ async function isPlainDirectory(dir) {
470
470
  return false;
471
471
  }
472
472
  }
473
- var import_promises4, import_node_path4;
473
+ var import_promises4, import_node_path5;
474
474
  var init_node_modules = __esm({
475
475
  "src/parsers/node_modules.ts"() {
476
476
  "use strict";
477
477
  import_promises4 = require("fs/promises");
478
- import_node_path4 = __toESM(require("path"), 1);
478
+ import_node_path5 = __toESM(require("path"), 1);
479
479
  init_types();
480
480
  }
481
481
  });
@@ -758,7 +758,7 @@ async function parseYarnLockfile(lockfilePath) {
758
758
  "LOCKFILE_PARSE_ERROR"
759
759
  );
760
760
  }
761
- const directNames = await readDirectDepNames(import_node_path5.default.dirname(lockfilePath));
761
+ const directNames = await readDirectDepNames(import_node_path6.default.dirname(lockfilePath));
762
762
  const entries = [];
763
763
  const seen = /* @__PURE__ */ new Set();
764
764
  for (const block of blocks) {
@@ -902,7 +902,7 @@ async function readDirectDepNames(cwd) {
902
902
  const names = /* @__PURE__ */ new Set();
903
903
  let raw;
904
904
  try {
905
- raw = await (0, import_promises6.readFile)(import_node_path5.default.join(cwd, "package.json"), "utf8");
905
+ raw = await (0, import_promises6.readFile)(import_node_path6.default.join(cwd, "package.json"), "utf8");
906
906
  } catch {
907
907
  return names;
908
908
  }
@@ -927,12 +927,12 @@ async function readDirectDepNames(cwd) {
927
927
  }
928
928
  return names;
929
929
  }
930
- var import_promises6, import_node_path5;
930
+ var import_promises6, import_node_path6;
931
931
  var init_yarn = __esm({
932
932
  "src/parsers/yarn.ts"() {
933
933
  "use strict";
934
934
  import_promises6 = require("fs/promises");
935
- import_node_path5 = __toESM(require("path"), 1);
935
+ import_node_path6 = __toESM(require("path"), 1);
936
936
  init_types();
937
937
  }
938
938
  });
@@ -941,7 +941,7 @@ var init_yarn = __esm({
941
941
  async function detectLockfile(cwd) {
942
942
  const probed = await Promise.all(
943
943
  LOCKFILE_CANDIDATES.map(async (candidate) => {
944
- const filePath = import_node_path6.default.join(cwd, candidate.filename);
944
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
945
945
  return { ...candidate, filePath, present: await exists(filePath) };
946
946
  })
947
947
  );
@@ -1019,7 +1019,7 @@ function staleWarning(source, missing) {
1019
1019
  async function presentLockfiles(cwd) {
1020
1020
  const probed = await Promise.all(
1021
1021
  LOCKFILE_CANDIDATES.map(async (candidate) => {
1022
- const filePath = import_node_path6.default.join(cwd, candidate.filename);
1022
+ const filePath = import_node_path7.default.join(cwd, candidate.filename);
1023
1023
  return { ...candidate, filePath, present: await exists(filePath) };
1024
1024
  })
1025
1025
  );
@@ -1045,12 +1045,12 @@ async function exists(filePath) {
1045
1045
  return false;
1046
1046
  }
1047
1047
  }
1048
- var import_promises7, import_node_path6, LOCKFILE_CANDIDATES;
1048
+ var import_promises7, import_node_path7, LOCKFILE_CANDIDATES;
1049
1049
  var init_parsers = __esm({
1050
1050
  "src/parsers/index.ts"() {
1051
1051
  "use strict";
1052
1052
  import_promises7 = require("fs/promises");
1053
- import_node_path6 = __toESM(require("path"), 1);
1053
+ import_node_path7 = __toESM(require("path"), 1);
1054
1054
  init_types();
1055
1055
  init_consistency();
1056
1056
  init_npm();
@@ -2243,7 +2243,8 @@ var PulseRuleClient = class {
2243
2243
  etag: response.headers?.get?.("etag") ?? null,
2244
2244
  firewall: data.firewall,
2245
2245
  whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
2246
- whitelist_keys: data.whitelist_keys ?? {}
2246
+ whitelist_keys: data.whitelist_keys ?? {},
2247
+ ...enforcementField(data)
2247
2248
  };
2248
2249
  this.#cache = result;
2249
2250
  this.#etag = result.etag;
@@ -2269,6 +2270,11 @@ var PulseRuleClient = class {
2269
2270
  this.#etag = null;
2270
2271
  }
2271
2272
  };
2273
+ function enforcementField(data) {
2274
+ if (!data || typeof data !== "object") return {};
2275
+ const v = data.enforcement ?? data.mode;
2276
+ return v === "block" || v === "dry-run" ? { enforcement: v } : {};
2277
+ }
2272
2278
 
2273
2279
  // src/protect/engine/fetch.js
2274
2280
  var MAX_BODY_BYTES = 1024 * 1024;
@@ -2876,9 +2882,191 @@ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
2876
2882
  </html>`;
2877
2883
  }
2878
2884
 
2879
- // src/protect/runtime.js
2885
+ // src/protect/rules/store.js
2880
2886
  var import_node_fs = require("fs");
2881
- var import_node_path7 = require("path");
2887
+ var import_node_path = require("path");
2888
+ function makeStore(options = {}) {
2889
+ let mem = null;
2890
+ const durable = durableTier(options);
2891
+ return {
2892
+ async read() {
2893
+ if (mem) return mem;
2894
+ const env = await durable.read();
2895
+ if (env) mem = env;
2896
+ return env;
2897
+ },
2898
+ async write(env) {
2899
+ if (env) mem = env;
2900
+ await durable.write(env);
2901
+ }
2902
+ };
2903
+ }
2904
+ function durableTier(options) {
2905
+ const adapter = options.ruleCache;
2906
+ if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
2907
+ return {
2908
+ read: async () => {
2909
+ try {
2910
+ return toEnvelope(await adapter.read());
2911
+ } catch {
2912
+ return null;
2913
+ }
2914
+ },
2915
+ write: async (env) => {
2916
+ try {
2917
+ await adapter.write(env);
2918
+ } catch {
2919
+ }
2920
+ }
2921
+ };
2922
+ }
2923
+ const dir = options.cacheDir;
2924
+ return {
2925
+ read: async () => cacheRead(dir),
2926
+ write: async (env) => cacheWrite(dir, env)
2927
+ };
2928
+ }
2929
+ function cachePath(dir) {
2930
+ return (0, import_node_path.join)(dir, "patchstack-rules.json");
2931
+ }
2932
+ function cacheWrite(dir, env) {
2933
+ if (!dir) return;
2934
+ try {
2935
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
2936
+ (0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
2937
+ } catch {
2938
+ }
2939
+ }
2940
+ function cacheRead(dir) {
2941
+ if (!dir) return null;
2942
+ try {
2943
+ return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
2944
+ } catch {
2945
+ return null;
2946
+ }
2947
+ }
2948
+ function toEnvelope(value) {
2949
+ if (!value || typeof value !== "object") return null;
2950
+ if (value.bundle && typeof value.bundle === "object") {
2951
+ return { bundle: value.bundle, etag: value.etag ?? null };
2952
+ }
2953
+ if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
2954
+ return { bundle: value, etag: null };
2955
+ }
2956
+ return null;
2957
+ }
2958
+
2959
+ // src/protect/rules/source.js
2960
+ async function resolveRules(options, store) {
2961
+ if (options.siteUuid) {
2962
+ const prior = await store.read();
2963
+ const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
2964
+ const res = await client.getRules();
2965
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
2966
+ if (res.success && !res.notModified) {
2967
+ const bundle = normalizeBundle(res);
2968
+ await store.write({ bundle, etag: res.etag ?? null });
2969
+ return bundle;
2970
+ }
2971
+ if (prior?.bundle) {
2972
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
2973
+ return normalizeBundle(prior.bundle);
2974
+ }
2975
+ if (options.rules) {
2976
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
2977
+ return normalizeBundle(options.rules);
2978
+ }
2979
+ options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
2980
+ return emptyBundle();
2981
+ }
2982
+ if (options.token) {
2983
+ const prior = await store.read();
2984
+ const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
2985
+ const res = await client.getRules();
2986
+ if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
2987
+ if (res.success && !res.notModified) {
2988
+ const bundle = normalizeBundle(res);
2989
+ await store.write({ bundle, etag: res.etag ?? null });
2990
+ return bundle;
2991
+ }
2992
+ if (prior?.bundle) {
2993
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
2994
+ return normalizeBundle(prior.bundle);
2995
+ }
2996
+ options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
2997
+ return emptyBundle();
2998
+ }
2999
+ if (options.rules) {
3000
+ return normalizeBundle(options.rules);
3001
+ }
3002
+ return emptyBundle();
3003
+ }
3004
+ function normalizeBundle(b) {
3005
+ const enforcement = b?.enforcement ?? b?.mode;
3006
+ return {
3007
+ firewall: Array.isArray(b.firewall) ? b.firewall : [],
3008
+ whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
3009
+ whitelist_keys: b.whitelist_keys ?? {},
3010
+ ...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
3011
+ };
3012
+ }
3013
+ function emptyBundle() {
3014
+ return { firewall: [], whitelists: [], whitelist_keys: {} };
3015
+ }
3016
+
3017
+ // src/protect/rules/refresh.js
3018
+ var JITTER_FRACTION3 = 0.1;
3019
+ var MAX_BACKOFF_MULTIPLIER = 8;
3020
+ function startRefresh(tick, { refreshMs, onError } = {}) {
3021
+ let stopped = false;
3022
+ let failures = 0;
3023
+ let timer = null;
3024
+ const schedule = () => {
3025
+ if (stopped) return;
3026
+ const backoff = Math.min(2 ** failures, MAX_BACKOFF_MULTIPLIER);
3027
+ const jitter = 1 - Math.random() * JITTER_FRACTION3;
3028
+ const delay = refreshMs * backoff * jitter;
3029
+ timer = setTimeout(run, delay);
3030
+ timer?.unref?.();
3031
+ };
3032
+ const run = async () => {
3033
+ if (stopped) return;
3034
+ try {
3035
+ await tick();
3036
+ failures = 0;
3037
+ } catch (err) {
3038
+ failures++;
3039
+ onError?.(err);
3040
+ }
3041
+ schedule();
3042
+ };
3043
+ schedule();
3044
+ return {
3045
+ stop() {
3046
+ stopped = true;
3047
+ if (timer) clearTimeout(timer);
3048
+ }
3049
+ };
3050
+ }
3051
+ function makeRefreshHandler(tick, secret) {
3052
+ return async (request) => {
3053
+ if (!secret) return new Response("not found", { status: 404 });
3054
+ let provided = null;
3055
+ try {
3056
+ provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
3057
+ } catch {
3058
+ provided = null;
3059
+ }
3060
+ if (provided !== secret) return new Response("forbidden", { status: 403 });
3061
+ let refreshed = true;
3062
+ try {
3063
+ await tick();
3064
+ } catch {
3065
+ refreshed = false;
3066
+ }
3067
+ return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
3068
+ };
3069
+ }
2882
3070
 
2883
3071
  // src/protect/supabase-guard.js
2884
3072
  var GUARD_PATH = "/_patchstack/guard";
@@ -2951,10 +3139,11 @@ function createServerFnGuard({ protection }) {
2951
3139
  };
2952
3140
  }
2953
3141
  async function createProtection(options = {}) {
2954
- const mode = options.mode === "block" ? "block" : "dry-run";
2955
3142
  const onError = options.onError;
2956
3143
  const onDetect = options.onDetect ?? defaultOnDetect;
2957
- const bundle = await resolveRules(options);
3144
+ const store = makeStore(options);
3145
+ const bundle = await resolveRules(options, store);
3146
+ let mode = resolveMode(options, bundle);
2958
3147
  let requestRules;
2959
3148
  let responseRules;
2960
3149
  let egressRules;
@@ -3149,7 +3338,9 @@ async function createProtection(options = {}) {
3149
3338
  return mode === "block";
3150
3339
  };
3151
3340
  const protection = {
3152
- mode,
3341
+ get mode() {
3342
+ return mode;
3343
+ },
3153
3344
  get rules() {
3154
3345
  return { request: requestRules, response: responseRules, egress: egressRules };
3155
3346
  },
@@ -3270,33 +3461,48 @@ async function createProtection(options = {}) {
3270
3461
  allowHosts: options.allowHosts
3271
3462
  });
3272
3463
  }
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) {
3464
+ const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
3465
+ const live = Boolean(options.siteUuid || options.token);
3466
+ const refreshSecret = options.refreshSecret ?? (typeof process !== "undefined" ? process.env.PATCHSTACK_REFRESH_SECRET : void 0);
3467
+ const refreshable = live && (options.refreshMs > 0 || Boolean(refreshSecret));
3468
+ let reporter = null;
3469
+ if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
3470
+ try {
3471
+ ({ reportManifest: reporter } = await Promise.resolve().then(() => (init_refresh_manifest(), refresh_manifest_exports)));
3472
+ } catch (err) {
3473
+ onError?.(err);
3474
+ }
3475
+ }
3476
+ const runRefreshTick = async () => {
3477
+ if (reporter) {
3277
3478
  try {
3278
- ({ reportManifest: reportManifest2 } = await Promise.resolve().then(() => (init_refresh_manifest(), refresh_manifest_exports)));
3479
+ await reporter(cwd);
3279
3480
  } catch (err) {
3280
3481
  onError?.(err);
3281
3482
  }
3282
3483
  }
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);
3484
+ const next = await resolveRules(options, store);
3485
+ mode = resolveMode(options, next);
3486
+ applyBundle(next);
3487
+ };
3488
+ if (live) {
3489
+ protection.refresh = () => runRefreshTick();
3490
+ protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
3491
+ }
3492
+ if (options.refreshMs > 0 && live) {
3493
+ const loop = startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError });
3494
+ protection.stopRefresh = loop.stop;
3297
3495
  }
3298
3496
  return protection;
3299
3497
  }
3498
+ function resolveMode(options, bundle) {
3499
+ const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
3500
+ if (env === "block" || env === "dry-run") return env;
3501
+ if (bundle?.enforcement === "block" || bundle?.enforcement === "dry-run") return bundle.enforcement;
3502
+ if (options?.mode === "block") return "block";
3503
+ if (options?.mode === "dry-run") return "dry-run";
3504
+ return "dry-run";
3505
+ }
3300
3506
  function byPhase(rules, phase) {
3301
3507
  return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
3302
3508
  }
@@ -3535,115 +3741,6 @@ function leakResponse() {
3535
3741
  headers: { "content-type": "application/json" }
3536
3742
  });
3537
3743
  }
3538
- async function resolveRules(options) {
3539
- const cache = makeCache(options);
3540
- if (options.siteUuid) {
3541
- const prior = await cache.read();
3542
- const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
3543
- const res = await client.getRules();
3544
- if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
3545
- if (res.success && !res.notModified) {
3546
- const bundle = normalizeBundle(res);
3547
- await cache.write({ bundle, etag: res.etag ?? null });
3548
- return bundle;
3549
- }
3550
- if (prior?.bundle) {
3551
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
3552
- return normalizeBundle(prior.bundle);
3553
- }
3554
- if (options.rules) {
3555
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
3556
- return normalizeBundle(options.rules);
3557
- }
3558
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
3559
- return emptyBundle();
3560
- }
3561
- if (options.token) {
3562
- const prior = await cache.read();
3563
- const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
3564
- const res = await client.getRules();
3565
- if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
3566
- if (res.success && !res.notModified) {
3567
- const bundle = normalizeBundle(res);
3568
- await cache.write({ bundle, etag: res.etag ?? null });
3569
- return bundle;
3570
- }
3571
- if (prior?.bundle) {
3572
- options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
3573
- return normalizeBundle(prior.bundle);
3574
- }
3575
- options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
3576
- return emptyBundle();
3577
- }
3578
- if (options.rules) {
3579
- return normalizeBundle(options.rules);
3580
- }
3581
- return emptyBundle();
3582
- }
3583
- function normalizeBundle(b) {
3584
- return {
3585
- firewall: Array.isArray(b.firewall) ? b.firewall : [],
3586
- whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
3587
- whitelist_keys: b.whitelist_keys ?? {}
3588
- };
3589
- }
3590
- function emptyBundle() {
3591
- return { firewall: [], whitelists: [], whitelist_keys: {} };
3592
- }
3593
- function makeCache(options) {
3594
- const adapter = options.ruleCache;
3595
- if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
3596
- return {
3597
- read: async () => {
3598
- try {
3599
- return toEnvelope(await adapter.read());
3600
- } catch {
3601
- return null;
3602
- }
3603
- },
3604
- write: async (env) => {
3605
- try {
3606
- await adapter.write(env);
3607
- } catch {
3608
- }
3609
- }
3610
- };
3611
- }
3612
- const dir = options.cacheDir;
3613
- return {
3614
- read: async () => cacheRead(dir),
3615
- write: async (env) => cacheWrite(dir, env)
3616
- };
3617
- }
3618
- function cachePath(dir) {
3619
- return (0, import_node_path7.join)(dir, "patchstack-rules.json");
3620
- }
3621
- function cacheWrite(dir, env) {
3622
- if (!dir) return;
3623
- try {
3624
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
3625
- (0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
3626
- } catch {
3627
- }
3628
- }
3629
- function cacheRead(dir) {
3630
- if (!dir) return null;
3631
- try {
3632
- return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
3633
- } catch {
3634
- return null;
3635
- }
3636
- }
3637
- function toEnvelope(value) {
3638
- if (!value || typeof value !== "object") return null;
3639
- if (value.bundle && typeof value.bundle === "object") {
3640
- return { bundle: value.bundle, etag: value.etag ?? null };
3641
- }
3642
- if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
3643
- return { bundle: value, etag: null };
3644
- }
3645
- return null;
3646
- }
3647
3744
  var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
3648
3745
  function blockBody(result) {
3649
3746
  return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };