77} from "@trigger.dev/database" ;
88import { z } from "zod" ;
99import { $transaction , prisma } from "~/db.server" ;
10+ import { env } from "~/env.server" ;
1011import { logger } from "~/services/logger.server" ;
1112import { getSecretStore } from "~/services/secrets/secretStore.server" ;
1213import { generateFriendlyId } from "~/v3/friendlyIdentifiers" ;
@@ -43,6 +44,14 @@ export const VercelSecretSchema = z.object({
4344
4445export 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+
4655export 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
93124export 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