1.8.5: accept the nest-server roles: string[] shape in isAdmin so the admin UI stops silently disappearing#5
Merged
Conversation
…he admin UI stops silently disappearing `isAdmin` was `user.value?.role === 'admin'` — Better-Auth's admin-plugin shape (a singular `role` string) only. But `@lenne.tech/nest-server` registers `roles` as a CORE Better-Auth additionalField (`type: 'string[]'`, `defaultValue: []`), so its users carry `roles: ['admin']` and NO singular `role` at all. Against a real nest-server backend `isAdmin` was therefore permanently `false`: every `v-if="isAdmin"` branch (admin nav, admin pages) vanished for actual admins, with no error anywhere — `undefined === 'admin'` is simply false. This was verified against a live nest-server deployment whose admin user returns `roles: ["admin"]` and no `role` field. Changes: - `LtUser` gains `roles?: string[]`, so the multi-role shape is a first-class, typed field rather than something consumers have to cast in. - `isAdmin` accepts EITHER shape (`role === 'admin'` OR `roles` includes 'admin'). `Array.isArray` guards a malformed `roles` in the client-writable auth-state cookie: junk yields `false` instead of throwing in the computed. - `roles` joins `AUTHZ_KEYS`, so the session merge treats it exactly like `role`. It is authorization-relevant now that `isAdmin` reads it: a stale cached `roles: ['admin']` would otherwise keep the admin UI alive after the backend revoked admin — the precise fail-open that list exists to prevent. Fail-closing costs nothing for backends that never send `roles`, because a key is only dropped when the CACHE has it and the session omits it — a `roles`-less backend never caches `roles`, so it is a no-op. And nest-server always returns `roles` from get-session (core additionalField), so its worst case is an honest `[]` (= not an admin), never a spurious drop. Tests: new `test/is-admin.test.ts` covers both shapes, multi-role arrays, empty arrays, absent user and malformed `roles`; `test/merge-session-user.test.ts` gains the `roles` fail-closed / downgrade / no-spurious-drop cases. All 4 new behavioural assertions were confirmed to fail against the pre-fix code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kaihaase
marked this pull request as draft
July 13, 2026 14:06
roles: string[]-Formroles: string[] shape in isAdmin so the admin UI stops silently disappearing
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.
Problem
isAdmincurrently checks the singularrolefield only:But the lt nest-server / Better-Auth user carries its roles as an array —
roles: string[]— and has norolefield at all. Against a real lt backend,isAdminis therefore permanentlyfalse: the entire admin UI silently disappears for every admin, with no error and no log line.This is not hypothetical. Verified live against a production instance —
get-sessionreturns:{ "roles": ["admin"], "email": "…", "id": "…" }— no
role. In nest-server,rolesis a core Better-Auth additional field, registered unconditionally withtype: 'string[]'anddefaultValue: [](not optional, not opt-in). Every lt project with an admin UI is affected.Fix
LtUsergainsroles?: string[]as a real, typed field.isAdminaccepts both shapes (singularrolestring androlesarray), so it stays backward compatible.Array.isArrayguard: the auth-state cookie is client-writable, so a tamperedroles: 42must yieldfalserather than throw inside a computed.rolesis added toAUTHZ_KEYS. OnceisAdminreadsroles, a stale cachedroles: ['admin']would keep the admin UI alive after the backend revoked the role — exactly the fail-open thatAUTHZ_KEYSexists to prevent. Fail-closingrolewhile fail-openingroles, when both feed the sameisAdmin, would be incoherent.A backend that never sends
rolesloses nothing:AUTHZ_KEYSonly drops a key when the cache has it and the session omits it — a roles-less backend never cachesroles, so the drop is a no-op.Tests
New
test/is-admin.test.ts(8 cases):{roles:['admin']}withoutrole→true,{role:'admin'}→true,{roles:['user']}→false,null→false, plus tampered non-array values.Extended
test/merge-session-user.test.tswith 3rolesAUTHZ cases.Counter-checked: with the old one-liner and the old
AUTHZ_KEYS, exactly 4 of the new assertions fail. A regression test that passes against the broken code would be worthless.Verification
format:checkclean ·lintexit 0 ·test:typesexit 0 · 128/128 tests (11 files) ·buildexit 0.Note
The version bump to
1.8.5follows this repo's convention (the PR commit does the bump; the maintainer adds the changelog entry separately onmainafter the merge — same as #4). If another PR lands first and this collides, that single hunk is trivial to drop.