diff --git a/.changeset/org-role-vocabulary-closed.md b/.changeset/org-role-vocabulary-closed.md new file mode 100644 index 000000000..325ea772e --- /dev/null +++ b/.changeset/org-role-vocabulary-closed.md @@ -0,0 +1,33 @@ +--- +'@object-ui/auth': patch +--- + +docs(auth): the org-role vocabulary is closed — correct the mirror's standing instruction (framework ADR-0108) + +`org-roles.ts` carried a standing instruction that is now wrong: *"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 the console's 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 +with none of the position system's controls — no `granted_by`, no validity +window, no scope check. `sys_member.role` is now a closed, framework-owned list +of `owner` / `admin` / `delegated_admin` / `member`, and an app's own business +roles are positions, granted through `sys_user_position` or an invitation's +placement (framework ADR-0105 D8). + +So this mirror is now complete **by construction** rather than by vigilance. +Nothing about the console's behaviour changes — the four names and their labels +are what they already were. + +Still a mirror rather than a derivation, but only for a packaging reason now: +the names live in `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLES` / +`BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which `@object-ui/auth` cannot import yet — +this package takes no dependency on `@objectstack/spec`, and those constants +ship in the first release carrying ADR-0108 (they are absent from the published +16.1.0). A new test pins the list to exactly those four in display order until +then, so drift fails loudly instead of silently offering a value the server's +enforced `select` would reject. diff --git a/packages/auth/src/__tests__/org-roles.test.ts b/packages/auth/src/__tests__/org-roles.test.ts index 015e0a3a4..d5256b2c6 100644 --- a/packages/auth/src/__tests__/org-roles.test.ts +++ b/packages/auth/src/__tests__/org-roles.test.ts @@ -33,6 +33,22 @@ describe('vocabulary', () => { expect(ORG_ROLES).toContain(ORG_ROLE_DELEGATED_ADMIN); }); + it('is EXACTLY the framework four — the closed vocabulary, mirrored', () => { + // [framework ADR-0108 / objectstack#3723] `sys_member.role` is a closed, + // framework-owned list; an app can no longer register a fifth name. This + // is the drift guard until `@object-ui/auth` can import + // `BUILTIN_MEMBERSHIP_ROLES` from `@objectstack/spec` (see org-roles.ts). + // + // Order matters: it is the display order both screens list, and it matches + // `BUILTIN_MEMBERSHIP_ROLE_OPTIONS` server-side. + // + // If this fails because the framework list changed, mirror the change — + // do NOT add a name the server does not offer. A value outside this set is + // rejected on write by an enforced `Field.select`, so the console would be + // offering something that always 400s. + expect([...ORG_ROLES]).toEqual(['owner', 'admin', 'delegated_admin', 'member']); + }); + it('every role has a label — a role with no label renders as a raw key', () => { for (const role of ORG_ROLES) { expect(ORG_ROLE_LABELS[role]?.key).toBeTruthy(); diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 5e35ffabb..1fbe18ef0 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -37,8 +37,9 @@ export { normalizePhoneIdentifier, looksLikePhoneIdentifier } from './phone-iden export { createAuthenticatedFetch, ActiveOrganizationStorage, type AuthenticatedAdapterOptions } from './createAuthenticatedFetch'; export { getUserInitials } from './types'; -// Organization membership-role vocabulary + the narrowing helpers every org -// screen shares (see `org-roles.ts` for why this mirrors rather than derives). +// Organization membership-role vocabulary — a CLOSED, framework-owned list of +// four (framework ADR-0108) — plus the narrowing helpers every org screen +// shares. See `org-roles.ts` for why this still mirrors rather than derives. export { ORG_ROLE_OWNER, ORG_ROLE_ADMIN, diff --git a/packages/auth/src/org-roles.ts b/packages/auth/src/org-roles.ts index 0fa9ca3a5..d26dd1bc9 100644 --- a/packages/auth/src/org-roles.ts +++ b/packages/auth/src/org-roles.ts @@ -15,13 +15,32 @@ * 'member'` in both `MembersPage` and `InviteMemberDialog`, so a role the * server had learned about was still unreachable from either screen. * - * ⚠️ This list MIRRORS the server; it does not derive from it. `/auth/config` - * exposes feature flags but no role vocabulary, so there is no surface to read - * — see objectstack-ai/objectstack#3723, which tracks making one list the - * source for all of them (the framework already carries the same list twice: - * the better-auth roles map and two enforced `Field.select` option sets). Until - * that lands, a role added server-side must be added HERE too — one place, not - * two. + * ## The vocabulary is CLOSED — this mirror is complete by construction + * + * [framework ADR-0108 / objectstack#3723] These four names are the WHOLE list, + * and the framework owns them. An application cannot add a fifth: the server + * used to register every declared `position` / `permission` name as an + * organization role, and that was retired, because every value stored in + * `sys_member.role` is projected into `current_user.positions` — so a business + * role handed out this way was capability with none of the position system's + * controls (no `granted_by`, no validity window, no scope check). + * + * So the standing instruction that used to live here — *"a role added + * server-side must be added HERE too"* — no longer applies. There are no + * server-side additions to chase. A membership role is an organization GRADE + * (what you may reach); what a person may DO is a position, granted through + * `sys_user_position` or an invitation's placement (framework ADR-0105 D8). + * + * ⚠️ Still a mirror, not a derivation — but now only for a packaging reason, + * not a design one. The names live in `@objectstack/spec` as + * `BUILTIN_MEMBERSHIP_ROLES` / `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which + * `@object-ui/auth` cannot import yet: this package does not depend on + * `@objectstack/spec`, and the constants ship in the first release carrying + * ADR-0108 (absent from the published 16.1.0). Once this package takes that + * dependency at a version that has them, the four `export const`s below become + * a re-export and `ORG_ROLES` becomes `[...BUILTIN_MEMBERSHIP_ROLES]` — the + * labels and grade ladder stay here, since they are console concerns. + * `orgRolesMatchFramework` in the tests pins the list until then. */ export const ORG_ROLE_OWNER = 'owner';