mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05:00
* feat: first round of tools for outlook * added more * outlook finished * added bun and docs * fix: added greptile comments * added greptile and bun lint * got rid of HTML --------- Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
678 lines
20 KiB
TypeScript
678 lines
20 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import {
|
|
AirtableIcon,
|
|
ConfluenceIcon,
|
|
DiscordIcon,
|
|
GithubIcon,
|
|
GmailIcon,
|
|
GoogleCalendarIcon,
|
|
GoogleDocsIcon,
|
|
GoogleDriveIcon,
|
|
GoogleIcon,
|
|
GoogleSheetsIcon,
|
|
JiraIcon,
|
|
MicrosoftIcon,
|
|
MicrosoftTeamsIcon,
|
|
NotionIcon,
|
|
OutlookIcon,
|
|
SupabaseIcon,
|
|
xIcon,
|
|
} from '@/components/icons'
|
|
import { createLogger } from '@/lib/logs/console-logger'
|
|
import { env } from './env'
|
|
|
|
const logger = createLogger('OAuth')
|
|
|
|
// Define the base OAuth provider type
|
|
export type OAuthProvider =
|
|
| 'google'
|
|
| 'github'
|
|
| 'x'
|
|
| 'supabase'
|
|
| 'confluence'
|
|
| 'airtable'
|
|
| 'notion'
|
|
| 'jira'
|
|
| 'discord'
|
|
| 'microsoft'
|
|
| string
|
|
|
|
export type OAuthService =
|
|
| 'google'
|
|
| 'google-email'
|
|
| 'google-drive'
|
|
| 'google-docs'
|
|
| 'google-sheets'
|
|
| 'github'
|
|
| 'x'
|
|
| 'supabase'
|
|
| 'confluence'
|
|
| 'airtable'
|
|
| 'notion'
|
|
| 'jira'
|
|
| 'discord'
|
|
| 'microsoft-teams'
|
|
| 'outlook'
|
|
// Define the interface for OAuth provider configuration
|
|
export interface OAuthProviderConfig {
|
|
id: OAuthProvider
|
|
name: string
|
|
icon: (props: { className?: string }) => ReactNode
|
|
services: Record<string, OAuthServiceConfig>
|
|
defaultService: string
|
|
}
|
|
|
|
// Define the interface for OAuth service configuration
|
|
export interface OAuthServiceConfig {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
providerId: string
|
|
icon: (props: { className?: string }) => ReactNode
|
|
baseProviderIcon: (props: { className?: string }) => ReactNode
|
|
scopes: string[]
|
|
}
|
|
|
|
// Define the available OAuth providers
|
|
export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
|
google: {
|
|
id: 'google',
|
|
name: 'Google',
|
|
icon: (props) => GoogleIcon(props),
|
|
services: {
|
|
gmail: {
|
|
id: 'gmail',
|
|
name: 'Gmail',
|
|
description: 'Automate email workflows and enhance communication efficiency.',
|
|
providerId: 'google-email',
|
|
icon: (props) => GmailIcon(props),
|
|
baseProviderIcon: (props) => GoogleIcon(props),
|
|
scopes: [
|
|
'https://www.googleapis.com/auth/gmail.send',
|
|
'https://www.googleapis.com/auth/gmail.modify',
|
|
// 'https://www.googleapis.com/auth/gmail.readonly',
|
|
'https://www.googleapis.com/auth/gmail.labels',
|
|
],
|
|
},
|
|
'google-drive': {
|
|
id: 'google-drive',
|
|
name: 'Google Drive',
|
|
description: 'Streamline file organization and document workflows.',
|
|
providerId: 'google-drive',
|
|
icon: (props) => GoogleDriveIcon(props),
|
|
baseProviderIcon: (props) => GoogleIcon(props),
|
|
scopes: ['https://www.googleapis.com/auth/drive.file'],
|
|
},
|
|
'google-docs': {
|
|
id: 'google-docs',
|
|
name: 'Google Docs',
|
|
description: 'Create, read, and edit Google Documents programmatically.',
|
|
providerId: 'google-docs',
|
|
icon: (props) => GoogleDocsIcon(props),
|
|
baseProviderIcon: (props) => GoogleIcon(props),
|
|
scopes: ['https://www.googleapis.com/auth/drive.file'],
|
|
},
|
|
'google-sheets': {
|
|
id: 'google-sheets',
|
|
name: 'Google Sheets',
|
|
description: 'Manage and analyze data with Google Sheets integration.',
|
|
providerId: 'google-sheets',
|
|
icon: (props) => GoogleSheetsIcon(props),
|
|
baseProviderIcon: (props) => GoogleIcon(props),
|
|
scopes: [
|
|
'https://www.googleapis.com/auth/spreadsheets',
|
|
'https://www.googleapis.com/auth/drive.file',
|
|
],
|
|
},
|
|
'google-calendar': {
|
|
id: 'google-calendar',
|
|
name: 'Google Calendar',
|
|
description: 'Schedule and manage events with Google Calendar.',
|
|
providerId: 'google-calendar',
|
|
icon: (props) => GoogleCalendarIcon(props),
|
|
baseProviderIcon: (props) => GoogleIcon(props),
|
|
scopes: ['https://www.googleapis.com/auth/calendar'],
|
|
},
|
|
},
|
|
defaultService: 'gmail',
|
|
},
|
|
microsoft: {
|
|
id: 'microsoft',
|
|
name: 'Microsoft',
|
|
icon: (props) => MicrosoftIcon(props),
|
|
services: {
|
|
'microsoft-teams': {
|
|
id: 'microsoft-teams',
|
|
name: 'Microsoft Teams',
|
|
description: 'Connect to Microsoft Teams and manage messages.',
|
|
providerId: 'microsoft-teams',
|
|
icon: (props) => MicrosoftTeamsIcon(props),
|
|
baseProviderIcon: (props) => MicrosoftIcon(props),
|
|
scopes: [
|
|
'openid',
|
|
'profile',
|
|
'email',
|
|
'User.Read',
|
|
'Chat.Read',
|
|
'Chat.ReadWrite',
|
|
'Chat.ReadBasic',
|
|
'Channel.ReadBasic.All',
|
|
'ChannelMessage.Send',
|
|
'ChannelMessage.Read.All',
|
|
'Group.Read.All',
|
|
'Group.ReadWrite.All',
|
|
'Team.ReadBasic.All',
|
|
'offline_access',
|
|
],
|
|
},
|
|
outlook: {
|
|
id: 'outlook',
|
|
name: 'Outlook',
|
|
description: 'Connect to Outlook and manage emails.',
|
|
providerId: 'outlook',
|
|
icon: (props) => OutlookIcon(props),
|
|
baseProviderIcon: (props) => MicrosoftIcon(props),
|
|
scopes: [
|
|
'openid',
|
|
'profile',
|
|
'email',
|
|
'Mail.ReadWrite',
|
|
'Mail.ReadBasic',
|
|
'Mail.Read',
|
|
'Mail.Send',
|
|
'offline_access',
|
|
],
|
|
},
|
|
},
|
|
defaultService: 'microsoft',
|
|
},
|
|
github: {
|
|
id: 'github',
|
|
name: 'GitHub',
|
|
icon: (props) => GithubIcon(props),
|
|
services: {
|
|
github: {
|
|
id: 'github',
|
|
name: 'GitHub',
|
|
description: 'Manage repositories, issues, and pull requests.',
|
|
providerId: 'github-repo',
|
|
icon: (props) => GithubIcon(props),
|
|
baseProviderIcon: (props) => GithubIcon(props),
|
|
scopes: ['repo', 'user:email', 'read:user', 'workflow'],
|
|
},
|
|
},
|
|
defaultService: 'github',
|
|
},
|
|
x: {
|
|
id: 'x',
|
|
name: 'X',
|
|
icon: (props) => xIcon(props),
|
|
services: {
|
|
x: {
|
|
id: 'x',
|
|
name: 'X',
|
|
description: 'Read and post tweets on X (formerly Twitter).',
|
|
providerId: 'x',
|
|
icon: (props) => xIcon(props),
|
|
baseProviderIcon: (props) => xIcon(props),
|
|
scopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'],
|
|
},
|
|
},
|
|
defaultService: 'x',
|
|
},
|
|
supabase: {
|
|
id: 'supabase',
|
|
name: 'Supabase',
|
|
icon: (props) => SupabaseIcon(props),
|
|
services: {
|
|
supabase: {
|
|
id: 'supabase',
|
|
name: 'Supabase',
|
|
description: 'Connect to your Supabase projects and manage data.',
|
|
providerId: 'supabase',
|
|
icon: (props) => SupabaseIcon(props),
|
|
baseProviderIcon: (props) => SupabaseIcon(props),
|
|
scopes: ['database.read', 'database.write', 'projects.read'],
|
|
},
|
|
},
|
|
defaultService: 'supabase',
|
|
},
|
|
confluence: {
|
|
id: 'confluence',
|
|
name: 'Confluence',
|
|
icon: (props) => ConfluenceIcon(props),
|
|
services: {
|
|
confluence: {
|
|
id: 'confluence',
|
|
name: 'Confluence',
|
|
description: 'Access Confluence content and documentation.',
|
|
providerId: 'confluence',
|
|
icon: (props) => ConfluenceIcon(props),
|
|
baseProviderIcon: (props) => ConfluenceIcon(props),
|
|
scopes: ['read:page:confluence', 'write:page:confluence', 'read:me', 'offline_access'],
|
|
},
|
|
},
|
|
defaultService: 'confluence',
|
|
},
|
|
jira: {
|
|
id: 'jira',
|
|
name: 'Jira',
|
|
icon: (props) => JiraIcon(props),
|
|
services: {
|
|
jira: {
|
|
id: 'jira',
|
|
name: 'Jira',
|
|
description: 'Access Jira projects and issues.',
|
|
providerId: 'jira',
|
|
icon: (props) => JiraIcon(props),
|
|
baseProviderIcon: (props) => JiraIcon(props),
|
|
scopes: [
|
|
'read:jira-user',
|
|
'read:jira-work',
|
|
'write:jira-work',
|
|
'read:project:jira',
|
|
'read:issue-type:jira',
|
|
'read:me',
|
|
'offline_access',
|
|
],
|
|
},
|
|
},
|
|
defaultService: 'jira',
|
|
},
|
|
airtable: {
|
|
id: 'airtable',
|
|
name: 'Airtable',
|
|
icon: (props) => AirtableIcon(props),
|
|
services: {
|
|
airtable: {
|
|
id: 'airtable',
|
|
name: 'Airtable',
|
|
description: 'Manage Airtable bases, tables, and records.',
|
|
providerId: 'airtable',
|
|
icon: (props) => AirtableIcon(props),
|
|
baseProviderIcon: (props) => AirtableIcon(props),
|
|
scopes: ['data.records:read', 'data.records:write'],
|
|
},
|
|
},
|
|
defaultService: 'airtable',
|
|
},
|
|
discord: {
|
|
id: 'discord',
|
|
name: 'Discord',
|
|
icon: (props) => DiscordIcon(props),
|
|
services: {
|
|
discord: {
|
|
id: 'discord',
|
|
name: 'Discord',
|
|
description: 'Read and send messages to Discord channels and interact with servers.',
|
|
providerId: 'discord',
|
|
icon: (props) => DiscordIcon(props),
|
|
baseProviderIcon: (props) => DiscordIcon(props),
|
|
scopes: ['identify', 'bot', 'messages.read', 'guilds', 'guilds.members.read'],
|
|
},
|
|
},
|
|
defaultService: 'discord',
|
|
},
|
|
notion: {
|
|
id: 'notion',
|
|
name: 'Notion',
|
|
icon: (props) => NotionIcon(props),
|
|
services: {
|
|
notion: {
|
|
id: 'notion',
|
|
name: 'Notion',
|
|
description: 'Connect to your Notion workspace to manage pages and databases.',
|
|
providerId: 'notion',
|
|
icon: (props) => NotionIcon(props),
|
|
baseProviderIcon: (props) => NotionIcon(props),
|
|
scopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
|
|
},
|
|
},
|
|
defaultService: 'notion',
|
|
},
|
|
}
|
|
|
|
// Helper function to get a service by provider and service ID
|
|
export function getServiceByProviderAndId(
|
|
provider: OAuthProvider,
|
|
serviceId?: string
|
|
): OAuthServiceConfig {
|
|
const providerConfig = OAUTH_PROVIDERS[provider]
|
|
if (!providerConfig) {
|
|
throw new Error(`Provider ${provider} not found`)
|
|
}
|
|
|
|
if (!serviceId) {
|
|
return providerConfig.services[providerConfig.defaultService]
|
|
}
|
|
|
|
return (
|
|
providerConfig.services[serviceId] || providerConfig.services[providerConfig.defaultService]
|
|
)
|
|
}
|
|
|
|
// Helper function to determine service ID from scopes
|
|
export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]): string {
|
|
const providerConfig = OAUTH_PROVIDERS[provider]
|
|
if (!providerConfig) {
|
|
return provider
|
|
}
|
|
|
|
if (provider === 'google') {
|
|
if (scopes.some((scope) => scope.includes('gmail') || scope.includes('mail'))) {
|
|
return 'gmail'
|
|
}
|
|
if (scopes.some((scope) => scope.includes('drive'))) {
|
|
return 'google-drive'
|
|
}
|
|
if (scopes.some((scope) => scope.includes('docs'))) {
|
|
return 'google-docs'
|
|
}
|
|
if (scopes.some((scope) => scope.includes('sheets'))) {
|
|
return 'google-sheets'
|
|
}
|
|
if (scopes.some((scope) => scope.includes('calendar'))) {
|
|
return 'google-calendar'
|
|
}
|
|
} else if (provider === 'microsoft-teams') {
|
|
return 'microsoft-teams'
|
|
} else if (provider === 'outlook') {
|
|
return 'outlook'
|
|
} else if (provider === 'github') {
|
|
return 'github'
|
|
} else if (provider === 'supabase') {
|
|
return 'supabase'
|
|
} else if (provider === 'x') {
|
|
return 'x'
|
|
} else if (provider === 'confluence') {
|
|
return 'confluence'
|
|
} else if (provider === 'jira') {
|
|
return 'jira'
|
|
} else if (provider === 'airtable') {
|
|
return 'airtable'
|
|
} else if (provider === 'notion') {
|
|
return 'notion'
|
|
} else if (provider === 'discord') {
|
|
return 'discord'
|
|
}
|
|
|
|
return providerConfig.defaultService
|
|
}
|
|
|
|
// Helper function to get provider ID from service ID
|
|
export function getProviderIdFromServiceId(serviceId: string): string {
|
|
for (const provider of Object.values(OAUTH_PROVIDERS)) {
|
|
for (const [id, service] of Object.entries(provider.services)) {
|
|
if (id === serviceId) {
|
|
return service.providerId
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default fallback
|
|
return serviceId
|
|
}
|
|
|
|
// Interface for credential objects
|
|
export interface Credential {
|
|
id: string
|
|
name: string
|
|
provider: OAuthProvider
|
|
serviceId?: string
|
|
lastUsed?: string
|
|
isDefault?: boolean
|
|
}
|
|
|
|
// Interface for provider configuration
|
|
export interface ProviderConfig {
|
|
baseProvider: string
|
|
featureType: string
|
|
}
|
|
|
|
/**
|
|
* Parse a provider string into its base provider and feature type
|
|
* This is a server-safe utility that can be used in both client and server code
|
|
*/
|
|
export function parseProvider(provider: OAuthProvider): ProviderConfig {
|
|
// Handle special cases first
|
|
if (provider === 'outlook') {
|
|
return {
|
|
baseProvider: 'microsoft',
|
|
featureType: 'outlook',
|
|
}
|
|
}
|
|
|
|
// Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' })
|
|
const [base, feature] = provider.split('-')
|
|
|
|
if (feature) {
|
|
return {
|
|
baseProvider: base,
|
|
featureType: feature,
|
|
}
|
|
}
|
|
|
|
// For simple providers, use 'default' as feature type
|
|
return {
|
|
baseProvider: provider,
|
|
featureType: 'default',
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh an OAuth token
|
|
* This is a server-side utility function to refresh OAuth tokens
|
|
* @param providerId The provider ID (e.g., 'google-drive')
|
|
* @param refreshToken The refresh token to use
|
|
* @returns Object containing the new access token and expiration time in seconds, or null if refresh failed
|
|
*/
|
|
export async function refreshOAuthToken(
|
|
providerId: string,
|
|
refreshToken: string
|
|
): Promise<{ accessToken: string; expiresIn: number; refreshToken: string } | null> {
|
|
try {
|
|
// Get the provider from the providerId (e.g., 'google-drive' -> 'google')
|
|
const provider = providerId.split('-')[0]
|
|
|
|
// Determine the token endpoint based on the provider
|
|
let tokenEndpoint: string
|
|
let clientId: string | undefined
|
|
let clientSecret: string | undefined
|
|
let useBasicAuth = false
|
|
|
|
switch (provider) {
|
|
case 'google':
|
|
tokenEndpoint = 'https://oauth2.googleapis.com/token'
|
|
clientId = env.GOOGLE_CLIENT_ID
|
|
clientSecret = env.GOOGLE_CLIENT_SECRET
|
|
break
|
|
case 'github':
|
|
tokenEndpoint = 'https://github.com/login/oauth/access_token'
|
|
clientId = env.GITHUB_CLIENT_ID
|
|
clientSecret = env.GITHUB_CLIENT_SECRET
|
|
break
|
|
case 'x':
|
|
tokenEndpoint = 'https://api.x.com/2/oauth2/token'
|
|
clientId = env.X_CLIENT_ID
|
|
clientSecret = env.X_CLIENT_SECRET
|
|
useBasicAuth = true
|
|
break
|
|
case 'confluence':
|
|
tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
|
|
clientId = env.CONFLUENCE_CLIENT_ID
|
|
clientSecret = env.CONFLUENCE_CLIENT_SECRET
|
|
useBasicAuth = true
|
|
break
|
|
case 'jira':
|
|
tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
|
|
clientId = env.JIRA_CLIENT_ID
|
|
clientSecret = env.JIRA_CLIENT_SECRET
|
|
useBasicAuth = true
|
|
break
|
|
case 'airtable':
|
|
tokenEndpoint = 'https://airtable.com/oauth2/v1/token'
|
|
clientId = env.AIRTABLE_CLIENT_ID
|
|
clientSecret = env.AIRTABLE_CLIENT_SECRET
|
|
useBasicAuth = true
|
|
break
|
|
case 'supabase':
|
|
tokenEndpoint = 'https://api.supabase.com/v1/oauth/token'
|
|
clientId = env.SUPABASE_CLIENT_ID
|
|
clientSecret = env.SUPABASE_CLIENT_SECRET
|
|
break
|
|
case 'notion':
|
|
tokenEndpoint = 'https://api.notion.com/v1/oauth/token'
|
|
clientId = env.NOTION_CLIENT_ID
|
|
clientSecret = env.NOTION_CLIENT_SECRET
|
|
break
|
|
case 'discord':
|
|
tokenEndpoint = 'https://discord.com/api/v10/oauth2/token'
|
|
clientId = env.DISCORD_CLIENT_ID
|
|
clientSecret = env.DISCORD_CLIENT_SECRET
|
|
useBasicAuth = true
|
|
break
|
|
case 'microsoft':
|
|
tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
|
|
clientId = env.MICROSOFT_CLIENT_ID
|
|
clientSecret = env.MICROSOFT_CLIENT_SECRET
|
|
break
|
|
case 'outlook':
|
|
tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
|
|
clientId = env.MICROSOFT_CLIENT_ID
|
|
clientSecret = env.MICROSOFT_CLIENT_SECRET
|
|
break
|
|
default:
|
|
throw new Error(`Unsupported provider: ${provider}`)
|
|
}
|
|
|
|
if (!clientId || !clientSecret) {
|
|
throw new Error(`Missing client credentials for provider: ${provider}`)
|
|
}
|
|
|
|
// Prepare request headers and body
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
...(provider === 'github' && {
|
|
Accept: 'application/json',
|
|
}),
|
|
}
|
|
|
|
// Prepare request body
|
|
const bodyParams: Record<string, string | undefined> = {
|
|
grant_type: 'refresh_token',
|
|
refresh_token: refreshToken,
|
|
}
|
|
|
|
// For Airtable, check if we have both client ID and secret
|
|
if (provider === 'airtable') {
|
|
// Airtable requires Basic Auth with client ID and secret in the Authorization header
|
|
// Do not include client_id or client_secret in the body when using Basic Auth
|
|
if (clientId && clientSecret) {
|
|
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
|
|
headers.Authorization = `Basic ${basicAuth}`
|
|
|
|
// Make sure to include refresh_token in body params but not client_id/client_secret
|
|
// This ensures we're not sending credentials in both header and body
|
|
bodyParams.client_id = undefined
|
|
bodyParams.client_secret = undefined
|
|
} else {
|
|
throw new Error('Both client ID and client secret are required for Airtable OAuth')
|
|
}
|
|
} else if (
|
|
provider === 'x' ||
|
|
provider === 'confluence' ||
|
|
provider === 'jira' ||
|
|
provider === 'discord'
|
|
) {
|
|
const authString = `${clientId}:${clientSecret}`
|
|
const basicAuth = Buffer.from(authString).toString('base64')
|
|
headers.Authorization = `Basic ${basicAuth}`
|
|
|
|
// When using Basic Auth, don't include client_id in body
|
|
bodyParams.client_id = undefined
|
|
bodyParams.client_secret = undefined
|
|
} else {
|
|
// For other providers, use the general approach
|
|
if (useBasicAuth) {
|
|
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
|
|
headers.Authorization = `Basic ${basicAuth}`
|
|
}
|
|
|
|
if (!useBasicAuth) {
|
|
bodyParams.client_id = clientId
|
|
bodyParams.client_secret = clientSecret
|
|
}
|
|
}
|
|
|
|
// Refresh the token
|
|
const response = await fetch(tokenEndpoint, {
|
|
method: 'POST',
|
|
headers,
|
|
body: new URLSearchParams(bodyParams as Record<string, string>).toString(),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
let errorData = errorText
|
|
|
|
// Try to parse the error as JSON for better diagnostics
|
|
try {
|
|
errorData = JSON.parse(errorText)
|
|
} catch (_e) {
|
|
// Not JSON, keep as text
|
|
}
|
|
|
|
logger.error('Token refresh failed:', {
|
|
status: response.status,
|
|
error: errorText,
|
|
parsedError: errorData,
|
|
provider,
|
|
})
|
|
throw new Error(`Failed to refresh token: ${response.status} ${errorText}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
// Extract token and expiration (different providers may use different field names)
|
|
const accessToken = data.access_token
|
|
|
|
// For Airtable, also capture the new refresh token if provided
|
|
// Airtable may rotate refresh tokens
|
|
let newRefreshToken = null
|
|
if (provider === 'airtable' && data.refresh_token) {
|
|
newRefreshToken = data.refresh_token
|
|
logger.info('Received new refresh token from Airtable')
|
|
}
|
|
|
|
// For Confluence and Jira, check if we got a new refresh token
|
|
if ((provider === 'confluence' || provider === 'jira') && data.refresh_token) {
|
|
newRefreshToken = data.refresh_token
|
|
logger.info(`Received new refresh token from ${provider}`)
|
|
}
|
|
|
|
// Get expiration time - use provider's value or default to 1 hour (3600 seconds)
|
|
// Different providers use different names for this field
|
|
const expiresIn = data.expires_in || data.expiresIn || 3600
|
|
|
|
if (!accessToken) {
|
|
logger.warn('No access token found in refresh response', data)
|
|
return null
|
|
}
|
|
|
|
logger.info('Token refreshed successfully with expiration', {
|
|
expiresIn,
|
|
hasNewRefreshToken: !!newRefreshToken,
|
|
provider,
|
|
})
|
|
|
|
return {
|
|
accessToken,
|
|
expiresIn,
|
|
refreshToken: newRefreshToken || refreshToken, // Return new refresh token if available
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error refreshing token:', { error })
|
|
return null
|
|
}
|
|
}
|