-
Notifications
You must be signed in to change notification settings - Fork 141
ai gateway managed keys #164
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| const managedIntegration = await db.query.integrations.findFirst({ | ||
| where: and( | ||
| eq(integrations.userId, session.user.id), | ||
| eq(integrations.type, "ai-gateway"), | ||
| eq(integrations.isManaged, true) | ||
| ), | ||
| }); |
There was a problem hiding this comment.
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:
- Create user with Vercel account linked
- Create managed integration for Team A: POST /api/ai-gateway/consent with teamId=A
- Create managed integration for Team B: POST /api/ai-gateway/consent with teamId=B
- Delete Team A integration: DELETE /api/ai-gateway/consent (via integration-form-dialog.tsx handleDelete)
- 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:
- Modified DELETE endpoint to require integrationId in request body
- Updated query to filter by: integrationId AND userId AND type=ai-gateway AND isManaged=true
- Added validation to return 404 if integration not found (security check)
- Updated client API revokeConsent() to accept and pass integrationId
- 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.
No description provided.