@patchstack/connect 0.5.15 → 0.5.17

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,12 +83,19 @@ 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>;
72
100
  /**
73
101
  * Where the rules in force came from, and whether the most recent resolution was clean — the same
@@ -128,6 +156,20 @@ export interface Protection {
128
156
  lastAcknowledgedAt: string | null;
129
157
  };
130
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
+ };
131
173
  }
132
174
 
133
175
  /**
@@ -233,7 +275,8 @@ export interface CreateProtectionOptions {
233
275
  detectionFlushMs?: number;
234
276
  /** Optional Source-Host header for connector hostname checks. */
235
277
  sourceHost?: string;
236
- /** 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`. */
237
280
  fetchImpl?: typeof fetch;
238
281
  /**
239
282
  * Re-fetch and hot-swap the live rules every N ms. For long-lived runtimes that aren't restarted
@@ -252,7 +295,8 @@ export interface CreateProtectionOptions {
252
295
  * During a refresh, also re-post the dependency manifest (the runtime counterpart to `scan`) so a
253
296
  * dependency added after boot — e.g. via `npm install <pkg>`, which fires no npm lifecycle hook —
254
297
  * is reported and enforced without a restart. Defaults on when a `siteUuid` is set; set false to
255
- * 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.
256
300
  */
257
301
  reportManifest?: boolean;
258
302
  /** Directory the manifest re-scan reads the lockfile from during a refresh. Default process.cwd(). */
@@ -273,11 +317,20 @@ export interface CreateProtectionOptions {
273
317
  read(): unknown | Promise<unknown>;
274
318
  write(envelope: unknown): unknown | Promise<unknown>;
275
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;
276
328
  /**
277
329
  * Declare which peers are this deployment's own reverse proxies, so a forwarded header can be believed.
278
330
  *
279
- * With no policy, the client address is whatever the transport observed — the socket peer on Node, and
280
- * 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
281
334
  * forwarded header is never trusted implicitly: it is ordinary request input that any caller can send.
282
335
  *
283
336
  * A policy must say WHO is trusted, not just which header to read. Declare at least one of:
@@ -305,7 +358,40 @@ export interface CreateProtectionOptions {
305
358
  header?: string;
306
359
  isTrusted?: (ip: string) => boolean;
307
360
  };
308
- /** 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
+ */
309
395
  responseRules?: unknown[];
310
396
  /** Override the default egress-phase (SSRF) rule set. */
311
397
  egressRules?: unknown[];
@@ -346,7 +432,15 @@ export interface CreateProtectionOptions {
346
432
  * only active when `egress` is on and node:dns is available (a no-op on edge runtimes).
347
433
  */
348
434
  screenDns?: boolean;
349
- /** 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
+ */
350
444
  maskWith?: string | ((category?: string) => string);
351
445
  /**
352
446
  * Operational problems the guard handled without failing a request. Without it, rules that are not
@@ -385,7 +479,7 @@ export function createSupabaseGuard(opts: {
385
479
  maxBodyBytes?: number;
386
480
  /** Maximum upstream request duration. Default 30 seconds. */
387
481
  timeoutMs?: number;
388
- }): (request: Request) => Promise<Response>;
482
+ }): (request: Request, ...hostArgs: unknown[]) => Promise<Response>;
389
483
 
390
484
  /**
391
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,12 +83,19 @@ 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>;
72
100
  /**
73
101
  * Where the rules in force came from, and whether the most recent resolution was clean — the same
@@ -128,6 +156,20 @@ export interface Protection {
128
156
  lastAcknowledgedAt: string | null;
129
157
  };
130
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
+ };
131
173
  }
132
174
 
133
175
  /**
@@ -233,7 +275,8 @@ export interface CreateProtectionOptions {
233
275
  detectionFlushMs?: number;
234
276
  /** Optional Source-Host header for connector hostname checks. */
235
277
  sourceHost?: string;
236
- /** 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`. */
237
280
  fetchImpl?: typeof fetch;
238
281
  /**
239
282
  * Re-fetch and hot-swap the live rules every N ms. For long-lived runtimes that aren't restarted
@@ -252,7 +295,8 @@ export interface CreateProtectionOptions {
252
295
  * During a refresh, also re-post the dependency manifest (the runtime counterpart to `scan`) so a
253
296
  * dependency added after boot — e.g. via `npm install <pkg>`, which fires no npm lifecycle hook —
254
297
  * is reported and enforced without a restart. Defaults on when a `siteUuid` is set; set false to
255
- * 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.
256
300
  */
257
301
  reportManifest?: boolean;
258
302
  /** Directory the manifest re-scan reads the lockfile from during a refresh. Default process.cwd(). */
@@ -273,11 +317,20 @@ export interface CreateProtectionOptions {
273
317
  read(): unknown | Promise<unknown>;
274
318
  write(envelope: unknown): unknown | Promise<unknown>;
275
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;
276
328
  /**
277
329
  * Declare which peers are this deployment's own reverse proxies, so a forwarded header can be believed.
278
330
  *
279
- * With no policy, the client address is whatever the transport observed — the socket peer on Node, and
280
- * 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
281
334
  * forwarded header is never trusted implicitly: it is ordinary request input that any caller can send.
282
335
  *
283
336
  * A policy must say WHO is trusted, not just which header to read. Declare at least one of:
@@ -305,7 +358,40 @@ export interface CreateProtectionOptions {
305
358
  header?: string;
306
359
  isTrusted?: (ip: string) => boolean;
307
360
  };
308
- /** 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
+ */
309
395
  responseRules?: unknown[];
310
396
  /** Override the default egress-phase (SSRF) rule set. */
311
397
  egressRules?: unknown[];
@@ -346,7 +432,15 @@ export interface CreateProtectionOptions {
346
432
  * only active when `egress` is on and node:dns is available (a no-op on edge runtimes).
347
433
  */
348
434
  screenDns?: boolean;
349
- /** 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
+ */
350
444
  maskWith?: string | ((category?: string) => string);
351
445
  /**
352
446
  * Operational problems the guard handled without failing a request. Without it, rules that are not
@@ -385,7 +479,7 @@ export function createSupabaseGuard(opts: {
385
479
  maxBodyBytes?: number;
386
480
  /** Maximum upstream request duration. Default 30 seconds. */
387
481
  timeoutMs?: number;
388
- }): (request: Request) => Promise<Response>;
482
+ }): (request: Request, ...hostArgs: unknown[]) => Promise<Response>;
389
483
 
390
484
  /**
391
485
  * Server-function guard: inspect a TanStack server function's decoded args against the same