@@ -31,6 +31,7 @@ import {
3131 extractSlackTeamId ,
3232 fanOutSlackTokenChain ,
3333 getFreshestSlackChain ,
34+ hasSlackChainMoved ,
3435 isSlackProvider ,
3536} from '@/lib/oauth/slack'
3637import {
@@ -673,12 +674,15 @@ interface CoalescedRefreshOptions {
673674/**
674675 * Slack lock budgets sized past `TOKEN_REFRESH_TIMEOUT_MS` (15s) in
675676 * lib/oauth/oauth.ts: installation-keyed locks make every sibling row's request
676- * a follower of one refresh. The lock TTL must not expire under a live refresh
677- * (15s provider call plus DB reads and the fan-out write), and followers poll
678- * for the lock's full lifetime so a slow-but-successful refresh is still
679- * observed rather than reported as a failure.
677+ * a follower of one refresh, so the TTL covers the provider call plus generous
678+ * headroom for the surrounding DB reads and the fan-out write, and followers
679+ * poll for the lock's full lifetime so a slow-but-successful refresh is still
680+ * observed rather than reported as a failure. These budgets are latency knobs,
681+ * not correctness guarantees — chain integrity under lock expiry or unlocked
682+ * concurrent writers is enforced by the version-guarded fan-out
683+ * (`ifChainUnchangedSince` in lib/oauth/slack.ts).
680684 */
681- const SLACK_LOCK_TTL_SEC = 20
685+ const SLACK_LOCK_TTL_SEC = 30
682686const SLACK_FOLLOWER_MAX_WAIT_MS = SLACK_LOCK_TTL_SEC * 1000
683687
684688async function performCoalescedRefresh ( {
@@ -727,23 +731,29 @@ async function performCoalescedRefresh({
727731 onLeader : async ( ) => {
728732 try {
729733 let refreshTokenToUse = refreshToken
734+ let slackChainVersion : Date | null = null
730735 if ( slackTeamId ) {
731736 const freshest = await getFreshestSlackChain ( slackTeamId )
732737 if ( ! freshest ) {
733738 throw new Error (
734739 `No refresh-capable account row found for Slack installation ${ slackTeamId } `
735740 )
736741 }
742+ slackChainVersion = freshest . chainVersion
737743 if (
738744 freshest . accessToken &&
739745 freshest . accessTokenExpiresAt &&
740746 freshest . accessTokenExpiresAt > new Date ( )
741747 ) {
742- await fanOutSlackTokenChain ( slackTeamId , {
743- accessToken : freshest . accessToken ,
744- refreshToken : freshest . refreshToken ,
745- accessTokenExpiresAt : freshest . accessTokenExpiresAt ,
746- } )
748+ await fanOutSlackTokenChain (
749+ slackTeamId ,
750+ {
751+ accessToken : freshest . accessToken ,
752+ refreshToken : freshest . refreshToken ,
753+ accessTokenExpiresAt : freshest . accessTokenExpiresAt ,
754+ } ,
755+ { ifChainUnchangedSince : freshest . chainVersion }
756+ )
747757 logger . info ( 'Reused freshest Slack installation token' , logContext )
748758 return freshest . accessToken
749759 }
@@ -758,19 +768,34 @@ async function performCoalescedRefresh({
758768 errorCode : result . errorCode ,
759769 } )
760770 if ( result . errorCode && isTerminalRefreshError ( result . errorCode ) ) {
761- await markCredentialDead ( scopeKey , result . errorCode )
771+ // A refresh that lost a race with a concurrent connect fails with
772+ // a revoked/rotated-out token even though the installation just
773+ // got a live chain — dead-flagging then would take down a healthy
774+ // credential for an hour.
775+ if (
776+ slackChainVersion &&
777+ ( await hasSlackChainMoved ( slackTeamId ! , slackChainVersion ) )
778+ ) {
779+ logger . info ( 'Skipping dead flag: Slack chain moved during refresh' , logContext )
780+ } else {
781+ await markCredentialDead ( scopeKey , result . errorCode )
782+ }
762783 }
763784 return null
764785 }
765786
766787 const accessTokenExpiresAt = new Date ( Date . now ( ) + result . expiresIn * 1000 )
767788
768789 if ( slackTeamId ) {
769- await fanOutSlackTokenChain ( slackTeamId , {
770- accessToken : result . accessToken ,
771- refreshToken : result . refreshToken || refreshTokenToUse ,
772- accessTokenExpiresAt,
773- } )
790+ await fanOutSlackTokenChain (
791+ slackTeamId ,
792+ {
793+ accessToken : result . accessToken ,
794+ refreshToken : result . refreshToken || refreshTokenToUse ,
795+ accessTokenExpiresAt,
796+ } ,
797+ { ifChainUnchangedSince : slackChainVersion ?? undefined }
798+ )
774799 } else {
775800 const updateData : Record < string , unknown > = {
776801 accessToken : result . accessToken ,
0 commit comments