@patchstack/connect 0.3.22 → 0.3.23

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.d.ts CHANGED
@@ -48,8 +48,23 @@ export interface CreateProtectionOptions {
48
48
  baseUrl?: string;
49
49
  /** Pulse site UUID — pull live per-site rules from the Pulse rules API (cached). */
50
50
  siteUuid?: string;
51
+ /**
52
+ * WP-format site API key (`{secret}-{oauth.id}`) for authenticated block logs
53
+ * via connector `POST /api/logs/log`. Falls back to `PATCHSTACK_API_KEY`, then
54
+ * `.patchstackrc.json` `apiKey`. Never put this in the public widget.
55
+ */
56
+ apiKey?: string;
51
57
  /** Override the Pulse rules API base URL. */
52
58
  pulseRulesUrl?: string;
59
+ /**
60
+ * When false, skip posting block events to connector `/api/logs/log`.
61
+ * Also disabled when `PATCHSTACK_TELEMETRY=off` or when no apiKey is available.
62
+ */
63
+ reportFirewallLog?: boolean;
64
+ /** Optional Source-Host header for connector hostname checks. */
65
+ sourceHost?: string;
66
+ /** Optional fetch override (tests). */
67
+ fetchImpl?: typeof fetch;
53
68
  /**
54
69
  * Re-fetch and hot-swap the live rules every N ms. For long-lived runtimes that aren't restarted
55
70
  * on change (an AI builder's sandbox/preview) so a rule that becomes relevant after boot still
@@ -104,8 +119,12 @@ export interface CreateProtectionOptions {
104
119
  phase?: Phase;
105
120
  mode: string;
106
121
  category?: string;
107
- rule?: { id?: string; category?: string };
122
+ rule?: { id?: string | number; category?: string };
108
123
  message?: string;
124
+ method?: string | null;
125
+ path?: string | null;
126
+ ip?: string | null;
127
+ userAgent?: string | null;
109
128
  }) => void;
110
129
  }
111
130
 
package/dist/protect.js CHANGED
@@ -1857,6 +1857,169 @@ function makeRefreshHandler(tick, secret) {
1857
1857
  };
1858
1858
  }
1859
1859
 
1860
+ // src/protect/firewall-log.js
1861
+ var DEFAULT_API_BASE = "https://api.patchstack.com";
1862
+ var DEFAULT_FLUSH_MS = 1e3;
1863
+ var MAX_BATCH = 50;
1864
+ var TOKEN_SKEW_MS = 6e4;
1865
+ function parseApiKey(apiKey) {
1866
+ if (typeof apiKey !== "string" || apiKey.length === 0) return null;
1867
+ const idx = apiKey.lastIndexOf("-");
1868
+ if (idx <= 0 || idx === apiKey.length - 1) return null;
1869
+ const clientSecret = apiKey.slice(0, idx);
1870
+ const clientId = apiKey.slice(idx + 1);
1871
+ if (!/^\d+$/.test(clientId) || clientSecret.length === 0) return null;
1872
+ return { clientId, clientSecret };
1873
+ }
1874
+ function resolveApiBase(pulseOrManifestUrl) {
1875
+ const fromEnv = typeof process !== "undefined" ? process.env?.PATCHSTACK_API_BASE : void 0;
1876
+ if (typeof fromEnv === "string" && fromEnv.length > 0) {
1877
+ return fromEnv.replace(/\/$/, "");
1878
+ }
1879
+ if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
1880
+ try {
1881
+ return new URL(pulseOrManifestUrl).origin;
1882
+ } catch {
1883
+ }
1884
+ }
1885
+ const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
1886
+ if (typeof endpoint === "string" && endpoint.length > 0) {
1887
+ try {
1888
+ return new URL(endpoint).origin;
1889
+ } catch {
1890
+ }
1891
+ }
1892
+ return DEFAULT_API_BASE;
1893
+ }
1894
+ function createFirewallLogReporter(opts) {
1895
+ const creds = parseApiKey(opts.apiKey);
1896
+ if (!creds) {
1897
+ return { record() {
1898
+ }, flush() {
1899
+ }, stop() {
1900
+ } };
1901
+ }
1902
+ const apiBase = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
1903
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
1904
+ const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS;
1905
+ const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
1906
+ let queue = [];
1907
+ let timer = null;
1908
+ let stopped = false;
1909
+ let cachedToken = null;
1910
+ let tokenInflight = null;
1911
+ const fetchAccessToken = async () => {
1912
+ if (cachedToken && Date.now() < cachedToken.expiresAt - TOKEN_SKEW_MS) {
1913
+ return cachedToken.token;
1914
+ }
1915
+ if (tokenInflight) return tokenInflight;
1916
+ tokenInflight = (async () => {
1917
+ try {
1918
+ const res = await fetchImpl(`${apiBase}/oauth/token`, {
1919
+ method: "POST",
1920
+ headers: {
1921
+ "Content-Type": "application/json",
1922
+ Accept: "application/json",
1923
+ "User-Agent": "@patchstack/connect"
1924
+ },
1925
+ body: JSON.stringify({
1926
+ grant_type: "client_credentials",
1927
+ client_id: creds.clientId,
1928
+ client_secret: creds.clientSecret
1929
+ })
1930
+ });
1931
+ if (!res || !res.ok) return null;
1932
+ const body = await res.json();
1933
+ const token = body?.access_token;
1934
+ if (typeof token !== "string" || token.length === 0) return null;
1935
+ const expiresIn = Number(body?.expires_in);
1936
+ const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
1937
+ cachedToken = { token, expiresAt: Date.now() + ttlMs };
1938
+ return token;
1939
+ } catch {
1940
+ return null;
1941
+ } finally {
1942
+ tokenInflight = null;
1943
+ }
1944
+ })();
1945
+ return tokenInflight;
1946
+ };
1947
+ const flush = () => {
1948
+ if (timer) {
1949
+ clearTimeout(timer);
1950
+ timer = null;
1951
+ }
1952
+ if (queue.length === 0 || typeof fetchImpl !== "function") return;
1953
+ const batch = queue.splice(0, MAX_BATCH);
1954
+ void (async () => {
1955
+ const token = await fetchAccessToken();
1956
+ if (!token) return;
1957
+ const body = new URLSearchParams();
1958
+ body.set("type", "firewall");
1959
+ body.set("logs", JSON.stringify(batch));
1960
+ try {
1961
+ const p = fetchImpl(`${apiBase}/api/logs/log`, {
1962
+ method: "POST",
1963
+ headers: {
1964
+ Authorization: `Bearer ${token}`,
1965
+ Accept: "application/json",
1966
+ "Content-Type": "application/x-www-form-urlencoded",
1967
+ "User-Agent": "@patchstack/connect",
1968
+ ...sourceHost ? { "Source-Host": sourceHost } : {}
1969
+ },
1970
+ body
1971
+ });
1972
+ if (p && typeof p.then === "function") p.catch(() => {
1973
+ });
1974
+ } catch {
1975
+ }
1976
+ })();
1977
+ };
1978
+ return {
1979
+ /**
1980
+ * @param {{
1981
+ * rule?: { id?: string | number },
1982
+ * method?: string | null,
1983
+ * path?: string | null,
1984
+ * ip?: string | null,
1985
+ * userAgent?: string | null,
1986
+ * }} event
1987
+ */
1988
+ record(event) {
1989
+ if (stopped) return;
1990
+ const fid = event?.rule?.id;
1991
+ if (fid === void 0 || fid === null || fid === "") return;
1992
+ queue.push({
1993
+ fid,
1994
+ method: event.method ?? null,
1995
+ request_uri: event.path ?? null,
1996
+ ip: event.ip ?? null,
1997
+ user_agent: event.userAgent ?? null,
1998
+ log_date: (/* @__PURE__ */ new Date()).toISOString()
1999
+ });
2000
+ if (queue.length >= MAX_BATCH) {
2001
+ flush();
2002
+ return;
2003
+ }
2004
+ if (!timer) timer = setTimeout(flush, flushMs);
2005
+ },
2006
+ flush,
2007
+ stop() {
2008
+ stopped = true;
2009
+ flush();
2010
+ }
2011
+ };
2012
+ }
2013
+ function telemetryEnabled() {
2014
+ const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
2015
+ if (v === void 0 || v === "") return true;
2016
+ return !/^(0|false|off|no)$/i.test(String(v));
2017
+ }
2018
+
2019
+ // src/protect/runtime.js
2020
+ import { readFileSync as readFileSync2 } from "fs";
2021
+ import { join as join2 } from "path";
2022
+
1860
2023
  // src/protect/supabase-guard.js
1861
2024
  var GUARD_PATH = "/_patchstack/guard";
1862
2025
  var HOP_BY_HOP = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection"]);
@@ -1929,7 +2092,26 @@ function createServerFnGuard({ protection }) {
1929
2092
  }
1930
2093
  async function createProtection(options = {}) {
1931
2094
  const onError = options.onError;
1932
- const onDetect = options.onDetect ?? defaultOnDetect;
2095
+ const userOnDetect = options.onDetect ?? defaultOnDetect;
2096
+ const apiKey = resolveApiKey(options);
2097
+ const firewallLog = apiKey && telemetryEnabled() && options.reportFirewallLog !== false ? createFirewallLogReporter({
2098
+ apiKey,
2099
+ apiBase: resolveApiBase(options.pulseRulesUrl ?? options.baseUrl),
2100
+ sourceHost: options.sourceHost,
2101
+ fetchImpl: options.fetchImpl
2102
+ }) : null;
2103
+ const onDetect = (detection) => {
2104
+ userOnDetect(detection);
2105
+ if (firewallLog && detection?.mode === "block") {
2106
+ firewallLog.record({
2107
+ rule: detection.rule,
2108
+ method: detection.method,
2109
+ path: detection.path,
2110
+ ip: detection.ip,
2111
+ userAgent: detection.userAgent
2112
+ });
2113
+ }
2114
+ };
1933
2115
  const store = makeStore(options);
1934
2116
  const bundle = await resolveRules(options, store);
1935
2117
  let mode = resolveMode(options, bundle);
@@ -1961,9 +2143,19 @@ async function createProtection(options = {}) {
1961
2143
  };
1962
2144
  applyBundle(bundle);
1963
2145
  const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
1964
- const decide = (phase, result, block, allow2) => {
2146
+ const decide = (phase, result, block, allow2, ctx = {}) => {
1965
2147
  if (!result || !result.blocked) return allow2();
1966
- onDetect({ phase, mode, category: result.rule?.category, rule: result.rule, message: result.message });
2148
+ onDetect({
2149
+ phase,
2150
+ mode,
2151
+ category: result.rule?.category,
2152
+ rule: result.rule,
2153
+ message: result.message,
2154
+ method: ctx.method,
2155
+ path: ctx.path,
2156
+ ip: ctx.ip,
2157
+ userAgent: ctx.userAgent
2158
+ });
1967
2159
  return mode === "block" ? block() : allow2();
1968
2160
  };
1969
2161
  const isTextCT = (ct) => {
@@ -2146,7 +2338,7 @@ async function createProtection(options = {}) {
2146
2338
  onError?.(err);
2147
2339
  return null;
2148
2340
  }
2149
- return decide("request", result, () => blockResponse(result, request), () => null);
2341
+ return decide("request", result, () => blockResponse(result, request), () => null, fetchRequestMeta(request));
2150
2342
  };
2151
2343
  },
2152
2344
  // Wrap a fetch handler: screens the request, then the response (redact/block).
@@ -2184,7 +2376,8 @@ async function createProtection(options = {}) {
2184
2376
  () => {
2185
2377
  if (exprOptions.screenResponses) wrapNodeResponse(res);
2186
2378
  next();
2187
- }
2379
+ },
2380
+ nodeRequestMeta(req)
2188
2381
  );
2189
2382
  };
2190
2383
  },
@@ -2236,7 +2429,8 @@ async function createProtection(options = {}) {
2236
2429
  if (req.body === void 0) req.body = shaped.body;
2237
2430
  if (nodeOptions.screenResponses) wrapNodeResponse(res);
2238
2431
  next();
2239
- }
2432
+ },
2433
+ nodeRequestMeta(req)
2240
2434
  );
2241
2435
  });
2242
2436
  };
@@ -2257,7 +2451,7 @@ async function createProtection(options = {}) {
2257
2451
  let reporter = null;
2258
2452
  if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
2259
2453
  try {
2260
- ({ reportManifest: reporter } = await import("./refresh-manifest-OW7WT5A6.js"));
2454
+ ({ reportManifest: reporter } = await import("./refresh-manifest-ZIFRZF25.js"));
2261
2455
  } catch (err) {
2262
2456
  onError?.(err);
2263
2457
  }
@@ -2280,7 +2474,12 @@ async function createProtection(options = {}) {
2280
2474
  }
2281
2475
  if (options.refreshMs > 0 && live) {
2282
2476
  const loop = startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError });
2283
- protection.stopRefresh = loop.stop;
2477
+ protection.stopRefresh = () => {
2478
+ loop.stop();
2479
+ firewallLog?.stop();
2480
+ };
2481
+ } else if (firewallLog) {
2482
+ protection.stopRefresh = () => firewallLog.stop();
2284
2483
  }
2285
2484
  return protection;
2286
2485
  }
@@ -2292,6 +2491,22 @@ function resolveMode(options, bundle) {
2292
2491
  if (options?.mode === "dry-run") return "dry-run";
2293
2492
  return "dry-run";
2294
2493
  }
2494
+ function resolveApiKey(options) {
2495
+ if (typeof options?.apiKey === "string" && options.apiKey.length > 0) return options.apiKey;
2496
+ if (typeof process !== "undefined") {
2497
+ const fromEnv = process.env?.PATCHSTACK_API_KEY;
2498
+ if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
2499
+ }
2500
+ try {
2501
+ if (typeof process === "undefined" || typeof process.cwd !== "function") return void 0;
2502
+ const cwd = options?.cwd ?? process.cwd();
2503
+ const raw = readFileSync2(join2(cwd, ".patchstackrc.json"), "utf8");
2504
+ const key = JSON.parse(raw)?.apiKey;
2505
+ if (typeof key === "string" && key.length > 0) return key;
2506
+ } catch {
2507
+ }
2508
+ return void 0;
2509
+ }
2295
2510
  function byPhase(rules, phase) {
2296
2511
  return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
2297
2512
  }
@@ -2555,6 +2770,33 @@ function defaultOnDetect({ phase, mode, category, rule, message }) {
2555
2770
  const tag = mode === "block" ? "BLOCK" : "DETECT (dry-run)";
2556
2771
  console.warn(`[patchstack] ${tag} phase=${phase ?? "request"} category=${category ?? "?"} rule=${rule?.id ?? "?"} ${message ?? ""}`.trim());
2557
2772
  }
2773
+ function fetchRequestMeta(request) {
2774
+ if (!request) return {};
2775
+ let path = null;
2776
+ try {
2777
+ path = new URL(request.url).pathname;
2778
+ } catch {
2779
+ path = typeof request.url === "string" ? request.url : null;
2780
+ }
2781
+ return {
2782
+ method: request.method ?? null,
2783
+ path,
2784
+ ip: request.headers?.get?.("x-forwarded-for") ?? null,
2785
+ userAgent: request.headers?.get?.("user-agent") ?? null
2786
+ };
2787
+ }
2788
+ function nodeRequestMeta(req) {
2789
+ if (!req) return {};
2790
+ const headers = req.headers ?? {};
2791
+ const ua = headers["user-agent"] ?? headers["User-Agent"];
2792
+ const fwd = headers["x-forwarded-for"] ?? headers["X-Forwarded-For"];
2793
+ return {
2794
+ method: req.method ?? null,
2795
+ path: req.originalUrl || req.url || null,
2796
+ ip: req.ip ?? (typeof fwd === "string" ? fwd : null),
2797
+ userAgent: typeof ua === "string" ? ua : Array.isArray(ua) ? ua[0] : null
2798
+ };
2799
+ }
2558
2800
  export {
2559
2801
  GUARD_PATH,
2560
2802
  PulseRuleClient,