@patchstack/connect 0.3.27 → 0.3.28

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.js CHANGED
@@ -483,6 +483,35 @@ var RequestResolver = class {
483
483
  }
484
484
  };
485
485
 
486
+ // src/protect/notify.js
487
+ var reported = /* @__PURE__ */ new Set();
488
+ function notify(fn, arg, label) {
489
+ if (typeof fn !== "function") return false;
490
+ try {
491
+ const result = fn(arg);
492
+ if (result !== null && typeof result === "object" && typeof result.then === "function") {
493
+ try {
494
+ result.then(void 0, (err) => warnOnce2(label, err));
495
+ } catch {
496
+ }
497
+ }
498
+ return true;
499
+ } catch (err) {
500
+ warnOnce2(label, err);
501
+ return false;
502
+ }
503
+ }
504
+ function warnOnce2(label, err) {
505
+ if (reported.has(label)) return;
506
+ reported.add(label);
507
+ try {
508
+ console.warn(
509
+ `Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this is reported once per process. Cause: ${err && err.message ? err.message : String(err)}`
510
+ );
511
+ } catch {
512
+ }
513
+ }
514
+
486
515
  // src/protect/engine/normalizer.js
487
516
  var HTML_ENTITIES = {
488
517
  "&": "&",
@@ -767,7 +796,7 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
767
796
  return out;
768
797
  }
769
798
  var warnedKeys = /* @__PURE__ */ new Set();
770
- function warnOnce2(key, message) {
799
+ function warnOnce3(key, message) {
771
800
  if (warnedKeys.has(key)) return;
772
801
  warnedKeys.add(key);
773
802
  console.warn(message);
@@ -1109,21 +1138,20 @@ var RuleEngine = class {
1109
1138
  this.#whitelistKeys = whitelist_keys;
1110
1139
  this.#onError = onError;
1111
1140
  if (Array.isArray(whitelists) && whitelists.some((w) => w && Array.isArray(w.rule_v2) && !w.rule_id)) {
1112
- warnOnce2(
1141
+ warnOnce3(
1113
1142
  "whitelist-no-rule-id",
1114
1143
  "[patchstack] a whitelist has no `rule_id` \u2014 it suppresses ALL rules when it matches. Scope each whitelist to a specific rule_id, and key it only on values an attacker cannot set."
1115
1144
  );
1116
1145
  }
1117
1146
  if (whitelist_keys && typeof whitelist_keys === "object" && Object.keys(whitelist_keys).length > 0) {
1118
- warnOnce2("whitelist-keys-unimplemented", "[patchstack] `whitelist_keys` is not implemented and has no effect.");
1147
+ warnOnce3("whitelist-keys-unimplemented", "[patchstack] `whitelist_keys` is not implemented and has no effect.");
1119
1148
  }
1120
1149
  }
1121
1150
  // A mitigation engine must never take down the app it protects: any error while
1122
1151
  // evaluating a rule is reported and the request is allowed through (fail open). A
1123
1152
  // malformed rule is skipped without aborting the rest of the ruleset.
1124
1153
  #reportError(err) {
1125
- if (this.#onError) {
1126
- this.#onError(err);
1154
+ if (notify(this.#onError, err, "onError")) {
1127
1155
  return;
1128
1156
  }
1129
1157
  const key = err && err.message ? err.message : String(err);
@@ -1268,13 +1296,15 @@ var PulseRuleClient = class {
1268
1296
  #ttlEffective = 0;
1269
1297
  #etag;
1270
1298
  #pulseAuth;
1271
- constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth } = {}) {
1299
+ #reportsDetections;
1300
+ constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, reportsDetections } = {}) {
1272
1301
  this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
1273
1302
  this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
1274
1303
  this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
1275
1304
  this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
1276
1305
  this.#etag = etag ?? null;
1277
1306
  this.#pulseAuth = pulseAuth ?? null;
1307
+ this.#reportsDetections = reportsDetections === true;
1278
1308
  if (!this.#siteUuid) {
1279
1309
  throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
1280
1310
  }
@@ -1286,13 +1316,14 @@ var PulseRuleClient = class {
1286
1316
  }
1287
1317
  const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
1288
1318
  try {
1289
- const headers = {
1290
- Accept: "application/json",
1291
- ...await pulseAuthHeader(
1292
- { pulseAuth: this.#pulseAuth, endpoint: this.#baseUrl, timeoutMs: this.#timeoutMs },
1293
- fetch
1294
- )
1295
- };
1319
+ const auth = await pulseAuthHeader(
1320
+ { pulseAuth: this.#pulseAuth, endpoint: this.#baseUrl, timeoutMs: this.#timeoutMs },
1321
+ fetch
1322
+ );
1323
+ const headers = { Accept: "application/json", ...auth };
1324
+ if (this.#reportsDetections && typeof auth.Authorization === "string") {
1325
+ headers["X-Patchstack-Detections"] = "enabled";
1326
+ }
1296
1327
  if (this.#etag) headers["If-None-Match"] = this.#etag;
1297
1328
  const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
1298
1329
  if (response.status === 304) {
@@ -1574,7 +1605,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
1574
1605
  const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
1575
1606
  const block = (url, host, method) => {
1576
1607
  if (!shouldBlock(url, host, method)) return false;
1577
- onBlock?.({ url, host, method });
1608
+ notify(onBlock, { url, host, method }, "onEgressBlock");
1578
1609
  return true;
1579
1610
  };
1580
1611
  let screen = null;
@@ -1589,12 +1620,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
1589
1620
  screen = null;
1590
1621
  }
1591
1622
  }
1592
- const skip = (reason, detail) => {
1593
- try {
1594
- onSkip?.({ phase: "egress", reason, detail });
1595
- } catch {
1596
- }
1597
- };
1623
+ const skip = (reason, detail) => notify(onSkip, { phase: "egress", reason, detail }, "onSkip");
1598
1624
  const resolvesToDisallowed = (url, host, method) => new Promise((resolve) => {
1599
1625
  if (!screen) {
1600
1626
  if (host && dnsScreen) skip("resolver-unavailable", { host });
@@ -2180,22 +2206,19 @@ function reportRejections(rejected, options, label) {
2180
2206
  const report = options.onRuleRejected;
2181
2207
  for (const r of rejected) {
2182
2208
  if (typeof report === "function") {
2183
- try {
2184
- report({ ...r, accepted: false });
2185
- } catch {
2186
- }
2209
+ notify(report, { ...r, accepted: false }, "onRuleRejected");
2187
2210
  }
2188
2211
  }
2189
2212
  const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
2190
- options.onError?.(new Error(
2213
+ notify(options.onError, new Error(
2191
2214
  `${label}: rejected the entire update because ${rejected.length} rule(s) failed validation \u2014 keeping the previous ruleset and NOT caching this response: ${sample}${rejected.length > 3 ? ", \u2026" : ""}`
2192
- ));
2215
+ ), "onError");
2193
2216
  }
2194
2217
  async function resolveRules(options, store, ctx = {}) {
2195
2218
  const timeoutMs = ctx.timeoutMs;
2196
2219
  if (options.siteUuid) {
2197
2220
  const prior = await store.read();
2198
- const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag, timeoutMs, pulseAuth: ctx.pulseAuth });
2221
+ const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag, timeoutMs, pulseAuth: ctx.pulseAuth, reportsDetections: options.reportDetections === true });
2199
2222
  const res = await client.getRules();
2200
2223
  if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle, options);
2201
2224
  if (res.success && !res.notModified) {
@@ -2211,14 +2234,14 @@ async function resolveRules(options, store, ctx = {}) {
2211
2234
  return bundle;
2212
2235
  }
2213
2236
  if (prior?.bundle) {
2214
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
2237
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
2215
2238
  return normalizeBundle(prior.bundle, options);
2216
2239
  }
2217
2240
  if (options.rules) {
2218
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
2241
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
2219
2242
  return normalizeBundle(options.rules, options);
2220
2243
  }
2221
- options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
2244
+ notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
2222
2245
  return emptyBundle();
2223
2246
  }
2224
2247
  if (options.token) {
@@ -2239,10 +2262,10 @@ async function resolveRules(options, store, ctx = {}) {
2239
2262
  return bundle;
2240
2263
  }
2241
2264
  if (prior?.bundle) {
2242
- options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
2265
+ notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
2243
2266
  return normalizeBundle(prior.bundle, options);
2244
2267
  }
2245
- options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
2268
+ notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
2246
2269
  return emptyBundle();
2247
2270
  }
2248
2271
  if (options.rules) {
@@ -2263,10 +2286,7 @@ function normalizeBundle(b, options = {}) {
2263
2286
  const report = options.onRuleRejected;
2264
2287
  if (typeof report === "function") {
2265
2288
  for (const r of rejected) {
2266
- try {
2267
- report(r);
2268
- } catch {
2269
- }
2289
+ notify(report, r, "onRuleRejected");
2270
2290
  }
2271
2291
  } else {
2272
2292
  const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
@@ -2308,7 +2328,7 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
2308
2328
  failures = 0;
2309
2329
  } catch (err) {
2310
2330
  failures++;
2311
- onError?.(err);
2331
+ notify(onError, err, "onError");
2312
2332
  }
2313
2333
  schedule();
2314
2334
  };
@@ -2340,10 +2360,124 @@ function makeRefreshHandler(tick, secret) {
2340
2360
  };
2341
2361
  }
2342
2362
 
2363
+ // src/protect/detections.js
2364
+ var DEFAULT_BASE_URL3 = "https://api.patchstack.com/monitor/pulse";
2365
+ var DEFAULT_FLUSH_MS = 5e3;
2366
+ var MAX_BATCH = 50;
2367
+ var MAX_QUEUE = 500;
2368
+ function ruleParameters(rule) {
2369
+ const out = /* @__PURE__ */ new Set();
2370
+ const walk = (conditions) => {
2371
+ if (!Array.isArray(conditions)) return;
2372
+ for (const condition of conditions) {
2373
+ if (!condition || typeof condition !== "object") continue;
2374
+ if (typeof condition.parameter === "string" && condition.parameter !== "rules") {
2375
+ out.add(condition.parameter);
2376
+ }
2377
+ if (Array.isArray(condition.rules)) walk(condition.rules);
2378
+ }
2379
+ };
2380
+ walk(rule?.rule_v2);
2381
+ return [...out];
2382
+ }
2383
+ function routeOf(path) {
2384
+ if (typeof path !== "string" || path === "") return null;
2385
+ const cut = path.search(/[?#]/);
2386
+ return cut === -1 ? path : path.slice(0, cut);
2387
+ }
2388
+ function createDetectionReporter(opts) {
2389
+ const siteUuid = opts.siteUuid ?? process.env?.PATCHSTACK_SITE_UUID;
2390
+ if (!siteUuid) {
2391
+ return { record() {
2392
+ }, flush() {
2393
+ }, stop() {
2394
+ }, dropped: () => 0 };
2395
+ }
2396
+ const configured = opts.baseUrl ?? process.env?.PATCHSTACK_PULSE_RULES_URL;
2397
+ const baseUrl = typeof configured === "string" && isSafeOrigin(configured) ? configured.replace(/\/$/, "") : DEFAULT_BASE_URL3;
2398
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
2399
+ const flushMs = Number.isFinite(opts.flushMs) && opts.flushMs > 0 ? opts.flushMs : DEFAULT_FLUSH_MS;
2400
+ const maxQueue = Number.isFinite(opts.maxQueue) && opts.maxQueue > 0 ? opts.maxQueue : MAX_QUEUE;
2401
+ let queue = [];
2402
+ let timer = null;
2403
+ let stopped = false;
2404
+ let dropped = 0;
2405
+ const flush = () => {
2406
+ if (timer) {
2407
+ clearTimeout(timer);
2408
+ timer = null;
2409
+ }
2410
+ if (queue.length === 0 || typeof fetchImpl !== "function") return;
2411
+ const batch = queue.splice(0, MAX_BATCH);
2412
+ const droppedWith = dropped;
2413
+ dropped = 0;
2414
+ void (async () => {
2415
+ try {
2416
+ const res = await fetchImpl(`${baseUrl}/detections/${encodeURIComponent(siteUuid)}`, {
2417
+ method: "POST",
2418
+ headers: {
2419
+ "Content-Type": "application/json",
2420
+ Accept: "application/json",
2421
+ "User-Agent": "@patchstack/connect",
2422
+ // Same credential path as the rules fetch, and unauthenticated when none resolves: the
2423
+ // server accepts the UUID, and reporting must never hinge on getting a token.
2424
+ ...await pulseAuthHeader({ pulseAuth: opts.pulseAuth, endpoint: baseUrl }, fetchImpl)
2425
+ },
2426
+ body: JSON.stringify({ detections: batch, dropped: droppedWith })
2427
+ });
2428
+ if (res && typeof res.then === "function") res.catch(() => {
2429
+ });
2430
+ } catch {
2431
+ }
2432
+ })();
2433
+ };
2434
+ return {
2435
+ /**
2436
+ * @param {{
2437
+ * rule?: { id?: string, rule_v2?: unknown },
2438
+ * phase?: string,
2439
+ * mode?: string,
2440
+ * path?: string,
2441
+ * }} detection
2442
+ */
2443
+ record(detection) {
2444
+ if (stopped) return;
2445
+ const ruleId = detection?.rule?.id;
2446
+ if (ruleId === void 0 || ruleId === null || ruleId === "") return;
2447
+ if (queue.length >= maxQueue) {
2448
+ queue.shift();
2449
+ dropped++;
2450
+ }
2451
+ queue.push({
2452
+ rule_id: ruleId,
2453
+ route: routeOf(detection.path),
2454
+ parameters: ruleParameters(detection.rule),
2455
+ phase: detection.phase ?? null,
2456
+ // The state this detection was handled under, which is the whole point: `false` is a rule that
2457
+ // saw traffic it would have stopped.
2458
+ enforced: detection.mode === "block",
2459
+ rules_etag: opts.rulesEtag ?? null,
2460
+ detected_at: (/* @__PURE__ */ new Date()).toISOString()
2461
+ });
2462
+ if (queue.length >= MAX_BATCH) {
2463
+ flush();
2464
+ return;
2465
+ }
2466
+ if (!timer) timer = setTimeout(flush, flushMs);
2467
+ },
2468
+ flush,
2469
+ stop() {
2470
+ stopped = true;
2471
+ flush();
2472
+ },
2473
+ dropped: () => dropped
2474
+ };
2475
+ }
2476
+
2343
2477
  // src/protect/firewall-log.js
2344
2478
  var DEFAULT_API_BASE = "https://api.patchstack.com";
2345
- var DEFAULT_FLUSH_MS = 1e3;
2346
- var MAX_BATCH = 50;
2479
+ var DEFAULT_FLUSH_MS2 = 1e3;
2480
+ var MAX_BATCH2 = 50;
2347
2481
  var TOKEN_SKEW_MS = 6e4;
2348
2482
  function parseApiKey(apiKey) {
2349
2483
  if (typeof apiKey !== "string" || apiKey.length === 0) return null;
@@ -2388,7 +2522,7 @@ function createFirewallLogReporter(opts) {
2388
2522
  }
2389
2523
  const apiBase = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
2390
2524
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
2391
- const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS;
2525
+ const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
2392
2526
  const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
2393
2527
  let queue = [];
2394
2528
  let timer = null;
@@ -2437,7 +2571,7 @@ function createFirewallLogReporter(opts) {
2437
2571
  timer = null;
2438
2572
  }
2439
2573
  if (queue.length === 0 || typeof fetchImpl !== "function") return;
2440
- const batch = queue.splice(0, MAX_BATCH);
2574
+ const batch = queue.splice(0, MAX_BATCH2);
2441
2575
  void (async () => {
2442
2576
  const token = await fetchAccessToken();
2443
2577
  if (!token) return;
@@ -2484,7 +2618,7 @@ function createFirewallLogReporter(opts) {
2484
2618
  user_agent: event.userAgent ?? null,
2485
2619
  log_date: (/* @__PURE__ */ new Date()).toISOString()
2486
2620
  });
2487
- if (queue.length >= MAX_BATCH) {
2621
+ if (queue.length >= MAX_BATCH2) {
2488
2622
  flush();
2489
2623
  return;
2490
2624
  }
@@ -2583,8 +2717,10 @@ async function createProtection(options = {}) {
2583
2717
  sourceHost: options.sourceHost,
2584
2718
  fetchImpl: options.fetchImpl
2585
2719
  }) : null;
2720
+ let detections = null;
2586
2721
  const onDetect = (detection) => {
2587
- userOnDetect(detection);
2722
+ notify(userOnDetect, detection, "onDetect");
2723
+ if (detections) detections.record(detection);
2588
2724
  if (firewallLog && detection?.mode === "block") {
2589
2725
  firewallLog.record({
2590
2726
  rule: detection.rule,
@@ -2598,7 +2734,24 @@ async function createProtection(options = {}) {
2598
2734
  const store = makeStore(options);
2599
2735
  const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
2600
2736
  const pulseAuth = await resolvePulseAuth(options);
2737
+ if (options.siteUuid && !pulseAuth) {
2738
+ const message = "Patchstack: no API credential resolved for site " + options.siteUuid + ". Rule updates may be rejected and this guard would keep running on its cached rules. Set PATCHSTACK_API_KEY (or pass { pulseAuth }) \u2014 required on runtimes without a filesystem.";
2739
+ notify(onError, new Error(message), "onError");
2740
+ console.warn(message);
2741
+ }
2601
2742
  const bundle = await resolveRules(options, store, { timeoutMs: bootTimeoutMs, pulseAuth });
2743
+ if (options.reportDetections === true && options.siteUuid && telemetryEnabled()) {
2744
+ detections = createDetectionReporter({
2745
+ siteUuid: options.siteUuid,
2746
+ baseUrl: options.pulseRulesUrl,
2747
+ pulseAuth,
2748
+ // The bundle the guard is actually running, so a hit can be attributed to the rules that produced
2749
+ // it rather than to whatever is current when the report is read.
2750
+ rulesEtag: (await store.read())?.etag ?? null,
2751
+ fetchImpl: options.fetchImpl,
2752
+ flushMs: options.detectionFlushMs
2753
+ });
2754
+ }
2602
2755
  let mode = resolveMode(options, bundle);
2603
2756
  let requestRules;
2604
2757
  let responseRules;
@@ -2641,12 +2794,7 @@ async function createProtection(options = {}) {
2641
2794
  const recordSkip = (phase, reason, detail) => {
2642
2795
  const key = `${phase}:${reason}`;
2643
2796
  skipCounts[key] = (skipCounts[key] ?? 0) + 1;
2644
- if (onSkip) {
2645
- try {
2646
- onSkip({ phase, reason, detail, count: skipCounts[key] });
2647
- } catch {
2648
- }
2649
- }
2797
+ notify(onSkip, { phase, reason, detail, count: skipCounts[key] }, "onSkip");
2650
2798
  };
2651
2799
  const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
2652
2800
  const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
@@ -2682,7 +2830,7 @@ async function createProtection(options = {}) {
2682
2830
  try {
2683
2831
  result = re.evaluate({ ...reqCtx || {}, _response: { ...meta, body: text } });
2684
2832
  } catch (err) {
2685
- onError?.(err);
2833
+ notify(onError, err, "onError");
2686
2834
  continue;
2687
2835
  }
2688
2836
  if (!result.blocked) continue;
@@ -2804,7 +2952,7 @@ async function createProtection(options = {}) {
2804
2952
  try {
2805
2953
  r = screenText(text, { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} }, reqCtx);
2806
2954
  } catch (err) {
2807
- onError?.(err);
2955
+ notify(onError, err, "onError");
2808
2956
  for (const c of chunks) origWrite(c);
2809
2957
  return origEnd(cb);
2810
2958
  }
@@ -2856,7 +3004,7 @@ async function createProtection(options = {}) {
2856
3004
  try {
2857
3005
  result = egressEngine.evaluate({ _egress: { url, host, method } });
2858
3006
  } catch (err) {
2859
- onError?.(err);
3007
+ notify(onError, err, "onError");
2860
3008
  return false;
2861
3009
  }
2862
3010
  if (!result.blocked) return false;
@@ -2890,7 +3038,7 @@ async function createProtection(options = {}) {
2890
3038
  try {
2891
3039
  result = engine.evaluate(await fromFetchRequest(request));
2892
3040
  } catch (err) {
2893
- onError?.(err);
3041
+ notify(onError, err, "onError");
2894
3042
  return null;
2895
3043
  }
2896
3044
  return decide("request", result, () => blockResponse(result, request), () => null, fetchRequestMeta(request));
@@ -2914,7 +3062,7 @@ async function createProtection(options = {}) {
2914
3062
  try {
2915
3063
  result = engine.evaluate(req);
2916
3064
  } catch (err) {
2917
- onError?.(err);
3065
+ notify(onError, err, "onError");
2918
3066
  if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req));
2919
3067
  return next();
2920
3068
  }
@@ -2953,7 +3101,7 @@ async function createProtection(options = {}) {
2953
3101
  chunks.push(chunk);
2954
3102
  });
2955
3103
  req.on("error", (err) => {
2956
- onError?.(err);
3104
+ notify(onError, err, "onError");
2957
3105
  next();
2958
3106
  });
2959
3107
  req.on("end", () => {
@@ -2965,7 +3113,7 @@ async function createProtection(options = {}) {
2965
3113
  shaped = fromNodeRequest(req, rawBody);
2966
3114
  result = engine.evaluate(shaped);
2967
3115
  } catch (err) {
2968
- onError?.(err);
3116
+ notify(onError, err, "onError");
2969
3117
  return next();
2970
3118
  }
2971
3119
  decide(
@@ -3010,9 +3158,9 @@ async function createProtection(options = {}) {
3010
3158
  let reporter = null;
3011
3159
  if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
3012
3160
  try {
3013
- ({ reportManifest: reporter } = await import("./refresh-manifest-KZSQD2X3.js"));
3161
+ ({ reportManifest: reporter } = await import("./refresh-manifest-VRBE6RH6.js"));
3014
3162
  } catch (err) {
3015
- onError?.(err);
3163
+ notify(onError, err, "onError");
3016
3164
  }
3017
3165
  }
3018
3166
  const runRefreshTick = async () => {
@@ -3020,7 +3168,7 @@ async function createProtection(options = {}) {
3020
3168
  try {
3021
3169
  await reporter(cwd);
3022
3170
  } catch (err) {
3023
- onError?.(err);
3171
+ notify(onError, err, "onError");
3024
3172
  }
3025
3173
  }
3026
3174
  const next = await resolveRules(options, store, { timeoutMs: options.refreshTimeoutMs, pulseAuth });
@@ -3036,9 +3184,13 @@ async function createProtection(options = {}) {
3036
3184
  protection.stopRefresh = () => {
3037
3185
  loop.stop();
3038
3186
  firewallLog?.stop();
3187
+ detections?.stop();
3039
3188
  };
3040
3189
  } else if (firewallLog) {
3041
- protection.stopRefresh = () => firewallLog.stop();
3190
+ protection.stopRefresh = () => {
3191
+ firewallLog.stop();
3192
+ detections?.stop();
3193
+ };
3042
3194
  }
3043
3195
  return protection;
3044
3196
  }