@@ -197,6 +197,26 @@ export type VercelEnvironmentVariableValue = {
197197 isSecret : boolean ;
198198} ;
199199
200+ /** Minimal shape of a shared (team-level) env var record from `GET /v1/env`. */
201+ const RawSharedEnvVarSchema = z
202+ . object ( {
203+ id : z . string ( ) . optional ( ) ,
204+ key : z . string ( ) . optional ( ) ,
205+ type : z . string ( ) . optional ( ) ,
206+ target : z . union ( [ z . array ( z . string ( ) ) , z . string ( ) ] ) . optional ( ) ,
207+ value : z . string ( ) . optional ( ) ,
208+ applyToAllCustomEnvironments : z . boolean ( ) . optional ( ) ,
209+ } )
210+ . passthrough ( ) ;
211+
212+ type RawSharedEnvVar = z . infer < typeof RawSharedEnvVarSchema > ;
213+
214+ /** Page shape of `GET /v1/env` (shared env vars), validated at the boundary. */
215+ const SharedEnvPageSchema = z . object ( {
216+ data : z . array ( RawSharedEnvVarSchema ) . default ( [ ] ) ,
217+ pagination : z . object ( { next : z . number ( ) . nullish ( ) } ) . nullish ( ) ,
218+ } ) ;
219+
200220/** Narrowed Vercel project type – only id and name. */
201221export type VercelProject = Pick < ResponseBodyProjects , "id" | "name" > ;
202222
@@ -298,6 +318,17 @@ export class VercelIntegrationRepository {
298318 static getVercelClient (
299319 integration : OrganizationIntegration & { tokenReference : SecretReference }
300320 ) : ResultAsync < Vercel , VercelApiError > {
321+ return this . getVercelClientAndToken ( integration ) . map ( ( { client } ) => client ) ;
322+ }
323+
324+ /**
325+ * Resolve both the Vercel SDK client and the raw bearer token. The raw token
326+ * is needed to paginate shared env vars via `fetch`, since the SDK's
327+ * `listSharedEnvVariable` exposes no `until` cursor param.
328+ */
329+ static getVercelClientAndToken (
330+ integration : OrganizationIntegration & { tokenReference : SecretReference }
331+ ) : ResultAsync < { client : Vercel ; accessToken : string } , VercelApiError > {
301332 return ResultAsync . fromPromise (
302333 ( async ( ) => {
303334 const secretStore = getSecretStore ( integration . tokenReference . provider ) ;
@@ -308,7 +339,7 @@ export class VercelIntegrationRepository {
308339 if ( ! secret ) {
309340 throw new Error ( "Failed to get Vercel access token" ) ;
310341 }
311- return new Vercel ( { bearerToken : secret . accessToken } ) ;
342+ return { client : new Vercel ( { bearerToken : secret . accessToken } ) , accessToken : secret . accessToken } ;
312343 } ) ( ) ,
313344 ( error ) => toVercelApiError ( error )
314345 ) ;
@@ -558,8 +589,68 @@ export class VercelIntegrationRepository {
558589 } ;
559590 }
560591
592+ /**
593+ * Fetch ALL shared (team-level) env var records, following pagination.
594+ *
595+ * Unlike the project env endpoint, the shared endpoint (`/v1/env`) DOES
596+ * paginate (≈25/page) and the SDK's `listSharedEnvVariable` exposes no cursor
597+ * param — so we walk pages via a raw fetch using `pagination.next` (a
598+ * millisecond-timestamp cursor) until it is null. Shared vars are an edge
599+ * case, so we load every page up front and return the full set.
600+ */
601+ static #fetchAllSharedEnvsRaw( params : {
602+ accessToken : string ;
603+ teamId : string ;
604+ projectId ?: string ;
605+ } ) : ResultAsync < RawSharedEnvVar [ ] , VercelApiError > {
606+ const { accessToken, teamId, projectId } = params ;
607+ return ResultAsync . fromPromise (
608+ ( async ( ) => {
609+ const all : RawSharedEnvVar [ ] = [ ] ;
610+ let until : number | undefined = undefined ;
611+ const MAX_PAGES = 200 ; // safety cap (1000-var ceiling / ~25 per page)
612+
613+ for ( let page = 0 ; page < MAX_PAGES ; page ++ ) {
614+ const url = new URL ( "https://api.vercel.com/v1/env" ) ;
615+ url . searchParams . set ( "teamId" , teamId ) ;
616+ if ( projectId ) url . searchParams . set ( "projectId" , projectId ) ;
617+ if ( until !== undefined ) url . searchParams . set ( "until" , String ( until ) ) ;
618+
619+ const response = await fetch ( url . toString ( ) , {
620+ method : "GET" ,
621+ headers : { Authorization : `Bearer ${ accessToken } ` } ,
622+ } ) ;
623+
624+ if ( ! response . ok ) {
625+ const body = await response . text ( ) . catch ( ( ) => "" ) ;
626+ const error = new Error (
627+ `Failed to fetch Vercel shared environment variables: ${ response . status } ${ response . statusText } — ${ body } `
628+ ) as Error & { status ?: number } ;
629+ error . status = response . status ;
630+ throw error ;
631+ }
632+
633+ const json = SharedEnvPageSchema . parse ( await response . json ( ) ) ;
634+ all . push ( ...json . data ) ;
635+
636+ // `next` is a millisecond-timestamp cursor; treat 0/null/undefined as "done".
637+ const next = json . pagination ?. next ;
638+ if ( ! next ) break ;
639+ until = next ;
640+
641+ if ( page === MAX_PAGES - 1 ) {
642+ logger . warn ( "Vercel shared env var pagination hit max page cap" , { teamId, projectId } ) ;
643+ }
644+ }
645+
646+ return all ;
647+ } ) ( ) ,
648+ ( error ) => toVercelApiError ( error )
649+ ) ;
650+ }
651+
561652 static getVercelSharedEnvironmentVariables (
562- client : Vercel ,
653+ accessToken : string ,
563654 teamId : string ,
564655 projectId ?: string // Optional: filter by project
565656 ) : ResultAsync < Array < {
@@ -569,19 +660,9 @@ export class VercelIntegrationRepository {
569660 isSecret : boolean ;
570661 target : string [ ] ;
571662 } > , VercelApiError > {
572- return wrapVercelCallWithRecovery (
573- client . environment . listSharedEnvVariable ( {
574- teamId,
575- ...( projectId && { projectId } ) ,
576- } ) ,
577- VercelSchemas . listSharedEnvVariable ,
578- "Failed to fetch Vercel shared environment variables" ,
579- { teamId, projectId } ,
580- toVercelApiError
581- ) . map ( ( response ) => {
582- const envVars = response . data || [ ] ;
663+ return this . #fetchAllSharedEnvsRaw( { accessToken, teamId, projectId } ) . map ( ( envVars ) => {
583664 return envVars
584- . filter ( ( env ) : env is typeof env & { id : string ; key : string } =>
665+ . filter ( ( env ) : env is RawSharedEnvVar & { id : string ; key : string } =>
585666 typeof env . id === "string" && typeof env . key === "string"
586667 )
587668 . map ( ( env ) => {
@@ -599,6 +680,7 @@ export class VercelIntegrationRepository {
599680
600681 static getVercelSharedEnvironmentVariableValues (
601682 client : Vercel ,
683+ accessToken : string ,
602684 teamId : string ,
603685 projectId ?: string // Optional: filter by project
604686 ) : ResultAsync <
@@ -612,17 +694,7 @@ export class VercelIntegrationRepository {
612694 } > ,
613695 VercelApiError
614696 > {
615- return wrapVercelCallWithRecovery (
616- client . environment . listSharedEnvVariable ( {
617- teamId,
618- ...( projectId && { projectId } ) ,
619- } ) ,
620- VercelSchemas . listSharedEnvVariable ,
621- "Failed to fetch Vercel shared environment variable values" ,
622- { teamId, projectId } ,
623- toVercelApiError
624- ) . andThen ( ( listResponse ) => {
625- const envVars = listResponse . data || [ ] ;
697+ return this . #fetchAllSharedEnvsRaw( { accessToken, teamId, projectId } ) . andThen ( ( envVars ) => {
626698 if ( envVars . length === 0 ) {
627699 return okAsync ( [ ] ) ;
628700 }
@@ -641,8 +713,8 @@ export class VercelIntegrationRepository {
641713
642714 if ( isSecret ) return null ;
643715
644- const listValue = ( env as any ) . value as string | undefined ;
645- const applyToAllCustomEnvs = ( env as any ) . applyToAllCustomEnvironments as boolean | undefined ;
716+ const listValue = env . value ;
717+ const applyToAllCustomEnvs = env . applyToAllCustomEnvironments ;
646718
647719 if ( listValue ) {
648720 return {
@@ -1201,7 +1273,7 @@ export class VercelIntegrationRepository {
12011273 syncEnvVarsMappingKeys : Object . keys ( params . syncEnvVarsMapping ) ,
12021274 } ) ;
12031275
1204- return this . getVercelClient ( params . orgIntegration ) . andThen ( ( client ) =>
1276+ return this . getVercelClientAndToken ( params . orgIntegration ) . andThen ( ( { client, accessToken } ) =>
12051277 ResultAsync . fromPromise (
12061278 ( async ( ) => {
12071279 const errors : string [ ] = [ ] ;
@@ -1267,6 +1339,7 @@ export class VercelIntegrationRepository {
12671339 if ( params . teamId ) {
12681340 const sharedResult = await this . getVercelSharedEnvironmentVariableValues (
12691341 client ,
1342+ accessToken ,
12701343 params . teamId ,
12711344 params . vercelProjectId
12721345 ) ;
0 commit comments