Merge pull request #3835 from Infisical/feat/gitlabSecretSync

feat(secret-sync): Add gitlab secret sync
This commit is contained in:
carlosmonastyrski
2025-06-25 17:53:12 -03:00
committed by GitHub
108 changed files with 5318 additions and 62 deletions
@@ -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 GITLAB_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "GitLab",
destination: SecretSync.GitLab,
connection: AppConnection.GitLab,
canImportSecrets: false
};
@@ -0,0 +1,4 @@
export enum GitLabSyncScope {
Project = "project",
Group = "group"
}
@@ -0,0 +1,452 @@
/* eslint-disable no-await-in-loop */
import { GitbeakerRequestError } from "@gitbeaker/rest";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import {
getGitLabClient,
GitLabConnectionMethod,
refreshGitLabToken,
TGitLabConnection
} from "@app/services/app-connection/gitlab";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TGitLabSyncWithCredentials, TGitLabVariable } from "@app/services/secret-sync/gitlab/gitlab-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { GitLabSyncScope } from "./gitlab-sync-enums";
interface TGitLabVariablePayload {
key?: string;
value: string;
variable_type?: "env_var" | "file";
environment_scope?: string;
protected?: boolean;
masked?: boolean;
masked_and_hidden?: boolean;
description?: string;
}
interface TGitLabVariableCreate extends TGitLabVariablePayload {
key: string;
}
interface TGitLabVariableUpdate extends Omit<TGitLabVariablePayload, "key"> {}
type TGitLabSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
const getValidAccessToken = async (
connection: TGitLabConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<string> => {
if (
connection.method === GitLabConnectionMethod.OAuth &&
connection.credentials.refreshToken &&
new Date(connection.credentials.expiresAt) < new Date()
) {
const accessToken = await refreshGitLabToken(
connection.credentials.refreshToken,
connection.id,
connection.orgId,
appConnectionDAL,
kmsService,
connection.credentials.instanceUrl
);
return accessToken;
}
return connection.credentials.accessToken;
};
const getGitLabVariables = async ({
accessToken,
connection,
scope,
resourceId,
targetEnvironment
}: {
accessToken: string;
connection: TGitLabConnection;
scope: GitLabSyncScope;
resourceId: string;
targetEnvironment?: string;
}): Promise<TGitLabVariable[]> => {
try {
const client = await getGitLabClient(
accessToken,
connection.credentials.instanceUrl,
connection.method === GitLabConnectionMethod.OAuth
);
let variables: TGitLabVariable[] = [];
if (scope === GitLabSyncScope.Project) {
variables = await client.ProjectVariables.all(resourceId);
} else {
variables = await client.GroupVariables.all(resourceId);
}
if (targetEnvironment) {
variables = variables.filter((v) => v.environmentScope === targetEnvironment);
}
return variables;
} catch (error) {
if (error instanceof GitbeakerRequestError) {
throw new SecretSyncError({
error: new Error(
`Failed to fetch variables: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
)
});
}
throw new SecretSyncError({
error
});
}
};
const createGitLabVariable = async ({
accessToken,
connection,
scope,
resourceId,
variable
}: {
accessToken: string;
connection: TGitLabConnection;
scope: GitLabSyncScope;
resourceId: string;
variable: TGitLabVariableCreate;
}): Promise<void> => {
try {
const client = await getGitLabClient(
accessToken,
connection.credentials.instanceUrl,
connection.method === GitLabConnectionMethod.OAuth
);
const payload = {
key: variable.key,
value: variable.value,
variableType: "env_var",
environmentScope: variable.environment_scope || "*",
protected: variable.protected || false,
masked: variable.masked || false,
masked_and_hidden: variable.masked_and_hidden || false,
raw: false
};
if (scope === GitLabSyncScope.Project) {
await client.ProjectVariables.create(resourceId, payload.key, payload.value, {
variableType: "env_var",
environmentScope: payload.environmentScope,
protected: payload.protected,
masked: payload.masked,
masked_and_hidden: payload.masked_and_hidden,
raw: false
});
} else {
await client.GroupVariables.create(resourceId, payload.key, payload.value, {
variableType: "env_var",
environmentScope: payload.environmentScope,
protected: payload.protected,
masked: payload.masked,
...(payload.masked_and_hidden && { masked_and_hidden: payload.masked_and_hidden }),
raw: false
});
}
} catch (error) {
if (error instanceof GitbeakerRequestError) {
throw new SecretSyncError({
error: new Error(
`Failed to create variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
),
secretKey: variable.key
});
}
throw new SecretSyncError({
error,
secretKey: variable.key
});
}
};
const updateGitLabVariable = async ({
accessToken,
connection,
scope,
resourceId,
key,
variable,
targetEnvironment
}: {
accessToken: string;
connection: TGitLabConnection;
scope: GitLabSyncScope;
resourceId: string;
key: string;
variable: TGitLabVariableUpdate;
targetEnvironment?: string;
}): Promise<void> => {
try {
const client = await getGitLabClient(
accessToken,
connection.credentials.instanceUrl,
connection.method === GitLabConnectionMethod.OAuth
);
const options = {
...(variable.environment_scope && { environmentScope: variable.environment_scope }),
...(variable.protected !== undefined && { protected: variable.protected }),
...(variable.masked !== undefined && { masked: variable.masked })
};
if (targetEnvironment) {
options.environmentScope = targetEnvironment;
}
if (scope === GitLabSyncScope.Project) {
await client.ProjectVariables.edit(resourceId, key, variable.value, {
...options,
filter: { environment_scope: targetEnvironment || "*" }
});
} else {
await client.GroupVariables.edit(resourceId, key, variable.value, {
...options,
filter: { environment_scope: targetEnvironment || "*" }
});
}
} catch (error) {
if (error instanceof GitbeakerRequestError) {
throw new SecretSyncError({
error: new Error(
`Failed to update variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
),
secretKey: key
});
}
throw new SecretSyncError({
error,
secretKey: key
});
}
};
const deleteGitLabVariable = async ({
accessToken,
connection,
scope,
resourceId,
key,
targetEnvironment,
allVariables
}: {
accessToken: string;
connection: TGitLabConnection;
scope: GitLabSyncScope;
resourceId: string;
key: string;
targetEnvironment?: string;
allVariables?: TGitLabVariable[];
}): Promise<void> => {
if (allVariables && !allVariables.find((v) => v.key === key)) {
return;
}
try {
const client = await getGitLabClient(
accessToken,
connection.credentials.instanceUrl,
connection.method === GitLabConnectionMethod.OAuth
);
const options: { filter?: { environment_scope: string } } = {};
if (targetEnvironment) {
options.filter = { environment_scope: targetEnvironment || "*" };
}
if (scope === GitLabSyncScope.Project) {
await client.ProjectVariables.remove(resourceId, key, options);
} else {
await client.GroupVariables.remove(resourceId, key);
}
} catch (error: unknown) {
if (error instanceof GitbeakerRequestError) {
throw new SecretSyncError({
error: new Error(
`Failed to delete variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
),
secretKey: key
});
}
throw new SecretSyncError({
error,
secretKey: key
});
}
};
export const GitLabSyncFns = {
syncSecrets: async (
secretSync: TGitLabSyncWithCredentials,
secretMap: TSecretMap,
{ appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps
): Promise<void> => {
const { connection, environment, destinationConfig } = secretSync;
const { scope, targetEnvironment } = destinationConfig;
const resourceId = scope === GitLabSyncScope.Project ? destinationConfig.projectId : destinationConfig.groupId;
const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService);
try {
const currentVariables = await getGitLabVariables({
accessToken,
connection,
scope,
resourceId,
targetEnvironment
});
const currentVariableMap = new Map(currentVariables.map((v) => [v.key, v]));
for (const [key, { value }] of Object.entries(secretMap)) {
if (value?.length < 8 && destinationConfig.shouldMaskSecrets) {
throw new SecretSyncError({
message: `Secret ${key} is too short to be masked. GitLab requires a minimum of 8 characters for masked secrets.`,
secretKey: key
});
}
try {
const existingVariable = currentVariableMap.get(key);
if (existingVariable) {
if (
existingVariable.value !== value ||
existingVariable.environmentScope !== targetEnvironment ||
existingVariable.protected !== destinationConfig.shouldProtectSecrets ||
existingVariable.masked !== destinationConfig.shouldMaskSecrets
) {
await updateGitLabVariable({
accessToken,
connection,
scope,
resourceId,
key,
variable: {
value,
environment_scope: targetEnvironment,
protected: destinationConfig.shouldProtectSecrets,
masked: destinationConfig.shouldMaskSecrets || existingVariable.hidden
},
targetEnvironment
});
}
} else {
await createGitLabVariable({
accessToken,
connection,
scope,
resourceId,
variable: {
key,
value,
variable_type: "env_var",
environment_scope: targetEnvironment || "*",
protected: destinationConfig.shouldProtectSecrets || false,
masked: destinationConfig.shouldMaskSecrets || false,
masked_and_hidden: destinationConfig.shouldHideSecrets || false
}
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (!secretSync.syncOptions.disableSecretDeletion) {
for (const variable of currentVariables) {
try {
const shouldDelete =
matchesSchema(variable.key, environment?.slug || "", secretSync.syncOptions.keySchema) &&
!(variable.key in secretMap);
if (shouldDelete) {
await deleteGitLabVariable({
accessToken,
connection,
scope,
resourceId,
key: variable.key,
targetEnvironment
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: variable.key
});
}
}
}
} catch (error) {
if (error instanceof SecretSyncError) {
throw error;
}
throw new SecretSyncError({
message: "Failed to sync secrets",
error
});
}
},
removeSecrets: async (
secretSync: TGitLabSyncWithCredentials,
secretMap: TSecretMap,
{ appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps
): Promise<void> => {
const { connection, destinationConfig } = secretSync;
const { scope, targetEnvironment } = destinationConfig;
const resourceId = scope === GitLabSyncScope.Project ? destinationConfig.projectId : destinationConfig.groupId;
const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService);
const allVariables = await getGitLabVariables({
accessToken,
connection,
scope,
resourceId,
targetEnvironment
});
for (const key of Object.keys(secretMap)) {
try {
await deleteGitLabVariable({
accessToken,
connection,
scope,
resourceId,
key,
targetEnvironment,
allVariables
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
},
getSecrets: async (secretSync: TGitLabSyncWithCredentials): Promise<TSecretMap> => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
}
};
@@ -0,0 +1,97 @@
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 { GitLabSyncScope } from "./gitlab-sync-enums";
const GitLabSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
z.object({
scope: z.literal(GitLabSyncScope.Project).describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.scope),
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.projectId),
projectName: z
.string()
.min(1, "Project name is required")
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.projectName),
targetEnvironment: z
.string()
.optional()
.default("*")
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.targetEnvironment),
shouldProtectSecrets: z
.boolean()
.optional()
.default(false)
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldProtectSecrets),
shouldMaskSecrets: z
.boolean()
.optional()
.default(false)
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldMaskSecrets),
shouldHideSecrets: z
.boolean()
.optional()
.default(false)
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldHideSecrets)
}),
z.object({
scope: z.literal(GitLabSyncScope.Group).describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.scope),
groupId: z.string().min(1, "Group ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.groupId),
groupName: z.string().min(1, "Group name is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.groupName),
targetEnvironment: z
.string()
.optional()
.default("*")
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.targetEnvironment),
shouldProtectSecrets: z
.boolean()
.optional()
.default(false)
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldProtectSecrets),
shouldMaskSecrets: z
.boolean()
.optional()
.default(false)
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldMaskSecrets),
shouldHideSecrets: z
.boolean()
.optional()
.default(false)
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldHideSecrets)
})
]);
const GitLabSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const GitLabSyncSchema = BaseSecretSyncSchema(SecretSync.GitLab, GitLabSyncOptionsConfig).extend({
destination: z.literal(SecretSync.GitLab),
destinationConfig: GitLabSyncDestinationConfigSchema
});
export const CreateGitLabSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.GitLab,
GitLabSyncOptionsConfig
).extend({
destinationConfig: GitLabSyncDestinationConfigSchema
});
export const UpdateGitLabSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.GitLab,
GitLabSyncOptionsConfig
).extend({
destinationConfig: GitLabSyncDestinationConfigSchema.optional()
});
export const GitLabSyncListItemSchema = z.object({
name: z.literal("GitLab"),
connection: z.literal(AppConnection.GitLab),
destination: z.literal(SecretSync.GitLab),
canImportSecrets: z.literal(false)
});
@@ -0,0 +1,58 @@
import { z } from "zod";
import { TGitLabConnection } from "@app/services/app-connection/gitlab";
import { CreateGitLabSyncSchema, GitLabSyncListItemSchema, GitLabSyncSchema } from "./gitlab-sync-schemas";
export type TGitLabSync = z.infer<typeof GitLabSyncSchema>;
export type TGitLabSyncInput = z.infer<typeof CreateGitLabSyncSchema>;
export type TGitLabSyncListItem = z.infer<typeof GitLabSyncListItemSchema>;
export type TGitLabSyncWithCredentials = TGitLabSync & {
connection: TGitLabConnection;
};
export type TGitLabVariable = {
key: string;
value: string;
protected: boolean;
masked: boolean;
environmentScope?: string;
hidden?: boolean;
};
export type TGitLabVariableCreate = {
key: string;
value: string;
variable_type?: "env_var" | "file";
protected?: boolean;
masked?: boolean;
raw?: boolean;
environment_scope?: string;
description?: string;
};
export type TGitLabVariableUpdate = {
value: string;
variable_type?: "env_var" | "file";
protected?: boolean;
masked?: boolean;
raw?: boolean;
environment_scope?: string;
description?: string | null;
};
export type TGitLabListVariables = {
accessToken: string;
projectId: string;
environmentScope?: string;
};
export type TGitLabCreateVariable = TGitLabListVariables & {
variable: TGitLabVariableCreate;
};
export type TGitLabUpdateVariable = TGitLabListVariables & {
key: string;
variable: TGitLabVariableUpdate;
};
@@ -0,0 +1,4 @@
export * from "./gitlab-sync-constants";
export * from "./gitlab-sync-fns";
export * from "./gitlab-sync-schemas";
export * from "./gitlab-sync-types";
@@ -19,6 +19,7 @@ export enum SecretSync {
Heroku = "heroku",
Render = "render",
Flyio = "flyio",
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages"
}
@@ -34,6 +34,7 @@ import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"
import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { GITLAB_SYNC_LIST_OPTION, GitLabSyncFns } from "./gitlab";
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
@@ -66,6 +67,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION
};
@@ -230,6 +232,8 @@ export const SecretSyncFns = {
return RenderSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Flyio:
return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitLab:
return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
@@ -318,6 +322,9 @@ export const SecretSyncFns = {
case SecretSync.Flyio:
secretMap = await FlyioSyncFns.getSecrets(secretSync);
break;
case SecretSync.GitLab:
secretMap = await GitLabSyncFns.getSecrets(secretSync);
break;
case SecretSync.CloudflarePages:
secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync);
break;
@@ -394,6 +401,8 @@ export const SecretSyncFns = {
return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Flyio:
return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitLab:
return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
@@ -22,6 +22,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Heroku]: "Heroku",
[SecretSync.Render]: "Render",
[SecretSync.Flyio]: "Fly.io",
[SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages"
};
@@ -46,6 +47,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Heroku]: AppConnection.Heroku,
[SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
};
@@ -70,5 +72,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Heroku]: SecretSyncPlanType.Regular,
[SecretSync.Render]: SecretSyncPlanType.Regular,
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular
};
@@ -72,8 +72,15 @@ import {
TAzureKeyVaultSyncListItem,
TAzureKeyVaultSyncWithCredentials
} from "./azure-key-vault";
import {
TCloudflarePagesSync,
TCloudflarePagesSyncInput,
TCloudflarePagesSyncListItem,
TCloudflarePagesSyncWithCredentials
} from "./cloudflare-pages/cloudflare-pages-types";
import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab";
import {
THCVaultSync,
THCVaultSyncInput,
@@ -106,12 +113,6 @@ import {
TTerraformCloudSyncWithCredentials
} from "./terraform-cloud";
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
import {
TCloudflarePagesSync,
TCloudflarePagesSyncInput,
TCloudflarePagesSyncListItem,
TCloudflarePagesSyncWithCredentials
} from "./cloudflare-pages/cloudflare-pages-types";
export type TSecretSync =
| TAwsParameterStoreSync
@@ -134,6 +135,7 @@ export type TSecretSync =
| THerokuSync
| TRenderSync
| TFlyioSync
| TGitLabSync
| TCloudflarePagesSync;
export type TSecretSyncWithCredentials =
@@ -157,6 +159,7 @@ export type TSecretSyncWithCredentials =
| THerokuSyncWithCredentials
| TRenderSyncWithCredentials
| TFlyioSyncWithCredentials
| TGitLabSyncWithCredentials
| TCloudflarePagesSyncWithCredentials;
export type TSecretSyncInput =
@@ -180,6 +183,7 @@ export type TSecretSyncInput =
| THerokuSyncInput
| TRenderSyncInput
| TFlyioSyncInput
| TGitLabSyncInput
| TCloudflarePagesSyncInput;
export type TSecretSyncListItem =
@@ -203,6 +207,7 @@ export type TSecretSyncListItem =
| THerokuSyncListItem
| TRenderSyncListItem
| TFlyioSyncListItem
| TGitLabSyncListItem
| TCloudflarePagesSyncListItem;
export type TSyncOptionsConfig = {