You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding: seven better-auth /admin/ routes refuse platform admins and have no ObjectStack consumer — decide whether they are capability or just vendor surface #9969
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-gate bucket already pins the refusal side, so no silent drift.
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:
/admin/create-user is the ONLY admin endpoint with the header-less-server-call trust shape (it resolves the session optionally and gates only if (session)). Every other one declares use: [adminMiddleware], which throws UNAUTHORIZED when no session resolves — so the "call it server-side without headers" trick that admin-user-endpoints.ts uses for create-user does not generalise to any of these seven.
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
adminplugin cannot be pointed at ObjectStack's platform-admin predicate, so every route it gates refuses platform admins. Its PR re-mounts the two that asys_useraction 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 — notype: 'api'action, no console call, no SDK row that reaches them, no doc that tells a user to call them. They are surface therawApp.all(${basePath}/*)catch-all publishes because better-auth registers it.The greppable list, so this does not have to be re-derived:
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_SURFACEinauth-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).better-auth-gatebucket already pins the refusal side, so no silent drift.user.role === 'admin'scalar ADR-0068 D2 stopped synthesizing #9652 established.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-useris the one worth watching: it is the closest to a real capability (and it carries the break-glass last-local-credential guard inauth-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:
/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.