@patchstack/connect 0.3.21 → 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/AGENT-INSTALL.md +14 -3
- package/dist/cli.js +62 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +277 -12
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +27 -2
- package/dist/protect.js +275 -13
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-XBOGN446.js → refresh-manifest-ZIFRZF25.js} +4 -1
- package/dist/refresh-manifest-ZIFRZF25.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-XBOGN446.js.map +0 -1
package/dist/protect.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface RuleBundle {
|
|
|
5
5
|
firewall: unknown[];
|
|
6
6
|
whitelists: unknown[];
|
|
7
7
|
whitelist_keys: Record<string, unknown>;
|
|
8
|
+
/** From the Pulse rules API when present (`block` = enforce, `dry-run` = detect only). */
|
|
9
|
+
enforcement?: "block" | "dry-run";
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export type Phase = "request" | "response" | "egress";
|
|
@@ -33,7 +35,11 @@ export interface Protection {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface CreateProtectionOptions {
|
|
36
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Fallback when the Pulse rules API does not send `enforcement`.
|
|
40
|
+
* Overridden by `PATCHSTACK_MODE` when set, otherwise by API `enforcement`.
|
|
41
|
+
* Default "dry-run". Scaffolded guards pass "block" when env is unset.
|
|
42
|
+
*/
|
|
37
43
|
mode?: "block" | "dry-run";
|
|
38
44
|
/** Explicit rule bundle (used as the token-less fallback). */
|
|
39
45
|
rules?: unknown;
|
|
@@ -42,8 +48,23 @@ export interface CreateProtectionOptions {
|
|
|
42
48
|
baseUrl?: string;
|
|
43
49
|
/** Pulse site UUID — pull live per-site rules from the Pulse rules API (cached). */
|
|
44
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;
|
|
45
57
|
/** Override the Pulse rules API base URL. */
|
|
46
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;
|
|
47
68
|
/**
|
|
48
69
|
* Re-fetch and hot-swap the live rules every N ms. For long-lived runtimes that aren't restarted
|
|
49
70
|
* on change (an AI builder's sandbox/preview) so a rule that becomes relevant after boot still
|
|
@@ -98,8 +119,12 @@ export interface CreateProtectionOptions {
|
|
|
98
119
|
phase?: Phase;
|
|
99
120
|
mode: string;
|
|
100
121
|
category?: string;
|
|
101
|
-
rule?: { id?: string; category?: string };
|
|
122
|
+
rule?: { id?: string | number; category?: string };
|
|
102
123
|
message?: string;
|
|
124
|
+
method?: string | null;
|
|
125
|
+
path?: string | null;
|
|
126
|
+
ip?: string | null;
|
|
127
|
+
userAgent?: string | null;
|
|
103
128
|
}) => void;
|
|
104
129
|
}
|
|
105
130
|
|
package/dist/protect.js
CHANGED
|
@@ -1032,7 +1032,8 @@ var PulseRuleClient = class {
|
|
|
1032
1032
|
etag: response.headers?.get?.("etag") ?? null,
|
|
1033
1033
|
firewall: data.firewall,
|
|
1034
1034
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
1035
|
-
whitelist_keys: data.whitelist_keys ?? {}
|
|
1035
|
+
whitelist_keys: data.whitelist_keys ?? {},
|
|
1036
|
+
...enforcementField(data)
|
|
1036
1037
|
};
|
|
1037
1038
|
this.#cache = result;
|
|
1038
1039
|
this.#etag = result.etag;
|
|
@@ -1058,6 +1059,11 @@ var PulseRuleClient = class {
|
|
|
1058
1059
|
this.#etag = null;
|
|
1059
1060
|
}
|
|
1060
1061
|
};
|
|
1062
|
+
function enforcementField(data) {
|
|
1063
|
+
if (!data || typeof data !== "object") return {};
|
|
1064
|
+
const v = data.enforcement ?? data.mode;
|
|
1065
|
+
return v === "block" || v === "dry-run" ? { enforcement: v } : {};
|
|
1066
|
+
}
|
|
1061
1067
|
|
|
1062
1068
|
// src/protect/engine/fetch.js
|
|
1063
1069
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
@@ -1785,10 +1791,12 @@ async function resolveRules(options, store) {
|
|
|
1785
1791
|
return emptyBundle();
|
|
1786
1792
|
}
|
|
1787
1793
|
function normalizeBundle(b) {
|
|
1794
|
+
const enforcement = b?.enforcement ?? b?.mode;
|
|
1788
1795
|
return {
|
|
1789
1796
|
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
1790
1797
|
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
1791
|
-
whitelist_keys: b.whitelist_keys ?? {}
|
|
1798
|
+
whitelist_keys: b.whitelist_keys ?? {},
|
|
1799
|
+
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
1792
1800
|
};
|
|
1793
1801
|
}
|
|
1794
1802
|
function emptyBundle() {
|
|
@@ -1849,6 +1857,169 @@ function makeRefreshHandler(tick, secret) {
|
|
|
1849
1857
|
};
|
|
1850
1858
|
}
|
|
1851
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
|
+
|
|
1852
2023
|
// src/protect/supabase-guard.js
|
|
1853
2024
|
var GUARD_PATH = "/_patchstack/guard";
|
|
1854
2025
|
var HOP_BY_HOP = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection"]);
|
|
@@ -1920,11 +2091,30 @@ function createServerFnGuard({ protection }) {
|
|
|
1920
2091
|
};
|
|
1921
2092
|
}
|
|
1922
2093
|
async function createProtection(options = {}) {
|
|
1923
|
-
const mode = options.mode === "block" ? "block" : "dry-run";
|
|
1924
2094
|
const onError = options.onError;
|
|
1925
|
-
const
|
|
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
|
+
};
|
|
1926
2115
|
const store = makeStore(options);
|
|
1927
2116
|
const bundle = await resolveRules(options, store);
|
|
2117
|
+
let mode = resolveMode(options, bundle);
|
|
1928
2118
|
let requestRules;
|
|
1929
2119
|
let responseRules;
|
|
1930
2120
|
let egressRules;
|
|
@@ -1953,9 +2143,19 @@ async function createProtection(options = {}) {
|
|
|
1953
2143
|
};
|
|
1954
2144
|
applyBundle(bundle);
|
|
1955
2145
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
1956
|
-
const decide = (phase, result, block, allow2) => {
|
|
2146
|
+
const decide = (phase, result, block, allow2, ctx = {}) => {
|
|
1957
2147
|
if (!result || !result.blocked) return allow2();
|
|
1958
|
-
onDetect({
|
|
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
|
+
});
|
|
1959
2159
|
return mode === "block" ? block() : allow2();
|
|
1960
2160
|
};
|
|
1961
2161
|
const isTextCT = (ct) => {
|
|
@@ -2119,7 +2319,9 @@ async function createProtection(options = {}) {
|
|
|
2119
2319
|
return mode === "block";
|
|
2120
2320
|
};
|
|
2121
2321
|
const protection = {
|
|
2122
|
-
mode
|
|
2322
|
+
get mode() {
|
|
2323
|
+
return mode;
|
|
2324
|
+
},
|
|
2123
2325
|
get rules() {
|
|
2124
2326
|
return { request: requestRules, response: responseRules, egress: egressRules };
|
|
2125
2327
|
},
|
|
@@ -2136,7 +2338,7 @@ async function createProtection(options = {}) {
|
|
|
2136
2338
|
onError?.(err);
|
|
2137
2339
|
return null;
|
|
2138
2340
|
}
|
|
2139
|
-
return decide("request", result, () => blockResponse(result, request), () => null);
|
|
2341
|
+
return decide("request", result, () => blockResponse(result, request), () => null, fetchRequestMeta(request));
|
|
2140
2342
|
};
|
|
2141
2343
|
},
|
|
2142
2344
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
@@ -2174,7 +2376,8 @@ async function createProtection(options = {}) {
|
|
|
2174
2376
|
() => {
|
|
2175
2377
|
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
2176
2378
|
next();
|
|
2177
|
-
}
|
|
2379
|
+
},
|
|
2380
|
+
nodeRequestMeta(req)
|
|
2178
2381
|
);
|
|
2179
2382
|
};
|
|
2180
2383
|
},
|
|
@@ -2226,7 +2429,8 @@ async function createProtection(options = {}) {
|
|
|
2226
2429
|
if (req.body === void 0) req.body = shaped.body;
|
|
2227
2430
|
if (nodeOptions.screenResponses) wrapNodeResponse(res);
|
|
2228
2431
|
next();
|
|
2229
|
-
}
|
|
2432
|
+
},
|
|
2433
|
+
nodeRequestMeta(req)
|
|
2230
2434
|
);
|
|
2231
2435
|
});
|
|
2232
2436
|
};
|
|
@@ -2247,7 +2451,7 @@ async function createProtection(options = {}) {
|
|
|
2247
2451
|
let reporter = null;
|
|
2248
2452
|
if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
|
|
2249
2453
|
try {
|
|
2250
|
-
({ reportManifest: reporter } = await import("./refresh-manifest-
|
|
2454
|
+
({ reportManifest: reporter } = await import("./refresh-manifest-ZIFRZF25.js"));
|
|
2251
2455
|
} catch (err) {
|
|
2252
2456
|
onError?.(err);
|
|
2253
2457
|
}
|
|
@@ -2260,7 +2464,9 @@ async function createProtection(options = {}) {
|
|
|
2260
2464
|
onError?.(err);
|
|
2261
2465
|
}
|
|
2262
2466
|
}
|
|
2263
|
-
|
|
2467
|
+
const next = await resolveRules(options, store);
|
|
2468
|
+
mode = resolveMode(options, next);
|
|
2469
|
+
applyBundle(next);
|
|
2264
2470
|
};
|
|
2265
2471
|
if (live) {
|
|
2266
2472
|
protection.refresh = () => runRefreshTick();
|
|
@@ -2268,10 +2474,39 @@ async function createProtection(options = {}) {
|
|
|
2268
2474
|
}
|
|
2269
2475
|
if (options.refreshMs > 0 && live) {
|
|
2270
2476
|
const loop = startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError });
|
|
2271
|
-
protection.stopRefresh =
|
|
2477
|
+
protection.stopRefresh = () => {
|
|
2478
|
+
loop.stop();
|
|
2479
|
+
firewallLog?.stop();
|
|
2480
|
+
};
|
|
2481
|
+
} else if (firewallLog) {
|
|
2482
|
+
protection.stopRefresh = () => firewallLog.stop();
|
|
2272
2483
|
}
|
|
2273
2484
|
return protection;
|
|
2274
2485
|
}
|
|
2486
|
+
function resolveMode(options, bundle) {
|
|
2487
|
+
const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
|
|
2488
|
+
if (env === "block" || env === "dry-run") return env;
|
|
2489
|
+
if (bundle?.enforcement === "block" || bundle?.enforcement === "dry-run") return bundle.enforcement;
|
|
2490
|
+
if (options?.mode === "block") return "block";
|
|
2491
|
+
if (options?.mode === "dry-run") return "dry-run";
|
|
2492
|
+
return "dry-run";
|
|
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
|
+
}
|
|
2275
2510
|
function byPhase(rules, phase) {
|
|
2276
2511
|
return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
|
|
2277
2512
|
}
|
|
@@ -2535,6 +2770,33 @@ function defaultOnDetect({ phase, mode, category, rule, message }) {
|
|
|
2535
2770
|
const tag = mode === "block" ? "BLOCK" : "DETECT (dry-run)";
|
|
2536
2771
|
console.warn(`[patchstack] ${tag} phase=${phase ?? "request"} category=${category ?? "?"} rule=${rule?.id ?? "?"} ${message ?? ""}`.trim());
|
|
2537
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
|
+
}
|
|
2538
2800
|
export {
|
|
2539
2801
|
GUARD_PATH,
|
|
2540
2802
|
PulseRuleClient,
|