Merge pull request #3926 from Infisical/feat/remindersImprovement

feat(secret-reminders): rework secret reminders logic
This commit is contained in:
carlosmonastyrski
2025-07-23 16:07:04 -03:00
committed by GitHub
37 changed files with 1885 additions and 532 deletions
-91
View File
@@ -5,8 +5,6 @@ import { TDbClient } from "@app/db";
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName, TQueueServiceFactory } from "@app/queue";
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
@@ -383,94 +381,6 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
const pruneSecretReminders = async (queueService: TQueueServiceFactory) => {
const REMINDER_PRUNE_BATCH_SIZE = 5_000;
const MAX_RETRY_ON_FAILURE = 3;
let numberOfRetryOnFailure = 0;
let deletedReminderCount = 0;
logger.info(`${QueueName.DailyResourceCleanUp}: secret reminders started`);
try {
const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder);
const reminderJobs = repeatableJobs
.map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key }))
.filter(Boolean);
if (reminderJobs.length === 0) {
logger.info(`${QueueName.DailyResourceCleanUp}: no reminder jobs found`);
return;
}
for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) {
try {
const batchIds = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE).map((r) => r.secretId);
const payload = {
$in: {
id: batchIds
}
};
const opts = {
limit: REMINDER_PRUNE_BATCH_SIZE
};
// Find existing secrets with pagination
// eslint-disable-next-line no-await-in-loop
const [secrets, secretsV2] = await Promise.all([
ormify(db, TableName.Secret).find(payload, opts),
ormify(db, TableName.SecretV2).find(payload, opts)
]);
const foundSecretIds = new Set([
...secrets.map((secret) => secret.id),
...secretsV2.map((secret) => secret.id)
]);
// Find IDs that don't exist in either table
const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId));
// Delete reminders for non-existent secrets
for (const secretId of secretIdsNotFound) {
const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey;
if (jobKey) {
// eslint-disable-next-line no-await-in-loop
await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey);
deletedReminderCount += 1;
}
}
numberOfRetryOnFailure = 0;
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, `Failed to process batch at offset ${offset}`);
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
break;
}
// Retry the current batch
offset -= REMINDER_PRUNE_BATCH_SIZE;
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure));
}
// Small delay between batches
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, 10));
}
} catch (error) {
logger.error(error, "Failed to complete secret reminder pruning");
} finally {
logger.info(
`${QueueName.DailyResourceCleanUp}: secret reminders completed. Deleted ${deletedReminderCount} reminders`
);
}
};
return {
...secretOrm,
update,
@@ -485,7 +395,6 @@ export const secretDALFactory = (db: TDbClient) => {
upsertSecretReferences,
findReferencedSecretReferences,
findAllProjectSecretValues,
pruneSecretReminders,
findManySecretsWithTags
};
};
+10 -17
View File
@@ -18,11 +18,9 @@ import { ProjectPermissionSecretActions } from "@app/ee/services/permission/proj
import { getConfig } from "@app/lib/config/env";
import { buildSecretBlindIndexFromName } from "@app/lib/crypto";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import {
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
@@ -34,6 +32,7 @@ import { KmsDataKey } from "../kms/kms-types";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TReminderServiceFactory } from "../reminder/reminder-types";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretDALFactory } from "./secret-dal";
@@ -745,7 +744,8 @@ export const fnSecretBulkDelete = async ({
tx,
actorId,
secretDAL,
secretQueueService
secretQueueService,
projectId
}: TFnSecretBulkDelete) => {
const deletedSecrets = await secretDAL.deleteMany(
inputSecrets.map(({ type, secretBlindIndex }) => ({
@@ -761,7 +761,10 @@ export const fnSecretBulkDelete = async ({
deletedSecrets
.filter(({ secretReminderRepeatDays }) => Boolean(secretReminderRepeatDays))
.map(({ id, secretReminderRepeatDays }) =>
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: secretReminderRepeatDays as number }, tx)
secretQueueService.removeSecretReminder(
{ secretId: id, repeatDays: secretReminderRepeatDays as number, projectId },
tx
)
)
);
@@ -1228,14 +1231,14 @@ export const decryptSecretWithBot = (
type TFnDeleteProjectSecretReminders = {
secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
};
export const fnDeleteProjectSecretReminders = async (
projectId: string,
{ secretDAL, secretV2BridgeDAL, queueService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders
{ secretDAL, secretV2BridgeDAL, reminderService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders
) => {
const projectFolders = await folderDAL.findByProjectId(projectId);
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId, false);
@@ -1250,23 +1253,13 @@ export const fnDeleteProjectSecretReminders = async (
$notNull: ["secretReminderRepeatDays"]
});
const appCfg = getConfig();
for await (const secret of projectSecrets) {
const repeatDays = shouldUseSecretV2Bridge
? (secret as { reminderRepeatDays: number }).reminderRepeatDays
: (secret as { secretReminderRepeatDays: number }).secretReminderRepeatDays;
// We're using the queue service directly to get around conflicting imports.
if (repeatDays) {
await queueService.stopRepeatableJob(
QueueName.SecretReminder,
QueueJobs.SecretReminder,
{
// on prod it this will be in days, in development this will be second
every: appCfg.NODE_ENV === "development" ? secondsToMillis(repeatDays) : daysToMillisecond(repeatDays)
},
`reminder-${secret.id}`
);
await reminderService.deleteReminderBySecretId(secret.id, projectId);
}
}
};
+38 -161
View File
@@ -19,7 +19,6 @@ import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/sn
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
@@ -40,7 +39,6 @@ import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-
import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectDALFactory } from "../project/project-dal";
import { createProjectKey } from "../project/project-fns";
@@ -49,12 +47,12 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TReminderServiceFactory } from "../reminder/reminder-types";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
@@ -92,7 +90,6 @@ type TSecretQueueFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "create">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers" | "create">;
smtpService: TSmtpService;
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
@@ -112,11 +109,8 @@ type TSecretQueueFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
secretReminderRecipientsDAL: Pick<
TSecretReminderRecipientsDALFactory,
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
>;
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsSyncSecretsByPath">;
reminderService: Pick<TReminderServiceFactory, "createReminderInternal" | "deleteReminderBySecretId">;
};
export type TGetSecrets = {
@@ -154,7 +148,6 @@ export const secretQueueFactory = ({
userDAL,
webhookDAL,
projectEnvDAL,
orgDAL,
smtpService,
projectDAL,
projectBotDAL,
@@ -177,9 +170,9 @@ export const secretQueueFactory = ({
projectUserMembershipRoleDAL,
projectKeyDAL,
resourceMetadataDAL,
secretReminderRecipientsDAL,
secretSyncQueue,
folderCommitService
folderCommitService,
reminderService
}: TSecretQueueFactoryDep) => {
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
@@ -189,19 +182,8 @@ export const secretQueueFactory = ({
const removeSecretReminder = async ({ deleteRecipients = true, ...dto }: TRemoveSecretReminderDTO, tx?: Knex) => {
if (deleteRecipients) {
await secretReminderRecipientsDAL.delete({ secretId: dto.secretId }, tx);
await reminderService.deleteReminderBySecretId(dto.secretId, dto.projectId, tx);
}
const appCfg = getConfig();
await queueService.stopRepeatableJob(
QueueName.SecretReminder,
QueueJobs.SecretReminder,
{
// on prod it this will be in days, in development this will be second
every: appCfg.NODE_ENV === "development" ? secondsToMillis(dto.repeatDays) : daysToMillisecond(dto.repeatDays)
},
`reminder-${dto.secretId}`
);
};
const $generateActor = async (actorId?: string, isManual?: boolean): Promise<Actor> => {
@@ -241,11 +223,9 @@ export const secretQueueFactory = ({
oldSecret,
newSecret,
projectId,
deleteRecipients = true
secretReminderRecipients
}: TCreateSecretReminderDTO) => {
try {
const appCfg = getConfig();
if (oldSecret.id !== newSecret.id) {
throw new BadRequestError({
name: "SecretReminderIdMismatch",
@@ -260,38 +240,13 @@ export const secretQueueFactory = ({
});
}
// If the secret already has a reminder, we should remove the existing one first.
if (oldSecret.secretReminderRepeatDays) {
await removeSecretReminder({
repeatDays: oldSecret.secretReminderRepeatDays,
secretId: oldSecret.id,
deleteRecipients
});
}
await queueService.queue(
QueueName.SecretReminder,
QueueJobs.SecretReminder,
{
note: newSecret.secretReminderNote,
projectId,
repeatDays: newSecret.secretReminderRepeatDays,
secretId: newSecret.id
},
{
jobId: `reminder-${newSecret.id}`,
repeat: {
// on prod it this will be in days, in development this will be second
every:
appCfg.NODE_ENV === "development"
? secondsToMillis(newSecret.secretReminderRepeatDays)
: daysToMillisecond(newSecret.secretReminderRepeatDays),
immediately: true
},
removeOnComplete: true,
removeOnFail: true
}
);
await reminderService.createReminderInternal({
secretId: newSecret.id,
message: newSecret.secretReminderNote,
repeatDays: newSecret.secretReminderRepeatDays,
recipients: secretReminderRecipients,
projectId
});
} catch (err) {
logger.error(err, "Failed to create secret reminder.");
throw new BadRequestError({
@@ -304,55 +259,30 @@ export const secretQueueFactory = ({
const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => {
const { secretReminderRepeatDays, secretReminderNote, secretReminderRecipients } = newSecret;
const recipientsUpdated =
secretReminderRecipients?.some(
(newId) => !oldSecret.secretReminderRecipients?.find((oldId) => newId === oldId)
) || secretReminderRecipients?.length !== oldSecret.secretReminderRecipients?.length;
await secretReminderRecipientsDAL.transaction(async (tx) => {
if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) {
if (
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote)
) {
await addSecretReminder({
oldSecret,
newSecret,
projectId,
deleteRecipients: false
});
} else if (
secretReminderRepeatDays === null &&
secretReminderNote === null &&
oldSecret.secretReminderRepeatDays
) {
await removeSecretReminder({
secretId: oldSecret.id,
repeatDays: oldSecret.secretReminderRepeatDays
});
}
if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) {
if (
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote)
) {
await addSecretReminder({
oldSecret,
newSecret,
projectId,
secretReminderRecipients: secretReminderRecipients ?? [],
deleteRecipients: false
});
} else if (
secretReminderRepeatDays === null &&
secretReminderNote === null &&
oldSecret.secretReminderRepeatDays
) {
await removeSecretReminder({
secretId: oldSecret.id,
repeatDays: oldSecret.secretReminderRepeatDays,
projectId
});
}
if (recipientsUpdated) {
// if no recipients, delete all existing recipients
if (!secretReminderRecipients?.length) {
const existingRecipients = await secretReminderRecipientsDAL.findUsersBySecretId(newSecret.id, tx);
if (existingRecipients) {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
}
} else {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
await secretReminderRecipientsDAL.insertMany(
secretReminderRecipients.map((r) => ({
secretId: newSecret.id,
userId: r,
projectId
})),
tx
);
}
}
});
}
};
const createManySecretsRawFn = createManySecretsRawFnFactory({
projectDAL,
@@ -1118,62 +1048,9 @@ export const secretQueueFactory = ({
}
});
// TODO(Carlos): remove this queue (needed for queue initialization and perform the migration)
queueService.start(QueueName.SecretReminder, async ({ data }) => {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}]`);
const { projectId } = data;
const organization = await orgDAL.findOrgByProjectId(projectId);
const project = await projectDAL.findById(projectId);
const secret = await secretV2BridgeDAL.findById(data.secretId);
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
const recipients = await secretReminderRecipientsDAL.findUsersBySecretId(data.secretId);
if (!organization) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
return;
}
if (!project) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no project found`);
return;
}
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
if (!projectMembers || !projectMembers.length) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no project members found`);
return;
}
const selectedRecipients = recipients?.length
? recipients.map((r) => r.email as string)
: projectMembers.map((m) => m.user.email as string);
await smtpService.sendMail({
template: SmtpTemplates.SecretReminder,
subjectLine: "Infisical secret reminder",
recipients: selectedRecipients,
substitutions: {
reminderNote: data.note, // May not be present.
projectName: project.name,
organizationName: organization.name
}
});
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
type: WebhookEvents.SecretReminderExpired,
payload: {
projectName: project.name,
projectId: project.id,
secretPath: folder?.path,
environment: folder?.environmentSlug || "",
reminderNote: data.note,
secretName: secret?.key,
secretId: data.secretId
}
});
logger.info(`(deprecated) secretReminderQueue.process: [secretDocument=${data.secretId}]`);
});
const startSecretV2Migration = async (projectId: string) => {
+46 -11
View File
@@ -46,6 +46,7 @@ import { ChangeType } from "../folder-commit/folder-commit-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TReminderServiceFactory } from "../reminder/reminder-types";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
@@ -129,6 +130,7 @@ type TSecretServiceFactoryDep = {
"insertMany" | "insertApprovalSecretTags"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
reminderService: Pick<TReminderServiceFactory, "createReminder">;
};
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
@@ -151,7 +153,8 @@ export const secretServiceFactory = ({
secretApprovalRequestSecretDAL,
secretV2BridgeService,
secretApprovalRequestService,
licenseService
licenseService,
reminderService
}: TSecretServiceFactoryDep) => {
const getSecretReference = async (projectId: string) => {
// if bot key missing means e2e still exist
@@ -551,7 +554,8 @@ export const secretServiceFactory = ({
await secretQueueService.removeSecretReminder(
{
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
secretId: secret.id,
projectId
},
tx
);
@@ -1082,7 +1086,8 @@ export const secretServiceFactory = ({
await secretQueueService.removeSecretReminder(
{
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
secretId: secret.id,
projectId
},
tx
);
@@ -1672,8 +1677,6 @@ export const secretServiceFactory = ({
secretComment,
secretValue,
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays,
secretMetadata
}
]
@@ -1855,9 +1858,6 @@ export const secretServiceFactory = ({
secretComment,
secretValue,
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays,
secretReminderRecipients,
secretMetadata
}
]
@@ -1866,9 +1866,6 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Approval as const, approval };
}
const secret = await secretV2BridgeService.updateSecret({
secretReminderRepeatDays,
secretReminderNote,
secretReminderRecipients,
skipMultilineEncoding,
tagIds,
secretComment,
@@ -1886,6 +1883,21 @@ export const secretServiceFactory = ({
secretValue,
secretMetadata
});
if (secretReminderRepeatDays) {
await reminderService.createReminder({
actor,
actorId,
actorOrgId,
actorAuthMethod,
reminder: {
secretId: secret.id,
message: secretReminderNote,
repeatDays: secretReminderRepeatDays,
recipients: secretReminderRecipients
}
});
}
return { type: SecretProtectionType.Direct as const, secret };
}
@@ -2318,6 +2330,29 @@ export const secretServiceFactory = ({
secrets: inputSecrets,
mode
});
await Promise.all(
inputSecrets
.filter((el) => el.secretReminderRepeatDays)
.map(async (secret) => {
await reminderService.createReminder({
actor,
actorId,
actorOrgId,
actorAuthMethod,
reminder: {
secretId: secrets.find(
(el) =>
(el.secretKey === secret.secretKey || el.secretKey === secret.newSecretName) &&
el.secretPath === (secret.secretPath || secretPath)
)?.id,
message: secret.secretReminderNote,
repeatDays: secret.secretReminderRepeatDays
}
});
})
);
return { type: SecretProtectionType.Direct as const, secrets };
}
@@ -312,6 +312,7 @@ export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretMetadata?: ResourceMetadataDTO;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretPath?: string;
}[];
};
@@ -412,6 +413,7 @@ export type TCreateSecretReminderDTO = {
oldSecret: TPartialSecret;
newSecret: TPartialSecret;
projectId: string;
secretReminderRecipients: string[];
deleteRecipients?: boolean;
};
@@ -419,6 +421,7 @@ export type TCreateSecretReminderDTO = {
export type TRemoveSecretReminderDTO = {
secretId: string;
repeatDays: number;
projectId: string;
deleteRecipients?: boolean;
};