Skip to content

Commit 3371540

Browse files
committed
Oauth link
1 parent c6ac0b4 commit 3371540

File tree

1 file changed

+131
-14
lines changed
  • apps/sim/lib/copilot/orchestrator/tool-executor

1 file changed

+131
-14
lines changed

apps/sim/lib/copilot/orchestrator/tool-executor/index.ts

Lines changed: 131 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
2-
import { mcpServers } from '@sim/db/schema'
2+
import { mcpServers, pendingCredentialDraft } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq, isNull } from 'drizzle-orm'
4+
import { and, eq, isNull, lt } from 'drizzle-orm'
55
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
66
import type {
77
ExecutionContext,
@@ -12,6 +12,7 @@ import { routeExecution } from '@/lib/copilot/tools/server/router'
1212
import { env } from '@/lib/core/config/env'
1313
import { getBaseUrl } from '@/lib/core/utils/urls'
1414
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
15+
import { getAllOAuthServices } from '@/lib/oauth/utils'
1516
import { validateMcpDomain } from '@/lib/mcp/domain-check'
1617
import { mcpService } from '@/lib/mcp/service'
1718
import { generateMcpServerId } from '@/lib/mcp/utils'
@@ -693,6 +694,112 @@ const SERVER_TOOLS = new Set<string>([
693694
'get_execution_summary',
694695
])
695696

697+
/**
698+
* Resolves a human-friendly provider name to a providerId and generates the
699+
* actual OAuth authorization URL via Better Auth's server-side API.
700+
*
701+
* Steps: resolve provider → create credential draft → look up user session →
702+
* call auth.api.oAuth2LinkAccount → return the real authorization URL.
703+
*/
704+
async function generateOAuthLink(
705+
userId: string,
706+
workspaceId: string | undefined,
707+
workflowId: string | undefined,
708+
providerName: string,
709+
baseUrl: string
710+
): Promise<{ url: string; providerId: string; serviceName: string }> {
711+
if (!workspaceId) {
712+
throw new Error('workspaceId is required to generate an OAuth link')
713+
}
714+
715+
const allServices = getAllOAuthServices()
716+
const normalizedInput = providerName.toLowerCase().trim()
717+
718+
const matched =
719+
allServices.find((s) => s.providerId === normalizedInput) ||
720+
allServices.find((s) => s.name.toLowerCase() === normalizedInput) ||
721+
allServices.find(
722+
(s) =>
723+
s.name.toLowerCase().includes(normalizedInput) ||
724+
normalizedInput.includes(s.name.toLowerCase())
725+
) ||
726+
allServices.find(
727+
(s) =>
728+
s.providerId.includes(normalizedInput) || normalizedInput.includes(s.providerId)
729+
)
730+
731+
if (!matched) {
732+
const available = allServices.map((s) => s.name).join(', ')
733+
throw new Error(
734+
`Provider "${providerName}" not found. Available providers: ${available}`
735+
)
736+
}
737+
738+
const { providerId, name: serviceName } = matched
739+
const callbackURL =
740+
workflowId && workspaceId
741+
? `${baseUrl}/workspace/${workspaceId}/w/${workflowId}`
742+
: `${baseUrl}/workspace/${workspaceId}`
743+
744+
// Trello and Shopify use custom auth routes, not genericOAuth
745+
if (providerId === 'trello') {
746+
return { url: `${baseUrl}/api/auth/trello/authorize`, providerId, serviceName }
747+
}
748+
if (providerId === 'shopify') {
749+
const returnUrl = encodeURIComponent(callbackURL)
750+
return {
751+
url: `${baseUrl}/api/auth/shopify/authorize?returnUrl=${returnUrl}`,
752+
providerId,
753+
serviceName,
754+
}
755+
}
756+
757+
// Create credential draft so the callback hook creates the credential
758+
const now = new Date()
759+
await db
760+
.delete(pendingCredentialDraft)
761+
.where(and(eq(pendingCredentialDraft.userId, userId), lt(pendingCredentialDraft.expiresAt, now)))
762+
await db
763+
.insert(pendingCredentialDraft)
764+
.values({
765+
id: crypto.randomUUID(),
766+
userId,
767+
workspaceId,
768+
providerId,
769+
displayName: serviceName,
770+
expiresAt: new Date(now.getTime() + 15 * 60 * 1000),
771+
createdAt: now,
772+
})
773+
.onConflictDoUpdate({
774+
target: [
775+
pendingCredentialDraft.userId,
776+
pendingCredentialDraft.providerId,
777+
pendingCredentialDraft.workspaceId,
778+
],
779+
set: {
780+
displayName: serviceName,
781+
expiresAt: new Date(now.getTime() + 15 * 60 * 1000),
782+
createdAt: now,
783+
},
784+
})
785+
786+
// Use Better Auth's server-side API with the real request headers (session cookie).
787+
const { auth } = await import('@/lib/auth/auth')
788+
const { headers: getHeaders } = await import('next/headers')
789+
const reqHeaders = await getHeaders()
790+
791+
const data = (await auth.api.oAuth2LinkAccount({
792+
body: { providerId, callbackURL },
793+
headers: reqHeaders,
794+
})) as { url?: string; redirect?: boolean }
795+
796+
if (!data?.url) {
797+
throw new Error('oAuth2LinkAccount did not return an authorization URL')
798+
}
799+
800+
return { url: data.url, providerId, serviceName }
801+
}
802+
696803
const SIM_WORKFLOW_TOOL_HANDLERS: Record<
697804
string,
698805
(params: Record<string, unknown>, context: ExecutionContext) => Promise<ToolCallResult>
@@ -741,28 +848,38 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
741848
executeUpdateWorkspaceMcpServer(p as unknown as UpdateWorkspaceMcpServerParams, c),
742849
delete_workspace_mcp_server: (p, c) =>
743850
executeDeleteWorkspaceMcpServer(p as unknown as DeleteWorkspaceMcpServerParams, c),
744-
oauth_get_auth_link: async (p, _c) => {
851+
oauth_get_auth_link: async (p, c) => {
745852
const providerName = (p.providerName || p.provider_name || 'the provider') as string
853+
const baseUrl = getBaseUrl()
854+
746855
try {
747-
const baseUrl = getBaseUrl()
748-
const settingsUrl = `${baseUrl}/workspace`
856+
const result = await generateOAuthLink(c.userId, c.workspaceId, c.workflowId, providerName, baseUrl)
749857
return {
750858
success: true,
751859
output: {
752-
message: `To connect ${providerName}, the user must authorize via their browser.`,
753-
oauth_url: settingsUrl,
754-
instructions: `Open ${settingsUrl} in a browser and go to the workflow editor to connect ${providerName} credentials.`,
755-
provider: providerName,
756-
baseUrl,
860+
message: `Authorization URL generated for ${result.serviceName}. The user must open this URL in a browser to authorize.`,
861+
oauth_url: result.url,
862+
instructions: `Open this URL in your browser to connect ${result.serviceName}: ${result.url}`,
863+
provider: result.serviceName,
864+
providerId: result.providerId,
757865
},
758866
}
759-
} catch {
867+
} catch (err) {
868+
logger.warn('Failed to generate OAuth link, falling back to generic URL', {
869+
providerName,
870+
error: err instanceof Error ? err.message : String(err),
871+
})
872+
const workspaceUrl = c.workspaceId
873+
? `${baseUrl}/workspace/${c.workspaceId}`
874+
: `${baseUrl}/workspace`
760875
return {
761-
success: true,
876+
success: false,
762877
output: {
763-
message: `To connect ${providerName}, the user must authorize via their browser.`,
764-
instructions: `Open the Sim workspace in a browser and go to the workflow editor to connect ${providerName} credentials.`,
878+
message: `Could not generate a direct OAuth link for ${providerName}. The user can connect manually from the workspace.`,
879+
oauth_url: workspaceUrl,
880+
instructions: `Open ${workspaceUrl} in a browser, go to Settings → Credentials, and connect ${providerName} from there.`,
765881
provider: providerName,
882+
error: err instanceof Error ? err.message : String(err),
766883
},
767884
}
768885
}

0 commit comments

Comments
 (0)