Skip to content

docs(spec): server.trustProxy describe names all three client-address consumers - #25116

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-25058-trustproxy-consumers-count
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-25058-trustproxy-consumers-count

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

…ird client-address consumer

The describe and TSDoc said "two consumers". The per-endpoint declarative
rate limiter keys anonymous callers by the same rule under the same
trustProxy whenever an endpoint's own rateLimit is enabled, independent of
server.security.rateLimit.enabled. Prose only.

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ae58b26f9c099ca44ac479d61df2727dea711d04 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 629a2e411a40052c1dbaa9329c0547afb27b6f1b — the merge of head 9aa6fdc4c8b6ca86101a1c8ec24d1f8f3da59c03 into base ae58b26f9c099ca44ac479d61df2727dea711d04, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 629a2e411a40052c1dbaa9329c0547afb27b6f1b && git checkout 629a2e411a40052c1dbaa9329c0547afb27b6f1b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ae58b26f9c099ca44ac479d61df2727dea711d04 9aa6fdc4c8b6ca86101a1c8ec24d1f8f3da59c03 && git checkout -B drift-repro ae58b26f9c099ca44ac479d61df2727dea711d04 && git merge --no-ff 9aa6fdc4c8b6ca86101a1c8ec24d1f8f3da59c03

node scripts/docs-audit/affected-docs.mjs --json ae58b26f9c099ca44ac479d61df2727dea711d04

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9aa6fdc4c8b6ca86101a1c8ec24d1f8f3da59c03
Local-runs: none
Scope: packages/spec/src/system/stack-server.zod.ts, content/docs/references/system/stack-server.mdx + .changeset/25058-trustproxy-three-consumers.md

① Derived judgments

  • Prose only. The only code hunk is the server.trustProxy TSDoc and .describe() (stack-server.zod.ts:213-231). z.boolean().default(false), the strict object, every key, every refinement and every export are unchanged. The one test that reads the description pins only /X-Forwarded-For/ and /reverse proxy you control/ (stack-server.test.ts:133-139), and both survive.
  • Three consumers, and no fourth caller of resolveClientAddress / resolveRateLimitKey on origin/main (tests excluded):
    • the inbound limiter, which exists only when server.security.rateLimit.enabled is set (inbound-rate-limit.ts:88-89, :270-272, :306; installed at dispatcher-plugin.ts:906-908);
    • the endpoint policy chain, gated on the endpoint's own enabled rateLimit and independent of the server budget (endpoint-policy.ts:159-174, :366-372; registry built at dispatcher-plugin.ts:2115-2118, threaded at :2278-2282);
    • AuthPlugin, given the value by objectstack serve (serve.ts:4263, auth-manager.ts:10059-10066, auth-plugin.ts:2784-2785).
  • The rule itself: header order (x-forwarded-for, then x-real-ip), the leftmost entry, and the transport-peer fallback all match plugin-auth/src/client-address.ts:69-89.
  • Generated docs: the generated mdx row is byte-equal to the evaluated describe (679 chars).

② Semver level

@objectstack/spec patch: the describe string ships in the published Zod runtime, and nothing an author writes or a consumer imports changes. Clause-②: no (prose only …) reads as a bare no (clause2-line.mjs:244-265). No ADR-0087 marker is owed.

③ Boundary flags

  • No governed surface is touched, and content/docs/releases/ is untouched.
  • Out of this card: docs/adr/0069-enterprise-authentication-hardening.md D5 still says "both apply it identically", which now trails this describe by one consumer. That is a Tier H follow-up.
  • Non-blocking: the comments at serve.ts:4259-4262 and auth-manager.ts:10053-10054 are true but not exhaustive.

Implemented-by: claude/issue-25058-trustproxy-consumers-count
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

Seat re-read of the check-runs at landing: all 42 runs on 9aa6fdc4c8 have completed: 36 succeeded, the rest were skipped, and none is red.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 13:34
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 21, 2026 13:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit edcc8ce Oct 21, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25058-trustproxy-consumers-count branch October 21, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(spec): server.trustProxy describe says "two consumers" but the per-endpoint rate limiter is a third

2 participants