diff --git a/.gitignore b/.gitignore index 7a8da287a..08d8929ac 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,9 @@ generated-types/ # others *.cert *.key + +apps/frontend/tsconfig.node.tsbuildinfo +apps/frontend/tsconfig.tsbuildinfo +apps/frontend/vite.config.d.ts +apps/frontend/vite.config.js +prod-backup.gz diff --git a/apps/backend/package.json b/apps/backend/package.json index c3c6f47bc..174897be5 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -41,6 +41,8 @@ "@repo/common": "*", "@repo/shared": "*", "@repo/sis-api": "*", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.2", "compression": "^1.8.1", "connect-redis": "^9.0.0", "cors": "^2.8.5", @@ -54,8 +56,10 @@ "helmet": "^8.1.0", "keyv": "^5.5.3", "lodash": "^4.17.21", - "mongodb": "^6.20.0", - "mongoose": "^8.19.1", + "mongodb": "^6.18.0", + "mongoose": "^8.17.0", + "node-cron": "^4.2.1", + "nodemailer": "^7.0.7", "papaparse": "^5.5.3", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", diff --git a/apps/backend/src/modules/user/controller.ts b/apps/backend/src/modules/user/controller.ts index 6475bb56b..527212ccc 100644 --- a/apps/backend/src/modules/user/controller.ts +++ b/apps/backend/src/modules/user/controller.ts @@ -79,3 +79,33 @@ export const getBookmarkedClasses = async ( return classes.map(formatClass); }; + +export const getMonitoredClasses = async ( + monitoredClasses: + | UserModule.MonitoredClassInput[] + | UserModule.MonitoredClass[] +) => { + const classes = []; + + for (const monitoredClass of monitoredClasses) { + const classData = monitoredClass.class; + + const _class = await ClassModel.findOne({ + year: classData.year, + semester: classData.semester, + sessionId: classData.sessionId ? classData.sessionId : "1", + subject: classData.subject, + courseNumber: classData.courseNumber, + number: classData.number, + }).lean(); + + if (!_class) continue; + + classes.push({ + class: formatClass(_class), + thresholds: monitoredClass.thresholds, + }); + } + + return classes; +}; diff --git a/apps/backend/src/modules/user/formatter.ts b/apps/backend/src/modules/user/formatter.ts index 97c01cef0..11fdbaf96 100644 --- a/apps/backend/src/modules/user/formatter.ts +++ b/apps/backend/src/modules/user/formatter.ts @@ -5,6 +5,7 @@ import { UserModule } from "./generated-types/module-types"; interface UserRelationships { bookmarkedCourses: UserModule.BookmarkedCourseInput[]; bookmarkedClasses: UserModule.BookmarkedClassInput[]; + monitoredClasses: UserModule.MonitoredClassInput[]; } export type IntermediateUser = Omit & @@ -21,5 +22,7 @@ export const formatUser = (user: UserType) => { bookmarkedClasses: user.bookmarkedClasses, majors: user.majors ? user.majors : [], minors: user.minors ? user.minors : [], + monitoredClasses: user.monitoredClasses, + notificationsOn: user.notificationsOn, } as IntermediateUser; }; diff --git a/apps/backend/src/modules/user/resolver.ts b/apps/backend/src/modules/user/resolver.ts index f19b82208..f27714c81 100644 --- a/apps/backend/src/modules/user/resolver.ts +++ b/apps/backend/src/modules/user/resolver.ts @@ -1,6 +1,7 @@ import { getBookmarkedClasses, getBookmarkedCourses, + getMonitoredClasses, getUser, updateUser, } from "./controller"; @@ -40,6 +41,21 @@ const resolvers: UserModule.Resolvers = { return courses as unknown as UserModule.Course[]; }, + + monitoredClasses: async (parent: UserModule.User | IntermediateUser) => { + if ( + parent.monitoredClasses[0] && + (parent.monitoredClasses[0] as UserModule.MonitoredClass).class + ) { + return parent.monitoredClasses as UserModule.MonitoredClass[]; + } + + const monitoredClasses = await getMonitoredClasses( + parent.monitoredClasses + ); + + return monitoredClasses as unknown as UserModule.MonitoredClass[]; + }, }, Mutation: { diff --git a/apps/backend/src/modules/user/typedefs/user.ts b/apps/backend/src/modules/user/typedefs/user.ts index ef579a2a3..b9f6f956e 100644 --- a/apps/backend/src/modules/user/typedefs/user.ts +++ b/apps/backend/src/modules/user/typedefs/user.ts @@ -1,6 +1,11 @@ import { gql } from "graphql-tag"; const typedef = gql` + type MonitoredClass { + class: Class! + thresholds: [Float]! + } + type User @cacheControl(scope: PRIVATE) { _id: ID! email: String! @@ -11,6 +16,8 @@ const typedef = gql` bookmarkedClasses: [Class!]! majors: [String!]! minors: [String!]! + monitoredClasses: [MonitoredClass!]! + notificationsOn: Boolean! } type Query { @@ -31,11 +38,41 @@ const typedef = gql` number: ClassNumber! } + input MonitoredClassRefInput { + year: Int! + semester: Semester! + sessionId: SessionIdentifier + subject: String! + courseNumber: CourseNumber! + number: ClassNumber! + } + + input MonitoredClassInput { + class: MonitoredClassRefInput! + thresholds: [Float!]! + } + + input MonitoredClassRefInput { + year: Int! + semester: Semester! + sessionId: SessionIdentifier + subject: String! + courseNumber: CourseNumber! + number: ClassNumber! + } + + input MonitoredClassInput { + class: MonitoredClassRefInput! + thresholds: [Float!]! + } + input UpdateUserInput { bookmarkedClasses: [BookmarkedClassInput!] bookmarkedCourses: [BookmarkedCourseInput!] majors: [String!] minors: [String!] + monitoredClasses: [MonitoredClassInput!] + notificationsOn: Boolean! } type Mutation { diff --git a/apps/backend/src/services/email.ts b/apps/backend/src/services/email.ts new file mode 100644 index 000000000..5d6376891 --- /dev/null +++ b/apps/backend/src/services/email.ts @@ -0,0 +1,3 @@ +// Connects to an SMTP server or email provider. +// Formats and sends the email to a user. +// Handles retries or errors if sending fails. diff --git a/apps/datapuller/package.json b/apps/datapuller/package.json index e6fd0b074..80463842d 100644 --- a/apps/datapuller/package.json +++ b/apps/datapuller/package.json @@ -19,6 +19,7 @@ "@repo/sis-api": "*", "dotenv": "^17.2.3", "luxon": "^3.7.2", + "@sendgrid/mail": "^8.1.4", "papaparse": "^5.5.3", "tslog": "^4.10.2" } diff --git a/apps/datapuller/src/main.ts b/apps/datapuller/src/main.ts index 8a0cb0ad4..ff7452788 100644 --- a/apps/datapuller/src/main.ts +++ b/apps/datapuller/src/main.ts @@ -3,6 +3,7 @@ import { parseArgs } from "node:util"; import classesPuller from "./pullers/classes"; import coursesPuller from "./pullers/courses"; import enrollmentHistoriesPuller from "./pullers/enrollment"; +import enrollmentChecksPuller from "./pullers/enrollment-checks"; import gradeDistributionsPuller from "./pullers/grade-distributions"; import sectionsPuller from "./pullers/sections"; import termsPuller from "./pullers/terms"; @@ -26,6 +27,7 @@ const pullerMap: { "grades-recent": gradeDistributionsPuller.recentPastTerms, "grades-last-five-years": gradeDistributionsPuller.lastFiveYearsTerms, enrollments: enrollmentHistoriesPuller.updateEnrollmentHistories, + "enrollment-checks": enrollmentChecksPuller.checkEnrollmentThresholds, "terms-all": termsPuller.allTerms, "terms-nearby": termsPuller.nearbyTerms, } as const; diff --git a/apps/datapuller/src/pullers/enrollment-checks.ts b/apps/datapuller/src/pullers/enrollment-checks.ts new file mode 100644 index 000000000..c3f99a626 --- /dev/null +++ b/apps/datapuller/src/pullers/enrollment-checks.ts @@ -0,0 +1,375 @@ +import sgMail from "@sendgrid/mail"; +import { Logger } from "tslog"; + +import { NewEnrollmentHistoryModel, UserModel } from "@repo/common"; + +import { Config } from "../shared/config"; +import { getActiveTerms } from "../shared/term-selectors"; + +interface EnrollmentThreshold { + percentage: number; + name: string; +} + +const ENROLLMENT_THRESHOLDS: EnrollmentThreshold[] = [ + { percentage: 50, name: "half_full" }, + { percentage: 75, name: "three_quarters_full" }, + { percentage: 90, name: "nearly_full" }, + { percentage: 100, name: "completely_full" }, +]; + +interface EnrollmentAlert { + termId: string; + sessionId: string; + sectionId: string; + subject: string; + courseNumber: string; + sectionNumber: string; + year: number; + semester: string; + threshold: string; + percentage: number; + enrolledCount: number; + maxEnroll: number; + waitlistedCount: number; + maxWaitlist: number; + timestamp: string; + previousPercentage?: number; +} + +const generateEnrollmentAlertEmail = ( + userEmail: string, + userName: string, + alerts: EnrollmentAlert[] +): { to: string; from: string; subject: string; html: string } => { + // TODO: Replace with actual email template/design + const alertsList = alerts + .map( + (alert) => ` +
+

+ ${alert.subject} ${alert.courseNumber} - Section ${alert.sectionNumber} +

+

+ Enrollment Status: ${alert.threshold.replace(/_/g, " ").toUpperCase()} +

+

+ Current Enrollment: ${alert.enrolledCount}/${alert.maxEnroll} (${alert.percentage}%) +

+ ${ + alert.waitlistedCount > 0 + ? `

+ Waitlist: ${alert.waitlistedCount}/${alert.maxWaitlist} +

` + : "" + } +

+ Term: ${alert.semester} ${alert.year} +

+
+ ` + ) + .join(""); + + const html = ` + + + + + + + +
+

BerkeleTime Enrollment Alert

+

Your monitored classes have reached enrollment thresholds

+
+ +
+

Hello ${userName},

+

The following classes you're monitoring have crossed their enrollment thresholds:

+
+ + ${alertsList} + +
+

You're receiving this email because you've set up enrollment notifications on BerkeleTime.

+

To manage your notification settings, please visit your profile page.

+
+ + + `; + + return { + to: userEmail, + from: process.env.SENDGRID_FROM_EMAIL || "octo.berkeleytime@asuc.org", + subject: `BerkeleTime Alert: ${alerts.length} class${alerts.length > 1 ? "es" : ""} reached enrollment threshold`, + html, + }; +}; + +const checkEnrollmentThresholds = async (config: Config) => { + const log = + config.log || + new Logger({ + type: "pretty", + prettyLogTimeZone: "local", + }); + log.trace(`Starting enrollment threshold checks...`); + + const allTerms = await getActiveTerms(); + const terms = allTerms.filter((term) => { + if (term.academicCareerCode !== "UGRD") { + return false; + } + + if (!term.sessions) return true; + return term.sessions.some((session) => { + if (!session.enrollBeginDate || !session.enrollEndDate) return false; + + const now = Date.now(); + const enrollBeginDate = new Date(session.enrollBeginDate).getTime(); + const enrollEndDate = new Date(session.enrollEndDate).getTime(); + + return now >= enrollBeginDate && now <= enrollEndDate; + }); + }); + + if (terms.length === 0) { + log.warn("No active terms found for enrollment checking"); + return; + } + + const termIds = terms.map((term) => term.id); + log.info( + `Checking enrollment thresholds for ${terms.length} active terms: ${terms.map((term) => term.name).join(", ")}` + ); + + const enrollmentHistories = await NewEnrollmentHistoryModel.find({ + termId: { $in: termIds }, + }).lean(); + + log.info(`Found ${enrollmentHistories.length} enrollment histories to check`); + + const alerts: EnrollmentAlert[] = []; + let totalChecks = 0; + + for (const history of enrollmentHistories) { + if (!history.history || history.history.length < 2) { + continue; + } + + const latestData = history.history[history.history.length - 1]; + const previousData = history.history[history.history.length - 2]; + + if (!latestData.enrolledCount || !latestData.maxEnroll) { + continue; + } + + totalChecks++; + + const currentPercentage = Math.round( + (latestData.enrolledCount / latestData.maxEnroll) * 100 + ); + + const previousPercentage = + previousData.enrolledCount && previousData.maxEnroll + ? Math.round( + (previousData.enrolledCount / previousData.maxEnroll) * 100 + ) + : undefined; + + // Check each threshold + for (const threshold of ENROLLMENT_THRESHOLDS) { + const crossedThreshold = + currentPercentage >= threshold.percentage && + (!previousPercentage || previousPercentage < threshold.percentage); + + if (crossedThreshold) { + alerts.push({ + termId: history.termId, + sessionId: history.sessionId, + sectionId: history.sectionId, + subject: history.subject, + courseNumber: history.courseNumber, + sectionNumber: history.sectionNumber, + year: history.year, + semester: history.semester, + threshold: threshold.name, + percentage: currentPercentage, + enrolledCount: latestData.enrolledCount, + maxEnroll: latestData.maxEnroll, + waitlistedCount: latestData.waitlistedCount || 0, + maxWaitlist: latestData.maxWaitlist || 0, + timestamp: latestData.endTime.toString(), + previousPercentage, + }); + } + } + } + + log.info(`Completed ${totalChecks} enrollment checks`); + + if (alerts.length > 0) { + log.info(`Found ${alerts.length} enrollment threshold alerts:`); + + const alertsByThreshold = alerts.reduce( + (acc, alert) => { + if (!acc[alert.threshold]) { + acc[alert.threshold] = []; + } + acc[alert.threshold].push(alert); + return acc; + }, + {} as Record + ); + + for (const [threshold, thresholdAlerts] of Object.entries( + alertsByThreshold + )) { + log.info(` ${threshold}: ${thresholdAlerts.length} sections`); + + thresholdAlerts.slice(0, 3).forEach((alert) => { + log.info( + ` ${alert.subject} ${alert.courseNumber} ${alert.sectionNumber} - ${alert.percentage}% (${alert.enrolledCount}/${alert.maxEnroll})` + ); + }); + + if (thresholdAlerts.length > 3) { + log.info(` ... and ${thresholdAlerts.length - 3} more`); + } + } + + const usersToNotify = new Map>(); + + for (const alert of alerts) { + const thresholdPercentage = ENROLLMENT_THRESHOLDS.find( + (t) => t.name === alert.threshold + )?.percentage; + + if (!thresholdPercentage) { + continue; + } + + const matchingUsers = await UserModel.find({ + notificationsOn: true, + monitoredClasses: { + $elemMatch: { + "class.year": alert.year, + "class.semester": alert.semester, + "class.subject": alert.subject, + "class.courseNumber": alert.courseNumber, + "class.number": alert.sectionNumber, + }, + }, + }).lean(); + + for (const user of matchingUsers) { + const monitoredClass = user.monitoredClasses?.find( + (mc) => + mc.class && + mc.class.year === alert.year && + mc.class.semester === alert.semester && + mc.class.subject === alert.subject && + mc.class.courseNumber === alert.courseNumber && + mc.class.number === alert.sectionNumber + ); + + if ( + monitoredClass && + monitoredClass.thresholds.includes(thresholdPercentage) + ) { + const userId = user._id.toString(); + if (!usersToNotify.has(userId)) { + usersToNotify.set(userId, new Set()); + } + usersToNotify.get(userId)!.add(alert); + } + } + } + + log.info(`Found ${usersToNotify.size} users to notify`); + + // Initialize SendGrid + const sendgridApiKey = process.env.SENDGRID_API_KEY; + if (!sendgridApiKey) { + log.warn("SENDGRID_API_KEY not set - skipping email notifications"); + } else { + sgMail.setApiKey(sendgridApiKey); + } + + let emailsSent = 0; + let emailsFailed = 0; + let emailsThrottled = 0; + + for (const [userId, userAlerts] of usersToNotify.entries()) { + log.info(` User ${userId}: ${userAlerts.size} alert(s)`); + + // Get user email for notifications + const user = await UserModel.findById(userId).lean(); + if (!user) { + log.warn(` User ${userId} not found, skipping`); + continue; + } + + // Only send email notifications + if (!user.notificationsOn) { + log.info(`User has notifications off, skipping email`); + continue; + } + + // Check if user was notified less than 2 hours ago + if (user.lastNotified) { + const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); + if (new Date(user.lastNotified) > twoHoursAgo) { + emailsThrottled++; + log.info( + ` User was notified less than 2 hours ago, skipping email` + ); + continue; + } + } + + for (const alert of userAlerts) { + log.info( + ` - ${alert.subject} ${alert.courseNumber} ${alert.sectionNumber}: ${alert.threshold} (${alert.percentage}%)` + ); + } + + // Send email if SendGrid is configured + if (sendgridApiKey && user.email) { + try { + const emailData = generateEnrollmentAlertEmail( + user.email, + user.name, + Array.from(userAlerts) + ); + + await sgMail.send(emailData); + + // Update lastNotified after successfully sending email + await UserModel.findByIdAndUpdate(userId, { + lastNotified: new Date(), + }); + + emailsSent++; + log.info(` ✓ Email sent to ${user.email}`); + } catch (error) { + emailsFailed++; + log.error(` ✗ Failed to send email to ${user.email}:`, error); + } + } + } + + if (sendgridApiKey) { + log.info( + `Email notifications: ${emailsSent} sent, ${emailsFailed} failed, ${emailsThrottled} throttled` + ); + } else { + log.info("Email sending skipped - SENDGRID_API_KEY not configured"); + } + } else { + log.info("No enrollment threshold alerts detected"); + } +}; + +export default { checkEnrollmentThresholds }; diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 0f2c3c38c..9377689f1 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -20,6 +20,7 @@ const Landing = lazy(() => import("@/app/Landing")); const Profile = { Root: lazy(() => import("@/app/Profile")), Account: lazy(() => import("@/app/Profile/Account")), + Notifications: lazy(() => import("@/app/Profile/Notifications")), Support: lazy(() => import("@/app/Profile/Support")), Ratings: lazy(() => import("@/app/Profile/Ratings")), Settings: lazy(() => import("@/app/Profile/Settings")), @@ -187,6 +188,14 @@ const router = createBrowserRouter([ ), index: true, }, + { + element: ( + + + + ), + path: "notifications", + }, { element: ( diff --git a/apps/frontend/src/app/Profile/Notifications/Notifications.module.scss b/apps/frontend/src/app/Profile/Notifications/Notifications.module.scss new file mode 100644 index 000000000..165dfec21 --- /dev/null +++ b/apps/frontend/src/app/Profile/Notifications/Notifications.module.scss @@ -0,0 +1,148 @@ +/** + * Notifications page styles + * Cleaned up - removed unused classes + */ + +.container { + max-width: 100%; +} + +.header { + margin-bottom: 32px; + + h1 { + font-size: 32px; + font-weight: 700; + margin: 0 0 24px 0; + color: var(--heading-color); + } +} + +.subtitle { + font-size: 16px; + color: var(--paragraph-color); + line-height: 1.6; + margin: 0 0 40px 0; + max-width: 80%; +} + +.section { + margin-bottom: 48px; + + h2 { + font-size: 24px; + font-weight: 600; + margin: 0 0 24px 0; + color: var(--heading-color); + max-width: 80%; + } +} + +.sectionDescription { + font-size: 16px; + color: var(--paragraph-color); + line-height: 1.6; + margin: 0 0 20px 0; +} + +.classGrid { + display: grid; + grid-template-columns: repeat(auto-fill, 326px); + gap: 20px; +} + +.classCardWrapper { + position: relative; +} + +.notificationButtonOverlay { + position: absolute; + top: 16px; + right: 16px; + z-index: 10; +} + +.toggleOptions { + display: flex; + flex-direction: column; + gap: 16px; +} + +.toggleOption { + display: flex; + align-items: center; + gap: 12px; + font-size: 16px; + color: var(--paragraph-color); + cursor: pointer; + user-select: none; + + input[type="checkbox"] { + width: 44px; + height: 24px; + cursor: pointer; + appearance: none; + background-color: var(--border-color); + border-radius: 12px; + position: relative; + transition: background-color 150ms ease-in-out; + flex-shrink: 0; + + &::before { + content: ""; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: white; + top: 2px; + left: 2px; + transition: transform 150ms ease-in-out; + } + + &:checked { + background-color: var(--blue-500); + + &::before { + transform: translateX(20px); + } + } + } + + span { + line-height: 1.4; + } + + h2 { + margin: 0; + } +} + +.noMarginHeading { + margin: 0; +} + +.toast { + position: fixed; + right: 32px; + bottom: -120px; + width: min(720px, calc(100% - 64px)); + background-color: var(--blue-500); + color: white; + padding: 16px 24px; + border-radius: 4px; + font-size: 15px; + line-height: 1.5; + opacity: 0; + pointer-events: none; + transition: + bottom 250ms ease, + opacity 200ms ease; + z-index: 11000; +} + +.toastVisible { + bottom: 32px; + opacity: 1; + pointer-events: auto; +} diff --git a/apps/frontend/src/app/Profile/Notifications/index.tsx b/apps/frontend/src/app/Profile/Notifications/index.tsx new file mode 100644 index 000000000..e5bdb3572 --- /dev/null +++ b/apps/frontend/src/app/Profile/Notifications/index.tsx @@ -0,0 +1,402 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import classNames from "classnames"; + +import { Text } from "@repo/theme"; + +import ClassCard from "@/components/ClassCard"; +import NotificationButton from "@/components/NotificationButton"; +import { useReadUser } from "@/hooks/api"; +import { IClass } from "@/lib/api/classes"; +import { IMonitoredClass } from "@/lib/api/users"; + +import styles from "./Notifications.module.scss"; + +const TOAST_DURATION_MS = 3000; +const TOAST_TRANSITION_MS = 250; + +const getClassKey = (monitoredClass: IMonitoredClass) => { + const cls = monitoredClass.class; + return `${cls.subject}-${cls.courseNumber}-${cls.number}-${cls.year}-${cls.semester}`; +}; + +// Test data for development +const TEST_DATA: IMonitoredClass[] = [ + { + class: { + title: "Foundations of the U.S Air Force", + subject: "AEROSPC", + number: "1A", + courseNumber: "1A", + year: 2024, + semester: "Spring", + sessionId: null, + unitsMin: 1, + unitsMax: 1, + course: { + title: "Foundations of U.S Air Force", + subject: "AEROSPC", + number: "1A", + gradeDistribution: { + average: "A+", + }, + }, + primarySection: { + enrollment: { + latest: { + enrolledCount: 0, + maxEnroll: 60, + waitlistedCount: 0, + maxWaitlist: 0, + }, + }, + }, + gradeDistribution: { + average: "A+", + }, + } as unknown as IClass, + thresholds: [50], + }, + { + class: { + title: "Intro to Computer Science", + subject: "COMPSCI", + number: "61A", + year: 2024, + semester: "Fall", + sessionId: null, + unitsMin: 4, + unitsMax: 4, + course: { + title: "Intro to Computer Science", + subject: "COMPSCI", + number: "61A", + gradeDistribution: { + average: "B+", + }, + }, + primarySection: { + enrollment: { + latest: { + enrolledCount: 450, + maxEnroll: 500, + waitlistedCount: 25, + maxWaitlist: 50, + }, + }, + }, + gradeDistribution: { + average: "B+", + }, + } as unknown as IClass, + thresholds: [50, 75, 90], + }, + { + class: { + title: "Calculus", + subject: "MATH", + number: "1A", + year: 2024, + semester: "Spring", + sessionId: null, + unitsMin: 4, + unitsMax: 4, + course: { + title: "Calculus", + subject: "MATH", + gradeDistribution: { + average: "B", + }, + }, + primarySection: { + enrollment: { + latest: { + enrolledCount: 200, + maxEnroll: 250, + waitlistedCount: 10, + maxWaitlist: 30, + }, + }, + }, + gradeDistribution: { + average: "B", + }, + } as unknown as IClass, + thresholds: [75, 100], + }, +]; + +export default function Notifications() { + const { data: user } = useReadUser(); + // TODO: Uncomment when backend mutations are implemented + // const [updateUser] = useUpdateUser(); + + const [monitoredClasses, setMonitoredClasses] = useState( + user?.monitoredClasses && user.monitoredClasses.length > 0 + ? user.monitoredClasses + : TEST_DATA + ); + + const [addDropDeadline, setAddDropDeadline] = useState(false); + const [lateChangeSchedule, setLateChangeSchedule] = useState(false); + const [receiveEmails, setReceiveEmails] = useState(true); + const [toastMessage, setToastMessage] = useState(""); + const [isToastVisible, setToastVisible] = useState(false); + const toastTimeoutRef = useRef | null>(null); + const toastHideTimeoutRef = useRef | null>( + null + ); + const prevThresholdMapRef = useRef>(new Map()); + const currentToastKeyRef = useRef(null); + + const clearToastTimeout = useCallback(() => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + toastTimeoutRef.current = null; + } + }, []); + + const clearToastHideTimeout = useCallback(() => { + if (toastHideTimeoutRef.current) { + clearTimeout(toastHideTimeoutRef.current); + toastHideTimeoutRef.current = null; + } + }, []); + + const hideToast = useCallback( + (onHidden?: () => void) => { + clearToastTimeout(); + if (!isToastVisible) { + onHidden?.(); + return; + } + + setToastVisible(false); + clearToastHideTimeout(); + toastHideTimeoutRef.current = setTimeout(() => { + toastHideTimeoutRef.current = null; + onHidden?.(); + }, TOAST_TRANSITION_MS); + }, + [clearToastTimeout, clearToastHideTimeout, isToastVisible] + ); + + const showToast = useCallback( + (toastKey: string, message: string) => { + const display = () => { + currentToastKeyRef.current = toastKey; + setToastMessage(message); + setToastVisible(true); + clearToastTimeout(); + toastTimeoutRef.current = setTimeout(() => { + hideToast(() => { + if (currentToastKeyRef.current === toastKey) { + currentToastKeyRef.current = null; + setToastMessage(""); + } + }); + }, TOAST_DURATION_MS); + }; + + if (isToastVisible) { + hideToast(display); + } else { + display(); + } + }, + [clearToastTimeout, hideToast, isToastVisible] + ); + + const handleThresholdChange = ( + classIndex: number, + threshold: number, + checked: boolean + ) => { + const updated = [...monitoredClasses]; + const currentThresholds = updated[classIndex].thresholds; + + if (checked) { + updated[classIndex].thresholds = [...currentThresholds, threshold].sort( + (a, b) => a - b + ); + } else { + updated[classIndex].thresholds = currentThresholds.filter( + (t) => t !== threshold + ); + } + + setMonitoredClasses(updated); + // TODO: Call backend mutation to update user.monitoredClasses + // await updateUser({ monitoredClasses: updated }); + }; + + const handleRemoveClass = async (classIndex: number) => { + const updated = monitoredClasses.filter((_, index) => index !== classIndex); + setMonitoredClasses(updated); + // TODO: Call backend mutation to update user.monitoredClasses + // await updateUser({ monitoredClasses: updated }); + }; + + useEffect(() => { + if (monitoredClasses.length === 0) { + prevThresholdMapRef.current.clear(); + currentToastKeyRef.current = null; + hideToast(() => setToastMessage("")); + return; + } + + const seenKeys = new Set(); + + for (let index = 0; index < monitoredClasses.length; index += 1) { + const monitoredClass = monitoredClasses[index]; + const key = getClassKey(monitoredClass); + seenKeys.add(key); + + const sortedThresholds = [...monitoredClass.thresholds].sort( + (a, b) => a - b + ); + const prevThresholds = prevThresholdMapRef.current.get(key); + + const hasChanged = + !prevThresholds || + sortedThresholds.length !== prevThresholds.length || + sortedThresholds.some( + (value, thresholdIndex) => value !== prevThresholds[thresholdIndex] + ); + + if (hasChanged) { + prevThresholdMapRef.current.set(key, sortedThresholds); + + if (sortedThresholds.length > 0) { + const thresholdsText = sortedThresholds + .map((threshold) => `${threshold}%`) + .join(", "); + const courseLabel = `${monitoredClass.class.subject} ${monitoredClass.class.courseNumber}`; + showToast( + key, + `You'll be notified about ${courseLabel} enrollment milestones (${thresholdsText}).` + ); + } else if (currentToastKeyRef.current === key) { + hideToast(() => { + currentToastKeyRef.current = null; + setToastMessage(""); + }); + } + + return; + } + + prevThresholdMapRef.current.set(key, sortedThresholds); + } + + prevThresholdMapRef.current.forEach((_value, key) => { + if (!seenKeys.has(key)) { + prevThresholdMapRef.current.delete(key); + if (currentToastKeyRef.current === key) { + hideToast(() => { + currentToastKeyRef.current = null; + setToastMessage(""); + }); + } + } + }); + }, [monitoredClasses, showToast, hideToast]); + + useEffect(() => { + return () => { + clearToastTimeout(); + clearToastHideTimeout(); + }; + }, [clearToastTimeout, clearToastHideTimeout]); + + return ( +
+
+

Course Enrollment Notifications

+

+ Manage the classes you are tracking by setting specific alerts for + enrollment thresholds. Notifications will be delivered to your + registered @berkeley.edu email address. +

+
+ +
+
+ +
+
+ +
+

Classes You're Tracking

+ + {monitoredClasses.length === 0 ? null : ( +
+ {monitoredClasses.map((monitoredClass, index) => ( +
+ +
+ + handleThresholdChange(index, threshold, checked) + } + onRemove={() => handleRemoveClass(index)} + uniqueId={`${monitoredClass.class.subject}-${monitoredClass.class.number}-${index}`} + variant="iconButton" + /> +
+
+ ))} +
+ )} +
+ +
+

Add/Drop Deadline Notifications

+ + Get notified about key academic deadlines, including add/drop and late + change of class schedule for the semester. + + +
+ + +
+
+
+ {toastMessage} +
+
+ ); +} diff --git a/apps/frontend/src/app/Profile/index.tsx b/apps/frontend/src/app/Profile/index.tsx index 89d726f97..c2a15a0d2 100644 --- a/apps/frontend/src/app/Profile/index.tsx +++ b/apps/frontend/src/app/Profile/index.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; import { + Bell, ChatBubbleQuestion, LogOut, ProfileCircle, @@ -53,6 +54,18 @@ export default function Root() { )} + + {({ isActive }) => ( +
+ + Notifications +
+ )} +
{({ isActive }) => (
([]); + const [isNotificationToastVisible, setNotificationToastVisible] = + useState(false); + const [notificationToastMessage, setNotificationToastMessage] = useState(""); + const toastTimeoutRef = useRef | null>(null); + const prevThresholdsRef = useRef([]); + const hasInitializedToastRef = useRef(false); + + const closeNotificationToast = useCallback(() => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + toastTimeoutRef.current = null; + } + setNotificationToastVisible(false); + }, []); + + const triggerNotificationToast = useCallback( + (message: string) => { + closeNotificationToast(); + setNotificationToastMessage(message); + + const showToast = () => { + setNotificationToastVisible(true); + }; + + if ( + typeof window !== "undefined" && + typeof window.requestAnimationFrame === "function" + ) { + window.requestAnimationFrame(showToast); + } else { + showToast(); + } + + toastTimeoutRef.current = setTimeout(() => { + setNotificationToastVisible(false); + toastTimeoutRef.current = null; + }, 3000); + }, + [closeNotificationToast] + ); + + // Sync notification thresholds from user.monitoredClasses + useEffect(() => { + if (!user?.monitoredClasses || !_class) return; + + const monitoredClass = user.monitoredClasses.find( + (mc) => + mc.class.subject === _class.subject && + mc.class.courseNumber === _class.courseNumber && + mc.class.number === _class.number && + mc.class.year === _class.year && + mc.class.semester === _class.semester + ); + + const thresholds = [...(monitoredClass?.thresholds || [])].sort( + (a, b) => a - b + ); + setNotificationThresholds(thresholds); + prevThresholdsRef.current = thresholds; + hasInitializedToastRef.current = true; + closeNotificationToast(); + setNotificationToastMessage(""); + }, [user, _class, closeNotificationToast]); + + useEffect(() => { + if (_class && !hasInitializedToastRef.current) { + hasInitializedToastRef.current = true; + prevThresholdsRef.current = notificationThresholds; + } + }, [_class, notificationThresholds]); + + const handleNotificationChange = useCallback( + async (threshold: number, checked: boolean) => { + if (!user || !_class) return; + + setNotificationThresholds((prev) => { + if (checked) { + return Array.from(new Set([...prev, threshold])).sort( + (a, b) => a - b + ); + } + return prev.filter((t) => t !== threshold).sort((a, b) => a - b); + }); + + // TODO: Call backend mutation to update user.monitoredClasses when backend is ready + // await updateUser({ monitoredClasses: updatedMonitoredClasses }); + }, + [user, _class] + ); + + const handleRemoveNotification = useCallback(async () => { + if (!user || !_class) return; + + const updatedMonitoredClasses = (user.monitoredClasses || []).filter( + (mc) => + !( + mc.class.subject === _class.subject && + mc.class.courseNumber === _class.courseNumber && + mc.class.number === _class.number && + mc.class.year === _class.year && + mc.class.semester === _class.semester + ) + ); + + setNotificationThresholds([]); + + // TODO: Call backend mutation to update user.monitoredClasses when backend is ready + // await updateUser({ monitoredClasses: updatedMonitoredClasses }); + }, [user, _class]); + + useEffect(() => { + if (!hasInitializedToastRef.current || !_class) return; + + const sortedThresholds = [...notificationThresholds].sort((a, b) => a - b); + const prevSorted = prevThresholdsRef.current; + + const hasChanged = + sortedThresholds.length !== prevSorted.length || + sortedThresholds.some((value, index) => value !== prevSorted[index]); + + if (hasChanged && sortedThresholds.length > 0) { + const thresholdsText = sortedThresholds + .map((threshold) => `${threshold}%`) + .join(", "); + triggerNotificationToast( + `You'll be notified about ${_class.subject} ${_class.courseNumber} enrollment milestones (${thresholdsText}).` + ); + } else if (sortedThresholds.length === 0) { + closeNotificationToast(); + setNotificationToastMessage(""); + } + + prevThresholdsRef.current = sortedThresholds; + }, [ + notificationThresholds, + _class, + triggerNotificationToast, + closeNotificationToast, + ]); + + useEffect(() => { + return () => { + closeNotificationToast(); + }; + }, [closeNotificationToast]); + useEffect(() => { if (!_class) return; @@ -352,6 +511,15 @@ export default function Class({ + {_class && ( + + )} @@ -547,6 +715,17 @@ export default function Class({ )} +
+ {notificationToastMessage} +
); diff --git a/apps/frontend/src/components/ClassCard/index.tsx b/apps/frontend/src/components/ClassCard/index.tsx index 1ef08b493..2022e1156 100644 --- a/apps/frontend/src/components/ClassCard/index.tsx +++ b/apps/frontend/src/components/ClassCard/index.tsx @@ -30,6 +30,7 @@ interface ClassProps { bookmarkToggle?: () => void; active?: boolean; wrapDescription?: boolean; + showGrades?: boolean; } export default function ClassCard({ @@ -45,10 +46,12 @@ export default function ClassCard({ bookmarkToggle, active = false, wrapDescription = false, + showGrades = true, ...props }: ClassProps & Omit, keyof ClassProps>) { - const gradeDistribution = - _class?.course?.gradeDistribution ?? _class?.gradeDistribution; + const gradeDistribution = showGrades + ? (_class?.course?.gradeDistribution ?? _class?.gradeDistribution) + : undefined; return ( (thresholds); + + const isActive = thresholds.length > 0; + + // Sync temp thresholds with prop changes + useEffect(() => { + setTempThresholds(thresholds); + }, [thresholds]); + + const handleTempThresholdChange = useCallback( + (threshold: number, checked: boolean) => { + setTempThresholds((prev) => { + if (checked) { + return [...prev, threshold].sort((a, b) => a - b); + } else { + return prev.filter((t) => t !== threshold); + } + }); + }, + [] + ); + + const applyThresholdChanges = useCallback(() => { + // Add new thresholds + tempThresholds.forEach((threshold) => { + if (!thresholds.includes(threshold)) { + onThresholdsChange(threshold, true); + } + }); + + // Remove old thresholds + thresholds.forEach((threshold) => { + if (!tempThresholds.includes(threshold)) { + onThresholdsChange(threshold, false); + } + }); + }, [tempThresholds, thresholds, onThresholdsChange]); + + const handleOpenChange = useCallback( + (open: boolean) => { + setShowPopup(open); + + // When closing the popover, handle threshold changes + if (!open) { + // If user removed all thresholds and there were some before, show confirmation + if (tempThresholds.length === 0 && thresholds.length > 0 && onRemove) { + setShowRemoveConfirmation(true); + } else { + applyThresholdChanges(); + } + } + }, + [tempThresholds, thresholds, onRemove, applyThresholdChanges] + ); + + const tooltipContent = useMemo(() => { + if (disabled) return "Notification disabled"; + return isActive ? "Manage notifications" : "Notify me"; + }, [disabled, isActive]); + + const buttonClassName = classNames( + styles.button, + { + [styles.active]: isActive, + [styles.iconButton]: variant === "iconButton", + [styles.card]: variant === "card", + }, + className + ); + + const handleCloseRemoveConfirmation = useCallback(() => { + setTempThresholds(thresholds); + setShowRemoveConfirmation(false); + }, [thresholds]); + + const handleConfirmRemove = useCallback(async () => { + if (!onRemove) return; + await onRemove(); + setShowRemoveConfirmation(false); + }, [onRemove]); + + return ( + <> + + + + {variant === "iconButton" ? ( + + + {isActive ? ( + + + ) : ( +
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setShowPopup(!showPopup); + } + }} + > + {isActive ? ( +
+ )} +
+
+ + + { + setTempThresholds(thresholds); // Reset on escape + setShowPopup(false); + }} + > + + {NOTIFICATION_THRESHOLDS.map((threshold) => ( + + ))} + + + + +
+ + {onRemove && ( + { + if (!open) { + handleCloseRemoveConfirmation(); + } + }} + > + + + + + +
+ Stop tracking this class? +
+
+ You will no longer receive any notifications for this course. +
+
+ + + + +
+
+
+ )} + + ); +} diff --git a/apps/frontend/src/lib/api/users.ts b/apps/frontend/src/lib/api/users.ts index 7e783c629..b5068d8c6 100644 --- a/apps/frontend/src/lib/api/users.ts +++ b/apps/frontend/src/lib/api/users.ts @@ -4,6 +4,11 @@ import { IClass } from "./classes"; import { ICourse } from "./courses"; import { Semester } from "./terms"; +export interface IMonitoredClass { + class: IClass; + thresholds: number[]; +} + export interface IUser { _id: string; name: string; @@ -11,6 +16,8 @@ export interface IUser { student: boolean; bookmarkedCourses: ICourse[]; bookmarkedClasses: IClass[]; + monitoredClasses?: IMonitoredClass[]; + notificationsOn?: boolean; } export interface ReadUserResponse { @@ -74,9 +81,16 @@ export interface IBookmarkedClassInput { sessionId: string | null; } +export interface IMonitoredClassInput { + class: IBookmarkedClassInput; + thresholds: number[]; +} + export interface IUserInput { bookmarkedCourses?: IBookmarkedCourseInput[]; bookmarkedClasses?: IBookmarkedClassInput[]; + monitoredClasses?: IMonitoredClassInput[]; + notificationsOn?: boolean; } export interface UpdateUserResponse { @@ -86,6 +100,7 @@ export interface UpdateUserResponse { export const UPDATE_USER = gql` mutation UpdateUser($user: UpdateUserInput!) { updateUser(user: $user) { + _id name email student @@ -102,7 +117,37 @@ export const UPDATE_USER = gql` year semester sessionId + unitsMin + unitsMax + course { + title + } + primarySection { + enrollment { + latest { + enrolledCount + maxEnroll + waitlistedCount + maxWaitlist + } + } + } + gradeDistribution { + average + } } + # monitoredClasses { # TODO: Uncomment when backend implements this field + # thresholds + # class { + # title + # subject + # number + # courseNumber + # year + # semester + # sessionId + # } + # } } } `; diff --git a/apps/frontend/src/types/notifications.ts b/apps/frontend/src/types/notifications.ts new file mode 100644 index 000000000..e3928b0e8 --- /dev/null +++ b/apps/frontend/src/types/notifications.ts @@ -0,0 +1,54 @@ +/** + * Notification-related types for course enrollment tracking + */ +import { IClass } from "@/lib/api/classes"; + +/** + * Supported enrollment threshold percentages + */ +export type NotificationThreshold = 50 | 75 | 90; + +/** + * All possible threshold values as a const array for validation + */ +export const NOTIFICATION_THRESHOLDS: readonly NotificationThreshold[] = [ + 50, 75, 90, +] as const; + +/** + * Monitored class with notification thresholds + * Re-exported for consistency with backend types + */ +export interface IMonitoredClass { + class: IClass; + thresholds: number[]; +} + +/** + * Class identifier for notification operations + */ +export interface ClassIdentifier { + subject: string; + courseNumber: string; + number: string; + year: number; + semester: string; +} + +/** + * Props for notification button variants + */ +export type NotificationButtonVariant = "iconButton" | "card"; + +/** + * Threshold change callback type + */ +export type OnThresholdChange = ( + threshold: number, + checked: boolean +) => void | Promise; + +/** + * Remove class callback type + */ +export type OnRemoveClass = () => void | Promise; diff --git a/infra/app/values.yaml b/infra/app/values.yaml index 5643701d6..a5e0180e6 100644 --- a/infra/app/values.yaml +++ b/infra/app/values.yaml @@ -75,3 +75,6 @@ datapuller: enrollments: schedule: "0/15 * * * *" args: ["--puller=enrollments"] + enrollment-checks: + schedule: "5,20,35,50 * * * *" + args: ["--puller=enrollment-checks"] diff --git a/package-lock.json b/package-lock.json index a244a7999..072d9e9d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,8 @@ "@repo/common": "*", "@repo/shared": "*", "@repo/sis-api": "*", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.2", "compression": "^1.8.1", "connect-redis": "^9.0.0", "cors": "^2.8.5", @@ -80,8 +82,10 @@ "helmet": "^8.1.0", "keyv": "^5.5.3", "lodash": "^4.17.21", - "mongodb": "^6.20.0", - "mongoose": "^8.19.1", + "mongodb": "^6.18.0", + "mongoose": "^8.17.0", + "node-cron": "^4.2.1", + "nodemailer": "^7.0.7", "papaparse": "^5.5.3", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -114,6 +118,7 @@ "@aws-sdk/client-s3": "^3.901.0", "@repo/common": "*", "@repo/sis-api": "*", + "@sendgrid/mail": "^8.1.4", "dotenv": "^17.2.3", "luxon": "^3.7.2", "papaparse": "^5.5.3", @@ -727,59 +732,602 @@ "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.901.0", - "@aws-sdk/credential-provider-node": "3.901.0", - "@aws-sdk/middleware-bucket-endpoint": "3.901.0", - "@aws-sdk/middleware-expect-continue": "3.901.0", - "@aws-sdk/middleware-flexible-checksums": "3.901.0", - "@aws-sdk/middleware-host-header": "3.901.0", - "@aws-sdk/middleware-location-constraint": "3.901.0", - "@aws-sdk/middleware-logger": "3.901.0", - "@aws-sdk/middleware-recursion-detection": "3.901.0", - "@aws-sdk/middleware-sdk-s3": "3.901.0", - "@aws-sdk/middleware-ssec": "3.901.0", - "@aws-sdk/middleware-user-agent": "3.901.0", - "@aws-sdk/region-config-resolver": "3.901.0", - "@aws-sdk/signature-v4-multi-region": "3.901.0", - "@aws-sdk/types": "3.901.0", - "@aws-sdk/util-endpoints": "3.901.0", - "@aws-sdk/util-user-agent-browser": "3.901.0", - "@aws-sdk/util-user-agent-node": "3.901.0", - "@aws-sdk/xml-builder": "3.901.0", - "@smithy/config-resolver": "^4.3.0", - "@smithy/core": "^3.14.0", - "@smithy/eventstream-serde-browser": "^4.2.0", - "@smithy/eventstream-serde-config-resolver": "^4.3.0", - "@smithy/eventstream-serde-node": "^4.2.0", - "@smithy/fetch-http-handler": "^5.3.0", - "@smithy/hash-blob-browser": "^4.2.0", - "@smithy/hash-node": "^4.2.0", - "@smithy/hash-stream-node": "^4.2.0", - "@smithy/invalid-dependency": "^4.2.0", - "@smithy/md5-js": "^4.2.0", - "@smithy/middleware-content-length": "^4.2.0", - "@smithy/middleware-endpoint": "^4.3.0", - "@smithy/middleware-retry": "^4.4.0", - "@smithy/middleware-serde": "^4.2.0", - "@smithy/middleware-stack": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/node-http-handler": "^4.3.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/smithy-client": "^4.7.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", - "@smithy/util-base64": "^4.2.0", + "@aws-sdk/core": "3.857.0", + "@aws-sdk/credential-provider-node": "3.857.0", + "@aws-sdk/middleware-bucket-endpoint": "3.840.0", + "@aws-sdk/middleware-expect-continue": "3.840.0", + "@aws-sdk/middleware-flexible-checksums": "3.857.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-location-constraint": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-sdk-s3": "3.857.0", + "@aws-sdk/middleware-ssec": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.857.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/signature-v4-multi-region": "3.857.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.857.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.2", + "@smithy/eventstream-serde-browser": "^4.0.4", + "@smithy/eventstream-serde-config-resolver": "^4.1.2", + "@smithy/eventstream-serde-node": "^4.0.4", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-blob-browser": "^4.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/hash-stream-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/md5-js": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.17", + "@smithy/middleware-retry": "^4.1.18", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.25", + "@smithy/util-defaults-mode-node": "^4.0.25", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-stream": "^4.2.3", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.6", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.913.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.913.0.tgz", + "integrity": "sha512-xurdihi/HH3em6XgvI+AlolCjqu74CxyOAEEWAJBk1Xp+yqNUaYR5d1m26iI7UuTy8gAZ+48UNvQBVhUXe5ZwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.911.0", + "@aws-sdk/credential-provider-node": "3.913.0", + "@aws-sdk/middleware-host-header": "3.910.0", + "@aws-sdk/middleware-logger": "3.910.0", + "@aws-sdk/middleware-recursion-detection": "3.910.0", + "@aws-sdk/middleware-user-agent": "3.911.0", + "@aws-sdk/region-config-resolver": "3.910.0", + "@aws-sdk/signature-v4-multi-region": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@aws-sdk/util-user-agent-browser": "3.910.0", + "@aws-sdk/util-user-agent-node": "3.911.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/core": "^3.16.1", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/hash-node": "^4.2.2", + "@smithy/invalid-dependency": "^4.2.2", + "@smithy/middleware-content-length": "^4.2.2", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-retry": "^4.4.3", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.0", - "@smithy/util-defaults-mode-browser": "^4.2.0", - "@smithy/util-defaults-mode-node": "^4.2.0", - "@smithy/util-endpoints": "^3.2.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-retry": "^4.2.0", - "@smithy/util-stream": "^4.4.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.2", + "@smithy/util-defaults-mode-node": "^4.2.3", + "@smithy/util-endpoints": "^3.2.2", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.911.0.tgz", + "integrity": "sha512-N9QAeMvN3D1ZyKXkQp4aUgC4wUMuA5E1HuVCkajc0bq1pnH4PIke36YlrDGGREqPlyLFrXCkws2gbL5p23vtlg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.911.0", + "@aws-sdk/middleware-host-header": "3.910.0", + "@aws-sdk/middleware-logger": "3.910.0", + "@aws-sdk/middleware-recursion-detection": "3.910.0", + "@aws-sdk/middleware-user-agent": "3.911.0", + "@aws-sdk/region-config-resolver": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@aws-sdk/util-user-agent-browser": "3.910.0", + "@aws-sdk/util-user-agent-node": "3.911.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/core": "^3.16.1", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/hash-node": "^4.2.2", + "@smithy/invalid-dependency": "^4.2.2", + "@smithy/middleware-content-length": "^4.2.2", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-retry": "^4.4.3", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.2", + "@smithy/util-defaults-mode-node": "^4.2.3", + "@smithy/util-endpoints": "^3.2.2", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.911.0.tgz", + "integrity": "sha512-k4QG9A+UCq/qlDJFmjozo6R0eXXfe++/KnCDMmajehIE9kh+b/5DqlGvAmbl9w4e92LOtrY6/DN3mIX1xs4sXw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.910.0", + "@aws-sdk/xml-builder": "3.911.0", + "@smithy/core": "^3.16.1", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/signature-v4": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.911.0.tgz", + "integrity": "sha512-6FWRwWn3LUZzLhqBXB+TPMW2ijCWUqGICSw8bVakEdODrvbiv1RT/MVUayzFwz/ek6e6NKZn6DbSWzx07N9Hjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.911.0.tgz", + "integrity": "sha512-xUlwKmIUW2fWP/eM3nF5u4CyLtOtyohlhGJ5jdsJokr3MrQ7w0tDITO43C9IhCn+28D5UbaiWnKw5ntkw7aVfA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/property-provider": "^4.2.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-stream": "^4.5.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.913.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.913.0.tgz", + "integrity": "sha512-iR4c4NQ1OSRKQi0SxzpwD+wP1fCy+QNKtEyCajuVlD0pvmoIHdrm5THK9e+2/7/SsQDRhOXHJfLGxHapD74WJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/credential-provider-env": "3.911.0", + "@aws-sdk/credential-provider-http": "3.911.0", + "@aws-sdk/credential-provider-process": "3.911.0", + "@aws-sdk/credential-provider-sso": "3.911.0", + "@aws-sdk/credential-provider-web-identity": "3.911.0", + "@aws-sdk/nested-clients": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/credential-provider-imds": "^4.2.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.913.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.913.0.tgz", + "integrity": "sha512-HQPLkKDxS83Q/nZKqg9bq4igWzYQeOMqhpx5LYs4u1GwsKeCsYrrfz12Iu4IHNWPp9EnGLcmdfbfYuqZGrsaSQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.911.0", + "@aws-sdk/credential-provider-http": "3.911.0", + "@aws-sdk/credential-provider-ini": "3.913.0", + "@aws-sdk/credential-provider-process": "3.911.0", + "@aws-sdk/credential-provider-sso": "3.911.0", + "@aws-sdk/credential-provider-web-identity": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/credential-provider-imds": "^4.2.2", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.911.0.tgz", + "integrity": "sha512-mKshhV5jRQffZjbK9x7bs+uC2IsYKfpzYaBamFsEov3xtARCpOiKaIlM8gYKFEbHT2M+1R3rYYlhhl9ndVWS2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.911.0.tgz", + "integrity": "sha512-JAxd4uWe0Zc9tk6+N0cVxe9XtJVcOx6Ms0k933ZU9QbuRMH6xti/wnZxp/IvGIWIDzf5fhqiGyw5MSyDeI5b1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.911.0", + "@aws-sdk/core": "3.911.0", + "@aws-sdk/token-providers": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.911.0.tgz", + "integrity": "sha512-urIbXWWG+cm54RwwTFQuRwPH0WPsMFSDF2/H9qO2J2fKoHRURuyblFCyYG3aVKZGvFBhOizJYexf5+5w3CJKBw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/nested-clients": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.910.0.tgz", + "integrity": "sha512-F9Lqeu80/aTM6S/izZ8RtwSmjfhWjIuxX61LX+/9mxJyEkgaECRxv0chsLQsLHJumkGnXRy/eIyMLBhcTPF5vg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.910.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-logger": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.910.0.tgz", + "integrity": "sha512-3LJyyfs1USvRuRDla1pGlzGRtXJBXD1zC9F+eE9Iz/V5nkmhyv52A017CvKWmYoR0DM9dzjLyPOI0BSSppEaTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.910.0", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.910.0.tgz", + "integrity": "sha512-m/oLz0EoCy+WoIVBnXRXJ4AtGpdl0kPE7U+VH9TsuUzHgxY1Re/176Q1HWLBRVlz4gr++lNsgsMWEC+VnAwMpw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.910.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.911.0.tgz", + "integrity": "sha512-P0mIIW/QkAGNvFu15Jqa5NSmHeQvZkkQY8nbQpCT3tGObZe4wRsq5u1mOS+CJp4DIBbRZuHeX7ohbX5kPMi4dg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.16.1", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/protocol-http": "^5.3.2", + "@smithy/signature-v4": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-stream": "^4.5.2", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.911.0.tgz", + "integrity": "sha512-rY3LvGvgY/UI0nmt5f4DRzjEh8135A2TeHcva1bgOmVfOI4vkkGfA20sNRqerOkSO6hPbkxJapO50UJHFzmmyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@smithy/core": "^3.16.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.911.0.tgz", + "integrity": "sha512-lp/sXbdX/S0EYaMYPVKga0omjIUbNNdFi9IJITgKZkLC6CzspihIoHd5GIdl4esMJevtTQQfkVncXTFkf/a4YA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.911.0", + "@aws-sdk/middleware-host-header": "3.910.0", + "@aws-sdk/middleware-logger": "3.910.0", + "@aws-sdk/middleware-recursion-detection": "3.910.0", + "@aws-sdk/middleware-user-agent": "3.911.0", + "@aws-sdk/region-config-resolver": "3.910.0", + "@aws-sdk/types": "3.910.0", + "@aws-sdk/util-endpoints": "3.910.0", + "@aws-sdk/util-user-agent-browser": "3.910.0", + "@aws-sdk/util-user-agent-node": "3.911.0", + "@smithy/config-resolver": "^4.3.2", + "@smithy/core": "^3.16.1", + "@smithy/fetch-http-handler": "^5.3.3", + "@smithy/hash-node": "^4.2.2", + "@smithy/invalid-dependency": "^4.2.2", + "@smithy/middleware-content-length": "^4.2.2", + "@smithy/middleware-endpoint": "^4.3.3", + "@smithy/middleware-retry": "^4.4.3", + "@smithy/middleware-serde": "^4.2.2", + "@smithy/middleware-stack": "^4.2.2", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/node-http-handler": "^4.4.1", + "@smithy/protocol-http": "^5.3.2", + "@smithy/smithy-client": "^4.8.1", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.2", + "@smithy/util-defaults-mode-node": "^4.2.3", + "@smithy/util-endpoints": "^3.2.2", + "@smithy/util-middleware": "^4.2.2", + "@smithy/util-retry": "^4.2.2", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.910.0.tgz", + "integrity": "sha512-gzQAkuHI3xyG6toYnH/pju+kc190XmvnB7X84vtN57GjgdQJICt9So/BD0U6h+eSfk9VBnafkVrAzBzWMEFZVw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.910.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/types": "^4.7.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.911.0.tgz", + "integrity": "sha512-SJ4dUcY9+HPDIMCHiskT8F7JrRVZF2Y1NUN0Yiy6VUHSULgq2MDlIzSQpNICnmXhk1F1E1B2jJG9XtPYrvtqUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/protocol-http": "^5.3.2", + "@smithy/signature-v4": "^5.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.911.0.tgz", + "integrity": "sha512-O1c5F1pbEImgEe3Vr8j1gpWu69UXWj3nN3vvLGh77hcrG5dZ8I27tSP5RN4Labm8Dnji/6ia+vqSYpN8w6KN5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.911.0", + "@aws-sdk/nested-clients": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/property-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/types": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.910.0.tgz", + "integrity": "sha512-o67gL3vjf4nhfmuSUNNkit0d62QJEwwHLxucwVJkR/rw9mfUtAWsgBs8Tp16cdUbMgsyQtCQilL8RAJDoGtadQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.910.0.tgz", + "integrity": "sha512-6XgdNe42ibP8zCQgNGDWoOF53RfEKzpU/S7Z29FTTJ7hcZv0SytC0ZNQQZSx4rfBl036YWYwJRoJMlT4AA7q9A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.910.0", + "@smithy/types": "^4.7.1", + "@smithy/url-parser": "^4.2.2", + "@smithy/util-endpoints": "^3.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.910.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.910.0.tgz", + "integrity": "sha512-iOdrRdLZHrlINk9pezNZ82P/VxO/UmtmpaOAObUN+xplCUJu31WNM2EE/HccC8PQw6XlAudpdA6HDTGiW6yVGg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.910.0", + "@smithy/types": "^4.7.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.911.0.tgz", + "integrity": "sha512-3l+f6ooLF6Z6Lz0zGi7vSKSUYn/EePPizv88eZQpEAFunBHv+CSVNPtxhxHfkm7X9tTsV4QGZRIqo3taMLolmA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.911.0", + "@aws-sdk/types": "3.910.0", + "@smithy/node-config-provider": "^4.3.2", + "@smithy/types": "^4.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/xml-builder": { + "version": "3.911.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.911.0.tgz", + "integrity": "sha512-/yh3oe26bZfCVGrIMRM9Z4hvvGJD+qx5tOLlydOkuBkm72aXON7D9+MucjJXTAcI8tF2Yq+JHa0478eHQOhnLg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.7.1", + "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1316,6 +1864,8 @@ }, "node_modules/@aws/lambda-invoke-store": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -6541,15 +7091,94 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sendgrid/client": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz", + "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==", + "license": "MIT", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.12.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz", + "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==", + "license": "MIT", + "dependencies": { + "@sendgrid/client": "^8.1.5", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/@shopify/draggable": { "version": "1.1.4", "license": "MIT" }, "node_modules/@smithy/abort-controller": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.3.tgz", + "integrity": "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6578,13 +7207,15 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.3.0", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.3.tgz", + "integrity": "sha512-xSql8A1Bl41O9JvGU/CtgiLBlwkvpHTSKRlvz9zOBvBCPjXghZ6ZkcVzmV2f7FLAA+80+aqKmIOmy8pEDrtCaw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" }, "engines": { @@ -6592,16 +7223,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.14.0", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.0.tgz", + "integrity": "sha512-Tir3DbfoTO97fEGUZjzGeoXgcQAUBRDTmuH9A8lxuP8ATrgezrAJ6cLuRvwdKN4ZbYNlHgKlBX69Hyu3THYhtg==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-base64": "^4.2.0", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-stream": "^4.4.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.3", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -6611,13 +7244,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.3.tgz", + "integrity": "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", "tslib": "^2.6.2" }, "engines": { @@ -6685,13 +7320,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.0", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.4.tgz", + "integrity": "sha512-bwigPylvivpRLCm+YK9I5wRIYjFESSVwl8JQ1vVx/XhCw0PtCi558NwTnT2DaVCl5pYlImGuQTSwMsZ+pIavRw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", - "@smithy/util-base64": "^4.2.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -6712,10 +7349,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.3.tgz", + "integrity": "sha512-6+NOdZDbfuU6s1ISp3UOk5Rg953RJ2aBLNLLBEcamLjHAg1Po9Ha7QIB5ZWhdRUVuOUrT8BVFR+O2KIPmw027g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -6737,10 +7376,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.3.tgz", + "integrity": "sha512-Cc9W5DwDuebXEDMpOpl4iERo8I0KFjTnomK2RMdhhR87GwrSmUmwMxS4P5JdRf+LsjOdIqumcerwRgYMr/tZ9Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6749,6 +7390,8 @@ }, "node_modules/@smithy/is-array-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -6770,11 +7413,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.3.tgz", + "integrity": "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6782,16 +7427,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.0", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.4.tgz", + "integrity": "sha512-/RJhpYkMOaUZoJEkddamGPPIYeKICKXOu/ojhn85dKDM0n5iDIhjvYAQLP3K5FPhgB203O3GpWzoK2OehEoIUw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.14.0", - "@smithy/middleware-serde": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/core": "^3.17.0", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" }, "engines": { @@ -6799,16 +7446,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.0", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.4.tgz", + "integrity": "sha512-vSgABQAkuUHRO03AhR2rWxVQ1un284lkBn+NFawzdahmzksAoOeVMnXXsuPViL4GlhRHXqFaMlc8Mj04OfQk1w==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/service-error-classification": "^4.2.0", - "@smithy/smithy-client": "^4.7.0", - "@smithy/types": "^4.6.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-retry": "^4.2.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/service-error-classification": "^4.2.3", + "@smithy/smithy-client": "^4.9.0", + "@smithy/types": "^4.8.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -6817,11 +7466,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.3.tgz", + "integrity": "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6829,10 +7480,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.3.tgz", + "integrity": "sha512-iGuOJkH71faPNgOj/gWuEGS6xvQashpLwWB1HjHq1lNNiVfbiJLpZVbhddPuDbx9l4Cgl0vPLq5ltRfSaHfspA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6840,12 +7493,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.0", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.3.tgz", + "integrity": "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6853,13 +7508,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.3.0", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.2.tgz", + "integrity": "sha512-MHFvTjts24cjGo1byXqhXrbqm7uznFD/ESFx8npHMWTFQVdBZjrT1hKottmp69LBTRm/JQzP/sn1vPt0/r6AYQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/abort-controller": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6867,10 +7524,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.3.tgz", + "integrity": "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6878,10 +7537,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.0", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.3.tgz", + "integrity": "sha512-Mn7f/1aN2/jecywDcRDvWWWJF4uwg/A0XjFMJtj72DsgHTByfjRltSqcT9NyE9RTdBSN6X1RSXrhn/YWQl8xlw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6889,10 +7550,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.3.tgz", + "integrity": "sha512-LOVCGCmwMahYUM/P0YnU/AlDQFjcu+gWbFJooC417QRB/lDJlWSn8qmPSDp+s4YVAHOgtgbNG4sR+SxF/VOcJQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -6901,10 +7564,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.3.tgz", + "integrity": "sha512-cYlSNHcTAX/wc1rpblli3aUlLMGgKZ/Oqn8hhjFASXMCXjIqeuQBei0cnq2JR8t4RtU9FpG6uyl6PxyArTiwKA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6912,20 +7577,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.3.tgz", + "integrity": "sha512-NkxsAxFWwsPsQiwFG2MzJ/T7uIR6AQNh1SzcxSUnmmIqIQMlLRQDKhc17M7IYjiuBXhrQRjQTo3CxX+DobS93g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0" + "@smithy/types": "^4.8.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.3.0", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.3.tgz", + "integrity": "sha512-9f9Ixej0hFhroOK2TxZfUUDR13WVa8tQzhSzPDgXe5jGL3KmaM9s8XN7RQwqtEypI82q9KHnKS71CJ+q/1xLtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6933,14 +7602,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.0", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.3.tgz", + "integrity": "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -6950,15 +7621,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.7.0", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.0.tgz", + "integrity": "sha512-qz7RTd15GGdwJ3ZCeBKLDQuUQ88m+skh2hJwcpPm1VqLeKzgZvXf6SrNbxvx7uOqvvkjCMXqx3YB5PDJyk00ww==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.14.0", - "@smithy/middleware-endpoint": "^4.3.0", - "@smithy/middleware-stack": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-stream": "^4.4.0", + "@smithy/core": "^3.17.0", + "@smithy/middleware-endpoint": "^4.3.4", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.3", "tslib": "^2.6.2" }, "engines": { @@ -6966,7 +7639,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.6.0", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.0.tgz", + "integrity": "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -6976,11 +7651,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.3.tgz", + "integrity": "sha512-I066AigYvY3d9VlU3zG9XzZg1yT10aNqvCaBTw9EPgu5GrsEl1aUkcMvhkIXascYH1A8W0LQo3B1Kr1cJNcQEw==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/querystring-parser": "^4.2.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -6988,7 +7665,9 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.2.0", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", @@ -7001,6 +7680,8 @@ }, "node_modules/@smithy/util-body-length-browser": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7010,7 +7691,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.0", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7021,6 +7704,8 @@ }, "node_modules/@smithy/util-buffer-from": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", @@ -7032,6 +7717,8 @@ }, "node_modules/@smithy/util-config-provider": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7041,13 +7728,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.2.0", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.3.tgz", + "integrity": "sha512-vqHoybAuZXbFXZqgzquiUXtdY+UT/aU33sxa4GBPkiYklmR20LlCn+d3Wc3yA5ZM13gQ92SZe/D8xh6hkjx+IQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.0", - "@smithy/types": "^4.6.0", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -7055,15 +7743,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.4.tgz", + "integrity": "sha512-X5/xrPHedifo7hJUUWKlpxVb2oDOiqPUXlvsZv1EZSjILoutLiJyWva3coBpn00e/gPSpH8Rn2eIbgdwHQdW7Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.3.0", - "@smithy/credential-provider-imds": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.0", - "@smithy/types": "^4.6.0", + "@smithy/config-resolver": "^4.3.3", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -7071,11 +7761,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.0", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.3.tgz", + "integrity": "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -7084,6 +7776,8 @@ }, "node_modules/@smithy/util-hex-encoding": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7093,10 +7787,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.3.tgz", + "integrity": "sha512-v5ObKlSe8PWUHCqEiX2fy1gNv6goiw6E5I/PN2aXg3Fb/hse0xeaAnSpXDiWl7x6LamVKq7senB+m5LOYHUAHw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -7104,11 +7800,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.0", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.3.tgz", + "integrity": "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/service-error-classification": "^4.2.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -7116,13 +7814,15 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.4.0", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.3.tgz", + "integrity": "sha512-oZvn8a5bwwQBNYHT2eNo0EU8Kkby3jeIg1P2Lu9EQtqDxki1LIjGRJM6dJ5CZUig8QmLxWxqOKWvg3mVoOBs5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.0", - "@smithy/node-http-handler": "^4.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-base64": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.2", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", @@ -7134,6 +7834,8 @@ }, "node_modules/@smithy/util-uri-escape": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7144,6 +7846,8 @@ }, "node_modules/@smithy/util-utf8": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", @@ -7157,8 +7861,8 @@ "version": "4.2.0", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/abort-controller": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { @@ -7167,6 +7871,8 @@ }, "node_modules/@smithy/uuid": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7726,10 +8432,28 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.0", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "license": "MIT" + }, + "node_modules/@types/nodemailer": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.2.tgz", + "integrity": "sha512-Zo6uOA9157WRgBk/ZhMpTQ/iCWLMk7OIs/Q9jvHarMvrzUUP/MDdPHL2U1zpf57HrrWGv4nYQn5uIxna0xY3xw==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" } }, "node_modules/@types/normalize-package-data": { @@ -8615,6 +9339,12 @@ "retry": "0.13.1" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/auto-bind": { "version": "4.0.0", "dev": true, @@ -8639,11 +9369,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axe-core": { - "version": "4.10.3", - "license": "MPL-2.0", - "engines": { - "node": ">=4" + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" } }, "node_modules/babel-plugin-macros": { @@ -9432,6 +10166,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/common-tags": { "version": "1.8.2", "dev": true, @@ -9637,6 +10383,7 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9863,6 +10610,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "license": "MIT", @@ -9890,6 +10646,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "license": "MIT", @@ -10105,6 +10870,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-toolkit": { "version": "1.39.10", "license": "MIT", @@ -10804,6 +11584,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "license": "MIT", @@ -10817,28 +11617,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "license": "ISC", + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "license": "ISC", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 6" } }, "node_modules/formdata-polyfill": { @@ -11772,6 +12564,7 @@ }, "node_modules/isexe": { "version": "2.0.0", + "dev": true, "license": "ISC" }, "node_modules/isomorphic-ws": { @@ -12577,7 +13370,9 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -12586,9 +13381,10 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">=6" @@ -12835,6 +13631,15 @@ "license": "MIT", "optional": true }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "dev": true, @@ -12924,6 +13729,15 @@ "version": "2.0.23", "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-package-data": { "version": "3.0.3", "license": "BSD-2-Clause", @@ -13436,6 +14250,7 @@ }, "node_modules/path-key": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13824,6 +14639,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/prr": { "version": "1.0.1", "dev": true, @@ -15067,6 +15888,7 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -15077,6 +15899,7 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16018,6 +16841,20 @@ "turbo-windows-arm64": "2.5.8" } }, + "node_modules/turbo-darwin-64": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.8.tgz", + "integrity": "sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/turbo-darwin-arm64": { "version": "2.5.8", "cpu": [ @@ -16030,6 +16867,62 @@ "darwin" ] }, + "node_modules/turbo-linux-64": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.8.tgz", + "integrity": "sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-linux-arm64": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.8.tgz", + "integrity": "sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-windows-64": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.8.tgz", + "integrity": "sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/turbo-windows-arm64": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.8.tgz", + "integrity": "sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/turf-extent": { "version": "1.0.4", "license": "MIT", @@ -16197,7 +17090,9 @@ } }, "node_modules/undici-types": { - "version": "7.14.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -16700,6 +17595,7 @@ }, "node_modules/which": { "version": "2.0.2", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/packages/common/src/models/user.ts b/packages/common/src/models/user.ts index 3be797bc3..f9f01dee2 100644 --- a/packages/common/src/models/user.ts +++ b/packages/common/src/models/user.ts @@ -75,6 +75,51 @@ export const userSchema = new Schema( }, ], }, + monitoredClasses: { + required: false, + default: [], + type: [ + { + class: { + year: { + type: Number, + required: true, + }, + semester: { + type: String, + enum: ["Spring", "Summer", "Fall", "Winter"], + required: true, + }, + subject: { + type: String, + required: true, + }, + courseNumber: { + type: String, + required: true, + }, + number: { + type: String, + required: true, + }, + }, + thresholds: [Number], + }, + ], + }, + notificationsOn: { + type: Boolean, + required: true, + default: false, + }, + lastNotified: { + type: Date, + required: false, + }, + refresh_token: { + type: String, + required: false, + }, minors: { type: [String], trim: true,