Repository: simstudioai/sim
Pull request #251: fix(oauth): fixed oauth server-side vs client-side checking, fixed x_search tool failing, remove unused route
Author: login: waleedlatif1
Created: 2025-04-12T00:03:35Z | Merged: 2025-04-12T00:57:34Z

- **fix(oauth): fixed oauth server-side vs client-side checking, fixed x_search tool failing, remove unused route**
- **added tests**
- **removed unnecessary checks for nil request id**

## Description

Fixed oauth server-side vs client-side checking, fixed x_search tool failing, remove unused route. Since we moved all the LLM requests server-side, we now have to add the workflowId as context to authenticate the requests

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] Code refactoring (no functional changes)

## How Has This Been Tested?

Ran many tools that require oauth as standalone blocks as well as apart of an agent. All of them worked.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] All tests pass locally and in CI (`npm test`)
- [x] My changes generate no new warnings
- [x] Any dependent changes have been merged and published in downstream modules
- [x]  I have updated version numbers as needed (if needed)

## Security Considerations:

- [x] My changes do not introduce any new security vulnerabilities
- [x] I have considered the security implications of my changes


Diff:
diff --git a/sim/app/api/auth/oauth/token/route.test.ts b/sim/app/api/auth/oauth/token/route.test.ts
new file mode 100644
index 00000000000..71a241a8155
--- /dev/null
+++ b/sim/app/api/auth/oauth/token/route.test.ts
@@ -0,0 +1,337 @@
+/**
+ * Tests for OAuth token API routes
+ *
+ * @vitest-environment node
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createMockRequest } from '@/app/api/__test-utils__/utils'
+
+describe('OAuth Token API Routes', () => {
+  const mockGetUserId = vi.fn()
+  const mockGetCredential = vi.fn()
+  const mockRefreshTokenIfNeeded = vi.fn()
+
+  const mockLogger = {
+    info: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+    debug: vi.fn(),
+  }
+
+  const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
+  const mockRequestId = mockUUID.slice(0, 8)
+
+  beforeEach(() => {
+    vi.resetModules()
+
+    vi.stubGlobal('crypto', {
+      randomUUID: vi.fn().mockReturnValue(mockUUID),
+    })
+
+    vi.doMock('../utils', () => ({
+      getUserId: mockGetUserId,
+      getCredential: mockGetCredential,
+      refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
+    }))
+
+    vi.doMock('@/lib/logs/console-logger', () => ({
+      createLogger: vi.fn().mockReturnValue(mockLogger),
+    }))
+  })
+
+  afterEach(() => {
+    vi.clearAllMocks()
+  })
+
+  /**
+   * POST route tests
+   */
+  describe('POST handler', () => {
+    it('should return access token successfully', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: 'test-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
+        providerId: 'google',
+      })
+      mockRefreshTokenIfNeeded.mockResolvedValueOnce({
+        accessToken: 'fresh-token',
+        refreshed: false,
+      })
+
+      // Create mock request
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+      })
+
+      // Import handler after setting up mocks
+      const { POST } = await import('./route')
+
+      // Call handler
+      const response = await POST(req)
+      const data = await response.json()
+
+      // Verify request was handled correctly
+      expect(response.status).toBe(200)
+      expect(data).toHaveProperty('accessToken', 'fresh-token')
+
+      // Verify mocks were called correctly
+      expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId, undefined)
+      expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
+      expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
+    })
+
+    it('should handle workflowId for server-side authentication', async () => {
+      mockGetUserId.mockResolvedValueOnce('workflow-owner-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: 'test-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
+        providerId: 'google',
+      })
+      mockRefreshTokenIfNeeded.mockResolvedValueOnce({
+        accessToken: 'fresh-token',
+        refreshed: false,
+      })
+
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+        workflowId: 'workflow-id',
+      })
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(200)
+      expect(data).toHaveProperty('accessToken', 'fresh-token')
+
+      expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId, 'workflow-id')
+      expect(mockGetCredential).toHaveBeenCalledWith(
+        mockRequestId,
+        'credential-id',
+        'workflow-owner-id'
+      )
+    })
+
+    it('should handle missing credentialId', async () => {
+      const req = createMockRequest('POST', {})
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(400)
+      expect(data).toHaveProperty('error', 'Credential ID is required')
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should handle authentication failure', async () => {
+      mockGetUserId.mockResolvedValueOnce(undefined)
+
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+      })
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(401)
+      expect(data).toHaveProperty('error', 'User not authenticated')
+    })
+
+    it('should handle workflow not found', async () => {
+      mockGetUserId.mockResolvedValueOnce(undefined)
+
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+        workflowId: 'nonexistent-workflow-id',
+      })
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(404)
+      expect(data).toHaveProperty('error', 'Workflow not found')
+    })
+
+    it('should handle credential not found', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce(undefined)
+
+      const req = createMockRequest('POST', {
+        credentialId: 'nonexistent-credential-id',
+      })
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(404)
+      expect(data).toHaveProperty('error', 'Credential not found')
+    })
+
+    it('should handle token refresh failure', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: 'test-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // Expired
+        providerId: 'google',
+      })
+      mockRefreshTokenIfNeeded.mockRejectedValueOnce(new Error('Refresh failure'))
+
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+      })
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(401)
+      expect(data).toHaveProperty('error', 'Failed to refresh access token')
+    })
+  })
+
+  /**
+   * GET route tests
+   */
+  describe('GET handler', () => {
+    it('should return access token successfully', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: 'test-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
+        providerId: 'google',
+      })
+      mockRefreshTokenIfNeeded.mockResolvedValueOnce({
+        accessToken: 'fresh-token',
+        refreshed: false,
+      })
+
+      const req = new Request(
+        'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
+      )
+
+      const { GET } = await import('./route')
+
+      const response = await GET(req as any)
+      const data = await response.json()
+
+      expect(response.status).toBe(200)
+      expect(data).toHaveProperty('accessToken', 'fresh-token')
+
+      expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId)
+      expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
+      expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
+    })
+
+    it('should handle missing credentialId', async () => {
+      const req = new Request('http://localhost:3000/api/auth/oauth/token')
+
+      const { GET } = await import('./route')
+
+      const response = await GET(req as any)
+      const data = await response.json()
+
+      expect(response.status).toBe(400)
+      expect(data).toHaveProperty('error', 'Credential ID is required')
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should handle authentication failure', async () => {
+      mockGetUserId.mockResolvedValueOnce(undefined)
+
+      const req = new Request(
+        'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
+      )
+
+      const { GET } = await import('./route')
+
+      const response = await GET(req as any)
+      const data = await response.json()
+
+      expect(response.status).toBe(401)
+      expect(data).toHaveProperty('error', 'User not authenticated')
+    })
+
+    it('should handle credential not found', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce(undefined)
+
+      const req = new Request(
+        'http://localhost:3000/api/auth/oauth/token?credentialId=nonexistent-credential-id'
+      )
+
+      const { GET } = await import('./route')
+
+      const response = await GET(req as any)
+      const data = await response.json()
+
+      expect(response.status).toBe(404)
+      expect(data).toHaveProperty('error', 'Credential not found')
+    })
+
+    it('should handle missing access token', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: null,
+        refreshToken: 'refresh-token',
+        providerId: 'google',
+      })
+
+      const req = new Request(
+        'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
+      )
+
+      const { GET } = await import('./route')
+
+      const response = await GET(req as any)
+      const data = await response.json()
+
+      expect(response.status).toBe(400)
+      expect(data).toHaveProperty('error', 'No access token available')
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should handle token refresh failure', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: 'test-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // Expired
+        providerId: 'google',
+      })
+      mockRefreshTokenIfNeeded.mockRejectedValueOnce(new Error('Refresh failure'))
+
+      const req = new Request(
+        'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
+      )
+
+      const { GET } = await import('./route')
+
+      const response = await GET(req as any)
+      const data = await response.json()
+
+      expect(response.status).toBe(401)
+      expect(data).toHaveProperty('error', 'Failed to refresh access token')
+    })
+  })
+})
diff --git a/sim/app/api/auth/oauth/token/route.ts b/sim/app/api/auth/oauth/token/route.ts
index dabe5c7dca2..d515b98330b 100644
--- a/sim/app/api/auth/oauth/token/route.ts
+++ b/sim/app/api/auth/oauth/token/route.ts
@@ -1,10 +1,6 @@
 import { NextRequest, NextResponse } from 'next/server'
-import { and, eq } from 'drizzle-orm'
-import { getSession } from '@/lib/auth'
 import { createLogger } from '@/lib/logs/console-logger'
-import { refreshOAuthToken } from '@/lib/oauth'
-import { db } from '@/db'
-import { account, workflow } from '@/db/schema'
+import { getCredential, getUserId, refreshTokenIfNeeded } from '../utils'
 
 const logger = createLogger('OAuthTokenAPI')
 
@@ -27,97 +23,29 @@ export async function POST(request: NextRequest) {
     }
 
     // Determine the user ID based on the context
-    let userId: string | undefined
-
-    // If workflowId is provided, this is a server-side request
-    if (workflowId) {
-      // Get the workflow to verify the user ID
-      const workflows = await db
-        .select({ userId: workflow.userId })
-        .from(workflow)
-        .where(eq(workflow.id, workflowId))
-        .limit(1)
-
-      if (!workflows.length) {
-        logger.warn(`[${requestId}] Workflow not found`)
-        return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
-      }
-
-      userId = workflows[0].userId
-    } else {
-      // This is a client-side request, use the session
-      const session = await getSession()
-
-      // Check if the user is authenticated
-      if (!session?.user?.id) {
-        logger.warn(`[${requestId}] Unauthenticated token request rejected`)
-        return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
-      }
-
-      userId = session.user.id
+    const userId = await getUserId(requestId, workflowId)
+
+    if (!userId) {
+      return NextResponse.json(
+        { error: workflowId ? 'Workflow not found' : 'User not authenticated' },
+        { status: workflowId ? 404 : 401 }
+      )
     }
 
     // Get the credential from the database
-    const credentials = await db
-      .select()
-      .from(account)
-      .where(and(eq(account.id, credentialId), eq(account.userId, userId)))
-      .limit(1)
-
-    if (!credentials.length) {
-      logger.warn(`[${requestId}] Credential not found`)
+    const credential = await getCredential(requestId, credentialId, userId)
+
+    if (!credential) {
       return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
     }
 
-    const credential = credentials[0]
-
-    // Check if we need to refresh the token
-    const expiresAt = credential.accessTokenExpiresAt
-    const now = new Date()
-    const needsRefresh = !expiresAt || expiresAt <= now
-
-    if (needsRefresh && credential.refreshToken) {
-      try {
-        const refreshResult = await refreshOAuthToken(
-          credential.providerId,
-          credential.refreshToken
-        )
-
-        if (!refreshResult) {
-          throw new Error('Failed to refresh token')
-        }
-
-        const {
-          accessToken: refreshedToken,
-          expiresIn,
-          refreshToken: newRefreshToken,
-        } = refreshResult
-
-        // Prepare update data
-        const updateData: any = {
-          accessToken: refreshedToken,
-          accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
-          updatedAt: new Date(),
-        }
-
-        // If we received a new refresh token, update it
-        if (newRefreshToken && newRefreshToken !== credential.refreshToken) {
-          logger.info(`[${requestId}] Updating refresh token for credential: ${credentialId}`)
-          updateData.refreshToken = newRefreshToken
-        }
-
-        await db.update(account).set(updateData).where(eq(account.id, credentialId))
-
-        logger.info(`[${requestId}] Successfully refreshed access token`)
-        return NextResponse.json({ accessToken: refreshedToken }, { status: 200 })
-      } catch (error) {
-        logger.error(`[${requestId}] Error refreshing token`, error)
-        return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 500 })
-      }
+    try {
+      // Refresh the token if needed
+      const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
+      return NextResponse.json({ accessToken }, { status: 200 })
+    } catch (error) {
+      return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })
     }
-
-    logger.info(`[${requestId}] Access token is valid`)
-    return NextResponse.json({ accessToken: credential.accessToken }, { status: 200 })
   } catch (error) {
     logger.error(`[${requestId}] Error getting access token`, error)
     return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
@@ -131,15 +59,6 @@ export async function GET(request: NextRequest) {
   const requestId = crypto.randomUUID().slice(0, 8) // Short request ID for correlation
 
   try {
-    // Get the session
-    const session = await getSession()
-
-    // Check if the user is authenticated
-    if (!session?.user?.id) {
-      logger.warn(`[${requestId}] Unauthenticated request rejected`)
-      return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
-    }
-
     // Get the credential ID from the query params
     const { searchParams } = new URL(request.url)
     const credentialId = searchParams.get('credentialId')
@@ -149,20 +68,18 @@ export async function GET(request: NextRequest) {
       return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
     }
 
-    // Get the credential from the database
-    const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
+    // For GET requests, we only support session-based authentication
+    const userId = await getUserId(requestId)
 
-    if (!credentials.length) {
-      logger.warn(`[${requestId}] Credential not found`, { credentialId })
-      return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
+    if (!userId) {
+      return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
     }
 
-    const credential = credentials[0]
+    // Get the credential from the database
+    const credential = await getCredential(requestId, credentialId, userId)
 
-    // Check if the credential belongs to the user
-    if (credential.userId !== session.user.id) {
-      logger.warn(`[${requestId}] Unauthorized credential access attempt`)
-      return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
+    if (!credential) {
+      return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
     }
 
     // Check if the access token is valid
@@ -171,57 +88,13 @@ export async function GET(request: NextRequest) {
       return NextResponse.json({ error: 'No access token available' }, { status: 400 })
     }
 
-    // Check if the token is expired and refresh if needed
-    const now = new Date()
-    const tokenExpiry = credential.accessTokenExpiresAt
-    let accessToken = credential.accessToken
-
-    if (tokenExpiry && tokenExpiry < now && credential.refreshToken) {
-      logger.info(`[${requestId}] Access token expired, attempting to refresh`)
-
-      try {
-        // Refresh the token using the centralized utility
-        const refreshResult = await refreshOAuthToken(
-          credential.providerId,
-          credential.refreshToken
-        )
-
-        if (!refreshResult) {
-          throw new Error('Failed to refresh token')
-        }
-
-        const {
-          accessToken: refreshedToken,
-          expiresIn,
-          refreshToken: newRefreshToken,
-        } = refreshResult
-        logger.info(`[${requestId}] Token refreshed successfully`)
-
-        // Prepare update data
-        const updateData: any = {
-          accessToken: refreshedToken,
-          accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
-          updatedAt: new Date(),
-        }
-
-        // If we received a new refresh token, update it
-        if (newRefreshToken && newRefreshToken !== credential.refreshToken) {
-          logger.info(`[${requestId}] Updating refresh token for credential: ${credentialId}`)
-          updateData.refreshToken = newRefreshToken
-        }
-
-        // Update the token in the database with the correct expiration time
-        await db.update(account).set(updateData).where(eq(account.id, credentialId))
-
-        accessToken = refreshedToken
-      } catch (refreshError) {
-        logger.error(`[${requestId}] Error refreshing token`, refreshError)
-        return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })
-      }
+    try {
+      // Refresh the token if needed
+      const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
+      return NextResponse.json({ accessToken }, { status: 200 })
+    } catch (error) {
+      return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })
     }
-
-    // Return the access token
-    return NextResponse.json({ accessToken }, { status: 200 })
   } catch (error) {
     logger.error(`[${requestId}] Error fetching access token`, error)
     return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
diff --git a/sim/app/api/auth/oauth/utils.test.ts b/sim/app/api/auth/oauth/utils.test.ts
new file mode 100644
index 00000000000..5f59e545d9e
--- /dev/null
+++ b/sim/app/api/auth/oauth/utils.test.ts
@@ -0,0 +1,292 @@
+/**
+ * Tests for OAuth utility functions
+ *
+ * @vitest-environment node
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+describe('OAuth Utils', () => {
+  const mockSession = { user: { id: 'test-user-id' } }
+  const mockDb = {
+    select: vi.fn().mockReturnThis(),
+    from: vi.fn().mockReturnThis(),
+    where: vi.fn().mockReturnThis(),
+    limit: vi.fn().mockReturnValue([]),
+    update: vi.fn().mockReturnThis(),
+    set: vi.fn().mockReturnThis(),
+    orderBy: vi.fn().mockReturnThis(),
+  }
+  const mockRefreshOAuthToken = vi.fn()
+  const mockLogger = {
+    info: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+    debug: vi.fn(),
+  }
+
+  beforeEach(() => {
+    vi.resetModules()
+
+    vi.doMock('@/lib/auth', () => ({
+      getSession: vi.fn().mockResolvedValue(mockSession),
+    }))
+
+    vi.doMock('@/db', () => ({
+      db: mockDb,
+    }))
+
+    vi.doMock('@/lib/oauth', () => ({
+      refreshOAuthToken: mockRefreshOAuthToken,
+    }))
+
+    vi.doMock('@/lib/logs/console-logger', () => ({
+      createLogger: vi.fn().mockReturnValue(mockLogger),
+    }))
+  })
+
+  afterEach(() => {
+    vi.clearAllMocks()
+  })
+
+  describe('getUserId', () => {
+    it('should get user ID from session when no workflowId is provided', async () => {
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id')
+
+      expect(userId).toBe('test-user-id')
+    })
+
+    it('should get user ID from workflow when workflowId is provided', async () => {
+      mockDb.limit.mockReturnValueOnce([{ userId: 'workflow-owner-id' }])
+
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id', 'workflow-id')
+
+      expect(mockDb.select).toHaveBeenCalled()
+      expect(mockDb.from).toHaveBeenCalled()
+      expect(mockDb.where).toHaveBeenCalled()
+      expect(mockDb.limit).toHaveBeenCalledWith(1)
+      expect(userId).toBe('workflow-owner-id')
+    })
+
+    it('should return undefined if no session is found', async () => {
+      vi.doMock('@/lib/auth', () => ({
+        getSession: vi.fn().mockResolvedValue(null),
+      }))
+
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id')
+
+      expect(userId).toBeUndefined()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should return undefined if workflow is not found', async () => {
+      mockDb.limit.mockReturnValueOnce([])
+
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id', 'nonexistent-workflow-id')
+
+      expect(userId).toBeUndefined()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+  })
+
+  describe('getCredential', () => {
+    it('should return credential when found', async () => {
+      const mockCredential = { id: 'credential-id', userId: 'test-user-id' }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      const { getCredential } = await import('./utils')
+
+      const credential = await getCredential('request-id', 'credential-id', 'test-user-id')
+
+      expect(mockDb.select).toHaveBeenCalled()
+      expect(mockDb.from).toHaveBeenCalled()
+      expect(mockDb.where).toHaveBeenCalled()
+      expect(mockDb.limit).toHaveBeenCalledWith(1)
+
+      expect(credential).toEqual(mockCredential)
+    })
+
+    it('should return undefined when credential is not found', async () => {
+      mockDb.limit.mockReturnValueOnce([])
+
+      const { getCredential } = await import('./utils')
+
+      const credential = await getCredential('request-id', 'nonexistent-id', 'test-user-id')
+
+      expect(credential).toBeUndefined()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+  })
+
+  describe('refreshTokenIfNeeded', () => {
+    it('should return valid token without refresh if not expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'valid-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour in the future
+        providerId: 'google',
+      }
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+
+      expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
+      expect(result).toEqual({ accessToken: 'valid-token', refreshed: false })
+      expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Access token is valid'))
+    })
+
+    it('should refresh token when expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+      }
+
+      mockRefreshOAuthToken.mockResolvedValueOnce({
+        accessToken: 'new-token',
+        expiresIn: 3600,
+        refreshToken: 'new-refresh-token',
+      })
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+
+      expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
+      expect(mockDb.update).toHaveBeenCalled()
+      expect(mockDb.set).toHaveBeenCalled()
+      expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
+      expect(mockLogger.info).toHaveBeenCalledWith(
+        expect.stringContaining('Successfully refreshed')
+      )
+    })
+
+    it('should handle refresh token error', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+      }
+
+      mockRefreshOAuthToken.mockRejectedValueOnce(new Error('Refresh failed'))
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      await expect(
+        refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+      ).rejects.toThrow()
+
+      expect(mockLogger.error).toHaveBeenCalled()
+    })
+
+    it('should not attempt refresh if no refresh token', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'token',
+        refreshToken: null,
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+      }
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+
+      expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
+      expect(result).toEqual({ accessToken: 'token', refreshed: false })
+    })
+  })
+
+  describe('refreshAccessTokenIfNeeded', () => {
+    it('should return valid access token without refresh if not expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'valid-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour in the future
+        providerId: 'google',
+        userId: 'test-user-id',
+      }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      const { refreshAccessTokenIfNeeded } = await import('./utils')
+
+      const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
+
+      expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
+      expect(token).toBe('valid-token')
+    })
+
+    it('should refresh token when expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+        userId: 'test-user-id',
+      }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      mockRefreshOAuthToken.mockResolvedValueOnce({
+        accessToken: 'new-token',
+        expiresIn: 3600,
+        refreshToken: 'new-refresh-token',
+      })
+
+      const { refreshAccessTokenIfNeeded } = await import('./utils')
+
+      const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
+
+      expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
+      expect(mockDb.update).toHaveBeenCalled()
+      expect(mockDb.set).toHaveBeenCalled()
+      expect(token).toBe('new-token')
+    })
+
+    it('should return null if credential not found', async () => {
+      mockDb.limit.mockReturnValueOnce([])
+
+      const { refreshAccessTokenIfNeeded } = await import('./utils')
+
+      const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id')
+
+      expect(token).toBeNull()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should return null if refresh fails', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+        userId: 'test-user-id',
+      }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      mockRefreshOAuthToken.mockResolvedValueOnce(null)
+
+      const { refreshAccessTokenIfNeeded } = await import('./utils')
+
+      const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
+
+      expect(token).toBeNull()
+      expect(mockLogger.error).toHaveBeenCalled()
+    })
+  })
+})
diff --git a/sim/app/api/auth/oauth/utils.ts b/sim/app/api/auth/oauth/utils.ts
index 5193098ce02..4eb50c063f4 100644
--- a/sim/app/api/auth/oauth/utils.ts
+++ b/sim/app/api/auth/oauth/utils.ts
@@ -1,11 +1,66 @@
 import { and, eq } from 'drizzle-orm'
+import { getSession } from '@/lib/auth'
 import { createLogger } from '@/lib/logs/console-logger'
 import { refreshOAuthToken } from '@/lib/oauth'
 import { db } from '@/db'
-import { account } from '@/db/schema'
+import { account, workflow } from '@/db/schema'
 
 const logger = createLogger('OAuthUtils')
 
+/**
+ * Get the user ID based on either a session or a workflow ID
+ */
+export async function getUserId(
+  requestId: string,
+  workflowId?: string
+): Promise<string | undefined> {
+  // If workflowId is provided, this is a server-side request
+  if (workflowId) {
+    // Get the workflow to verify the user ID
+    const workflows = await db
+      .select({ userId: workflow.userId })
+      .from(workflow)
+      .where(eq(workflow.id, workflowId))
+      .limit(1)
+
+    if (!workflows.length) {
+      logger.warn(`[${requestId}] Workflow not found`)
+      return undefined
+    }
+
+    return workflows[0].userId
+  } else {
+    // This is a client-side request, use the session
+    const session = await getSession()
+
+    // Check if the user is authenticated
+    if (!session?.user?.id) {
+      logger.warn(`[${requestId}] Unauthenticated request rejected`)
+      return undefined
+    }
+
+    return session.user.id
+  }
+}
+
+/**
+ * Get a credential by ID and verify it belongs to the user
+ */
+export async function getCredential(requestId: string, credentialId: string, userId: string) {
+  const credentials = await db
+    .select()
+    .from(account)
+    .where(and(eq(account.id, credentialId), eq(account.userId, userId)))
+    .limit(1)
+
+  if (!credentials.length) {
+    logger.warn(`[${requestId}] Credential not found`)
+    return undefined
+  }
+
+  return credentials[0]
+}
+
 export async function getOAuthToken(userId: string, providerId: string): Promise<string | null> {
   const connections = await db
     .select({
@@ -94,28 +149,21 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
  * Refreshes an OAuth token if needed based on credential information
  * @param credentialId The ID of the credential to check and potentially refresh
  * @param userId The user ID who owns the credential (for security verification)
- * @param requestId Optional request ID for log correlation
+ * @param requestId Request ID for log correlation
  * @returns The valid access token or null if refresh fails
  */
 export async function refreshAccessTokenIfNeeded(
   credentialId: string,
   userId: string,
-  requestId?: string
+  requestId: string
 ): Promise<string | null> {
-  // Get the credential from the database
-  const credentials = await db
-    .select()
-    .from(account)
-    .where(and(eq(account.id, credentialId), eq(account.userId, userId)))
-    .limit(1)
+  // Get the credential directly using the getCredential helper
+  const credential = await getCredential(requestId, credentialId, userId)
 
-  if (!credentials.length) {
-    logger.warn(`[${requestId || ''}] Credential not found: ${credentialId}`)
+  if (!credential) {
     return null
   }
 
-  const credential = credentials[0]
-
   // Check if we need to refresh the token
   const expiresAt = credential.accessTokenExpiresAt
   const now = new Date()
@@ -124,22 +172,17 @@ export async function refreshAccessTokenIfNeeded(
   let accessToken = credential.accessToken
 
   if (needsRefresh && credential.refreshToken) {
-    logger.info(
-      `[${requestId || ''}] Token expired, attempting to refresh for credential: ${credentialId}`
-    )
+    logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
     try {
       const refreshedToken = await refreshOAuthToken(credential.providerId, credential.refreshToken)
 
       if (!refreshedToken) {
-        logger.error(
-          `[${requestId || ''}] Failed to refresh token for credential: ${credentialId}`,
-          {
-            credentialId,
-            providerId: credential.providerId,
-            userId: credential.userId,
-            hasRefreshToken: !!credential.refreshToken,
-          }
-        )
+        logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, {
+          credentialId,
+          providerId: credential.providerId,
+          userId: credential.userId,
+          hasRefreshToken: !!credential.refreshToken,
+        })
         return null
       }
 
@@ -152,19 +195,17 @@ export async function refreshAccessTokenIfNeeded(
 
       // If we received a new refresh token, update it
       if (refreshedToken.refreshToken && refreshedToken.refreshToken !== credential.refreshToken) {
-        logger.info(`[${requestId || ''}] Updating refresh token for credential: ${credentialId}`)
+        logger.info(`[${requestId}] Updating refresh token for credential`)
         updateData.refreshToken = refreshedToken.refreshToken
       }
 
       // Update the token in the database
       await db.update(account).set(updateData).where(eq(account.id, credentialId))
 
-      logger.info(
-        `[${requestId || ''}] Successfully refreshed access token for credential: ${credentialId}`
-      )
+      logger.info(`[${requestId}] Successfully refreshed access token for credential`)
       return refreshedToken.accessToken
     } catch (error) {
-      logger.error(`[${requestId || ''}] Error refreshing token for credential: ${credentialId}`, {
+      logger.error(`[${requestId}] Error refreshing token for credential`, {
         error: error instanceof Error ? error.message : String(error),
         stack: error instanceof Error ? error.stack : undefined,
         providerId: credential.providerId,
@@ -174,9 +215,62 @@ export async function refreshAccessTokenIfNeeded(
       return null
     }
   } else if (!accessToken) {
-    logger.error(`[${requestId || ''}] Missing access token for credential: ${credential.id}`)
+    logger.error(`[${requestId}] Missing access token for credential`)
     return null
   }
 
+  logger.info(`[${requestId}] Access token is valid for credential`)
   return accessToken
 }
+
+/**
+ * Enhanced version that returns additional information about the refresh operation
+ */
+export async function refreshTokenIfNeeded(
+  requestId: string,
+  credential: any,
+  credentialId: string
+): Promise<{ accessToken: string; refreshed: boolean }> {
+  // Check if we need to refresh the token
+  const expiresAt = credential.accessTokenExpiresAt
+  const now = new Date()
+  const needsRefresh = !expiresAt || expiresAt <= now
+
+  // If token is still valid, return it directly
+  if (!needsRefresh || !credential.refreshToken) {
+    logger.info(`[${requestId}] Access token is valid`)
+    return { accessToken: credential.accessToken, refreshed: false }
+  }
+
+  try {
+    const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken)
+
+    if (!refreshResult) {
+      logger.error(`[${requestId}] Failed to refresh token for credential`)
+      throw new Error('Failed to refresh token')
+    }
+
+    const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult
+
+    // Prepare update data
+    const updateData: any = {
+      accessToken: refreshedToken,
+      accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
+      updatedAt: new Date(),
+    }
+
+    // If we received a new refresh token, update it
+    if (newRefreshToken && newRefreshToken !== credential.refreshToken) {
+      logger.info(`[${requestId}] Updating refresh token`)
+      updateData.refreshToken = newRefreshToken
+    }
+
+    await db.update(account).set(updateData).where(eq(account.id, credentialId))
+
+    logger.info(`[${requestId}] Successfully refreshed access token`)
+    return { accessToken: refreshedToken, refreshed: true }
+  } catch (error) {
+    logger.error(`[${requestId}] Error refreshing token`, error)
+    throw error
+  }
+}
diff --git a/sim/app/api/auth/session/route.ts b/sim/app/api/auth/session/route.ts
deleted file mode 100644
index 1a374a793cd..00000000000
--- a/sim/app/api/auth/session/route.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server'
-import { eq } from 'drizzle-orm'
-import { db } from '@/db'
-import { session, user } from '@/db/schema'
-
-export async function GET(request: NextRequest) {
-  try {
-    const token = request.nextUrl.searchParams.get('token')
-
-    if (!token) {
-      return NextResponse.json({ error: 'Token is required' }, { status: 400 })
-    }
-
-    // Get session by token
-    const sessionRecord = await db
-      .select()
-      .from(session)
-      .where(eq(session.id, token))
-      .limit(1)
-      .then((rows) => rows[0])
-
-    if (!sessionRecord) {
-      return NextResponse.json({ error: 'Invalid session' }, { status: 401 })
-    }
-
-    // Get user from session
-    const userRecord = await db
-      .select()
-      .from(user)
-      .where(eq(user.id, sessionRecord.userId))
-      .limit(1)
-      .then((rows) => rows[0])
-
-    if (!userRecord) {
-      return NextResponse.json({ error: 'User not found' }, { status: 404 })
-    }
-
-    // Return minimal user info (only what's needed)
-    return NextResponse.json({
-      user: {
-        id: userRecord.id,
-        email: userRecord.email,
-        name: userRecord.name,
-      },
-    })
-  } catch (error) {
-    console.error('Session API error:', error)
-    return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
-  }
-}
diff --git a/sim/app/api/providers/route.ts b/sim/app/api/providers/route.ts
index 40456d0dbb4..bf1de4b9c6d 100644
--- a/sim/app/api/providers/route.ts
+++ b/sim/app/api/providers/route.ts
@@ -23,14 +23,12 @@ export async function POST(request: NextRequest) {
       maxTokens,
       apiKey,
       responseFormat,
+      workflowId,
     } = body
 
-    logger.info(`Provider request received for ${provider} model: ${model}`)
-
     let finalApiKey: string
     try {
       finalApiKey = getApiKey(provider, model, apiKey)
-      logger.info(`API key obtained for ${provider} ${model}`)
     } catch (error) {
       logger.error('Failed to get API key:', error)
       return NextResponse.json(
@@ -49,6 +47,7 @@ export async function POST(request: NextRequest) {
       maxTokens,
       apiKey: finalApiKey,
       responseFormat,
+      workflowId,
     })
 
     return NextResponse.json(response)
diff --git a/sim/blocks/blocks/x.ts b/sim/blocks/blocks/x.ts
index 97e8fafb13f..252a242453e 100644
--- a/sim/blocks/blocks/x.ts
+++ b/sim/blocks/blocks/x.ts
@@ -156,7 +156,7 @@ export const XBlock: BlockConfig<XResponse> = {
 
         // Convert string values to appropriate types
         const parsedParams: Record<string, any> = {
-          accessToken: credential,
+          credential: credential,
         }
 
         // Add other params
diff --git a/sim/executor/handlers/agent/agent-handler.ts b/sim/executor/handlers/agent/agent-handler.ts
index 78b594a3881..661474ef240 100644
--- a/sim/executor/handlers/agent/agent-handler.ts
+++ b/sim/executor/handlers/agent/agent-handler.ts
@@ -150,6 +150,7 @@ export class AgentBlockHandler implements BlockHandler {
       maxTokens: inputs.maxTokens,
       apiKey: inputs.apiKey,
       responseFormat,
+      workflowId: context.workflowId,
     }
 
     logger.info(`Provider request prepared`, {
@@ -158,6 +159,7 @@ export class AgentBlockHandler implements BlockHandler {
       hasContext: !!providerRequest.context,
       hasTools: !!providerRequest.tools,
       hasApiKey: !!providerRequest.apiKey,
+      workflowId: providerRequest.workflowId,
     })
 
     // Get the app URL from environment variable or use default
diff --git a/sim/lib/auth.ts b/sim/lib/auth.ts
index 3a0c76471ea..4501fe596e0 100644
--- a/sim/lib/auth.ts
+++ b/sim/lib/auth.ts
@@ -367,6 +367,7 @@ export const auth = betterAuth({
           pkce: true,
           responseType: 'code',
           prompt: 'consent',
+          authentication: 'basic',
           redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/x`,
           getUserInfo: async (tokens) => {
             try {
diff --git a/sim/lib/oauth.ts b/sim/lib/oauth.ts
index 9ccc9f19d05..4dc633ea8c8 100644
--- a/sim/lib/oauth.ts
+++ b/sim/lib/oauth.ts
@@ -353,6 +353,7 @@ export async function refreshOAuthToken(
         tokenEndpoint = 'https://api.x.com/2/oauth2/token'
         clientId = process.env.X_CLIENT_ID
         clientSecret = process.env.X_CLIENT_SECRET
+        useBasicAuth = true
         break
       case 'confluence':
         tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
@@ -407,6 +408,15 @@ export async function refreshOAuthToken(
       } else {
         throw new Error('Both client ID and client secret are required for Airtable OAuth')
       }
+    } else if (provider === 'x') {
+      // Handle X differently
+      // Confidential client - use Basic Auth
+      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
+      delete bodyParams.client_id
     } else {
       // For other providers, use the general approach
       if (useBasicAuth) {
@@ -429,15 +439,20 @@ export async function refreshOAuthToken(
 
     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,
-        headers: JSON.stringify(headers, null, 2).replace(
-          /"Authorization":"[^"]*"/,
-          '"Authorization":"[REDACTED]"'
-        ),
-        bodyParams: JSON.stringify(bodyParams),
       })
       throw new Error(`Failed to refresh token: ${response.status} ${errorText}`)
     }
diff --git a/sim/providers/anthropic/index.ts b/sim/providers/anthropic/index.ts
index 46d1e1a4fc0..f84b0a8d454 100644
--- a/sim/providers/anthropic/index.ts
+++ b/sim/providers/anthropic/index.ts
@@ -254,7 +254,11 @@ ${fieldDescriptions}
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+              }
               const result = await executeTool(toolName, mergedArgs)
               const toolCallEndTime = Date.now()
               const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/providers/cerebras/index.ts b/sim/providers/cerebras/index.ts
index f890c44d2d9..bf95c4d50cd 100644
--- a/sim/providers/cerebras/index.ts
+++ b/sim/providers/cerebras/index.ts
@@ -170,7 +170,11 @@ export const cerebrasProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+              }
               const result = await executeTool(toolName, mergedArgs)
               const toolCallEndTime = Date.now()
               const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/providers/deepseek/index.ts b/sim/providers/deepseek/index.ts
index 951e5fb4177..78d0bb947e3 100644
--- a/sim/providers/deepseek/index.ts
+++ b/sim/providers/deepseek/index.ts
@@ -144,7 +144,11 @@ export const deepseekProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+              }
               const result = await executeTool(toolName, mergedArgs)
               const toolCallEndTime = Date.now()
               const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/providers/google/index.ts b/sim/providers/google/index.ts
index e89dab69f0b..0352a1b6bfa 100644
--- a/sim/providers/google/index.ts
+++ b/sim/providers/google/index.ts
@@ -214,7 +214,11 @@ export const googleProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+              }
               const result = await executeTool(toolName, mergedArgs)
               const toolCallEndTime = Date.now()
               const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/providers/groq/index.ts b/sim/providers/groq/index.ts
index 22648cd3a43..b64ff28136f 100644
--- a/sim/providers/groq/index.ts
+++ b/sim/providers/groq/index.ts
@@ -145,7 +145,11 @@ export const groqProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+              }
               const result = await executeTool(toolName, mergedArgs)
               const toolCallEndTime = Date.now()
               const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/providers/ollama/index.ts b/sim/providers/ollama/index.ts
index 2445599f4c3..600eaaf9301 100644
--- a/sim/providers/ollama/index.ts
+++ b/sim/providers/ollama/index.ts
@@ -163,7 +163,11 @@ export const ollamaProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+              }
               const result = await executeTool(toolName, mergedArgs)
               const toolCallEndTime = Date.now()
               const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/providers/openai/index.ts b/sim/providers/openai/index.ts
index 359301707e5..c2bb0c7b971 100644
--- a/sim/providers/openai/index.ts
+++ b/sim/providers/openai/index.ts
@@ -160,7 +160,11 @@ export const openaiProvider: ProviderConfig = {
 
             // Execute the tool
             const toolCallStartTime = Date.now()
-            const mergedArgs = { ...tool.params, ...toolArgs }
+            const mergedArgs = {
+              ...tool.params,
+              ...toolArgs,
+              ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+            }
             const result = await executeTool(toolName, mergedArgs)
             const toolCallEndTime = Date.now()
             const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/providers/types.ts b/sim/providers/types.ts
index d465618873e..e04545a72f9 100644
--- a/sim/providers/types.ts
+++ b/sim/providers/types.ts
@@ -138,6 +138,7 @@ export interface ProviderRequest {
     strict?: boolean
   }
   local_execution?: boolean
+  workflowId?: string // Optional workflow ID for authentication context
 }
 
 // Map of provider IDs to their configurations
diff --git a/sim/providers/xai/index.ts b/sim/providers/xai/index.ts
index 2ae651b14c6..9eccb081ccd 100644
--- a/sim/providers/xai/index.ts
+++ b/sim/providers/xai/index.ts
@@ -143,7 +143,11 @@ export const xAIProvider: ProviderConfig = {
               if (!tool) continue
 
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
+              }
               const result = await executeTool(toolName, mergedArgs)
               const toolCallEndTime = Date.now()
               const toolCallDuration = toolCallEndTime - toolCallStartTime
diff --git a/sim/tools/index.ts b/sim/tools/index.ts
index 9df98dc106f..d53fe17e65d 100644
--- a/sim/tools/index.ts
+++ b/sim/tools/index.ts
@@ -332,9 +332,11 @@ export async function executeTool(
 
   try {
     const tool = getTool(toolId)
+    // Ensure context is preserved if it exists
+    const contextParams = { ...params }
 
     // Validate the tool and its parameters
-    validateToolRequest(toolId, tool, params)
+    validateToolRequest(toolId, tool, contextParams)
 
     // After validation, we know tool exists
     if (!tool) {
@@ -344,7 +346,7 @@ export async function executeTool(
     // For any tool with direct execution capability, try it first
     if (tool.directExecution) {
       try {
-        const directResult = await tool.directExecution(params)
+        const directResult = await tool.directExecution(contextParams)
         if (directResult) {
           // Add timing data to the result
           const endTime = new Date()
@@ -354,7 +356,11 @@ export async function executeTool(
           // Apply post-processing if available and not skipped
           if (tool.postProcess && directResult.success && !skipPostProcess) {
             try {
-              const postProcessResult = await tool.postProcess(directResult, params, executeTool)
+              const postProcessResult = await tool.postProcess(
+                directResult,
+                contextParams,
+                executeTool
+              )
               return {
                 ...postProcessResult,
                 timing: {
@@ -394,12 +400,12 @@ export async function executeTool(
 
     // For internal routes or when skipProxy is true, call the API directly
     if (tool.request.isInternalRoute || skipProxy) {
-      const result = await handleInternalRequest(toolId, tool, params)
+      const result = await handleInternalRequest(toolId, tool, contextParams)
 
       // Apply post-processing if available and not skipped
       if (tool.postProcess && result.success && !skipPostProcess) {
         try {
-          const postProcessResult = await tool.postProcess(result, params, executeTool)
+          const postProcessResult = await tool.postProcess(result, contextParams, executeTool)
 
           // Add timing data to the post-processed result
           const endTime = new Date()
@@ -446,12 +452,12 @@ export async function executeTool(
     }
 
     // For external APIs, use the proxy
-    const result = await handleProxyRequest(toolId, params)
+    const result = await handleProxyRequest(toolId, contextParams)
 
     // Apply post-processing if available and not skipped
     if (tool.postProcess && result.success && !skipPostProcess) {
       try {
-        const postProcessResult = await tool.postProcess(result, params, executeTool)
+        const postProcessResult = await tool.postProcess(result, contextParams, executeTool)
 
         // Add timing data to the post-processed result
         const endTime = new Date()
diff --git a/sim/tools/x/search.ts b/sim/tools/x/search.ts
index 20eeb76ec8c..b3a97a09c76 100644
--- a/sim/tools/x/search.ts
+++ b/sim/tools/x/search.ts
@@ -63,7 +63,11 @@ export const searchTool: ToolConfig<XSearchParams, XSearchResponse> = {
         'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
       })
 
-      if (params.maxResults) queryParams.append('max_results', params.maxResults.toString())
+      if (params.maxResults && params.maxResults < 10) {
+        queryParams.append('max_results', '10')
+      } else if (params.maxResults) {
+        queryParams.append('max_results', params.maxResults.toString())
+      }
       if (params.startTime) queryParams.append('start_time', params.startTime)
       if (params.endTime) queryParams.append('end_time', params.endTime)
       if (params.sortOrder) queryParams.append('sort_order', params.sortOrder)
@@ -127,12 +131,15 @@ export const searchTool: ToolConfig<XSearchParams, XSearchResponse> = {
   },
 
   transformError: (error) => {
+    // Log the full error object for debugging
+    console.error('X Search API Error:', JSON.stringify(error, null, 2))
+
     if (error.title === 'Unauthorized') {
       return 'Invalid or expired access token. Please reconnect your X account.'
     }
     if (error.title === 'Invalid Request') {
       return 'Invalid search query. Please check your search parameters.'
     }
-    return error.detail || 'An error occurred while searching X'
+    return error.detail || `An error occurred while searching X`
   },
 }


Reviews:
user: login: cubic-dev-ai[bot]
body: mrge found 12 issues. View them in [mrge.io](https://mrge.io/pr/simstudioai/sim/pull/251)
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-12T00:06:02Z

Review comments:
diff_hunk: @@ -332,9 +332,16 @@ export async function executeTool(
 
   try {
     const tool = getTool(toolId)
+    logger.info(`Executing tool ${toolId} with params: ${JSON.stringify(params)}`)
path: sim/tools/index.ts
user: login: cubic-dev-ai[bot]
body: Logging all tool parameters could expose sensitive information such as credentials or tokens
created_at: 2025-04-12T00:06:01Z
author_association: NONE
diff_hunk: @@ -332,9 +332,16 @@ export async function executeTool(
 
   try {
     const tool = getTool(toolId)
+    logger.info(`Executing tool ${toolId} with params: ${JSON.stringify(params)}`)
+
+    // Extract workflowId from context if available, so we can use it across all execution paths
+    const workflowId = params?._context?.workflowId
+
+    // Ensure context is preserved if it exists
+    const contextParams = { ...params, _context: { workflowId } }
path: sim/tools/index.ts
user: login: cubic-dev-ai[bot]
body: This overwrites any existing _context properties with just the workflowId, potentially losing other context data
created_at: 2025-04-12T00:06:01Z
author_association: NONE
diff_hunk: @@ -0,0 +1,337 @@
+/**
+ * Tests for OAuth token API routes
+ *
+ * @vitest-environment node
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createMockRequest } from '@/app/api/__test-utils__/utils'
+
+describe('OAuth Token API Routes', () => {
+  const mockGetUserId = vi.fn()
+  const mockGetCredential = vi.fn()
+  const mockRefreshTokenIfNeeded = vi.fn()
+
+  const mockLogger = {
+    info: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+    debug: vi.fn(),
+  }
+
+  const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
+  const mockRequestId = mockUUID.slice(0, 8)
+
+  beforeEach(() => {
+    vi.resetModules()
+
+    vi.stubGlobal('crypto', {
+      randomUUID: vi.fn().mockReturnValue(mockUUID),
+    })
+
+    vi.doMock('../utils', () => ({
+      getUserId: mockGetUserId,
+      getCredential: mockGetCredential,
+      refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
+    }))
+
+    vi.doMock('@/lib/logs/console-logger', () => ({
+      createLogger: vi.fn().mockReturnValue(mockLogger),
+    }))
+  })
+
+  afterEach(() => {
+    vi.clearAllMocks()
+  })
+
+  /**
+   * POST route tests
+   */
+  describe('POST handler', () => {
+    it('should return access token successfully', async () => {
+      mockGetUserId.mockResolvedValueOnce('test-user-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: 'test-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
+        providerId: 'google',
+      })
+      mockRefreshTokenIfNeeded.mockResolvedValueOnce({
+        accessToken: 'fresh-token',
+        refreshed: false,
+      })
+
+      // Create mock request
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+      })
+
+      // Import handler after setting up mocks
+      const { POST } = await import('./route')
+
+      // Call handler
+      const response = await POST(req)
+      const data = await response.json()
+
+      // Verify request was handled correctly
+      expect(response.status).toBe(200)
+      expect(data).toHaveProperty('accessToken', 'fresh-token')
+
+      // Verify mocks were called correctly
+      expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId, undefined)
+      expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
+      expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
+    })
+
+    it('should handle workflowId for server-side authentication', async () => {
+      mockGetUserId.mockResolvedValueOnce('workflow-owner-id')
+      mockGetCredential.mockResolvedValueOnce({
+        id: 'credential-id',
+        accessToken: 'test-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
+        providerId: 'google',
+      })
+      mockRefreshTokenIfNeeded.mockResolvedValueOnce({
+        accessToken: 'fresh-token',
+        refreshed: false,
+      })
+
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+        workflowId: 'workflow-id',
+      })
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(200)
+      expect(data).toHaveProperty('accessToken', 'fresh-token')
+
+      expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId, 'workflow-id')
+      expect(mockGetCredential).toHaveBeenCalledWith(
+        mockRequestId,
+        'credential-id',
+        'workflow-owner-id'
+      )
+    })
+
+    it('should handle missing credentialId', async () => {
+      const req = createMockRequest('POST', {})
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(400)
+      expect(data).toHaveProperty('error', 'Credential ID is required')
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should handle authentication failure', async () => {
+      mockGetUserId.mockResolvedValueOnce(undefined)
+
+      const req = createMockRequest('POST', {
+        credentialId: 'credential-id',
+      })
+
+      const { POST } = await import('./route')
+
+      const response = await POST(req)
+      const data = await response.json()
+
+      expect(response.status).toBe(401)
path: sim/app/api/auth/oauth/token/route.test.ts
user: login: cubic-dev-ai[bot]
body: Inconsistent error status codes for token refresh failures between handlers
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -170,7 +170,11 @@ export const cerebrasProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
path: sim/providers/cerebras/index.ts
user: login: cubic-dev-ai[bot]
body: Missing inline documentation explaining the purpose of adding workflowId to _context
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -63,7 +63,11 @@ export const searchTool: ToolConfig<XSearchParams, XSearchResponse> = {
         'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
       })
 
-      if (params.maxResults) queryParams.append('max_results', params.maxResults.toString())
+      if (params.maxResults && params.maxResults < 10) {
path: sim/tools/x/search.ts
user: login: cubic-dev-ai[bot]
body: Missing comment explaining why the tool enforces a minimum of 10 results when the user requests fewer, which conflicts with the parameter documentation
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -127,12 +131,15 @@ export const searchTool: ToolConfig<XSearchParams, XSearchResponse> = {
   },
 
   transformError: (error) => {
+    // Log the full error object for debugging
+    console.error('X Search API Error:', JSON.stringify(error, null, 2))
+
     if (error.title === 'Unauthorized') {
       return 'Invalid or expired access token. Please reconnect your X account.'
     }
     if (error.title === 'Invalid Request') {
       return 'Invalid search query. Please check your search parameters.'
     }
-    return error.detail || 'An error occurred while searching X'
+    return error.detail || `An error occurred while searching X: ${JSON.stringify(error)}`
path: sim/tools/x/search.ts
user: login: cubic-dev-ai[bot]
body: Including full error details in user-facing error messages could expose sensitive information
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -156,7 +156,7 @@ export const XBlock: BlockConfig<XResponse> = {
 
         // Convert string values to appropriate types
         const parsedParams: Record<string, any> = {
-          accessToken: credential,
+          credential: credential,
path: sim/blocks/blocks/x.ts
user: login: cubic-dev-ai[bot]
body: Parameter name mismatch between block config and tool implementation. The X tools expect an 'accessToken' parameter, but this change renames it to 'credential'.
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -27,97 +23,29 @@ export async function POST(request: NextRequest) {
     }
 
     // Determine the user ID based on the context
-    let userId: string | undefined
-
-    // If workflowId is provided, this is a server-side request
-    if (workflowId) {
-      // Get the workflow to verify the user ID
-      const workflows = await db
-        .select({ userId: workflow.userId })
-        .from(workflow)
-        .where(eq(workflow.id, workflowId))
-        .limit(1)
-
-      if (!workflows.length) {
-        logger.warn(`[${requestId}] Workflow not found`)
-        return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
-      }
-
-      userId = workflows[0].userId
-    } else {
-      // This is a client-side request, use the session
-      const session = await getSession()
-
-      // Check if the user is authenticated
-      if (!session?.user?.id) {
-        logger.warn(`[${requestId}] Unauthenticated token request rejected`)
-        return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
-      }
-
-      userId = session.user.id
+    const userId = await getUserId(requestId, workflowId)
+
+    if (!userId) {
+      return NextResponse.json(
+        { error: workflowId ? 'Workflow not found' : 'User not authenticated' },
+        { status: workflowId ? 404 : 401 }
+      )
     }
 
     // Get the credential from the database
-    const credentials = await db
-      .select()
-      .from(account)
-      .where(and(eq(account.id, credentialId), eq(account.userId, userId)))
-      .limit(1)
-
-    if (!credentials.length) {
-      logger.warn(`[${requestId}] Credential not found`)
+    const credential = await getCredential(requestId, credentialId, userId)
+
+    if (!credential) {
       return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
     }
 
-    const credential = credentials[0]
-
-    // Check if we need to refresh the token
-    const expiresAt = credential.accessTokenExpiresAt
-    const now = new Date()
-    const needsRefresh = !expiresAt || expiresAt <= now
-
-    if (needsRefresh && credential.refreshToken) {
-      try {
-        const refreshResult = await refreshOAuthToken(
-          credential.providerId,
-          credential.refreshToken
-        )
-
-        if (!refreshResult) {
-          throw new Error('Failed to refresh token')
-        }
-
-        const {
-          accessToken: refreshedToken,
-          expiresIn,
-          refreshToken: newRefreshToken,
-        } = refreshResult
-
-        // Prepare update data
-        const updateData: any = {
-          accessToken: refreshedToken,
-          accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
-          updatedAt: new Date(),
-        }
-
-        // If we received a new refresh token, update it
-        if (newRefreshToken && newRefreshToken !== credential.refreshToken) {
-          logger.info(`[${requestId}] Updating refresh token for credential: ${credentialId}`)
-          updateData.refreshToken = newRefreshToken
-        }
-
-        await db.update(account).set(updateData).where(eq(account.id, credentialId))
-
-        logger.info(`[${requestId}] Successfully refreshed access token`)
-        return NextResponse.json({ accessToken: refreshedToken }, { status: 200 })
-      } catch (error) {
-        logger.error(`[${requestId}] Error refreshing token`, error)
-        return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 500 })
-      }
+    try {
+      // Refresh the token if needed
+      const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
+      return NextResponse.json({ accessToken }, { status: 200 })
+    } catch (error) {
+      return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 500 })
path: sim/app/api/auth/oauth/token/route.ts
user: login: cubic-dev-ai[bot]
body: Inconsistent error status codes between handlers. POST returns 500 for token refresh failures while GET returns 401.
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,292 @@
+/**
+ * Tests for OAuth utility functions
+ *
+ * @vitest-environment node
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+describe('OAuth Utils', () => {
+  const mockSession = { user: { id: 'test-user-id' } }
+  const mockDb = {
+    select: vi.fn().mockReturnThis(),
+    from: vi.fn().mockReturnThis(),
+    where: vi.fn().mockReturnThis(),
+    limit: vi.fn().mockReturnValue([]),
+    update: vi.fn().mockReturnThis(),
+    set: vi.fn().mockReturnThis(),
+    orderBy: vi.fn().mockReturnThis(),
+  }
+  const mockRefreshOAuthToken = vi.fn()
+  const mockLogger = {
+    info: vi.fn(),
+    warn: vi.fn(),
+    error: vi.fn(),
+    debug: vi.fn(),
+  }
+
+  beforeEach(() => {
+    vi.resetModules()
+
+    vi.doMock('@/lib/auth', () => ({
+      getSession: vi.fn().mockResolvedValue(mockSession),
+    }))
+
+    vi.doMock('@/db', () => ({
+      db: mockDb,
+    }))
+
+    vi.doMock('@/lib/oauth', () => ({
+      refreshOAuthToken: mockRefreshOAuthToken,
+    }))
+
+    vi.doMock('@/lib/logs/console-logger', () => ({
+      createLogger: vi.fn().mockReturnValue(mockLogger),
+    }))
+  })
+
+  afterEach(() => {
+    vi.clearAllMocks()
+  })
+
+  describe('getUserId', () => {
+    it('should get user ID from session when no workflowId is provided', async () => {
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id')
+
+      expect(userId).toBe('test-user-id')
+    })
+
+    it('should get user ID from workflow when workflowId is provided', async () => {
+      mockDb.limit.mockReturnValueOnce([{ userId: 'workflow-owner-id' }])
+
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id', 'workflow-id')
+
+      expect(mockDb.select).toHaveBeenCalled()
+      expect(mockDb.from).toHaveBeenCalled()
+      expect(mockDb.where).toHaveBeenCalled()
+      expect(mockDb.limit).toHaveBeenCalledWith(1)
+      expect(userId).toBe('workflow-owner-id')
+    })
+
+    it('should return undefined if no session is found', async () => {
+      vi.doMock('@/lib/auth', () => ({
+        getSession: vi.fn().mockResolvedValue(null),
+      }))
+
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id')
+
+      expect(userId).toBeUndefined()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should return undefined if workflow is not found', async () => {
+      mockDb.limit.mockReturnValueOnce([])
+
+      const { getUserId } = await import('./utils')
+
+      const userId = await getUserId('request-id', 'nonexistent-workflow-id')
+
+      expect(userId).toBeUndefined()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+  })
+
+  describe('getCredential', () => {
+    it('should return credential when found', async () => {
+      const mockCredential = { id: 'credential-id', userId: 'test-user-id' }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      const { getCredential } = await import('./utils')
+
+      const credential = await getCredential('request-id', 'credential-id', 'test-user-id')
+
+      expect(mockDb.select).toHaveBeenCalled()
+      expect(mockDb.from).toHaveBeenCalled()
+      expect(mockDb.where).toHaveBeenCalled()
+      expect(mockDb.limit).toHaveBeenCalledWith(1)
+
+      expect(credential).toEqual(mockCredential)
+    })
+
+    it('should return undefined when credential is not found', async () => {
+      mockDb.limit.mockReturnValueOnce([])
+
+      const { getCredential } = await import('./utils')
+
+      const credential = await getCredential('request-id', 'nonexistent-id', 'test-user-id')
+
+      expect(credential).toBeUndefined()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+  })
+
+  describe('refreshTokenIfNeeded', () => {
+    it('should return valid token without refresh if not expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'valid-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour in the future
+        providerId: 'google',
+      }
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+
+      expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
+      expect(result).toEqual({ accessToken: 'valid-token', refreshed: false })
+      expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Access token is valid'))
+    })
+
+    it('should refresh token when expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+      }
+
+      mockRefreshOAuthToken.mockResolvedValueOnce({
+        accessToken: 'new-token',
+        expiresIn: 3600,
+        refreshToken: 'new-refresh-token',
+      })
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+
+      expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
+      expect(mockDb.update).toHaveBeenCalled()
+      expect(mockDb.set).toHaveBeenCalled()
+      expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
+      expect(mockLogger.info).toHaveBeenCalledWith(
+        expect.stringContaining('Successfully refreshed')
+      )
+    })
+
+    it('should handle refresh token error', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+      }
+
+      mockRefreshOAuthToken.mockRejectedValueOnce(new Error('Refresh failed'))
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      await expect(
+        refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+      ).rejects.toThrow()
+
+      expect(mockLogger.error).toHaveBeenCalled()
+    })
+
+    it('should not attempt refresh if no refresh token', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'token',
+        refreshToken: null,
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+      }
+
+      const { refreshTokenIfNeeded } = await import('./utils')
+
+      const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
+
+      expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
+      expect(result).toEqual({ accessToken: 'token', refreshed: false })
+    })
+  })
+
+  describe('refreshAccessTokenIfNeeded', () => {
+    it('should return valid access token without refresh if not expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'valid-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour in the future
+        providerId: 'google',
+        userId: 'test-user-id',
+      }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      const { refreshAccessTokenIfNeeded } = await import('./utils')
+
+      const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
+
+      expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
+      expect(token).toBe('valid-token')
+    })
+
+    it('should refresh token when expired', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+        userId: 'test-user-id',
+      }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      mockRefreshOAuthToken.mockResolvedValueOnce({
+        accessToken: 'new-token',
+        expiresIn: 3600,
+        refreshToken: 'new-refresh-token',
+      })
+
+      const { refreshAccessTokenIfNeeded } = await import('./utils')
+
+      const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
+
+      expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
+      expect(mockDb.update).toHaveBeenCalled()
+      expect(mockDb.set).toHaveBeenCalled()
+      expect(token).toBe('new-token')
+    })
+
+    it('should return null if credential not found', async () => {
+      mockDb.limit.mockReturnValueOnce([])
+
+      const { refreshAccessTokenIfNeeded } = await import('./utils')
+
+      const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id')
+
+      expect(token).toBeNull()
+      expect(mockLogger.warn).toHaveBeenCalled()
+    })
+
+    it('should return null if refresh fails', async () => {
+      const mockCredential = {
+        id: 'credential-id',
+        accessToken: 'expired-token',
+        refreshToken: 'refresh-token',
+        accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // 1 hour in the past
+        providerId: 'google',
+        userId: 'test-user-id',
+      }
+      mockDb.limit.mockReturnValueOnce([mockCredential])
+
+      mockRefreshOAuthToken.mockResolvedValueOnce(null)
path: sim/app/api/auth/oauth/utils.test.ts
user: login: cubic-dev-ai[bot]
body: Inconsistent test scenario - in the implementation, refreshAccessTokenIfNeeded returns null when refreshOAuthToken returns null, but the test explicitly returns null from the mock instead of throwing an error like in refreshTokenIfNeeded
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -145,7 +145,11 @@ export const groqProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
path: sim/providers/groq/index.ts
user: login: cubic-dev-ai[bot]
body: Potential overwrite of existing _context values that may be present in tool.params or toolArgs
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -144,7 +144,11 @@ export const deepseekProvider: ProviderConfig = {
 
               // Execute the tool
               const toolCallStartTime = Date.now()
-              const mergedArgs = { ...tool.params, ...toolArgs }
+              const mergedArgs = {
+                ...tool.params,
+                ...toolArgs,
+                ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}),
path: sim/providers/deepseek/index.ts
user: login: cubic-dev-ai[bot]
body: Inconsistent context handling. The tools/index.ts already handles extracting workflowId from params._context, but this implementation creates a nested context object { _context: { workflowId } } which could lead to _context.workflowId being undefined while _context._context.workflowId holds the actual ID.
created_at: 2025-04-12T00:06:02Z
author_association: NONE
diff_hunk: @@ -174,9 +215,61 @@ export async function refreshAccessTokenIfNeeded(
       return null
     }
   } else if (!accessToken) {
-    logger.error(`[${requestId || ''}] Missing access token for credential: ${credential.id}`)
+    logger.error(`[${requestId}] Missing access token for credential`)
     return null
   }
 
+  logger.info(`[${requestId}] Access token is valid for credential`)
   return accessToken
 }
+
+/**
+ * Enhanced version that returns additional information about the refresh operation
+ */
+export async function refreshTokenIfNeeded(
path: sim/app/api/auth/oauth/utils.ts
user: login: cubic-dev-ai[bot]
body: Function uses 'any' type for credential parameter, which bypasses TypeScript's type safety features
created_at: 2025-04-12T00:06:02Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-04-12T00:03:40Z
body: [vc]: #wiWYiKGMMGbJ74sa86PS+Krnz6f+zPa1Mh8ZHYsF664=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzaW0iLCJyb290RGlyZWN0b3J5Ijoic2ltIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InNpbS1naXQtZml4LW9hdXRoLXNpbS1zdHVkaW8udmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2ltLXN0dWRpby9zaW0vMjZRQUpxUjdGVlM0YlkyYVBYdFJrWmdmOHQ3ciIsInByZXZpZXdVcmwiOiJzaW0tZ2l0LWZpeC1vYXV0aC1zaW0tc3R1ZGlvLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJkb2NzIiwicm9vdERpcmVjdG9yeSI6ImRvY3MiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZG9jcy1naXQtZml4LW9hdXRoLXNpbS1zdHVkaW8udmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2ltLXN0dWRpby9kb2NzL0YyNFM0ZFBIS3luM05WZ2tta3p0Q2FCc0JNZUIiLCJwcmV2aWV3VXJsIjoiZG9jcy1naXQtZml4LW9hdXRoLXNpbS1zdHVkaW8udmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **docs** | ✅ Ready ([Inspect](https://vercel.com/sim-studio/docs/F24S4dPHKyn3NVgkmkztCaBsBMeB)) | [Visit Preview](https://docs-git-fix-oauth-sim-studio.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/docs-git-fix-oauth-sim-studio.vercel.app?via=pr-comment-feedback-link) | Apr 12, 2025 0:56am |
| **sim** | ✅ Ready ([Inspect](https://vercel.com/sim-studio/sim/26QAJqR7FVS4bY2aPXtRkZgf8t7r)) | [Visit Preview](https://sim-git-fix-oauth-sim-studio.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/sim-git-fix-oauth-sim-studio.vercel.app?via=pr-comment-feedback-link) | Apr 12, 2025 0:56am |



author_association: NONE
user: login: cubic-dev-ai[bot]
created_at: 2025-04-12T00:03:41Z
body: Review this PR on [mrge.io](https://mrge.io/pr/simstudioai/sim/pull/251)
author_association: NONE