@@ -886,6 +886,37 @@ const REFRESH_LOCK_HEADROOM_MS = 15_000
886886const REFRESH_LOCK_TTL_SEC = Math . ceil ( ( TOKEN_REFRESH_TIMEOUT_MS + REFRESH_LOCK_HEADROOM_MS ) / 1000 )
887887const REFRESH_FOLLOWER_MAX_WAIT_MS = REFRESH_LOCK_TTL_SEC * 1000
888888
889+ interface StoredChain {
890+ accessToken : string | null
891+ accessTokenExpiresAt : Date | null
892+ refreshToken : string | null
893+ }
894+
895+ /** The chain an account row holds now, or nothing when the account is gone. */
896+ async function readStoredChain ( accountId : string ) : Promise < StoredChain | undefined > {
897+ const [ stored ] = await db
898+ . select ( {
899+ accessToken : account . accessToken ,
900+ accessTokenExpiresAt : account . accessTokenExpiresAt ,
901+ refreshToken : account . refreshToken ,
902+ } )
903+ . from ( account )
904+ . where ( eq ( account . id , accountId ) )
905+ . limit ( 1 )
906+ return stored
907+ }
908+
909+ /**
910+ * The stored access token when it can still serve a request, as a follower would take it: a
911+ * chain another writer just rotated carries one, and a token that has already expired is no
912+ * answer at all.
913+ */
914+ function usableStoredToken ( stored : StoredChain , providerId : string ) : string | null {
915+ return stored . accessToken && ! isOAuthAccessTokenExpiring ( stored . accessTokenExpiresAt , providerId )
916+ ? stored . accessToken
917+ : null
918+ }
919+
889920async function performCoalescedRefresh ( {
890921 accountId,
891922 providerId,
@@ -979,18 +1010,25 @@ async function performCoalescedRefresh({
9791010 message : result . message ,
9801011 } )
9811012 if ( result . errorCode && isTerminalRefreshError ( result . errorCode ) ) {
982- // A refresh that lost a race with a concurrent connect fails with
983- // a revoked/rotated-out token even though the installation just
984- // got a live chain — dead-flagging then would take down a healthy
985- // credential for an hour.
1013+ // A refresh that lost a race with a concurrent connect or a newer
1014+ // rotation fails with a revoked/rotated-out token even though the
1015+ // account just got a live chain — dead-flagging then would take
1016+ // down a healthy credential for an hour.
9861017 if (
9871018 slackChainVersion &&
9881019 ( await hasSlackChainMoved ( slackTeamId ! , slackChainVersion ) )
9891020 ) {
9901021 logger . info ( 'Skipping dead flag: Slack chain moved during refresh' , logContext )
991- } else {
992- await markCredentialDead ( scopeKey , result . errorCode )
1022+ return null
9931023 }
1024+ if ( ! slackTeamId ) {
1025+ const stored = await readStoredChain ( accountId )
1026+ if ( stored && stored . refreshToken !== refreshToken ) {
1027+ logger . info ( 'Skipping dead flag: chain moved during refresh' , logContext )
1028+ return usableStoredToken ( stored , providerId )
1029+ }
1030+ }
1031+ await markCredentialDead ( scopeKey , result . errorCode )
9941032 }
9951033 return null
9961034 }
@@ -1041,16 +1079,16 @@ async function performCoalescedRefresh({
10411079 . where ( and ( eq ( account . id , accountId ) , eq ( account . refreshToken , refreshToken ) ) )
10421080 . returning ( { id : account . id } )
10431081 if ( rotated . length === 0 ) {
1082+ const stored = await readStoredChain ( accountId )
1083+ if ( ! stored ) {
1084+ logger . warn ( 'Rotation write found no account; the credential is gone' , logContext )
1085+ return null
1086+ }
10441087 logger . warn (
10451088 'Rotation write lost to a newer chain; using the stored token' ,
10461089 logContext
10471090 )
1048- const [ stored ] = await db
1049- . select ( { accessToken : account . accessToken } )
1050- . from ( account )
1051- . where ( eq ( account . id , accountId ) )
1052- . limit ( 1 )
1053- return stored ?. accessToken ?? result . accessToken
1091+ return usableStoredToken ( stored , providerId )
10541092 }
10551093 }
10561094
0 commit comments