Repository navigation
finding: seven better-auth /admin/ routes refuse platform admins and have no ObjectStack consumer — decide whether they are capability or just vendor surface #9969
Description
Split out of #9652 while implementing its Option-2 fallback. Filed unassigned; observation, not a repro-and-fix.
#9652 measured that better-auth 1.7.1's admin plugin cannot be pointed at ObjectStack's platform-admin predicate, so every route it gates refuses platform admins. Its PR re-mounts the two that a sys_user action calls (ban-user, unban-user); #9968 carries the two that need a decision (impersonate-user, set-role). These seven are the remainder, and what separates them is that a repo-wide grep for their paths finds no ObjectStack consumer at all — no type: 'api' action, no console call, no SDK row that reaches them, no doc that tells a user to call them. They are surface the rawApp.all(${basePath}/*) catch-all publishes because better-auth registers it.
The greppable list, so this does not have to be re-derived:
POST /api/v1/auth/admin/remove-user
POST /api/v1/auth/admin/revoke-user-session
POST /api/v1/auth/admin/revoke-user-sessions
POST /api/v1/auth/admin/list-user-sessions
POST /api/v1/auth/admin/update-user
GET /api/v1/auth/admin/list-users
GET /api/v1/auth/admin/get-user
All seven answer a platform admin 403 YOU_ARE_NOT_ALLOWED_TO_* today, identically to a plain member. They fail closed — this is a broken capability claim, never an escalation.
The decision
Re-implementing all seven as ObjectStack raw mounts is the largest slice of #9652's Option 2 and buys capability nothing in this repo currently pulls. Against that, they ARE published, ledgered (BETTER_AUTH_MOUNTED_SURFACE in auth-route-ledger.ts) and reachable by any client that reads the OpenAPI document, so "advertised but always refuses the only caller entitled to it" is a declared-≠-enforced claim of its own (Prime Directive #10).
- A. Leave them. Record here that the refusal is known and intended pending pull. Zero code; the dogfood sweep's
better-auth-gatebucket already pins the refusal side, so no silent drift. - B. Re-implement on demand — whichever one first gets an ObjectStack consumer gets an ObjectStack mount at that moment, using the pattern Every better-auth-native /admin/ route refuses ObjectStack platform admins — the vendor gates on the legacy
user.role === 'admin'scalar ADR-0068 D2 stopped synthesizing #9652 established. - C. Re-implement all seven now for parity with the surface the ledger publishes.
Recommendation: B, with A as the standing state — startup scope discipline says do not build seven routes nobody calls, and B makes the trigger explicit so the next author does not have to rediscover why they 403. remove-user is the one worth watching: it is the closest to a real capability (and it carries the break-glass last-local-credential guard in auth-manager.ts's before-hook, which stays intact precisely because that route is not shadowed).
Two mechanics worth recording for whoever picks this up, both measured in #9652:
- Shadowing a vendor path with a raw Hono mount silently detaches every better-auth hook keyed on that path.
/admin/remove-usercarries the break-glass guard. Every better-auth-native /admin/ route refuses ObjectStack platform admins — the vendor gates on the legacyuser.role === 'admin'scalar ADR-0068 D2 stopped synthesizing #9652 extracted that guard intolast-local-credential.tsso both call sites share one implementation — reuse it rather than reimplementing. /admin/create-useris the ONLY admin endpoint with the header-less-server-call trust shape (it resolves the session optionally and gates onlyif (session)). Every other one declaresuse: [adminMiddleware], which throwsUNAUTHORIZEDwhen no session resolves — so the "call it server-side without headers" trick thatadmin-user-endpoints.tsuses for create-user does not generalise to any of these seven.
Parent: #9652.
Activity
user.role === 'admin'scalar ADR-0068 D2 stopped synthesizing #9652os-warren commentedon Aug 19, 2026
Triage: graded
pm:blocked·domain:services· type Task, blocked behind the family decision rather than escalated as a second inbox card. Rationale: the maintainer's ruling on #9968 (impersonate-user residue + set-role veto tension, same vendor-admin family, same package) will set the posture for consumerless vendor/admin/surface; this card's outcome follows it. Expected post-ruling grade: adopt the dev report's recommendation — B with A as the standing state (on-demand re-implementation; record the intended refusal inauth-route-ledger.tsso "advertised, refuses the only entitled caller" stops being an undocumented declared≠enforced claim) — unless the #9968 ruling says otherwise. One decision card per family keeps the inbox lean.Blocked-by: #9968
Generated by Claude Code
feat(pm): H4/H14/blocking-index read comment-level Blocked-by lines (#…
feat(pm): H4/H14/blocking-index read comment-level Blocked-by lines (o…
os-zhuang commentedon Aug 24, 2026
分诊席四维分析(维护者 2026-08-24 点名要求;裁决仍归维护者)。
os-decision-facets
一句话问题:七条 better-auth
/admin/路由(remove-user、revoke-sessions、list-users 等)对平台管理员一律 403,且全仓零消费者——留着并记录(A)、按需逐条重实现(B)、还是现在补齐七条(C)?① 实际业务需求:实测零 ObjectStack 消费者(无 action、无 console 调用、无 SDK 行、无文档指引)。没有任何拉动。→ A/B。
② 项目长远合理性:「广告着却拒绝唯一有权的调用者」确是 declared≠enforced,但它已被台账(
BETTER_AUTH_MOUNTED_SURFACE)与 dogfood 的better-auth-gate桶钉成已知边界——不是静默漂移;#9652 已确立按需重挂载的成熟模式(ban/unban 已走过)。→ B。③ 防 AI 犯错:403 是 fail-closed,无法被利用;风险只是读 OpenAPI 的集成者预期落空——A 的记录即缓解。反而 C 会新增七条无人调用、无人演练的安全路由(且其中 shadowing 会静默摘掉 vendor hook,
remove-user上挂着 break-glass 守卫)——未使用的安全面是最容易烂的面。→ B,反对 C。④ 创业阶段不扩散需求:C 是七条没人要的路由。→ B。
推荐:B(第一个出现 ObjectStack 消费者的路由按 #9652 模式当场重挂载),A 为常态记录(四轴同向;即卡面自荐)。
remove-user单独盯——它离真实能力最近且带 break-glass 守卫。裁后本卡关闭(裁决即记录,pm:blocking的下游按解锁扫描处理)。本分析看不见什么:您对企业客户管理面的产品规划——若 Setup 用户管理近期要吃这几条能力,直接点名哪几条,按 B 提前触发。Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Maintainer ruling recorded — B, with A as the standing state; closing
Provenance: maintainer, 2026-08-24, live PM chat, verbatim: 「接受你的建议。」 on the aligned four-facet analysis above (which adopted this card's own recommendation).
Ruled: the seven better-auth
/admin/routes stay as they are — published, ledgered, refusing platform admins fail-closed, with the refusal pinned by the dogfoodbetter-auth-gatebucket. The moment any of them gains an ObjectStack consumer, that route gets an ObjectStack mount at that moment, using the #9652 pattern (reuselast-local-credential.tsfor the break-glass guard; remember shadowing detaches vendor hooks on that path; the create-user header-less trust shape does not generalise). C (build all seven now) is rejected — unexercised security surface.remove-useris the named watch item.Closing not planned — the ruling is the record; the B trigger is written above and needs no open card to fire (the first consumer's own card carries the mount work). Downstream
Blocked-by: #9969lines unlock via the standard scan; seats re-verify on current refs.Generated by Claude Code
fix(auth): authorize before the break-glass guard on /admin/remove-us…
docs(qa): park the 24 partial-coverage gaps of the 17.7 pre-release r…