Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/25058-trustproxy-three-consumers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@objectstack/spec': patch
---

docs(spec): `server.trustProxy`'s description names all three consumers of the client-address rule

Clause-②: no (prose only: no shape, key, default or accepted value moves)

The `.describe()` on `server.trustProxy` said the client-address rule had two consumers: the inbound rate limiter and the authentication plugin. There is a third. A declared endpoint (`apis:`) whose own `rateLimit` is enabled is metered by the endpoint policy chain, which keys anonymous callers by the same rule under the same `trustProxy`, whether or not `server.security.rateLimit.enabled` is set. The description and the TSDoc now name it. No key, type, default or accepted value changes.
2 changes: 1 addition & 1 deletion content/docs/references/system/stack-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const result = ServerRateLimitConfigSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **security** | `{ rateLimit?: object }` | optional | Server-level security configuration. Today: the global inbound rate limit. |
| **trustProxy** | `boolean` | optional (default: `false`) | Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is the transport's own peer address, which a client cannot forge. One client-address rule, two consumers: the inbound rate limiter when `server.security.rateLimit.enabled` is set, and the authentication plugin, whose IP allow-list (`allowed_ip_ranges`) judges and whose login audit stamps (`last_login_ip`) the same address. |
| **trustProxy** | `boolean` | optional (default: `false`) | Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is the transport's own peer address, which a client cannot forge. One client-address rule, three consumers: the inbound rate limiter when `server.security.rateLimit.enabled` is set; the per-endpoint rate limiter of every declared endpoint (`apis:`) whose own `rateLimit` is enabled, whether or not the server-level one is; and the authentication plugin, whose IP allow-list (`allowed_ip_ranges`) judges and whose login audit stamps (`last_login_ip`) the same address. |

### Nested Shape: `StackServerConfig.security`

Expand Down
21 changes: 13 additions & 8 deletions packages/spec/src/system/stack-server.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,19 +210,24 @@ export const StackServerConfigSchema = lazySchema(() => strictObject(
* of authorship — the operator asserting "a proxy I control rewrites this
* header on every request" — and never a default.
*
* Both consumers apply one rule, `resolveClientAddress` from
* Three consumers apply one rule, `resolveClientAddress` from
* `@objectstack/plugin-auth/client-address`: the runtime's inbound rate
* limiter, and `AuthPlugin` (passed this value by `objectstack serve`),
* whose ADR-0069 D5 IP allow-list and D7 `last_login_ip` audit use the
* address it establishes — so a header believed without such a proxy is
* an allow-list bypass as well as a rate-limit one.
* limiter; the endpoint policy chain's per-endpoint rate limiter, for
* every declared endpoint whose own `rateLimit` is enabled, whether or not
* the server-level budget is; and `AuthPlugin` (passed this value by
* `objectstack serve`), whose ADR-0069 D5 IP allow-list and D7
* `last_login_ip` audit use the address it establishes — so a header
* believed without such a proxy is an allow-list bypass as well as a
* rate-limit one.
*/
trustProxy: z.boolean().default(false).describe(
'Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy '
+ 'you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is '
+ "the transport's own peer address, which a client cannot forge. One client-address rule, two consumers: the "
+ 'inbound rate limiter when `server.security.rateLimit.enabled` is set, and the authentication plugin, whose IP '
+ 'allow-list (`allowed_ip_ranges`) judges and whose login audit stamps (`last_login_ip`) the same address.',
+ "the transport's own peer address, which a client cannot forge. One client-address rule, three consumers: the "
+ 'inbound rate limiter when `server.security.rateLimit.enabled` is set; the per-endpoint rate limiter of every '
+ 'declared endpoint (`apis:`) whose own `rateLimit` is enabled, whether or not the server-level one is; and the '
+ 'authentication plugin, whose IP allow-list (`allowed_ip_ranges`) judges and whose login audit stamps '
+ '(`last_login_ip`) the same address.',
),
},
));
Expand Down
Loading