-
Notifications
You must be signed in to change notification settings - Fork 0
feat: improve Composio tool description with service categories and u… #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0da55e6
29c7fb3
43a470c
50f75a6
b3a8f31
376aab8
1cc260a
90796fb
9808865
47cf4bf
d351e3c
b74b074
1cc3ec5
453f1bd
5a9d55e
7bc18e0
bdd98b7
b0cedbc
9b9db79
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<NextResponse> { | ||
| 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 }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<NextResponse> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const headers = getCorsHeaders(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const accountIdOrError = await getApiKeyAccountId(request); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (accountIdOrError instanceof NextResponse) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return accountIdOrError; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
32
to
36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Missing Zod schema validation per coding guidelines. As per the coding guidelines, all API endpoints should use a validate function for input parsing with Zod schema validation. The current manual query parameter validation should be replaced with a Zod schema. ♻️ Suggested implementation with Zod import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getConnectors } from "@/lib/composio/connectors";
+import { z } from "zod";
+
+const getConnectorsQuerySchema = z.object({
+ account_id: z.string().min(1, "account_id query parameter is required"),
+});Then update the GET handler: try {
- const accountId = request.nextUrl.searchParams.get("account_id");
-
- if (!accountId) {
- return NextResponse.json(
- { error: "account_id query parameter is required" },
- { status: 400, headers },
- );
- }
+ const queryParams = {
+ account_id: request.nextUrl.searchParams.get("account_id"),
+ };
+ const parseResult = getConnectorsQuerySchema.safeParse(queryParams);
+
+ if (!parseResult.success) {
+ return NextResponse.json(
+ { error: parseResult.error.issues[0]?.message ?? "Invalid query parameters" },
+ { status: 400, headers },
+ );
+ }
+
+ const { account_id: accountId } = parseResult.data;Based on coding guidelines, all API endpoints should use a validate function for input parsing with Zod schema validation. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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<NextResponse> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
vercel[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<AuthorizeResult> { | ||
| 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, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ConnectorInfo[]> { | ||
| 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, | ||
| })); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { getConnectors, type ConnectorInfo } from "./getConnectors"; | ||
| export { authorizeConnector, type AuthorizeResult } from "./authorizeConnector"; | ||
| export { disconnectConnector } from "./disconnectConnector"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(), | ||
sweetmantech marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| 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; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.