Repository navigation
fix(auth): drop the stored active organization when the server reports none (objectui#12698) - #12765
Conversation
…s none On a reload after a refused set-active (better-auth nulls the session's active organization before refusing a non-member), refreshOrganizations heard "no active organization" from the server and left the previous id in ActiveOrganizationStorage, where it kept stamping X-Tenant-ID and keying the metadata seed cache. It is now cleared on that answer, including when the ADR-0081 single-membership repair is refused. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
…12698 pin Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
WorkspaceSwitcher and CurrentOrganizationIndicator rendered `activeOrganization ?? orgList[0]`, so a session the server reports no active organization for (e.g. after a refused set-active) still showed the first membership. Both now render their existing no-organization-context branch instead (objectui#12698). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ator pin Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Review: ACCEPT — objectui PR #12765 ·
|
Fixes #12698.
Clause-②: no
Root cause (measured)
better-auth's
POST /organization/set-activesets the session's active organization to null before it refuses a non-member (crud-org.mjs, better-auth 1.7.2:adapter.setActiveOrganization(token, null)thenFORBIDDEN USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION). So after the refused forge the server really holds no active organization. On the reload, the client gets two things wrong:WorkspaceSwitcher.tsx:63andCurrentOrganizationIndicator.tsx:61(inpackages/app-shell/src/layout/) both renderactiveOrganization ?? orgList[0]. When the server answers "none",AuthProvidercorrectly holdsactiveOrganization = null, and the header names the first membership anyway. Measured with the realAuthProviderand the real header components against a mocked server that has no active organization:useAuth().activeOrganizationis null,getActiveOrganizationis called once andsetActiveis not called, yet the switcher reads "Plant A".set-activecall, the server is re-stamped, and the indicator reads "Plant B". That is truthful.refreshOrganizationsasked the server, heard "none", and left the previous id inActiveOrganizationStorage. That id kept stampingX-Tenant-IDand keying the metadata seed cache (MetadataProvider.activeOrgScope) for an organization the session does not hold. The same measurement readstorage = org_bin all three cases.PM assumption A1 is not what happens.
activeOrganizationis not hydrated from storage on load: it starts asuseState(null). The!activeOrganizationguard therefore does not skip the server on a reload, and the server is asked once.Change
AuthProvider.refreshOrganizations: when the server-asking branch ends with no active organization (the server said none, and at one membership the ADR-0081 repair was refused),ActiveOrganizationStorage.clear()runs. A user switch that commits while the request is in flight setsactiveOrgIdRef, and a guard on that ref keeps it from being wiped. The repair itself is unchanged. It already re-stamps the server and adopts the server's answer, so it was truthful; pin (b) shows this.ConsoleShell.tsxis not touched:RequireOrganizationalready readsactiveOrganizationtruthfully.Pins:
packages/auth/src/__tests__/staleActiveOrg-12698.test.tsxThe tests use the real provider and real storage, with no
vi.mock. The mocked server holds its own active organization, and a refusedset-activenulls it the way better-auth does.set-activecall, the header names the organization the server confirmed, and storage equals the server's value. A second case refuses the repair: no active organization and storage is cleared.getActiveOrganizationis called once,set-activeis never called, and storage is unchanged.Ablation (the
clear()line replaced by a marker; markergrep -c0 → 1; numstat1 1): (a) ×2 and (b-refused) red, (b)/(c)/(d) green. Restore usedgit checkout HEAD --and was checked by hash: HEAD blob8f0420107…equals the restored file, andgit diff HEADis empty. The test imports../AuthProviderfromsrc, so no build or dist step is involved.Local verification at
d45dbf32f@object-ui/authtype-checkexit 0.tsconfig.test.jsonlists the new test file (--listFilescount 1).vitest run --maxWorkers=1 packages/auth/exit 0: 32 files, 303 tests.--filter '@object-ui/auth^...': no projects matched, so nothing ran.--format json): 0 errors and 4 warnings. All 4 warnings exist on base7ead04904, shifted by +12 lines. The config enables no type-aware linting (noparserOptions.project/projectServiceineslint.config.js), so this diff cannot change the verdict on any untouched file.check:phantom-deps,self-import,unreferenced-sources,doc-example-readers,handler-key-reads,side-effects-array,element-data-source-declaration,spec-symbols,action-forward-parity,designer-field-key-parity,icon-record-names,i18n-keys,i18n-drift,i18n-designer-parity,control-bytes,check-hand-rolled-comment-mask.mjs,check-changeset-fixed.mjs.Acceptance notes
WorkspaceSwitcher.tsxandCurrentOrganizationIndicator.tsxno longer fall back toorgList[0]; pins*.noActiveOrg-12698.test.tsx, ablation red on (a), green (b) controls; layout dir 90 files and 623 tests green; heada4055997a.refreshOrganizationsdoes not ask the server when the membership list is empty, so a stored id with zero memberships is not cleared by this change. That case was not measured as reachable here.Generated by Claude Code