feat(secret-sync): Add Heroku Secret Sync

This commit is contained in:
carlosmonastyrski
2025-06-17 10:52:55 -03:00
parent 46f9927cf1
commit c2612f242c
86 changed files with 1772 additions and 52 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,167 @@
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/${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/${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 currentConfigVars = await getHerokuConfigVars({ authToken, app });
const updatedConfigVars: THerokuConfigVars = {};
for (const [key, { value }] of Object.entries(secretMap)) {
updatedConfigVars[key] = value;
}
if (!secretSync.syncOptions.disableSecretDeletion) {
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";