|
| 1 | +--- |
| 2 | +'@objectstack/platform-objects': minor |
| 3 | +'@objectstack/plugin-auth': minor |
| 4 | +--- |
| 5 | + |
| 6 | +`sys_session.revoke_reason` accepts `organization_membership_ended` — "Remove member" now actually signs the person out |
| 7 | + |
| 8 | +Removing a member deleted the `sys_member` row and left the session alive, for up to seven |
| 9 | +days. #15409 closed the security half per request (a session whose `activeOrganizationId` |
| 10 | +is not backed by a membership resolves with no active organization). This is the courtesy |
| 11 | +half an admin was promised, and it is **never the enforcement**: a trigger can be missed, |
| 12 | +an evaluation cannot. |
| 13 | + |
| 14 | +- **New `revoke_reason` value, `organization_membership_ended`** — an accept-set widening |
| 15 | + on a published system object, hence `minor` on `@objectstack/platform-objects`. Every |
| 16 | + reason before it is a timer (`idle_timeout`, `absolute_max`, `concurrent_cap`) or an |
| 17 | + interactive revoke (`user_revoked`, `admin`); this is the first authorization-event |
| 18 | + cause. There is no Zod enum behind the column — it is free `text` — so the field's own |
| 19 | + description is the published vocabulary, and that is where the value is declared. The |
| 20 | + string deliberately matches the one the API-key arm of the same ruling family already |
| 21 | + mints for this event (`authRefusal.reason` in `resolve-authz-context.ts`), so one grep |
| 22 | + finds every place the platform acts on a membership ending. |
| 23 | +- **The trigger acts on the ORGANIZATION'S CLAIM, never on the user** (maintainer ruling, |
| 24 | + decision batch #49 item 4, option B). A user who still holds another membership is |
| 25 | + **re-pointed** to it — never signed out of organizations they legitimately belong to. A |
| 26 | + user with no remaining membership has their session revoked through the existing |
| 27 | + `revoked_at` / `revoke_reason` mechanism, which expires it in place: better-auth returns |
| 28 | + nothing on the next request and the Console's existing 401 → login redirect handles it, |
| 29 | + with **no client change**. |
| 30 | +- **The seam is an engine hook on `sys_member`**, not a hook on better-auth's |
| 31 | + `/organization/remove-member`. A census measured that the endpoint, a direct delete, a |
| 32 | + bulk delete, the cascade from a `sys_user` delete and an organization re-point all reach |
| 33 | + the hook, while an endpoint hook would have reached one of them. Same precedent as |
| 34 | + `last-admin-guard.ts`. |
| 35 | +- **New public surface on `@objectstack/plugin-auth`** — `MEMBERSHIP_ENDED_REVOKE_REASON`, |
| 36 | + `endSessionClaimsForEndedMembership` and `registerMembershipEndedSessionTrigger`, hence |
| 37 | + `minor` rather than `patch`. |
| 38 | + |
| 39 | +Known open by measurement, not by omission: a raw driver delete bypasses the trigger |
| 40 | +entirely, and cloud's package-uninstall sample-data purge is one (filed as cloud#2003). The |
| 41 | +per-request check covers it; the courtesy does not. |
0 commit comments