Skip to content

Commit 503d3f6

Browse files
os-zhuangclaude
andauthored
docs(auth): the org-role vocabulary is closed — correct the mirror's standing instruction (framework ADR-0108) (#2907)
`org-roles.ts` told the next maintainer "a role added server-side must be added HERE too". There are no server-side additions left to chase. The framework used to register every declared `position` / `permission` name as an organization role, so this list could always fall behind the server's. That channel was retired (framework ADR-0108, objectstack#3723): every value stored in `sys_member.role` is projected into `current_user.positions`, so a business role handed out that way was capability carrying none of the position system's controls. `sys_member.role` is now a closed, framework-owned list of owner / admin / delegated_admin / member; an app's business roles are positions, granted through `sys_user_position` or an invitation's placement (framework ADR-0105 D8). The mirror is therefore complete by construction rather than by vigilance. No behaviour change — the four names and labels are what they already were. Still a mirror rather than a derivation, but for a packaging reason now, not a design one: the names live in `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLES`, which `@object-ui/auth` cannot import yet (this package takes no dependency on `@objectstack/spec`, and the constants are absent from the published 16.1.0 — they ship with the first ADR-0108 release). The doc records the exact swap for when it can. A new test pins the list to those four in display order so drift fails loudly instead of silently offering a value the server's enforced select rejects. Claude-Session: https://claude.ai/code/session_0186LhwkUBupmLJUUAMda5hU Co-authored-by: Claude <noreply@anthropic.com>
1 parent ab46110 commit 503d3f6

4 files changed

Lines changed: 78 additions & 9 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@object-ui/auth': patch
3+
---
4+
5+
docs(auth): the org-role vocabulary is closed — correct the mirror's standing instruction (framework ADR-0108)
6+
7+
`org-roles.ts` carried a standing instruction that is now wrong: *"a role added
8+
server-side must be added HERE too."* There are no server-side additions left
9+
to chase.
10+
11+
The framework used to register every declared `position` / `permission` name as
12+
an organization role, so the console's list could always fall behind the
13+
server's. That channel was retired (framework ADR-0108, objectstack#3723):
14+
every value stored in `sys_member.role` is projected into
15+
`current_user.positions`, so a business role handed out that way was capability
16+
with none of the position system's controls — no `granted_by`, no validity
17+
window, no scope check. `sys_member.role` is now a closed, framework-owned list
18+
of `owner` / `admin` / `delegated_admin` / `member`, and an app's own business
19+
roles are positions, granted through `sys_user_position` or an invitation's
20+
placement (framework ADR-0105 D8).
21+
22+
So this mirror is now complete **by construction** rather than by vigilance.
23+
Nothing about the console's behaviour changes — the four names and their labels
24+
are what they already were.
25+
26+
Still a mirror rather than a derivation, but only for a packaging reason now:
27+
the names live in `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLES` /
28+
`BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which `@object-ui/auth` cannot import yet —
29+
this package takes no dependency on `@objectstack/spec`, and those constants
30+
ship in the first release carrying ADR-0108 (they are absent from the published
31+
16.1.0). A new test pins the list to exactly those four in display order until
32+
then, so drift fails loudly instead of silently offering a value the server's
33+
enforced `select` would reject.

packages/auth/src/__tests__/org-roles.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ describe('vocabulary', () => {
3333
expect(ORG_ROLES).toContain(ORG_ROLE_DELEGATED_ADMIN);
3434
});
3535

36+
it('is EXACTLY the framework four — the closed vocabulary, mirrored', () => {
37+
// [framework ADR-0108 / objectstack#3723] `sys_member.role` is a closed,
38+
// framework-owned list; an app can no longer register a fifth name. This
39+
// is the drift guard until `@object-ui/auth` can import
40+
// `BUILTIN_MEMBERSHIP_ROLES` from `@objectstack/spec` (see org-roles.ts).
41+
//
42+
// Order matters: it is the display order both screens list, and it matches
43+
// `BUILTIN_MEMBERSHIP_ROLE_OPTIONS` server-side.
44+
//
45+
// If this fails because the framework list changed, mirror the change —
46+
// do NOT add a name the server does not offer. A value outside this set is
47+
// rejected on write by an enforced `Field.select`, so the console would be
48+
// offering something that always 400s.
49+
expect([...ORG_ROLES]).toEqual(['owner', 'admin', 'delegated_admin', 'member']);
50+
});
51+
3652
it('every role has a label — a role with no label renders as a raw key', () => {
3753
for (const role of ORG_ROLES) {
3854
expect(ORG_ROLE_LABELS[role]?.key).toBeTruthy();

packages/auth/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ export { normalizePhoneIdentifier, looksLikePhoneIdentifier } from './phone-iden
3737
export { createAuthenticatedFetch, ActiveOrganizationStorage, type AuthenticatedAdapterOptions } from './createAuthenticatedFetch';
3838
export { getUserInitials } from './types';
3939

40-
// Organization membership-role vocabulary + the narrowing helpers every org
41-
// screen shares (see `org-roles.ts` for why this mirrors rather than derives).
40+
// Organization membership-role vocabulary — a CLOSED, framework-owned list of
41+
// four (framework ADR-0108) — plus the narrowing helpers every org screen
42+
// shares. See `org-roles.ts` for why this still mirrors rather than derives.
4243
export {
4344
ORG_ROLE_OWNER,
4445
ORG_ROLE_ADMIN,

packages/auth/src/org-roles.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,32 @@
1515
* 'member'` in both `MembersPage` and `InviteMemberDialog`, so a role the
1616
* server had learned about was still unreachable from either screen.
1717
*
18-
* ⚠️ This list MIRRORS the server; it does not derive from it. `/auth/config`
19-
* exposes feature flags but no role vocabulary, so there is no surface to read
20-
* — see objectstack-ai/objectstack#3723, which tracks making one list the
21-
* source for all of them (the framework already carries the same list twice:
22-
* the better-auth roles map and two enforced `Field.select` option sets). Until
23-
* that lands, a role added server-side must be added HERE too — one place, not
24-
* two.
18+
* ## The vocabulary is CLOSED — this mirror is complete by construction
19+
*
20+
* [framework ADR-0108 / objectstack#3723] These four names are the WHOLE list,
21+
* and the framework owns them. An application cannot add a fifth: the server
22+
* used to register every declared `position` / `permission` name as an
23+
* organization role, and that was retired, because every value stored in
24+
* `sys_member.role` is projected into `current_user.positions` — so a business
25+
* role handed out this way was capability with none of the position system's
26+
* controls (no `granted_by`, no validity window, no scope check).
27+
*
28+
* So the standing instruction that used to live here — *"a role added
29+
* server-side must be added HERE too"* — no longer applies. There are no
30+
* server-side additions to chase. A membership role is an organization GRADE
31+
* (what you may reach); what a person may DO is a position, granted through
32+
* `sys_user_position` or an invitation's placement (framework ADR-0105 D8).
33+
*
34+
* ⚠️ Still a mirror, not a derivation — but now only for a packaging reason,
35+
* not a design one. The names live in `@objectstack/spec` as
36+
* `BUILTIN_MEMBERSHIP_ROLES` / `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which
37+
* `@object-ui/auth` cannot import yet: this package does not depend on
38+
* `@objectstack/spec`, and the constants ship in the first release carrying
39+
* ADR-0108 (absent from the published 16.1.0). Once this package takes that
40+
* dependency at a version that has them, the four `export const`s below become
41+
* a re-export and `ORG_ROLES` becomes `[...BUILTIN_MEMBERSHIP_ROLES]` — the
42+
* labels and grade ladder stay here, since they are console concerns.
43+
* `orgRolesMatchFramework` in the tests pins the list until then.
2544
*/
2645

2746
export const ORG_ROLE_OWNER = 'owner';

0 commit comments

Comments
 (0)