Measurement only. ⛔ No fix lands under this card — the deliverable is a reading posted here. If it finds a hole, the repair is its own card under its own ruling.
The question
The organization_membership_ended guard in packages/core/src/security/resolve-authz-context.ts opens with:
if (keyPrincipal?.tenantId && input.tenancyPosture) {
It is API-key-gated. A session principal never reaches it. And a session's organization is also a stored claim — around line 361 of the same file:
tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
So the two facts that made #15256 a p0 — the wall compares against a stored claim, and the fact that would expose an ended membership is not an input to the layer that could act on it — appear to hold for sessions as well, and #15365 did not touch that path: it made the posture available, but the guard that consumes it still only runs for API keys.
⚠️ Not asserted as a defect. better-auth's organization plugin may maintain activeOrganizationId itself — clearing or re-pointing it when a membership ends — in which case the claim is vetted at its source and there is nothing here. Nobody has looked. Both prior measurements (#15163 and cloud#1982) drove API keys only, by construction.
The measurement
On the same rig shape cloud#1982 used — a real objectstack serve boot of apps/objectos-ee under a live isolated posture with the real Organizations plugin mounted, OS_DATABASE_URL on a FILE so writes can be read back — with a session (cookie), not an API key:
- Sign in a user who has a
sys_member row for org_alpha, and let the session's activeOrganizationId become org_alpha. Confirm it from the session record, not from the UI.
- Remove that membership (delete the
sys_member row) while the session stays alive. ⛔ Do not sign out.
- Drive
GET and POST on a plain org-stamped object (sys_business_unit served cloud#1982 well). Does the session still read org_alpha's rows? Does a write land, and does the row read back from the store carrying org_alpha?
- Repeat on the wiring where
kernel is bound, if one can be booted — the API-key hole is closed there, and the question is whether the session hole is too.
- Read the code and state which layer, if any, re-vets
activeOrganizationId after a membership ends: better-auth's own organization plugin, a hook in packages/organizations, or nothing.
Controls, both directions, or the run has measured nothing: a session whose membership is intact must still read and write (a rig that serves nobody proves nothing); and a signed-out request must still be refused. Seed rows in a second organization and assert they are never served.
Outcomes
Scope note
The wall-enforcing posture needs org-scoping, whose only registrar is the cloud-private packages/organizations — so this is an enterprise-surface question and the rig has to be a cloud one, as cloud#1982's was. ⛔ Do not substitute the framework's SimulatedOrgScopingPlugin stand-in and present it as the answer; state it plainly if that is all you can boot.
Refs: #15256 (the API-key hole and its ruling) · PR #15365 (a72704375) · #15163 · cloud#1982 · packages/core/src/security/resolve-authz-context.ts.
Measurement only. ⛔ No fix lands under this card — the deliverable is a reading posted here. If it finds a hole, the repair is its own card under its own ruling.
The question
The
organization_membership_endedguard inpackages/core/src/security/resolve-authz-context.tsopens with:It is API-key-gated. A session principal never reaches it. And a session's organization is also a stored claim — around line 361 of the same file:
So the two facts that made #15256 a p0 — the wall compares against a stored claim, and the fact that would expose an ended membership is not an input to the layer that could act on it — appear to hold for sessions as well, and #15365 did not touch that path: it made the posture available, but the guard that consumes it still only runs for API keys.
activeOrganizationIditself — clearing or re-pointing it when a membership ends — in which case the claim is vetted at its source and there is nothing here. Nobody has looked. Both prior measurements (#15163 and cloud#1982) drove API keys only, by construction.The measurement
On the same rig shape cloud#1982 used — a real
objectstack serveboot ofapps/objectos-eeunder a liveisolatedposture with the realOrganizationsplugin mounted,OS_DATABASE_URLon a FILE so writes can be read back — with a session (cookie), not an API key:sys_memberrow fororg_alpha, and let the session'sactiveOrganizationIdbecomeorg_alpha. Confirm it from the session record, not from the UI.sys_memberrow) while the session stays alive. ⛔ Do not sign out.GETandPOSTon a plain org-stamped object (sys_business_unitserved cloud#1982 well). Does the session still readorg_alpha's rows? Does a write land, and does the row read back from the store carryingorg_alpha?kernelis bound, if one can be booted — the API-key hole is closed there, and the question is whether the session hole is too.activeOrganizationIdafter a membership ends: better-auth's own organization plugin, a hook inpackages/organizations, or nothing.Controls, both directions, or the run has measured nothing: a session whose membership is intact must still read and write (a rig that serves nobody proves nothing); and a signed-out request must still be refused. Seed rows in a second organization and assert they are never served.
Outcomes
activeOrganizationId) ⇒ close this card with the reading, and add one sentence beside the guard saying why it is key-gated, so the next reader does not re-open the question.isolated— the wall compares against the caller's own unvetted claim #15256, on a credential that is far more common than an API key. File it with the measured blast radius and its own decision card; ⛔ the repair is not to be improvised here.Scope note
The wall-enforcing posture needs
org-scoping, whose only registrar is the cloud-privatepackages/organizations— so this is an enterprise-surface question and the rig has to be a cloud one, as cloud#1982's was. ⛔ Do not substitute the framework'sSimulatedOrgScopingPluginstand-in and present it as the answer; state it plainly if that is all you can boot.Refs: #15256 (the API-key hole and its ruling) · PR #15365 (
a72704375) · #15163 · cloud#1982 ·packages/core/src/security/resolve-authz-context.ts.