diff --git a/sim/app/api/auth/oauth/connections/route.ts b/sim/app/api/auth/oauth/connections/route.ts index 78ea654428..d00f60264b 100644 --- a/sim/app/api/auth/oauth/connections/route.ts +++ b/sim/app/api/auth/oauth/connections/route.ts @@ -3,7 +3,6 @@ import { eq } from 'drizzle-orm' import { jwtDecode } from 'jwt-decode' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' -import { OAuthService } from '@/lib/oauth' import { db } from '@/db' import { account, user } from '@/db/schema' @@ -15,9 +14,6 @@ interface GoogleIdToken { name?: string } -// Valid OAuth providers -const VALID_PROVIDERS = ['google', 'github', 'x'] - /** * Get all OAuth connections for the current user */ @@ -53,7 +49,7 @@ export async function GET(request: NextRequest) { // Extract the base provider and feature type from providerId (e.g., 'google-email' -> 'google', 'email') const [provider, featureType = 'default'] = acc.providerId.split('-') - if (provider && VALID_PROVIDERS.includes(provider)) { + if (provider) { // Try multiple methods to get a user-friendly display name let displayName = '' diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 303f95d2bf..42c844ac31 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -43,6 +43,7 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/userinfo.email': 'View your email address', 'https://www.googleapis.com/auth/userinfo.profile': 'View your basic profile info', 'https://www.googleapis.com/auth/spreadsheets': 'View and manage your Google Sheets', + 'read:confluence-content.all': 'Read Confluence content', repo: 'Access your repositories', workflow: 'Manage repository workflows', 'user:email': 'Access your email address', diff --git a/sim/blocks/blocks/confluence.ts b/sim/blocks/blocks/confluence.ts index 214cb56158..262e419a9b 100644 --- a/sim/blocks/blocks/confluence.ts +++ b/sim/blocks/blocks/confluence.ts @@ -20,11 +20,14 @@ export const ConfluenceBlock: BlockConfig = { placeholder: 'Enter Confluence domain (e.g., yourcompany.atlassian.net)', }, { - id: 'email', - title: 'Email', - type: 'short-input', + id: 'credential', + title: 'Confluence Account', + type: 'oauth-input', layout: 'full', - placeholder: 'Enter your Atlassian email address', + provider: 'confluence', + serviceId: 'confluence', + requiredScopes: ['read:confluence-content.all', 'read:me', 'offline_access'], + placeholder: 'Select Confluence account', }, { id: 'pageId', @@ -33,24 +36,25 @@ export const ConfluenceBlock: BlockConfig = { layout: 'full', placeholder: 'Enter the confluence page ID (e.g., 12340)', }, - { - id: 'apiKey', - title: 'OAuth Token', - type: 'short-input', - layout: 'full', - placeholder: 'Enter your Confluence OAuth token', - password: true, - connectionDroppable: false, - }, ], tools: { access: ['confluence_retrieve'], + config: { + tool: () => 'confluence_retrieve', + params: (params) => { + const { credential, ...rest } = params + + return { + accessToken: credential, + ...rest, + } + }, + }, }, inputs: { - apiKey: { type: 'string', required: true }, - pageId: { type: 'string', required: true }, domain: { type: 'string', required: true }, - email: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + pageId: { type: 'string', required: true }, }, outputs: { response: { diff --git a/sim/blocks/index.ts b/sim/blocks/index.ts index be93115c6b..6e92328ff4 100644 --- a/sim/blocks/index.ts +++ b/sim/blocks/index.ts @@ -2,6 +2,7 @@ import { AgentBlock } from './blocks/agent' import { ApiBlock } from './blocks/api' import { ConditionBlock } from './blocks/condition' +import { ConfluenceBlock } from './blocks/confluence' import { GoogleDocsBlock } from './blocks/docs' import { GoogleDriveBlock } from './blocks/drive' import { EvaluatorBlock } from './blocks/evaluator' @@ -30,7 +31,6 @@ import { WhatsAppBlock } from './blocks/whatsapp' import { XBlock } from './blocks/x' import { YouTubeBlock } from './blocks/youtube' import { BlockConfig } from './types' -import { ConfluenceBlock } from './blocks/confluence' // Export blocks for ease of use export { @@ -72,6 +72,7 @@ const blocks: Record = { agent: AgentBlock, api: ApiBlock, condition: ConditionBlock, + confluence: ConfluenceBlock, evaluator: EvaluatorBlock, exa: ExaBlock, firecrawl: FirecrawlBlock, @@ -99,7 +100,6 @@ const blocks: Record = { whatsapp: WhatsAppBlock, x: XBlock, youtube: YouTubeBlock, - confluence: ConfluenceBlock, } // Helper functions diff --git a/sim/lib/auth.ts b/sim/lib/auth.ts index 5569c13ea7..aec1040654 100644 --- a/sim/lib/auth.ts +++ b/sim/lib/auth.ts @@ -52,7 +52,7 @@ export const auth = betterAuth({ accountLinking: { enabled: true, allowDifferentEmails: true, - trustedProviders: ['google', 'github', 'email-password'], + trustedProviders: ['google', 'github', 'email-password', 'confluence'], }, }, socialProviders: { @@ -296,6 +296,56 @@ export const auth = betterAuth({ } }, }, + + // Confluence provider + { + providerId: 'confluence', + clientId: process.env.CONFLUENCE_CLIENT_ID as string, + clientSecret: process.env.CONFLUENCE_CLIENT_SECRET as string, + authorizationUrl: 'https://auth.atlassian.com/authorize', + tokenUrl: 'https://auth.atlassian.com/oauth/token', + userInfoUrl: 'https://api.atlassian.com/me', + scopes: ['read:confluence-content.all', 'read:me', 'offline_access'], + responseType: 'code', + pkce: true, + accessType: 'offline', + prompt: 'consent', + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/confluence`, + getUserInfo: async (tokens) => { + try { + const response = await fetch('https://api.atlassian.com/me', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (!response.ok) { + logger.error('Error fetching Confluence user info:', { + status: response.status, + statusText: response.statusText, + }) + return null + } + + const profile = await response.json() + + const now = new Date() + + return { + id: profile.account_id, + name: profile.name || profile.display_name || 'Confluence User', + email: profile.email || `${profile.account_id}@atlassian.com`, + image: profile.picture || null, + emailVerified: true, // Assume verified since it's an Atlassian account + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Confluence getUserInfo:', { error }) + return null + } + }, + }, ], }), ], diff --git a/sim/lib/oauth.ts b/sim/lib/oauth.ts index 28c3d32854..cca381c98e 100644 --- a/sim/lib/oauth.ts +++ b/sim/lib/oauth.ts @@ -1,5 +1,6 @@ import { ReactNode } from 'react' import { + ConfluenceIcon, GithubIcon, GmailIcon, GoogleCalendarIcon, @@ -15,7 +16,7 @@ import { createLogger } from '@/lib/logs/console-logger' const logger = createLogger('OAuth') // Define the base OAuth provider type -export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | string +export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | 'confluence' | string export type OAuthService = | 'google' | 'google-email' @@ -25,6 +26,7 @@ export type OAuthService = | 'github' | 'x' | 'supabase' + | 'confluence' // Define the interface for OAuth provider configuration export interface OAuthProviderConfig { @@ -171,6 +173,23 @@ export const OAUTH_PROVIDERS: Record = { }, 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:confluence-content.all', 'read:me', 'offline_access'], + }, + }, + defaultService: 'confluence', + }, } // Helper function to get a service by provider and service ID @@ -219,6 +238,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] return 'supabase' } else if (provider === 'x') { return 'x' + } else if (provider === 'confluence') { + return 'confluence' } return providerConfig.defaultService @@ -312,6 +333,11 @@ export async function refreshOAuthToken( clientId = process.env.X_CLIENT_ID clientSecret = process.env.X_CLIENT_SECRET break + case 'confluence': + tokenEndpoint = 'https://auth.atlassian.com/oauth/token' + clientId = process.env.CONFLUENCE_CLIENT_ID + clientSecret = process.env.CONFLUENCE_CLIENT_SECRET + break default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/sim/tools/confluence/retrieve.ts b/sim/tools/confluence/retrieve.ts index 722bce1f7d..300881e2d4 100644 --- a/sim/tools/confluence/retrieve.ts +++ b/sim/tools/confluence/retrieve.ts @@ -1,10 +1,9 @@ import { ToolConfig, ToolResponse } from '../types' export interface ConfluenceRetrieveParams { - apiKey: string + accessToken: string pageId: string - domain: string - email: string + domain: string } export interface ConfluenceRetrieveResponse extends ToolResponse { @@ -16,18 +15,21 @@ export interface ConfluenceRetrieveResponse extends ToolResponse { } } -export const confluenceRetrieveTool: ToolConfig = { +export const confluenceRetrieveTool: ToolConfig< + ConfluenceRetrieveParams, + ConfluenceRetrieveResponse +> = { id: 'confluence_retrieve', name: 'Confluence Retrieve', description: 'Retrieve content from Confluence pages using the Confluence API.', version: '1.0.0', params: { - apiKey: { + accessToken: { type: 'string', required: true, requiredForToolCall: true, - description: 'Your Confluence API token', + description: 'OAuth access token for Confluence', }, domain: { type: 'string', @@ -35,30 +37,24 @@ export const confluenceRetrieveTool: ToolConfig { - return `https://${params.domain}/wiki/rest/api/content/${params.pageId}?expand=body.view`; + return `https://${params.domain}/wiki/rest/api/content/${params.pageId}?expand=body.view` }, method: 'GET', headers: (params: ConfluenceRetrieveParams) => { return { 'Content-Type': 'application/json', - Authorization: `Basic ${Buffer.from(`${params.email}:${params.apiKey}`).toString('base64')}`, - }; - } + Authorization: `Bearer ${params.accessToken}`, + } + }, }, transformResponse: async (response: Response) => { @@ -66,15 +62,15 @@ export const confluenceRetrieveTool: ToolConfig]*>/g, '') - .replace(/ /g, ' ') + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') - .replace(/\s+/g, ' ') - .trim(); + .replace(/\s+/g, ' ') + .trim() return { success: true, @@ -83,12 +79,12 @@ export const confluenceRetrieveTool: ToolConfig { const message = error.message || 'Confluence retrieve failed' return message - } + }, }