fix(auth): OIDC token rotation no longer reloads every other tab

useSessionSync reloaded on any out-of-tab session change, so a routine
refresh in one tab hard-reloaded the others mid-call. Classify the
change: removed → reload, user/device changed → reload, same device with
a new token → swap it into the running client (setAccessToken + the
shared refresh token) in place. The refresher takes a Web Lock and adopts
tokens another tab already rotated instead of racing the issuer.
Unit-tested classifier.

Fixes #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-12 19:46:05 -04:00
co-authored by Claude Opus 5
parent dddaa4183e
commit d0dccdeb67
4 changed files with 143 additions and 18 deletions
+5 -3
View File
@@ -208,9 +208,11 @@ export function ClientRoot({ children }: ClientRootProps) {
);
useLogoutListener(mx);
// Cross-tab session sync: another tab logging out / in (access token changed
// in localStorage) reloads this tab so it never runs with stale credentials.
useSessionSync();
// Cross-tab session sync: another tab logging out / in reloads this tab so it
// never runs with stale credentials. A same-device token *rotation* (OIDC
// refresh in another tab) is swapped into the running client instead of
// reloading, which would drop an in-progress call/upload (#16).
useSessionSync(mx);
useEffect(() => {
if (loadState.status === AsyncStatus.Idle) {