feat(console): scoped-invitation placement — invite straight into a unit and positions (framework ADR-0105 D8) - #2868
Merged
Conversation
…nit and positions (framework ADR-0105 D8) An invitation may now carry placement intent (business unit + positions), applied when it is accepted, so a plant admin's invitee arrives already in the right unit and role instead of waiting on a platform admin. - @object-ui/auth: inviteMember accepts optional businessUnitId/positions (better-auth invitation additionalFields); new describeDelegableScope() reads GET /api/v1/security/my-delegable-scope. - InviteMemberDialog: optional Placement section listing ONLY the units the issuer may place into and the positions they may hand out; positions appear once a unit is chosen (an unanchored assignment is refused server-side, so offering it first would mislead). The narrowing is convenience, not the boundary — the server authorizes the pair against the ISSUER's adminScope at issuance and rejects the whole invitation when out of scope. So the section is HIDDEN when the caller has no delegable authority or the deployment exposes no delegated-administration runtime (501 => null): never a form the server would refuse. An ordinary invitation is byte-identical to before. Tests: hidden with no authority, hidden with no surface, options are exactly the delegable ones, a complete placement reaches inviteMember, a half-chosen one does not. auth + app-shell 1955/1955; lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
July 27, 2026 14:44
os-zhuang
added a commit
that referenced
this pull request
Jul 28, 2026
…pickers (framework#3697) (#2891) Follow-on to objectstack-ai/objectstack#3722, which registered a fourth organization role. Companion to #2868, which shipped the placement half. #3722 gave ADR-0105 D8's scope-bounded issuance gate its missing caller: `delegated_admin`, the grade that may reach `/organization/invite-member` without being an org admin. But the console could not select the role at all — `MembersPage` and `InviteMemberDialog` each inlined `type Role = 'owner' | 'admin' | 'member'` — so the capability the framework grew was unreachable from either screen. One vocabulary, not two: role names, labels and narrowing rules move into `@object-ui/auth`'s new `org-roles` module and both screens consume it. The list still MIRRORS the server rather than deriving from it (`/auth/config` publishes feature flags but no role vocabulary); the module says so and points at objectstack-ai/objectstack#3723. Both pickers now narrow to what the server will accept, mirroring DIFFERENT gates: - invite role ← `beforeCreateInvitation`'s role cap: never above the issuer's own grade; a below-admin issuer may invite as `member` only. A `delegated_admin` picking "Admin" would have 403'd. - change role ← better-auth's `update-member-role`: needs `member:["update"]` (owner/admin only), and only an owner may set `owner` or re-role an existing owner. An actor who may re-role nobody gets no items rather than three that would 403. Narrowing is convenience, not the boundary — the server re-checks — and it fails toward less. An ordinary invitation's request body is byte-identical to before. Verified: 654 files / 7684 tests passing; lint 0 errors on both packages; tsc 29/29.
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.
The D8 issuance UX over the gated engine seam. Framework side: objectstack-ai/objectstack#3663 (engine: carrier + issuance gate + accept-time apply) and objectstack-ai/objectstack#3674 (the delegable-scope read surface this narrows with). Tracking: objectstack-ai/objectstack#3541.
Why it's here and not in
cloudI originally scoped this as a cloud PR (cloud#874's "scoped-invitations UX"). It isn't:
cloudhas no invite UI at all — every organization-management screen (OrganizationsPage,MembersPage,InvitationsPage,InviteMemberDialog) already lives open inapp-shell. Cloud sells the runtime that entitles the feature, not the console screens. Putting the picker here is consistent with where org management already is, and it stays inert without the entitled runtime (see below).What
@object-ui/auth—inviteMemberaccepts optionalbusinessUnitId/positions, passed through better-auth's invitationadditionalFields; newdescribeDelegableScope()readsGET /api/v1/security/my-delegable-scope(DelegableScopetype exported).InviteMemberDialog— an optional Placement section listing only the units the issuer may place into and the positions they may hand out. Positions appear once a unit is chosen: an unanchored assignment is refused server-side, so offering it first would mislead.The property that matters
The narrowing is convenience, not the boundary. The server authorizes the pair against the issuer's
adminScope(ADR-0090 D12) at issuance and rejects the whole invitation when it's out of scope. So the UI is free to be helpful without being load-bearing — and it fails toward less, not more:null) ⇒ section hidden;An ordinary invitation is unchanged: with no placement chosen the request body is byte-identical to before.
Verification
InviteMemberDialog.placement.test.tsx— 5 cases: hidden with no authority, hidden with no surface, options are exactly the delegable ones (and positions gate on a chosen unit), a complete placement reachesinviteMember, a half-chosen one does not.vitest run packages/auth packages/app-shell— 232 files, 1955 tests, all passing.turbo lintclean for both packages; changeset included (minor ×2).Follow-up
End-to-end HTTP verification against a real
group-posture boot belongs with cloud'see-group-showcase(it already boots the enterprise runtime that registers theinvitation-placementservice): plant admin invites into their own subtree → accepted →sys_user_positionlands; out-of-subtree issuance → 403.🤖 Generated with Claude Code
https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
Generated by Claude Code