Skip to content

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

@os-warren

Split out of #15417, whose triage recorded this split criterion in advance so the taker would not have to block on it. #15417'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/main at abdceef8c, member and platform-admin sessions, both configurations of the same stack:

better-auth admin plugin OFF — the stock composition:

auth.api /admin/ endpoints: 9  (all /admin/oauth2/*, all SERVER_ONLY: true)

POST /api/v1/auth/admin/update-user   member -> 404  len=0  ct=(none)
GET  /api/v1/auth/admin/list-users    member -> 404  len=0  ct=(none)
POST /api/v1/auth/admin/set-role      member -> 404  len=0  ct=(none)
POST /api/v1/auth/admin/definitely-not-a-route-1989  member -> 404  len=0  ct=(none)   <-- CONTROL

better-auth admin plugin ON (OS_SCIM_ENABLED=true, which forces admin on via ADR-0071 — the one env knob that reaches it):

auth.api /admin/ endpoints: 24

POST /api/v1/auth/admin/update-user   member -> 403  {"message":"You are not allowed to update users","code":"YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS"}
GET  /api/v1/auth/admin/list-users    member -> 403  {"message":"You are not allowed to list users","code":"YOU_ARE_NOT_ALLOWED_TO_LIST_USERS"}
POST /api/v1/auth/admin/set-role      member -> 403  {"message":"You are not allowed to change users role","code":"YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE"}

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_SURFACE is pinned at the maximal LEDGERED_PLUGIN_CONFIG and 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 #15417 read it:

On a composition that does not enable the admin plugin, three routes the SDK can build URLs for answer 404 with a zero-length body and no content-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_LEDGER carries requires for 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

What this is not

Not a privilege leak, and #15417 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 — #15417'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.ts already 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 #15417.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions