@patchstack/connect 0.5.12 → 0.5.16
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 +60 -32
- package/README.md +19 -19
- package/dist/cli.js +1275 -642
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +16 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +16 -9
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +2 -2
- package/dist/protect.cjs +2132 -915
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +135 -17
- package/dist/protect.d.ts +135 -17
- package/dist/protect.edge.js +2195 -985
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +2198 -988
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-47HJRCDX.js → refresh-manifest-DZQOEO3Q.js} +17 -10
- package/dist/refresh-manifest-DZQOEO3Q.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-47HJRCDX.js.map +0 -1
package/dist/protect.d.cts
CHANGED
|
@@ -15,8 +15,9 @@ export interface Protection {
|
|
|
15
15
|
mode: "block" | "dry-run";
|
|
16
16
|
/** Active rules split by phase. */
|
|
17
17
|
rules: { request: unknown[]; response: unknown[]; egress: unknown[] };
|
|
18
|
-
/** (request) => Response (403 when blocked) | null (allow / dry-run). Request phase only.
|
|
19
|
-
|
|
18
|
+
/** (request, ...hostArgs) => Response (403 when blocked) | null (allow / dry-run). Request phase only.
|
|
19
|
+
* Any further arguments are the host's handler arguments, passed on to `peerAddress`. */
|
|
20
|
+
fetchGuard(): (request: Request, ...hostArgs: unknown[]) => Promise<Response | null>;
|
|
20
21
|
/** Screens the request, then the response (secret-leak redaction / withhold). */
|
|
21
22
|
fetch(handler: (request: Request, ...rest: unknown[]) => unknown): (request: Request, ...rest: unknown[]) => Promise<unknown>;
|
|
22
23
|
/**
|
|
@@ -25,11 +26,30 @@ export interface Protection {
|
|
|
25
26
|
* Pass the originating `request` wherever it is available. A response rule can be scoped to a route or a
|
|
26
27
|
* method (`when`), and that scope can only be applied if the engine is given the request the response
|
|
27
28
|
* belongs to — without it, a scoped response rule is delivered, counted as protection, and never matches.
|
|
29
|
+
*
|
|
30
|
+
* The client address is the one resolved when this guard screened that request, if it did; otherwise it
|
|
31
|
+
* is resolved here, and any further arguments are passed to `peerAddress` as the host's handler arguments.
|
|
32
|
+
*
|
|
33
|
+
* A rule that reads only response headers (`response.header.*`, `response.headers`) redacts or blocks on
|
|
34
|
+
* the headers alone, so it is enforced even when the body cannot be screened. A redaction masks only the
|
|
35
|
+
* header value it matched; protecting the body takes a rule on `response.body`, which masks the body and
|
|
36
|
+
* the same text wherever it appears in a header. A withheld response carries only its own `content-type`
|
|
37
|
+
* and `content-length`.
|
|
28
38
|
*/
|
|
29
|
-
screenResponse(response: Response, request?: Request): Promise<Response>;
|
|
39
|
+
screenResponse(response: Response, request?: Request, ...hostArgs: unknown[]): Promise<Response>;
|
|
30
40
|
express(options?: { screenResponses?: boolean }): (req: unknown, res: unknown, next: () => void) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Node / Connect middleware that reads the request body itself and exposes it as `req.body`.
|
|
43
|
+
*
|
|
44
|
+
* A body longer than `maxBodyBytes` (default 1 MiB) has its first `maxBodyBytes` screened, is counted as
|
|
45
|
+
* a `body-cap` skip in `coverage()` / `onSkip`, and is not exposed as `req.body`.
|
|
46
|
+
*/
|
|
31
47
|
node(options?: { maxBodyBytes?: number; screenResponses?: boolean }): (req: unknown, res: unknown, next: () => void) => void;
|
|
32
|
-
/** Present when `egress: true` —
|
|
48
|
+
/** Present when `egress: true` — removes this protection's outbound screen. Outbound calls are
|
|
49
|
+
* screened by every protection that has one registered, and any one of them can refuse a call:
|
|
50
|
+
* a host in one protection's `allowHosts` is still refused when another protection refuses it.
|
|
51
|
+
* When the last one leaves, the original `fetch` and `node:http`/`node:https` functions are
|
|
52
|
+
* restored. `stop()` calls this too; `stopRefresh()` does not. */
|
|
33
53
|
uninstallEgress?: () => void;
|
|
34
54
|
/** Present with a live source — re-fetch + hot-swap the rules once (used by the loop + push).
|
|
35
55
|
* Resolves with the outcome of the attempt: `ok: false` means the rules in force came from the
|
|
@@ -46,7 +66,8 @@ export interface Protection {
|
|
|
46
66
|
* the configured refresh secret (a push/zero-day trigger). No secret set → the handler 404s. */
|
|
47
67
|
refreshHandler?: () => (request: Request) => Promise<Response>;
|
|
48
68
|
/** Stops everything with a timer or a buffer behind it: the refresh loop, the block log, the
|
|
49
|
-
* detection reporter (flushing what it holds)
|
|
69
|
+
* detection reporter (flushing what it holds), and this protection's outbound screen. Always
|
|
70
|
+
* present, and safe to call twice. */
|
|
50
71
|
/**
|
|
51
72
|
* Stop everything holding a timer or a buffer.
|
|
52
73
|
*
|
|
@@ -62,13 +83,33 @@ export interface Protection {
|
|
|
62
83
|
* reporter is finished with it, not that the underlying request has ended.
|
|
63
84
|
* - A runtime that terminates the process regardless still wins, whatever this resolves.
|
|
64
85
|
*
|
|
65
|
-
*
|
|
66
|
-
* dropped, and `detectionHealth()` reports each
|
|
67
|
-
*
|
|
86
|
+
* Both reporters account for what they held. Every detection event ends up delivered, refused or
|
|
87
|
+
* dropped, and `detectionHealth()` reports each; every block-log record ends up delivered, failed or
|
|
88
|
+
* dropped, and `blockLogHealth()` reports those.
|
|
68
89
|
*/
|
|
69
90
|
stop: () => Promise<void>;
|
|
70
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* How often the guard passed something through without inspecting it, in the cases it can observe,
|
|
93
|
+
* keyed `<phase>:<reason>` (for example `request:body-cap`, `response:live-stream`). Zero skips means
|
|
94
|
+
* nothing the guard could see was bypassed, not that nothing was.
|
|
95
|
+
*/
|
|
96
|
+
coverage(): { skipped: Record<string, number> };
|
|
97
|
+
/** Stops the rule refresh only — the poll loop and its recovery retries. The reporters and this
|
|
98
|
+
* protection's outbound screening keep running; use `stop()` to end those too. */
|
|
71
99
|
stopRefresh: () => Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Where the rules in force came from, and whether the most recent resolution was clean — the same
|
|
102
|
+
* shape `refresh()` resolves with, kept current by boot, every refresh, and recovery.
|
|
103
|
+
*
|
|
104
|
+
* With a live source, `ok: false` means the guard is not running the rules the source would give it
|
|
105
|
+
* now: `origin` says what it is running instead. When no `refreshMs` loop is configured and the first
|
|
106
|
+
* resolution was not clean, the guard retries on a lengthening schedule until one is.
|
|
107
|
+
*/
|
|
108
|
+
readonly ruleSource: {
|
|
109
|
+
ok: boolean;
|
|
110
|
+
origin: "api" | "cache" | "bundled" | "empty";
|
|
111
|
+
reason?: string;
|
|
112
|
+
};
|
|
72
113
|
/** Whether this guard reports security events, and if not, why not.
|
|
73
114
|
*
|
|
74
115
|
* Reporting is on for a site enrolled in Patchstack-managed mitigation that is running managed rules
|
|
@@ -115,6 +156,20 @@ export interface Protection {
|
|
|
115
156
|
lastAcknowledgedAt: string | null;
|
|
116
157
|
};
|
|
117
158
|
};
|
|
159
|
+
/** Present when block-log reporting is on — where the block records went, in records. Carries no
|
|
160
|
+
* request data. */
|
|
161
|
+
blockLogHealth?: () => {
|
|
162
|
+
/** Records the queue accepted. Each ends up delivered, failed, or dropped by a shutdown. */
|
|
163
|
+
recorded: number;
|
|
164
|
+
/** Records in a batch the endpoint acknowledged. */
|
|
165
|
+
delivered: number;
|
|
166
|
+
/** Records in a batch that was refused, or that could not be sent (including a failed token exchange). */
|
|
167
|
+
failed: number;
|
|
168
|
+
/** Records a shutdown discarded, plus records turned away because the queue was full. */
|
|
169
|
+
dropped: number;
|
|
170
|
+
/** Records waiting to be sent now. */
|
|
171
|
+
queued: number;
|
|
172
|
+
};
|
|
118
173
|
}
|
|
119
174
|
|
|
120
175
|
/**
|
|
@@ -220,7 +275,8 @@ export interface CreateProtectionOptions {
|
|
|
220
275
|
detectionFlushMs?: number;
|
|
221
276
|
/** Optional Source-Host header for connector hostname checks. */
|
|
222
277
|
sourceHost?: string;
|
|
223
|
-
/** Optional fetch override (tests).
|
|
278
|
+
/** Optional fetch override for the block-log and detection reporters (tests). The rules client and
|
|
279
|
+
* the manifest re-post use the global `fetch`. */
|
|
224
280
|
fetchImpl?: typeof fetch;
|
|
225
281
|
/**
|
|
226
282
|
* Re-fetch and hot-swap the live rules every N ms. For long-lived runtimes that aren't restarted
|
|
@@ -239,12 +295,19 @@ export interface CreateProtectionOptions {
|
|
|
239
295
|
* During a refresh, also re-post the dependency manifest (the runtime counterpart to `scan`) so a
|
|
240
296
|
* dependency added after boot — e.g. via `npm install <pkg>`, which fires no npm lifecycle hook —
|
|
241
297
|
* is reported and enforced without a restart. Defaults on when a `siteUuid` is set; set false to
|
|
242
|
-
* refresh rules only. Only meaningful with `refreshMs > 0`
|
|
298
|
+
* refresh rules only. Only meaningful with a Pulse `siteUuid` and a refresh path: `refreshMs > 0` or a
|
|
299
|
+
* refresh secret. A manual `refresh()` re-posts too when either is configured.
|
|
243
300
|
*/
|
|
244
301
|
reportManifest?: boolean;
|
|
245
302
|
/** Directory the manifest re-scan reads the lockfile from during a refresh. Default process.cwd(). */
|
|
246
303
|
cwd?: string;
|
|
247
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* Directory for the last-known-good rule cache (disk — the default cache backend).
|
|
306
|
+
*
|
|
307
|
+
* A cache belongs to the source it was fetched for: the site UUID or token, and the rules endpoint.
|
|
308
|
+
* One written for any other source — or by a version that did not record its source — reads as
|
|
309
|
+
* empty, so it is never enforced, revalidated or used to attribute detections for this guard.
|
|
310
|
+
*/
|
|
248
311
|
cacheDir?: string;
|
|
249
312
|
/**
|
|
250
313
|
* Pluggable last-known-good cache, for runtimes without a filesystem (Workers/Deno). Overrides
|
|
@@ -254,11 +317,20 @@ export interface CreateProtectionOptions {
|
|
|
254
317
|
read(): unknown | Promise<unknown>;
|
|
255
318
|
write(envelope: unknown): unknown | Promise<unknown>;
|
|
256
319
|
};
|
|
320
|
+
/**
|
|
321
|
+
* The transport peer of a Fetch request, for runtimes where the host knows it and the `Request` does
|
|
322
|
+
* not — e.g. `(req, info) => info.remoteAddr.hostname` on Deno, `(req, server) => server.requestIP(req)?.address`
|
|
323
|
+
* on Bun. Called with the request the host served and the arguments its handler received (passed through
|
|
324
|
+
* `fetch(handler)`, `fetchGuard()(request, ...args)` and `screenResponse(response, request, ...args)`).
|
|
325
|
+
* The result counts as the peer for client address resolution, including `trustedProxy`. A throw, or a result that is not an address, supplies no peer.
|
|
326
|
+
*/
|
|
327
|
+
peerAddress?: (request: Request, ...hostArgs: unknown[]) => string | null | undefined;
|
|
257
328
|
/**
|
|
258
329
|
* Declare which peers are this deployment's own reverse proxies, so a forwarded header can be believed.
|
|
259
330
|
*
|
|
260
|
-
* With no policy, the client address is whatever the transport observed — the socket peer on Node,
|
|
261
|
-
*
|
|
331
|
+
* With no policy, the client address is whatever the transport observed — the socket peer on Node, the
|
|
332
|
+
* `peerAddress` result on a Fetch runtime, and nothing at all where neither is available, in which case
|
|
333
|
+
* the provenance reads `unavailable` (and, with a policy set, the guard warns once). A
|
|
262
334
|
* forwarded header is never trusted implicitly: it is ordinary request input that any caller can send.
|
|
263
335
|
*
|
|
264
336
|
* A policy must say WHO is trusted, not just which header to read. Declare at least one of:
|
|
@@ -286,7 +358,40 @@ export interface CreateProtectionOptions {
|
|
|
286
358
|
header?: string;
|
|
287
359
|
isTrusted?: (ip: string) => boolean;
|
|
288
360
|
};
|
|
289
|
-
/**
|
|
361
|
+
/** How long the boot-time rules fetch may take before the guard starts on its cache or bundled
|
|
362
|
+
* fallback. Default 5000ms. Refreshes use `refreshTimeoutMs`. */
|
|
363
|
+
bootTimeoutMs?: number;
|
|
364
|
+
/** How long a refresh's rules fetch may take. Default 30000ms. */
|
|
365
|
+
refreshTimeoutMs?: number;
|
|
366
|
+
/** Largest request body the Fetch path buffers for inspection. A larger body is passed through
|
|
367
|
+
* uninspected and counted as a `request:body-cap` skip. Default 1 MiB. The Node guard takes its own
|
|
368
|
+
* `node({ maxBodyBytes })`. */
|
|
369
|
+
maxBodyBytes?: number;
|
|
370
|
+
/**
|
|
371
|
+
* Apply the valid part of a live rules update when some of its rules fail validation. By default the
|
|
372
|
+
* whole update is refused, the previous rules stay in force and the response is not cached; either
|
|
373
|
+
* way every rejected rule is reported through `onRuleRejected`.
|
|
374
|
+
*/
|
|
375
|
+
acceptPartialBundle?: boolean;
|
|
376
|
+
/** Permit whitelist entries with no `rule_id`, which apply to every rule. Refused by default. */
|
|
377
|
+
allowGlobalWhitelists?: boolean;
|
|
378
|
+
/** Called for each delivered rule that failed validation and is not enforced. Without it, rejections
|
|
379
|
+
* are written to the console. */
|
|
380
|
+
onRuleRejected?: (rejection: { id: string | number; reason: string; accepted?: boolean }) => void;
|
|
381
|
+
/** Called each time the guard passes something through without inspecting it (an oversized or
|
|
382
|
+
* encoded body, a live stream, a binary body, an outbound call it could not resolve). `detail` carries
|
|
383
|
+
* operational context such as sizes and hostnames — keep it server-side. `protection.coverage()`
|
|
384
|
+
* holds the running counts. */
|
|
385
|
+
onSkip?: (skip: {
|
|
386
|
+
phase: Phase;
|
|
387
|
+
reason: string;
|
|
388
|
+
detail?: Record<string, unknown>;
|
|
389
|
+
count: number;
|
|
390
|
+
}) => void;
|
|
391
|
+
/**
|
|
392
|
+
* Override the default response-phase (secret-leak) rule set. A rule that reads only response headers is
|
|
393
|
+
* enforced from the headers, and masks only the header it matched — see `screenResponse`.
|
|
394
|
+
*/
|
|
290
395
|
responseRules?: unknown[];
|
|
291
396
|
/** Override the default egress-phase (SSRF) rule set. */
|
|
292
397
|
egressRules?: unknown[];
|
|
@@ -327,8 +432,21 @@ export interface CreateProtectionOptions {
|
|
|
327
432
|
* only active when `egress` is on and node:dns is available (a no-op on edge runtimes).
|
|
328
433
|
*/
|
|
329
434
|
screenDns?: boolean;
|
|
330
|
-
/**
|
|
435
|
+
/**
|
|
436
|
+
* Redaction mask (string or per-category function). Default "[REDACTED]".
|
|
437
|
+
*
|
|
438
|
+
* In a JSON response, a text-span redaction or encoding may change string values only. When a
|
|
439
|
+
* rewrite in `block` mode would change the document's structure instead — a match inside a key
|
|
440
|
+
* name or across fields, a bare number or literal, or a mask that leaves the document invalid — the
|
|
441
|
+
* whole response is withheld rather than masked. Structural `array_key_value` masking replaces the
|
|
442
|
+
* leaf it targets, and keeps every other value, including each number, exactly as it was spelled.
|
|
443
|
+
*/
|
|
331
444
|
maskWith?: string | ((category?: string) => string);
|
|
445
|
+
/**
|
|
446
|
+
* Operational problems the guard handled without failing a request. Without it, rules that are not
|
|
447
|
+
* current — a failed fetch, a rejected update, held build-scoped rules — are written to the console
|
|
448
|
+
* once per cause instead.
|
|
449
|
+
*/
|
|
332
450
|
onError?: (err: unknown) => void;
|
|
333
451
|
onEgressBlock?: (info: { url: string; host: string | null; method: string }) => void;
|
|
334
452
|
onDetect?: (detection: {
|
|
@@ -361,7 +479,7 @@ export function createSupabaseGuard(opts: {
|
|
|
361
479
|
maxBodyBytes?: number;
|
|
362
480
|
/** Maximum upstream request duration. Default 30 seconds. */
|
|
363
481
|
timeoutMs?: number;
|
|
364
|
-
}): (request: Request) => Promise<Response>;
|
|
482
|
+
}): (request: Request, ...hostArgs: unknown[]) => Promise<Response>;
|
|
365
483
|
|
|
366
484
|
/**
|
|
367
485
|
* Server-function guard: inspect a TanStack server function's decoded args against the same
|
package/dist/protect.d.ts
CHANGED
|
@@ -15,8 +15,9 @@ export interface Protection {
|
|
|
15
15
|
mode: "block" | "dry-run";
|
|
16
16
|
/** Active rules split by phase. */
|
|
17
17
|
rules: { request: unknown[]; response: unknown[]; egress: unknown[] };
|
|
18
|
-
/** (request) => Response (403 when blocked) | null (allow / dry-run). Request phase only.
|
|
19
|
-
|
|
18
|
+
/** (request, ...hostArgs) => Response (403 when blocked) | null (allow / dry-run). Request phase only.
|
|
19
|
+
* Any further arguments are the host's handler arguments, passed on to `peerAddress`. */
|
|
20
|
+
fetchGuard(): (request: Request, ...hostArgs: unknown[]) => Promise<Response | null>;
|
|
20
21
|
/** Screens the request, then the response (secret-leak redaction / withhold). */
|
|
21
22
|
fetch(handler: (request: Request, ...rest: unknown[]) => unknown): (request: Request, ...rest: unknown[]) => Promise<unknown>;
|
|
22
23
|
/**
|
|
@@ -25,11 +26,30 @@ export interface Protection {
|
|
|
25
26
|
* Pass the originating `request` wherever it is available. A response rule can be scoped to a route or a
|
|
26
27
|
* method (`when`), and that scope can only be applied if the engine is given the request the response
|
|
27
28
|
* belongs to — without it, a scoped response rule is delivered, counted as protection, and never matches.
|
|
29
|
+
*
|
|
30
|
+
* The client address is the one resolved when this guard screened that request, if it did; otherwise it
|
|
31
|
+
* is resolved here, and any further arguments are passed to `peerAddress` as the host's handler arguments.
|
|
32
|
+
*
|
|
33
|
+
* A rule that reads only response headers (`response.header.*`, `response.headers`) redacts or blocks on
|
|
34
|
+
* the headers alone, so it is enforced even when the body cannot be screened. A redaction masks only the
|
|
35
|
+
* header value it matched; protecting the body takes a rule on `response.body`, which masks the body and
|
|
36
|
+
* the same text wherever it appears in a header. A withheld response carries only its own `content-type`
|
|
37
|
+
* and `content-length`.
|
|
28
38
|
*/
|
|
29
|
-
screenResponse(response: Response, request?: Request): Promise<Response>;
|
|
39
|
+
screenResponse(response: Response, request?: Request, ...hostArgs: unknown[]): Promise<Response>;
|
|
30
40
|
express(options?: { screenResponses?: boolean }): (req: unknown, res: unknown, next: () => void) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Node / Connect middleware that reads the request body itself and exposes it as `req.body`.
|
|
43
|
+
*
|
|
44
|
+
* A body longer than `maxBodyBytes` (default 1 MiB) has its first `maxBodyBytes` screened, is counted as
|
|
45
|
+
* a `body-cap` skip in `coverage()` / `onSkip`, and is not exposed as `req.body`.
|
|
46
|
+
*/
|
|
31
47
|
node(options?: { maxBodyBytes?: number; screenResponses?: boolean }): (req: unknown, res: unknown, next: () => void) => void;
|
|
32
|
-
/** Present when `egress: true` —
|
|
48
|
+
/** Present when `egress: true` — removes this protection's outbound screen. Outbound calls are
|
|
49
|
+
* screened by every protection that has one registered, and any one of them can refuse a call:
|
|
50
|
+
* a host in one protection's `allowHosts` is still refused when another protection refuses it.
|
|
51
|
+
* When the last one leaves, the original `fetch` and `node:http`/`node:https` functions are
|
|
52
|
+
* restored. `stop()` calls this too; `stopRefresh()` does not. */
|
|
33
53
|
uninstallEgress?: () => void;
|
|
34
54
|
/** Present with a live source — re-fetch + hot-swap the rules once (used by the loop + push).
|
|
35
55
|
* Resolves with the outcome of the attempt: `ok: false` means the rules in force came from the
|
|
@@ -46,7 +66,8 @@ export interface Protection {
|
|
|
46
66
|
* the configured refresh secret (a push/zero-day trigger). No secret set → the handler 404s. */
|
|
47
67
|
refreshHandler?: () => (request: Request) => Promise<Response>;
|
|
48
68
|
/** Stops everything with a timer or a buffer behind it: the refresh loop, the block log, the
|
|
49
|
-
* detection reporter (flushing what it holds)
|
|
69
|
+
* detection reporter (flushing what it holds), and this protection's outbound screen. Always
|
|
70
|
+
* present, and safe to call twice. */
|
|
50
71
|
/**
|
|
51
72
|
* Stop everything holding a timer or a buffer.
|
|
52
73
|
*
|
|
@@ -62,13 +83,33 @@ export interface Protection {
|
|
|
62
83
|
* reporter is finished with it, not that the underlying request has ended.
|
|
63
84
|
* - A runtime that terminates the process regardless still wins, whatever this resolves.
|
|
64
85
|
*
|
|
65
|
-
*
|
|
66
|
-
* dropped, and `detectionHealth()` reports each
|
|
67
|
-
*
|
|
86
|
+
* Both reporters account for what they held. Every detection event ends up delivered, refused or
|
|
87
|
+
* dropped, and `detectionHealth()` reports each; every block-log record ends up delivered, failed or
|
|
88
|
+
* dropped, and `blockLogHealth()` reports those.
|
|
68
89
|
*/
|
|
69
90
|
stop: () => Promise<void>;
|
|
70
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* How often the guard passed something through without inspecting it, in the cases it can observe,
|
|
93
|
+
* keyed `<phase>:<reason>` (for example `request:body-cap`, `response:live-stream`). Zero skips means
|
|
94
|
+
* nothing the guard could see was bypassed, not that nothing was.
|
|
95
|
+
*/
|
|
96
|
+
coverage(): { skipped: Record<string, number> };
|
|
97
|
+
/** Stops the rule refresh only — the poll loop and its recovery retries. The reporters and this
|
|
98
|
+
* protection's outbound screening keep running; use `stop()` to end those too. */
|
|
71
99
|
stopRefresh: () => Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Where the rules in force came from, and whether the most recent resolution was clean — the same
|
|
102
|
+
* shape `refresh()` resolves with, kept current by boot, every refresh, and recovery.
|
|
103
|
+
*
|
|
104
|
+
* With a live source, `ok: false` means the guard is not running the rules the source would give it
|
|
105
|
+
* now: `origin` says what it is running instead. When no `refreshMs` loop is configured and the first
|
|
106
|
+
* resolution was not clean, the guard retries on a lengthening schedule until one is.
|
|
107
|
+
*/
|
|
108
|
+
readonly ruleSource: {
|
|
109
|
+
ok: boolean;
|
|
110
|
+
origin: "api" | "cache" | "bundled" | "empty";
|
|
111
|
+
reason?: string;
|
|
112
|
+
};
|
|
72
113
|
/** Whether this guard reports security events, and if not, why not.
|
|
73
114
|
*
|
|
74
115
|
* Reporting is on for a site enrolled in Patchstack-managed mitigation that is running managed rules
|
|
@@ -115,6 +156,20 @@ export interface Protection {
|
|
|
115
156
|
lastAcknowledgedAt: string | null;
|
|
116
157
|
};
|
|
117
158
|
};
|
|
159
|
+
/** Present when block-log reporting is on — where the block records went, in records. Carries no
|
|
160
|
+
* request data. */
|
|
161
|
+
blockLogHealth?: () => {
|
|
162
|
+
/** Records the queue accepted. Each ends up delivered, failed, or dropped by a shutdown. */
|
|
163
|
+
recorded: number;
|
|
164
|
+
/** Records in a batch the endpoint acknowledged. */
|
|
165
|
+
delivered: number;
|
|
166
|
+
/** Records in a batch that was refused, or that could not be sent (including a failed token exchange). */
|
|
167
|
+
failed: number;
|
|
168
|
+
/** Records a shutdown discarded, plus records turned away because the queue was full. */
|
|
169
|
+
dropped: number;
|
|
170
|
+
/** Records waiting to be sent now. */
|
|
171
|
+
queued: number;
|
|
172
|
+
};
|
|
118
173
|
}
|
|
119
174
|
|
|
120
175
|
/**
|
|
@@ -220,7 +275,8 @@ export interface CreateProtectionOptions {
|
|
|
220
275
|
detectionFlushMs?: number;
|
|
221
276
|
/** Optional Source-Host header for connector hostname checks. */
|
|
222
277
|
sourceHost?: string;
|
|
223
|
-
/** Optional fetch override (tests).
|
|
278
|
+
/** Optional fetch override for the block-log and detection reporters (tests). The rules client and
|
|
279
|
+
* the manifest re-post use the global `fetch`. */
|
|
224
280
|
fetchImpl?: typeof fetch;
|
|
225
281
|
/**
|
|
226
282
|
* Re-fetch and hot-swap the live rules every N ms. For long-lived runtimes that aren't restarted
|
|
@@ -239,12 +295,19 @@ export interface CreateProtectionOptions {
|
|
|
239
295
|
* During a refresh, also re-post the dependency manifest (the runtime counterpart to `scan`) so a
|
|
240
296
|
* dependency added after boot — e.g. via `npm install <pkg>`, which fires no npm lifecycle hook —
|
|
241
297
|
* is reported and enforced without a restart. Defaults on when a `siteUuid` is set; set false to
|
|
242
|
-
* refresh rules only. Only meaningful with `refreshMs > 0`
|
|
298
|
+
* refresh rules only. Only meaningful with a Pulse `siteUuid` and a refresh path: `refreshMs > 0` or a
|
|
299
|
+
* refresh secret. A manual `refresh()` re-posts too when either is configured.
|
|
243
300
|
*/
|
|
244
301
|
reportManifest?: boolean;
|
|
245
302
|
/** Directory the manifest re-scan reads the lockfile from during a refresh. Default process.cwd(). */
|
|
246
303
|
cwd?: string;
|
|
247
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* Directory for the last-known-good rule cache (disk — the default cache backend).
|
|
306
|
+
*
|
|
307
|
+
* A cache belongs to the source it was fetched for: the site UUID or token, and the rules endpoint.
|
|
308
|
+
* One written for any other source — or by a version that did not record its source — reads as
|
|
309
|
+
* empty, so it is never enforced, revalidated or used to attribute detections for this guard.
|
|
310
|
+
*/
|
|
248
311
|
cacheDir?: string;
|
|
249
312
|
/**
|
|
250
313
|
* Pluggable last-known-good cache, for runtimes without a filesystem (Workers/Deno). Overrides
|
|
@@ -254,11 +317,20 @@ export interface CreateProtectionOptions {
|
|
|
254
317
|
read(): unknown | Promise<unknown>;
|
|
255
318
|
write(envelope: unknown): unknown | Promise<unknown>;
|
|
256
319
|
};
|
|
320
|
+
/**
|
|
321
|
+
* The transport peer of a Fetch request, for runtimes where the host knows it and the `Request` does
|
|
322
|
+
* not — e.g. `(req, info) => info.remoteAddr.hostname` on Deno, `(req, server) => server.requestIP(req)?.address`
|
|
323
|
+
* on Bun. Called with the request the host served and the arguments its handler received (passed through
|
|
324
|
+
* `fetch(handler)`, `fetchGuard()(request, ...args)` and `screenResponse(response, request, ...args)`).
|
|
325
|
+
* The result counts as the peer for client address resolution, including `trustedProxy`. A throw, or a result that is not an address, supplies no peer.
|
|
326
|
+
*/
|
|
327
|
+
peerAddress?: (request: Request, ...hostArgs: unknown[]) => string | null | undefined;
|
|
257
328
|
/**
|
|
258
329
|
* Declare which peers are this deployment's own reverse proxies, so a forwarded header can be believed.
|
|
259
330
|
*
|
|
260
|
-
* With no policy, the client address is whatever the transport observed — the socket peer on Node,
|
|
261
|
-
*
|
|
331
|
+
* With no policy, the client address is whatever the transport observed — the socket peer on Node, the
|
|
332
|
+
* `peerAddress` result on a Fetch runtime, and nothing at all where neither is available, in which case
|
|
333
|
+
* the provenance reads `unavailable` (and, with a policy set, the guard warns once). A
|
|
262
334
|
* forwarded header is never trusted implicitly: it is ordinary request input that any caller can send.
|
|
263
335
|
*
|
|
264
336
|
* A policy must say WHO is trusted, not just which header to read. Declare at least one of:
|
|
@@ -286,7 +358,40 @@ export interface CreateProtectionOptions {
|
|
|
286
358
|
header?: string;
|
|
287
359
|
isTrusted?: (ip: string) => boolean;
|
|
288
360
|
};
|
|
289
|
-
/**
|
|
361
|
+
/** How long the boot-time rules fetch may take before the guard starts on its cache or bundled
|
|
362
|
+
* fallback. Default 5000ms. Refreshes use `refreshTimeoutMs`. */
|
|
363
|
+
bootTimeoutMs?: number;
|
|
364
|
+
/** How long a refresh's rules fetch may take. Default 30000ms. */
|
|
365
|
+
refreshTimeoutMs?: number;
|
|
366
|
+
/** Largest request body the Fetch path buffers for inspection. A larger body is passed through
|
|
367
|
+
* uninspected and counted as a `request:body-cap` skip. Default 1 MiB. The Node guard takes its own
|
|
368
|
+
* `node({ maxBodyBytes })`. */
|
|
369
|
+
maxBodyBytes?: number;
|
|
370
|
+
/**
|
|
371
|
+
* Apply the valid part of a live rules update when some of its rules fail validation. By default the
|
|
372
|
+
* whole update is refused, the previous rules stay in force and the response is not cached; either
|
|
373
|
+
* way every rejected rule is reported through `onRuleRejected`.
|
|
374
|
+
*/
|
|
375
|
+
acceptPartialBundle?: boolean;
|
|
376
|
+
/** Permit whitelist entries with no `rule_id`, which apply to every rule. Refused by default. */
|
|
377
|
+
allowGlobalWhitelists?: boolean;
|
|
378
|
+
/** Called for each delivered rule that failed validation and is not enforced. Without it, rejections
|
|
379
|
+
* are written to the console. */
|
|
380
|
+
onRuleRejected?: (rejection: { id: string | number; reason: string; accepted?: boolean }) => void;
|
|
381
|
+
/** Called each time the guard passes something through without inspecting it (an oversized or
|
|
382
|
+
* encoded body, a live stream, a binary body, an outbound call it could not resolve). `detail` carries
|
|
383
|
+
* operational context such as sizes and hostnames — keep it server-side. `protection.coverage()`
|
|
384
|
+
* holds the running counts. */
|
|
385
|
+
onSkip?: (skip: {
|
|
386
|
+
phase: Phase;
|
|
387
|
+
reason: string;
|
|
388
|
+
detail?: Record<string, unknown>;
|
|
389
|
+
count: number;
|
|
390
|
+
}) => void;
|
|
391
|
+
/**
|
|
392
|
+
* Override the default response-phase (secret-leak) rule set. A rule that reads only response headers is
|
|
393
|
+
* enforced from the headers, and masks only the header it matched — see `screenResponse`.
|
|
394
|
+
*/
|
|
290
395
|
responseRules?: unknown[];
|
|
291
396
|
/** Override the default egress-phase (SSRF) rule set. */
|
|
292
397
|
egressRules?: unknown[];
|
|
@@ -327,8 +432,21 @@ export interface CreateProtectionOptions {
|
|
|
327
432
|
* only active when `egress` is on and node:dns is available (a no-op on edge runtimes).
|
|
328
433
|
*/
|
|
329
434
|
screenDns?: boolean;
|
|
330
|
-
/**
|
|
435
|
+
/**
|
|
436
|
+
* Redaction mask (string or per-category function). Default "[REDACTED]".
|
|
437
|
+
*
|
|
438
|
+
* In a JSON response, a text-span redaction or encoding may change string values only. When a
|
|
439
|
+
* rewrite in `block` mode would change the document's structure instead — a match inside a key
|
|
440
|
+
* name or across fields, a bare number or literal, or a mask that leaves the document invalid — the
|
|
441
|
+
* whole response is withheld rather than masked. Structural `array_key_value` masking replaces the
|
|
442
|
+
* leaf it targets, and keeps every other value, including each number, exactly as it was spelled.
|
|
443
|
+
*/
|
|
331
444
|
maskWith?: string | ((category?: string) => string);
|
|
445
|
+
/**
|
|
446
|
+
* Operational problems the guard handled without failing a request. Without it, rules that are not
|
|
447
|
+
* current — a failed fetch, a rejected update, held build-scoped rules — are written to the console
|
|
448
|
+
* once per cause instead.
|
|
449
|
+
*/
|
|
332
450
|
onError?: (err: unknown) => void;
|
|
333
451
|
onEgressBlock?: (info: { url: string; host: string | null; method: string }) => void;
|
|
334
452
|
onDetect?: (detection: {
|
|
@@ -361,7 +479,7 @@ export function createSupabaseGuard(opts: {
|
|
|
361
479
|
maxBodyBytes?: number;
|
|
362
480
|
/** Maximum upstream request duration. Default 30 seconds. */
|
|
363
481
|
timeoutMs?: number;
|
|
364
|
-
}): (request: Request) => Promise<Response>;
|
|
482
|
+
}): (request: Request, ...hostArgs: unknown[]) => Promise<Response>;
|
|
365
483
|
|
|
366
484
|
/**
|
|
367
485
|
* Server-function guard: inspect a TanStack server function's decoded args against the same
|