trainingpeaks-sdk
Version:
TypeScript SDK for TrainingPeaks API integration
123 lines (122 loc) • 4.78 kB
JavaScript
import { refreshAuthToken as refreshUserAuthToken } from '../../adapters/public-api/endpoints/users/v3/token/index.js';
import { hasRefreshCapability, isTokenExpired, refreshAuthToken as updateAuthToken, shouldRefreshToken, } from '../../domain/index.js';
const performTokenRefresh = async (currentToken, httpClient, sessionStorage, logger) => {
try {
logger.info('Starting token refresh', {
currentTokenExpires: currentToken.expires,
refreshToken: currentToken.refreshToken ? '***' : 'not_available',
});
const refreshResponse = await refreshUserAuthToken(httpClient, {
refreshToken: currentToken.refreshToken,
});
if (!refreshResponse.success || !refreshResponse.data) {
logger.error('Token refresh failed - invalid response', {
success: refreshResponse.success,
hasData: !!refreshResponse.data,
error: refreshResponse.error,
});
return null;
}
const tokenData = refreshResponse.data.token;
const newToken = updateAuthToken(currentToken, tokenData.access_token, new Date(tokenData.expires), tokenData.refresh_token);
logger.info('Token refresh successful', {
oldExpires: currentToken.expires,
newExpires: newToken.expires,
tokenType: newToken.tokenType,
});
const session = await sessionStorage.get();
if (session) {
await sessionStorage.set({
...session,
token: newToken,
});
logger.debug('Session updated with refreshed token');
}
return newToken;
}
catch (error) {
logger.error('Token refresh failed with error', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
return null;
}
};
const ensureValidToken = async (state, httpClient, sessionStorage, logger) => {
try {
const session = await sessionStorage.get();
if (!session?.token) {
logger.debug('No token found in session');
return null;
}
const token = session.token;
if (!shouldRefreshToken(token) && !isTokenExpired(token)) {
return token;
}
logger.info('Token needs refresh', {
isExpired: isTokenExpired(token),
shouldRefresh: shouldRefreshToken(token),
hasRefreshCapability: hasRefreshCapability(token),
});
if (!hasRefreshCapability(token)) {
logger.warn('Token cannot be refreshed - no refresh token available');
return null;
}
if (state.refreshPromise) {
logger.debug('Refresh already in progress, waiting for completion');
return await state.refreshPromise;
}
const now = Date.now();
const currentCooldown = Math.min(30000 * Math.pow(2, state.failureCount), 300000);
if (now - state.lastRefreshAttempt < currentCooldown) {
logger.warn('Token refresh attempted too recently, skipping', {
lastAttempt: state.lastRefreshAttempt,
failureCount: state.failureCount,
currentCooldown,
cooldownRemaining: currentCooldown - (now - state.lastRefreshAttempt),
});
return isTokenExpired(token) ? null : token;
}
state.refreshPromise = performTokenRefresh(token, httpClient, sessionStorage, logger);
try {
const refreshedToken = await state.refreshPromise;
state.lastRefreshAttempt = now;
if (refreshedToken) {
state.failureCount = 0;
}
else {
state.failureCount++;
}
return refreshedToken;
}
catch (error) {
state.lastRefreshAttempt = now;
state.failureCount++;
throw error;
}
finally {
state.refreshPromise = null;
}
}
catch (error) {
logger.error('Failed to ensure valid token', { error });
return null;
}
};
const resetTokenRefreshState = (state) => {
state.refreshPromise = null;
state.lastRefreshAttempt = 0;
};
export const createTokenRefreshHandler = (httpClient, sessionStorage, logger) => {
const state = {
refreshPromise: null,
lastRefreshAttempt: 0,
failureCount: 0,
REFRESH_COOLDOWN: 30000,
MAX_BACKOFF: 300000,
};
return {
ensureValidToken: () => ensureValidToken(state, httpClient, sessionStorage, logger),
reset: () => resetTokenRefreshState(state),
};
};