fix(web): scope the OAuth recovery restores to the attempt that made them
Copilot round 2, both findings valid: - `resumePendingReauth`'s restore could resurrect a challenge whose session ended during the awaits — a server switch or disconnect clears the slot on purpose. It is now gated on the attempt still targeting the live session, client identity included (reconnecting to the same server builds a replacement client an id-only check would admit), and uses a functional update so a newer deferral queued in the same batch wins rather than being read past. - `prepareOAuthRedirect` cleared whichever resume snapshot was current, not the one it wrote. Nothing single-flights that callback, so a redirect failing before navigation could delete a later attempt's callback-routing state. `writeOAuthResumeSnapshot` now returns the serialization it stored, and `clearOwnOAuthResumeSnapshot` removes it only while it is still the current one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Quk1hBUtyhkXowwMXzAnAY Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ecad587afe
commit
76b75d3759
1 changed files with 166 additions and 12 deletions
|
|
@ -44,6 +44,7 @@ import {
|
|||
applyOAuthResumeUi,
|
||||
buildTabUiSnapshot,
|
||||
clearOAuthResumeSnapshot,
|
||||
clearOwnOAuthResumeSnapshot,
|
||||
consumeOAuthResumeSnapshot,
|
||||
oauthResumeInsufficientScopeMessage,
|
||||
oauthResumeToastMessage,
|
||||
|
|
@ -529,7 +530,7 @@ export function useOAuthRecovery({
|
|||
onBeforeOAuthRedirect(authorizationUrl);
|
||||
// Write immediately before navigation so implicit client teardown (during
|
||||
// connect setup) cannot clear the snapshot after we persist it.
|
||||
writeOAuthResumeSnapshot({
|
||||
const snapshotToken = writeOAuthResumeSnapshot({
|
||||
version: 1,
|
||||
serverId,
|
||||
activeTab,
|
||||
|
|
@ -548,10 +549,15 @@ export function useOAuthRecovery({
|
|||
// read it as an *abandoned* redirect and offer that diagnosis, which is
|
||||
// wrong — and the failure has to be reported the way every sibling arm
|
||||
// reports one, with the red border (#1621) and the re-auth banner.
|
||||
//
|
||||
// Scoped to the snapshot *this* attempt wrote: nothing single-flights
|
||||
// `prepareOAuthRedirect`, so a later attempt may already have replaced
|
||||
// it, and deleting that one would strand the redirect that is actually
|
||||
// in flight.
|
||||
oauthClient
|
||||
?.beginInteractiveAuthorization(authorizationUrl)
|
||||
.catch((err: unknown) => {
|
||||
clearOAuthResumeSnapshot();
|
||||
clearOwnOAuthResumeSnapshot(snapshotToken);
|
||||
setFailedServerId(serverId);
|
||||
showReAuthBanner(serverId, err);
|
||||
});
|
||||
|
|
@ -851,13 +857,26 @@ export function useOAuthRecovery({
|
|||
// the recovery was delivered. It still is owed, so restore it and let
|
||||
// the next trigger retry rather than losing it silently (#2165).
|
||||
//
|
||||
// Only into an empty slot, though: the tab can go hidden during the
|
||||
// awaits above and a *newer* challenge defer itself into it, and that
|
||||
// one describes the session as it is now. Latest wins — putting this
|
||||
// attempt's stale challenge back over it would retry authorization
|
||||
// data the server has already superseded.
|
||||
if (sessionRef.current.pendingReauth === null) {
|
||||
setPendingReauth(pending);
|
||||
// Only into the session this attempt belongs to, and only into an
|
||||
// empty slot.
|
||||
//
|
||||
// The awaits above can outlive the session entirely — a server switch
|
||||
// or a disconnect clears the slot on purpose, and restoring into that
|
||||
// would resurrect a challenge for a session that has ended. The client
|
||||
// identity is part of the check, not just the server id: reconnecting
|
||||
// to the SAME server builds a replacement `InspectorClient`, which an
|
||||
// id-only check would wave through.
|
||||
//
|
||||
// And the tab can go hidden mid-flight with a *newer* challenge
|
||||
// deferring itself into the slot; that one describes the session as it
|
||||
// is now. The restore is a functional update so it sees the queued
|
||||
// state rather than the render-time value, and yields to whatever is
|
||||
// already there — latest wins.
|
||||
if (
|
||||
sessionRef.current.activeServerId === pending.serverId &&
|
||||
sessionRef.current.inspectorClient === client
|
||||
) {
|
||||
setPendingReauth((prev) => prev ?? pending);
|
||||
}
|
||||
notifications.show({
|
||||
title: "Could not continue authorization",
|
||||
|
|
|
|||
Loading…
Reference in a new issue