fix(server-timing): emit admin-gated per-request Server-Timing on the standard server (#3361)#3384
Merged
Merged
Conversation
… standard server (#3361) The per-request, admin-gated `Server-Timing` path (#2408) — an admin sends `X-OS-Debug-Timing: 1`/`json` and sees auth/db/hooks/serialize spans while an ordinary user sees nothing — never emitted on the shipped `os serve`/`dev` server. The disclosure gate the Hono middleware opens is only ever flipped by the runtime dispatcher's `timedResolveExecutionContext`, but the data (`/api/v1/data/*`) and metadata (`/api/v1/meta/*`) routes there are served by `@objectstack/rest`'s `RestServer` (which shadows the Hono plugin's own CRUD), and its identity resolver never opened the gate. Only global mode (`OS_SERVER_TIMING=true`) — which discloses to every caller, not just admins — worked, so the documented admin-gated path was unavailable on the OSS server. - observability: home `isPerfDisclosurePrincipal(ec)` here (the gate's package), the ONE shared definition of "who may pull per-request timings" used by every HTTP entry point. `@objectstack/runtime` re-exports it for back-compat. - rest: `RestServer.resolveExecCtx` opens the gate for an admin/service principal via the carried `posture` rung — the fix that makes `os serve`/`dev` emit. - plugin-hono-server: the standalone CRUD surface's self-contained `resolveCtx` opens the gate too, deriving the rung for the gate decision ONLY (never writing an imprecise `posture` onto the returned context — `ctx.posture` is an enforcement input for Layer 0 tier adjudication, ADR-0099 D1). Tests: a rest integration test driving the real `resolveAuthzContext` → `derivePosture` pipeline (admin opens the gate; member/anon do not) and a Hono e2e test that boots the app and asserts an admin gets `Server-Timing` while a member/anonymous caller does not — the end-to-end coverage the issue calls out as the CI gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHeWEHnvQHiN9f9P1t47Sj
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 23 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 21, 2026 14:06
os-zhuang
pushed a commit
that referenced
this pull request
Jul 21, 2026
… discovery (#3369) Rebased onto main after the #3361 Server-Timing fix landed independently (#3384) and the notifications mark-read e2e landed (#3388). This keeps only the net-new route-parity work; the redundant #3361 fix is dropped in favour of main's canonical version. - plugin-hono-server: make the STANDALONE discovery honest. The static `registerDiscoveryAndCrudEndpoints` list advertised metadata/packages/ analytics/workflow/automation/ai/notifications/i18n/storage/ui — none of which HonoServerPlugin mounts on its own — so a truly standalone deployment advertised a dozen routes that 404 (the literal `declared !== enforced` gap #3369 cites). It now advertises only what it serves (`/data` + `/auth/me/*`). Shadowed by the dispatcher/REST service-aware discovery under `os serve`, so real deployments are unaffected. - runtime: add the route-parity e2e gate. Boots the real hono app the way `os serve` mounts it (hono server + dispatcher plugin, services provisioned), reads `/api/v1/discovery`, and asserts every advertised/dispatcher route is reachable (never 404/405/501) for anonymous AND admin principals, and that discovery is service-aware in both directions (no dead advertisement). Closes the class of "works in the dispatcher, dead on os serve" bugs (#3361/#3362/ MCP 501) with one boot-the-real-server test. Server-Timing and notifications mark-read specifics are covered by #3384 / #3388; this gate does not duplicate them. - http-dispatcher: document why `routes.mcp` is advertised on the `isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on service presence — comment only, keeps the dispatcher symmetric with @objectstack/rest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
os-zhuang
pushed a commit
that referenced
this pull request
Jul 21, 2026
…3369) Adds the "boot-the-real-server" route-parity gate #3369 calls for, rebased onto main after the #3361 Server-Timing fix (#3384) and the notifications mark-read e2e (#3388) landed independently. Keeps only the net-new, non-overlapping work. - runtime: route-parity e2e gate (`route-parity.integration.test.ts`). Boots the real hono app the way `os serve` mounts it (hono server + dispatcher plugin, services provisioned), reads `/api/v1/discovery`, and asserts every advertised / dispatcher-registered route is reachable (never 404/405/501) for an anonymous AND an admin principal, and that discovery is service-aware in both directions (no dead advertisement). Closes the "works in the dispatcher, dead on os serve" class (#3361/#3362/MCP 501) with one test. Does not duplicate the Server-Timing (#3384) / notifications mark-read (#3388) / REST `/data`·`/meta`·`/ui` (@objectstack/client) coverage. - http-dispatcher: document why `routes.mcp` is advertised on the `isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on service presence — comment only, keeps the dispatcher symmetric with @objectstack/rest. Test + comment only; no runtime code changes (no-release changeset). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
os-zhuang
added a commit
that referenced
this pull request
Jul 21, 2026
…3369) (#3386) Adds the "boot-the-real-server" route-parity gate #3369 calls for, rebased onto main after the #3361 Server-Timing fix (#3384) and the notifications mark-read e2e (#3388) landed independently. Keeps only the net-new, non-overlapping work. - runtime: route-parity e2e gate (`route-parity.integration.test.ts`). Boots the real hono app the way `os serve` mounts it (hono server + dispatcher plugin, services provisioned), reads `/api/v1/discovery`, and asserts every advertised / dispatcher-registered route is reachable (never 404/405/501) for an anonymous AND an admin principal, and that discovery is service-aware in both directions (no dead advertisement). Closes the "works in the dispatcher, dead on os serve" class (#3361/#3362/MCP 501) with one test. Does not duplicate the Server-Timing (#3384) / notifications mark-read (#3388) / REST `/data`·`/meta`·`/ui` (@objectstack/client) coverage. - http-dispatcher: document why `routes.mcp` is advertised on the `isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on service presence — comment only, keeps the dispatcher symmetric with @objectstack/rest. Test + comment only; no runtime code changes (no-release changeset). Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3361.
Problem
The per-request, admin-gated
Server-Timingpath (#2408) — an admin sendsX-OS-Debug-Timing: 1(orjson) and seesauth/db/hooks/serializespans while an ordinary user sees nothing — never emitted on the shippedos serve/os dev/os startserver. Only global mode (OS_SERVER_TIMING=true), which discloses to every caller and so is not admin-gated, worked.Root cause
The disclosure gate the Hono middleware opens per-request is only ever flipped open by the runtime dispatcher's
timedResolveExecutionContext(http-dispatcher.ts). But onos serve/devthe data (/api/v1/data/*) and metadata (/api/v1/meta/*) routes are served by@objectstack/rest'sRestServer, which registers its routes instart()and therefore shadows the Hono plugin's own CRUD (registered later onkernel:ready).RestServer.resolveExecCtxresolves the principal but never calledallowPerfDisclosure(), so the gate stayed closed and the header was always withheld.Fix
@objectstack/observability—isPerfDisclosurePrincipal(ec)now lives here, in the package that owns the disclosure gate: the single shared definition of "who may pull per-request timings" (isSystem/principalKindservice·system /posturePLATFORM_ADMIN·TENANT_ADMIN). Prevents any admin-serving path from hand-rolling its own rule and drifting.@objectstack/runtime— re-exports the predicate for back-compat (itstimedResolveExecutionContext+ existing tests are unchanged).@objectstack/rest—RestServer.resolveExecCtxopens the gate for an admin/service principal via the carriedposturerung. This is the fix that makesos serve/devemit.@objectstack/plugin-hono-server— the standalone CRUD surface's self-containedresolveCtxopens the gate too. Because that resolver derives no posture rung, it derives one from the resolved permission-set grants for the gate decision only, on a throw-away object — it never writespostureonto the returned context, sincectx.postureis an enforcement input for Layer 0 cross-tenant tier adjudication (ADR-0099 D1,security-plugin.ts) and only the authoritative resolver may set it.Tests
packages/rest/src/rest-server-timing.test.ts— drives the real resolution pipeline (computeExecCtx→resolveAuthzContext→derivePosture) inside an ambient disclosure gate: aPLATFORM_ADMINopens it, aMEMBERand an anonymous caller do not, and it is a safe no-op when no gate is active.packages/plugins/plugin-hono-server/src/server-timing-e2e.test.ts— boots the Hono app with the real/api/v1/data/:objecthandler and asserts an admin sendingX-OS-Debug-Timinggets aServer-Timingheader while a member (200, no header) and an anonymous caller (401, no header) do not, and that an admin gets nothing without the opt-in header. This is the end-to-end coverage the issue calls out as the CI gap.packages/observability/src/perf-timing.test.ts— unit coverage for the predicate in its new home.Verification
.d.ts): ✅turbo run buildfor rest / plugin-hono-server / runtime — 24 tasks, all green.turbo run testfor observability / rest / plugin-hono-server / runtime — 27 tasks; new suites: observability 41, rest 4, hono e2e + existing 28, runtime 577.A changeset (
patchacross the four packages, which version together in the fixed group) is included.Generated by Claude Code