/** * OAuth Flow Helpers * * Pure utility functions for OAuth operations */ import { refreshAuthorization } from '@modelcontextprotocol/sdk/client/auth.js'; import type { AuthorizationServerMetadata, OAuthClientInformation, OAuthTokens, } from '@modelcontextprotocol/sdk/shared/auth.js'; /** * Check if tokens are expired or about to expire * * @param tokens - OAuth tokens to check * @param tokenExpiryTime - Tracked expiry timestamp (if available) * @param bufferSeconds - Number of seconds before expiry to consider expired (default: 300 = 5 minutes) * @returns true if tokens are expired or will expire soon */ export function areTokensExpired( tokens: OAuthTokens | undefined, tokenExpiryTime?: number, bufferSeconds = 300 ): boolean { if (!tokens) { return true; } // If we have a tracked expiry time, use it (most accurate) if (tokenExpiryTime) { const now = Date.now(); return now >= tokenExpiryTime - bufferSeconds * 1000; } // If no expiry info, assume tokens are valid if (tokens.expires_in === undefined) { return false; } // Check if tokens will expire within the buffer period // Note: expires_in is now derived from stored expires_at, so it's always accurate return tokens.expires_in <= bufferSeconds; } /** * Refresh OAuth tokens with retry logic * * @param serverUrl - Authorization server URL * @param clientInfo - Client information * @param refreshToken - Current refresh token * @param addClientAuth - Client authentication function * @param maxRetries - Maximum retry attempts (default: 3) * @param retryDelay - Base delay between retries in ms (default: 1000) * @param fetchFn - Optional custom fetch function * @returns New OAuth tokens * @throws Error if refresh fails after all retries */ export async function refreshTokensWithRetry( serverUrl: string | URL, clientInfo: OAuthClientInformation, refreshToken: string, addClientAuth?: ( headers: Headers, params: URLSearchParams, url: string | URL, metadata?: AuthorizationServerMetadata ) => void | Promise, maxRetries = 3, retryDelay = 1000, fetchFn?: typeof fetch ): Promise { let lastError: Error | undefined; /* eslint-disable no-await-in-loop */ for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Attempt token refresh using SDK function const newTokens = await refreshAuthorization(serverUrl, { clientInformation: clientInfo, refreshToken, addClientAuthentication: addClientAuth, fetchFn, }); return newTokens; } catch (error) { lastError = error as Error; // If this isn't the last attempt, wait before retrying if (attempt < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1)) ); } } } /* eslint-enable no-await-in-loop */ throw new Error( `Token refresh failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}` ); } /** * Calculate token expiry timestamp * * @param expiresIn - Token lifetime in seconds * @returns Expiry timestamp in milliseconds */ export function calculateTokenExpiry(expiresIn: number): number { return Date.now() + expiresIn * 1000; }