From ac1adddd50247e323e9727b64621b84d6b496ac1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 12 Sep 2026 18:24:14 +0200 Subject: [PATCH] fix: the account boundary explains itself instead of reading as an SSO failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping SSO on a frozen realm did nothing at all — no navigation, no message. The guard is correct: a page bound to one account must not start a login for another. But silence is indistinguishable from a broken button, and it was reported as "single sign on error" when no SSO request had been made; the server logged no OIDC activity because the click never left the page. The button now says what is happening and names the control that clears it. The guard itself is unchanged — same checks, same order, same outcomes. The recovery screen said "your saved credentials no longer match this session", which describes the mechanism rather than the situation. The ordinary cause is signing in as a different account on a device that already held one. It now says that, and says plainly that nothing was lost and no data was mixed — which is the reassurance the safeguard has earned and was not giving. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU --- public/js/accountBoundary.js | 8 +++++++- public/js/auth.js | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/public/js/accountBoundary.js b/public/js/accountBoundary.js index ab42fe74..c1ea5fd4 100644 --- a/public/js/accountBoundary.js +++ b/public/js/accountBoundary.js @@ -14,8 +14,14 @@ function recoverSignIn() { signInRequired = true; freeze(); + // Say what happened, not only that something did. This fires when the page + // is bound to one account and the session belongs to another — switching + // accounts on a shared device is the ordinary cause, and it reads as a + // failure when the message does not name it. document.querySelector('#account-recovery p').textContent = - 'Your saved credentials no longer match this session. Sign in again in a fresh page.'; + 'This page was opened for a different account than the one now signed in. ' + + 'That is a safeguard, not an error: nothing is lost, and no data from either ' + + 'account has been mixed. Start a fresh session to continue.'; document.querySelector('#account-recovery button').textContent = 'Sign in again'; } function read() { diff --git a/public/js/auth.js b/public/js/auth.js index 0ae1d303..f6b9a470 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -191,9 +191,21 @@ document.addEventListener('DOMContentLoaded', function() { } catch (e) {} var ssoButton = document.getElementById('btn-sso'); if (ssoButton) ssoButton.addEventListener('click', async function(e) { - if (!e.isTrusted || authenticationPending || boundary.blocked()) { e.preventDefault(); return; } + // A frozen realm cannot start a login, and silently doing nothing reads as + // "SSO is broken" — it is the account boundary holding, not a failure. Say + // so, and point at the one control that clears it. + if (boundary.blocked()) { + e.preventDefault(); + showToast('This page is locked to a different account. Tap "Sign in again" to start a fresh session, then use SSO.', 'error'); + return; + } + if (!e.isTrusted || authenticationPending) { e.preventDefault(); return; } boundary.capture(); // Detect a replaced sibling session before capturing credentials. - if (boundary.blocked()) { e.preventDefault(); return; } + if (boundary.blocked()) { + e.preventDefault(); + showToast('This page is locked to a different account. Tap "Sign in again" to start a fresh session, then use SSO.', 'error'); + return; + } // No owner marker can also mean bootstrap failed with an old cookie still // valid. Every SSO start must prove cookie absence before issuing intent. e.preventDefault();