diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3657fbb2784..77719ed99b4 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2711,6 +2711,9 @@ export const AppConnections = { FLYIO: { accessToken: "The Access Token used to access fly.io." }, + DEVIN: { + apiKey: "The Devin service-user API key used to authenticate against the Devin v3 API." + }, GITLAB: { instanceUrl: "The GitLab instance URL to connect with.", accessToken: "The Access Token used to access GitLab.", @@ -3020,6 +3023,9 @@ export const SecretSyncs = { FLYIO: { appId: "The ID of the Fly.io app to sync secrets to." }, + DEVIN: { + orgId: "The Devin organization ID to sync secrets to." + }, GITLAB: { projectId: "The GitLab Project ID to sync secrets to.", projectName: "The GitLab Project Name to sync secrets to.", diff --git a/backend/src/lib/knex/scim.ts b/backend/src/lib/knex/scim.ts index 7418768adff..3758ed984b0 100644 --- a/backend/src/lib/knex/scim.ts +++ b/backend/src/lib/knex/scim.ts @@ -3,8 +3,8 @@ import { Compare, Filter, parse } from "scim2-parse-filter"; import { TableName } from "@app/db/schemas"; -import { sanitizeSqlLikeString } from "../fn"; import { BadRequestError } from "../errors"; +import { sanitizeSqlLikeString } from "../fn"; const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => { if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") { 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 66de308e1bc..fb46f596ec0 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 @@ -74,6 +74,7 @@ import { SanitizedDatabricksConnectionSchema } from "@app/services/app-connection/databricks"; import { DbtConnectionListItemSchema, SanitizedDbtConnectionSchema } from "@app/services/app-connection/dbt"; +import { DevinConnectionListItemSchema, SanitizedDevinConnectionSchema } from "@app/services/app-connection/devin"; import { DigitalOceanConnectionListItemSchema, SanitizedDigitalOceanConnectionSchema @@ -229,6 +230,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedDbtConnectionSchema.options, ...SanitizedOpenRouterConnectionSchema.options, ...SanitizedAnthropicConnectionSchema.options, + ...SanitizedDevinConnectionSchema.options, ...SanitizedAzureEntraIdConnectionSchema.options, ...SanitizedVenafiConnectionSchema.options, ...SanitizedExternalInfisicalConnectionSchema.options, @@ -293,7 +295,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ ExternalInfisicalConnectionListItemSchema, DopplerConnectionListItemSchema, NetScalerConnectionListItemSchema, - AnthropicConnectionListItemSchema + AnthropicConnectionListItemSchema, + DevinConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/devin-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/devin-connection-router.ts new file mode 100644 index 00000000000..fa938be2fdd --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/devin-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateDevinConnectionSchema, + SanitizedDevinConnectionSchema, + UpdateDevinConnectionSchema +} from "@app/services/app-connection/devin"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerDevinConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Devin, + server, + sanitizedResponseSchema: SanitizedDevinConnectionSchema, + createSchema: CreateDevinConnectionSchema, + updateSchema: UpdateDevinConnectionSchema + }); +}; 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 8a9fc98591a..2b8535315da 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -21,6 +21,7 @@ import { registerCircleCIConnectionRouter } from "./circleci-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDbtConnectionRouter } from "./dbt-connection-router"; +import { registerDevinConnectionRouter } from "./devin-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router"; import { registerDopplerConnectionRouter } from "./doppler-connection-router"; @@ -118,5 +119,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Devin, + server, + responseSchema: DevinSyncSchema, + createSchema: CreateDevinSyncSchema, + updateSchema: UpdateDevinSyncSchema + }); 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 abe6d17f251..a8d40071f7a 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -16,6 +16,7 @@ import { registerCircleCISyncRouter } from "./circleci-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; +import { registerDevinSyncRouter } from "./devin-sync-router"; import { registerDigitalOceanAppPlatformSyncRouter } from "./digital-ocean-app-platform-sync-router"; import { registerExternalInfisicalSyncRouter } from "./external-infisical-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router"; @@ -77,5 +78,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 53ea4f312e8..7146d400c8f 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -55,7 +55,8 @@ export enum AppConnection { ExternalInfisical = "external-infisical", Doppler = "doppler", NetScaler = "netscaler", - Anthropic = "anthropic" + Anthropic = "anthropic", + Devin = "devin" } 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 ea24b7e4c01..30426997010 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -108,6 +108,7 @@ import { validateDatabricksConnectionCredentials } from "./databricks/databricks-connection-fns"; import { DbtConnectionMethod, getDbtConnectionListItem, validateDbtConnectionCredentials } from "./dbt"; +import { DevinConnectionMethod, getDevinConnectionListItem, validateDevinConnectionCredentials } from "./devin"; import { DigitalOceanConnectionMethod, getDigitalOceanConnectionListItem, @@ -289,6 +290,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getSmbConnectionListItem(), getOpenRouterConnectionListItem(), getAnthropicConnectionListItem(), + getDevinConnectionListItem(), getCircleCIConnectionListItem(), getAzureEntraIdConnectionListItem(), getVenafiConnectionListItem(), @@ -437,6 +439,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.SMB]: validateSmbConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OpenRouter]: validateOpenRouterConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Anthropic]: validateAnthropicConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Devin]: validateDevinConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.CircleCI]: validateCircleCIConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.AzureEntraId]: validateAzureEntraIdConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Venafi]: validateVenafiConnectionCredentials as TAppConnectionCredentialsValidator, @@ -532,6 +535,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case OctopusDeployConnectionMethod.ApiKey: case OpenRouterConnectionMethod.ApiKey: case AnthropicConnectionMethod.ApiKey: + case DevinConnectionMethod.ApiKey: return "API Key"; case ChefConnectionMethod.UserKey: return "User Key"; @@ -648,6 +652,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.SMB]: platformManagedCredentialsNotSupported, [AppConnection.OpenRouter]: platformManagedCredentialsNotSupported, [AppConnection.Anthropic]: platformManagedCredentialsNotSupported, + [AppConnection.Devin]: platformManagedCredentialsNotSupported, [AppConnection.CircleCI]: platformManagedCredentialsNotSupported, [AppConnection.AzureEntraId]: platformManagedCredentialsNotSupported, [AppConnection.Venafi]: platformManagedCredentialsNotSupported, diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index f564dcffae5..165b89781e6 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -57,7 +57,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.ExternalInfisical]: "Infisical", [AppConnection.Doppler]: "Doppler", [AppConnection.NetScaler]: "NetScaler", - [AppConnection.Anthropic]: "Anthropic" + [AppConnection.Anthropic]: "Anthropic", + [AppConnection.Devin]: "Devin" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -117,5 +118,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -462,6 +469,7 @@ export type TAppConnectionInput = { id: string } & ( | TDopplerConnectionInput | TNetScalerConnectionInput | TAnthropicConnectionInput + | TDevinConnectionInput ); export type TSqlConnectionInput = @@ -558,7 +566,8 @@ export type TAppConnectionConfig = | TExternalInfisicalConnectionConfig | TDopplerConnectionConfig | TNetScalerConnectionConfig - | TAnthropicConnectionConfig; + | TAnthropicConnectionConfig + | TDevinConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -617,7 +626,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateExternalInfisicalConnectionCredentialsSchema | TValidateDopplerConnectionCredentialsSchema | TValidateNetScalerConnectionCredentialsSchema - | TValidateAnthropicConnectionCredentialsSchema; + | TValidateAnthropicConnectionCredentialsSchema + | TValidateDevinConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/devin/devin-connection-enums.ts b/backend/src/services/app-connection/devin/devin-connection-enums.ts new file mode 100644 index 00000000000..6782a7c8eae --- /dev/null +++ b/backend/src/services/app-connection/devin/devin-connection-enums.ts @@ -0,0 +1,3 @@ +export enum DevinConnectionMethod { + ApiKey = "api-key" +} diff --git a/backend/src/services/app-connection/devin/devin-connection-fns.ts b/backend/src/services/app-connection/devin/devin-connection-fns.ts new file mode 100644 index 00000000000..9b78ca687bf --- /dev/null +++ b/backend/src/services/app-connection/devin/devin-connection-fns.ts @@ -0,0 +1,41 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { DevinConnectionMethod } from "./devin-connection-enums"; +import { TDevinConnectionConfig } from "./devin-connection-types"; + +export const getDevinConnectionListItem = () => { + return { + name: "Devin" as const, + app: AppConnection.Devin as const, + methods: Object.values(DevinConnectionMethod) as [DevinConnectionMethod.ApiKey] + }; +}; + +export const validateDevinConnectionCredentials = async (config: TDevinConnectionConfig) => { + const { apiKey } = config.credentials; + + try { + await request.get(`${IntegrationUrls.DEVIN_API_URL}/v3/self`, { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json" + } + }); + } 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/devin/devin-connection-schemas.ts b/backend/src/services/app-connection/devin/devin-connection-schemas.ts new file mode 100644 index 00000000000..84685464e4f --- /dev/null +++ b/backend/src/services/app-connection/devin/devin-connection-schemas.ts @@ -0,0 +1,63 @@ +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 { APP_CONNECTION_NAME_MAP } from "../app-connection-maps"; +import { DevinConnectionMethod } from "./devin-connection-enums"; + +export const DevinConnectionApiKeyCredentialsSchema = z.object({ + apiKey: z + .string() + .trim() + .min(1, "API Key required") + .max(1000) + .startsWith("cog_", "API Key must start with 'cog_'") + .describe(AppConnections.CREDENTIALS.DEVIN.apiKey) +}); + +const BaseDevinConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Devin) }); + +export const DevinConnectionSchema = BaseDevinConnectionSchema.extend({ + method: z.literal(DevinConnectionMethod.ApiKey), + credentials: DevinConnectionApiKeyCredentialsSchema +}); + +export const SanitizedDevinConnectionSchema = z.discriminatedUnion("method", [ + BaseDevinConnectionSchema.extend({ + method: z.literal(DevinConnectionMethod.ApiKey), + credentials: DevinConnectionApiKeyCredentialsSchema.pick({}) + }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Devin]} (API Key)` })) +]); + +export const ValidateDevinConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(DevinConnectionMethod.ApiKey).describe(AppConnections.CREATE(AppConnection.Devin).method), + credentials: DevinConnectionApiKeyCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Devin).credentials) + }) +]); + +export const CreateDevinConnectionSchema = ValidateDevinConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Devin) +); + +export const UpdateDevinConnectionSchema = z + .object({ + credentials: DevinConnectionApiKeyCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Devin).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Devin)); + +export const DevinConnectionListItemSchema = z + .object({ + name: z.literal("Devin"), + app: z.literal(AppConnection.Devin), + methods: z.nativeEnum(DevinConnectionMethod).array() + }) + .describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Devin] })); diff --git a/backend/src/services/app-connection/devin/devin-connection-types.ts b/backend/src/services/app-connection/devin/devin-connection-types.ts new file mode 100644 index 00000000000..d3a255a32c1 --- /dev/null +++ b/backend/src/services/app-connection/devin/devin-connection-types.ts @@ -0,0 +1,22 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateDevinConnectionSchema, + DevinConnectionSchema, + ValidateDevinConnectionCredentialsSchema +} from "./devin-connection-schemas"; + +export type TDevinConnection = z.infer; + +export type TDevinConnectionInput = z.infer & { + app: AppConnection.Devin; +}; + +export type TValidateDevinConnectionCredentialsSchema = typeof ValidateDevinConnectionCredentialsSchema; + +export type TDevinConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/app-connection/devin/index.ts b/backend/src/services/app-connection/devin/index.ts new file mode 100644 index 00000000000..3b34b00ccd3 --- /dev/null +++ b/backend/src/services/app-connection/devin/index.ts @@ -0,0 +1,4 @@ +export * from "./devin-connection-enums"; +export * from "./devin-connection-fns"; +export * from "./devin-connection-schemas"; +export * from "./devin-connection-types"; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index e2581d26c8d..e2f5182b217 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -99,6 +99,7 @@ export enum IntegrationUrls { AZURE_DEVOPS_API_URL = "https://dev.azure.com", HUMANITEC_API_URL = "https://api.humanitec.io", CAMUNDA_API_URL = "https://api.cloud.camunda.io", + DEVIN_API_URL = "https://api.devin.ai", GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, diff --git a/backend/src/services/secret-sync/devin/devin-sync-constants.ts b/backend/src/services/secret-sync/devin/devin-sync-constants.ts new file mode 100644 index 00000000000..55dc44c3119 --- /dev/null +++ b/backend/src/services/secret-sync/devin/devin-sync-constants.ts @@ -0,0 +1,11 @@ +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 DEVIN_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Devin", + destination: SecretSync.Devin, + connection: AppConnection.Devin, + canRemoveSecretsOnDeletion: true, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/devin/devin-sync-fns.ts b/backend/src/services/secret-sync/devin/devin-sync-fns.ts new file mode 100644 index 00000000000..317f8b7bb4d --- /dev/null +++ b/backend/src/services/secret-sync/devin/devin-sync-fns.ts @@ -0,0 +1,183 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { TDevinListSecretsResponse, TDevinSecret, TDevinSyncWithCredentials } from "./devin-sync-types"; + +const DEVIN_LIST_PAGE_SIZE = 200; +const DEVIN_LIST_MAX_PAGES = 50; + +const buildAuthHeaders = (apiKey: string) => ({ + Authorization: `Bearer ${apiKey}`, + Accept: "application/json", + "Content-Type": "application/json" +}); + +const buildSecretsUrl = (orgId: string) => + `${IntegrationUrls.DEVIN_API_URL}/v3/organizations/${encodeURIComponent(orgId)}/secrets`; + +const listAllDevinSecrets = async ({ apiKey, orgId }: { apiKey: string; orgId: string }) => { + const secrets: TDevinSecret[] = []; + let cursor: string | null = null; + + for (let page = 0; page < DEVIN_LIST_MAX_PAGES; page += 1) { + const params: Record = { first: DEVIN_LIST_PAGE_SIZE }; + if (cursor) params.after = cursor; + + const { data } = await request.get(buildSecretsUrl(orgId), { + params, + headers: buildAuthHeaders(apiKey) + }); + + secrets.push(...data.items); + + if (!data.has_next_page) return secrets; + cursor = data.end_cursor; + } + + throw new Error( + `Devin secrets listing exceeded the maximum of ${DEVIN_LIST_MAX_PAGES} pages (${DEVIN_LIST_MAX_PAGES * DEVIN_LIST_PAGE_SIZE} secrets).` + ); +}; + +const deleteDevinSecret = async ({ apiKey, orgId, secretId }: { apiKey: string; orgId: string; secretId: string }) => { + await request.delete(`${buildSecretsUrl(orgId)}/${encodeURIComponent(secretId)}`, { + headers: buildAuthHeaders(apiKey) + }); +}; + +const createDevinSecret = async ({ + apiKey, + orgId, + key, + value, + note +}: { + apiKey: string; + orgId: string; + key: string; + value: string; + note?: string; +}) => { + await request.post( + buildSecretsUrl(orgId), + { + type: "key-value", + key, + value, + is_sensitive: true, + note + }, + { headers: buildAuthHeaders(apiKey) } + ); +}; + +const upsertDevinSecret = async ({ + apiKey, + orgId, + key, + value, + note, + existingSecretsByKey +}: { + apiKey: string; + orgId: string; + key: string; + value: string; + note?: string; + existingSecretsByKey: Map; +}) => { + try { + await createDevinSecret({ apiKey, orgId, key, value, note }); + } catch (error) { + const status = error instanceof AxiosError ? error.response?.status : undefined; + const existing = existingSecretsByKey.get(key); + + if (!existing || status !== 409) { + throw new SecretSyncError({ error, secretKey: key }); + } + + try { + await deleteDevinSecret({ apiKey, orgId, secretId: existing.secret_id }); + await createDevinSecret({ apiKey, orgId, key, value, note }); + } catch (retryError) { + throw new SecretSyncError({ error: retryError, secretKey: key }); + } + } +}; + +const buildSyncNote = (secretSync: TDevinSyncWithCredentials) => { + const envSlug = secretSync.environment?.slug; + return envSlug ? `Synced from Infisical (${envSlug})` : "Synced from Infisical"; +}; + +export const DevinSyncFns = { + syncSecrets: async (secretSync: TDevinSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection: { + credentials: { apiKey } + }, + destinationConfig: { orgId }, + environment, + syncOptions + } = secretSync; + + const note = buildSyncNote(secretSync); + + const existingSecrets = await listAllDevinSecrets({ apiKey, orgId }); + const existingSecretsByKey = new Map(); + for (const secret of existingSecrets) { + if (secret.key) existingSecretsByKey.set(secret.key, secret); + } + + for (const [key, { value }] of Object.entries(secretMap)) { + await upsertDevinSecret({ apiKey, orgId, key, value, note, existingSecretsByKey }); + } + + if (syncOptions.disableSecretDeletion) return; + + for (const secret of existingSecrets) { + const isStaleManagedSecret = + secret.key && + matchesSchema(secret.key, environment?.slug || "", syncOptions.keySchema) && + !(secret.key in secretMap); + + if (isStaleManagedSecret) { + try { + await deleteDevinSecret({ apiKey, orgId, secretId: secret.secret_id }); + } catch (error) { + throw new SecretSyncError({ error, secretKey: secret.key ?? undefined }); + } + } + } + }, + removeSecrets: async (secretSync: TDevinSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection: { + credentials: { apiKey } + }, + destinationConfig: { orgId } + } = secretSync; + + const existingSecrets = await listAllDevinSecrets({ apiKey, orgId }); + + for (const secret of existingSecrets) { + if (secret.key && secret.key in secretMap) { + try { + await deleteDevinSecret({ apiKey, orgId, secretId: secret.secret_id }); + } catch (error) { + throw new SecretSyncError({ error, secretKey: secret.key }); + } + } + } + }, + getSecrets: async (secretSync: TDevinSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + } +}; diff --git a/backend/src/services/secret-sync/devin/devin-sync-schemas.ts b/backend/src/services/secret-sync/devin/devin-sync-schemas.ts new file mode 100644 index 00000000000..22d8d6d91f6 --- /dev/null +++ b/backend/src/services/secret-sync/devin/devin-sync-schemas.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; + +const DevinSyncDestinationConfigSchema = z.object({ + orgId: z + .string() + .trim() + .min(1, "Organization ID is required") + .startsWith("org-", "Organization ID must start with 'org-'") + .describe(SecretSyncs.DESTINATION_CONFIG.DEVIN.orgId) +}); + +const DevinSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const DevinSyncSchema = BaseSecretSyncSchema(SecretSync.Devin, DevinSyncOptionsConfig) + .extend({ + destination: z.literal(SecretSync.Devin), + destinationConfig: DevinSyncDestinationConfigSchema + }) + .describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.Devin] })); + +export const CreateDevinSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Devin, + DevinSyncOptionsConfig +).extend({ + destinationConfig: DevinSyncDestinationConfigSchema +}); + +export const UpdateDevinSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Devin, + DevinSyncOptionsConfig +).extend({ + destinationConfig: DevinSyncDestinationConfigSchema.optional() +}); + +export const DevinSyncListItemSchema = z + .object({ + name: z.literal("Devin"), + connection: z.literal(AppConnection.Devin), + destination: z.literal(SecretSync.Devin), + canImportSecrets: z.literal(false), + canRemoveSecretsOnDeletion: z.literal(true) + }) + .describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.Devin] })); diff --git a/backend/src/services/secret-sync/devin/devin-sync-types.ts b/backend/src/services/secret-sync/devin/devin-sync-types.ts new file mode 100644 index 00000000000..e3bc9a519ce --- /dev/null +++ b/backend/src/services/secret-sync/devin/devin-sync-types.ts @@ -0,0 +1,37 @@ +import z from "zod"; + +import { TDevinConnection } from "@app/services/app-connection/devin"; + +import { CreateDevinSyncSchema, DevinSyncListItemSchema, DevinSyncSchema } from "./devin-sync-schemas"; + +export type TDevinSync = z.infer; + +export type TDevinSyncInput = z.infer; + +export type TDevinSyncListItem = z.infer; + +export type TDevinSyncWithCredentials = TDevinSync & { + connection: TDevinConnection; +}; + +export type TDevinSecretType = "cookie" | "key-value" | "totp"; + +export type TDevinSecret = { + secret_id: string; + key: string | null; + note: string | null; + is_sensitive: boolean; + secret_type: TDevinSecretType; + access_type: "org" | "personal"; + created_by: string; + created_at: number; + updated_by: string | null; + updated_at: number | null; +}; + +export type TDevinListSecretsResponse = { + items: TDevinSecret[]; + end_cursor: string | null; + has_next_page: boolean; + total: number | null; +}; diff --git a/backend/src/services/secret-sync/devin/index.ts b/backend/src/services/secret-sync/devin/index.ts new file mode 100644 index 00000000000..ac4e5ec4f3d --- /dev/null +++ b/backend/src/services/secret-sync/devin/index.ts @@ -0,0 +1,4 @@ +export * from "./devin-sync-constants"; +export * from "./devin-sync-fns"; +export * from "./devin-sync-schemas"; +export * from "./devin-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 92dbd907fa9..4f847ed0273 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -35,7 +35,8 @@ export enum SecretSync { OctopusDeploy = "octopus-deploy", CircleCI = "circleci", AzureEntraIdScim = "azure-entra-id-scim", - ExternalInfisical = "external-infisical" + ExternalInfisical = "external-infisical", + Devin = "devin" } 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 cfd34bf4cad..df717f57b9f 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -52,6 +52,7 @@ import { CIRCLECI_SYNC_LIST_OPTION, CircleCISyncFns } from "./circleci"; 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 { DEVIN_SYNC_LIST_OPTION, DevinSyncFns } from "./devin"; import { DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION, DigitalOceanAppPlatformSyncFns @@ -117,7 +118,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.OctopusDeploy]: OCTOPUS_DEPLOY_SYNC_LIST_OPTION, [SecretSync.CircleCI]: CIRCLECI_SYNC_LIST_OPTION, [SecretSync.AzureEntraIdScim]: AZURE_ENTRA_ID_SCIM_SYNC_LIST_OPTION, - [SecretSync.ExternalInfisical]: EXTERNAL_INFISICAL_SYNC_LIST_OPTION + [SecretSync.ExternalInfisical]: EXTERNAL_INFISICAL_SYNC_LIST_OPTION, + [SecretSync.Devin]: DEVIN_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -378,6 +380,8 @@ export const SecretSyncFns = { // Key schema is intentionally not applied for Infisical-to-Infisical syncs to prevent // infinite sync loops where the prefixed key triggers another sync cycle. return ExternalInfisicalSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.Devin: + return DevinSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -518,6 +522,9 @@ export const SecretSyncFns = { case SecretSync.ExternalInfisical: secretMap = await ExternalInfisicalSyncFns.getSecrets(secretSync); break; + case SecretSync.Devin: + secretMap = await DevinSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -627,6 +634,8 @@ export const SecretSyncFns = { // Key schema is intentionally not applied for Infisical-to-Infisical syncs to prevent // infinite sync loops where the prefixed key triggers another sync cycle. return ExternalInfisicalSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.Devin: + return DevinSyncFns.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 9fe6f036422..c570be2a952 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -39,7 +39,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.OctopusDeploy]: "Octopus Deploy", [SecretSync.CircleCI]: "CircleCI", [SecretSync.AzureEntraIdScim]: "Azure Entra ID SCIM", - [SecretSync.ExternalInfisical]: "Infisical" + [SecretSync.ExternalInfisical]: "Infisical", + [SecretSync.Devin]: "Devin" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -79,7 +80,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.OctopusDeploy]: AppConnection.OctopusDeploy, [SecretSync.CircleCI]: AppConnection.CircleCI, [SecretSync.AzureEntraIdScim]: AppConnection.AzureEntraId, - [SecretSync.ExternalInfisical]: AppConnection.ExternalInfisical + [SecretSync.ExternalInfisical]: AppConnection.ExternalInfisical, + [SecretSync.Devin]: AppConnection.Devin }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -119,7 +121,8 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.OctopusDeploy]: SecretSyncPlanType.Regular, [SecretSync.CircleCI]: SecretSyncPlanType.Regular, [SecretSync.AzureEntraIdScim]: SecretSyncPlanType.Regular, - [SecretSync.ExternalInfisical]: SecretSyncPlanType.Regular + [SecretSync.ExternalInfisical]: SecretSyncPlanType.Regular, + [SecretSync.Devin]: SecretSyncPlanType.Regular }; export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { @@ -168,7 +171,8 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { [SecretSync.OctopusDeploy]: [], [SecretSync.CircleCI]: [], [SecretSync.AzureEntraIdScim]: [], - [SecretSync.ExternalInfisical]: [] + [SecretSync.ExternalInfisical]: [], + [SecretSync.Devin]: [] }; const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true; @@ -234,7 +238,8 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record + Check out the configuration docs for [Devin Connections](/integrations/app-connections/devin) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/devin/delete.mdx b/docs/api-reference/endpoints/app-connections/devin/delete.mdx new file mode 100644 index 00000000000..86b51898536 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/devin/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/devin/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/devin/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/devin/get-by-id.mdx new file mode 100644 index 00000000000..b74d5dd3eaf --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/devin/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/devin/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/devin/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/devin/get-by-name.mdx new file mode 100644 index 00000000000..4a39145c4ba --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/devin/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/devin/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/devin/list.mdx b/docs/api-reference/endpoints/app-connections/devin/list.mdx new file mode 100644 index 00000000000..480136520f1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/devin/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/devin" +--- diff --git a/docs/api-reference/endpoints/app-connections/devin/update.mdx b/docs/api-reference/endpoints/app-connections/devin/update.mdx new file mode 100644 index 00000000000..10df3ad2966 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/devin/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/devin/{connectionId}" +--- + + + Check out the configuration docs for [Devin Connections](/integrations/app-connections/devin) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/devin/create.mdx b/docs/api-reference/endpoints/secret-syncs/devin/create.mdx new file mode 100644 index 00000000000..32085394553 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/devin" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/devin/delete.mdx b/docs/api-reference/endpoints/secret-syncs/devin/delete.mdx new file mode 100644 index 00000000000..2d24dd80975 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/devin/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/devin/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/devin/get-by-id.mdx new file mode 100644 index 00000000000..32dc04d1886 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/devin/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/devin/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/devin/get-by-name.mdx new file mode 100644 index 00000000000..d0c15001a39 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/devin/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/devin/list.mdx b/docs/api-reference/endpoints/secret-syncs/devin/list.mdx new file mode 100644 index 00000000000..b58ff3377b4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/devin" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/devin/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/devin/remove-secrets.mdx new file mode 100644 index 00000000000..81ee30359e3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/devin/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/devin/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/devin/sync-secrets.mdx new file mode 100644 index 00000000000..ccf99332b15 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/devin/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/devin/update.mdx b/docs/api-reference/endpoints/secret-syncs/devin/update.mdx new file mode 100644 index 00000000000..c916f595e66 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/devin/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/devin/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 11d4b62261d..37e6a8b8ffb 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -124,6 +124,7 @@ "integrations/app-connections/cloudflare", "integrations/app-connections/databricks", "integrations/app-connections/dbt", + "integrations/app-connections/devin", "integrations/app-connections/digital-ocean", "integrations/app-connections/doppler", "integrations/app-connections/dns-made-easy", @@ -586,6 +587,7 @@ "integrations/secret-syncs/cloudflare-pages", "integrations/secret-syncs/cloudflare-workers", "integrations/secret-syncs/databricks", + "integrations/secret-syncs/devin", "integrations/secret-syncs/digital-ocean-app-platform", "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", @@ -1472,6 +1474,18 @@ "api-reference/endpoints/app-connections/dbt/delete" ] }, + { + "group": "Devin", + "pages": [ + "api-reference/endpoints/app-connections/devin/list", + "api-reference/endpoints/app-connections/devin/available", + "api-reference/endpoints/app-connections/devin/get-by-id", + "api-reference/endpoints/app-connections/devin/get-by-name", + "api-reference/endpoints/app-connections/devin/create", + "api-reference/endpoints/app-connections/devin/update", + "api-reference/endpoints/app-connections/devin/delete" + ] + }, { "group": "Digital Ocean", "pages": [ @@ -2617,6 +2631,19 @@ "api-reference/endpoints/secret-syncs/databricks/remove-secrets" ] }, + { + "group": "Devin", + "pages": [ + "api-reference/endpoints/secret-syncs/devin/list", + "api-reference/endpoints/secret-syncs/devin/get-by-id", + "api-reference/endpoints/secret-syncs/devin/get-by-name", + "api-reference/endpoints/secret-syncs/devin/create", + "api-reference/endpoints/secret-syncs/devin/update", + "api-reference/endpoints/secret-syncs/devin/delete", + "api-reference/endpoints/secret-syncs/devin/sync-secrets", + "api-reference/endpoints/secret-syncs/devin/remove-secrets" + ] + }, { "group": "Digital Ocean", "pages": [ diff --git a/docs/images/app-connections/devin/devin-app-connection-form.png b/docs/images/app-connections/devin/devin-app-connection-form.png new file mode 100644 index 00000000000..d948b4806ec Binary files /dev/null and b/docs/images/app-connections/devin/devin-app-connection-form.png differ diff --git a/docs/images/app-connections/devin/devin-app-connection-option.png b/docs/images/app-connections/devin/devin-app-connection-option.png new file mode 100644 index 00000000000..212380b4fca Binary files /dev/null and b/docs/images/app-connections/devin/devin-app-connection-option.png differ diff --git a/docs/images/app-connections/devin/step-1.png b/docs/images/app-connections/devin/step-1.png new file mode 100644 index 00000000000..6d43c12cdb3 Binary files /dev/null and b/docs/images/app-connections/devin/step-1.png differ diff --git a/docs/images/app-connections/devin/step-2.png b/docs/images/app-connections/devin/step-2.png new file mode 100644 index 00000000000..0acc07a082a Binary files /dev/null and b/docs/images/app-connections/devin/step-2.png differ diff --git a/docs/images/secret-syncs/devin/configure-destination.png b/docs/images/secret-syncs/devin/configure-destination.png new file mode 100644 index 00000000000..129d6f14af8 Binary files /dev/null and b/docs/images/secret-syncs/devin/configure-destination.png differ diff --git a/docs/images/secret-syncs/devin/configure-details.png b/docs/images/secret-syncs/devin/configure-details.png new file mode 100644 index 00000000000..9659a1addd3 Binary files /dev/null and b/docs/images/secret-syncs/devin/configure-details.png differ diff --git a/docs/images/secret-syncs/devin/configure-source.png b/docs/images/secret-syncs/devin/configure-source.png new file mode 100644 index 00000000000..fc8b5d0e6db Binary files /dev/null and b/docs/images/secret-syncs/devin/configure-source.png differ diff --git a/docs/images/secret-syncs/devin/configure-sync-options.png b/docs/images/secret-syncs/devin/configure-sync-options.png new file mode 100644 index 00000000000..7cba692527c Binary files /dev/null and b/docs/images/secret-syncs/devin/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/devin/org-id.png b/docs/images/secret-syncs/devin/org-id.png new file mode 100644 index 00000000000..740d2d38f71 Binary files /dev/null and b/docs/images/secret-syncs/devin/org-id.png differ diff --git a/docs/images/secret-syncs/devin/review-configuration.png b/docs/images/secret-syncs/devin/review-configuration.png new file mode 100644 index 00000000000..449aba64c32 Binary files /dev/null and b/docs/images/secret-syncs/devin/review-configuration.png differ diff --git a/docs/images/secret-syncs/devin/select-option.png b/docs/images/secret-syncs/devin/select-option.png new file mode 100644 index 00000000000..c71a8b732a9 Binary files /dev/null and b/docs/images/secret-syncs/devin/select-option.png differ diff --git a/docs/images/secret-syncs/devin/sync-created.png b/docs/images/secret-syncs/devin/sync-created.png new file mode 100644 index 00000000000..ecc096cd09a Binary files /dev/null and b/docs/images/secret-syncs/devin/sync-created.png differ diff --git a/docs/integrations/app-connections/devin.mdx b/docs/integrations/app-connections/devin.mdx new file mode 100644 index 00000000000..7392259e74f --- /dev/null +++ b/docs/integrations/app-connections/devin.mdx @@ -0,0 +1,93 @@ +--- +title: "Devin Connection" +description: "Learn how to configure a Devin connection for Infisical." +--- + +[Devin](https://devin.ai/) is an AI software engineer from Cognition. Infisical supports connecting to Devin using a service-user **API Key**, which is used to push secrets into a Devin organization via [Devin Secret Sync](/integrations/secret-syncs/devin). + +## Prerequisites + +- A [Devin account](https://app.devin.ai/) with an organization you can manage + +## Create a Devin API Key + + + + In your Devin organization settings, create a service user that Infisical will act as. Fill in the service user details and submit the form. + + ![Devin Service User](/images/app-connections/devin/step-1.png) + + + Devin displays the new service user's API key immediately after creation. Copy the key, it begins with the `cog_` prefix and will only be shown once. Store it securely; you will use it when creating the Infisical connection. + + ![Devin API Key](/images/app-connections/devin/step-2.png) + + + +## Create Devin Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** → **App Connections** (or the **Integrations** → **App Connections** tab in your project). + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click **Add Connection** and choose **Devin** from the list of available connections. + + ![Select Devin Connection](/images/app-connections/devin/devin-app-connection-option.png) + + + Complete the form with: + - A **name** for the connection (e.g. `devin-prod`) + - An optional **description** + - Your **Devin API Key** (the `cog_…` value from the steps above) + + ![Devin Connection Form](/images/app-connections/devin/devin-app-connection-form.png) + + + After clicking Create, your **Devin Connection** is established and ready to use with your Infisical project. + + + + + Create a Devin connection via the API. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/devin \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-devin-connection", + "method": "api-key", + "credentials": { + "apiKey": "cog_" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-devin-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2026-04-20T19:46:34.831Z", + "updatedAt": "2026-04-20T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "app": "devin", + "method": "api-key", + "credentials": {} + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/devin.mdx b/docs/integrations/secret-syncs/devin.mdx new file mode 100644 index 00000000000..1b799387530 --- /dev/null +++ b/docs/integrations/secret-syncs/devin.mdx @@ -0,0 +1,167 @@ +--- +title: "Devin Sync" +description: "Learn how to configure a Devin Sync for Infisical." +--- + +**Prerequisites:** +- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) +- Create a [Devin Connection](/integrations/app-connections/devin) + + + + + + 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 the **Devin** option from the list of available secret syncs. + + ![Select Devin](/images/secret-syncs/devin/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/devin/configure-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/devin/configure-destination.png) + + - **Devin Connection**: The Devin Connection to authenticate with. + - **Organization ID**: The Devin organization ID that secrets should be synced to. You can find this in the **Service users** section of your Devin organization settings. + ![Configure Destination](/images/secret-syncs/devin/org-id.png) + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/devin/configure-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets in the Devin organization not present in Infisical. + + + Devin Sync does not support importing existing secrets from Devin into Infisical. Only **Overwrite Destination Secrets** is available as an initial sync behavior. + + + Infisical syncs all secrets to Devin as **key-value** secrets. If a TOTP or Cookie secret in your Devin organization shares a key with a secret being synced, it will be replaced with a key-value secret. Likewise, TOTP or Cookie secrets matching your **Key Schema** will be removed during sync unless **Disable Secret Deletion** is enabled. + + - **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 Devin organization. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your Devin Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/devin/configure-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Devin Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/devin/review-configuration.png) + + + If enabled, your Devin Sync will begin syncing your secrets to the destination organization. + + ![Sync Created](/images/secret-syncs/devin/sync-created.png) + + + + + To create a **Devin Sync**, make an API request to the [Create Devin Sync](/api-reference/endpoints/secret-syncs/devin/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/devin \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-devin-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": { + "orgId": "org-..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-devin-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2026-04-20T05:31:56Z", + "updatedAt": "2026-04-20T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2026-04-20T05: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": "devin", + "name": "my-devin-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": "devin", + "destinationConfig": { + "orgId": "org-..." + } + } + } + ``` + + diff --git a/frontend/public/images/integrations/Devin.png b/frontend/public/images/integrations/Devin.png new file mode 100644 index 00000000000..74cc04a32f8 Binary files /dev/null and b/frontend/public/images/integrations/Devin.png differ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DevinSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DevinSyncFields.tsx new file mode 100644 index 00000000000..7c79aa0bac2 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DevinSyncFields.tsx @@ -0,0 +1,31 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const DevinSyncFields = () => { + const { control } = useFormContext(); + + return ( + <> + + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 00174af087f..4a673b7349a 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -18,6 +18,7 @@ import { CircleCISyncFields } from "./CircleCISyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; +import { DevinSyncFields } from "./DevinSyncFields"; import { DigitalOceanAppPlatformSyncFields } from "./DigitalOceanAppPlatformSyncFields"; import { ExternalInfisicalSyncFields } from "./ExternalInfisicalSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields"; @@ -121,6 +122,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.ExternalInfisical: return ; + case SecretSync.Devin: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 827aebbb5d9..f3b33c70ee2 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -108,6 +108,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.CircleCI: case SecretSync.AzureEntraIdScim: case SecretSync.ExternalInfisical: + case SecretSync.Devin: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DevinSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DevinSyncReviewFields.tsx new file mode 100644 index 00000000000..25652e306ec --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/DevinSyncReviewFields.tsx @@ -0,0 +1,12 @@ +import { useFormContext } from "react-hook-form"; + +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const DevinSyncReviewFields = () => { + const { watch } = useFormContext(); + const orgId = watch("destinationConfig.orgId"); + + return {orgId}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index ea2d1b50739..03471a186d4 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -30,6 +30,7 @@ import { CircleCISyncReviewFields } from "./CircleCISyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; +import { DevinSyncReviewFields } from "./DevinSyncReviewFields"; import { DigitalOceanAppPlatformSyncReviewFields } from "./DigitalOceanAppPlatformSyncReviewFields"; import { ExternalInfisicalSyncReviewFields } from "./ExternalInfisicalSyncReviewFields"; import { FlyioSyncOptionsReviewFields, FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; @@ -198,6 +199,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.ExternalInfisical: DestinationFieldsComponent = ; break; + case SecretSync.Devin: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/devin-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/devin-sync-destination-schema.ts new file mode 100644 index 00000000000..9adc328af30 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/devin-sync-destination-schema.ts @@ -0,0 +1,17 @@ +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 DevinSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Devin), + destinationConfig: z.object({ + orgId: z + .string() + .trim() + .min(1, "Organization ID required") + .startsWith("org-", "Organization ID must start with 'org-'") + }) + }) +); 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 f9bc319b74c..c7d7b90cb7c 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 @@ -15,6 +15,7 @@ import { CircleCISyncDestinationSchema } from "./circleci-sync-destination-schem import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema"; import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; +import { DevinSyncDestinationSchema } from "./devin-sync-destination-schema"; import { DigitalOceanAppPlatformSyncDestinationSchema } from "./digital-ocean-app-platform-sync-destination-schema"; import { ExternalInfisicalSyncDestinationSchema } from "./external-infisical-sync-destination-schema"; import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; @@ -75,7 +76,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ ChefSyncDestinationSchema, CircleCISyncDestinationSchema, AzureEntraIdScimSyncDestinationSchema, - ExternalInfisicalSyncDestinationSchema + ExternalInfisicalSyncDestinationSchema, + DevinSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 3ddd68127a8..716cb2beeaf 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -24,6 +24,7 @@ import { CloudflareConnectionMethod, DatabricksConnectionMethod, DbtConnectionMethod, + DevinConnectionMethod, FlyioConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, @@ -162,7 +163,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.ExternalInfisical]: { name: "Infisical", image: "Infisical.png" }, [AppConnection.Doppler]: { name: "Doppler", image: "Doppler.png" }, [AppConnection.NetScaler]: { name: "NetScaler", image: "NetScaler.png" }, - [AppConnection.Anthropic]: { name: "Anthropic", image: "Anthropic.png" } + [AppConnection.Anthropic]: { name: "Anthropic", image: "Anthropic.png" }, + [AppConnection.Devin]: { name: "Devin", image: "Devin.png", size: 55 } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -242,6 +244,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case ChecklyConnectionMethod.ApiKey: case OpenRouterConnectionMethod.ApiKey: case AnthropicConnectionMethod.ApiKey: + case DevinConnectionMethod.ApiKey: return { name: "API Key", icon: faKey }; case ChefConnectionMethod.UserKey: return { name: "User Key", icon: faKey }; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 4357a8a3ecd..e02d10f5fb5 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -141,6 +141,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.OctopusDeploy]: AppConnection.OctopusDeploy, [SecretSync.CircleCI]: AppConnection.CircleCI, [SecretSync.AzureEntraIdScim]: AppConnection.AzureEntraId, - [SecretSync.ExternalInfisical]: AppConnection.ExternalInfisical + [SecretSync.ExternalInfisical]: AppConnection.ExternalInfisical, + [SecretSync.Devin]: AppConnection.Devin }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 77d7a365acc..3348029ecfd 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -55,5 +55,6 @@ export enum AppConnection { ExternalInfisical = "external-infisical", Doppler = "doppler", NetScaler = "netscaler", - Anthropic = "anthropic" + Anthropic = "anthropic", + Devin = "devin" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 2a0daae1126..ff1fdeaa70b 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -243,6 +243,10 @@ export type TAnthropicConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Anthropic; }; +export type TDevinConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Devin; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -300,7 +304,8 @@ export type TAppConnectionOption = | TExternalInfisicalConnectionOption | TDopplerConnectionOption | TNetScalerConnectionOption - | TAnthropicConnectionOption; + | TAnthropicConnectionOption + | TDevinConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -360,4 +365,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Doppler]: TDopplerConnectionOption; [AppConnection.NetScaler]: TNetScalerConnectionOption; [AppConnection.Anthropic]: TAnthropicConnectionOption; + [AppConnection.Devin]: TDevinConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/devin-connection.ts b/frontend/src/hooks/api/appConnections/types/devin-connection.ts new file mode 100644 index 00000000000..749a53c08d0 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/devin-connection.ts @@ -0,0 +1,13 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum DevinConnectionMethod { + ApiKey = "api-key" +} + +export type TDevinConnection = TRootAppConnection & { app: AppConnection.Devin } & { + method: DevinConnectionMethod.ApiKey; + credentials: { + apiKey: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 2594dec8986..26af250d9ef 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -19,6 +19,7 @@ import { TCircleCIConnection } from "./circleci-connection"; import { TCloudflareConnection } from "./cloudflare-connection"; import { TDatabricksConnection } from "./databricks-connection"; import { TDbtConnection } from "./dbt-connection"; +import { TDevinConnection } from "./devin-connection"; import { TDigitalOceanConnection } from "./digital-ocean"; import { TDNSMadeEasyConnection } from "./dns-made-easy-connection"; import { TDopplerConnection } from "./doppler-connection"; @@ -76,6 +77,7 @@ export * from "./circleci-connection"; export * from "./cloudflare-connection"; export * from "./databricks-connection"; export * from "./dbt-connection"; +export * from "./devin-connection"; export * from "./dns-made-easy-connection"; export * from "./doppler-connection"; export * from "./external-infisical-connection"; @@ -171,7 +173,8 @@ export type TAppConnection = | TVenafiConnection | TExternalInfisicalConnection | TDopplerConnection - | TNetScalerConnection; + | TNetScalerConnection + | TDevinConnection; export type TAvailableAppConnection = Pick; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 2200b02e26a..24c213db6ba 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -35,7 +35,8 @@ export enum SecretSync { OctopusDeploy = "octopus-deploy", CircleCI = "circleci", AzureEntraIdScim = "azure-entra-id-scim", - ExternalInfisical = "external-infisical" + ExternalInfisical = "external-infisical", + Devin = "devin" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/devin-sync.ts b/frontend/src/hooks/api/secretSyncs/types/devin-sync.ts new file mode 100644 index 00000000000..cd91d6aa575 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/devin-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 TDevinSync = TRootSecretSync & { + destination: SecretSync.Devin; + destinationConfig: { + orgId: string; + }; + connection: { + app: AppConnection.Devin; + 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 21dc0d1c8c9..39511038a92 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -16,6 +16,7 @@ import { TCircleCISync } from "./circleci-sync"; import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; import { TCloudflareWorkersSync } from "./cloudflare-workers-sync"; import { TDatabricksSync } from "./databricks-sync"; +import { TDevinSync } from "./devin-sync"; import { TDigitalOceanAppPlatformSync } from "./digital-ocean-app-platform-sync"; import { TExternalInfisicalSync } from "./external-infisical-sync"; import { TFlyioSync } from "./flyio-sync"; @@ -86,7 +87,8 @@ export type TSecretSync = | TOctopusDeploySync | TCircleCISync | TAzureEntraIdScimSync - | TExternalInfisicalSync; + | TExternalInfisicalSync + | TDevinSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 408a252ebf1..d68868edbb4 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -33,6 +33,7 @@ import { CircleCIConnectionForm } from "./CircleCIConnectionForm"; import { CloudflareConnectionForm } from "./CloudflareConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { DbtConnectionForm } from "./DbtConnectionForm"; +import { DevinConnectionForm } from "./DevinConnectionForm"; import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm"; import { DNSMadeEasyConnectionForm } from "./DNSMadeEasyConnectionForm"; import { DopplerConnectionForm } from "./DopplerConnectionForm"; @@ -267,6 +268,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Anthropic: return ; + case AppConnection.Devin: + return ; case AppConnection.CircleCI: return ; case AppConnection.Venafi: @@ -482,6 +485,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Anthropic: return ; + case AppConnection.Devin: + return ; case AppConnection.CircleCI: return ; case AppConnection.ExternalInfisical: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DevinConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DevinConnectionForm.tsx new file mode 100644 index 00000000000..e37989cce56 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DevinConnectionForm.tsx @@ -0,0 +1,135 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { DevinConnectionMethod, TDevinConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TDevinConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Devin) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(DevinConnectionMethod.ApiKey), + credentials: z.object({ + apiKey: z + .string() + .trim() + .min(1, "API Key required") + .startsWith("cog_", "API Key must start with 'cog_'") + }) + }) +]); + +type FormData = z.infer; + +export const DevinConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Devin, + method: DevinConnectionMethod.ApiKey + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DevinSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DevinSyncDestinationCol.tsx new file mode 100644 index 00000000000..61eb20be4ce --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/DevinSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TDevinSync } from "@app/hooks/api/secretSyncs/types/devin-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TDevinSync; +}; + +export const DevinSyncDestinationCol = ({ 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 571353c10f2..7d2a74818b4 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 @@ -15,6 +15,7 @@ import { CircleCISyncDestinationCol } from "./CircleCISyncDestinationCol"; import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol"; import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; +import { DevinSyncDestinationCol } from "./DevinSyncDestinationCol"; import { DigitalOceanAppPlatformSyncDestinationCol } from "./DigitalOceanAppPlatformSyncDestinationCol"; import { ExternalInfisicalSyncDestinationCol } from "./ExternalInfisicalSyncDestinationCol"; import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol"; @@ -118,6 +119,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.ExternalInfisical: return ; + case SecretSync.Devin: + 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 974fcc1051d..dccaccd51d1 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 @@ -233,6 +233,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectId; secondaryText = `${destinationConfig.environment} - ${destinationConfig.secretPath}`; break; + case SecretSync.Devin: + primaryText = destinationConfig.orgId; + secondaryText = "Organization"; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DevinSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DevinSyncDestinationSection.tsx new file mode 100644 index 00000000000..f62c246c0f0 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DevinSyncDestinationSection.tsx @@ -0,0 +1,12 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TDevinSync } from "@app/hooks/api/secretSyncs/types/devin-sync"; + +type Props = { + secretSync: TDevinSync; +}; + +export const DevinSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + return {destinationConfig.orgId}; +}; 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 fa4a2a729be..8492459433d 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -25,6 +25,7 @@ import { CircleCISyncDestinationSection } from "./CircleCISyncDestinationSection import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection"; import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; +import { DevinSyncDestinationSection } from "./DevinSyncDestinationSection"; import { DigitalOceanAppPlatformSyncDestinationSection } from "./DigitalOceanAppPlatformSyncDestinationSection"; import { ExternalInfisicalSyncDestinationSection } from "./ExternalInfisicalSyncDestinationSection"; import { FlyioSyncDestinationSection } from "./FlyioSyncDestinationSection"; @@ -175,6 +176,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.ExternalInfisical: DestinationComponents = ; break; + case SecretSync.Devin: + 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 77e06a0be81..2b05951e5e8 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -80,6 +80,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.CircleCI: case SecretSync.AzureEntraIdScim: case SecretSync.ExternalInfisical: + case SecretSync.Devin: AdditionalSyncOptionsComponent = null; break; default: