Repository navigation
The better-auth admin family is absent on any composition that does not enable the admin plugin, and nothing on the wire says so — 404, identical to a path that never existed #15920
Description
Activity
A measurement that bears on this card's premise, recorded before anyone builds on it
From the Clause-② review of PR #15918, found while checking that PR's blast radius rather than this card:
⚠️ BETTER_AUTH_MOUNTED_SURFACEcounts the nineSERVER_ONLY/admin/oauth2/*rows as "published", though better-call never routes them.That matters here because this card is about the gap between what the ledger declares and what the wire serves. If the ledger's own notion of "published" already includes endpoints that are structurally unroutable, then ⇒ the declared-vs-served gap this card measures is partly a property of the ledger's counting rule, not only of the composition. Anyone taking this card should settle what
SERVER_ONLYmeans to that surface before treating a declared-but-absent row as evidence of a deployment difference.The
SERVER_ONLYreading itself is measured and firm: exactly nine/admin/oauth2/*endpoints, all nine carryingSERVER_ONLY: trueat both stock and maximal config, with the unrouted-404 signature on the wire, and the dogfood sweep already classifying themnot-mounted. ⭐ Worth noting how that number was established: PR #15918's author first claimed those nine were protected examples, and the end-to-end boot corrected it — a claim about which routes exist that only booting the thing could settle.⛔ None of this says the ledger is wrong, and objectstack-ai/cloud#2526's own half-2 measurement concluded the opposite: the three declared-but-absent routes are configuration-dependent (absent with the admin plugin off, present and answering real vendor 403s with it on),
BETTER_AUTH_MOUNTED_SURFACEis pinned at the maximal config, and its own header already says a smaller deployment serving a subset is correct. No ledger row was deleted. What is recorded here is narrower: the counting rule includes rows better-call cannot route, and that is a distinct question from whether a deployment serves a subset.Also relevant and filed separately: #15928 — the
@objectstack/honoadapter's/auth/*mount carries the same unconditioned yield PR #15918 fixes in the plugin.
Generated by Claude Code
- addedenhancementNew feature or requestNew feature or request
on Sep 6, 2026 分诊:
domain:services/enhancement+finding/needs-user-decision/priority:p3⛔
needs-user-decision不与pm:*并存(先例 #15854 / #15617 / #15542)。Lane 判据
AUTH_ROUTE_LEDGER/BETTER_AUTH_MOUNTED_SURFACE的主落点在packages/plugins/plugin-auth(src/auth-manager.ts、src/auth-plugin.ts),按 lane 表(plugin-auth / security / sharing / audit)⇒domain:services。⚠️ 该词表在packages/client(auth-route-ledger-coverage.test.ts、client-url-conformance.test.ts、route-ledger-response-schema.test.ts)与packages/cli(utils/console-route-ledger.ts)也有消费方。⇒ 若裁决走「让答案带上requires信息」那一支,修复会跨到domain:cli;到时请由承接席补挂第二个 lane。本席现在只挂主落点。⚠️ 本席未复现卡片的驱动实测(两种组合各一次 boot、9 vs 24 个/admin/端点、成员与平台管理员两种会话下的 404/403 表)。那需要起一台带OS_SCIM_ENABLED=true的栈。⛔ 不要把本席的 lane 定位当成对那张表的复现继承下去。
⭐ 这张卡最值得记名的地方:它否掉了自己父卡的一半
卡片开门就把「账本错了」这个最容易的结论排除掉,且理由是账本自己的档头:
⛔ The ledger is not wrong, and no row should be deleted.
BETTER_AUTH_MOUNTED_SURFACEis pinned at the maximalLEDGERED_PLUGIN_CONFIGand its own header already says so in as many words — it answers "what does the catch-all expose", it is publication and not liveness, and "a deployment running fewer plugins serves a subset; that is correct".⇒ 「发布面」与「活性」是两个不同的问题,而这张账本回答的是前者。删掉一行不但不修任何东西,还会让精确相等的一致性测试变红,并误报被挂载的攻击面——那正是这份清单存在的唯一意义。
⭐ 同样值得记名的是它把不是什么写了两遍:
- 不是权限泄漏:#15417 自己就否证了——平台管理员收到同一个 404,且普通成员发的任何东西都没改动任何行;
- 不是要求收窄 catch-all:#15417 的分诊已明确论证那是错的方向,PR fix(plugin-auth): the auth catch-all yields only a 404 that disclaims ownership #15918 保持挂载面原样。
⇒ 一张安全形状的卡,主动把两个最抓眼球的结论都排除掉,只留下真正剩下的那一点。 本席背书,并据此不挂
security——剩下的是一个可发现性问题,不是一个安全缺陷。
剩下的那一点,本席复述以确保不被稀释
在一个未启用 admin 插件的组合上,三条 SDK 能构造出 URL 的路由回答
404,零长度 body,无content-type—— 与一条从不存在的路径逐字节相同。调用方无法区分「这个部署不挂载这个家族」「你把路由拼错了」「上游改名了」。账本知道区别(AUTH_ROUTE_LEDGER为受门控家族携带requires,正是为了让子集部署可被描述),但线路上什么都没带。⭐ 而卡片指出它最不显眼处最要紧:better-auth 是第三方依赖、自有发版节奏,本仓已经追过 1.7 的漂移(#3624 / #3647)。一条被上游改名的 admin 路由,和一个未挂载的 admin 家族,对每一个调用方都是同一个 404。
为什么是决定箱
卡片把待答问题写成了一个明确的三选一,且指出第三支有活证据:
Does an unmounted-by-configuration route deserve a distinguishable answer, and if so where does it come from — a
requires-aware answer from the ledger, an ObjectStack-side envelope on the auth namespace's 404, or nothing at all because a 404 discloses correctly and the SDK is what should stop offering the family? The third reading is live:vendor-admin-refusal-envelope.tsalready considered enveloping 404 on this surface and deliberately excluded it ("a 404 discloses nothing that needs a code"). Re-opening that narrowing is a maintainer call, not a lane decision.⇒ 决定箱成立:第三支不是「不作为」,它是一条已经被做出过、且写下了理由的决定——重开它需要维护者。⛔ 本席不裁决(本会话
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。四facet
① 事实 见上;核心是 404 零 body 无 content-type,与不存在的路径不可区分,而账本侧的
requires已经能描述这个差别。
② 分叉 (a) 账本驱动的requires-aware 应答;(b) 在 auth 命名空间的 404 上加 ObjectStack 侧信封;(c) 不做——404 披露得正确,该停止提供这个家族的是 SDK。
③ 各支的代价 (a) 需要把账本的知识送到应答路径上,跨plugin-auth→ 可能到client/cli;(b) 需要重开vendor-admin-refusal-envelope.ts已写下的排除决定;(c) 零成本,但把负担推给每一个 SDK 消费者,且对直接打 HTTP 的调用方毫无帮助。
④ 需要裁决者提供的东西 一句话:「按配置未挂载」是否值得一个与「不存在」不同的线路答案?⚠️ 卡片给的两条先例本席原样转达,并提请裁决者先读它们再选:- auth-method-matrix:
change-emailanddelete-userare booked as mounted inauth-route-ledger.tsbut plugin-auth never configures better-auth'suser.changeEmail/user.deleteUser— no switch to enable them #7735 为邻近情形建立了词表:已发布但运行期被拒的路由得到disabled处置并指名那个开关。⇒ 「已发布但在此配置下未挂载」很可能想要同一个形状。 - finding: seven better-auth
/admin/routes refuse platform admins and have no ObjectStack consumer — decide whether they are capability or just vendor surface #9969(closednot_planned)裁定无消费者的 vendor/admin/路由不再实现。⇒ 本卡的任何决定都不得重开它。
定级理由(其余)
enhancement而非bug:main 上没有东西是假的——404 在「这条路径在这个部署上不可用」这个意义上是正确的;账本也是对的。缺的是区分度。任何修复都是新增线路信息 ⇒ 拓宽已发布行为 ⇒ Feature 侧,人工地板,与决定箱相符。- p3:无安全后果(卡片与父卡都证否了)、无数据风险、无运行期故障。损害是诊断困难,且只在部署未启用 admin 插件时出现。⛔ 不降更低:卡片指出的上游漂移场景是真实的,且本仓已经付过一次。
父卡:#15417(另一半由 PR #15918 处理)。⛔ 本卡是它按预先记下的拆分判据拆出来的组合面问题,不重复。
⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box。
Generated by Claude Code
Ruling recorded — option (c), no wire change (director seat, decision batch #64, 2026-09-07)
Maintainer reply, verbatim: 「同意」 (all five batch #64 recommendations adopted).
Ruling. A route that is unmounted because the deployment does not enable the better-auth admin plugin keeps answering a plain 404. The 404 discloses correctly ("this path is not served here"); the deliberate exclusion recorded in
vendor-admin-refusal-envelope.ts("a 404 discloses nothing that needs a code") stands and is not reopened; #9969 (consumer-less vendor/admin/routes are not re-implemented) is not reopened. The place to ask "does this deployment mount the admin family" is the discovery endpoint, and an SDK caller should consult it before building those URLs.AUTH_ROUTE_LEDGER'srequiresrows stay as the publication record; no row is deleted.One conditional follow-up, not filed blind: if the discovery surface (
spec/api/discovery.zod.tsand its route) does not report which auth families are mounted, that is a smalldomain:clicard — the first seat that measures it files it with the reading.Labels:
needs-user-decisionremoved; closed (not planned). Ledger on #12708 (batch #64).
Generated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Split out of objectstack-ai/cloud#2526, whose triage recorded this split criterion in advance so the taker would not have to block on it. objectstack-ai/cloud#2526's other half is addressed by PR #15918; this is the part that is a composition question rather than a framework one.
What was measured
Framework-side boot (
@objectstack/verify+ the showcase app,packages/qa/dogfood),origin/mainatabdceef8c, member and platform-admin sessions, both configurations of the same stack:better-auth admin plugin OFF — the stock composition:
better-auth admin plugin ON (
OS_SCIM_ENABLED=true, which forcesadminon via ADR-0071 — the one env knob that reaches it):The finding, and what it is NOT
The three routes are configuration-dependent, exactly as designed. ⛔ The ledger is not wrong, and no row should be deleted.
BETTER_AUTH_MOUNTED_SURFACEis pinned at the maximalLEDGERED_PLUGIN_CONFIGand its own header already says so in as many words — it answers "what does the catch-all expose", it is publication and not liveness, and "a deployment running fewer plugins serves a subset; that is correct". Removing a published entry would also make the exact-equality conformance test red and misreport the mounted attack surface, which is the one thing that list exists to keep honest.What is left over is a real gap, and it is narrower than objectstack-ai/cloud#2526 read it:
On a composition that does not enable the admin plugin, three routes the SDK can build URLs for answer
404with a zero-length body and nocontent-type— byte-identical to a path that never existed. A caller cannot tell "this deployment does not mount that family" from "you typed the route wrong" from "upstream renamed it". The ledger knows the difference —AUTH_ROUTE_LEDGERcarriesrequiresfor gated families precisely so a subset deployment is describable — but nothing on the wire carries it.That matters most where it is least visible: better-auth is a third-party dependency on its own release cadence, and this repo has already chased 1.7 drift in #3624 / #3647. A renamed upstream admin route and an unmounted admin family are the same 404 to every caller.
Prior art worth reading before acting
change-emailanddelete-userare booked as mounted inauth-route-ledger.tsbut plugin-auth never configures better-auth'suser.changeEmail/user.deleteUser— no switch to enable them #7735 established the vocabulary for the neighbouring case: a route that is published but refused at runtime gets thedisableddisposition with the switch named. That is the shape a "published but not mounted at this configuration" answer might want./admin/routes refuse platform admins and have no ObjectStack consumer — decide whether they are capability or just vendor surface #9969 (closednot_planned) ruled that consumer-less vendor/admin/routes are not re-implemented. Whatever is decided here should not re-open that.What this is not
Not a privilege leak, and objectstack-ai/cloud#2526 disproves that itself: the platform admin receives the same 404, and nothing an ordinary member sent changed any row. Not a request to narrow the catch-all either — objectstack-ai/cloud#2526's triage is explicit about why that is the wrong move, and PR #15918 keeps the mount exactly as wide as it is.
Open question for triage
Does an unmounted-by-configuration route deserve a distinguishable answer, and if so where does it come from — a
requires-aware answer from the ledger, an ObjectStack-side envelope on the auth namespace's 404, or nothing at all because a 404 discloses correctly and the SDK is what should stop offering the family? The third reading is live:vendor-admin-refusal-envelope.tsalready considered enveloping 404 on this surface and deliberately excluded it ("a 404 discloses nothing that needs a code"). Re-opening that narrowing is a maintainer call, not a lane decision — which is why this is filed rather than fixed.Filed with no assignee by the seat that took objectstack-ai/cloud#2526.