( credentialId: string, userId: string, requestId: string, scopes?: string[], impersonateEmail?: string )
| 499 | * @returns The valid access token or null if refresh fails |
| 500 | */ |
| 501 | export async function refreshAccessTokenIfNeeded( |
| 502 | credentialId: string, |
| 503 | userId: string, |
| 504 | requestId: string, |
| 505 | scopes?: string[], |
| 506 | impersonateEmail?: string |
| 507 | ): Promise<string | null> { |
| 508 | const resolved = await resolveOAuthAccountId(credentialId) |
| 509 | if (!resolved) { |
| 510 | return null |
| 511 | } |
| 512 | |
| 513 | if (resolved.credentialType === 'service_account' && resolved.credentialId) { |
| 514 | if (resolved.providerId === ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID) { |
| 515 | logger.info(`[${requestId}] Using Atlassian service account token for credential`) |
| 516 | return getAtlassianServiceAccountToken(resolved.credentialId) |
| 517 | } |
| 518 | if (!scopes?.length) { |
| 519 | throw new Error('Scopes are required for service account credentials') |
| 520 | } |
| 521 | logger.info(`[${requestId}] Using service account token for credential`) |
| 522 | return getServiceAccountToken(resolved.credentialId, scopes, impersonateEmail) |
| 523 | } |
| 524 | |
| 525 | // Use the already-resolved account ID to avoid a redundant resolveOAuthAccountId query |
| 526 | const credential = await getCredentialByAccountId(requestId, resolved.accountId, userId) |
| 527 | |
| 528 | if (!credential) { |
| 529 | return null |
| 530 | } |
| 531 | |
| 532 | // Decide if we should refresh: token missing OR expired |
| 533 | const accessTokenExpiresAt = credential.accessTokenExpiresAt |
| 534 | const refreshTokenExpiresAt = credential.refreshTokenExpiresAt |
| 535 | const now = new Date() |
| 536 | |
| 537 | // Check if access token needs refresh (missing or expired) |
| 538 | const accessTokenNeedsRefresh = |
| 539 | !!credential.refreshToken && |
| 540 | (!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now)) |
| 541 | |
| 542 | // Check if we should proactively refresh to prevent refresh token expiry |
| 543 | // This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity |
| 544 | const proactiveRefreshThreshold = new Date( |
| 545 | now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000 |
| 546 | ) |
| 547 | const refreshTokenNeedsProactiveRefresh = |
| 548 | !!credential.refreshToken && |
| 549 | isMicrosoftProvider(credential.providerId) && |
| 550 | refreshTokenExpiresAt && |
| 551 | refreshTokenExpiresAt <= proactiveRefreshThreshold |
| 552 | |
| 553 | const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh |
| 554 | |
| 555 | const accessToken = credential.accessToken |
| 556 | |
| 557 | if (shouldRefresh) { |
| 558 | const resolvedCredentialId = |
no test coverage detected