Improve vercer secret sync integration

This commit is contained in:
carlosmonastyrski
2025-04-10 09:20:18 -03:00
parent ed5c18b5ac
commit e877a4c9e9
5 changed files with 106 additions and 63 deletions

View File

@@ -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<VercelOrgWithApps[]> => {
export const listProjects = async (appConnection: TVercelConnection): Promise<VercelOrgWithApps[]> => {
const { credentials } = appConnection;
const { apiToken } = credentials;

View File

@@ -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({

View File

@@ -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<VercelApiSecret[]> => {
const getVercelSecretsWithRetries = async (
secretSync: TVercelSyncWithCredentials,
attempt = 0
): Promise<VercelApiSecret[]> => {
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<VercelApiSecret> => {
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<VercelApiSecret[]> => {
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);
}

View File

@@ -27,11 +27,12 @@ description: "Learn how to configure a Vercel Sync for Infisical."
</Tip>
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"
}
}
}

View File

@@ -184,6 +184,7 @@ export const VercelSyncFields = () => {
placeholder="Select a branch..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option?.id || ""}
isClearable
/>
</FormControl>
)}