@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.
@@ -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
- fetchGuard(): (request: Request) => Promise<Response | null>;
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` — restores the original global fetch. */
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). Always present, and safe to call twice. */
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
- * 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.
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
- /** Alias of `stop`, under the name callers already have. */
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` and a Pulse `siteUuid`.
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
- /** Directory for the last-known-good rule cache (disk — the default cache backend). */
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, and
261
- * nothing at all in a runtime that exposes no peer, where the provenance reads `unavailable`. A
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
- /** Override the default response-phase (secret-leak) rule set. */
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
- /** Redaction mask (string or per-category function). Default "[REDACTED]". */
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
- fetchGuard(): (request: Request) => Promise<Response | null>;
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` — restores the original global fetch. */
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). Always present, and safe to call twice. */
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
- * 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.
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
- /** Alias of `stop`, under the name callers already have. */
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` and a Pulse `siteUuid`.
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
- /** Directory for the last-known-good rule cache (disk — the default cache backend). */
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, and
261
- * nothing at all in a runtime that exposes no peer, where the provenance reads `unavailable`. A
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
- /** Override the default response-phase (secret-leak) rule set. */
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
- /** Redaction mask (string or per-category function). Default "[REDACTED]". */
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