Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import refreshConnectedAccount from "@/lib/composio/googleSheets/refreshConnectedAccount";
import { refreshConnectedAccount } from "@/lib/composio/refreshConnectedAccount";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
Expand All @@ -15,10 +13,7 @@ export async function OPTIONS() {
}

/**
* POST handler for refreshing a connected account.
*
* @param request - The request object.
* @returns A NextResponse with the refreshed connected account.
* POST handler for refreshing a Google Drive connected account.
*/
export async function POST(request: NextRequest) {
try {
Expand All @@ -28,34 +23,28 @@ export async function POST(request: NextRequest) {
if (!accountId) {
return NextResponse.json(
{ error: "accountId is required" },
{
status: 400,
headers: getCorsHeaders(),
},
{ status: 400, headers: getCorsHeaders() },
);
}

const response = await refreshConnectedAccount(accountId, redirectUrl);
const response = await refreshConnectedAccount(
"GOOGLE_DRIVE",
accountId,
redirectUrl,
);

return NextResponse.json(
{ message: "Connected account refreshed successfully", ...response },
{
status: 200,
headers: getCorsHeaders(),
},
{ status: 200, headers: getCorsHeaders() },
);
} catch (error) {
console.error("Error refreshing connected account:", error);

const errorMessage = error instanceof Error ? error.message : "Internal server error";
const errorMessage =
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error logging in the catch block. The old endpoint logged all errors with console.error(), but the new implementation doesn't, reducing observability in production.

View Details
📝 Patch Details
diff --git a/app/api/connectedAccounts/googleDrive/refresh/route.ts b/app/api/connectedAccounts/googleDrive/refresh/route.ts
index a2bccc3..b427726 100644
--- a/app/api/connectedAccounts/googleDrive/refresh/route.ts
+++ b/app/api/connectedAccounts/googleDrive/refresh/route.ts
@@ -38,6 +38,7 @@ export async function POST(request: NextRequest) {
       { status: 200, headers: getCorsHeaders() },
     );
   } catch (error) {
+    console.error("Error refreshing connected account:", error);
     const errorMessage =
       error instanceof Error ? error.message : "Internal server error";
     const statusCode = errorMessage.includes("not found") ? 404 : 500;
diff --git a/app/api/connectedAccounts/googleSheets/refresh/route.ts b/app/api/connectedAccounts/googleSheets/refresh/route.ts
index 4b6fb47..42a8b4a 100644
--- a/app/api/connectedAccounts/googleSheets/refresh/route.ts
+++ b/app/api/connectedAccounts/googleSheets/refresh/route.ts
@@ -38,6 +38,7 @@ export async function POST(request: NextRequest) {
       { status: 200, headers: getCorsHeaders() },
     );
   } catch (error) {
+    console.error("Error refreshing connected account:", error);
     const errorMessage =
       error instanceof Error ? error.message : "Internal server error";
     const statusCode = errorMessage.includes("not found") ? 404 : 500;

Analysis

Missing error logging in refresh endpoints

What fails: The POST handlers in app/api/connectedAccounts/googleDrive/refresh/route.ts and app/api/connectedAccounts/googleSheets/refresh/route.ts do not log errors to the server console when refresh operations fail, reducing observability in production.

How to reproduce:

// Call the endpoint with an invalid or expired accountId that will cause refreshConnectedAccount() to throw
const response = await fetch('/api/connectedAccounts/googleDrive/refresh', {
  method: 'POST',
  body: JSON.stringify({ accountId: 'invalid-id', redirectUrl: 'https://example.com' })
});

Result: When the underlying API call fails, no error is logged to the server console. The error is sent to the client but server-side observability is lost.

Expected behavior: Errors should be logged server-side with console.error("Error refreshing connected account:", error); matching:

  1. The original endpoint implementation (git commit e528311: removed file had this logging)
  2. Similar error handling patterns in the codebase (app/api/image/generate/route.ts, app/api/x402/image/generate/route.ts)

References:

  • Original implementation logged errors before refactoring in commit e528311
  • Similar endpoints use console.error for production monitoring

error instanceof Error ? error.message : "Internal server error";
const statusCode = errorMessage.includes("not found") ? 404 : 500;

return NextResponse.json(
{ error: errorMessage },
{
status: statusCode,
headers: getCorsHeaders(),
},
{ status: statusCode, headers: getCorsHeaders() },
);
}
}
50 changes: 50 additions & 0 deletions app/api/connectedAccounts/googleSheets/refresh/route.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need an API endpoint for refreshing the sheets token?

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from "next/server";
import { refreshConnectedAccount } from "@/lib/composio/refreshConnectedAccount";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* POST handler for refreshing a Google Sheets connected account.
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { accountId, redirectUrl } = body;

if (!accountId) {
return NextResponse.json(
{ error: "accountId is required" },
{ status: 400, headers: getCorsHeaders() },
);
}
Comment on lines +23 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use api key instead of accountId

  • accountId is not secure.
  • api key is secure.


const response = await refreshConnectedAccount(
"GOOGLE_SHEETS",
accountId,
redirectUrl,
);

return NextResponse.json(
{ message: "Connected account refreshed successfully", ...response },
{ status: 200, headers: getCorsHeaders() },
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Internal server error";
const statusCode = errorMessage.includes("not found") ? 404 : 500;

return NextResponse.json(
{ error: errorMessage },
{ status: statusCode, headers: getCorsHeaders() },
);
}
}
28 changes: 28 additions & 0 deletions lib/composio/authenticateToolkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CreateConnectedAccountOptions } from "@composio/core";
import { getComposioClient } from "./client";
import { ComposioToolkitKey, getToolkitAuthConfigId } from "./toolkits";

/**
* Initiate OAuth authentication for a Composio toolkit.
*
* @param toolkitKey - The toolkit to authenticate (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE")
* @param userId - The user ID to authenticate
* @param options - Optional callback URL and other options
* @returns The connection request from Composio
*/
export async function authenticateToolkit(
toolkitKey: ComposioToolkitKey,
userId: string,
options?: CreateConnectedAccountOptions,
) {
const authConfigId = getToolkitAuthConfigId(toolkitKey);
const composio = getComposioClient();

const connectionRequest = await composio.connectedAccounts.initiate(
userId,
authConfigId,
options,
);

return connectionRequest;
}
37 changes: 37 additions & 0 deletions lib/composio/getConnectedAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CreateConnectedAccountOptions } from "@composio/core";
import { getComposioClient } from "./client";
import { authenticateToolkit } from "./authenticateToolkit";
import { ComposioToolkitKey, getToolkitConfig } from "./toolkits";

/**
* Get a user's connected account for a Composio toolkit.
* If no connection exists, initiates the authentication flow.
*
* @param toolkitKey - The toolkit to check (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE")
* @param accountId - The user's account ID
* @param options - Optional callback URL and other options
* @returns The user's connected accounts for this toolkit
*/
export async function getConnectedAccount(
toolkitKey: ComposioToolkitKey,
accountId: string,
options?: CreateConnectedAccountOptions,
) {
const config = getToolkitConfig(toolkitKey);
const composio = getComposioClient();

let userAccounts = await composio.connectedAccounts.list({
userIds: [accountId],
toolkitSlugs: [config.slug],
});

if (userAccounts.items.length === 0) {
await authenticateToolkit(toolkitKey, accountId, options);
userAccounts = await composio.connectedAccounts.list({
userIds: [accountId],
toolkitSlugs: [config.slug],
});
}
Comment on lines +28 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Logic issue: OAuth flow requires user interaction before retry.

After calling authenticateToolkit on line 29, the code immediately retries the account list. However, authenticateToolkit initiates an OAuth flow that requires user interaction (redirect, consent, callback). The retry will likely return empty because the OAuth flow hasn't completed yet.

Consider one of these approaches:

  1. Return the connection request to the caller for handling
  2. Document this expected behavior clearly
  3. Remove the retry and let the caller handle the flow
Proposed approach 1: Return connection request
 export async function getConnectedAccount(
   toolkitKey: ComposioToolkitKey,
   accountId: string,
   options?: CreateConnectedAccountOptions,
 ) {
   const config = getToolkitConfig(toolkitKey);
   const composio = getComposioClient();

   let userAccounts = await composio.connectedAccounts.list({
     userIds: [accountId],
     toolkitSlugs: [config.slug],
   });

   if (userAccounts.items.length === 0) {
-    await authenticateToolkit(toolkitKey, accountId, options);
-    userAccounts = await composio.connectedAccounts.list({
-      userIds: [accountId],
-      toolkitSlugs: [config.slug],
-    });
+    const connectionRequest = await authenticateToolkit(toolkitKey, accountId, options);
+    // Return connection request for caller to handle OAuth flow
+    return { items: [], connectionRequest };
   }

   return userAccounts;
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @lib/composio/getConnectedAccount.ts around lines 28 - 34, The code calls
authenticateToolkit(toolkitKey, accountId, options) which starts an interactive
OAuth flow and then immediately retries composio.connectedAccounts.list — this
will usually still be empty; instead stop retrying: remove the second call to
composio.connectedAccounts.list after authenticateToolkit and change
getConnectedAccount (or the function containing this block) to return the
connection request result (or a flag/object indicating the OAuth flow was
initiated) so the caller can perform the redirect/consent and retry list after
the callback completes; update any callers to handle the connection-request
response rather than relying on an immediate retry.


return userAccounts;
}
29 changes: 0 additions & 29 deletions lib/composio/googleSheets/authenticateGoogleSheetsToolkit.ts

This file was deleted.

33 changes: 0 additions & 33 deletions lib/composio/googleSheets/getConnectedAccount.ts

This file was deleted.

60 changes: 0 additions & 60 deletions lib/composio/googleSheets/refreshConnectedAccount.ts

This file was deleted.

72 changes: 72 additions & 0 deletions lib/composio/refreshConnectedAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { getConnectedAccount } from "./getConnectedAccount";
import { getComposioApiKey } from "./getComposioApiKey";
import { ComposioToolkitKey, getToolkitConfig } from "./toolkits";

export interface ConnectedAccountRefreshResponse {
id: string;
redirect_url: string | null;
status:
| "INITIALIZING"
| "INITIATED"
| "ACTIVE"
| "FAILED"
| "EXPIRED"
| "INACTIVE";
}

const COMPOSIO_API_BASE_URL = "https://backend.composio.dev";

/**
* Refresh a connected account for a Composio toolkit.
*
* @param toolkitKey - The toolkit to refresh (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE")
* @param accountId - The user's account ID
* @param redirectUrl - The URL to redirect to after OAuth
* @returns The refresh response with OAuth redirect URL
*/
export async function refreshConnectedAccount(
toolkitKey: ComposioToolkitKey,
accountId: string,
redirectUrl?: string,
): Promise<ConnectedAccountRefreshResponse | null> {
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix return type inconsistency.

The function signature declares Promise<ConnectedAccountRefreshResponse | null>, but the implementation never returns null. It either throws an error (lines 37, 43, 66) or returns the parsed JSON response (line 71). The return type should be Promise<ConnectedAccountRefreshResponse> to accurately reflect the actual behavior.

🔧 Proposed fix
-): Promise<ConnectedAccountRefreshResponse | null> {
+): Promise<ConnectedAccountRefreshResponse> {

Also applies to: 71-71

🤖 Prompt for AI Agents
In @lib/composio/refreshConnectedAccount.ts around lines 27 - 31, The declared
return type of refreshConnectedAccount is inaccurate: change the function
signature from Promise<ConnectedAccountRefreshResponse | null> to
Promise<ConnectedAccountRefreshResponse> because the implementation never
returns null (it throws on errors and returns parsed JSON via the response
handling in refreshConnectedAccount). Update the return type in the
refreshConnectedAccount declaration accordingly and verify callers don't rely on
a null return (adjust callers if any assume null).

const apiKey = getComposioApiKey();
const config = getToolkitConfig(toolkitKey);
const accounts = await getConnectedAccount(toolkitKey, accountId);

if (!accounts.items || accounts.items.length === 0) {
throw new Error(`${config.name} connected account not found`);
}

const connectedAccountId = accounts.items[0].id;

if (!connectedAccountId) {
throw new Error(`${config.name} connected account ID not found`);
}

const url = new URL(
`${COMPOSIO_API_BASE_URL}/api/v3/connected_accounts/${connectedAccountId}/refresh`,
);

const body: { redirect_url?: string } = {};
if (redirectUrl) {
body.redirect_url = redirectUrl;
}

const response = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify(body),
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to refresh ${config.name} connected account: ${response.status} ${errorText}`,
);
}

return await response.json();
}
Loading