Repository navigation
Conversation
Chadpiha
force-pushed
the
feat/offline-account-presentation
branch
from
October 28, 2026 08:16
fd2b76e to
12ccbc9
Compare
boseq
approved these changes
Oct 28, 2026
Chadpiha
force-pushed
the
feat/offline-account-presentation
branch
from
October 28, 2026 13:39
751cda5 to
1e44b48
Compare
An offline launch ran the whole app as a guest: get-session never resolved, so policy, the workspace list cache and team spaces never loaded. When get-session gets no answer, present the account main's token-bound binding names, if the renderer's scope was last reconciled for that account and a profile it validated in the last 30 days is saved. Nothing validates the credential generation, so sync and authenticated cloud API calls stay fenced; a later get-session confirms or purges as before, and a 401 keeps that credential from being presented offline again.
The dictation window's cross-origin get-session always fails, so it took the offline path on every launch, online too, duplicating the scope mirror. Only the control panel now presents an offline account. - Keep it through refetches: Better Auth marks each one pending while it holds no session, which flickered the account to guest. - Ask for the session again on focus and every minute while an account is shown without a validated credential, since nothing else does when the auth server recovers while the network stayed up. - Only a 401 is a verdict (proxy 4xx are not); it also drops the saved profile, and a session with no user stays a verdict through a later 5xx. - Stop deleting main's managed-config cache on every identity change, so it serves an offline launch; main prunes older credentials' entries. - Refuse checkout, billing portal, plan switches and referral invites without a validated session: main sends the stored bearer unfenced. - Show the account read-only: an offline notice in Settings, Offline in the profile menu, Retry instead of Sign in on the leaderboard, and a plain message when accepting an invitation.
Better Auth clears sessionError when a refetch starts, so after a failed refetch reconciliation re-committed the validated lease with no answer, and the session retry turned that into a once-a-minute cycle that restamped the saved profile, defeating its 30-day bound. A failed refetch now marks the generation, and only a new answer clears it. - Present a session whose refetch failed read-only (isOfflineAccount), the same as an offline launch. - Keep presenting the offline account once the server answers for it until reconciliation validates it, so reconnecting is never signed out. - On a non-401 error, read main's binding before dropping the session and apply both in one update. - Ignore session requests Better Auth cancelled to start another one. - Stamp the profile with the server's answer, refresh it on later answers. - Retry only when no session request is in flight; load the offline account's caches once, not per useAuth consumer.
Since an identity change no longer clears the store, a new identity whose first config fetch failed held the previous identity's enforced scopes. Only the same identity's last config stands in now.
Team and workspace dialogs showed main's "no validated credential generation" string, and billing refusals a generic error or nothing. cloudApi now turns AUTH_CONTEXT_UNVALIDATED into auth.unconfirmedSession, which billing, referral invites and the invitation modal share. The usage-limit prompt disables Upgrade for an account shown offline. The Settings notice gets a title that doesn't repeat its Offline badge.
A crash before sign-out could leave the deleted account presentable offline for up to 30 days.
Chadpiha
force-pushed
the
feat/offline-account-presentation
branch
from
October 30, 2026 12:40
1e44b48 to
06a5035
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When the app launched with no network, it ran entirely as a guest. Better Auth's
get-sessionnever resolved, sofetchPolicynever ran,usePolicyStore.accountIdstayednull, and team spaces, the workspace switcher and the account UI were all hidden. #2921's workspace list cache was never read on that path.When
get-sessiongets no answer (network error, 408, 429 or 5xx), the app now shows the last validated account read-only, if all of these hold:accountScopeBinding.js, keyed by a sha256 of the exact bearer token it validated) names that account for the current credential generation;"none");openwhispr:accountProfile).In that state,
useAuthreportsisSignedIn: true, the saveduser,session: nulland a newisOfflineAccount: true. Policy, managed config and the workspace list load from their existing caches for that account.What stays fenced
getValidatedAuthGeneration()staysnull. Sync, leaderboard and insights don't run.cloudApicalls are refused before any request (AUTH_CONTEXT_UNVALIDATED).set-active-account-scopeandsetIsSignedInrun only after a realget-session.Back online
Signing out offline clears the token, which ends the offline account, and clears the saved profile.
The threat model is in
docs/superpowers/specs/2026-10-23-offline-account-presentation-design.md.Changes
src/lib/authRequestContext.ts: tracks the offline account and the generations the server rejected; addsgetOfflineAccount().handleAuthRequestErrornow separates "the server couldn't answer" from "the server rejected this credential".src/lib/offlineAccountProfile.ts(new): the saved profile and the pureresolveOfflineAccountdecision.src/hooks/useAuth.ts: shows the offline account, loads its cached policy, managed config and workspace list, saves the profile while validated, and clears it on purge.src/lib/auth.ts: sign-out clears the saved profile.No main-process changes and no new UI strings.
Tests
test/hooks/useAuthOfflineAccount.test.js(new) mounts the realuseAuthover the real auth context and account scope:test/lib/authRequestContext.test.js: binding match and mismatch, 503 counted as offline, 401 rejection, confirmation, sign-out.test/lib/offlineAccountProfile.test.js: id mismatch, 30-day age, malformed entries, and the decision helper.test/stores/workspaceListCache.test.js: a fenced list request shows the account's own cache and never another account's.Locally, the auth, account-scope, workspace and sign-out tests pass (72/72), and
tscand eslint are clean. The rest ofnpm testfails only for environment reasons here:better-sqlite3is built for a different Node version, and some connector tests time out under load but pass when run alone.