mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* fix(auth): send the user to login when the session expires A 401 mid-session only dropped the stored token: the auth store kept `user` set and `status: "authenticated"`, so neither shell's login gate (desktop `App.tsx`, web `[workspaceSlug]/layout.tsx`) ever fired. The shell stayed mounted and, with the token now gone, every following request went out unauthenticated — the user got a wall of "missing authorization" toasts and no way forward. Add `sessionExpired()` to the auth store: an idempotent teardown that lands in exactly the state a cold boot with a dead token reaches. The api client's 401 hook drives it, and AuthInitializer's boot rejection now delegates to it, so both dead-session paths converge on one implementation. Mobile has done this since its own 401 handler. The login page says why the user is there, rather than reading as the app having dropped their work for no reason. The claim is only made when this client actually presented a credential the server rejected — a first visit's identity probe 401s too. Two follow-ons the new path exposed: - Drop the query cache on an involuntary session end. Every query has `staleTime: Infinity`, so whoever signed in next would render the dead session's data and never refetch it away. Explicit logout already did this; the involuntary path did not, and it could not be reached before. - Snapshot the CLI-authorize token before probing the cookie. The probe is expected to 401 for a token-mode session, and the teardown clears the very key the fallback then looks for, so token-mode users could never authorize a CLI. Co-authored-by: multica-agent <[email protected]> * fix(desktop): keep the daemon running when the UI session expires Desktop's `onLogout` stops the local daemon, and routing session expiry through it meant an expiring UI credential killed a process that was authenticating with its own PAT and might be running agent work. Split the two events. `onSessionExpired` defaults to `onLogout`, so a shell only needs its own handler when part of its logout teardown is too destructive for an expiry it did not ask for — which on desktop is the daemon, and the tab layout with it. An account switch stays safe because it is handled at the other end: `daemon:sync-token` mints a fresh PAT and restarts the daemon whenever the user id changes. The no-token boot exit runs the same teardown rather than the logout one. Otherwise the daemon survived the expiry and then died on the next app start, when a missing token was read as the user having signed out. Both desktop teardowns move to platform/session-teardown with their side effects injected, so the difference between them is testable without an Electron window — verified failing when expiry stops the daemon. `AuthInitializer` no longer takes `onLogout`: every unauthenticated exit now delegates to the store, which owns both callbacks. `initCore` takes a named options object, since onLogin / onLogout / onSessionExpired are three adjacent `() => void` that only argument order told apart. Co-authored-by: multica-agent <[email protected]> * fix(auth): erase the previous session's client state on expiry Review found two holes in the new expiry path. The expiry only dropped the query cache, while logout also resets in-memory drafts, clears per-workspace persisted state, the tab layout and the last-workspace cookie. But the screen an expiry lands on offers a login form, and the next person to use it is not guaranteed to be the one who was just signed in — and when they share a workspace, the post-login stale-tab validator finds nothing to prune. A colleague on a shared machine would inherit drafts, chat selection and tab titles: the exact leak drafts/cleanup-registry exists to close. So an expiry now erases exactly what a logout erases. The teardown moves to core's platform/session-cleanup, shared by both, and desktop's expiry path resets tabs too — leaving the daemon as the only difference between the two, which is the point of the split. Unsent drafts are lost on an expiry now, the same as on logout; keeping them would mean gating the restore on user identity, which the login page cannot decide before it has one. Second, the idempotence guard sat above the credential teardown, so a login attempt that 401s — Desktop's deep link writes a token, then verifies it — never left "unauthenticated" and returned with the invalid token still in storage, to be replayed at the next launch. That was a regression against the old 401 handler, which always removed it. Dropping the credential now happens first and unconditionally; the guard only suppresses the parts that end a session. Both regressions verified failing before the fix. Co-authored-by: multica-agent <[email protected]> * fix(auth): clean up the previous session on a cold-start rejection The cleanup only ran on a transition out of `authenticated`, and it enumerated the keys to remove from the workspace list in the Query cache. The case that leaks worst satisfies neither: the app starts, the stale token on disk is rejected at the identity probe, and no workspace list was ever loaded — so A's drafts and chat state sat on disk waiting for whoever signed in next. Key it on being unauthenticated instead of on leaving `authenticated`, and enumerate the stored keys rather than the workspace list. Storage adapters gain an optional `keys()`; workspace-scoped entries are `${base}:${slug}` and every base is known, so the slugs no longer have to be. That also catches workspaces the last session had left. This cannot sign anyone out. Reaching `unauthenticated` IS the decision to end a session, and only a 401 on a presented credential or having no credential at all gets there — a client that cannot reach the server settles on `recovering` and keeps its token and its drafts. Pinned with regression tests for offline, 500 and 504. Also closes the test gap that hid this: the "no workspace list" case asserted only the global tabs key, so it passed while every per-workspace key survived. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]>