fix(plugin-hono-server): current-user 端点改由「拥有该请求的 kernel」应答 (cloud#927) - #4159
Merged
os-zhuang merged 1 commit intoJul 30, 2026
Merged
Conversation
…ernel that owns the request (cloud#927)
`/auth/me/permissions`, `/auth/me/localization` and `/me/apps` resolved their
answer from the locator captured at REGISTRATION time. On a single-environment
host that is the only kernel, so it is right. On a MULTI-TENANT host it is the
routing shell — and identity is not there: cloud's `ArtifactKernelFactory` mounts
`AuthPlugin` per environment and its host kernel deliberately has none
("AuthPlugin is intentionally NOT injected on the host"). So `getService('auth')`
threw, `resolveCtx` fell to its catch, and every authenticated tenant caller got
`{authenticated:false}`.
Worse than an error: objectui's `MePermissionsProvider` reads
`authenticated:false` as ANONYMOUS and keeps its permissive default
(`return data.authenticated !== true`) because a guest surface has no resolvable
permissions by design. So the console's FLS / `apiOperations` hints were
systematically wrong — not a bypass (the server still enforces per request), but
exactly the client/server divergence `foldWildcardSuperUser` and
`clampManagedObjectWrites` exist to close, one layer up.
These endpoints now consult the host's ADR-0006 `kernel-resolver` seam per
request — the same one the runtime dispatcher has used since Phase 5, so
multi-tenant routing has one strategy rather than two:
- no resolver registered → unchanged (single-environment hosts, `os serve`, the
QA conformance host);
- a kernel → that kernel's `auth`/`objectql`/`metadata`/`security.permissions`;
- `undefined` → the registration-time locator, the seam's contract for an
unscoped/control-plane request;
- a throw → NO answer: the thrown status when it carries one (cloud's
`KernelWarmingError` is 503 + `Retry-After`), else 503
`environment_unavailable`. Falling back would hand back a confidently-wrong
`{authenticated:false}` the client fails OPEN on.
Read LAZILY, per request, never captured at registration: a host may register
these routes before `kernel.bootstrap()` to outrank an `/api/v1/auth/*` wildcard,
which is before the plugin registering the resolver has run `init()`. The three
handlers are wrapped so the per-request locator SHADOWS the registration-time
`ctx`, leaving the bodies byte-identical rather than threading it read by read.
`CurrentUserEndpointsContext` gains an optional `getKernel()` — the
`defaultKernel` the seam takes. `PluginContext` already satisfies it, so
plugin-mounting hosts need no change; a hand-rolled locator should add it (the
changeset carries the FROM → TO).
Tests: 12 new (149 total in this package). Env kernel answers and OUTRANKS a host
that has its own auth; all three endpoints resolve; the resolver receives the
prefix-stripped `routePath` + the host kernel + real `Headers`; no-resolver /
`undefined` / default-kernel / no-`getKernel` all keep the old answer; warming
503 carries `Retry-After`; a status-less failure is 503 and explicitly NOT
`{authenticated:false}`; a 4xx passes through. Affected set 73/73 packages; all
lint-job and spec-artifact gates pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gEeLZ4oTeSXG6fKG1r3vd
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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.
cloud#927 的框架侧。#4144 补的是"有没有提供者",这条是"哪个 kernel 提供"——两件不同的事,后者在多租户宿主上一直是错的。
问题:注册期的定位器 ≠ 拥有该请求的 kernel
这三条端点从注册时捕获的服务定位器解析答案。单环境宿主上那就是唯一的 kernel,因此正确。多租户宿主上那是路由外壳,而身份不在那里:
ArtifactKernelFactory把AuthPlugin挂在每个环境的 kernel 上(per-project HKDF secret);objectos-stack.ts原话:"AuthPlugin is intentionally NOT injected on the host";registerService('auth', …)只有plugin-auth一处。于是
ctx.getService('auth')在宿主上抛错 →resolveCtx落到 catch → 返回undefined→ 端点走匿名分支:这比报错更糟:objectui 的
MePermissionsProvider把authenticated:false当作匿名并保持宽松默认(return data.authenticated !== true)—— 匿名/公开面没有可解析的权限是设计使然,锁死每个字段会把公开表单弄坏。所以已登录 tenant 用户的 console,其 FLS /apiOperations提示是系统性错误的。不是权限绕过(ADR-0057 D10 起服务端才是权威闸门,仍然逐请求强制),但正是foldWildcardSuperUser/clampManagedObjectWrites那一整套整形逻辑存在的目的(让客户端提示与服务端实际强制一致),只不过错在上一层。改法:走宿主已有的
kernel-resolver缝框架早就为这件事声明了规范缝:
kernel-resolver(ADR-0006 Phase 5),dispatcher 从那时起一直在用。这三条端点不查它,才是缺陷本身。 现在改为逐请求查询,多租户路由从此只有一份策略而不是两份。四种结果,两条失败路径都是刻意的:
os serve、QA conformance 宿主零差异。auth/objectql/metadata/security.permissions应答。这就是修复。undefinedKernelWarmingError是 503 +Retry-After),否则 503environment_unavailable。退回默认 kernel 会交回一个自信而错误的{authenticated:false},客户端据此 fail-OPEN —— 严格劣于一个可重试的错误。逐请求惰性读取,绝不在注册时捕获:宿主可能在
kernel.bootstrap()之前注册这些路由(为了抢在/api/v1/auth/*通配之前,见 cloud#924),而那早于注册 resolver 的插件跑init()。测试里 resolver 是在registerCurrentUserEndpoints之后才装进服务表的,正是为了钉住这一点。三个 handler 用一层包装,让逐请求定位器遮蔽(shadow)注册期的
ctx—— handler 体一行未改,而不是逐个读去穿参数。影响面 / FROM → TO
CurrentUserEndpointsContext增加可选getKernel(): unknown,即该缝需要的defaultKernel。PluginContext本来就满足,所以挂HonoServerPlugin的宿主无需改动;自己手写定位器的宿主应该补上:registerCurrentUserEndpoints({ rawApp: httpServer.getRawApp(), - ctx: { getService: (n) => kernel.getService(n) }, + ctx: { getService: (n) => kernel.getService(n), getKernel: () => kernel }, });不补的话,多租户宿主问不到"谁拥有这个请求",就退回旧的(错误的)来源 —— 是静默降级而非中性选择,所以即使当下是单环境也值得补。
已知代价(issue 里点名要求评估的两条)
多租户宿主上每个这三条的请求现在都会走一次
resolveKernel,对 cloud 而言就是resolveByHostname(冷分片会构建 tenant driver,可能数秒)+KernelManager.getOrCreate(可能冷建 kernel)。两点判断:Retry-After正是同一宿主上/api/v1/auth/*(AuthProxyPlugin)和 dispatcher 已经在给的形状,客户端可以重试;而旧行为是一个不可重试、静默 fail-open 的 200。验证
plugin-hono-server全量turbo run test --affectedcheck:authz-resolver、check:wildcard-fallthrough、check:error-code-casing)packages/spec八个生成物门禁tsc --noEmit新增 12 例:
{authenticated:false});auth时仍然优先环境 kernel —— 钉住"这是解析取胜"而不只是"填了个空洞";routePath(cloud 的 resolver 对它施加控制面跳过策略)、第二参是宿主 kernel、headers 是真的Headers;undefined/返回默认 kernel/没有getKernel—— 四种都保持旧答案;Retry-After: 4;{authenticated:false};关联
KernelResolver缝)。🤖 Generated with Claude Code
https://claude.ai/code/session_016gEeLZ4oTeSXG6fKG1r3vd
Generated by Claude Code