You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Residual of #2258, stated there rather than fixed. #2258 made the Workspace's user-initiated "Sign out" end whichever credential is live (store.signOutEverywhere() → authStore.logout() then signOut()). The expiry path was deliberately left alone: fetchRoster's 401 → endSession({expired:true}) → signOut() clears only the portal token, and if a platform JWT is also present in the browser, isPlatformSession re-derives and the client silently becomes the operator — the same class as #2258, by a different door.
Why it was left
Expiry is not a user act. Ending the platform session there would log out an operator working in another tab because a client's idle timeout lapsed.
The alternative — a persisted "suppress the platform fallback" flag — was rejected in fix(workspace): Sign out doesn't sign out — the platform JWT silently re-authenticates on refresh #2258 on evidence: the platform JWT is an axios DEFAULT header and per-request headers merge over defaults, so a flag hides the disclosure while every portal request still carries the operator's credential (see docs/memory/feature-flows/workspace-session-signout.md → Why not the suppression flag).
Reachability
Narrow. The Workspace OTP form never renders while a platform JWT exists (Portal.vue gates it on !isClientSignedIn, and isPlatformSession is already true), so this needs the platform login to arrive after the client signed in on the same browser (a shared machine where an operator logs in later), and then the client session to idle out (7d default) with that tab still open or revisited.
Acceptance Criteria
After a client session expires on a browser that also holds a platform login, the Workspace shows the "session expired" sign-in state — not the operator's roster — and a refresh keeps it there.
An operator's platform session in another tab is not ended by a client's idle timeout.
Unit coverage in src/frontend/tests/unit/workspaceSession.spec.js.
Technical Notes
The honest options are both non-trivial, which is why this is filed rather than folded into #2258:
A positive in-memory entry marker (workspaceEntered, NOT persisted) that isPlatformSession requires and that expiry clears — fails closed to the sign-in form when lost, and needs no localStorage; but it needs a "Continue as " escape for the operator, and a live-request guard equivalent to the flag's (the axios-default problem still applies to any state that hides a live JWT).
A self-service portal logout endpoint (server-side, per-email today via dependencies.revoke_portal_sessions_for_email — ent#281) does NOT help here: the client credential is already gone; the problem is the other credential.
Summary
Residual of #2258, stated there rather than fixed. #2258 made the Workspace's user-initiated "Sign out" end whichever credential is live (
store.signOutEverywhere()→authStore.logout()thensignOut()). The expiry path was deliberately left alone:fetchRoster's 401 →endSession({expired:true})→signOut()clears only the portal token, and if a platform JWT is also present in the browser,isPlatformSessionre-derives and the client silently becomes the operator — the same class as #2258, by a different door.Why it was left
docs/memory/feature-flows/workspace-session-signout.md→ Why not the suppression flag).Reachability
Narrow. The Workspace OTP form never renders while a platform JWT exists (
Portal.vuegates it on!isClientSignedIn, andisPlatformSessionis already true), so this needs the platform login to arrive after the client signed in on the same browser (a shared machine where an operator logs in later), and then the client session to idle out (7d default) with that tab still open or revisited.Acceptance Criteria
src/frontend/tests/unit/workspaceSession.spec.js.Technical Notes
The honest options are both non-trivial, which is why this is filed rather than folded into #2258:
workspaceEntered, NOT persisted) thatisPlatformSessionrequires and that expiry clears — fails closed to the sign-in form when lost, and needs no localStorage; but it needs a "Continue as " escape for the operator, and a live-request guard equivalent to the flag's (the axios-default problem still applies to any state that hides a live JWT).dependencies.revoke_portal_sessions_for_email— ent#281) does NOT help here: the client credential is already gone; the problem is the other credential.Related: #2258 (parent fix), ent#357 (implicit platform entry), ent#281 (operator-side revocation).