@patchstack/connect 0.3.31 → 0.3.33
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 +175 -18
- package/README.md +7 -2
- package/dist/{chunk-LLKP5EJS.js → chunk-3G2I6QL6.js} +1 -1
- package/dist/chunk-3G2I6QL6.js.map +1 -0
- package/dist/cli.js +143 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +2645 -1417
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +316 -0
- package/dist/protect.d.ts +134 -18
- package/dist/protect.edge.js +2723 -1477
- package/dist/protect.edge.js.map +3 -4
- package/dist/protect.js +2645 -1421
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-KNZQBC4V.js → refresh-manifest-SWCPQ52Z.js} +2 -2
- package/dist/refresh-manifest-SWCPQ52Z.js.map +1 -0
- package/package.json +44 -11
- package/dist/chunk-LLKP5EJS.js.map +0 -1
- package/dist/refresh-manifest-KNZQBC4V.js.map +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Public types for `@patchstack/connect/protect` (the vendored runtime is plain JS, so the
|
|
2
|
+
// declarations are hand-authored and shipped alongside dist/protect.js).
|
|
3
|
+
|
|
4
|
+
export interface RuleBundle {
|
|
5
|
+
firewall: unknown[];
|
|
6
|
+
whitelists: unknown[];
|
|
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";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Phase = "request" | "response" | "egress";
|
|
13
|
+
|
|
14
|
+
export interface Protection {
|
|
15
|
+
mode: "block" | "dry-run";
|
|
16
|
+
/** Active rules split by phase. */
|
|
17
|
+
rules: { request: unknown[]; response: unknown[]; egress: unknown[] };
|
|
18
|
+
/** (request) => Response (403 when blocked) | null (allow / dry-run). Request phase only. */
|
|
19
|
+
fetchGuard(): (request: Request) => Promise<Response | null>;
|
|
20
|
+
/** Screens the request, then the response (secret-leak redaction / withhold). */
|
|
21
|
+
fetch(handler: (request: Request, ...rest: unknown[]) => unknown): (request: Request, ...rest: unknown[]) => Promise<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Screen a fetch Response through the response-phase rules (redact/withhold/encode).
|
|
24
|
+
*
|
|
25
|
+
* Pass the originating `request` wherever it is available. A response rule can be scoped to a route or a
|
|
26
|
+
* method (`when`), and that scope can only be applied if the engine is given the request the response
|
|
27
|
+
* belongs to — without it, a scoped response rule is delivered, counted as protection, and never matches.
|
|
28
|
+
*/
|
|
29
|
+
screenResponse(response: Response, request?: Request): Promise<Response>;
|
|
30
|
+
express(options?: { screenResponses?: boolean }): (req: unknown, res: unknown, next: () => void) => void;
|
|
31
|
+
node(options?: { maxBodyBytes?: number; screenResponses?: boolean }): (req: unknown, res: unknown, next: () => void) => void;
|
|
32
|
+
/** Present when `egress: true` — restores the original global fetch. */
|
|
33
|
+
uninstallEgress?: () => void;
|
|
34
|
+
/** Present with a live source — re-fetch + hot-swap the rules once (used by the loop + push).
|
|
35
|
+
* Resolves with the outcome of the attempt: `ok: false` means the rules in force came from the
|
|
36
|
+
* cache or the bundled fallback, not from the source. It does not reject on a source failure. */
|
|
37
|
+
/** Refresh the rules now. `ok` is whether the resolution was clean; `origin` is which source supplied
|
|
38
|
+
* the rules now in force — `api` and `cache` are Patchstack-delivered, `bundled` is the caller's own
|
|
39
|
+
* `rules` option, `empty` is none. A fallback is `ok: false` with the origin it fell back to. */
|
|
40
|
+
refresh?: () => Promise<{
|
|
41
|
+
ok: boolean;
|
|
42
|
+
origin?: "api" | "cache" | "bundled" | "empty";
|
|
43
|
+
reason?: string;
|
|
44
|
+
}>;
|
|
45
|
+
/** Present with a live source — a fetch handler that runs `refresh()` when the request carries
|
|
46
|
+
* the configured refresh secret (a push/zero-day trigger). No secret set → the handler 404s. */
|
|
47
|
+
refreshHandler?: () => (request: Request) => Promise<Response>;
|
|
48
|
+
/** Stops everything with a timer or a buffer behind it: the refresh loop, the block log, the
|
|
49
|
+
* detection reporter (flushing what it holds). Always present, and safe to call twice. */
|
|
50
|
+
/**
|
|
51
|
+
* Stop everything holding a timer or a buffer.
|
|
52
|
+
*
|
|
53
|
+
* Resolves once the reporters this reaches — the detection reporter and the block log — have finished
|
|
54
|
+
* or been given up on, so a shutdown handler can await it instead of racing process exit. Each has its
|
|
55
|
+
* own budget, and when one elapses that reporter is ENDED: its requests are aborted, it starts nothing
|
|
56
|
+
* further, and it discards what it was holding. Ignoring the promise behaves as it always has.
|
|
57
|
+
*
|
|
58
|
+
* Two limits are worth knowing, because neither can be promised away:
|
|
59
|
+
*
|
|
60
|
+
* - A request is aborted, not guaranteed to stop. A transport that ignores its abort signal is
|
|
61
|
+
* DETACHED — this stops waiting on it and stops acting on its result — so "resolved" means the
|
|
62
|
+
* reporter is finished with it, not that the underlying request has ended.
|
|
63
|
+
* - A runtime that terminates the process regardless still wins, whatever this resolves.
|
|
64
|
+
*
|
|
65
|
+
* What is accounted for also differs by reporter. Every detection event ends up delivered, refused or
|
|
66
|
+
* dropped, and `detectionHealth()` reports each. Block-log records have no counters at all, so one lost
|
|
67
|
+
* to a failed token exchange, a failed post, or a shutdown that ran out of time is reported nowhere.
|
|
68
|
+
*/
|
|
69
|
+
stop: () => Promise<void>;
|
|
70
|
+
/** Alias of `stop`, under the name callers already have. */
|
|
71
|
+
stopRefresh: () => Promise<void>;
|
|
72
|
+
/** Whether this guard reports security events, and if not, why not.
|
|
73
|
+
*
|
|
74
|
+
* Reporting is on for a site enrolled in Patchstack-managed mitigation that is running managed rules
|
|
75
|
+
* with a credential, and off everywhere else. Each state is distinct so "no events arrived" can be
|
|
76
|
+
* told apart from "reporting is off" — and it follows refreshes, so a guard that starts on cached or
|
|
77
|
+
* bundled rules and later receives managed rules begins reporting without a restart.
|
|
78
|
+
*
|
|
79
|
+
* - `on` — events are being sent
|
|
80
|
+
* - `disabled-by-config` — `PATCHSTACK_REPORT_DETECTIONS` is false, or `reportDetections: false`
|
|
81
|
+
* - `disabled-by-telemetry-opt-out` — `PATCHSTACK_TELEMETRY` is false
|
|
82
|
+
* - `not-enrolled` — no site identity
|
|
83
|
+
* - `no-managed-rules` — the rules in force did not come from Patchstack
|
|
84
|
+
* - `unavailable-no-credential` — enrolled, but no credential resolved */
|
|
85
|
+
detectionReporting:
|
|
86
|
+
| "on"
|
|
87
|
+
| "disabled-by-config"
|
|
88
|
+
| "disabled-by-telemetry-opt-out"
|
|
89
|
+
| "not-enrolled"
|
|
90
|
+
| "no-managed-rules"
|
|
91
|
+
| "unavailable-no-credential";
|
|
92
|
+
/** Present when detection reporting is on — delivery counts (in events) and the last acknowledgement.
|
|
93
|
+
* Carries no request data. */
|
|
94
|
+
detectionHealth?: () => {
|
|
95
|
+
/** Events attempted, acknowledged, refused or unreachable, and dropped for queue pressure. */
|
|
96
|
+
sent: number;
|
|
97
|
+
delivered: number;
|
|
98
|
+
failed: number;
|
|
99
|
+
dropped: number;
|
|
100
|
+
/** Backoff attempts beyond the first. A path that only ever succeeds on a retry is working, and is
|
|
101
|
+
* worth telling apart from one that never has to retry. */
|
|
102
|
+
retried: number;
|
|
103
|
+
/** Redeliveries made after the endpoint refused a token, which are not backoff retries: a rotated or
|
|
104
|
+
* revoked credential is worth seeing as itself rather than as a delivery failure. */
|
|
105
|
+
reauthorized: number;
|
|
106
|
+
lastDeliveredAt: string | null;
|
|
107
|
+
/** Capability announcements, counted separately: these carry no events, so they never move the
|
|
108
|
+
* counters above. Zero here alongside delivered events is normal, and so is the reverse. */
|
|
109
|
+
capability: {
|
|
110
|
+
announced: number;
|
|
111
|
+
acknowledged: number;
|
|
112
|
+
failed: number;
|
|
113
|
+
/** Retries of a declaration, counted apart from event retries for the same reason as the rest. */
|
|
114
|
+
retried: number;
|
|
115
|
+
lastAcknowledgedAt: string | null;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface CreateProtectionOptions {
|
|
121
|
+
/**
|
|
122
|
+
* Fallback when the Pulse rules API does not send `enforcement`.
|
|
123
|
+
* Overridden by `PATCHSTACK_MODE` when set, otherwise by API `enforcement`.
|
|
124
|
+
* Default "dry-run". Scaffolded guards pass "block" when env is unset.
|
|
125
|
+
*/
|
|
126
|
+
mode?: "block" | "dry-run";
|
|
127
|
+
/** Explicit rule bundle (used as the token-less fallback). */
|
|
128
|
+
rules?: unknown;
|
|
129
|
+
/** Patchstack WAF token — pull live per-site rules from the API. */
|
|
130
|
+
token?: string;
|
|
131
|
+
baseUrl?: string;
|
|
132
|
+
/** Pulse site UUID — pull live per-site rules from the Pulse rules API (cached). */
|
|
133
|
+
siteUuid?: string;
|
|
134
|
+
/**
|
|
135
|
+
* WP-format site API key (`{secret}-{oauth.id}`) for authenticated block logs
|
|
136
|
+
* via connector `POST /api/logs/log`. Falls back to `PATCHSTACK_API_KEY`, then
|
|
137
|
+
* `apiKey` in `.patchstackrc.local.json` (where setup writes it) and then in
|
|
138
|
+
* `.patchstackrc.json` (where installs that predate the split still hold it).
|
|
139
|
+
* Never put this in the public widget.
|
|
140
|
+
*/
|
|
141
|
+
apiKey?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Credential for the authenticated rules lookup. Falls back to
|
|
144
|
+
* `PATCHSTACK_PULSE_AUTH`, then `pulseAuth` in `.patchstackrc.local.json` and
|
|
145
|
+
* then in `.patchstackrc.json`, then to `apiKey`. Exchanged for a short-lived
|
|
146
|
+
* token; never sent directly. Never put this in the public widget.
|
|
147
|
+
*/
|
|
148
|
+
pulseAuth?: string;
|
|
149
|
+
/** Override the Pulse rules API base URL. */
|
|
150
|
+
pulseRulesUrl?: string;
|
|
151
|
+
/**
|
|
152
|
+
* When false, skip posting block events to connector `/api/logs/log`.
|
|
153
|
+
* Also disabled when `PATCHSTACK_TELEMETRY=off` or when no apiKey is available.
|
|
154
|
+
*/
|
|
155
|
+
reportFirewallLog?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Opt OUT of reporting every rule that fired — including one in `dry-run` that did not block — to the
|
|
158
|
+
* Pulse detections endpoint.
|
|
159
|
+
*
|
|
160
|
+
* Reporting is ON by default for a site enrolled with Patchstack that is running Patchstack-delivered
|
|
161
|
+
* rules and has a resolvable credential; it is off for a local install and for a guard running its own
|
|
162
|
+
* `rules`. This option can only switch it OFF: passing `true` cannot enable reporting for a site that is
|
|
163
|
+
* not enrolled, because whether a site is managed is Patchstack's answer and not a caller's to assert.
|
|
164
|
+
* `PATCHSTACK_REPORT_DETECTIONS=0` does the same thing from the environment.
|
|
165
|
+
*
|
|
166
|
+
* Why it exists: a rule that blocks nothing reports nothing, so a rule that is quietly wrong and a rule
|
|
167
|
+
* that is protecting look identical from the outside.
|
|
168
|
+
*
|
|
169
|
+
* What it sends on EVERY detection: the rule id and its revision, the request path, the query string's
|
|
170
|
+
* parameter NAMES, the method, the parameters the rule reads, the phase, whether it was enforced, the
|
|
171
|
+
* rule-bundle ETag, and a timestamp — plus the values of the parameters the matched rule names, under a
|
|
172
|
+
* capture plan derived from that rule.
|
|
173
|
+
*
|
|
174
|
+
* Two fields depend on the phase. A request or response detection also carries the user agent and the
|
|
175
|
+
* client address with its provenance. An egress detection carries neither: the call was the
|
|
176
|
+
* application's own, so there is no visitor to attribute it to, and both read `null`/`unavailable`.
|
|
177
|
+
*
|
|
178
|
+
* A rule earns each captured value by naming what it reads. A rule reading the whole request (`raw`,
|
|
179
|
+
* `all`) permits nothing; response values are never captured; raw request bytes need an explicit,
|
|
180
|
+
* reviewed opt-in on the rule itself. Values are bounded in number and length, and what a bound left
|
|
181
|
+
* out is counted. The User-Agent is the one exception to the rule-scoped policy: it is part of the
|
|
182
|
+
* baseline and travels whether or not a rule names it, because a detection nobody can attribute is of
|
|
183
|
+
* little use. It does NOT send the value of any other parameter the matched rule does not name, any
|
|
184
|
+
* response value, or the query string's values as baseline metadata — `route` and `query_keys`
|
|
185
|
+
* describe a URL without disclosing what was in it, while a rule naming `egress.url` captures that URL
|
|
186
|
+
* as the rule read it. An egress detection has no user agent and no client address: the call was the
|
|
187
|
+
* application's own, so there is no visitor to attribute it to.
|
|
188
|
+
*
|
|
189
|
+
* `AGENT-INSTALL.md` carries the full statement, and is the version to read before enabling this.
|
|
190
|
+
*
|
|
191
|
+
* `detectionReporting` names the state, including the reason when reporting is off.
|
|
192
|
+
*/
|
|
193
|
+
reportDetections?: boolean;
|
|
194
|
+
/** How long to buffer detections before posting a batch. Default 5000ms. */
|
|
195
|
+
detectionFlushMs?: number;
|
|
196
|
+
/** Optional Source-Host header for connector hostname checks. */
|
|
197
|
+
sourceHost?: string;
|
|
198
|
+
/** Optional fetch override (tests). */
|
|
199
|
+
fetchImpl?: typeof fetch;
|
|
200
|
+
/**
|
|
201
|
+
* Re-fetch and hot-swap the live rules every N ms. For long-lived runtimes that aren't restarted
|
|
202
|
+
* on change (an AI builder's sandbox/preview) so a rule that becomes relevant after boot still
|
|
203
|
+
* applies. Default 0 (off) — a real deploy restarts the process, which re-fetches anyway. Only
|
|
204
|
+
* meaningful with a live source (`siteUuid`/`token`).
|
|
205
|
+
*/
|
|
206
|
+
refreshMs?: number;
|
|
207
|
+
/**
|
|
208
|
+
* Shared secret gating the push refresh endpoint (`refreshHandler()`): the platform/SaaS hits the
|
|
209
|
+
* endpoint with this secret to trigger an immediate refresh. Falls back to `PATCHSTACK_REFRESH_SECRET`.
|
|
210
|
+
* Unset → the endpoint 404s (never an open refresh trigger).
|
|
211
|
+
*/
|
|
212
|
+
refreshSecret?: string;
|
|
213
|
+
/**
|
|
214
|
+
* During a refresh, also re-post the dependency manifest (the runtime counterpart to `scan`) so a
|
|
215
|
+
* dependency added after boot — e.g. via `npm install <pkg>`, which fires no npm lifecycle hook —
|
|
216
|
+
* is reported and enforced without a restart. Defaults on when a `siteUuid` is set; set false to
|
|
217
|
+
* refresh rules only. Only meaningful with `refreshMs > 0` and a Pulse `siteUuid`.
|
|
218
|
+
*/
|
|
219
|
+
reportManifest?: boolean;
|
|
220
|
+
/** Directory the manifest re-scan reads the lockfile from during a refresh. Default process.cwd(). */
|
|
221
|
+
cwd?: string;
|
|
222
|
+
/** Directory for the last-known-good rule cache (disk — the default cache backend). */
|
|
223
|
+
cacheDir?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Pluggable last-known-good cache, for runtimes without a filesystem (Workers/Deno). Overrides
|
|
226
|
+
* the disk cache. Stores/returns an opaque envelope; read may return null when nothing is cached.
|
|
227
|
+
*/
|
|
228
|
+
ruleCache?: {
|
|
229
|
+
read(): unknown | Promise<unknown>;
|
|
230
|
+
write(envelope: unknown): unknown | Promise<unknown>;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Declare which peers are this deployment's own reverse proxies, so a forwarded header can be believed.
|
|
234
|
+
*
|
|
235
|
+
* With no policy, the client address is whatever the transport observed — the socket peer on Node, and
|
|
236
|
+
* nothing at all in a runtime that exposes no peer, where the provenance reads `unavailable`. A
|
|
237
|
+
* forwarded header is never trusted implicitly: it is ordinary request input that any caller can send.
|
|
238
|
+
*
|
|
239
|
+
* A policy must say WHO is trusted, not just which header to read. Declare at least one of:
|
|
240
|
+
*
|
|
241
|
+
* - `peers` — CIDRs or bare addresses of your front end. An empty list means no peer is trusted, and
|
|
242
|
+
* one unparseable entry rejects the whole policy.
|
|
243
|
+
* - `hops` — the number of trusted proxies counting from the peer inward, as in the numeric form of
|
|
244
|
+
* Express's `trust proxy`.
|
|
245
|
+
* - `isTrusted` — a predicate over an address.
|
|
246
|
+
*
|
|
247
|
+
* `header` defaults to `x-forwarded-for`. The chain is read from the application side inward, stopping
|
|
248
|
+
* at the first address that is not trusted, because a proxy appends rather than replaces — so a value
|
|
249
|
+
* the caller prepended is ignored.
|
|
250
|
+
*
|
|
251
|
+
* Any unrecognised key, or any malformed value, rejects the policy rather than being ignored. There are
|
|
252
|
+
* no provider presets: a provider's name does not establish that the provider overwrote the header.
|
|
253
|
+
*
|
|
254
|
+
* Note that `req.ip` is never consulted on the Express path. Under `trust proxy` it is itself
|
|
255
|
+
* header-derived by a policy this guard has not verified, so an application behind a proxy sees the
|
|
256
|
+
* proxy's address until it declares a policy here.
|
|
257
|
+
*/
|
|
258
|
+
trustedProxy?: {
|
|
259
|
+
peers?: string[];
|
|
260
|
+
hops?: number;
|
|
261
|
+
header?: string;
|
|
262
|
+
isTrusted?: (ip: string) => boolean;
|
|
263
|
+
};
|
|
264
|
+
/** Override the default response-phase (secret-leak) rule set. */
|
|
265
|
+
responseRules?: unknown[];
|
|
266
|
+
/** Override the default egress-phase (SSRF) rule set. */
|
|
267
|
+
egressRules?: unknown[];
|
|
268
|
+
/** Opt in to wrapping global fetch to screen the app's outbound calls (SSRF). */
|
|
269
|
+
egress?: boolean;
|
|
270
|
+
/** Hosts exempt from egress screening. */
|
|
271
|
+
allowHosts?: string[];
|
|
272
|
+
/**
|
|
273
|
+
* Screen the Node http/https path against DNS rebinding: resolve outbound hostnames and block +
|
|
274
|
+
* pin to the vetted address when they map to a disallowed (internal/metadata) IP. Default true;
|
|
275
|
+
* only active when `egress` is on and node:dns is available (a no-op on edge runtimes).
|
|
276
|
+
*/
|
|
277
|
+
screenDns?: boolean;
|
|
278
|
+
/** Redaction mask (string or per-category function). Default "[REDACTED]". */
|
|
279
|
+
maskWith?: string | ((category?: string) => string);
|
|
280
|
+
onError?: (err: unknown) => void;
|
|
281
|
+
onEgressBlock?: (info: { url: string; host: string | null; method: string }) => void;
|
|
282
|
+
onDetect?: (detection: {
|
|
283
|
+
phase?: Phase;
|
|
284
|
+
mode: string;
|
|
285
|
+
category?: string;
|
|
286
|
+
rule?: { id?: string | number; category?: string };
|
|
287
|
+
message?: string;
|
|
288
|
+
method?: string | null;
|
|
289
|
+
path?: string | null;
|
|
290
|
+
/** The client address resolved for this request, or null when none could be established. */
|
|
291
|
+
ip?: string | null;
|
|
292
|
+
/** Where `ip` came from: the transport peer, a forwarded header a declared trusted proxy set, or
|
|
293
|
+
* nothing this guard can stand behind. `ip` is null when this is `unavailable`. */
|
|
294
|
+
clientIpSource?: "runtime" | "trusted-proxy" | "unavailable";
|
|
295
|
+
userAgent?: string | null;
|
|
296
|
+
}) => void;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function createProtection(options?: CreateProtectionOptions): Promise<Protection>;
|
|
300
|
+
|
|
301
|
+
export const GUARD_PATH: string;
|
|
302
|
+
|
|
303
|
+
/** Browser-tunnel guard: evaluate then forward to the app's own Supabase project (SSRF-pinned). */
|
|
304
|
+
export function createSupabaseGuard(opts: {
|
|
305
|
+
protection: Protection;
|
|
306
|
+
supabaseUrl?: string;
|
|
307
|
+
fetchImpl?: typeof fetch;
|
|
308
|
+
}): (request: Request) => Promise<Response>;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Server-function guard: inspect a TanStack server function's decoded args against the same
|
|
312
|
+
* policy. Returns a block receipt to throw on (aborts the call before it writes), or null to allow.
|
|
313
|
+
*/
|
|
314
|
+
export function createServerFnGuard(opts: {
|
|
315
|
+
protection: Protection;
|
|
316
|
+
}): (data: unknown) => Promise<{ rule?: string; message: string } | null>;
|
package/dist/protect.d.ts
CHANGED
|
@@ -34,27 +34,86 @@ export interface Protection {
|
|
|
34
34
|
/** Present with a live source — re-fetch + hot-swap the rules once (used by the loop + push).
|
|
35
35
|
* Resolves with the outcome of the attempt: `ok: false` means the rules in force came from the
|
|
36
36
|
* cache or the bundled fallback, not from the source. It does not reject on a source failure. */
|
|
37
|
-
|
|
37
|
+
/** Refresh the rules now. `ok` is whether the resolution was clean; `origin` is which source supplied
|
|
38
|
+
* the rules now in force — `api` and `cache` are Patchstack-delivered, `bundled` is the caller's own
|
|
39
|
+
* `rules` option, `empty` is none. A fallback is `ok: false` with the origin it fell back to. */
|
|
40
|
+
refresh?: () => Promise<{
|
|
41
|
+
ok: boolean;
|
|
42
|
+
origin?: "api" | "cache" | "bundled" | "empty";
|
|
43
|
+
reason?: string;
|
|
44
|
+
}>;
|
|
38
45
|
/** Present with a live source — a fetch handler that runs `refresh()` when the request carries
|
|
39
46
|
* the configured refresh secret (a push/zero-day trigger). No secret set → the handler 404s. */
|
|
40
47
|
refreshHandler?: () => (request: Request) => Promise<Response>;
|
|
41
48
|
/** Stops everything with a timer or a buffer behind it: the refresh loop, the block log, the
|
|
42
49
|
* detection reporter (flushing what it holds). Always present, and safe to call twice. */
|
|
43
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Stop everything holding a timer or a buffer.
|
|
52
|
+
*
|
|
53
|
+
* Resolves once the reporters this reaches — the detection reporter and the block log — have finished
|
|
54
|
+
* or been given up on, so a shutdown handler can await it instead of racing process exit. Each has its
|
|
55
|
+
* own budget, and when one elapses that reporter is ENDED: its requests are aborted, it starts nothing
|
|
56
|
+
* further, and it discards what it was holding. Ignoring the promise behaves as it always has.
|
|
57
|
+
*
|
|
58
|
+
* Two limits are worth knowing, because neither can be promised away:
|
|
59
|
+
*
|
|
60
|
+
* - A request is aborted, not guaranteed to stop. A transport that ignores its abort signal is
|
|
61
|
+
* DETACHED — this stops waiting on it and stops acting on its result — so "resolved" means the
|
|
62
|
+
* reporter is finished with it, not that the underlying request has ended.
|
|
63
|
+
* - A runtime that terminates the process regardless still wins, whatever this resolves.
|
|
64
|
+
*
|
|
65
|
+
* What is accounted for also differs by reporter. Every detection event ends up delivered, refused or
|
|
66
|
+
* dropped, and `detectionHealth()` reports each. Block-log records have no counters at all, so one lost
|
|
67
|
+
* to a failed token exchange, a failed post, or a shutdown that ran out of time is reported nowhere.
|
|
68
|
+
*/
|
|
69
|
+
stop: () => Promise<void>;
|
|
44
70
|
/** Alias of `stop`, under the name callers already have. */
|
|
45
|
-
stopRefresh: () => void
|
|
46
|
-
/** Whether
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
71
|
+
stopRefresh: () => Promise<void>;
|
|
72
|
+
/** Whether this guard reports security events, and if not, why not.
|
|
73
|
+
*
|
|
74
|
+
* Reporting is on for a site enrolled in Patchstack-managed mitigation that is running managed rules
|
|
75
|
+
* with a credential, and off everywhere else. Each state is distinct so "no events arrived" can be
|
|
76
|
+
* told apart from "reporting is off" — and it follows refreshes, so a guard that starts on cached or
|
|
77
|
+
* bundled rules and later receives managed rules begins reporting without a restart.
|
|
78
|
+
*
|
|
79
|
+
* - `on` — events are being sent
|
|
80
|
+
* - `disabled-by-config` — `PATCHSTACK_REPORT_DETECTIONS` is false, or `reportDetections: false`
|
|
81
|
+
* - `disabled-by-telemetry-opt-out` — `PATCHSTACK_TELEMETRY` is false
|
|
82
|
+
* - `not-enrolled` — no site identity
|
|
83
|
+
* - `no-managed-rules` — the rules in force did not come from Patchstack
|
|
84
|
+
* - `unavailable-no-credential` — enrolled, but no credential resolved */
|
|
85
|
+
detectionReporting:
|
|
86
|
+
| "on"
|
|
87
|
+
| "disabled-by-config"
|
|
88
|
+
| "disabled-by-telemetry-opt-out"
|
|
89
|
+
| "not-enrolled"
|
|
90
|
+
| "no-managed-rules"
|
|
91
|
+
| "unavailable-no-credential";
|
|
50
92
|
/** Present when detection reporting is on — delivery counts (in events) and the last acknowledgement.
|
|
51
93
|
* Carries no request data. */
|
|
52
94
|
detectionHealth?: () => {
|
|
95
|
+
/** Events attempted, acknowledged, refused or unreachable, and dropped for queue pressure. */
|
|
53
96
|
sent: number;
|
|
54
97
|
delivered: number;
|
|
55
98
|
failed: number;
|
|
56
99
|
dropped: number;
|
|
100
|
+
/** Backoff attempts beyond the first. A path that only ever succeeds on a retry is working, and is
|
|
101
|
+
* worth telling apart from one that never has to retry. */
|
|
102
|
+
retried: number;
|
|
103
|
+
/** Redeliveries made after the endpoint refused a token, which are not backoff retries: a rotated or
|
|
104
|
+
* revoked credential is worth seeing as itself rather than as a delivery failure. */
|
|
105
|
+
reauthorized: number;
|
|
57
106
|
lastDeliveredAt: string | null;
|
|
107
|
+
/** Capability announcements, counted separately: these carry no events, so they never move the
|
|
108
|
+
* counters above. Zero here alongside delivered events is normal, and so is the reverse. */
|
|
109
|
+
capability: {
|
|
110
|
+
announced: number;
|
|
111
|
+
acknowledged: number;
|
|
112
|
+
failed: number;
|
|
113
|
+
/** Retries of a declaration, counted apart from event retries for the same reason as the rest. */
|
|
114
|
+
retried: number;
|
|
115
|
+
lastAcknowledgedAt: string | null;
|
|
116
|
+
};
|
|
58
117
|
};
|
|
59
118
|
}
|
|
60
119
|
|
|
@@ -95,20 +154,41 @@ export interface CreateProtectionOptions {
|
|
|
95
154
|
*/
|
|
96
155
|
reportFirewallLog?: boolean;
|
|
97
156
|
/**
|
|
98
|
-
*
|
|
99
|
-
* detections endpoint.
|
|
157
|
+
* Opt OUT of reporting every rule that fired — including one in `dry-run` that did not block — to the
|
|
158
|
+
* Pulse detections endpoint.
|
|
159
|
+
*
|
|
160
|
+
* Reporting is ON by default for a site enrolled with Patchstack that is running Patchstack-delivered
|
|
161
|
+
* rules and has a resolvable credential; it is off for a local install and for a guard running its own
|
|
162
|
+
* `rules`. This option can only switch it OFF: passing `true` cannot enable reporting for a site that is
|
|
163
|
+
* not enrolled, because whether a site is managed is Patchstack's answer and not a caller's to assert.
|
|
164
|
+
* `PATCHSTACK_REPORT_DETECTIONS=0` does the same thing from the environment.
|
|
165
|
+
*
|
|
166
|
+
* Why it exists: a rule that blocks nothing reports nothing, so a rule that is quietly wrong and a rule
|
|
167
|
+
* that is protecting look identical from the outside.
|
|
168
|
+
*
|
|
169
|
+
* What it sends on EVERY detection: the rule id and its revision, the request path, the query string's
|
|
170
|
+
* parameter NAMES, the method, the parameters the rule reads, the phase, whether it was enforced, the
|
|
171
|
+
* rule-bundle ETag, and a timestamp — plus the values of the parameters the matched rule names, under a
|
|
172
|
+
* capture plan derived from that rule.
|
|
100
173
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
174
|
+
* Two fields depend on the phase. A request or response detection also carries the user agent and the
|
|
175
|
+
* client address with its provenance. An egress detection carries neither: the call was the
|
|
176
|
+
* application's own, so there is no visitor to attribute it to, and both read `null`/`unavailable`.
|
|
103
177
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
178
|
+
* A rule earns each captured value by naming what it reads. A rule reading the whole request (`raw`,
|
|
179
|
+
* `all`) permits nothing; response values are never captured; raw request bytes need an explicit,
|
|
180
|
+
* reviewed opt-in on the rule itself. Values are bounded in number and length, and what a bound left
|
|
181
|
+
* out is counted. The User-Agent is the one exception to the rule-scoped policy: it is part of the
|
|
182
|
+
* baseline and travels whether or not a rule names it, because a detection nobody can attribute is of
|
|
183
|
+
* little use. It does NOT send the value of any other parameter the matched rule does not name, any
|
|
184
|
+
* response value, or the query string's values as baseline metadata — `route` and `query_keys`
|
|
185
|
+
* describe a URL without disclosing what was in it, while a rule naming `egress.url` captures that URL
|
|
186
|
+
* as the rule read it. An egress detection has no user agent and no client address: the call was the
|
|
187
|
+
* application's own, so there is no visitor to attribute it to.
|
|
108
188
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
189
|
+
* `AGENT-INSTALL.md` carries the full statement, and is the version to read before enabling this.
|
|
190
|
+
*
|
|
191
|
+
* `detectionReporting` names the state, including the reason when reporting is off.
|
|
112
192
|
*/
|
|
113
193
|
reportDetections?: boolean;
|
|
114
194
|
/** How long to buffer detections before posting a batch. Default 5000ms. */
|
|
@@ -149,6 +229,38 @@ export interface CreateProtectionOptions {
|
|
|
149
229
|
read(): unknown | Promise<unknown>;
|
|
150
230
|
write(envelope: unknown): unknown | Promise<unknown>;
|
|
151
231
|
};
|
|
232
|
+
/**
|
|
233
|
+
* Declare which peers are this deployment's own reverse proxies, so a forwarded header can be believed.
|
|
234
|
+
*
|
|
235
|
+
* With no policy, the client address is whatever the transport observed — the socket peer on Node, and
|
|
236
|
+
* nothing at all in a runtime that exposes no peer, where the provenance reads `unavailable`. A
|
|
237
|
+
* forwarded header is never trusted implicitly: it is ordinary request input that any caller can send.
|
|
238
|
+
*
|
|
239
|
+
* A policy must say WHO is trusted, not just which header to read. Declare at least one of:
|
|
240
|
+
*
|
|
241
|
+
* - `peers` — CIDRs or bare addresses of your front end. An empty list means no peer is trusted, and
|
|
242
|
+
* one unparseable entry rejects the whole policy.
|
|
243
|
+
* - `hops` — the number of trusted proxies counting from the peer inward, as in the numeric form of
|
|
244
|
+
* Express's `trust proxy`.
|
|
245
|
+
* - `isTrusted` — a predicate over an address.
|
|
246
|
+
*
|
|
247
|
+
* `header` defaults to `x-forwarded-for`. The chain is read from the application side inward, stopping
|
|
248
|
+
* at the first address that is not trusted, because a proxy appends rather than replaces — so a value
|
|
249
|
+
* the caller prepended is ignored.
|
|
250
|
+
*
|
|
251
|
+
* Any unrecognised key, or any malformed value, rejects the policy rather than being ignored. There are
|
|
252
|
+
* no provider presets: a provider's name does not establish that the provider overwrote the header.
|
|
253
|
+
*
|
|
254
|
+
* Note that `req.ip` is never consulted on the Express path. Under `trust proxy` it is itself
|
|
255
|
+
* header-derived by a policy this guard has not verified, so an application behind a proxy sees the
|
|
256
|
+
* proxy's address until it declares a policy here.
|
|
257
|
+
*/
|
|
258
|
+
trustedProxy?: {
|
|
259
|
+
peers?: string[];
|
|
260
|
+
hops?: number;
|
|
261
|
+
header?: string;
|
|
262
|
+
isTrusted?: (ip: string) => boolean;
|
|
263
|
+
};
|
|
152
264
|
/** Override the default response-phase (secret-leak) rule set. */
|
|
153
265
|
responseRules?: unknown[];
|
|
154
266
|
/** Override the default egress-phase (SSRF) rule set. */
|
|
@@ -175,7 +287,11 @@ export interface CreateProtectionOptions {
|
|
|
175
287
|
message?: string;
|
|
176
288
|
method?: string | null;
|
|
177
289
|
path?: string | null;
|
|
290
|
+
/** The client address resolved for this request, or null when none could be established. */
|
|
178
291
|
ip?: string | null;
|
|
292
|
+
/** Where `ip` came from: the transport peer, a forwarded header a declared trusted proxy set, or
|
|
293
|
+
* nothing this guard can stand behind. `ip` is null when this is `unavailable`. */
|
|
294
|
+
clientIpSource?: "runtime" | "trusted-proxy" | "unavailable";
|
|
179
295
|
userAgent?: string | null;
|
|
180
296
|
}) => void;
|
|
181
297
|
}
|