@@ -197,6 +197,16 @@ export type VercelEnvironmentVariableValue = {
197197 isSecret : boolean ;
198198} ;
199199
200+ /** Minimal shape of a shared (team-level) env var record from `GET /v1/env`. */
201+ type RawSharedEnvVar = {
202+ id ?: string ;
203+ key ?: string ;
204+ type ?: string ;
205+ target ?: string [ ] | string ;
206+ value ?: string ;
207+ applyToAllCustomEnvironments ?: boolean ;
208+ } ;
209+
200210/** Narrowed Vercel project type – only id and name. */
201211export type VercelProject = Pick < ResponseBodyProjects , "id" | "name" > ;
202212
@@ -298,6 +308,17 @@ export class VercelIntegrationRepository {
298308 static getVercelClient (
299309 integration : OrganizationIntegration & { tokenReference : SecretReference }
300310 ) : ResultAsync < Vercel , VercelApiError > {
311+ return this . getVercelClientAndToken ( integration ) . map ( ( { client } ) => client ) ;
312+ }
313+
314+ /**
315+ * Resolve both the Vercel SDK client and the raw bearer token. The raw token
316+ * is needed to paginate shared env vars via `fetch`, since the SDK's
317+ * `listSharedEnvVariable` exposes no `until` cursor param.
318+ */
319+ static getVercelClientAndToken (
320+ integration : OrganizationIntegration & { tokenReference : SecretReference }
321+ ) : ResultAsync < { client : Vercel ; accessToken : string } , VercelApiError > {
301322 return ResultAsync . fromPromise (
302323 ( async ( ) => {
303324 const secretStore = getSecretStore ( integration . tokenReference . provider ) ;
@@ -308,7 +329,7 @@ export class VercelIntegrationRepository {
308329 if ( ! secret ) {
309330 throw new Error ( "Failed to get Vercel access token" ) ;
310331 }
311- return new Vercel ( { bearerToken : secret . accessToken } ) ;
332+ return { client : new Vercel ( { bearerToken : secret . accessToken } ) , accessToken : secret . accessToken } ;
312333 } ) ( ) ,
313334 ( error ) => toVercelApiError ( error )
314335 ) ;
@@ -558,8 +579,71 @@ export class VercelIntegrationRepository {
558579 } ;
559580 }
560581
582+ /**
583+ * Fetch ALL shared (team-level) env var records, following pagination.
584+ *
585+ * Unlike the project env endpoint, the shared endpoint (`/v1/env`) DOES
586+ * paginate (≈25/page) and the SDK's `listSharedEnvVariable` exposes no cursor
587+ * param — so we walk pages via a raw fetch using `pagination.next` (a
588+ * millisecond-timestamp cursor) until it is null. Shared vars are an edge
589+ * case, so we load every page up front and return the full set.
590+ */
591+ static #fetchAllSharedEnvsRaw( params : {
592+ accessToken : string ;
593+ teamId : string ;
594+ projectId ?: string ;
595+ } ) : ResultAsync < RawSharedEnvVar [ ] , VercelApiError > {
596+ const { accessToken, teamId, projectId } = params ;
597+ return ResultAsync . fromPromise (
598+ ( async ( ) => {
599+ const all : RawSharedEnvVar [ ] = [ ] ;
600+ let until : number | undefined = undefined ;
601+ const MAX_PAGES = 200 ; // safety cap (1000-var ceiling / ~25 per page)
602+
603+ for ( let page = 0 ; page < MAX_PAGES ; page ++ ) {
604+ const url = new URL ( "https://api.vercel.com/v1/env" ) ;
605+ url . searchParams . set ( "teamId" , teamId ) ;
606+ if ( projectId ) url . searchParams . set ( "projectId" , projectId ) ;
607+ if ( until !== undefined ) url . searchParams . set ( "until" , String ( until ) ) ;
608+
609+ const response = await fetch ( url . toString ( ) , {
610+ method : "GET" ,
611+ headers : { Authorization : `Bearer ${ accessToken } ` } ,
612+ } ) ;
613+
614+ if ( ! response . ok ) {
615+ const body = await response . text ( ) . catch ( ( ) => "" ) ;
616+ const error = new Error (
617+ `Failed to fetch Vercel shared environment variables: ${ response . status } ${ response . statusText } — ${ body } `
618+ ) as Error & { status ?: number } ;
619+ error . status = response . status ;
620+ throw error ;
621+ }
622+
623+ const json = ( await response . json ( ) ) as {
624+ data ?: RawSharedEnvVar [ ] ;
625+ pagination ?: { next ?: number | null } | null ;
626+ } ;
627+ all . push ( ...( json . data ?? [ ] ) ) ;
628+
629+ // `next` is a millisecond-timestamp cursor; treat 0/null/undefined as "done".
630+ const next = json . pagination ?. next ;
631+ if ( ! next ) break ;
632+ until = next ;
633+
634+ if ( page === MAX_PAGES - 1 ) {
635+ logger . warn ( "Vercel shared env var pagination hit max page cap" , { teamId, projectId } ) ;
636+ }
637+ }
638+
639+ return all ;
640+ } ) ( ) ,
641+ ( error ) => toVercelApiError ( error )
642+ ) ;
643+ }
644+
561645 static getVercelSharedEnvironmentVariables (
562- client : Vercel ,
646+ accessToken : string ,
563647 teamId : string ,
564648 projectId ?: string // Optional: filter by project
565649 ) : ResultAsync < Array < {
@@ -569,19 +653,9 @@ export class VercelIntegrationRepository {
569653 isSecret : boolean ;
570654 target : string [ ] ;
571655 } > , 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 || [ ] ;
656+ return this . #fetchAllSharedEnvsRaw( { accessToken, teamId, projectId } ) . map ( ( envVars ) => {
583657 return envVars
584- . filter ( ( env ) : env is typeof env & { id : string ; key : string } =>
658+ . filter ( ( env ) : env is RawSharedEnvVar & { id : string ; key : string } =>
585659 typeof env . id === "string" && typeof env . key === "string"
586660 )
587661 . map ( ( env ) => {
@@ -599,6 +673,7 @@ export class VercelIntegrationRepository {
599673
600674 static getVercelSharedEnvironmentVariableValues (
601675 client : Vercel ,
676+ accessToken : string ,
602677 teamId : string ,
603678 projectId ?: string // Optional: filter by project
604679 ) : ResultAsync <
@@ -612,17 +687,7 @@ export class VercelIntegrationRepository {
612687 } > ,
613688 VercelApiError
614689 > {
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 || [ ] ;
690+ return this . #fetchAllSharedEnvsRaw( { accessToken, teamId, projectId } ) . andThen ( ( envVars ) => {
626691 if ( envVars . length === 0 ) {
627692 return okAsync ( [ ] ) ;
628693 }
@@ -641,8 +706,8 @@ export class VercelIntegrationRepository {
641706
642707 if ( isSecret ) return null ;
643708
644- const listValue = ( env as any ) . value as string | undefined ;
645- const applyToAllCustomEnvs = ( env as any ) . applyToAllCustomEnvironments as boolean | undefined ;
709+ const listValue = env . value ;
710+ const applyToAllCustomEnvs = env . applyToAllCustomEnvironments ;
646711
647712 if ( listValue ) {
648713 return {
@@ -1201,7 +1266,7 @@ export class VercelIntegrationRepository {
12011266 syncEnvVarsMappingKeys : Object . keys ( params . syncEnvVarsMapping ) ,
12021267 } ) ;
12031268
1204- return this . getVercelClient ( params . orgIntegration ) . andThen ( ( client ) =>
1269+ return this . getVercelClientAndToken ( params . orgIntegration ) . andThen ( ( { client, accessToken } ) =>
12051270 ResultAsync . fromPromise (
12061271 ( async ( ) => {
12071272 const errors : string [ ] = [ ] ;
@@ -1267,6 +1332,7 @@ export class VercelIntegrationRepository {
12671332 if ( params . teamId ) {
12681333 const sharedResult = await this . getVercelSharedEnvironmentVariableValues (
12691334 client ,
1335+ accessToken ,
12701336 params . teamId ,
12711337 params . vercelProjectId
12721338 ) ;
0 commit comments