From e877a4c9e97829218f6ec13858739d6ae7aeee6c Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Thu, 10 Apr 2025 09:20:18 -0300 Subject: [PATCH] Improve vercer secret sync integration --- .../vercel/vercel-connection-fns.ts | 4 +- .../vercel/vercel-connection-schemas.ts | 5 +- .../secret-sync/vercel/vercel-sync-fns.ts | 144 +++++++++++------- docs/integrations/secret-syncs/vercel.mdx | 15 +- .../VercelSyncFields.tsx | 1 + 5 files changed, 106 insertions(+), 63 deletions(-) diff --git a/backend/src/services/app-connection/vercel/vercel-connection-fns.ts b/backend/src/services/app-connection/vercel/vercel-connection-fns.ts index daf7a713f..43de8641d 100644 --- a/backend/src/services/app-connection/vercel/vercel-connection-fns.ts +++ b/backend/src/services/app-connection/vercel/vercel-connection-fns.ts @@ -9,8 +9,8 @@ import { IntegrationUrls } from "@app/services/integration-auth/integration-list import { VercelConnectionMethod } from "./vercel-connection-enums"; import { + TVercelConnection, TVercelConnectionConfig, - TVercelConnectionInput, VercelApp, VercelEnvironment, VercelOrgWithApps @@ -184,7 +184,7 @@ type VercelUserResponse = { }; }; -export const listProjects = async (appConnection: TVercelConnectionInput): Promise => { +export const listProjects = async (appConnection: TVercelConnection): Promise => { const { credentials } = appConnection; const { apiToken } = credentials; diff --git a/backend/src/services/app-connection/vercel/vercel-connection-schemas.ts b/backend/src/services/app-connection/vercel/vercel-connection-schemas.ts index 12d730da8..60baa4f5c 100644 --- a/backend/src/services/app-connection/vercel/vercel-connection-schemas.ts +++ b/backend/src/services/app-connection/vercel/vercel-connection-schemas.ts @@ -11,12 +11,11 @@ import { import { VercelConnectionMethod } from "./vercel-connection-enums"; export const VercelConnectionAccessTokenCredentialsSchema = z.object({ - apiToken: z.string().trim().min(1, "API Token required") + apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.VERCEL.apiToken) }); const BaseVercelConnectionSchema = BaseAppConnectionSchema.extend({ - app: z.literal(AppConnection.Vercel), - isPlatformManagedCredentials: z.boolean().optional() + app: z.literal(AppConnection.Vercel) }); export const VercelConnectionSchema = BaseVercelConnectionSchema.extend({ diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts index 2a1341c16..713971283 100644 --- a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts +++ b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts @@ -15,10 +15,13 @@ const MAX_RETRIES = 5; const sleep = async () => new Promise((resolve) => { - setTimeout(resolve, 1000); + setTimeout(resolve, 60000); }); -const getVercelSecrets = async (secretSync: TVercelSyncWithCredentials, attempt = 0): Promise => { +const getVercelSecretsWithRetries = async ( + secretSync: TVercelSyncWithCredentials, + attempt = 0 +): Promise => { const { destinationConfig, connection: { @@ -41,59 +44,96 @@ const getVercelSecrets = async (secretSync: TVercelSyncWithCredentials, attempt } } ); - - const filteredSecrets = data.envs.filter((secret) => { - if (!isVercelDefaultEnvType(destinationConfig.env)) { - if (secret.customEnvironmentIds?.includes(destinationConfig.env)) { - return true; - } - return false; - } - if (secret.target.includes(destinationConfig.env)) { - // If it's preview environment with a branch specified - if ( - destinationConfig.env === VercelEnvironmentType.Preview && - destinationConfig.branch && - secret.gitBranch && - secret.gitBranch !== destinationConfig.branch - ) { - return false; - } - return true; - } - return false; - }); - - // For secrets of type "encrypted", we need to get their decrypted value - const secretsWithValues = await Promise.all( - filteredSecrets.map(async (secret) => { - if (secret.type === "encrypted") { - const { data: decryptedSecret } = await request.get( - `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${secret.id}?teamId=${destinationConfig.teamId}`, - { - params, - headers: { - Authorization: `Bearer ${apiToken}`, - "Accept-Encoding": "application/json" - } - } - ); - return decryptedSecret as VercelApiSecret; - } - return secret; - }) - ); - - return secretsWithValues; + return data.envs; } catch (error) { - if ((error as { code: string }).code === "rate_limited" && attempt < MAX_RETRIES) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { await sleep(); - return await getVercelSecrets(secretSync, attempt + 1); + return await getVercelSecretsWithRetries(secretSync, attempt + 1); } throw error; } }; +const getDecryptedVercelSecret = async ( + secretSync: TVercelSyncWithCredentials, + secret: VercelApiSecret, + attempt = 0 +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + const params: { [key: string]: string } = { + decrypt: "true", + ...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {}) + }; + + try { + const { data: decryptedSecret } = await request.get( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${secret.id}?teamId=${destinationConfig.teamId}`, + { + params, + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + return decryptedSecret as VercelApiSecret; + } catch (error) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { + await sleep(); + return await getDecryptedVercelSecret(secretSync, secret, attempt + 1); + } + throw error; + } +}; + +const getVercelSecrets = async (secretSync: TVercelSyncWithCredentials): Promise => { + const { destinationConfig } = secretSync; + + const secrets = await getVercelSecretsWithRetries(secretSync); + + const filteredSecrets = secrets.filter((secret) => { + if (!isVercelDefaultEnvType(destinationConfig.env)) { + if (secret.customEnvironmentIds?.includes(destinationConfig.env)) { + return true; + } + return false; + } + if (secret.target.includes(destinationConfig.env)) { + // If it's preview environment with a branch specified + if ( + destinationConfig.env === VercelEnvironmentType.Preview && + destinationConfig.branch && + secret.gitBranch && + secret.gitBranch !== destinationConfig.branch + ) { + return false; + } + return true; + } + return false; + }); + + // For secrets of type "encrypted", we need to get their decrypted value + const secretsWithValues = await Promise.all( + filteredSecrets.map(async (secret) => { + if (secret.type === "encrypted") { + const decryptedSecret = await getDecryptedVercelSecret(secretSync, secret); + return decryptedSecret; + } + return secret; + }) + ); + + return secretsWithValues; +}; + const deleteSecret = async ( secretSync: TVercelSyncWithCredentials, vercelSecret: VercelApiSecret, @@ -117,7 +157,7 @@ const deleteSecret = async ( } ); } catch (error) { - if ((error as { code: string }).code === "rate_limited" && attempt < MAX_RETRIES) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { await sleep(); return await deleteSecret(secretSync, vercelSecret, attempt + 1); } @@ -162,7 +202,7 @@ const createSecret = async ( } ); } catch (error) { - if ((error as { code: string }).code === "rate_limited" && attempt < MAX_RETRIES) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { await sleep(); return await createSecret(secretSync, secretMap, key, attempt + 1); } @@ -202,7 +242,7 @@ const updateSecret = async ( await request.patch( `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}?teamId=${destinationConfig.teamId}`, { - key: vercelSecret.key, + ...(vercelSecret.type !== "sensitive" && { key: vercelSecret.key }), value: secretMap[vercelSecret.key].value, type: vercelSecret.type, target, @@ -219,7 +259,7 @@ const updateSecret = async ( } ); } catch (error) { - if ((error as { code: string }).code === "rate_limited" && attempt < MAX_RETRIES) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { await sleep(); return await updateSecret(secretSync, secretMap, vercelSecret, attempt + 1); } diff --git a/docs/integrations/secret-syncs/vercel.mdx b/docs/integrations/secret-syncs/vercel.mdx index 735877f8e..593874dee 100644 --- a/docs/integrations/secret-syncs/vercel.mdx +++ b/docs/integrations/secret-syncs/vercel.mdx @@ -27,11 +27,12 @@ description: "Learn how to configure a Vercel Sync for Infisical." 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/vercel/vercel-destination.png) - - **Vercel Connection**: The Vercel Connection to authenticate with. - - **Vercel App**: The application to deploy secrets to. - - **Vercel App Environment**: The environment to deploy secrets to. - - **Vercel Preview Branch (Optional)**: Specify a branch for preview deployments if needed. + - **Vercel Connection**: The Vercel Connection to authenticate with. + - **Vercel App**: The application to deploy secrets to. + - **Vercel App Environment**: The environment to deploy secrets to. + - **Vercel Preview Branch (Optional)**: Specify a branch for preview deployments if needed. After configuring these parameters, click the **Next** button to continue to the Sync Options step. @@ -82,7 +83,8 @@ description: "Learn how to configure a Vercel Sync for Infisical." "app": "prj_bz7zgHvQETPvJWc5tmIr0tGRH9kE", "env": "preview", "branch": "test", - "appName": "nextjs-boilerplate" + "appName": "nextjs-boilerplate", + "teamId": "team_0d444b5088888dd257" } }' ``` @@ -136,7 +138,8 @@ description: "Learn how to configure a Vercel Sync for Infisical." "app": "prj_bz7zgHvQETPvJWc5tmIr0tGRH9kE", "env": "preview", "branch": "test", - "appName": "nextjs-boilerplate" + "appName": "nextjs-boilerplate", + "teamId": "team_0d444b5088888dd257" } } } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx index 708284279..5c328079d 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx @@ -184,6 +184,7 @@ export const VercelSyncFields = () => { placeholder="Select a branch..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option?.id || ""} + isClearable /> )}