| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { getProviderConnections, updateProviderConnection } from "@/lib/localDb"; |
| import { |
| getAccessToken, |
| supportsTokenRefresh, |
| isUnrecoverableRefreshError, |
| } from "@omniroute/open-sse/services/tokenRefresh.ts"; |
|
|
| |
| const TICK_MS = 60 * 1000; |
| const DEFAULT_HEALTH_CHECK_INTERVAL_MIN = 60; |
| const LOG_PREFIX = "[HealthCheck]"; |
|
|
| |
| let initialized = false; |
| let intervalHandle = null; |
|
|
| |
| |
| |
| export function initTokenHealthCheck() { |
| if (initialized) return; |
| initialized = true; |
|
|
| console.log( |
| `${LOG_PREFIX} Starting proactive token health-check (tick every ${TICK_MS / 1000}s)` |
| ); |
|
|
| |
| setTimeout(() => { |
| sweep(); |
| intervalHandle = setInterval(sweep, TICK_MS); |
| }, 10_000); |
| } |
|
|
| |
| |
| |
| export function stopTokenHealthCheck() { |
| if (intervalHandle) { |
| clearInterval(intervalHandle); |
| intervalHandle = null; |
| } |
| initialized = false; |
| } |
|
|
| |
| async function sweep() { |
| try { |
| const connections = await getProviderConnections({ authType: "oauth" }); |
|
|
| if (!connections || connections.length === 0) return; |
|
|
| for (const conn of connections) { |
| try { |
| await checkConnection(conn); |
| } catch (err) { |
| |
| console.error(`${LOG_PREFIX} Error checking ${conn.name || conn.id}:`, err.message); |
| } |
| } |
| } catch (err) { |
| console.error(`${LOG_PREFIX} Sweep error:`, err.message); |
| } |
| } |
|
|
| |
| |
| |
| async function checkConnection(conn) { |
| |
| const intervalMin = conn.healthCheckInterval ?? DEFAULT_HEALTH_CHECK_INTERVAL_MIN; |
| if (intervalMin <= 0) return; |
| if (!conn.isActive) return; |
| if (!conn.refreshToken || typeof conn.refreshToken !== "string") return; |
|
|
| |
| if (conn.testStatus === "expired") return; |
|
|
| if (!supportsTokenRefresh(conn.provider)) { |
| const now = new Date().toISOString(); |
| await updateProviderConnection(conn.id, { lastHealthCheckAt: now }); |
| console.log( |
| `${LOG_PREFIX} Skipping ${conn.provider}/${conn.name || conn.email || conn.id} (refresh unsupported)` |
| ); |
| return; |
| } |
|
|
| const intervalMs = intervalMin * 60 * 1000; |
| const lastCheck = conn.lastHealthCheckAt ? new Date(conn.lastHealthCheckAt).getTime() : 0; |
|
|
| |
| if (Date.now() - lastCheck < intervalMs) return; |
|
|
| console.log( |
| `${LOG_PREFIX} Refreshing ${conn.provider}/${conn.name || conn.email || conn.id} (interval: ${intervalMin}min)` |
| ); |
|
|
| const credentials = { |
| refreshToken: conn.refreshToken, |
| accessToken: conn.accessToken, |
| expiresAt: conn.tokenExpiresAt, |
| providerSpecificData: conn.providerSpecificData, |
| }; |
|
|
| const result = await getAccessToken(conn.provider, credentials, { |
| info: (tag, msg) => console.log(`${LOG_PREFIX} [${tag}] ${msg}`), |
| warn: (tag, msg) => console.warn(`${LOG_PREFIX} [${tag}] ${msg}`), |
| error: (tag, msg, extra) => console.error(`${LOG_PREFIX} [${tag}] ${msg}`, extra || ""), |
| }); |
|
|
| const now = new Date().toISOString(); |
|
|
| |
| |
| |
| |
| if (isUnrecoverableRefreshError(result)) { |
| await updateProviderConnection(conn.id, { |
| lastHealthCheckAt: now, |
| testStatus: "expired", |
| lastError: `Refresh token consumed (${result.error}). Please re-authenticate this account.`, |
| lastErrorAt: now, |
| lastErrorType: result.error, |
| lastErrorSource: "oauth", |
| errorCode: result.error, |
| isActive: false, |
| refreshToken: null, |
| }); |
| console.error( |
| `${LOG_PREFIX} β ${conn.provider}/${conn.name || conn.email || conn.id} β ` + |
| `Refresh token is permanently invalid (${result.error}). ` + |
| `Connection deactivated. Re-authenticate to restore.` |
| ); |
| return; |
| } |
|
|
| if (result && result.accessToken) { |
| |
| const updateData: any = { |
| accessToken: result.accessToken, |
| lastHealthCheckAt: now, |
| testStatus: "active", |
| lastError: null, |
| lastErrorAt: null, |
| lastErrorType: null, |
| lastErrorSource: null, |
| errorCode: null, |
| }; |
|
|
| if (result.refreshToken) { |
| updateData.refreshToken = result.refreshToken; |
| } |
|
|
| if (result.expiresIn) { |
| updateData.tokenExpiresAt = new Date(Date.now() + result.expiresIn * 1000).toISOString(); |
| } |
|
|
| await updateProviderConnection(conn.id, updateData); |
| console.log(`${LOG_PREFIX} β ${conn.provider}/${conn.name || conn.email || conn.id} refreshed`); |
| } else { |
| |
| await updateProviderConnection(conn.id, { |
| lastHealthCheckAt: now, |
| testStatus: "error", |
| lastError: "Health check: token refresh failed", |
| lastErrorAt: now, |
| lastErrorType: "token_refresh_failed", |
| lastErrorSource: "oauth", |
| errorCode: "refresh_failed", |
| }); |
| console.warn( |
| `${LOG_PREFIX} β ${conn.provider}/${conn.name || conn.email || conn.id} refresh failed` |
| ); |
| } |
| } |
|
|
| |
| initTokenHealthCheck(); |
|
|
| export default initTokenHealthCheck; |
|
|