|
| 1 | +--- |
| 2 | +'@objectstack/platform-objects': patch |
| 3 | +'@objectstack/plugin-auth': patch |
| 4 | +'@objectstack/verify': patch |
| 5 | +'@objectstack/spec': patch |
| 6 | +'@objectstack/cli': patch |
| 7 | +--- |
| 8 | + |
| 9 | +fix(auth): app-declared organization roles are now storable, not just registerable (#3723) |
| 10 | + |
| 11 | +`AuthManagerOptions.additionalOrgRoles` registered every `permission` / |
| 12 | +`position` name a stack declared with better-auth's organization plugin, so |
| 13 | +`POST /organization/invite-member { role: 'sales_rep' }` passed the role check — |
| 14 | +and then the write failed, because `sys_invitation.role` and `sys_member.role` |
| 15 | +were closed selects listing `owner|admin|member` only: |
| 16 | + |
| 17 | +``` |
| 18 | +ValidationError: role must be one of: owner, admin, member |
| 19 | + { field: 'role', code: 'invalid_option' } |
| 20 | +``` |
| 21 | + |
| 22 | +A select is enforced on write and better-auth's own inserts are not exempt (they |
| 23 | +run through the ordinary ObjectQL validator), so any stack declaring role names |
| 24 | +was registering roles that could be requested and never stored. |
| 25 | + |
| 26 | +Both gatekeepers now read one list. `normalizeAdditionalOrgRoles` is the single |
| 27 | +normalizer; its output feeds better-auth's role map **and** the two `select` |
| 28 | +option lists, so neither side can accept a name the other rejects. The built-in |
| 29 | +roles (`owner`, `admin`, `delegated_admin`, `member`) live in |
| 30 | +`@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which is all the |
| 31 | +platform objects declare statically — app roles are appended at boot. |
| 32 | + |
| 33 | +New exports: |
| 34 | + |
| 35 | +- `@objectstack/spec` — `MEMBERSHIP_ROLE_{OWNER,ADMIN,MEMBER,DELEGATED_ADMIN}`, |
| 36 | + `BUILTIN_MEMBERSHIP_ROLES`, `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, |
| 37 | + `MEMBERSHIP_ROLE_NAME_PATTERN`, `MEMBERSHIP_ROLE_NAME_MIN_LENGTH` |
| 38 | + (`MEMBERSHIP_ROLE_DELEGATED_ADMIN` moved from `identity/eval-user.zod` to |
| 39 | + `identity/membership-role`; the package-level export path is unchanged). |
| 40 | +- `@objectstack/plugin-auth` — `collectStackOrgRoles`, |
| 41 | + `normalizeAdditionalOrgRoles`, `membershipRoleOptions`, |
| 42 | + `withMembershipRoleOptions`. |
| 43 | + |
| 44 | +Hosts that boot `AuthPlugin` from a loaded stack should derive |
| 45 | +`additionalOrgRoles` with `collectStackOrgRoles(stack)` rather than walking the |
| 46 | +stack themselves — `objectstack serve`, the `@objectstack/verify` harness and |
| 47 | +`DevPlugin` now all do. The harness previously passed none, which is why a |
| 48 | +dogfood proof could boot a stack whose declared roles better-auth had never |
| 49 | +heard of; `DevPlugin` documents itself as equivalent to the full stack and |
| 50 | +silently excluded app roles from that equivalence. |
| 51 | + |
| 52 | +`additionalOrgRoles` accepts `{ name, label }` alongside a bare name, and |
| 53 | +`collectStackOrgRoles` now returns those descriptors. The label is what the |
| 54 | +declaring `position` / `permission` metadata already says, so the role picker |
| 55 | +shows `Executive` for a position declared as such instead of title-casing the |
| 56 | +machine name into `Exec` — a third source of truth for one string. Presentation |
| 57 | +only: better-auth sees just the name, and the stored value is always the name. |
| 58 | +Passing `string[]` keeps working unchanged. |
| 59 | + |
| 60 | +Behaviour change worth noting: a declared role name that is not a valid machine |
| 61 | +name (`/^[a-z][a-z0-9_]*$/`, min 2 chars) is no longer registered at all, with a |
| 62 | +boot warning. `Field.select` strips characters outside `[a-z0-9_]`, so such a |
| 63 | +name would be registered verbatim and stored mangled — the same mismatch with |
| 64 | +extra steps. Every name that passes `SnakeCaseIdentifierSchema` is unaffected. |
0 commit comments