Skip to content

fix(auth): stop purgeSignedOutClientCaches aborting the sign-out sweep on one throwing removeItem - #5816

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5777-signout-purge-per-key-try
Aug 23, 2026
Merged

fix(auth): stop purgeSignedOutClientCaches aborting the sign-out sweep on one throwing removeItem#5816
os-zhuang merged 2 commits into
mainfrom
claude/issue-5777-signout-purge-per-key-try

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#5777

The defect

purgeSignedOutClientCaches() in packages/auth/src/AuthProvider.tsx wrapped its whole
removeItem loop in one try. A removeItem that threw on key n aborted the walk, so
every still-unvisited objectui:metadata:* key after it was never swept, and the failure
was swallowed — signOut() believed the purge had completed. Same defect class as
#5763 (sweepStore in ActiveOrganizationStorage.ts), different file and caller
(sign-out, not sign-in) — filed separately per that card's scope note and fixed here.

Per its own docstring and #5198, the entries this purges are the previous principal's
org-scoped, PERMISSION-FILTERED app list — a cross-principal disclosure risk on a shared
browser when they survive, not mere staleness. A partial sweep here is the sharper half
of the pair the triage comment names.

The fix

Mirrors #5763's landed shape on main (ff2d54717, merged 07:56:17Z — read there per
dispatch, not from that PR's discussion):

Whether #5763's report-vs-silent reasoning transfers (dispatch's open question)

It does, and the reasoning is the same on both axes sweepStore's doc comment gives for
choosing "report, don't quarantine" over clear()'s read-back quarantine:

  • No local get() to guard.purgeSignedOutClientCaches does not own reads for the
    metadata seed cache — MetadataProvider (@object-ui/app-shell) does, per this
    function's own docstring (the prefix is spelled out rather than imported specifically
    because ownership sits on that side of the dependency boundary). There is nothing here
    for a quarantine to guard, same as sweepStore walking "another package's recents
    cache, a metadata seed" it does not own reads for either.
  • The caller cannot act on it either way.sweepStore's caller (SessionUserScope.adopt,
    sign-in) cannot act on a partial-sweep failure and must not throw. The sign-out caller
    is even further foreclosed: the session is already ending when purgeSignedOutClientCaches()
    runs in signOut's finally block, so there is no decision left to make either.

So this PR mirrors sweepStore exactly rather than diverging: report via console.warn,
never throw, no quarantine layer added (which would have been the general
storage-error-handling refactor both cards' scope notes rule out).

Tests

packages/auth/src/__tests__/signOut-client-cache-purge-5198.test.tsx — two new cases,
same pattern as #5763's sibling test in sessionUserChangePurge-5664.test.tsx:

  • sweeps every other metadata key when one removeItem throws (#5777) — a poisoned
    key with real metadata keys on both sides of it (insertion order), asserting the
    poisoned key survives, every other metadata key is still removed, a non-matching key
    is untouched either way, ActiveOrganizationStorage.clear() still runs, and exactly
    one console.warn names the poisoned key.
  • reports nothing when every removal sticks (#5777) — control: the warning is a
    measurement of an actual failure, not a constant.

Reverse-verification, on a real commit (per dispatch's ⛔ never against uncommitted
edits):
committed the fix (da392fbbd), then git checkout HEAD~1 -- packages/auth/src/AuthProvider.tsx
to restore the pre-fix whole-loop-try shape and re-ran the same test file:

❯ sweeps every other metadata key when one removeItem throws (#5777)
AssertionError: expected '[{"name":"account"}]' to be null
Test Files 1 failed (1)
Tests 1 failed | 5 passed (6)

The new test fails against the pre-fix shape exactly as expected (the other 5 pass
either way — they don't exercise a throwing removeItem). Restored the fix via
git checkout claude/issue-5777-signout-purge-per-key-try -- packages/auth/src/AuthProvider.tsx,
confirmed git diff HEAD --stat empty (working tree matches the commit), and re-ran:

 Test Files 1 passed (1)
Tests 6 passed (6)

Full package: pnpm exec vitest run packages/auth/ --maxWorkers=2 (run from repo root,
per AGENTS.md — never pnpm --filter/package-cwd, which silently collects
apps/console instead) → 24 files / 251 tests passed, on c0b7876ba (this PR's head).

pnpm --filter '@object-ui/auth' type-check (tsc --noEmit && tsc -p tsconfig.test.json,
after building the dependency closure first) → clean, on c0b7876ba.

node scripts/check-changeset-presence.mjs, check-control-bytes.mjs,
check-type-check-coverage.mjs, check-lint-coverage.mjs → all ✅ on c0b7876ba.
eslint on the two changed files → 0 errors, 3 pre-existing warnings unrelated to this
diff (React effect setState warnings at lines 300/760/767, outside the edited region).

Scope

purgeSignedOutClientCaches() and its own test file only, per the card's scope note —
not a general refactor of AuthProvider.tsx or the storage-error-handling module.

Generated by Claude Code


Generated by Claude Code

…p on one throwing removeItem
Same defect class as #5763 (sweepStore), different file and caller: the
`try` wrapped the whole loop instead of each `removeItem`, so a key that
throws on removal aborted the walk and left every metadata-seed-cache key
after it unswept. The surviving entries are the org-scoped seed cache
#5198 classifies as a cross-principal disclosure risk on shared browsers,
so a partial sweep here is sharper than the sign-in sibling.
Mirrors #5763's landed shape (main, ff2d547): `try` now guards only the
`Object.keys` snapshot; each `removeItem` gets its own `try`; a partial
sweep is reported via `console.warn` naming the unswept keys rather than
quarantined, because this function does not own reads for the metadata
seed cache (MetadataProvider does) the same way `sweepStore` does not own
reads for the keys it walks.
Fixes#5777
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3227.0 KB3990.2 KB
Main entry chunk (gzip)152.7 KB350 KB
Entry fileindex-Cg00oZ2q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)12.80KB4.47KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)503.96KB113.94KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)186.86KB44.21KB
plugin-dashboard (index.js)128.44KB32.96KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.34KB60.99KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.61KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)44.39KB14.99KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)3.40KB1.68KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

purgeSignedOutClientCaches's try wraps the whole loop, so one throwing removeItem silently cancels the rest of the sign-out sweep

2 participants

@os-zhuang@claude