@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/AGENT-INSTALL.md +2 -2
- package/dist/cli.js +14 -0
- 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 +252 -7
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +20 -1
- package/dist/protect.js +250 -8
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-OW7WT5A6.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-OW7WT5A6.js.map +0 -1
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
|
|
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({
|
|
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-
|
|
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 =
|
|
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,
|