mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
Add secret sync vercel integration
This commit is contained in:
@@ -6,7 +6,8 @@ export enum SecretSync {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Databricks = "databricks",
|
||||
Humanitec = "humanitec"
|
||||
Humanitec = "humanitec",
|
||||
Vercel = "vercel"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
||||
|
||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
@@ -35,7 +36,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION,
|
||||
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
|
||||
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION
|
||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -121,6 +123,8 @@ export const SecretSyncFns = {
|
||||
}).syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.Humanitec:
|
||||
return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.Vercel:
|
||||
return VercelSyncFns.syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -165,6 +169,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Humanitec:
|
||||
secretMap = await HumanitecSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Vercel:
|
||||
secretMap = await VercelSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -207,6 +214,8 @@ export const SecretSyncFns = {
|
||||
}).removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.Humanitec:
|
||||
return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.Vercel:
|
||||
return VercelSyncFns.removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -9,7 +9,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.AzureKeyVault]: "Azure Key Vault",
|
||||
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[SecretSync.Databricks]: "Databricks",
|
||||
[SecretSync.Humanitec]: "Humanitec"
|
||||
[SecretSync.Humanitec]: "Humanitec",
|
||||
[SecretSync.Vercel]: "Vercel"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -20,5 +21,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||
[SecretSync.Humanitec]: AppConnection.Humanitec
|
||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||
[SecretSync.Vercel]: AppConnection.Vercel
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
THumanitecSyncListItem,
|
||||
THumanitecSyncWithCredentials
|
||||
} from "./humanitec";
|
||||
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
|
||||
|
||||
export type TSecretSync =
|
||||
| TAwsParameterStoreSync
|
||||
@@ -58,7 +59,8 @@ export type TSecretSync =
|
||||
| TAzureKeyVaultSync
|
||||
| TAzureAppConfigurationSync
|
||||
| TDatabricksSync
|
||||
| THumanitecSync;
|
||||
| THumanitecSync
|
||||
| TVercelSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -68,7 +70,8 @@ export type TSecretSyncWithCredentials =
|
||||
| TAzureKeyVaultSyncWithCredentials
|
||||
| TAzureAppConfigurationSyncWithCredentials
|
||||
| TDatabricksSyncWithCredentials
|
||||
| THumanitecSyncWithCredentials;
|
||||
| THumanitecSyncWithCredentials
|
||||
| TVercelSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -78,7 +81,8 @@ export type TSecretSyncInput =
|
||||
| TAzureKeyVaultSyncInput
|
||||
| TAzureAppConfigurationSyncInput
|
||||
| TDatabricksSyncInput
|
||||
| THumanitecSyncInput;
|
||||
| THumanitecSyncInput
|
||||
| TVercelSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -88,7 +92,8 @@ export type TSecretSyncListItem =
|
||||
| TAzureKeyVaultSyncListItem
|
||||
| TAzureAppConfigurationSyncListItem
|
||||
| TDatabricksSyncListItem
|
||||
| THumanitecSyncListItem;
|
||||
| THumanitecSyncListItem
|
||||
| TVercelSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./vercel-sync-constants";
|
||||
export * from "./vercel-sync-enums";
|
||||
export * from "./vercel-sync-fns";
|
||||
export * from "./vercel-sync-schemas";
|
||||
export * from "./vercel-sync-types";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const VERCEL_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Vercel",
|
||||
destination: SecretSync.Vercel,
|
||||
connection: AppConnection.Vercel,
|
||||
canImportSecrets: false
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export enum VercelSyncScope {
|
||||
Application = "application",
|
||||
Environment = "environment"
|
||||
}
|
||||
|
||||
export const VercelEnvironmentType = {
|
||||
Development: "development",
|
||||
Preview: "preview",
|
||||
Production: "production"
|
||||
} as const;
|
||||
|
||||
export type VercelEnvironment = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType];
|
||||
@@ -0,0 +1,222 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { VercelEnvironmentType } from "./vercel-sync-enums";
|
||||
import { TVercelSyncWithCredentials, VercelApiSecret } from "./vercel-sync-types";
|
||||
|
||||
const getVercelSecrets = async (secretSync: TVercelSyncWithCredentials) => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const params: { [key: string]: string } = {
|
||||
decrypt: "true",
|
||||
...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {})
|
||||
};
|
||||
|
||||
const { data } = await request.get<{ envs: VercelApiSecret[] }>(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const filteredSecrets = data.envs.filter((secret) => {
|
||||
// For environment-specific filtering
|
||||
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}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
return decryptedSecret as VercelApiSecret;
|
||||
}
|
||||
return secret;
|
||||
})
|
||||
);
|
||||
|
||||
return secretsWithValues;
|
||||
};
|
||||
|
||||
const deleteSecret = async (secretSync: TVercelSyncWithCredentials, vercelSecret: VercelApiSecret) => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
try {
|
||||
await request.delete(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: vercelSecret.key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createSecret = async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap, key: string) => {
|
||||
try {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${destinationConfig.app}/env`,
|
||||
{
|
||||
key,
|
||||
value: secretMap[key].value,
|
||||
type: "encrypted",
|
||||
target: [destinationConfig.env],
|
||||
...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch
|
||||
? { gitBranch: destinationConfig.branch }
|
||||
: {})
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecret = async (
|
||||
secretSync: TVercelSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
vercelSecret: VercelApiSecret
|
||||
) => {
|
||||
try {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
// Only update if not sensitive type
|
||||
if (vercelSecret.type !== "sensitive") {
|
||||
await request.patch(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}`,
|
||||
{
|
||||
key: vercelSecret.key,
|
||||
value: secretMap[vercelSecret.key].value,
|
||||
type: vercelSecret.type,
|
||||
target: vercelSecret.target.includes(destinationConfig.env)
|
||||
? [...vercelSecret.target]
|
||||
: [...vercelSecret.target, destinationConfig.env],
|
||||
...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch
|
||||
? { gitBranch: destinationConfig.branch }
|
||||
: {})
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
logger.info(`Vercel secret ${vercelSecret.key} is of type 'sensitive' and cannot be updated through the API`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: vercelSecret.key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const VercelSyncFns = {
|
||||
syncSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const vercelSecrets = await getVercelSecrets(secretSync);
|
||||
const vercelSecretsMap = new Map(vercelSecrets.map((s) => [s.key, s]));
|
||||
|
||||
// Create or update secrets
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
const existingSecret = vercelSecretsMap.get(key);
|
||||
|
||||
if (!existingSecret) {
|
||||
await createSecret(secretSync, secretMap, key);
|
||||
} else if (existingSecret.value !== secretMap[key].value) {
|
||||
await updateSecret(secretSync, secretMap, existingSecret);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete secrets if disableSecretDeletion is not set
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const vercelSecret of vercelSecrets) {
|
||||
if (!secretMap[vercelSecret.key]) {
|
||||
await deleteSecret(secretSync, vercelSecret);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getSecrets: async (secretSync: TVercelSyncWithCredentials): Promise<TSecretMap> => {
|
||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||
},
|
||||
|
||||
removeSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const vercelSecrets = await getVercelSecrets(secretSync);
|
||||
|
||||
for await (const vercelSecret of vercelSecrets) {
|
||||
if (vercelSecret.key in secretMap) {
|
||||
await deleteSecret(secretSync, vercelSecret);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { VercelEnvironmentType } from "./vercel-sync-enums";
|
||||
|
||||
const VercelSyncDestinationConfigSchema = z
|
||||
.object({
|
||||
app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.app),
|
||||
appName: z.string().min(1, "App Name is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.appName),
|
||||
env: z
|
||||
.enum([VercelEnvironmentType.Development, VercelEnvironmentType.Preview, VercelEnvironmentType.Production])
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.env),
|
||||
branch: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.branch)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.env === VercelEnvironmentType.Preview) {
|
||||
return !!data.branch && data.branch.trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Branch is required for Preview environments",
|
||||
path: ["branch"]
|
||||
}
|
||||
);
|
||||
|
||||
const VercelSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||
|
||||
export const VercelSyncSchema = BaseSecretSyncSchema(SecretSync.Vercel, VercelSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Vercel),
|
||||
destinationConfig: VercelSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateVercelSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Vercel,
|
||||
VercelSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: VercelSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateVercelSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Vercel,
|
||||
VercelSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: VercelSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const VercelSyncListItemSchema = z.object({
|
||||
name: z.literal("Vercel"),
|
||||
connection: z.literal(AppConnection.Vercel),
|
||||
destination: z.literal(SecretSync.Vercel),
|
||||
canImportSecrets: z.literal(false)
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TVercelConnection } from "@app/services/app-connection/vercel";
|
||||
|
||||
import { CreateVercelSyncSchema, VercelSyncListItemSchema, VercelSyncSchema } from "./vercel-sync-schemas";
|
||||
|
||||
export type TVercelSyncListItem = z.infer<typeof VercelSyncListItemSchema>;
|
||||
|
||||
export type TVercelSync = z.infer<typeof VercelSyncSchema>;
|
||||
|
||||
export type TVercelSyncInput = z.infer<typeof CreateVercelSyncSchema>;
|
||||
|
||||
export type TVercelSyncWithCredentials = TVercelSync & {
|
||||
connection: TVercelConnection;
|
||||
};
|
||||
|
||||
export type VercelSecret = {
|
||||
description: string;
|
||||
is_secret: boolean;
|
||||
key: string;
|
||||
source: "app" | "env";
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface VercelApiSecret {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
target: string[];
|
||||
gitBranch?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
configurationId?: string;
|
||||
system?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user