Skip to content

Commit edcc8ce

Browse files
docs(spec): server.trustProxy describe names all three client-address consumers (#25116)
Closes #25058 Clause-②: no (prose only: no shape, key, default or accepted value moves) ## What The `server.trustProxy` `.describe()` and TSDoc in `packages/spec/src/system/stack-server.zod.ts` said the client-address rule had two consumers. There are three. The text now names the endpoint policy chain's per-endpoint rate limiter as well, and the generated row in `content/docs/references/system/stack-server.mdx` is regenerated from it (`gen:docs`, as `check:generated` named it stale). Prose only: `z.boolean().default(false)`, the strict object around it and every sibling key are unchanged. Changeset: `@objectstack/spec` `patch`. ## Every consumer, checked on `origin/main` (`ba244f1b48`, merged into this branch) All three reach the one rule, `resolveClientAddress` (`packages/plugins/plugin-auth/src/client-address.ts`), under the one authored `server.trustProxy` that `objectstack serve` reads at `packages/cli/src/commands/serve.ts:4733` (dispatcher) and `:4263` (auth). 1. **Inbound rate limiter**: `packages/runtime/src/security/inbound-rate-limit.ts:156-159` (via `resolveRateLimitKey`, `:306-310`), installed at `packages/runtime/src/dispatcher-plugin.ts:906-908`. It is `null`, so nothing is registered, unless a budget is declared and enabled. That matches the qualifier "when `server.security.rateLimit.enabled` is set". 2. **Endpoint policy chain**: `packages/runtime/src/endpoint-policy.ts:366-372` calls `resolveRateLimitKey` with `trustProxy: input.trustProxy === true` whenever `limiters.limiterFor(endpoint)` returns a limiter, and that happens only when the endpoint's own `rateLimit` is enabled (`deriveBucketConfig`, `:162-174`). The value is threaded at `dispatcher-plugin.ts:2278-2282`. The registry is built unconditionally at `dispatcher-plugin.ts:2115`, so this limiter does not depend on the server-level budget. That matches the qualifier "whether or not the server-level one is". 3. **`AuthPlugin`**: `AuthManager.resolveClientAddress` (`packages/plugins/plugin-auth/src/auth-manager.ts:10059-10066`), used by the D5 allow-list (`auth-plugin.ts:2777-2784`) and the D7 `last_login_ip` stamp. **No fourth.** `git grep` over the whole tree, excluding tests, for `resolveClientAddress`, `resolveRateLimitKey` and `trustProxy` finds no other caller. The other uses (`client-address-forwarding.ts`, `client-address-vendor.ts`, `request-client-address.ts`) are internal to `@objectstack/plugin-auth`, which means they fall under consumer 3. ## Verification (head `9aa6fdc4c8`) - `pnpm --filter @objectstack/spec build`: exit 0. - `pnpm --filter @objectstack/spec check:generated`: exit 0 with "All 14 generated artifacts are up to date". Before `gen:docs` it said `content/docs/references/**` was stale. - `pnpm --filter @objectstack/spec test`: 706 files, 21459 passed and 1 todo. - `pnpm --filter @objectstack/spec test:repo`: 72 files, 1237 passed. - `pnpm --filter @objectstack/spec typecheck`: exit 0. - `node scripts/pm/dispatch-gates.mjs --ran` reported "105 derived famil(ies) accounted for — 104 run, 1 NOT-MEASURED". The changeset gates (`check-empty-changeset`, `check-changeset-no-major`, `check-adr-0087-registration`, `check:changeset-gate-self-tests`) and `check:nul-bytes` all exited 0. - NOT MEASURED: `check:dual-build-cjs-loads`. Reason: it needs every workspace package's `dist/`, and a full build was not run locally. CI builds them. ## Acceptance notes - `docs/adr/0069-enterprise-authentication-hardening.md` (D5 amendment, around line 145) still implies two consumers: "imported back by the runtime's inbound rate limiter, so both apply it identically". It is not edited here because `docs/adr/**` is a governed surface outside this card. carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f2b6cf2 commit edcc8ce

3 files changed

Lines changed: 23 additions & 9 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `server.trustProxy`'s description names all three consumers of the client-address rule
6+
7+
Clause-②: no (prose only: no shape, key, default or accepted value moves)
8+
9+
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.

‎content/docs/references/system/stack-server.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const result = ServerRateLimitConfigSchema.parse(data);
101101
| Property | Type | Required | Description |
102102
| :--- | :--- | :--- | :--- |
103103
| **security** | `{ rateLimit?: object }` | optional | Server-level security configuration. Today: the global inbound rate limit. |
104-
| **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. |
104+
| **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. |
105105

106106
### Nested Shape: `StackServerConfig.security`
107107

‎packages/spec/src/system/stack-server.zod.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,19 +210,24 @@ export const StackServerConfigSchema = lazySchema(() => strictObject(
210210
* of authorship — the operator asserting "a proxy I control rewrites this
211211
* header on every request" — and never a default.
212212
*
213-
* Both consumers apply one rule, `resolveClientAddress` from
213+
* Three consumers apply one rule, `resolveClientAddress` from
214214
* `@objectstack/plugin-auth/client-address`: the runtime's inbound rate
215-
* limiter, and `AuthPlugin` (passed this value by `objectstack serve`),
216-
* whose ADR-0069 D5 IP allow-list and D7 `last_login_ip` audit use the
217-
* address it establishes — so a header believed without such a proxy is
218-
* an allow-list bypass as well as a rate-limit one.
215+
* limiter; the endpoint policy chain's per-endpoint rate limiter, for
216+
* every declared endpoint whose own `rateLimit` is enabled, whether or not
217+
* the server-level budget is; and `AuthPlugin` (passed this value by
218+
* `objectstack serve`), whose ADR-0069 D5 IP allow-list and D7
219+
* `last_login_ip` audit use the address it establishes — so a header
220+
* believed without such a proxy is an allow-list bypass as well as a
221+
* rate-limit one.
219222
*/
220223
trustProxy: z.boolean().default(false).describe(
221224
'Believe `X-Forwarded-For` / `X-Real-IP` when identifying a caller. Declare `true` ONLY when a reverse proxy '
222225
+ 'you control overwrites those headers on every inbound request. Left `false` (the default) the caller IP is '
223-
+ "the transport's own peer address, which a client cannot forge. One client-address rule, two consumers: the "
224-
+ 'inbound rate limiter when `server.security.rateLimit.enabled` is set, and the authentication plugin, whose IP '
225-
+ 'allow-list (`allowed_ip_ranges`) judges and whose login audit stamps (`last_login_ip`) the same address.',
226+
+ "the transport's own peer address, which a client cannot forge. One client-address rule, three consumers: the "
227+
+ 'inbound rate limiter when `server.security.rateLimit.enabled` is set; the per-endpoint rate limiter of every '
228+
+ 'declared endpoint (`apis:`) whose own `rateLimit` is enabled, whether or not the server-level one is; and the '
229+
+ 'authentication plugin, whose IP allow-list (`allowed_ip_ranges`) judges and whose login audit stamps '
230+
+ '(`last_login_ip`) the same address.',
226231
),
227232
},
228233
));

0 commit comments

Comments
 (0)