Skip to content

Commit 2cd89b0

Browse files
committed
feat(vercel): pass environment id and centralize OAuth exchange
Update onboarding flow to propagate the selected Vercel environment when reloading data and move the OAuth token exchange into the VercelIntegration repository. - Add vercelEnvironmentId parameter to onDataReload and include it in the vercelFetcher load URL so the UI can reload with the specific environment context. - Remove local exchangeCodeForToken implementation from the route and call VercelIntegrationRepository.exchangeCodeForToken instead to centralize OAuth token exchange logic. - Rework redirects and settings path handling to compute settingsPath earlier and reuse it consistently on success and error paths. - Import env into the vercel integration model file (prepares use of configuration from env.server). These changes improve consistency, reduce duplicated OAuth code, and ensure environment-specific reloads work correctly.
1 parent 3104e7e commit 2cd89b0

File tree

6 files changed

+287
-238
lines changed

6 files changed

+287
-238
lines changed

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

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "@trigger.dev/database";
88
import { z } from "zod";
99
import { $transaction, prisma } from "~/db.server";
10+
import { env } from "~/env.server";
1011
import { logger } from "~/services/logger.server";
1112
import { getSecretStore } from "~/services/secrets/secretStore.server";
1213
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
@@ -43,6 +44,14 @@ export const VercelSecretSchema = z.object({
4344

4445
export type VercelSecret = z.infer<typeof VercelSecretSchema>;
4546

47+
export type TokenResponse = {
48+
accessToken: string;
49+
tokenType: string;
50+
teamId?: string;
51+
userId?: string;
52+
raw: Record<string, unknown>;
53+
};
54+
4655
export type VercelEnvironmentVariable = {
4756
id: string;
4857
key: string;
@@ -71,26 +80,101 @@ export type VercelAPIResult<T> = {
7180
error: string;
7281
};
7382

74-
function isVercelAuthError(error: unknown): boolean {
83+
const VercelErrorSchema = z.union([
84+
z.object({ status: z.number() }),
85+
z.object({ response: z.object({ status: z.number() }) }),
86+
z.object({ statusCode: z.number() }),
87+
]);
88+
89+
function extractVercelErrorStatus(error: unknown): number | null {
7590
if (error && typeof error === 'object' && 'status' in error) {
76-
const status = (error as { status?: number }).status;
77-
return status === 401 || status === 403;
91+
const parsed = VercelErrorSchema.safeParse(error);
92+
if (parsed.success && 'status' in parsed.data) {
93+
return parsed.data.status;
94+
}
7895
}
96+
7997
if (error && typeof error === 'object' && 'response' in error) {
80-
const response = (error as { response?: { status?: number } }).response;
81-
return response?.status === 401 || response?.status === 403;
98+
const parsed = VercelErrorSchema.safeParse(error);
99+
if (parsed.success && 'response' in parsed.data) {
100+
return parsed.data.response.status;
101+
}
82102
}
103+
83104
if (error && typeof error === 'object' && 'statusCode' in error) {
84-
const statusCode = (error as { statusCode?: number }).statusCode;
85-
return statusCode === 401 || statusCode === 403;
105+
const parsed = VercelErrorSchema.safeParse(error);
106+
if (parsed.success && 'statusCode' in parsed.data) {
107+
return parsed.data.statusCode;
108+
}
86109
}
87-
if (error && typeof error === 'string' && (error.includes('401') || error.includes('403'))) {
88-
return true;
110+
111+
if (typeof error === 'string') {
112+
if (error.includes('401')) return 401;
113+
if (error.includes('403')) return 403;
89114
}
90-
return false;
115+
116+
return null;
117+
}
118+
119+
function isVercelAuthError(error: unknown): boolean {
120+
const status = extractVercelErrorStatus(error);
121+
return status === 401 || status === 403;
91122
}
92123

93124
export class VercelIntegrationRepository {
125+
static async exchangeCodeForToken(code: string): Promise<TokenResponse | null> {
126+
const clientId = env.VERCEL_INTEGRATION_CLIENT_ID;
127+
const clientSecret = env.VERCEL_INTEGRATION_CLIENT_SECRET;
128+
const redirectUri = `${env.APP_ORIGIN}/vercel/callback`;
129+
130+
if (!clientId || !clientSecret) {
131+
logger.error("Vercel integration not configured");
132+
return null;
133+
}
134+
135+
try {
136+
const response = await fetch("https://api.vercel.com/v2/oauth/access_token", {
137+
method: "POST",
138+
headers: {
139+
"Content-Type": "application/x-www-form-urlencoded",
140+
},
141+
body: new URLSearchParams({
142+
client_id: clientId,
143+
client_secret: clientSecret,
144+
code,
145+
redirect_uri: redirectUri,
146+
}),
147+
});
148+
149+
if (!response.ok) {
150+
const errorText = await response.text();
151+
logger.error("Failed to exchange Vercel OAuth code", {
152+
status: response.status,
153+
error: errorText,
154+
});
155+
return null;
156+
}
157+
158+
const data = (await response.json()) as {
159+
access_token: string;
160+
token_type: string;
161+
team_id?: string;
162+
user_id?: string;
163+
};
164+
165+
return {
166+
accessToken: data.access_token,
167+
tokenType: data.token_type,
168+
teamId: data.team_id,
169+
userId: data.user_id,
170+
raw: data as Record<string, unknown>,
171+
};
172+
} catch (error) {
173+
logger.error("Error exchanging Vercel OAuth code", { error });
174+
return null;
175+
}
176+
}
177+
94178
static async getVercelClient(
95179
integration: OrganizationIntegration & { tokenReference: SecretReference }
96180
): Promise<Vercel> {
@@ -253,7 +337,7 @@ export class VercelIntegrationRepository {
253337
static async getVercelEnvironmentVariables(
254338
client: Vercel,
255339
projectId: string,
256-
teamId?: string | null
340+
teamId?: string | null,
257341
): Promise<VercelAPIResult<VercelEnvironmentVariable[]>> {
258342
try {
259343
const response = await client.projects.filterProjectEnvs({
@@ -277,6 +361,7 @@ export class VercelIntegrationRepository {
277361
type,
278362
isSecret,
279363
target: normalizeTarget(env.target),
364+
customEnvironmentIds: env.customEnvironmentIds as string[] ?? [],
280365
};
281366
}),
282367
};

0 commit comments

Comments
 (0)