@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/AGENT-INSTALL.md +12 -1
- package/dist/cli.js +48 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +255 -158
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +19 -2
- package/dist/protect.js +230 -133
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-XBOGN446.js → refresh-manifest-OW7WT5A6.js} +1 -1
- package/dist/refresh-manifest-OW7WT5A6.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";
|
|
@@ -23,12 +25,21 @@ export interface Protection {
|
|
|
23
25
|
node(options?: { maxBodyBytes?: number; screenResponses?: boolean }): (req: unknown, res: unknown, next: () => void) => void;
|
|
24
26
|
/** Present when `egress: true` — restores the original global fetch. */
|
|
25
27
|
uninstallEgress?: () => void;
|
|
26
|
-
/** Present
|
|
28
|
+
/** Present with a live source — re-fetch + hot-swap the rules once (used by the loop + push). */
|
|
29
|
+
refresh?: () => Promise<void>;
|
|
30
|
+
/** Present with a live source — a fetch handler that runs `refresh()` when the request carries
|
|
31
|
+
* the configured refresh secret (a push/zero-day trigger). No secret set → the handler 404s. */
|
|
32
|
+
refreshHandler?: () => (request: Request) => Promise<Response>;
|
|
33
|
+
/** Present when `refreshMs > 0` — stops the live rule-refresh loop. */
|
|
27
34
|
stopRefresh?: () => void;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export interface CreateProtectionOptions {
|
|
31
|
-
/**
|
|
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
|
+
*/
|
|
32
43
|
mode?: "block" | "dry-run";
|
|
33
44
|
/** Explicit rule bundle (used as the token-less fallback). */
|
|
34
45
|
rules?: unknown;
|
|
@@ -46,6 +57,12 @@ export interface CreateProtectionOptions {
|
|
|
46
57
|
* meaningful with a live source (`siteUuid`/`token`).
|
|
47
58
|
*/
|
|
48
59
|
refreshMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Shared secret gating the push refresh endpoint (`refreshHandler()`): the platform/SaaS hits the
|
|
62
|
+
* endpoint with this secret to trigger an immediate refresh. Falls back to `PATCHSTACK_REFRESH_SECRET`.
|
|
63
|
+
* Unset → the endpoint 404s (never an open refresh trigger).
|
|
64
|
+
*/
|
|
65
|
+
refreshSecret?: string;
|
|
49
66
|
/**
|
|
50
67
|
* During a refresh, also re-post the dependency manifest (the runtime counterpart to `scan`) so a
|
|
51
68
|
* dependency added after boot — e.g. via `npm install <pkg>`, which fires no npm lifecycle hook —
|
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;
|
|
@@ -1665,9 +1671,191 @@ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
|
1665
1671
|
</html>`;
|
|
1666
1672
|
}
|
|
1667
1673
|
|
|
1668
|
-
// src/protect/
|
|
1674
|
+
// src/protect/rules/store.js
|
|
1669
1675
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
1670
1676
|
import { join } from "path";
|
|
1677
|
+
function makeStore(options = {}) {
|
|
1678
|
+
let mem = null;
|
|
1679
|
+
const durable = durableTier(options);
|
|
1680
|
+
return {
|
|
1681
|
+
async read() {
|
|
1682
|
+
if (mem) return mem;
|
|
1683
|
+
const env = await durable.read();
|
|
1684
|
+
if (env) mem = env;
|
|
1685
|
+
return env;
|
|
1686
|
+
},
|
|
1687
|
+
async write(env) {
|
|
1688
|
+
if (env) mem = env;
|
|
1689
|
+
await durable.write(env);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function durableTier(options) {
|
|
1694
|
+
const adapter = options.ruleCache;
|
|
1695
|
+
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
1696
|
+
return {
|
|
1697
|
+
read: async () => {
|
|
1698
|
+
try {
|
|
1699
|
+
return toEnvelope(await adapter.read());
|
|
1700
|
+
} catch {
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1703
|
+
},
|
|
1704
|
+
write: async (env) => {
|
|
1705
|
+
try {
|
|
1706
|
+
await adapter.write(env);
|
|
1707
|
+
} catch {
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
const dir = options.cacheDir;
|
|
1713
|
+
return {
|
|
1714
|
+
read: async () => cacheRead(dir),
|
|
1715
|
+
write: async (env) => cacheWrite(dir, env)
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function cachePath(dir) {
|
|
1719
|
+
return join(dir, "patchstack-rules.json");
|
|
1720
|
+
}
|
|
1721
|
+
function cacheWrite(dir, env) {
|
|
1722
|
+
if (!dir) return;
|
|
1723
|
+
try {
|
|
1724
|
+
mkdirSync(dir, { recursive: true });
|
|
1725
|
+
writeFileSync(cachePath(dir), JSON.stringify(env));
|
|
1726
|
+
} catch {
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function cacheRead(dir) {
|
|
1730
|
+
if (!dir) return null;
|
|
1731
|
+
try {
|
|
1732
|
+
return toEnvelope(JSON.parse(readFileSync(cachePath(dir), "utf8")));
|
|
1733
|
+
} catch {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function toEnvelope(value) {
|
|
1738
|
+
if (!value || typeof value !== "object") return null;
|
|
1739
|
+
if (value.bundle && typeof value.bundle === "object") {
|
|
1740
|
+
return { bundle: value.bundle, etag: value.etag ?? null };
|
|
1741
|
+
}
|
|
1742
|
+
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
1743
|
+
return { bundle: value, etag: null };
|
|
1744
|
+
}
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/protect/rules/source.js
|
|
1749
|
+
async function resolveRules(options, store) {
|
|
1750
|
+
if (options.siteUuid) {
|
|
1751
|
+
const prior = await store.read();
|
|
1752
|
+
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
|
|
1753
|
+
const res = await client.getRules();
|
|
1754
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1755
|
+
if (res.success && !res.notModified) {
|
|
1756
|
+
const bundle = normalizeBundle(res);
|
|
1757
|
+
await store.write({ bundle, etag: res.etag ?? null });
|
|
1758
|
+
return bundle;
|
|
1759
|
+
}
|
|
1760
|
+
if (prior?.bundle) {
|
|
1761
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1762
|
+
return normalizeBundle(prior.bundle);
|
|
1763
|
+
}
|
|
1764
|
+
if (options.rules) {
|
|
1765
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
|
|
1766
|
+
return normalizeBundle(options.rules);
|
|
1767
|
+
}
|
|
1768
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1769
|
+
return emptyBundle();
|
|
1770
|
+
}
|
|
1771
|
+
if (options.token) {
|
|
1772
|
+
const prior = await store.read();
|
|
1773
|
+
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
|
|
1774
|
+
const res = await client.getRules();
|
|
1775
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1776
|
+
if (res.success && !res.notModified) {
|
|
1777
|
+
const bundle = normalizeBundle(res);
|
|
1778
|
+
await store.write({ bundle, etag: res.etag ?? null });
|
|
1779
|
+
return bundle;
|
|
1780
|
+
}
|
|
1781
|
+
if (prior?.bundle) {
|
|
1782
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1783
|
+
return normalizeBundle(prior.bundle);
|
|
1784
|
+
}
|
|
1785
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1786
|
+
return emptyBundle();
|
|
1787
|
+
}
|
|
1788
|
+
if (options.rules) {
|
|
1789
|
+
return normalizeBundle(options.rules);
|
|
1790
|
+
}
|
|
1791
|
+
return emptyBundle();
|
|
1792
|
+
}
|
|
1793
|
+
function normalizeBundle(b) {
|
|
1794
|
+
const enforcement = b?.enforcement ?? b?.mode;
|
|
1795
|
+
return {
|
|
1796
|
+
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
1797
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
1798
|
+
whitelist_keys: b.whitelist_keys ?? {},
|
|
1799
|
+
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
function emptyBundle() {
|
|
1803
|
+
return { firewall: [], whitelists: [], whitelist_keys: {} };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/protect/rules/refresh.js
|
|
1807
|
+
var JITTER_FRACTION3 = 0.1;
|
|
1808
|
+
var MAX_BACKOFF_MULTIPLIER = 8;
|
|
1809
|
+
function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
1810
|
+
let stopped = false;
|
|
1811
|
+
let failures = 0;
|
|
1812
|
+
let timer = null;
|
|
1813
|
+
const schedule = () => {
|
|
1814
|
+
if (stopped) return;
|
|
1815
|
+
const backoff = Math.min(2 ** failures, MAX_BACKOFF_MULTIPLIER);
|
|
1816
|
+
const jitter = 1 - Math.random() * JITTER_FRACTION3;
|
|
1817
|
+
const delay = refreshMs * backoff * jitter;
|
|
1818
|
+
timer = setTimeout(run, delay);
|
|
1819
|
+
timer?.unref?.();
|
|
1820
|
+
};
|
|
1821
|
+
const run = async () => {
|
|
1822
|
+
if (stopped) return;
|
|
1823
|
+
try {
|
|
1824
|
+
await tick();
|
|
1825
|
+
failures = 0;
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
failures++;
|
|
1828
|
+
onError?.(err);
|
|
1829
|
+
}
|
|
1830
|
+
schedule();
|
|
1831
|
+
};
|
|
1832
|
+
schedule();
|
|
1833
|
+
return {
|
|
1834
|
+
stop() {
|
|
1835
|
+
stopped = true;
|
|
1836
|
+
if (timer) clearTimeout(timer);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function makeRefreshHandler(tick, secret) {
|
|
1841
|
+
return async (request) => {
|
|
1842
|
+
if (!secret) return new Response("not found", { status: 404 });
|
|
1843
|
+
let provided = null;
|
|
1844
|
+
try {
|
|
1845
|
+
provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
|
|
1846
|
+
} catch {
|
|
1847
|
+
provided = null;
|
|
1848
|
+
}
|
|
1849
|
+
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
1850
|
+
let refreshed = true;
|
|
1851
|
+
try {
|
|
1852
|
+
await tick();
|
|
1853
|
+
} catch {
|
|
1854
|
+
refreshed = false;
|
|
1855
|
+
}
|
|
1856
|
+
return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1671
1859
|
|
|
1672
1860
|
// src/protect/supabase-guard.js
|
|
1673
1861
|
var GUARD_PATH = "/_patchstack/guard";
|
|
@@ -1740,10 +1928,11 @@ function createServerFnGuard({ protection }) {
|
|
|
1740
1928
|
};
|
|
1741
1929
|
}
|
|
1742
1930
|
async function createProtection(options = {}) {
|
|
1743
|
-
const mode = options.mode === "block" ? "block" : "dry-run";
|
|
1744
1931
|
const onError = options.onError;
|
|
1745
1932
|
const onDetect = options.onDetect ?? defaultOnDetect;
|
|
1746
|
-
const
|
|
1933
|
+
const store = makeStore(options);
|
|
1934
|
+
const bundle = await resolveRules(options, store);
|
|
1935
|
+
let mode = resolveMode(options, bundle);
|
|
1747
1936
|
let requestRules;
|
|
1748
1937
|
let responseRules;
|
|
1749
1938
|
let egressRules;
|
|
@@ -1938,7 +2127,9 @@ async function createProtection(options = {}) {
|
|
|
1938
2127
|
return mode === "block";
|
|
1939
2128
|
};
|
|
1940
2129
|
const protection = {
|
|
1941
|
-
mode
|
|
2130
|
+
get mode() {
|
|
2131
|
+
return mode;
|
|
2132
|
+
},
|
|
1942
2133
|
get rules() {
|
|
1943
2134
|
return { request: requestRules, response: responseRules, egress: egressRules };
|
|
1944
2135
|
},
|
|
@@ -2059,33 +2250,48 @@ async function createProtection(options = {}) {
|
|
|
2059
2250
|
allowHosts: options.allowHosts
|
|
2060
2251
|
});
|
|
2061
2252
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2253
|
+
const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
|
|
2254
|
+
const live = Boolean(options.siteUuid || options.token);
|
|
2255
|
+
const refreshSecret = options.refreshSecret ?? (typeof process !== "undefined" ? process.env.PATCHSTACK_REFRESH_SECRET : void 0);
|
|
2256
|
+
const refreshable = live && (options.refreshMs > 0 || Boolean(refreshSecret));
|
|
2257
|
+
let reporter = null;
|
|
2258
|
+
if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
|
|
2259
|
+
try {
|
|
2260
|
+
({ reportManifest: reporter } = await import("./refresh-manifest-OW7WT5A6.js"));
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
onError?.(err);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
const runRefreshTick = async () => {
|
|
2266
|
+
if (reporter) {
|
|
2066
2267
|
try {
|
|
2067
|
-
|
|
2268
|
+
await reporter(cwd);
|
|
2068
2269
|
} catch (err) {
|
|
2069
2270
|
onError?.(err);
|
|
2070
2271
|
}
|
|
2071
2272
|
}
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
}, options.refreshMs);
|
|
2084
|
-
timer.unref?.();
|
|
2085
|
-
protection.stopRefresh = () => clearInterval(timer);
|
|
2273
|
+
const next = await resolveRules(options, store);
|
|
2274
|
+
mode = resolveMode(options, next);
|
|
2275
|
+
applyBundle(next);
|
|
2276
|
+
};
|
|
2277
|
+
if (live) {
|
|
2278
|
+
protection.refresh = () => runRefreshTick();
|
|
2279
|
+
protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
|
|
2280
|
+
}
|
|
2281
|
+
if (options.refreshMs > 0 && live) {
|
|
2282
|
+
const loop = startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError });
|
|
2283
|
+
protection.stopRefresh = loop.stop;
|
|
2086
2284
|
}
|
|
2087
2285
|
return protection;
|
|
2088
2286
|
}
|
|
2287
|
+
function resolveMode(options, bundle) {
|
|
2288
|
+
const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
|
|
2289
|
+
if (env === "block" || env === "dry-run") return env;
|
|
2290
|
+
if (bundle?.enforcement === "block" || bundle?.enforcement === "dry-run") return bundle.enforcement;
|
|
2291
|
+
if (options?.mode === "block") return "block";
|
|
2292
|
+
if (options?.mode === "dry-run") return "dry-run";
|
|
2293
|
+
return "dry-run";
|
|
2294
|
+
}
|
|
2089
2295
|
function byPhase(rules, phase) {
|
|
2090
2296
|
return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
|
|
2091
2297
|
}
|
|
@@ -2324,115 +2530,6 @@ function leakResponse() {
|
|
|
2324
2530
|
headers: { "content-type": "application/json" }
|
|
2325
2531
|
});
|
|
2326
2532
|
}
|
|
2327
|
-
async function resolveRules(options) {
|
|
2328
|
-
const cache = makeCache(options);
|
|
2329
|
-
if (options.siteUuid) {
|
|
2330
|
-
const prior = await cache.read();
|
|
2331
|
-
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
|
|
2332
|
-
const res = await client.getRules();
|
|
2333
|
-
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
2334
|
-
if (res.success && !res.notModified) {
|
|
2335
|
-
const bundle = normalizeBundle(res);
|
|
2336
|
-
await cache.write({ bundle, etag: res.etag ?? null });
|
|
2337
|
-
return bundle;
|
|
2338
|
-
}
|
|
2339
|
-
if (prior?.bundle) {
|
|
2340
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
2341
|
-
return normalizeBundle(prior.bundle);
|
|
2342
|
-
}
|
|
2343
|
-
if (options.rules) {
|
|
2344
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
|
|
2345
|
-
return normalizeBundle(options.rules);
|
|
2346
|
-
}
|
|
2347
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
2348
|
-
return emptyBundle();
|
|
2349
|
-
}
|
|
2350
|
-
if (options.token) {
|
|
2351
|
-
const prior = await cache.read();
|
|
2352
|
-
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
|
|
2353
|
-
const res = await client.getRules();
|
|
2354
|
-
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
2355
|
-
if (res.success && !res.notModified) {
|
|
2356
|
-
const bundle = normalizeBundle(res);
|
|
2357
|
-
await cache.write({ bundle, etag: res.etag ?? null });
|
|
2358
|
-
return bundle;
|
|
2359
|
-
}
|
|
2360
|
-
if (prior?.bundle) {
|
|
2361
|
-
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
2362
|
-
return normalizeBundle(prior.bundle);
|
|
2363
|
-
}
|
|
2364
|
-
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
2365
|
-
return emptyBundle();
|
|
2366
|
-
}
|
|
2367
|
-
if (options.rules) {
|
|
2368
|
-
return normalizeBundle(options.rules);
|
|
2369
|
-
}
|
|
2370
|
-
return emptyBundle();
|
|
2371
|
-
}
|
|
2372
|
-
function normalizeBundle(b) {
|
|
2373
|
-
return {
|
|
2374
|
-
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
2375
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
2376
|
-
whitelist_keys: b.whitelist_keys ?? {}
|
|
2377
|
-
};
|
|
2378
|
-
}
|
|
2379
|
-
function emptyBundle() {
|
|
2380
|
-
return { firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2381
|
-
}
|
|
2382
|
-
function makeCache(options) {
|
|
2383
|
-
const adapter = options.ruleCache;
|
|
2384
|
-
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
2385
|
-
return {
|
|
2386
|
-
read: async () => {
|
|
2387
|
-
try {
|
|
2388
|
-
return toEnvelope(await adapter.read());
|
|
2389
|
-
} catch {
|
|
2390
|
-
return null;
|
|
2391
|
-
}
|
|
2392
|
-
},
|
|
2393
|
-
write: async (env) => {
|
|
2394
|
-
try {
|
|
2395
|
-
await adapter.write(env);
|
|
2396
|
-
} catch {
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
};
|
|
2400
|
-
}
|
|
2401
|
-
const dir = options.cacheDir;
|
|
2402
|
-
return {
|
|
2403
|
-
read: async () => cacheRead(dir),
|
|
2404
|
-
write: async (env) => cacheWrite(dir, env)
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
function cachePath(dir) {
|
|
2408
|
-
return join(dir, "patchstack-rules.json");
|
|
2409
|
-
}
|
|
2410
|
-
function cacheWrite(dir, env) {
|
|
2411
|
-
if (!dir) return;
|
|
2412
|
-
try {
|
|
2413
|
-
mkdirSync(dir, { recursive: true });
|
|
2414
|
-
writeFileSync(cachePath(dir), JSON.stringify(env));
|
|
2415
|
-
} catch {
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
function cacheRead(dir) {
|
|
2419
|
-
if (!dir) return null;
|
|
2420
|
-
try {
|
|
2421
|
-
return toEnvelope(JSON.parse(readFileSync(cachePath(dir), "utf8")));
|
|
2422
|
-
} catch {
|
|
2423
|
-
return null;
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
function toEnvelope(value) {
|
|
2427
|
-
if (!value || typeof value !== "object") return null;
|
|
2428
|
-
if (value.bundle && typeof value.bundle === "object") {
|
|
2429
|
-
return { bundle: value.bundle, etag: value.etag ?? null };
|
|
2430
|
-
}
|
|
2431
|
-
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
2432
|
-
return { bundle: value, etag: null };
|
|
2433
|
-
}
|
|
2434
|
-
return null;
|
|
2435
|
-
}
|
|
2436
2533
|
var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
|
|
2437
2534
|
function blockBody(result) {
|
|
2438
2535
|
return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };
|