feat: Digital Ocean App connection and App Platform secret sync (#4203)

* fix: save wip

* feat: final impl

* feat: docs

* Update backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* chore: remove empty conflict files

* Update backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: lint

* fix: api client

* fix: lint and types

* fix: typecheck lint

* fix: docs

* fix: docs

* fix: linting

---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Sid
2025-07-23 19:59:29 +05:30
committed by GitHub
co-authored by greptile-apps[bot]
parent 95b997c100
commit ab56a69d59
90 changed files with 1363 additions and 8 deletions
@@ -430,4 +430,3 @@ export async function down(knex: Knex): Promise<void> {
});
}
}
+3
View File
@@ -2290,6 +2290,9 @@ export const AppConnections = {
accessKey: "The Key used to access Supabase.",
instanceUrl: "The URL used to access Supabase."
},
DIGITAL_OCEAN_APP_PLATFORM: {
apiToken: "The API token used to authenticate with Digital Ocean App Platform."
},
OKTA: {
instanceUrl: "The URL used to access your Okta organization.",
apiToken: "The API token used to authenticate with Okta."
@@ -51,6 +51,10 @@ import {
DatabricksConnectionListItemSchema,
SanitizedDatabricksConnectionSchema
} from "@app/services/app-connection/databricks";
import {
DigitalOceanConnectionListItemSchema,
SanitizedDigitalOceanConnectionSchema
} from "@app/services/app-connection/digital-ocean";
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
@@ -140,6 +144,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options,
...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedOktaConnectionSchema.options
]);
@@ -178,6 +183,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema,
DigitalOceanConnectionListItemSchema,
OktaConnectionListItemSchema
]);
@@ -0,0 +1,57 @@
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 {
CreateDigitalOceanConnectionSchema,
SanitizedDigitalOceanConnectionSchema,
UpdateDigitalOceanConnectionSchema
} from "@app/services/app-connection/digital-ocean";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerDigitalOceanConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.DigitalOcean,
server,
createSchema: CreateDigitalOceanConnectionSchema,
updateSchema: UpdateDigitalOceanConnectionSchema,
sanitizedResponseSchema: SanitizedDigitalOceanConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/apps`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
apps: z
.object({
id: z.string(),
spec: z.object({
name: z.string()
})
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const apps = await server.services.appConnection.digitalOcean.listApps(connectionId, req.permission);
return { apps };
}
});
};
@@ -14,6 +14,7 @@ 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 { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
@@ -74,5 +75,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Railway]: registerRailwayConnectionRouter,
[AppConnection.Checkly]: registerChecklyConnectionRouter,
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter
};
@@ -0,0 +1,17 @@
import {
CreateDigitalOceanAppPlatformSyncSchema,
DigitalOceanAppPlatformSyncSchema,
UpdateDigitalOceanAppPlatformSyncSchema
} from "@app/services/secret-sync/digital-ocean-app-platform";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerDigitalOceanAppPlatformSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.DigitalOceanAppPlatform,
server,
responseSchema: DigitalOceanAppPlatformSyncSchema,
createSchema: CreateDigitalOceanAppPlatformSyncSchema,
updateSchema: UpdateDigitalOceanAppPlatformSyncSchema
});
@@ -13,6 +13,7 @@ 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";
import { registerDigitalOceanAppPlatformSyncRouter } from "./digital-ocean-app-platform-sync-router";
import { registerFlyioSyncRouter } from "./flyio-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
@@ -59,5 +60,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Zabbix]: registerZabbixSyncRouter,
[SecretSync.Railway]: registerRailwaySyncRouter,
[SecretSync.Checkly]: registerChecklySyncRouter,
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
[SecretSync.Bitbucket]: registerBitbucketSyncRouter
};
@@ -33,6 +33,10 @@ import {
CloudflareWorkersSyncSchema
} from "@app/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
import {
DigitalOceanAppPlatformSyncListItemSchema,
DigitalOceanAppPlatformSyncSchema
} from "@app/services/secret-sync/digital-ocean-app-platform";
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
@@ -77,6 +81,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
ZabbixSyncSchema,
RailwaySyncSchema,
ChecklySyncSchema,
DigitalOceanAppPlatformSyncSchema,
BitbucketSyncSchema
]);
@@ -104,6 +109,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
GitLabSyncListItemSchema,
CloudflarePagesSyncListItemSchema,
CloudflareWorkersSyncListItemSchema,
DigitalOceanAppPlatformSyncListItemSchema,
ZabbixSyncListItemSchema,
RailwaySyncListItemSchema,
ChecklySyncListItemSchema,
@@ -33,6 +33,7 @@ export enum AppConnection {
Bitbucket = "bitbucket",
Checkly = "checkly",
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Okta = "okta"
}
@@ -68,6 +68,11 @@ import {
getDatabricksConnectionListItem,
validateDatabricksConnectionCredentials
} from "./databricks";
import {
DigitalOceanConnectionMethod,
getDigitalOceanConnectionListItem,
validateDigitalOceanConnectionCredentials
} from "./digital-ocean";
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
@@ -157,6 +162,7 @@ export const listAppConnectionOptions = () => {
getBitbucketConnectionListItem(),
getChecklyConnectionListItem(),
getSupabaseConnectionListItem(),
getDigitalOceanConnectionListItem(),
getOktaConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -244,6 +250,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
};
@@ -283,6 +290,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case CloudflareConnectionMethod.APIToken:
case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
case DigitalOceanConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
@@ -372,6 +380,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported
};
@@ -35,6 +35,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Checkly]: "Checkly",
[AppConnection.Supabase]: "Supabase",
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
[AppConnection.Okta]: "Okta"
};
@@ -73,5 +74,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular
};
@@ -61,6 +61,8 @@ import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/clou
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
import { flyioConnectionService } from "./flyio/flyio-connection-service";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
@@ -145,6 +147,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
};
@@ -607,6 +610,7 @@ export const appConnectionServiceFactory = ({
bitbucket: bitbucketConnectionService(connectAppConnectionById),
checkly: checklyConnectionService(connectAppConnectionById),
supabase: supabaseConnectionService(connectAppConnectionById),
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById)
};
};
@@ -87,6 +87,12 @@ import {
TDatabricksConnectionInput,
TValidateDatabricksConnectionCredentialsSchema
} from "./databricks";
import {
TDigitalOceanConnection,
TDigitalOceanConnectionConfig,
TDigitalOceanConnectionInput,
TValidateDigitalOceanCredentialsSchema
} from "./digital-ocean";
import {
TFlyioConnection,
TFlyioConnectionConfig,
@@ -238,6 +244,7 @@ export type TAppConnection = { id: string } & (
| TRailwayConnection
| TChecklyConnection
| TSupabaseConnection
| TDigitalOceanConnection
| TOktaConnection
);
@@ -280,6 +287,7 @@ export type TAppConnectionInput = { id: string } & (
| TRailwayConnectionInput
| TChecklyConnectionInput
| TSupabaseConnectionInput
| TDigitalOceanConnectionInput
| TOktaConnectionInput
);
@@ -330,6 +338,7 @@ export type TAppConnectionConfig =
| TRailwayConnectionConfig
| TChecklyConnectionConfig
| TSupabaseConnectionConfig
| TDigitalOceanConnectionConfig
| TOktaConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
@@ -367,6 +376,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema
| TValidateSupabaseConnectionCredentialsSchema
| TValidateDigitalOceanCredentialsSchema
| TValidateOktaConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
@@ -0,0 +1,3 @@
export enum DigitalOceanConnectionMethod {
ApiToken = "api-token"
}
@@ -0,0 +1,37 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
import { DigitalOceanAppPlatformPublicAPI } from "./digital-ocean-connection-public-client";
import { DigitalOceanConnectionListItemSchema } from "./digital-ocean-connection-schemas";
import { TDigitalOceanConnectionConfig } from "./digital-ocean-connection-types";
export const getDigitalOceanConnectionListItem = () => {
return {
name: "Digital Ocean" as z.infer<typeof DigitalOceanConnectionListItemSchema>["name"],
app: AppConnection.DigitalOcean as const,
methods: Object.values(DigitalOceanConnectionMethod)
};
};
export const validateDigitalOceanConnectionCredentials = async (config: TDigitalOceanConnectionConfig) => {
try {
await DigitalOceanAppPlatformPublicAPI.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;
};
@@ -0,0 +1,105 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable class-methods-use-this */
import { AxiosInstance } from "axios";
import { createRequestClient } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
import {
TDigitalOceanApp,
TDigitalOceanConnectionConfig,
TDigitalOceanVariable
} from "./digital-ocean-connection-types";
class DigitalOceanAppPlatformPublicClient {
private readonly client: AxiosInstance;
constructor() {
this.client = createRequestClient({
baseURL: `${IntegrationUrls.DIGITAL_OCEAN_API_URL}/v2`,
headers: {
"Content-Type": "application/json",
Accept: "application/json"
}
});
}
async healthcheck(connection: TDigitalOceanConnectionConfig) {
switch (connection.method) {
case DigitalOceanConnectionMethod.ApiToken:
await this.getApps(connection);
break;
default:
throw new Error(`Unsupported connection method`);
}
}
async getApps(connection: TDigitalOceanConnectionConfig) {
const response = await this.client.get<{ apps: TDigitalOceanApp[] }>(`/apps`, {
headers: {
Authorization: `Bearer ${connection.credentials.apiToken}`
}
});
return response.data.apps;
}
async getApp(connection: TDigitalOceanConnectionConfig, appId: string) {
const response = await this.client.get<{ app: TDigitalOceanApp }>(`/apps/${appId}`, {
headers: {
Authorization: `Bearer ${connection.credentials.apiToken}`
}
});
return response.data.app;
}
async getVariables(connection: TDigitalOceanConnectionConfig, appId: string): Promise<TDigitalOceanVariable[]> {
const app = await this.getApp(connection, appId);
return app.spec.envs || [];
}
async putVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) {
const response = await this.getApp(connection, appId);
return this.client.put(
`/apps/${appId}`,
{
spec: {
...response.spec,
envs: input
}
},
{
headers: {
Authorization: `Bearer ${connection.credentials.apiToken}`
}
}
);
}
async deleteVariables(connection: TDigitalOceanConnectionConfig, appId: string, ...input: TDigitalOceanVariable[]) {
const response = await this.getApp(connection, appId);
const existing = response.spec.envs || [];
const variables = existing.filter((v) => input.find((i) => i.key === v.key));
return this.client.put(
`/apps/${appId}`,
{
spec: {
...response.spec,
envs: variables
}
},
{
headers: {
Authorization: `Bearer ${connection.credentials.apiToken}`
}
}
);
}
}
export const DigitalOceanAppPlatformPublicAPI = new DigitalOceanAppPlatformPublicClient();
@@ -0,0 +1,67 @@
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 { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
export const DigitalOceanConnectionMethodSchema = z
.nativeEnum(DigitalOceanConnectionMethod)
.describe(AppConnections.CREATE(AppConnection.DigitalOcean).method);
export const DigitalOceanConnectionAccessTokenCredentialsSchema = z.object({
apiToken: z
.string()
.trim()
.min(1, "API Token required")
.max(255)
.describe(AppConnections.CREDENTIALS.DIGITAL_OCEAN_APP_PLATFORM.apiToken)
});
const BaseDigitalOceanConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.DigitalOcean)
});
export const DigitalOceanConnectionSchema = BaseDigitalOceanConnectionSchema.extend({
method: DigitalOceanConnectionMethodSchema,
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema
});
export const SanitizedDigitalOceanConnectionSchema = z.discriminatedUnion("method", [
BaseDigitalOceanConnectionSchema.extend({
method: DigitalOceanConnectionMethodSchema,
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateDigitalOceanConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: DigitalOceanConnectionMethodSchema,
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.DigitalOcean).credentials
)
})
]);
export const CreateDigitalOceanConnectionSchema = ValidateDigitalOceanConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.DigitalOcean)
);
export const UpdateDigitalOceanConnectionSchema = z
.object({
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.DigitalOcean).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DigitalOcean));
export const DigitalOceanConnectionListItemSchema = z.object({
name: z.literal("Digital Ocean"),
app: z.literal(AppConnection.DigitalOcean),
methods: z.nativeEnum(DigitalOceanConnectionMethod).array()
});
@@ -0,0 +1,29 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { DigitalOceanAppPlatformPublicAPI } from "./digital-ocean-connection-public-client";
import { TDigitalOceanConnection } from "./digital-ocean-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TDigitalOceanConnection>;
export const digitalOceanAppPlatformConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
const connection = await getAppConnection(AppConnection.DigitalOcean, connectionId, actor);
try {
const apps = await DigitalOceanAppPlatformPublicAPI.getApps(connection);
return apps;
} catch (error) {
logger.error(error, "Failed to list apps on Digital Ocean");
return [];
}
};
return {
listApps
};
};
@@ -0,0 +1,42 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateDigitalOceanConnectionSchema,
DigitalOceanConnectionSchema,
ValidateDigitalOceanConnectionCredentialsSchema
} from "./digital-ocean-connection-schemas";
export type TDigitalOceanConnection = z.infer<typeof DigitalOceanConnectionSchema>;
export type TDigitalOceanConnectionInput = z.infer<typeof CreateDigitalOceanConnectionSchema> & {
app: AppConnection.DigitalOcean;
};
export type TValidateDigitalOceanCredentialsSchema = typeof ValidateDigitalOceanConnectionCredentialsSchema;
export type TDigitalOceanConnectionConfig = DiscriminativePick<
TDigitalOceanConnection,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TDigitalOceanVariable = {
key: string;
value: string;
type: "SECRET" | "GENERAL";
};
export type TDigitalOceanApp = {
id: string;
spec: {
name: string;
services: Array<{
name: string;
}>;
envs?: TDigitalOceanVariable[];
};
};
@@ -0,0 +1,4 @@
export * from "./digital-ocean-connection-constants";
export * from "./digital-ocean-connection-fns";
export * from "./digital-ocean-connection-schemas";
export * from "./digital-ocean-connection-types";
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Digital Ocean App Platform" as const,
destination: SecretSync.DigitalOceanAppPlatform,
connection: AppConnection.DigitalOcean,
canImportSecrets: false
};
@@ -0,0 +1,83 @@
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean";
import { DigitalOceanAppPlatformPublicAPI } from "@app/services/app-connection/digital-ocean/digital-ocean-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 { TDigitalOceanAppPlatformSyncWithCredentials } from "./digital-ocean-app-platform-sync-types";
export const DigitalOceanAppPlatformSyncFns = {
async getSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials) {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
async syncSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const config = secretSync.destinationConfig;
const existing = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId);
const variables: Record<string, TDigitalOceanVariable> = Object.fromEntries(existing.map((v) => [v.key, v]));
for (const [key, value] of Object.entries(secretMap)) {
variables[key] = {
key,
value: value.value,
type: "SECRET"
} as TDigitalOceanVariable;
}
if (!disableSecretDeletion) {
for (const v of existing) {
if (!matchesSchema(v.key, environment?.slug || "", keySchema)) continue;
if (!(v.key in secretMap)) {
delete variables[v.key];
}
}
}
try {
const vars = Object.values(variables);
await DigitalOceanAppPlatformPublicAPI.putVariables(secretSync.connection, config.appId, ...vars);
} catch (error) {
throw new SecretSyncError({
error
});
}
},
async removeSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) {
const config = secretSync.destinationConfig;
try {
const existingSecrets = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId);
const vars = Object.entries(existingSecrets)
.map(([key, v]) => {
if (!(key in secretMap)) return;
return {
key,
value: v.value,
type: "SECRET"
} as TDigitalOceanVariable;
})
.filter(Boolean) as TDigitalOceanVariable[];
await DigitalOceanAppPlatformPublicAPI.deleteVariables(secretSync.connection, config.appId, ...vars);
} catch (error) {
throw new SecretSyncError({
error
});
}
}
};
@@ -0,0 +1,46 @@
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 DigitalOceanAppPlatformSyncDestinationConfigSchema = z.object({
appId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
appName: z.string().min(1, "Account Name is required").max(255, "Account Name must be less than 255 characters")
});
const DigitalOceanAppPlatformSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const DigitalOceanAppPlatformSyncSchema = BaseSecretSyncSchema(
SecretSync.DigitalOceanAppPlatform,
DigitalOceanAppPlatformSyncOptionsConfig
).extend({
destination: z.literal(SecretSync.DigitalOceanAppPlatform),
destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema
});
export const CreateDigitalOceanAppPlatformSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.DigitalOceanAppPlatform,
DigitalOceanAppPlatformSyncOptionsConfig
).extend({
destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema
});
export const UpdateDigitalOceanAppPlatformSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.DigitalOceanAppPlatform,
DigitalOceanAppPlatformSyncOptionsConfig
).extend({
destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema.optional()
});
export const DigitalOceanAppPlatformSyncListItemSchema = z.object({
name: z.literal("Digital Ocean App Platform"),
connection: z.literal(AppConnection.DigitalOcean),
destination: z.literal(SecretSync.DigitalOceanAppPlatform),
canImportSecrets: z.literal(false)
});
@@ -0,0 +1,23 @@
import z from "zod";
import { TDigitalOceanConnection, TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean";
import {
CreateDigitalOceanAppPlatformSyncSchema,
DigitalOceanAppPlatformSyncListItemSchema,
DigitalOceanAppPlatformSyncSchema
} from "./digital-ocean-app-platform-sync-schemas";
export type TDigitalOceanAppPlatformSyncListItem = z.infer<typeof DigitalOceanAppPlatformSyncListItemSchema>;
export type TDigitalOceanAppPlatformSync = z.infer<typeof DigitalOceanAppPlatformSyncSchema>;
export type TDigitalOceanAppPlatformSyncInput = z.infer<typeof CreateDigitalOceanAppPlatformSyncSchema>;
export type TDigitalOceanAppPlatformSyncWithCredentials = TDigitalOceanAppPlatformSync & {
connection: TDigitalOceanConnection;
};
export type TDigitalOceanAppPlatformSecret = TDigitalOceanVariable & {
type: "SECRET";
};
@@ -0,0 +1,4 @@
export * from "./digital-ocean-app-platform-sync-constants";
export * from "./digital-ocean-app-platform-sync-fns";
export * from "./digital-ocean-app-platform-sync-schemas";
export * from "./digital-ocean-app-platform-sync-types";
@@ -26,6 +26,7 @@ export enum SecretSync {
Zabbix = "zabbix",
Railway = "railway",
Checkly = "checkly",
DigitalOceanAppPlatform = "digital-ocean-app-platform",
Bitbucket = "bitbucket"
}
@@ -35,6 +35,10 @@ 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";
import {
DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION,
DigitalOceanAppPlatformSyncFns
} from "./digital-ocean-app-platform";
import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
@@ -82,6 +86,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION,
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION,
[SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION,
[SecretSync.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION,
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION
};
@@ -260,6 +265,8 @@ export const SecretSyncFns = {
return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.DigitalOceanAppPlatform:
return DigitalOceanAppPlatformSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket:
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
@@ -369,6 +376,9 @@ export const SecretSyncFns = {
case SecretSync.Supabase:
secretMap = await SupabaseSyncFns.getSecrets(secretSync);
break;
case SecretSync.DigitalOceanAppPlatform:
secretMap = await DigitalOceanAppPlatformSyncFns.getSecrets(secretSync);
break;
case SecretSync.Bitbucket:
secretMap = await BitbucketSyncFns.getSecrets(secretSync);
break;
@@ -459,6 +469,8 @@ export const SecretSyncFns = {
return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.DigitalOceanAppPlatform:
return DigitalOceanAppPlatformSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket:
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
@@ -29,6 +29,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Zabbix]: "Zabbix",
[SecretSync.Railway]: "Railway",
[SecretSync.Checkly]: "Checkly",
[SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform",
[SecretSync.Bitbucket]: "Bitbucket"
};
@@ -60,6 +61,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway,
[SecretSync.Checkly]: AppConnection.Checkly,
[SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean,
[SecretSync.Bitbucket]: AppConnection.Bitbucket
};
@@ -91,5 +93,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Zabbix]: SecretSyncPlanType.Regular,
[SecretSync.Railway]: SecretSyncPlanType.Regular,
[SecretSync.Checkly]: SecretSyncPlanType.Regular,
[SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular,
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular
};
@@ -96,6 +96,11 @@ import {
TCloudflareWorkersSyncListItem,
TCloudflareWorkersSyncWithCredentials
} from "./cloudflare-workers";
import {
TDigitalOceanAppPlatformSyncInput,
TDigitalOceanAppPlatformSyncListItem,
TDigitalOceanAppPlatformSyncWithCredentials
} from "./digital-ocean-app-platform/digital-ocean-app-platform-sync-types";
import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab";
@@ -203,6 +208,7 @@ export type TSecretSyncWithCredentials =
| TRailwaySyncWithCredentials
| TChecklySyncWithCredentials
| TSupabaseSyncWithCredentials
| TDigitalOceanAppPlatformSyncWithCredentials
| TBitbucketSyncWithCredentials;
export type TSecretSyncInput =
@@ -233,6 +239,7 @@ export type TSecretSyncInput =
| TRailwaySyncInput
| TChecklySyncInput
| TSupabaseSyncInput
| TDigitalOceanAppPlatformSyncInput
| TBitbucketSyncInput;
export type TSecretSyncListItem =
@@ -263,6 +270,7 @@ export type TSecretSyncListItem =
| TRailwaySyncListItem
| TChecklySyncListItem
| TSupabaseSyncListItem
| TDigitalOceanAppPlatformSyncListItem
| TBitbucketSyncListItem;
export type TSyncOptionsConfig = {