diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 59734583a..2d5d24e55 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2282,6 +2282,9 @@ export const AppConnections = { }, RAILWAY: { apiToken: "The API token used to authenticate with Railway." + }, + CHECKLY: { + apiKey: "The API key used to authenticate with Checkly." } } }; @@ -2488,6 +2491,9 @@ export const SecretSyncs = { environmentName: "The Railway environment to sync secrets to.", serviceId: "The Railway service that secrets should be synced to.", serviceName: "The Railway service that secrets should be synced to." + }, + CHECKLY: { + accountId: "The ID of the Checkly account to sync secrets to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index f692e700f..62d4ab979 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -39,6 +39,10 @@ import { CamundaConnectionListItemSchema, SanitizedCamundaConnectionSchema } from "@app/services/app-connection/camunda"; +import { + ChecklyConnectionListItemSchema, + SanitizedChecklyConnectionSchema +} from "@app/services/app-connection/checkly"; import { CloudflareConnectionListItemSchema, SanitizedCloudflareConnectionSchema @@ -128,7 +132,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedCloudflareConnectionSchema.options, ...SanitizedBitbucketConnectionSchema.options, ...SanitizedZabbixConnectionSchema.options, - ...SanitizedRailwayConnectionSchema.options + ...SanitizedRailwayConnectionSchema.options, + ...SanitizedChecklyConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -163,7 +168,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ CloudflareConnectionListItemSchema, BitbucketConnectionListItemSchema, ZabbixConnectionListItemSchema, - RailwayConnectionListItemSchema + RailwayConnectionListItemSchema, + ChecklyConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/checkly-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/checkly-connection-router.ts new file mode 100644 index 000000000..bbe3fbbfb --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/checkly-connection-router.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateChecklyConnectionSchema, + SanitizedChecklyConnectionSchema, + UpdateChecklyConnectionSchema +} from "@app/services/app-connection/checkly"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerChecklyConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Checkly, + server, + sanitizedResponseSchema: SanitizedChecklyConnectionSchema, + createSchema: CreateChecklyConnectionSchema, + updateSchema: UpdateChecklyConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/accounts`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + accounts: z + .object({ + name: z.string(), + id: z.string(), + runtimeId: z.string() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const accounts = await server.services.appConnection.checkly.listAccounts(connectionId, req.permission); + + return { accounts }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 524abc18d..67a420cb7 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -11,6 +11,7 @@ import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-r import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; +import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; @@ -68,5 +69,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Checkly, + server, + responseSchema: ChecklySyncSchema, + createSchema: CreateChecklySyncSchema, + updateSchema: UpdateChecklySyncSchema + }); 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 4e1d185a8..67b865d31 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -8,6 +8,7 @@ import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configurati import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; +import { registerChecklySyncRouter } from "./checkly-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; @@ -54,5 +55,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index b9c405654..e47ff08a9 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -30,7 +30,8 @@ export enum AppConnection { Cloudflare = "cloudflare", Zabbix = "zabbix", Railway = "railway", - Bitbucket = "bitbucket" + Bitbucket = "bitbucket", + Checkly = "checkly" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 9dadcc4e5..c75745edb 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -56,6 +56,7 @@ import { validateBitbucketConnectionCredentials } from "./bitbucket"; import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda"; +import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly"; import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum"; import { getCloudflareConnectionListItem, @@ -146,7 +147,8 @@ export const listAppConnectionOptions = () => { getCloudflareConnectionListItem(), getZabbixConnectionListItem(), getRailwayConnectionListItem(), - getBitbucketConnectionListItem() + getBitbucketConnectionListItem(), + getChecklyConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -229,7 +231,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -287,6 +290,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case LdapConnectionMethod.SimpleBind: return "Simple Bind"; case RenderConnectionMethod.ApiKey: + case ChecklyConnectionMethod.ApiKey: return "API Key"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -350,7 +354,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, [AppConnection.Zabbix]: platformManagedCredentialsNotSupported, [AppConnection.Railway]: platformManagedCredentialsNotSupported, - [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported + [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, + [AppConnection.Checkly]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 4f274516c..519a4e5ea 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -32,7 +32,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Cloudflare]: "Cloudflare", [AppConnection.Zabbix]: "Zabbix", [AppConnection.Railway]: "Railway", - [AppConnection.Bitbucket]: "Bitbucket" + [AppConnection.Bitbucket]: "Bitbucket", + [AppConnection.Checkly]: "Checkly" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -67,5 +68,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -256,6 +263,7 @@ export type TAppConnectionInput = { id: string } & ( | TBitbucketConnectionInput | TZabbixConnectionInput | TRailwayConnectionInput + | TChecklyConnectionInput ); export type TSqlConnectionInput = @@ -302,7 +310,8 @@ export type TAppConnectionConfig = | TCloudflareConnectionConfig | TBitbucketConnectionConfig | TZabbixConnectionConfig - | TRailwayConnectionConfig; + | TRailwayConnectionConfig + | TChecklyConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -336,7 +345,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateCloudflareConnectionCredentialsSchema | TValidateBitbucketConnectionCredentialsSchema | TValidateZabbixConnectionCredentialsSchema - | TValidateRailwayConnectionCredentialsSchema; + | TValidateRailwayConnectionCredentialsSchema + | TValidateChecklyConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/checkly/checkly-connection-constants.ts b/backend/src/services/app-connection/checkly/checkly-connection-constants.ts new file mode 100644 index 000000000..c62f59c0a --- /dev/null +++ b/backend/src/services/app-connection/checkly/checkly-connection-constants.ts @@ -0,0 +1,3 @@ +export enum ChecklyConnectionMethod { + ApiKey = "api-key" +} diff --git a/backend/src/services/app-connection/checkly/checkly-connection-fns.ts b/backend/src/services/app-connection/checkly/checkly-connection-fns.ts new file mode 100644 index 000000000..96df54e85 --- /dev/null +++ b/backend/src/services/app-connection/checkly/checkly-connection-fns.ts @@ -0,0 +1,35 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { ChecklyConnectionMethod } from "./checkly-connection-constants"; +import { ChecklyPublicAPI } from "./checkly-connection-public-client"; +import { TChecklyConnectionConfig } from "./checkly-connection-types"; + +export const getChecklyConnectionListItem = () => { + return { + name: "Checkly" as const, + app: AppConnection.Checkly as const, + methods: Object.values(ChecklyConnectionMethod) + }; +}; + +export const validateChecklyConnectionCredentials = async (config: TChecklyConnectionConfig) => { + try { + await ChecklyPublicAPI.healthcheck(config); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + + throw new BadRequestError({ + message: "Unable to validate connection - verify credentials" + }); + } + + return config.credentials; +}; diff --git a/backend/src/services/app-connection/checkly/checkly-connection-public-client.ts b/backend/src/services/app-connection/checkly/checkly-connection-public-client.ts new file mode 100644 index 000000000..4e5db231f --- /dev/null +++ b/backend/src/services/app-connection/checkly/checkly-connection-public-client.ts @@ -0,0 +1,186 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable class-methods-use-this */ +import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxiosError } from "axios"; + +import { createRequestClient } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { ChecklyConnectionMethod } from "./checkly-connection-constants"; +import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types"; + +export function getChecklyAuthHeaders( + connection: TChecklyConnectionConfig, + accountId?: string +): Record { + switch (connection.method) { + case ChecklyConnectionMethod.ApiKey: + return { + Authorization: `Bearer ${connection.credentials.apiKey}`, + ...(accountId && { "X-Checkly-Account": accountId }) + }; + default: + throw new Error(`Unsupported Checkly connection method`); + } +} + +export function getChecklyRatelimiter(response: AxiosResponse): { + maxAttempts: number; + isRatelimited: boolean; + wait: () => Promise; +} { + const wait = () => { + return new Promise((res) => { + setTimeout(res, 60 * 1000); // Wait for 60 seconds + }); + }; + + return { + isRatelimited: response.status === HttpStatusCode.TooManyRequests, + wait, + maxAttempts: 3 + }; +} + +class ChecklyPublicClient { + private client: AxiosInstance; + + constructor() { + this.client = createRequestClient({ + baseURL: IntegrationUrls.CHECKLY_API_URL, + headers: { + "Content-Type": "application/json" + } + }); + } + + async send( + connection: TChecklyConnectionConfig, + config: AxiosRequestConfig & { accountId?: string }, + retryAttempt = 0 + ): Promise { + const response = await this.client.request({ + ...config, + timeout: 1000 * 60, // 60 seconds timeout + validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests, + headers: getChecklyAuthHeaders(connection, config.accountId) + }); + const limiter = getChecklyRatelimiter(response); + + if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) { + await limiter.wait(); + return this.send(connection, config, retryAttempt + 1); + } + + return response.data; + } + + healthcheck(connection: TChecklyConnectionConfig) { + switch (connection.method) { + case ChecklyConnectionMethod.ApiKey: + return this.getChecklyAccounts(connection); + default: + throw new Error(`Unsupported Checkly connection method`); + } + } + + async getVariables(connection: TChecklyConnectionConfig, accountId: string, limit: number = 50, page: number = 1) { + const res = await this.send(connection, { + accountId, + method: "GET", + url: `/v1/variables`, + params: { + limit, + page + } + }); + + return res; + } + + async createVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) { + const res = await this.send(connection, { + accountId, + method: "POST", + url: `/v1/variables`, + data: variable + }); + + return res; + } + + async updateVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) { + const res = await this.send(connection, { + accountId, + method: "PUT", + url: `/v1/variables/${variable.key}`, + data: variable + }); + + return res; + } + + async getVariable(connection: TChecklyConnectionConfig, accountId: string, variable: Pick) { + try { + const res = await this.send(connection, { + accountId, + method: "GET", + url: `/v1/variables/${variable.key}` + }); + + return res; + } catch (error) { + if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) { + return null; + } + + throw error; + } + } + + async upsertVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) { + const res = await this.getVariable(connection, accountId, variable); + + if (!res) { + return this.createVariable(connection, accountId, variable); + } + + await this.updateVariable(connection, accountId, variable); + + return res; + } + + async deleteVariable( + connection: TChecklyConnectionConfig, + accountId: string, + variable: Pick + ) { + try { + const res = await this.send(connection, { + accountId, + method: "DELETE", + url: `/v1/variables/${variable.key}` + }); + + return res; + } catch (error) { + if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) { + return null; + } + + throw error; + } + } + + async getChecklyAccounts(connection: TChecklyConnectionConfig) { + // This endpoint is in beta and might be subject to changes + // Refer: https://developers.checklyhq.com/reference/getv1accounts + const res = await this.send(connection, { + method: "GET", + url: `/v1/accounts` + }); + + return res; + } +} + +export const ChecklyPublicAPI = new ChecklyPublicClient(); diff --git a/backend/src/services/app-connection/checkly/checkly-connection-schemas.ts b/backend/src/services/app-connection/checkly/checkly-connection-schemas.ts new file mode 100644 index 000000000..174e5bce0 --- /dev/null +++ b/backend/src/services/app-connection/checkly/checkly-connection-schemas.ts @@ -0,0 +1,62 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { ChecklyConnectionMethod } from "./checkly-connection-constants"; + +export const ChecklyConnectionMethodSchema = z + .nativeEnum(ChecklyConnectionMethod) + .describe(AppConnections.CREATE(AppConnection.Checkly).method); + +export const ChecklyConnectionAccessTokenCredentialsSchema = z.object({ + apiKey: z.string().trim().min(1, "API Key required").max(255).describe(AppConnections.CREDENTIALS.CHECKLY.apiKey) +}); + +const BaseChecklyConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.Checkly) +}); + +export const ChecklyConnectionSchema = BaseChecklyConnectionSchema.extend({ + method: ChecklyConnectionMethodSchema, + credentials: ChecklyConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedChecklyConnectionSchema = z.discriminatedUnion("method", [ + BaseChecklyConnectionSchema.extend({ + method: ChecklyConnectionMethodSchema, + credentials: ChecklyConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateChecklyConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: ChecklyConnectionMethodSchema, + credentials: ChecklyConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Checkly).credentials + ) + }) +]); + +export const CreateChecklyConnectionSchema = ValidateChecklyConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Checkly) +); + +export const UpdateChecklyConnectionSchema = z + .object({ + credentials: ChecklyConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Checkly).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Checkly)); + +export const ChecklyConnectionListItemSchema = z.object({ + name: z.literal("Checkly"), + app: z.literal(AppConnection.Checkly), + methods: z.nativeEnum(ChecklyConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/checkly/checkly-connection-service.ts b/backend/src/services/app-connection/checkly/checkly-connection-service.ts new file mode 100644 index 000000000..c3598320f --- /dev/null +++ b/backend/src/services/app-connection/checkly/checkly-connection-service.ts @@ -0,0 +1,30 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { ChecklyPublicAPI } from "./checkly-connection-public-client"; +import { TChecklyConnection } from "./checkly-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const checklyConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listAccounts = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Checkly, connectionId, actor); + try { + const accounts = await ChecklyPublicAPI.getChecklyAccounts(appConnection); + return accounts!; + } catch (error) { + logger.error(error, "Failed to list accounts on Checkly"); + return []; + } + }; + + return { + listAccounts + }; +}; diff --git a/backend/src/services/app-connection/checkly/checkly-connection-types.ts b/backend/src/services/app-connection/checkly/checkly-connection-types.ts new file mode 100644 index 000000000..e8bb242ba --- /dev/null +++ b/backend/src/services/app-connection/checkly/checkly-connection-types.ts @@ -0,0 +1,35 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + ChecklyConnectionSchema, + CreateChecklyConnectionSchema, + ValidateChecklyConnectionCredentialsSchema +} from "./checkly-connection-schemas"; + +export type TChecklyConnection = z.infer; + +export type TChecklyConnectionInput = z.infer & { + app: AppConnection.Checkly; +}; + +export type TValidateChecklyConnectionCredentialsSchema = typeof ValidateChecklyConnectionCredentialsSchema; + +export type TChecklyConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TChecklyVariable = { + key: string; + value: string; + locked: boolean; + secret: boolean; +}; + +export type TChecklyAccount = { + id: string; + name: string; + runtimeId: string; +}; diff --git a/backend/src/services/app-connection/checkly/index.ts b/backend/src/services/app-connection/checkly/index.ts new file mode 100644 index 000000000..341413feb --- /dev/null +++ b/backend/src/services/app-connection/checkly/index.ts @@ -0,0 +1,4 @@ +export * from "./checkly-connection-constants"; +export * from "./checkly-connection-fns"; +export * from "./checkly-connection-schemas"; +export * from "./checkly-connection-types"; diff --git a/backend/src/services/secret-sync/checkly/checkly-sync-constants.ts b/backend/src/services/secret-sync/checkly/checkly-sync-constants.ts new file mode 100644 index 000000000..9c21fef63 --- /dev/null +++ b/backend/src/services/secret-sync/checkly/checkly-sync-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 CHECKLY_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Checkly", + destination: SecretSync.Checkly, + connection: AppConnection.Checkly, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/checkly/checkly-sync-fns.ts b/backend/src/services/secret-sync/checkly/checkly-sync-fns.ts new file mode 100644 index 000000000..eded130bb --- /dev/null +++ b/backend/src/services/secret-sync/checkly/checkly-sync-fns.ts @@ -0,0 +1,102 @@ +/* eslint-disable no-continue */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { ChecklyPublicAPI } from "@app/services/app-connection/checkly/checkly-connection-public-client"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; +import { TSecretMap } from "../secret-sync-types"; +import { TChecklySyncWithCredentials } from "./checkly-sync-types"; + +export const ChecklySyncFns = { + async getSecrets(secretSync: TChecklySyncWithCredentials) { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + async syncSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) { + const { + environment, + syncOptions: { disableSecretDeletion, keySchema } + } = secretSync; + + const config = secretSync.destinationConfig; + + const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId); + + const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable])); + + for await (const key of Object.keys(secretMap)) { + try { + const entry = secretMap[key]; + + // If value is empty, we skip the upsert - checkly does not allow empty values + if (entry.value.trim() === "") { + // Delete the secret from Checkly if its empty + if (!disableSecretDeletion) { + await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { + key + }); + } + continue; // Skip empty values + } + + await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, { + key, + value: entry.value, + secret: true, + locked: true + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (disableSecretDeletion) return; + + for await (const key of Object.keys(checklySecrets)) { + try { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + + if (!secretMap[key]) { + await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { + key + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + }, + + async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) { + const config = secretSync.destinationConfig; + + const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId); + + const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable])); + + for await (const secret of Object.keys(checklySecrets)) { + try { + if (secret in secretMap) { + await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { + key: secret + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: secret + }); + } + } + } +}; diff --git a/backend/src/services/secret-sync/checkly/checkly-sync-schemas.ts b/backend/src/services/secret-sync/checkly/checkly-sync-schemas.ts new file mode 100644 index 000000000..04f444357 --- /dev/null +++ b/backend/src/services/secret-sync/checkly/checkly-sync-schemas.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +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 ChecklySyncDestinationConfigSchema = z.object({ + accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"), + accountName: z.string().min(1, "Account Name is required").max(255, "Account ID must be less than 255 characters") +}); + +const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const ChecklySyncSchema = BaseSecretSyncSchema(SecretSync.Checkly, ChecklySyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Checkly), + destinationConfig: ChecklySyncDestinationConfigSchema +}); + +export const CreateChecklySyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Checkly, + ChecklySyncOptionsConfig +).extend({ + destinationConfig: ChecklySyncDestinationConfigSchema +}); + +export const UpdateChecklySyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Checkly, + ChecklySyncOptionsConfig +).extend({ + destinationConfig: ChecklySyncDestinationConfigSchema.optional() +}); + +export const ChecklySyncListItemSchema = z.object({ + name: z.literal("Checkly"), + connection: z.literal(AppConnection.Checkly), + destination: z.literal(SecretSync.Checkly), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/checkly/checkly-sync-types.ts b/backend/src/services/secret-sync/checkly/checkly-sync-types.ts new file mode 100644 index 000000000..6434cec39 --- /dev/null +++ b/backend/src/services/secret-sync/checkly/checkly-sync-types.ts @@ -0,0 +1,23 @@ +import z from "zod"; + +import { TChecklyConnection, TChecklyVariable } from "@app/services/app-connection/checkly"; + +import { ChecklySyncListItemSchema, ChecklySyncSchema, CreateChecklySyncSchema } from "./checkly-sync-schemas"; + +export type TChecklySyncListItem = z.infer; + +export type TChecklySync = z.infer; + +export type TChecklySyncInput = z.infer; + +export type TChecklySyncWithCredentials = TChecklySync & { + connection: TChecklyConnection; +}; + +export type TChecklySecret = TChecklyVariable; + +export type TChecklyVariablesGraphResponse = { + data: { + variables: Record; + }; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 64a191a22..7e377ee13 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -24,7 +24,8 @@ export enum SecretSync { CloudflareWorkers = "cloudflare-workers", Zabbix = "zabbix", - Railway = "railway" + Railway = "railway", + Checkly = "checkly" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 7dc19d3e9..a755c97b7 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -29,6 +29,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; +import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants"; +import { ChecklySyncFns } from "./checkly/checkly-sync-fns"; 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"; @@ -76,7 +78,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, - [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION + [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION, + [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -250,6 +253,8 @@ export const SecretSyncFns = { return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Railway: return RailwaySyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Checkly: + return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -351,6 +356,9 @@ export const SecretSyncFns = { case SecretSync.Railway: secretMap = await RailwaySyncFns.getSecrets(secretSync); break; + case SecretSync.Checkly: + secretMap = await ChecklySyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -434,6 +442,8 @@ export const SecretSyncFns = { return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Railway: return RailwaySyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Checkly: + return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index dd1d5b146..a83418d15 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -27,7 +27,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.CloudflareWorkers]: "Cloudflare Workers", [SecretSync.Zabbix]: "Zabbix", - [SecretSync.Railway]: "Railway" + [SecretSync.Railway]: "Railway", + [SecretSync.Checkly]: "Checkly" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -56,7 +57,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, [SecretSync.Zabbix]: AppConnection.Zabbix, - [SecretSync.Railway]: AppConnection.Railway + [SecretSync.Railway]: AppConnection.Railway, + [SecretSync.Checkly]: AppConnection.Checkly }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -85,5 +87,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular, [SecretSync.Zabbix]: SecretSyncPlanType.Regular, - [SecretSync.Railway]: SecretSyncPlanType.Regular + [SecretSync.Railway]: SecretSyncPlanType.Regular, + [SecretSync.Checkly]: 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 998328b85..3fcef9493 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -72,6 +72,12 @@ import { TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; +import { + TChecklySync, + TChecklySyncInput, + TChecklySyncListItem, + TChecklySyncWithCredentials +} from "./checkly/checkly-sync-types"; import { TCloudflarePagesSync, TCloudflarePagesSyncInput, @@ -152,7 +158,8 @@ export type TSecretSync = | TCloudflarePagesSync | TCloudflareWorkersSync | TZabbixSync - | TRailwaySync; + | TRailwaySync + | TChecklySync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -179,7 +186,8 @@ export type TSecretSyncWithCredentials = | TCloudflarePagesSyncWithCredentials | TCloudflareWorkersSyncWithCredentials | TZabbixSyncWithCredentials - | TRailwaySyncWithCredentials; + | TRailwaySyncWithCredentials + | TChecklySyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -206,7 +214,8 @@ export type TSecretSyncInput = | TCloudflarePagesSyncInput | TCloudflareWorkersSyncInput | TZabbixSyncInput - | TRailwaySyncInput; + | TRailwaySyncInput + | TChecklySyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -233,7 +242,8 @@ export type TSecretSyncListItem = | TCloudflarePagesSyncListItem | TCloudflareWorkersSyncListItem | TZabbixSyncListItem - | TRailwaySyncListItem; + | TRailwaySyncListItem + | TChecklySyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/app-connections/checkly/available.mdx b/docs/api-reference/endpoints/app-connections/checkly/available.mdx new file mode 100644 index 000000000..c07f1e11a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/checkly/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/checkly/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/checkly/create.mdx b/docs/api-reference/endpoints/app-connections/checkly/create.mdx new file mode 100644 index 000000000..33aa7940e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/checkly/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/checkly" +--- + + + Check out the configuration docs for [Checkly Connections](/integrations/app-connections/checkly) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/checkly/delete.mdx b/docs/api-reference/endpoints/app-connections/checkly/delete.mdx new file mode 100644 index 000000000..31812e054 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/checkly/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/checkly/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/checkly/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/checkly/get-by-id.mdx new file mode 100644 index 000000000..f700275b3 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/checkly/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/checkly/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/checkly/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/checkly/get-by-name.mdx new file mode 100644 index 000000000..15827c32e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/checkly/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/checkly/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/checkly/list.mdx b/docs/api-reference/endpoints/app-connections/checkly/list.mdx new file mode 100644 index 000000000..a86b6259a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/checkly/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/checkly" +--- diff --git a/docs/api-reference/endpoints/app-connections/checkly/update.mdx b/docs/api-reference/endpoints/app-connections/checkly/update.mdx new file mode 100644 index 000000000..dcf03c8ef --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/checkly/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/checkly/{connectionId}" +--- + + + Check out the configuration docs for [Checkly Connections](/integrations/app-connections/checkly) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/create.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/create.mdx new file mode 100644 index 000000000..a0b638568 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/checkly" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/delete.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/delete.mdx new file mode 100644 index 000000000..4c4ee0b00 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/checkly/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/get-by-id.mdx new file mode 100644 index 000000000..e61a942c9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/checkly/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/get-by-name.mdx new file mode 100644 index 000000000..ff41b9629 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/checkly/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/list.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/list.mdx new file mode 100644 index 000000000..cb0a57794 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/checkly" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/remove-secrets.mdx new file mode 100644 index 000000000..666d2657b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/checkly/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/sync-secrets.mdx new file mode 100644 index 000000000..7204f528a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/checkly/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/checkly/update.mdx b/docs/api-reference/endpoints/secret-syncs/checkly/update.mdx new file mode 100644 index 000000000..a5aa9b693 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/checkly/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/checkly/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 62149ca99..fd75a8cde 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -472,6 +472,7 @@ "integrations/app-connections/azure-key-vault", "integrations/app-connections/bitbucket", "integrations/app-connections/camunda", + "integrations/app-connections/checkly", "integrations/app-connections/cloudflare", "integrations/app-connections/databricks", "integrations/app-connections/flyio", @@ -513,6 +514,7 @@ "integrations/secret-syncs/azure-devops", "integrations/secret-syncs/azure-key-vault", "integrations/secret-syncs/camunda", + "integrations/secret-syncs/checkly", "integrations/secret-syncs/cloudflare-pages", "integrations/secret-syncs/cloudflare-workers", "integrations/secret-syncs/databricks", @@ -1328,6 +1330,17 @@ "api-reference/endpoints/app-connections/camunda/delete" ] }, + { + "group": "Checkly", + "pages": [ + "api-reference/endpoints/app-connections/checkly/list", + "api-reference/endpoints/app-connections/checkly/get-by-id", + "api-reference/endpoints/app-connections/checkly/get-by-name", + "api-reference/endpoints/app-connections/checkly/create", + "api-reference/endpoints/app-connections/checkly/update", + "api-reference/endpoints/app-connections/checkly/delete" + ] + }, { "group": "Cloudflare", "pages": [ @@ -1708,6 +1721,19 @@ "api-reference/endpoints/secret-syncs/camunda/remove-secrets" ] }, + { + "group": "Checkly", + "pages": [ + "api-reference/endpoints/secret-syncs/checkly/list", + "api-reference/endpoints/secret-syncs/checkly/get-by-id", + "api-reference/endpoints/secret-syncs/checkly/get-by-name", + "api-reference/endpoints/secret-syncs/checkly/create", + "api-reference/endpoints/secret-syncs/checkly/update", + "api-reference/endpoints/secret-syncs/checkly/delete", + "api-reference/endpoints/secret-syncs/checkly/sync-secrets", + "api-reference/endpoints/secret-syncs/checkly/remove-secrets" + ] + }, { "group": "Cloudflare Pages", "pages": [ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-api-keys.png b/docs/images/app-connections/checkly/checkly-app-connection-api-keys.png new file mode 100644 index 000000000..c03dcc806 Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-api-keys.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-create-api-key.png b/docs/images/app-connections/checkly/checkly-app-connection-create-api-key.png new file mode 100644 index 000000000..56fe4ad36 Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-create-api-key.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-create-form.png b/docs/images/app-connections/checkly/checkly-app-connection-create-form.png new file mode 100644 index 000000000..e743b8079 Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-create-form.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-form.png b/docs/images/app-connections/checkly/checkly-app-connection-form.png new file mode 100644 index 000000000..818138c56 Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-form.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-generated.png b/docs/images/app-connections/checkly/checkly-app-connection-generated.png new file mode 100644 index 000000000..cb1ced15f Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-generated.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-key-generated.png b/docs/images/app-connections/checkly/checkly-app-connection-key-generated.png new file mode 100644 index 000000000..7fbb66502 Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-key-generated.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-option.png b/docs/images/app-connections/checkly/checkly-app-connection-option.png new file mode 100644 index 000000000..8189b1826 Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-option.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-profile.png b/docs/images/app-connections/checkly/checkly-app-connection-profile.png new file mode 100644 index 000000000..b9974683f Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-profile.png differ diff --git a/docs/images/app-connections/checkly/checkly-app-connection-user-settings.png b/docs/images/app-connections/checkly/checkly-app-connection-user-settings.png new file mode 100644 index 000000000..a918e62cf Binary files /dev/null and b/docs/images/app-connections/checkly/checkly-app-connection-user-settings.png differ diff --git a/docs/images/app-connections/railway/SCR-20250712-pjrc.png b/docs/images/app-connections/railway/SCR-20250712-pjrc.png new file mode 100644 index 000000000..7c39c0d5b Binary files /dev/null and b/docs/images/app-connections/railway/SCR-20250712-pjrc.png differ diff --git a/docs/images/secret-syncs/checkly/checkly-sync-created.png b/docs/images/secret-syncs/checkly/checkly-sync-created.png new file mode 100644 index 000000000..70ff19b20 Binary files /dev/null and b/docs/images/secret-syncs/checkly/checkly-sync-created.png differ diff --git a/docs/images/secret-syncs/checkly/checkly-sync-destination.png b/docs/images/secret-syncs/checkly/checkly-sync-destination.png new file mode 100644 index 000000000..bb3df8d94 Binary files /dev/null and b/docs/images/secret-syncs/checkly/checkly-sync-destination.png differ diff --git a/docs/images/secret-syncs/checkly/checkly-sync-details.png b/docs/images/secret-syncs/checkly/checkly-sync-details.png new file mode 100644 index 000000000..536ca8ff9 Binary files /dev/null and b/docs/images/secret-syncs/checkly/checkly-sync-details.png differ diff --git a/docs/images/secret-syncs/checkly/checkly-sync-options.png b/docs/images/secret-syncs/checkly/checkly-sync-options.png new file mode 100644 index 000000000..24caa2a9b Binary files /dev/null and b/docs/images/secret-syncs/checkly/checkly-sync-options.png differ diff --git a/docs/images/secret-syncs/checkly/checkly-sync-review.png b/docs/images/secret-syncs/checkly/checkly-sync-review.png new file mode 100644 index 000000000..29f50a0a6 Binary files /dev/null and b/docs/images/secret-syncs/checkly/checkly-sync-review.png differ diff --git a/docs/images/secret-syncs/checkly/checkly-sync-source.png b/docs/images/secret-syncs/checkly/checkly-sync-source.png new file mode 100644 index 000000000..0d2a83f19 Binary files /dev/null and b/docs/images/secret-syncs/checkly/checkly-sync-source.png differ diff --git a/docs/images/secret-syncs/checkly/select-option.png b/docs/images/secret-syncs/checkly/select-option.png new file mode 100644 index 000000000..7c39c0d5b Binary files /dev/null and b/docs/images/secret-syncs/checkly/select-option.png differ diff --git a/docs/integrations/app-connections/checkly.mdx b/docs/integrations/app-connections/checkly.mdx new file mode 100644 index 000000000..38234744d --- /dev/null +++ b/docs/integrations/app-connections/checkly.mdx @@ -0,0 +1,106 @@ +--- +title: "Checkly Connection" +description: "Learn how to configure a Checkly Connection for Infisical." +--- + +Infisical supports the use of [API Keys](https://app.checklyhq.com/settings/user/api-keys) to connect with Checkly. + + Checkly requires the account user to have Read/Write or Admin permissions + + +## Create a Checkly API Token + + + + ![Dashboard Page](/images/app-connections/checkly/checkly-app-connection-profile.png) + + + ![User Settings Page](/images/app-connections/checkly/checkly-app-connection-api-keys.png) + + + ![Api Keys Page](/images/app-connections/checkly/checkly-app-connection-create-api-key.png) + + + Provide a descriptive name for the token. + + ![Enter Name](/images/app-connections/checkly/checkly-app-connection-create-form.png) + + + + ![Create Token](/images/app-connections/checkly/checkly-app-connection-key-generated.png) + + + +## Create a Checkly Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click **+ Add Connection** and choose **Checkly Connection** from the list of integrations. + + ![Select Checkly Connection](/images/app-connections/checkly/checkly-app-connection-option.png) + + + Complete the form by providing: + - A descriptive name for the connection + - An optional description + - The API Key value from the previous step + + ![Checkly Connection Modal](/images/app-connections/checkly/checkly-app-connection-form.png) + + + After submitting the form, your **Checkly Connection** will be successfully created and ready to use with your Infisical projects. + + ![Checkly Connection Created](/images/app-connections/checkly/checkly-app-connection-generated.png) + + + + + + + To create a Checkly Connection via API, send a request to the [Create Checkly Connection](/api-reference/endpoints/app-connections/checkly/create) endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/checkly \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-checkly-connection", + "method": "api-key", + "credentials": { + "apiKey": "[API KEY]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-checkly-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "checkly", + "method": "api-key", + "credentials": {} + } + } + ``` + + + diff --git a/docs/integrations/secret-syncs/checkly.mdx b/docs/integrations/secret-syncs/checkly.mdx new file mode 100644 index 000000000..4599fe634 --- /dev/null +++ b/docs/integrations/secret-syncs/checkly.mdx @@ -0,0 +1,163 @@ +--- +title: "Checkly Sync" +description: "Learn how to configure a Checkly Sync for Infisical." +--- + +**Prerequisites:** + +- Create a [Checkly Connection](/integrations/app-connections/checkly) + + + + + + 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) + + + ![Select Checkly](/images/secret-syncs/checkly/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/checkly/checkly-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). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/checkly/checkly-sync-destination.png) + + - **Checkly Connection**: The Checkly Connection to authenticate with. + - **Account**: The Checkly account to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/checkly/checkly-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. + + Checkly does not support importing secrets. + + - **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. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **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. + + + Configure the **Details** of your Checkly Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/checkly/checkly-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Checkly Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/checkly/checkly-sync-review.png) + + + If enabled, your Checkly Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/checkly/checkly-sync-created.png) + + + + + To create a **Checkly Sync**, make an API request to the [Create Checkly Sync](/api-reference/endpoints/secret-syncs/checkly/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/checkly \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-checkly-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "destinationConfig": { + "accountId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "accountName": "Example Company" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-checkly-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "checkly", + "name": "my-checkly-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "checkly", + "destinationConfig": { + "accountId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "accountName": "Example Company", + } + } + } + ``` + + + diff --git a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx index 6940f0ab3..fed312ace 100644 --- a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx @@ -63,6 +63,7 @@ export const SecretSyncSelect = ({ onSelect }: Props) => { const { image, name } = SECRET_SYNC_MAP[destination]; return ( + + + + + + + ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ChecklySyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ChecklySyncDestinationCol.tsx new file mode 100644 index 000000000..4a21c4b0b --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/ChecklySyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TChecklySync } from "@app/hooks/api/secretSyncs/types/checkly-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TChecklySync; +}; + +export const ChecklySyncDestinationCol = ({ 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 5a4d451fc..54ee79fe7 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 @@ -7,6 +7,7 @@ import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfiguration import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol"; import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; +import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol"; import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol"; import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; @@ -82,6 +83,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Railway: return ; + case SecretSync.Checkly: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` 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 2e892a91c..bc4bd28f5 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 @@ -163,8 +163,12 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { } break; case SecretSync.Railway: - primaryText = "Railway Project"; - secondaryText = destinationConfig.projectName; + primaryText = destinationConfig.projectName; + secondaryText = "Railway Project"; + break; + case SecretSync.Checkly: + primaryText = destinationConfig.accountName; + secondaryText = "Checkly Account"; break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ChecklySyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ChecklySyncDestinationSection.tsx new file mode 100644 index 000000000..4920bb16b --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/ChecklySyncDestinationSection.tsx @@ -0,0 +1,12 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TChecklySync } from "@app/hooks/api/secretSyncs/types/checkly-sync"; + +type Props = { + secretSync: TChecklySync; +}; + +export const ChecklySyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + return {destinationConfig.accountName}; +}; 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 71755c7e9..68e6f4325 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -18,6 +18,7 @@ import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigura import { AzureDevOpsSyncDestinationSection } from "./AzureDevOpsSyncDestinationSection"; import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; +import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection"; import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection"; import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; @@ -126,6 +127,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Railway: DestinationComponents = ; break; + case SecretSync.Checkly: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } 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 60710e3ef..456425f5c 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -63,6 +63,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.CloudflareWorkers: case SecretSync.Zabbix: case SecretSync.Railway: + case SecretSync.Checkly: AdditionalSyncOptionsComponent = null; break; default: