diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a61e542b1..59734583a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2472,6 +2472,9 @@ export const SecretSyncs = { projectName: "The name of the Cloudflare Pages project to sync secrets to.", environment: "The environment of the Cloudflare Pages project to sync secrets to." }, + CLOUDFLARE_WORKERS: { + scriptId: "The ID of the Cloudflare Workers script to sync secrets to." + }, ZABBIX: { scope: "The Zabbix scope that secrets should be synced to.", hostId: "The ID of the Zabbix host to sync secrets to.", diff --git a/backend/src/server/routes/v1/app-connection-routers/cloudflare-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/cloudflare-connection-router.ts index bd3507a7d..d18d0564e 100644 --- a/backend/src/server/routes/v1/app-connection-routers/cloudflare-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/cloudflare-connection-router.ts @@ -50,4 +50,32 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi return projects; } }); + + server.route({ + method: "GET", + url: `/:connectionId/cloudflare-workers-scripts`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const projects = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission); + + return projects; + } + }); }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/cloudflare-workers-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-workers-sync-router.ts new file mode 100644 index 000000000..a045a5031 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-workers-sync-router.ts @@ -0,0 +1,17 @@ +import { + CloudflareWorkersSyncSchema, + CreateCloudflareWorkersSyncSchema, + UpdateCloudflareWorkersSyncSchema +} from "@app/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerCloudflareWorkersSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.CloudflareWorkers, + server, + responseSchema: CloudflareWorkersSyncSchema, + createSchema: CreateCloudflareWorkersSyncSchema, + updateSchema: UpdateCloudflareWorkersSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 038fce7aa..4e1d185a8 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -9,6 +9,7 @@ import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; +import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; @@ -50,6 +51,8 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { @@ -43,6 +44,28 @@ export const listCloudflarePagesProjects = async ( })); }; +export const listCloudflareWorkersScripts = async ( + appConnection: TCloudflareConnection +): Promise => { + const { + credentials: { apiToken, accountId } + } = appConnection; + + const { data } = await request.get<{ result: { id: string }[] }>( + `${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}/workers/scripts`, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + + return data.result.map((a) => ({ + id: a.id + })); +}; + export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => { const { apiToken, accountId } = config.credentials; diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts index 2d1f38786..5a8a161fc 100644 --- a/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts @@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { listCloudflarePagesProjects } from "./cloudflare-connection-fns"; +import { listCloudflarePagesProjects, listCloudflareWorkersScripts } from "./cloudflare-connection-fns"; import { TCloudflareConnection } from "./cloudflare-connection-types"; type TGetAppConnectionFunc = ( @@ -19,12 +19,31 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF return projects; } catch (error) { - logger.error(error, "Failed to list Cloudflare Pages projects for Cloudflare connection"); + logger.error( + error, + `Failed to list Cloudflare Pages projects for Cloudflare connection [connectionId=${connectionId}]` + ); + return []; + } + }; + + const listWorkersScripts = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor); + try { + const projects = await listCloudflareWorkersScripts(appConnection); + + return projects; + } catch (error) { + logger.error( + error, + `Failed to list Cloudflare Workers scripts for Cloudflare connection [connectionId=${connectionId}]` + ); return []; } }; return { - listPagesProjects + listPagesProjects, + listWorkersScripts }; }; diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts index 6b2ee0d04..0ac1b708c 100644 --- a/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts @@ -28,3 +28,7 @@ export type TCloudflarePagesProject = { id: string; name: string; }; + +export type TCloudflareWorkersScript = { + id: string; +}; diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-constants.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-constants.ts new file mode 100644 index 000000000..459fda316 --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-constants.ts @@ -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 CLOUDFLARE_WORKERS_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Cloudflare Workers", + destination: SecretSync.CloudflareWorkers, + connection: AppConnection.Cloudflare, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-fns.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-fns.ts new file mode 100644 index 000000000..98c9869f0 --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-fns.ts @@ -0,0 +1,121 @@ +import { request } from "@app/lib/config/request"; +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +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 { TCloudflareWorkersSyncWithCredentials } from "./cloudflare-workers-types"; + +const getSecretKeys = async (secretSync: TCloudflareWorkersSyncWithCredentials): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken, accountId } + } + } = secretSync; + + const { data } = await request.get<{ + result: Array<{ name: string }>; + }>( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${destinationConfig.scriptId}/secrets`, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + + return data.result.map((s) => s.name); +}; + +export const CloudflareWorkersSyncFns = { + syncSecrets: async (secretSync: TCloudflareWorkersSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection: { + credentials: { apiToken, accountId } + }, + destinationConfig: { scriptId } + } = secretSync; + + const existingSecretNames = await getSecretKeys(secretSync); + const secretMapKeys = new Set(Object.keys(secretMap)); + + for await (const [key, val] of Object.entries(secretMap)) { + await delayMs(Math.max(0, applyJitter(100, 200))); + await request.put( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${scriptId}/secrets`, + { name: key, text: val.value, type: "secret_text" }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json" + } + } + ); + } + + if (!secretSync.syncOptions.disableSecretDeletion) { + const secretsToDelete = existingSecretNames.filter((existingKey) => { + const isManagedBySchema = matchesSchema( + existingKey, + secretSync.environment?.slug || "", + secretSync.syncOptions.keySchema + ); + const isInNewSecretMap = secretMapKeys.has(existingKey); + return !isInNewSecretMap && isManagedBySchema; + }); + + for await (const key of secretsToDelete) { + await delayMs(Math.max(0, applyJitter(100, 200))); + await request.delete( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${scriptId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${apiToken}` + } + } + ); + } + } + }, + + getSecrets: async (secretSync: TCloudflareWorkersSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + removeSecrets: async (secretSync: TCloudflareWorkersSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection: { + credentials: { apiToken, accountId } + }, + destinationConfig: { scriptId } + } = secretSync; + + const existingSecretNames = await getSecretKeys(secretSync); + const secretMapToRemoveKeys = new Set(Object.keys(secretMap)); + + for await (const existingKey of existingSecretNames) { + const isManagedBySchema = matchesSchema( + existingKey, + secretSync.environment?.slug || "", + secretSync.syncOptions.keySchema + ); + const isInSecretMapToRemove = secretMapToRemoveKeys.has(existingKey); + + if (isInSecretMapToRemove && isManagedBySchema) { + await delayMs(Math.max(0, applyJitter(100, 200))); + await request.delete( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${scriptId}/secrets/${existingKey}`, + { + headers: { + Authorization: `Bearer ${apiToken}` + } + } + ); + } + } + } +}; diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas.ts new file mode 100644 index 000000000..b5698867a --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas.ts @@ -0,0 +1,55 @@ +import RE2 from "re2"; +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"; + +const CloudflareWorkersSyncDestinationConfigSchema = z.object({ + scriptId: z + .string() + .min(1, "Script ID is required") + .max(64) + .refine((val) => { + const re2 = new RE2(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/); + return re2.test(val); + }, "Invalid script ID format") + .describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_WORKERS.scriptId) +}); + +const CloudflareWorkersSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const CloudflareWorkersSyncSchema = BaseSecretSyncSchema( + SecretSync.CloudflareWorkers, + CloudflareWorkersSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.CloudflareWorkers), + destinationConfig: CloudflareWorkersSyncDestinationConfigSchema +}); + +export const CreateCloudflareWorkersSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.CloudflareWorkers, + CloudflareWorkersSyncOptionsConfig +).extend({ + destinationConfig: CloudflareWorkersSyncDestinationConfigSchema +}); + +export const UpdateCloudflareWorkersSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.CloudflareWorkers, + CloudflareWorkersSyncOptionsConfig +).extend({ + destinationConfig: CloudflareWorkersSyncDestinationConfigSchema.optional() +}); + +export const CloudflareWorkersSyncListItemSchema = z.object({ + name: z.literal("Cloudflare Workers"), + connection: z.literal(AppConnection.Cloudflare), + destination: z.literal(SecretSync.CloudflareWorkers), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-types.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-types.ts new file mode 100644 index 000000000..1b5adda7b --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-types.ts @@ -0,0 +1,19 @@ +import z from "zod"; + +import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types"; + +import { + CloudflareWorkersSyncListItemSchema, + CloudflareWorkersSyncSchema, + CreateCloudflareWorkersSyncSchema +} from "./cloudflare-workers-schemas"; + +export type TCloudflareWorkersSyncListItem = z.infer; + +export type TCloudflareWorkersSync = z.infer; + +export type TCloudflareWorkersSyncInput = z.infer; + +export type TCloudflareWorkersSyncWithCredentials = TCloudflareWorkersSync & { + connection: TCloudflareConnection; +}; diff --git a/backend/src/services/secret-sync/cloudflare-workers/index.ts b/backend/src/services/secret-sync/cloudflare-workers/index.ts new file mode 100644 index 000000000..5d7916a4d --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/index.ts @@ -0,0 +1,4 @@ +export * from "./cloudflare-workers-constants"; +export * from "./cloudflare-workers-fns"; +export * from "./cloudflare-workers-schemas"; +export * from "./cloudflare-workers-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index c7dc0c9bb..64a191a22 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -21,6 +21,8 @@ export enum SecretSync { Flyio = "flyio", GitLab = "gitlab", CloudflarePages = "cloudflare-pages", + CloudflareWorkers = "cloudflare-workers", + Zabbix = "zabbix", Railway = "railway" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 34b24eece..7dc19d3e9 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -31,6 +31,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants"; import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"; +import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers"; import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; @@ -72,6 +73,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION, [SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION, [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION, + [SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, + [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION }; @@ -241,6 +244,8 @@ export const SecretSyncFns = { return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.CloudflareWorkers: + return CloudflareWorkersSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Zabbix: return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Railway: @@ -337,6 +342,9 @@ export const SecretSyncFns = { case SecretSync.CloudflarePages: secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync); break; + case SecretSync.CloudflareWorkers: + secretMap = await CloudflareWorkersSyncFns.getSecrets(secretSync); + break; case SecretSync.Zabbix: secretMap = await ZabbixSyncFns.getSecrets(secretSync); break; @@ -420,6 +428,8 @@ export const SecretSyncFns = { return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.CloudflareWorkers: + return CloudflareWorkersSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Zabbix: return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Railway: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 938679332..dd1d5b146 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -24,6 +24,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Flyio]: "Fly.io", [SecretSync.GitLab]: "GitLab", [SecretSync.CloudflarePages]: "Cloudflare Pages", + [SecretSync.CloudflareWorkers]: "Cloudflare Workers", + [SecretSync.Zabbix]: "Zabbix", [SecretSync.Railway]: "Railway" }; @@ -51,6 +53,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.GitLab]: AppConnection.GitLab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare, + [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, + [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway }; @@ -78,6 +82,8 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Flyio]: SecretSyncPlanType.Regular, [SecretSync.GitLab]: SecretSyncPlanType.Regular, [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular, + [SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular, + [SecretSync.Zabbix]: SecretSyncPlanType.Regular, [SecretSync.Railway]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 7eaba35f2..998328b85 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -78,6 +78,12 @@ import { TCloudflarePagesSyncListItem, TCloudflarePagesSyncWithCredentials } from "./cloudflare-pages/cloudflare-pages-types"; +import { + TCloudflareWorkersSync, + TCloudflareWorkersSyncInput, + TCloudflareWorkersSyncListItem, + TCloudflareWorkersSyncWithCredentials +} from "./cloudflare-workers"; import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab"; @@ -144,6 +150,7 @@ export type TSecretSync = | TFlyioSync | TGitLabSync | TCloudflarePagesSync + | TCloudflareWorkersSync | TZabbixSync | TRailwaySync; @@ -170,6 +177,7 @@ export type TSecretSyncWithCredentials = | TFlyioSyncWithCredentials | TGitLabSyncWithCredentials | TCloudflarePagesSyncWithCredentials + | TCloudflareWorkersSyncWithCredentials | TZabbixSyncWithCredentials | TRailwaySyncWithCredentials; @@ -196,6 +204,7 @@ export type TSecretSyncInput = | TFlyioSyncInput | TGitLabSyncInput | TCloudflarePagesSyncInput + | TCloudflareWorkersSyncInput | TZabbixSyncInput | TRailwaySyncInput; @@ -222,6 +231,7 @@ export type TSecretSyncListItem = | TFlyioSyncListItem | TGitLabSyncListItem | TCloudflarePagesSyncListItem + | TCloudflareWorkersSyncListItem | TZabbixSyncListItem | TRailwaySyncListItem; diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/create.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/create.mdx new file mode 100644 index 000000000..d53c8eb0d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/cloudflare-workers" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/delete.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/delete.mdx new file mode 100644 index 000000000..919074e74 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/cloudflare-workers/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id.mdx new file mode 100644 index 000000000..5d425e722 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/cloudflare-workers/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name.mdx new file mode 100644 index 000000000..877b58aed --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/cloudflare-workers/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/list.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/list.mdx new file mode 100644 index 000000000..1377c2282 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/cloudflare-workers" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets.mdx new file mode 100644 index 000000000..6446ede42 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/cloudflare-workers/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets.mdx new file mode 100644 index 000000000..1980f0797 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/cloudflare-workers/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/update.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/update.mdx new file mode 100644 index 000000000..4dc511b2c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/cloudflare-workers/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 8e6972429..3a516febb 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -514,6 +514,7 @@ "integrations/secret-syncs/azure-key-vault", "integrations/secret-syncs/camunda", "integrations/secret-syncs/cloudflare-pages", + "integrations/secret-syncs/cloudflare-workers", "integrations/secret-syncs/databricks", "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", @@ -1720,6 +1721,19 @@ "api-reference/endpoints/secret-syncs/cloudflare-pages/remove-secrets" ] }, + { + "group": "Cloudflare Workers", + "pages": [ + "api-reference/endpoints/secret-syncs/cloudflare-workers/list", + "api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id", + "api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name", + "api-reference/endpoints/secret-syncs/cloudflare-workers/create", + "api-reference/endpoints/secret-syncs/cloudflare-workers/update", + "api-reference/endpoints/secret-syncs/cloudflare-workers/delete", + "api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets", + "api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets" + ] + }, { "group": "Databricks", "pages": [ diff --git a/docs/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png b/docs/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png new file mode 100644 index 000000000..d752b51e3 Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png new file mode 100644 index 000000000..25c366ac9 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-destination.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-destination.png new file mode 100644 index 000000000..c7ccac175 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-destination.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png new file mode 100644 index 000000000..235b6f147 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-options.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-options.png new file mode 100644 index 000000000..aec833bf4 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-options.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png new file mode 100644 index 000000000..c32b164c5 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-source.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-source.png new file mode 100644 index 000000000..3044dbac9 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-source.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png b/docs/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png new file mode 100644 index 000000000..a7462a3bd Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png differ diff --git a/docs/integrations/app-connections/cloudflare.mdx b/docs/integrations/app-connections/cloudflare.mdx index 66ba1256f..6d79d2567 100644 --- a/docs/integrations/app-connections/cloudflare.mdx +++ b/docs/integrations/app-connections/cloudflare.mdx @@ -35,6 +35,17 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for - **Account** - **Cloudflare Pages** - **Edit** - **Account** - **Account Settings** - **Read** + Add these permissions to your API token and click **Continue to summary**, then **Create Token** to generate your API token. + + + Use the following permissions to grant Infisical access to sync secrets to Cloudflare Workers: + + ![Configure Token](/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png) + + **Required Permissions:** + - **Account** - **Workers Scripts** - **Edit** + - **Account** - **Account Settings** - **Read** + Add these permissions to your API token and click **Continue to summary**, then **Create Token** to generate your API token. @@ -44,7 +55,7 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for After creation, copy and securely store your API token as it will not be shown again. - + ![Generated API Token](/images/app-connections/cloudflare/cloudflare-generated-token.png) diff --git a/docs/integrations/secret-syncs/cloudflare-workers.mdx b/docs/integrations/secret-syncs/cloudflare-workers.mdx new file mode 100644 index 000000000..d6e7e25e8 --- /dev/null +++ b/docs/integrations/secret-syncs/cloudflare-workers.mdx @@ -0,0 +1,128 @@ +--- +title: "Cloudflare Workers Sync" +description: "Learn how to configure a Cloudflare Workers Sync for Infisical." +--- + +**Prerequisites:** + +- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) +- Create a [Cloudflare Connection](/integrations/app-connections/cloudflare) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Cloudflare Workers** option. + ![Select Cloudflare Workers](/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-destination.png) + + - **Cloudflare Connection**: The Cloudflare Connection to authenticate with. + - **Cloudflare Workers Script**: Choose the Cloudflare Workers script you want to sync secrets to. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Cloudflare Workers Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Cloudflare Workers Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png) + + 8. If enabled, your Cloudflare Workers Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png) + + + + To create a **Cloudflare Workers Sync**, make an API request to the [Create Cloudflare Workers Sync](/api-reference/endpoints/secret-syncs/cloudflare-workers/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/cloudflare-workers \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-cloudflare-workers-sync", + "projectId": "your-project-id", + "description": "an example sync", + "connectionId": "your-cloudflare-connection-id", + "environment": "production", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scriptId": "my-workers-script" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "your-sync-id", + "name": "my-cloudflare-workers-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "your-folder-id", + "connectionId": "your-cloudflare-connection-id", + "createdAt": "2024-05-01T12:00:00Z", + "updatedAt": "2024-05-01T12:00:00Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2024-05-01T12:00:00Z", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "your-project-id", + "connection": { + "app": "cloudflare", + "name": "my-cloudflare-connection", + "id": "your-cloudflare-connection-id" + }, + "environment": { + "slug": "production", + "name": "Production", + "id": "your-env-id" + }, + "folder": { + "id": "your-folder-id", + "path": "/my-secrets" + }, + "destination": "cloudflare-workers", + "destinationConfig": { + "scriptId": "my-workers-script" + } + } + } + ``` + + + diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx index d19092d63..eb196c9a4 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx @@ -4,7 +4,7 @@ import { SingleValue } from "react-select"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; import { - TCloudflareProject, + TCloudflarePagesProject, useCloudflareConnectionListPagesProjects } from "@app/hooks/api/appConnections/cloudflare"; import { SecretSync } from "@app/hooks/api/secretSyncs"; @@ -52,7 +52,7 @@ export const CloudflarePagesSyncFields = () => { isDisabled={!connectionId} value={projects ? (projects.find((project) => project.name === value) ?? []) : []} onChange={(option) => { - onChange((option as SingleValue)?.name ?? null); + onChange((option as SingleValue)?.name ?? null); }} options={projects} placeholder="Select a project..." diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflareWorkersSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflareWorkersSyncFields.tsx new file mode 100644 index 000000000..c8354fabc --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflareWorkersSyncFields.tsx @@ -0,0 +1,59 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { + TCloudflareWorkersScript, + useCloudflareConnectionListWorkersScripts +} from "@app/hooks/api/appConnections/cloudflare"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const CloudflareWorkersSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.CloudflareWorkers } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: scripts = [], isPending: isScriptsPending } = + useCloudflareConnectionListWorkersScripts(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.scriptId", ""); + }} + /> + ( + + script.id === value) || []} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + }} + options={scripts} + placeholder="Select a worker script..." + getOptionLabel={(option) => option.id} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index f530e0a52..cc732728c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -11,6 +11,7 @@ import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; +import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; @@ -78,6 +79,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.CloudflarePages: return ; + case SecretSync.CloudflareWorkers: + return ; case SecretSync.Zabbix: return ; case SecretSync.Railway: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 57967f130..c86f63c20 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -58,6 +58,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: + case SecretSync.CloudflareWorkers: case SecretSync.Zabbix: case SecretSync.Railway: AdditionalSyncOptionsFieldsComponent = null; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflareWorkersReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflareWorkersReviewFields.tsx new file mode 100644 index 000000000..6fe0f1f6a --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflareWorkersReviewFields.tsx @@ -0,0 +1,14 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const CloudflareWorkersSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.CloudflareWorkers } + >(); + const scriptId = watch("destinationConfig.scriptId"); + + return {scriptId}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 3b3a9cf74..52dc7fd02 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -20,6 +20,7 @@ import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; +import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; @@ -126,6 +127,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.CloudflarePages: DestinationFieldsComponent = ; break; + case SecretSync.CloudflareWorkers: + DestinationFieldsComponent = ; + break; case SecretSync.Zabbix: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/schemas/cloudflare-workers-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/cloudflare-workers-sync-destination-schema.ts new file mode 100644 index 000000000..5e4c16d09 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/cloudflare-workers-sync-destination-schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const CloudflareWorkersSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.CloudflareWorkers), + destinationConfig: z.object({ + scriptId: z + .string() + .trim() + .min(1, "Script ID is required") + .max(64) + .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "Invalid script ID format") + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 425fd2414..82a8fa2ff 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destinatio import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema"; +import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; @@ -48,6 +49,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ FlyioSyncDestinationSchema, GitlabSyncDestinationSchema, CloudflarePagesSyncDestinationSchema, + CloudflareWorkersSyncDestinationSchema, + ZabbixSyncDestinationSchema, RailwaySyncDestinationSchema ]); diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 5821ce959..8e9bda613 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -82,6 +82,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.GitLab]: AppConnection.Gitlab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare, + [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, + [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway }; diff --git a/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx b/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx index ae2efea99..f4ca87ad1 100644 --- a/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx +++ b/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx @@ -3,21 +3,23 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { appConnectionKeys } from "../queries"; -import { TCloudflareProject } from "./types"; +import { TCloudflarePagesProject, TCloudflareWorkersScript } from "./types"; const cloudflareConnectionKeys = { all: [...appConnectionKeys.all, "cloudflare"] as const, listPagesProjects: (connectionId: string) => - [...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const + [...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const, + listWorkersScripts: (connectionId: string) => + [...cloudflareConnectionKeys.all, "workers-scripts", connectionId] as const }; export const useCloudflareConnectionListPagesProjects = ( connectionId: string, options?: Omit< UseQueryOptions< - TCloudflareProject[], + TCloudflarePagesProject[], unknown, - TCloudflareProject[], + TCloudflarePagesProject[], ReturnType >, "queryKey" | "queryFn" @@ -26,7 +28,7 @@ export const useCloudflareConnectionListPagesProjects = ( return useQuery({ queryKey: cloudflareConnectionKeys.listPagesProjects(connectionId), queryFn: async () => { - const { data } = await apiRequest.get( + const { data } = await apiRequest.get( `/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-pages-projects` ); @@ -35,3 +37,28 @@ export const useCloudflareConnectionListPagesProjects = ( ...options }); }; + +export const useCloudflareConnectionListWorkersScripts = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TCloudflareWorkersScript[], + unknown, + TCloudflareWorkersScript[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: cloudflareConnectionKeys.listWorkersScripts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-workers-scripts` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/cloudflare/types.ts b/frontend/src/hooks/api/appConnections/cloudflare/types.ts index ed7bda478..1d5ea53d4 100644 --- a/frontend/src/hooks/api/appConnections/cloudflare/types.ts +++ b/frontend/src/hooks/api/appConnections/cloudflare/types.ts @@ -1,4 +1,8 @@ -export type TCloudflareProject = { +export type TCloudflarePagesProject = { id: string; name: string; }; + +export type TCloudflareWorkersScript = { + id: string; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index a6ba1fd40..c549ba351 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -21,6 +21,8 @@ export enum SecretSync { Flyio = "flyio", GitLab = "gitlab", CloudflarePages = "cloudflare-pages", + CloudflareWorkers = "cloudflare-workers", + Zabbix = "zabbix", Railway = "railway" } diff --git a/frontend/src/hooks/api/secretSyncs/types/cloudflare-workers-sync.ts b/frontend/src/hooks/api/secretSyncs/types/cloudflare-workers-sync.ts new file mode 100644 index 000000000..224fe8938 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/cloudflare-workers-sync.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TCloudflareWorkersSync = TRootSecretSync & { + destination: SecretSync.CloudflareWorkers; + destinationConfig: { + scriptId: string; + }; + connection: { + app: AppConnection.Cloudflare; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 7071576bf..dd300c0df 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -10,6 +10,7 @@ import { TAzureDevOpsSync } from "./azure-devops-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TCamundaSync } from "./camunda-sync"; import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; +import { TCloudflareWorkersSync } from "./cloudflare-workers-sync"; import { TDatabricksSync } from "./databricks-sync"; import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; @@ -56,6 +57,7 @@ export type TSecretSync = | TFlyioSync | TGitLabSync | TCloudflarePagesSync + | TCloudflareWorkersSync | TZabbixSync | TRailwaySync; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CloudflareWorkersSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CloudflareWorkersSyncDestinationCol.tsx new file mode 100644 index 000000000..2efbdc75e --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CloudflareWorkersSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TCloudflareWorkersSync } from "@app/hooks/api/secretSyncs/types/cloudflare-workers-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TCloudflareWorkersSync; +}; + +export const CloudflareWorkersSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 1b357cffb..5a4d451fc 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol"; import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol"; +import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; @@ -75,6 +76,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.CloudflarePages: return ; + case SecretSync.CloudflareWorkers: + return ; case SecretSync.Zabbix: return ; case SecretSync.Railway: diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index a8bad462d..a6c2f4f5e 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -145,6 +145,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectName; secondaryText = destinationConfig.environment; break; + case SecretSync.CloudflareWorkers: + primaryText = destinationConfig.scriptId; + secondaryText = "Script ID"; + break; case SecretSync.Zabbix: if (destinationConfig.scope === ZabbixSyncScope.Host) { primaryText = destinationConfig.hostName; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CloudflareWorkersSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CloudflareWorkersSyncDestinationSection.tsx new file mode 100644 index 000000000..7d60315ca --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CloudflareWorkersSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TCloudflareWorkersSync } from "@app/hooks/api/secretSyncs/types/cloudflare-workers-sync"; + +type Props = { + secretSync: TCloudflareWorkersSync; +}; + +export const CloudflareWorkersSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { scriptId } + } = secretSync; + + return {scriptId}; +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 568c3b3db..71755c7e9 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -19,6 +19,7 @@ import { AzureDevOpsSyncDestinationSection } from "./AzureDevOpsSyncDestinationS import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection"; +import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; import { FlyioSyncDestinationSection } from "./FlyioSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; @@ -116,6 +117,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.CloudflarePages: DestinationComponents = ; break; + case SecretSync.CloudflareWorkers: + DestinationComponents = ; + break; case SecretSync.Zabbix: DestinationComponents = ; break; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index debdfcbdf..60710e3ef 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -60,6 +60,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: + case SecretSync.CloudflareWorkers: case SecretSync.Zabbix: case SecretSync.Railway: AdditionalSyncOptionsComponent = null;