Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync

This commit is contained in:
Sheen Capadngan
2025-06-20 19:24:45 +08:00
117 changed files with 3096 additions and 538 deletions
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const HEROKU_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Heroku",
destination: SecretSync.Heroku,
connection: AppConnection.Heroku,
canImportSecrets: true
};
@@ -0,0 +1,170 @@
import { request } from "@app/lib/config/request";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { HerokuConnectionMethod, refreshHerokuToken, THerokuConnection } from "@app/services/app-connection/heroku";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import {
THerokuConfigVars,
THerokuListVariables,
THerokuSyncWithCredentials,
THerokuUpdateVariables
} from "@app/services/secret-sync/heroku/heroku-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
type THerokuSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
const getValidAuthToken = async (
connection: THerokuConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<string> => {
if (
connection.method === HerokuConnectionMethod.OAuth &&
connection.credentials.refreshToken &&
connection.credentials.expiresAt < new Date()
) {
const authToken = await refreshHerokuToken(
connection.credentials.refreshToken,
connection.id,
connection.orgId,
appConnectionDAL,
kmsService
);
return authToken;
}
return connection.credentials.authToken;
};
const getHerokuConfigVars = async ({ authToken, app }: THerokuListVariables): Promise<THerokuConfigVars> => {
const { data } = await request.get<THerokuConfigVars>(
`${IntegrationUrls.HEROKU_API_URL}/apps/${encodeURIComponent(app)}/config-vars`,
{
headers: {
Authorization: `Bearer ${authToken}`,
Accept: "application/vnd.heroku+json; version=3"
}
}
);
return data;
};
const updateHerokuConfigVars = async ({ authToken, app, configVars }: THerokuUpdateVariables) => {
return request.patch(`${IntegrationUrls.HEROKU_API_URL}/apps/${encodeURIComponent(app)}/config-vars`, configVars, {
headers: {
Authorization: `Bearer ${authToken}`,
Accept: "application/vnd.heroku+json; version=3",
"Content-Type": "application/json"
}
});
};
export const HerokuSyncFns = {
syncSecrets: async (
secretSync: THerokuSyncWithCredentials,
secretMap: TSecretMap,
{ appConnectionDAL, kmsService }: THerokuSyncFactoryDeps
) => {
const {
connection,
environment,
destinationConfig: { app }
} = secretSync;
const authToken = await getValidAuthToken(connection, appConnectionDAL, kmsService);
try {
const updatedConfigVars: THerokuConfigVars = {};
for (const [key, { value }] of Object.entries(secretMap)) {
updatedConfigVars[key] = value;
}
if (!secretSync.syncOptions.disableSecretDeletion) {
const currentConfigVars = await getHerokuConfigVars({ authToken, app });
for (const key of Object.keys(currentConfigVars)) {
if (matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema) && !(key in secretMap)) {
updatedConfigVars[key] = null;
}
}
}
await updateHerokuConfigVars({
authToken,
app,
configVars: updatedConfigVars
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: "batch_update"
});
}
},
removeSecrets: async (
secretSync: THerokuSyncWithCredentials,
secretMap: TSecretMap,
{ appConnectionDAL, kmsService }: THerokuSyncFactoryDeps
) => {
const {
connection,
destinationConfig: { app }
} = secretSync;
const authToken = await getValidAuthToken(connection, appConnectionDAL, kmsService);
try {
const currentConfigVars = await getHerokuConfigVars({ authToken, app });
const configVarsToUpdate: Record<string, null> = {};
for (const key of Object.keys(secretMap)) {
if (key in currentConfigVars) {
configVarsToUpdate[key] = null;
}
}
if (Object.keys(configVarsToUpdate).length > 0) {
await updateHerokuConfigVars({
authToken,
app,
configVars: configVarsToUpdate
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: "batch_remove"
});
}
},
getSecrets: async (
secretSync: THerokuSyncWithCredentials,
{ appConnectionDAL, kmsService }: THerokuSyncFactoryDeps
): Promise<TSecretMap> => {
const {
connection,
destinationConfig: { app }
} = secretSync;
const authToken = await getValidAuthToken(connection, appConnectionDAL, kmsService);
const data = await getHerokuConfigVars({ authToken, app });
const transformed = Object.entries(data).reduce((acc, [key, value]) => {
if (!value) {
return acc;
}
acc[key] = { value };
return acc;
}, {} as TSecretMap);
return transformed;
}
};
@@ -0,0 +1,44 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const HerokuSyncDestinationConfigSchema = z.object({
app: z.string().trim().min(1, "App required").describe(SecretSyncs.DESTINATION_CONFIG.HEROKU.app),
appName: z.string().trim().min(1, "App name required").describe(SecretSyncs.DESTINATION_CONFIG.HEROKU.appName)
});
const HerokuSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const HerokuSyncSchema = BaseSecretSyncSchema(SecretSync.Heroku, HerokuSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Heroku),
destinationConfig: HerokuSyncDestinationConfigSchema
});
export const CreateHerokuSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Heroku,
HerokuSyncOptionsConfig
).extend({
destinationConfig: HerokuSyncDestinationConfigSchema
});
export const UpdateHerokuSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Heroku,
HerokuSyncOptionsConfig
).extend({
destinationConfig: HerokuSyncDestinationConfigSchema.optional()
});
export const HerokuSyncListItemSchema = z.object({
name: z.literal("Heroku"),
connection: z.literal(AppConnection.Heroku),
destination: z.literal(SecretSync.Heroku),
canImportSecrets: z.literal(true)
});
@@ -0,0 +1,24 @@
import { z } from "zod";
import { THerokuConnection } from "@app/services/app-connection/heroku";
import { CreateHerokuSyncSchema, HerokuSyncListItemSchema, HerokuSyncSchema } from "./heroku-sync-schemas";
export type THerokuSync = z.infer<typeof HerokuSyncSchema>;
export type THerokuSyncInput = z.infer<typeof CreateHerokuSyncSchema>;
export type THerokuSyncListItem = z.infer<typeof HerokuSyncListItemSchema>;
export type THerokuSyncWithCredentials = THerokuSync & {
connection: THerokuConnection;
};
export type THerokuConfigVars = Record<string, string | null>;
export type THerokuListVariables = {
authToken: string;
app: string;
};
export type THerokuUpdateVariables = THerokuListVariables & {
configVars: THerokuConfigVars;
};
@@ -0,0 +1,4 @@
export * from "./heroku-sync-constants";
export * from "./heroku-sync-fns";
export * from "./heroku-sync-schemas";
export * from "./heroku-sync-types";
@@ -16,6 +16,7 @@ export enum SecretSync {
TeamCity = "teamcity",
OCIVault = "oci-vault",
OnePass = "1password",
Heroku = "heroku",
Render = "render",
Flyio = "flyio",
CloudflarePages = "cloudflare-pages"
@@ -35,6 +35,7 @@ import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
@@ -62,6 +63,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION,
[SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION,
[SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION,
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION
@@ -210,6 +212,8 @@ export const SecretSyncFns = {
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Heroku:
return HerokuSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill:
@@ -305,6 +309,9 @@ export const SecretSyncFns = {
case SecretSync.OnePass:
secretMap = await OnePassSyncFns.getSecrets(secretSync);
break;
case SecretSync.Heroku:
secretMap = await HerokuSyncFns.getSecrets(secretSync, { appConnectionDAL, kmsService });
break;
case SecretSync.Render:
secretMap = await RenderSyncFns.getSecrets(secretSync);
break;
@@ -381,6 +388,8 @@ export const SecretSyncFns = {
return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OnePass:
return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Heroku:
return HerokuSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.Render:
return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Flyio:
@@ -19,6 +19,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.TeamCity]: "TeamCity",
[SecretSync.OCIVault]: "OCI Vault",
[SecretSync.OnePass]: "1Password",
[SecretSync.Heroku]: "Heroku",
[SecretSync.Render]: "Render",
[SecretSync.Flyio]: "Fly.io",
[SecretSync.CloudflarePages]: "Cloudflare Pages"
@@ -42,6 +43,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.TeamCity]: AppConnection.TeamCity,
[SecretSync.OCIVault]: AppConnection.OCI,
[SecretSync.OnePass]: AppConnection.OnePass,
[SecretSync.Heroku]: AppConnection.Heroku,
[SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
@@ -65,6 +67,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.TeamCity]: SecretSyncPlanType.Regular,
[SecretSync.OCIVault]: SecretSyncPlanType.Enterprise,
[SecretSync.OnePass]: SecretSyncPlanType.Regular,
[SecretSync.Heroku]: SecretSyncPlanType.Regular,
[SecretSync.Render]: SecretSyncPlanType.Regular,
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular
@@ -80,6 +80,7 @@ import {
THCVaultSyncListItem,
THCVaultSyncWithCredentials
} from "./hc-vault/hc-vault-sync-types";
import { THerokuSync, THerokuSyncInput, THerokuSyncListItem, THerokuSyncWithCredentials } from "./heroku";
import {
THumanitecSync,
THumanitecSyncInput,
@@ -130,6 +131,7 @@ export type TSecretSync =
| TTeamCitySync
| TOCIVaultSync
| TOnePassSync
| THerokuSync
| TRenderSync
| TFlyioSync
| TCloudflarePagesSync;
@@ -152,6 +154,7 @@ export type TSecretSyncWithCredentials =
| TTeamCitySyncWithCredentials
| TOCIVaultSyncWithCredentials
| TOnePassSyncWithCredentials
| THerokuSyncWithCredentials
| TRenderSyncWithCredentials
| TFlyioSyncWithCredentials
| TCloudflarePagesSyncWithCredentials;
@@ -174,6 +177,7 @@ export type TSecretSyncInput =
| TTeamCitySyncInput
| TOCIVaultSyncInput
| TOnePassSyncInput
| THerokuSyncInput
| TRenderSyncInput
| TFlyioSyncInput
| TCloudflarePagesSyncInput;
@@ -196,6 +200,7 @@ export type TSecretSyncListItem =
| TTeamCitySyncListItem
| TOCIVaultSyncListItem
| TOnePassSyncListItem
| THerokuSyncListItem
| TRenderSyncListItem
| TFlyioSyncListItem
| TCloudflarePagesSyncListItem;