diff --git a/CLAUDE.md b/CLAUDE.md index ea5b2671..5b86b980 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,6 +143,45 @@ export async function selectTableName({ - All API routes should have JSDoc comments - Run `pnpm lint` before committing +## Authentication + +**Never use `account_id` in request bodies or tool schemas.** Always derive the account ID from authentication: + +- **API routes**: Use `x-api-key` header via `getApiKeyAccountId()` +- **MCP tools**: Use `extra.authInfo` via `resolveAccountId()` + +Both API keys and Privy access tokens resolve to an `accountId`. Never accept `account_id` as user input. + +### API Routes + +```typescript +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; + +const accountIdOrError = await getApiKeyAccountId(request); +if (accountIdOrError instanceof NextResponse) { + return accountIdOrError; +} +const accountId = accountIdOrError; +``` + +### MCP Tools + +```typescript +import { resolveAccountId } from "@/lib/mcp/resolveAccountId"; +import type { McpAuthInfo } from "@/lib/mcp/verifyApiKey"; + +const authInfo = extra.authInfo as McpAuthInfo | undefined; +const { accountId, error } = await resolveAccountId({ + authInfo, + accountIdOverride: undefined, +}); +``` + +This ensures: +- Callers cannot impersonate other accounts +- Authentication is always enforced +- Account ID is derived from validated credentials + ## Input Validation All API endpoints should use a **validate function** for input parsing. Use Zod for schema validation. diff --git a/app/api/connectors/authorize/route.ts b/app/api/connectors/authorize/route.ts new file mode 100644 index 00000000..5bf54d6b --- /dev/null +++ b/app/api/connectors/authorize/route.ts @@ -0,0 +1,67 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { authorizeConnector } from "@/lib/composio/connectors"; +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { validateAuthorizeConnectorBody } from "@/lib/composio/connectors/validateAuthorizeConnectorBody"; + +/** + * OPTIONS handler for CORS preflight requests. + */ +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: getCorsHeaders(), + }); +} + +/** + * POST /api/connectors/authorize + * + * Generate an OAuth authorization URL for a specific connector. + * + * Authentication: x-api-key header required. + * The account ID is inferred from the API key. + * + * Request body: + * - connector: The connector slug, e.g., "googlesheets" (required) + * - callback_url: Optional custom callback URL after OAuth + * + * @returns The redirect URL for OAuth authorization + */ +export async function POST(request: NextRequest): Promise { + const headers = getCorsHeaders(); + + try { + const accountIdOrError = await getApiKeyAccountId(request); + if (accountIdOrError instanceof NextResponse) { + return accountIdOrError; + } + + const accountId = accountIdOrError; + const body = await request.json(); + + const validated = validateAuthorizeConnectorBody(body); + if (validated instanceof NextResponse) { + return validated; + } + + const { connector, callback_url } = validated; + const result = await authorizeConnector(accountId, connector, callback_url); + + return NextResponse.json( + { + success: true, + data: { + connector: result.connector, + redirectUrl: result.redirectUrl, + }, + }, + { status: 200, headers }, + ); + } catch (error) { + const message = + error instanceof Error ? error.message : "Failed to authorize connector"; + return NextResponse.json({ error: message }, { status: 500, headers }); + } +} diff --git a/app/api/connectors/route.ts b/app/api/connectors/route.ts new file mode 100644 index 00000000..8dfdab95 --- /dev/null +++ b/app/api/connectors/route.ts @@ -0,0 +1,109 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { getConnectors } from "@/lib/composio/connectors"; +import { disconnectConnector } from "@/lib/composio/connectors/disconnectConnector"; +import { validateDisconnectConnectorBody } from "@/lib/composio/connectors/validateDisconnectConnectorBody"; +import { verifyConnectorOwnership } from "@/lib/composio/connectors/verifyConnectorOwnership"; +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; + +/** + * OPTIONS handler for CORS preflight requests. + */ +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: getCorsHeaders(), + }); +} + +/** + * GET /api/connectors + * + * List all available connectors and their connection status for a user. + * + * Authentication: x-api-key header required. + * + * @returns List of connectors with connection status + */ +export async function GET(request: NextRequest): Promise { + const headers = getCorsHeaders(); + + try { + const accountIdOrError = await getApiKeyAccountId(request); + if (accountIdOrError instanceof NextResponse) { + return accountIdOrError; + } + + const accountId = accountIdOrError; + + const connectors = await getConnectors(accountId); + + return NextResponse.json( + { + success: true, + data: { + connectors, + }, + }, + { status: 200, headers }, + ); + } catch (error) { + const message = + error instanceof Error ? error.message : "Failed to fetch connectors"; + return NextResponse.json({ error: message }, { status: 500, headers }); + } +} + +/** + * DELETE /api/connectors + * + * Disconnect a connected account from Composio. + * + * Authentication: x-api-key header required. + * + * Body: { connected_account_id: string } + */ +export async function DELETE(request: NextRequest): Promise { + const headers = getCorsHeaders(); + + try { + const accountIdOrError = await getApiKeyAccountId(request); + if (accountIdOrError instanceof NextResponse) { + return accountIdOrError; + } + + const accountId = accountIdOrError; + const body = await request.json(); + + const validated = validateDisconnectConnectorBody(body); + if (validated instanceof NextResponse) { + return validated; + } + + const { connected_account_id } = validated; + + // Verify the connected account belongs to the authenticated user + const isOwner = await verifyConnectorOwnership(accountId, connected_account_id); + if (!isOwner) { + return NextResponse.json( + { error: "Connected account not found or does not belong to this user" }, + { status: 403, headers } + ); + } + + const result = await disconnectConnector(connected_account_id); + + return NextResponse.json( + { + success: true, + data: result, + }, + { status: 200, headers }, + ); + } catch (error) { + const message = + error instanceof Error ? error.message : "Failed to disconnect connector"; + return NextResponse.json({ error: message }, { status: 500, headers }); + } +} diff --git a/lib/composio/connectors/authorizeConnector.ts b/lib/composio/connectors/authorizeConnector.ts new file mode 100644 index 00000000..23a61134 --- /dev/null +++ b/lib/composio/connectors/authorizeConnector.ts @@ -0,0 +1,45 @@ +import { getComposioClient } from "../client"; +import { getCallbackUrl } from "../getCallbackUrl"; + +/** + * Result of authorizing a connector. + */ +export interface AuthorizeResult { + connector: string; + redirectUrl: string; +} + +/** + * Generate an OAuth authorization URL for a connector. + * + * Why: Used by the /api/connectors/authorize endpoint to let users + * connect from the settings page (not in-chat). + * + * @param userId - The user's account ID + * @param connector - The connector slug (e.g., "googlesheets", "gmail") + * @param customCallbackUrl - Optional custom callback URL after OAuth + * @returns The redirect URL for OAuth + */ +export async function authorizeConnector( + userId: string, + connector: string, + customCallbackUrl?: string, +): Promise { + const composio = getComposioClient(); + + const callbackUrl = + customCallbackUrl || getCallbackUrl({ destination: "connectors" }); + + const session = await composio.create(userId, { + manageConnections: { + callbackUrl, + }, + }); + + const connectionRequest = await session.authorize(connector); + + return { + connector, + redirectUrl: connectionRequest.redirectUrl, + }; +} diff --git a/lib/composio/connectors/disconnectConnector.ts b/lib/composio/connectors/disconnectConnector.ts new file mode 100644 index 00000000..3aff9b9e --- /dev/null +++ b/lib/composio/connectors/disconnectConnector.ts @@ -0,0 +1,36 @@ +import { getComposioApiKey } from "../getComposioApiKey"; + +/** + * Disconnect a connected account from Composio. + * + * Why: Composio's Tool Router SDK doesn't expose a disconnect method, + * so we call the REST API directly to delete the connection. + * + * @param connectedAccountId - The ID of the connected account to disconnect + * @returns Success status + */ +export async function disconnectConnector( + connectedAccountId: string, +): Promise<{ success: boolean }> { + const apiKey = getComposioApiKey(); + + // Composio v3 API uses DELETE method + const url = `https://backend.composio.dev/api/v3/connected_accounts/${connectedAccountId}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + "x-api-key": apiKey, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to disconnect (${response.status}): ${errorText}`, + ); + } + + return { success: true }; +} diff --git a/lib/composio/connectors/getConnectors.ts b/lib/composio/connectors/getConnectors.ts new file mode 100644 index 00000000..6f5ac127 --- /dev/null +++ b/lib/composio/connectors/getConnectors.ts @@ -0,0 +1,30 @@ +import { getComposioClient } from "../client"; + +/** + * Connector info returned by Composio. + */ +export interface ConnectorInfo { + slug: string; + name: string; + isConnected: boolean; + connectedAccountId?: string; +} + +/** + * Get all connectors and their connection status for a user. + * + * @param userId - The user's account ID + * @returns List of connectors with connection status + */ +export async function getConnectors(userId: string): Promise { + const composio = getComposioClient(); + const session = await composio.create(userId); + const toolkits = await session.toolkits(); + + return toolkits.items.map((toolkit) => ({ + slug: toolkit.slug, + name: toolkit.name, + isConnected: toolkit.connection?.isActive ?? false, + connectedAccountId: toolkit.connection?.connectedAccount?.id, + })); +} diff --git a/lib/composio/connectors/index.ts b/lib/composio/connectors/index.ts new file mode 100644 index 00000000..fa866c66 --- /dev/null +++ b/lib/composio/connectors/index.ts @@ -0,0 +1,3 @@ +export { getConnectors, type ConnectorInfo } from "./getConnectors"; +export { authorizeConnector, type AuthorizeResult } from "./authorizeConnector"; +export { disconnectConnector } from "./disconnectConnector"; diff --git a/lib/composio/connectors/validateAuthorizeConnectorBody.ts b/lib/composio/connectors/validateAuthorizeConnectorBody.ts new file mode 100644 index 00000000..df3570e3 --- /dev/null +++ b/lib/composio/connectors/validateAuthorizeConnectorBody.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { z } from "zod"; + +export const authorizeConnectorBodySchema = z.object({ + connector: z + .string({ message: "connector is required" }) + .min(1, "connector cannot be empty (e.g., 'googlesheets', 'gmail')"), + callback_url: z.string().url("callback_url must be a valid URL").optional(), +}); + +export type AuthorizeConnectorBody = z.infer< + typeof authorizeConnectorBodySchema +>; + +/** + * Validates request body for POST /api/connectors/authorize. + * + * @param body - The request body + * @returns A NextResponse with an error if validation fails, or the validated body if validation passes. + */ +export function validateAuthorizeConnectorBody( + body: unknown +): NextResponse | AuthorizeConnectorBody { + const result = authorizeConnectorBodySchema.safeParse(body); + + if (!result.success) { + const firstError = result.error.issues[0]; + return NextResponse.json( + { + error: firstError.message, + }, + { + status: 400, + headers: getCorsHeaders(), + } + ); + } + + return result.data; +} diff --git a/lib/composio/connectors/validateDisconnectConnectorBody.ts b/lib/composio/connectors/validateDisconnectConnectorBody.ts new file mode 100644 index 00000000..70dfb150 --- /dev/null +++ b/lib/composio/connectors/validateDisconnectConnectorBody.ts @@ -0,0 +1,34 @@ +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { z } from "zod"; + +export const disconnectConnectorBodySchema = z.object({ + connected_account_id: z.string().min(1, "connected_account_id is required"), +}); + +export type DisconnectConnectorBody = z.infer; + +/** + * Validates request body for POST /api/connectors/disconnect. + * + * @param body - The request body + * @returns A NextResponse with an error if validation fails, or the validated body if validation passes. + */ +export function validateDisconnectConnectorBody(body: unknown): NextResponse | DisconnectConnectorBody { + const result = disconnectConnectorBodySchema.safeParse(body); + + if (!result.success) { + const firstError = result.error.issues[0]; + return NextResponse.json( + { + error: firstError.message, + }, + { + status: 400, + headers: getCorsHeaders(), + }, + ); + } + + return result.data; +} diff --git a/lib/composio/connectors/verifyConnectorOwnership.ts b/lib/composio/connectors/verifyConnectorOwnership.ts new file mode 100644 index 00000000..dc4f7a67 --- /dev/null +++ b/lib/composio/connectors/verifyConnectorOwnership.ts @@ -0,0 +1,23 @@ +import { getConnectors } from "./getConnectors"; + +/** + * Verifies that a connected account ID belongs to the specified user. + * + * Why: Before disconnecting a connector, we must verify ownership to prevent + * users from disconnecting other users' connectors (authorization bypass). + * + * @param accountId - The authenticated user's account ID + * @param connectedAccountId - The connected account ID to verify + * @returns true if the connected account belongs to the user, false otherwise + */ +export async function verifyConnectorOwnership( + accountId: string, + connectedAccountId: string +): Promise { + const connectors = await getConnectors(accountId); + + // Check if any of the user's connectors have this connected account ID + return connectors.some( + (connector) => connector.connectedAccountId === connectedAccountId + ); +} diff --git a/lib/composio/getCallbackUrl.ts b/lib/composio/getCallbackUrl.ts new file mode 100644 index 00000000..570c9251 --- /dev/null +++ b/lib/composio/getCallbackUrl.ts @@ -0,0 +1,34 @@ +import { getFrontendBaseUrl } from "./getFrontendBaseUrl"; + +/** + * Build OAuth callback URL based on environment and destination. + * + * Why: Composio redirects users back after OAuth. We need different + * destinations depending on context (chat room vs settings page). + */ + +type CallbackDestination = "chat" | "connectors"; + +interface CallbackOptions { + destination: CallbackDestination; + roomId?: string; +} + +/** + * Build callback URL for OAuth redirects. + * + * @param options.destination - Where to redirect: "chat" or "connectors" + * @param options.roomId - For chat destination, the room ID to return to + * @returns Full callback URL with success indicator + */ +export function getCallbackUrl(options: CallbackOptions): string { + const baseUrl = getFrontendBaseUrl(); + + if (options.destination === "connectors") { + return `${baseUrl}/settings/connectors?connected=true`; + } + + // Chat destination + const path = options.roomId ? `/chat/${options.roomId}` : "/chat"; + return `${baseUrl}${path}?connected=true`; +} diff --git a/lib/composio/getFrontendBaseUrl.ts b/lib/composio/getFrontendBaseUrl.ts new file mode 100644 index 00000000..7a5dbe18 --- /dev/null +++ b/lib/composio/getFrontendBaseUrl.ts @@ -0,0 +1,20 @@ +const TEST_FRONTEND_URL = "https://test-recoup-chat.vercel.app"; + +/** + * Get the frontend base URL based on environment. + */ +export function getFrontendBaseUrl(): string { + if (process.env.VERCEL_ENV === "production") { + return "https://chat.recoupable.com"; + } + + if (process.env.VERCEL_GIT_COMMIT_REF === "test") { + return TEST_FRONTEND_URL; + } + + if (process.env.VERCEL_URL) { + return `https://${process.env.VERCEL_URL}`; + } + + return "http://localhost:3000"; +} diff --git a/lib/composio/toolRouter/createSession.ts b/lib/composio/toolRouter/createSession.ts new file mode 100644 index 00000000..1cd36e41 --- /dev/null +++ b/lib/composio/toolRouter/createSession.ts @@ -0,0 +1,29 @@ +import { getComposioClient } from "../client"; +import { getCallbackUrl } from "../getCallbackUrl"; + +/** + * Toolkits available in Tool Router sessions. + * Add more toolkits here as we expand Composio integration. + */ +const ENABLED_TOOLKITS = ["googlesheets"]; + +/** + * Create a Composio Tool Router session for a user. + */ +export async function createToolRouterSession(userId: string, roomId?: string) { + const composio = getComposioClient(); + + const callbackUrl = getCallbackUrl({ + destination: "chat", + roomId, + }); + + const session = await composio.create(userId, { + toolkits: ENABLED_TOOLKITS, + manageConnections: { + callbackUrl, + }, + }); + + return session; +} diff --git a/lib/composio/toolRouter/getTools.ts b/lib/composio/toolRouter/getTools.ts new file mode 100644 index 00000000..0140dd58 --- /dev/null +++ b/lib/composio/toolRouter/getTools.ts @@ -0,0 +1,76 @@ +import { createToolRouterSession } from "./createSession"; +import type { Tool, ToolSet } from "ai"; + +/** + * Tools we want to expose from Composio Tool Router. + * Once we're ready to add all tools, remove this filter. + */ +const ALLOWED_TOOLS = [ + "COMPOSIO_MANAGE_CONNECTIONS", + "COMPOSIO_SEARCH_TOOLS", + "COMPOSIO_GET_TOOL_SCHEMAS", + "COMPOSIO_MULTI_EXECUTE_TOOL", +]; + +/** + * Runtime validation to check if an object is a valid Vercel AI SDK Tool. + * + * Why: The Composio SDK's session.tools() returns a ToolSet (Record) + * from the configured provider. With VercelProvider, this returns Vercel AI SDK tools. + * We validate at runtime to ensure type safety before using bracket notation access. + * + * @param tool - The object to validate + * @returns true if the object has required Tool properties + */ +function isValidTool(tool: unknown): tool is Tool { + if (typeof tool !== "object" || tool === null) { + return false; + } + + const obj = tool as Record; + + // Vercel AI SDK Tool requires: description (optional), parameters, execute + // The execute function is what makes it callable + const hasExecute = typeof obj.execute === "function"; + const hasParameters = "parameters" in obj; + + return hasExecute && hasParameters; +} + +/** + * Get Composio Tool Router tools for a user. + * + * Returns a filtered subset of meta-tools: + * - COMPOSIO_MANAGE_CONNECTIONS - OAuth/auth management + * - COMPOSIO_SEARCH_TOOLS - Find available connectors + * - COMPOSIO_GET_TOOL_SCHEMAS - Get parameter schemas + * - COMPOSIO_MULTI_EXECUTE_TOOL - Execute actions + * + * @param userId - Unique identifier for the user (accountId) + * @param roomId - Optional chat room ID for OAuth redirect + * @returns ToolSet containing filtered Vercel AI SDK tools + */ +export async function getComposioTools( + userId: string, + roomId?: string +): Promise { + const session = await createToolRouterSession(userId, roomId); + const allTools = await session.tools(); + + // Filter to only allowed tools with runtime validation + const filteredTools: ToolSet = {}; + + for (const toolName of ALLOWED_TOOLS) { + // Use Object.prototype.hasOwnProperty to safely check for property existence + // This handles both plain objects and class instances safely + if (Object.prototype.hasOwnProperty.call(allTools, toolName)) { + const tool = (allTools as Record)[toolName]; + + if (isValidTool(tool)) { + filteredTools[toolName] = tool; + } + } + } + + return filteredTools; +} diff --git a/lib/composio/toolRouter/index.ts b/lib/composio/toolRouter/index.ts new file mode 100644 index 00000000..0e3bb33c --- /dev/null +++ b/lib/composio/toolRouter/index.ts @@ -0,0 +1,2 @@ +export { createToolRouterSession } from "./createSession"; +export { getComposioTools } from "./getTools"; diff --git a/lib/mcp/tools/composio/index.ts b/lib/mcp/tools/composio/index.ts new file mode 100644 index 00000000..de072863 --- /dev/null +++ b/lib/mcp/tools/composio/index.ts @@ -0,0 +1,14 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerComposioTools } from "./registerComposioTools"; + +/** + * Registers all Composio-related tools on the MCP server. + * + * Currently registers: + * - composio: Meta-tool for accessing Composio Tool Router + * + * @param server - The MCP server instance to register tools on. + */ +export function registerAllComposioTools(server: McpServer): void { + registerComposioTools(server); +} diff --git a/lib/mcp/tools/composio/registerComposioTools.ts b/lib/mcp/tools/composio/registerComposioTools.ts new file mode 100644 index 00000000..8c6bec3e --- /dev/null +++ b/lib/mcp/tools/composio/registerComposioTools.ts @@ -0,0 +1,38 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +import type { McpAuthInfo } from "@/lib/mcp/verifyApiKey"; +import { resolveAccountId } from "@/lib/mcp/resolveAccountId"; +import { getComposioTools } from "@/lib/composio/toolRouter"; +import { getCallToolResult } from "@/lib/mcp/getCallToolResult"; + +const composioSchema = z.object({ + room_id: z.string().optional().describe("Chat room ID for OAuth redirect"), +}); + +type ComposioArgs = z.infer; + +export function registerComposioTools(server: McpServer): void { + server.registerTool( + "composio", + { + description: "Get Composio tools for Google Sheets integration.", + inputSchema: composioSchema, + }, + async (args: ComposioArgs, extra: RequestHandlerExtra) => { + const authInfo = extra.authInfo as McpAuthInfo | undefined; + const { accountId, error } = await resolveAccountId({ + authInfo, + accountIdOverride: undefined, + }); + + if (error || !accountId) { + return getCallToolResult(JSON.stringify({ error: error || "Authentication required" })); + } + + const tools = await getComposioTools(accountId, args.room_id); + return getCallToolResult(JSON.stringify(tools)); + } + ); +} diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index 984ba928..ca72d82a 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -16,6 +16,7 @@ import { registerCreateSegmentsTool } from "./registerCreateSegmentsTool"; import { registerAllYouTubeTools } from "./youtube"; import { registerTranscribeTools } from "./transcribe"; import { registerSendEmailTool } from "./registerSendEmailTool"; +import { registerAllComposioTools } from "./composio"; import { registerAllArtistTools } from "./artists"; /** @@ -28,6 +29,7 @@ export const registerAllTools = (server: McpServer): void => { registerAllArtistTools(server); registerAllArtistSocialsTools(server); registerAllCatalogTools(server); + registerAllComposioTools(server); registerAllFileTools(server); registerAllImageTools(server); registerAllSora2Tools(server); diff --git a/package.json b/package.json index acbc005b..117a350a 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "@ai-sdk/openai": "^3.0.10", "@coinbase/cdp-sdk": "^1.38.6", "@coinbase/x402": "^0.7.3", - "@composio/core": "^0.2.6", - "@composio/vercel": "^0.2.18", + "@composio/core": "^0.3.4", + "@composio/vercel": "^0.3.4", "@modelcontextprotocol/sdk": "^1.24.3", "@privy-io/node": "^0.6.2", "@supabase/supabase-js": "^2.86.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91256a2a..e4f20f8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,11 +30,11 @@ importers: specifier: ^0.7.3 version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@composio/core': - specifier: ^0.2.6 - version: 0.2.6(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + specifier: ^0.3.4 + version: 0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@composio/vercel': - specifier: ^0.2.18 - version: 0.2.18(@composio/core@0.2.6(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13)) + specifier: ^0.3.4 + version: 0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13)) '@modelcontextprotocol/sdk': specifier: ^1.24.3 version: 1.24.3(zod@4.1.13) @@ -270,24 +270,24 @@ packages: '@coinbase/x402@0.7.3': resolution: {integrity: sha512-mSxxbPnDCvSLfq6ZZAB5P1HyYQfQNSdWyY01Cn7yjzTuGUFFgZ7onDkbZnZ1Yry0UnG467aqrmjs6AgoYgpzCQ==} - '@composio/client@0.1.0-alpha.40': - resolution: {integrity: sha512-xkd+LseCiuFLxiXXtjFgON0bO4M/1AsBQD4omhf2nl5tXJI0sXj72wCf1EXhT2jIu5pgJqJ4H64ZzwoM0fHDvg==} + '@composio/client@0.1.0-alpha.52': + resolution: {integrity: sha512-IFPa77R/v4JUX0QeRHRTEkJCBZ+OqMQBh1rIn4Nv5PIP1eP3sZqxlTYJUKPS4qLNnwBJRD+Vn7/jUs5J7pdv5g==} - '@composio/core@0.2.6': - resolution: {integrity: sha512-QQgZz9o0ikl+A38PhyE0/j5V1y5wDDl8z6yfYw/WYAkk7AqjOJI9eJ/eujwgkTWxKnMaxrkthVzsjtvg/KIoYw==} + '@composio/core@0.3.4': + resolution: {integrity: sha512-6jHKJ9kbuJcCPdW8k8jFN2UlszO5e1TJIQ9WkJWOxNO7Qw8wzZ5Ay5pqL5XtU3beDxzQS/YDwBYDVHFppjx2Rg==} peerDependencies: - zod: '>=3.25.76 <5' + zod: ^3.25 || ^4 '@composio/json-schema-to-zod@0.1.19': resolution: {integrity: sha512-OynnORVWjsqDv13EvFa4Bb+B1SzBqpkWGi6qXm4vpB3EG65o3T9FbhDCqWB3ZufnMmH1T/NYS526O0lnn2LoCQ==} peerDependencies: zod: '>=3.25.76 <4 || >=4.1 <5' - '@composio/vercel@0.2.18': - resolution: {integrity: sha512-UuswOOjD+PZcqZT0vw+gHolLKMM1UI8n1EmB2vTlBqpOTsA0eTXfUuYcSED5qsvPoB9r6yWyg271MF3MeAPSZQ==} + '@composio/vercel@0.3.4': + resolution: {integrity: sha512-uo4MsSnif6fghvaGb5h473DAf+mFLJRlfFZ/XL4Jmf2TrMz7Z4ErBngep5WfHJ8vLJhbsI06AJ0BJ6ti3V5Ijg==} peerDependencies: - '@composio/core': 0.2.6 - ai: ^5.0.44 + '@composio/core': 0.3.4 + ai: 5.0.52 '@crawlee/types@3.15.3': resolution: {integrity: sha512-RvgVPXrsQw4GQIUXrC1z1aNOedUPJnZ/U/8n+jZ0fu1Iw9moJVMuiuIxSI8q1P6BA84aWZdalyfDWBZ3FMjsiw==} @@ -5933,11 +5933,11 @@ snapshots: - utf-8-validate - ws - '@composio/client@0.1.0-alpha.40': {} + '@composio/client@0.1.0-alpha.52': {} - '@composio/core@0.2.6(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': + '@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: - '@composio/client': 0.1.0-alpha.40 + '@composio/client': 0.1.0-alpha.52 '@composio/json-schema-to-zod': 0.1.19(zod@4.1.13) '@types/json-schema': 7.0.15 chalk: 4.1.2 @@ -5954,9 +5954,9 @@ snapshots: dependencies: zod: 4.1.13 - '@composio/vercel@0.2.18(@composio/core@0.2.6(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13))': + '@composio/vercel@0.3.4(@composio/core@0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ai@6.0.0-beta.122(zod@4.1.13))': dependencies: - '@composio/core': 0.2.6(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + '@composio/core': 0.3.4(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) ai: 6.0.0-beta.122(zod@4.1.13) '@crawlee/types@3.15.3':