Skip to content

Conversation

@ctate
Copy link
Collaborator

@ctate ctate commented Dec 15, 2025

No description provided.

@vercel
Copy link
Contributor

vercel bot commented Dec 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
workflow-builder Ready Ready Preview, Comment Dec 15, 2025 11:16pm

Comment on lines 228 to 234
const managedIntegration = await db.query.integrations.findFirst({
where: and(
eq(integrations.userId, session.user.id),
eq(integrations.type, "ai-gateway"),
eq(integrations.isManaged, true)
),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

The DELETE endpoint deletes an unpredictable managed integration when users have multiple, because it uses findFirst() without specifying which integration to delete. This causes data loss when users try to delete a specific managed integration.

View Details
📝 Patch Details
diff --git a/app/api/ai-gateway/consent/route.ts b/app/api/ai-gateway/consent/route.ts
index db8f4fa..3926f1b 100644
--- a/app/api/ai-gateway/consent/route.ts
+++ b/app/api/ai-gateway/consent/route.ts
@@ -214,6 +214,7 @@ export async function POST(request: Request) {
 /**
  * DELETE /api/ai-gateway/consent
  * Revoke consent and delete the API key
+ * Expects: { integrationId: string } in request body to specify which managed integration to delete
  */
 export async function DELETE(request: Request) {
   if (!isAiGatewayManagedKeysEnabled()) {
@@ -225,14 +226,38 @@ export async function DELETE(request: Request) {
     return Response.json({ error: "Not authenticated" }, { status: 401 });
   }
 
+  // Get integrationId from request body
+  let integrationId: string | null = null;
+  try {
+    const body = await request.json();
+    integrationId = body.integrationId;
+  } catch {
+    // If no body, will handle below
+  }
+
+  if (!integrationId) {
+    return Response.json(
+      { error: "integrationId is required" },
+      { status: 400 }
+    );
+  }
+
   const managedIntegration = await db.query.integrations.findFirst({
     where: and(
+      eq(integrations.id, integrationId),
       eq(integrations.userId, session.user.id),
       eq(integrations.type, "ai-gateway"),
       eq(integrations.isManaged, true)
     ),
   });
 
+  if (!managedIntegration) {
+    return Response.json(
+      { error: "Managed integration not found" },
+      { status: 404 }
+    );
+  }
+
   // Get managedKeyId and teamId from config (decrypt it first since it's stored encrypted)
   let config: { managedKeyId?: string; teamId?: string } | null = null;
   if (managedIntegration?.config) {
diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx
index ad89265..a7ece2e 100644
--- a/components/settings/integration-form-dialog.tsx
+++ b/components/settings/integration-form-dialog.tsx
@@ -764,7 +764,7 @@ export function IntegrationFormDialog({
 
       // If this is a managed connection and user wants to revoke the key
       if (integration.isManaged && revokeKey) {
-        await api.aiGateway.revokeConsent();
+        await api.aiGateway.revokeConsent(integration.id);
       } else {
         await api.integration.delete(integration.id);
       }
diff --git a/lib/api-client.ts b/lib/api-client.ts
index 73224e4..c5dc2c8 100644
--- a/lib/api-client.ts
+++ b/lib/api-client.ts
@@ -648,9 +648,10 @@ export const aiGatewayApi = {
     }),
 
   // Revoke consent and delete managed API key
-  revokeConsent: () =>
+  revokeConsent: (integrationId: string) =>
     apiCall<AiGatewayConsentResponse>("/api/ai-gateway/consent", {
       method: "DELETE",
+      body: JSON.stringify({ integrationId }),
     }),
 };
 

Analysis

DELETE endpoint uses unpredictable findFirst() when deleting managed AI Gateway integrations

What fails: DELETE /api/ai-gateway/consent endpoint (line 228-234) uses db.query.integrations.findFirst() without an integrationId filter to retrieve which managed integration to delete. When users have multiple managed integrations per team, this causes an unpredictable integration to be deleted instead of the intended one.

How to reproduce:

  1. Create user with Vercel account linked
  2. Create managed integration for Team A: POST /api/ai-gateway/consent with teamId=A
  3. Create managed integration for Team B: POST /api/ai-gateway/consent with teamId=B
  4. Delete Team A integration: DELETE /api/ai-gateway/consent (via integration-form-dialog.tsx handleDelete)
  5. Observe: Either Team A OR Team B integration may be deleted (unpredictable)

Result: Random managed integration deleted instead of the requested one. Database now has dangling Vercel API key for the unintended team.

Expected: Should delete only the specified managed integration. The DELETE endpoint should require integrationId to uniquely identify which of the user's managed integrations to revoke.

Why this is a bug:

  • PostgreSQL documentation explicitly states: LIMIT without ORDER BY produces unpredictable results - "When using LIMIT, it is important to use an ORDER BY clause... Otherwise you will get an unpredictable subset of the query's rows."
  • Code comment on line 105 explicitly confirms multiple managed integrations are supported: "users can have multiple managed keys for different teams"
  • Drizzle ORM's findFirst() adds LIMIT 1 to the query without ORDER BY, making row selection arbitrary

Fix applied:

  1. Modified DELETE endpoint to require integrationId in request body
  2. Updated query to filter by: integrationId AND userId AND type=ai-gateway AND isManaged=true
  3. Added validation to return 404 if integration not found (security check)
  4. Updated client API revokeConsent() to accept and pass integrationId
  5. Updated UI component handleDelete() to pass integration.id to revokeConsent()

This ensures only the correct managed integration is deleted, preventing data loss when users have multiple team integrations.

@ctate ctate merged commit 4afb194 into main Dec 15, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant