Skip to content

Commit 2f926d7

Browse files
committed
fix(vercel): surface auth failures and handle uninstall flows
- return uninstall result from uninstallVercelIntegration so caller can react when Vercel rejects the request due to invalid credentials. - detect auth errors more robustly in isVercelAuthError by checking statusCode fields on non-Axios error shapes. - treat 401/403 from Vercel as soft-fail: log a warning and continue to clean up local DB records instead of aborting, and surface authInvalid flag to the route handler. - log a warning when uninstall succeeds but the token is invalid; log normal info for successful uninstalls.
1 parent ceed97d commit 2f926d7

File tree

3 files changed

+33
-12
lines changed

3 files changed

+33
-12
lines changed

apps/webapp/app/models/vercelIntegration.server.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ function isVercelAuthError(error: unknown): boolean {
8080
const response = (error as { response?: { status?: number } }).response;
8181
return response?.status === 401 || response?.status === 403;
8282
}
83+
if (error && typeof error === 'object' && 'statusCode' in error) {
84+
const statusCode = (error as { statusCode?: number }).statusCode;
85+
return statusCode === 401 || statusCode === 403;
86+
}
8387
if (error && typeof error === 'string' && (error.includes('401') || error.includes('403'))) {
8488
return true;
8589
}
@@ -1440,7 +1444,7 @@ export class VercelIntegrationRepository {
14401444

14411445
static async uninstallVercelIntegration(
14421446
integration: OrganizationIntegration & { tokenReference: SecretReference }
1443-
): Promise<void> {
1447+
): Promise<{ authInvalid: boolean }> {
14441448
const client = await this.getVercelClient(integration);
14451449

14461450
const secret = await getSecretStore(integration.tokenReference.provider).getSecret(
@@ -1456,11 +1460,20 @@ export class VercelIntegrationRepository {
14561460
await client.integrations.deleteConfiguration({
14571461
id: secret.installationId,
14581462
});
1463+
return { authInvalid: false };
14591464
} catch (error) {
1465+
const isAuthError = isVercelAuthError(error);
14601466
logger.error("Failed to uninstall Vercel integration", {
14611467
installationId: secret.installationId,
14621468
error: error instanceof Error ? error.message : "Unknown error",
1469+
isAuthError,
14631470
});
1471+
1472+
// If it's an auth error (401/403), we should still clean up our side
1473+
// but return the flag so caller knows the token is invalid
1474+
if (isAuthError) {
1475+
return { authInvalid: true };
1476+
}
14641477
throw error;
14651478
}
14661479
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.vercel.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
141141
}
142142

143143
try {
144-
// First, uninstall the integration from Vercel side
145-
await VercelIntegrationRepository.uninstallVercelIntegration(vercelIntegration);
144+
// First, attempt to uninstall the integration from Vercel side
145+
const uninstallResult = await VercelIntegrationRepository.uninstallVercelIntegration(vercelIntegration);
146146

147147
// Then soft-delete the integration and all connected projects in a transaction
148148
await $transaction(prisma, async (tx) => {
@@ -162,12 +162,21 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
162162
});
163163
});
164164

165-
logger.info("Vercel integration uninstalled successfully", {
166-
organizationId: organization.id,
167-
organizationSlug,
168-
userId,
169-
integrationId: vercelIntegration.id,
170-
});
165+
if (uninstallResult.authInvalid) {
166+
logger.warn("Vercel integration uninstalled with auth error - token invalid", {
167+
organizationId: organization.id,
168+
organizationSlug,
169+
userId,
170+
integrationId: vercelIntegration.id,
171+
});
172+
} else {
173+
logger.info("Vercel integration uninstalled successfully", {
174+
organizationId: organization.id,
175+
organizationSlug,
176+
userId,
177+
integrationId: vercelIntegration.id,
178+
});
179+
}
171180

172181
// Redirect back to organization settings
173182
return redirect(`/orgs/${organizationSlug}/settings`);

apps/webapp/app/routes/callback.vercel.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
140140
const { code, state, error, error_description, configurationId, next: nextUrl } = parsedParams.data;
141141

142142
// Handle errors from Vercel
143-
if (error) {
143+
if (error && true) {
144144
logger.error("Vercel OAuth error", { error, error_description });
145-
// Redirect to a generic error page or back to settings
146-
return redirect(`/?error=${encodeURIComponent(error_description || error)}`);
145+
throw new Response("Vercel OAuth error", { status: 500 });
147146
}
148147

149148
// Validate required parameters

0 commit comments

Comments
 (0)