diff --git a/src/backend/index.ts b/src/backend/index.ts index 8131dadcf8..504ebdf3c0 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import express, { Router } from 'express'; import cors from 'cors'; import cookieParser from 'cookie-parser'; import { getUserAndOrganization, prodHeaders, requireJwtDev, requireJwtProd } from './src/utils/auth.utils'; @@ -17,7 +17,7 @@ import wbsElementTemplatesRouter from './src/routes/wbs-element-templates.routes import carsRouter from './src/routes/cars.routes'; import organizationRouter from './src/routes/organizations.routes'; import recruitmentRouter from './src/routes/recruitment.routes'; -import { slackEvents } from './src/routes/slack.routes'; +import { getReceiver } from './src/integrations/slack'; import announcementsRouter from './src/routes/announcements.routes'; import onboardingRouter from './src/routes/onboarding.routes'; import popUpsRouter from './src/routes/pop-up.routes'; @@ -25,6 +25,8 @@ import statisticsRouter from './src/routes/statistics.routes'; import retrospectiveRouter from './src/routes/retrospective.routes'; import partsRouter from './src/routes/parts.routes'; import financeRouter from './src/routes/finance.routes'; +// Import slack routes (event listeners will be registered if Slack is configured) +import './src/routes/slack.routes'; const app = express(); @@ -61,9 +63,15 @@ const options: cors.CorsOptions = { allowedHeaders }; -// so we can listen to slack messages -// NOTE: must be done before using json -app.use('/slack', slackEvents.requestListener()); +// Mount Slack Bolt receiver BEFORE other middleware to handle raw body parsing +// Bolt's receiver handles its own body parsing and request verification +// The receiver is configured to handle requests at /slack/events +// Only mount if Slack is configured (when SLACK_BOT_TOKEN is set) +const receiver = getReceiver(); +if (receiver) { + app.use(receiver.router as unknown as Router); +} + app.get('/health', (_req, res) => { res.status(200).json({ status: 'healthy' }); }); diff --git a/src/backend/package.json b/src/backend/package.json index 92ec84d984..6f1f592282 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -10,8 +10,7 @@ }, "dependencies": { "@prisma/client": "^6.2.1", - "@slack/events-api": "^3.0.1", - "@slack/web-api": "^7.8.0", + "@slack/bolt": "^3.22.0", "@types/concat-stream": "^2.0.0", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", diff --git a/src/backend/src/controllers/slack.controllers.ts b/src/backend/src/controllers/slack.controllers.ts index e7336711d2..cec8a072ae 100644 --- a/src/backend/src/controllers/slack.controllers.ts +++ b/src/backend/src/controllers/slack.controllers.ts @@ -1,6 +1,6 @@ -import { getWorkspaceId } from '../integrations/slack'; +import { getWorkspaceId, replyToMessageInThread } from '../integrations/slack'; import OrganizationsService from '../services/organizations.services'; -import SlackServices from '../services/slack.services'; +import SlackServices, { SlackBlockActionBody, SaboSubmissionActionValue } from '../services/slack.services'; export default class SlackController { static async processMessageEvent(event: any) { @@ -15,4 +15,71 @@ export default class SlackController { console.log(error); } } + + /** + * Handles the Slack block action for SABO submission confirmation. + * Performs action-specific validation and extracts relevant fields from the Slack action body. + * If validation fails, replies to the user in Slack with an error message. + * + * @param body The validated Slack block action body (general structure validated in routes) + */ + static async handleSaboSubmittedAction(body: SlackBlockActionBody) { + const { user, container, actions } = body; + const channelId = container.channel_id; + const threadTs = container.thread_ts || container.message_ts; + const [firstAction] = actions; + + try { + // Action-specific validation: verify action_id + if (firstAction.action_id !== 'sabo_submitted_confirmation') { + console.error('Unexpected action_id:', firstAction.action_id); + await replyToMessageInThread( + channelId, + threadTs, + `❌ An error occurred: Unexpected action type "${firstAction.action_id}". Please contact the software team.` + ); + return; + } + + // Action-specific validation: verify value format + let actionValue: SaboSubmissionActionValue; + try { + actionValue = JSON.parse(firstAction.value); + } catch (parseError) { + const parseErrorMsg = parseError instanceof Error ? parseError.message : 'Unknown parse error'; + await replyToMessageInThread( + channelId, + threadTs, + `❌ An error occurred: Invalid action data format.\n\n*Error:* ${parseErrorMsg}\n*Value:* \`${firstAction.value}\`\n\nPlease contact the software team.` + ); + return; + } + + // Validate that reimbursementRequestId exists in the parsed value + if (!actionValue.reimbursementRequestId || typeof actionValue.reimbursementRequestId !== 'string') { + const actionValueStr = JSON.stringify(actionValue, null, 2); + await replyToMessageInThread( + channelId, + threadTs, + `❌ An error occurred: Missing or invalid reimbursement request ID.\n\n*Parsed value:*\n\`\`\`${actionValueStr}\`\`\`\n\nPlease contact the software team.` + ); + return; + } + + // Extract validated fields + const userSlackId = user.id; + const { reimbursementRequestId } = actionValue; + + // Pass the extracted fields to the service layer for business logic + await SlackServices.handleSaboSubmittedAction(userSlackId, reimbursementRequestId); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + await replyToMessageInThread( + channelId, + threadTs, + `❌ An unexpected error occurred while processing your request.\n\n*Error message:* ${errorMessage}\n\nPlease contact the software team and provide them with this information.` + ); + throw error; + } + } } diff --git a/src/backend/src/integrations/slack.ts b/src/backend/src/integrations/slack.ts index 6e855acae7..acf390437d 100644 --- a/src/backend/src/integrations/slack.ts +++ b/src/backend/src/integrations/slack.ts @@ -1,7 +1,49 @@ -import { ChatPostMessageResponse, WebClient } from '@slack/web-api'; +import { App, ExpressReceiver } from '@slack/bolt'; import { HttpException } from '../utils/errors.utils'; -const slack = new WebClient(process.env.SLACK_BOT_TOKEN); +let receiver: ExpressReceiver | null = null; +let slackApp: App | null = null; +let slack: any = null; // Type will be inferred from slackApp.client (WebClient from Bolt) + +/** + * Initializes the Slack Bolt app, receiver, and client if not already initialized + * Only initializes if SLACK_BOT_TOKEN is present + */ +const initializeSlack = () => { + const { SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET } = process.env; + + // Don't initialize if no token is configured (e.g., in tests) + if (!SLACK_BOT_TOKEN) { + return; + } + + // Don't re-initialize if already initialized + if (slackApp) { + return; + } + + // Initialize the receiver, app, and client + receiver = new ExpressReceiver({ + signingSecret: SLACK_SIGNING_SECRET || '', + endpoints: '/slack/events' + }); + + slackApp = new App({ + token: SLACK_BOT_TOKEN, + receiver + }); + + slack = slackApp.client; +}; + +/** + * Get the Slack WebClient (initializes Slack if needed) + * @returns the Slack WebClient or null if no token is configured + */ +const getSlackClient = () => { + initializeSlack(); + return slack; +}; /** * Send a slack message @@ -12,14 +54,13 @@ const slack = new WebClient(process.env.SLACK_BOT_TOKEN); * @returns the channel id and timestamp of the created slack message */ export const sendMessage = async (slackId: string, message: string, link?: string, linkButtonText?: string) => { - const { SLACK_BOT_TOKEN } = process.env; - if (!SLACK_BOT_TOKEN) return; + const client = getSlackClient(); + if (!client) return; const block = generateSlackTextBlock(message, link, linkButtonText); try { - const response: ChatPostMessageResponse = await slack.chat.postMessage({ - token: SLACK_BOT_TOKEN, + const response = await client.chat.postMessage({ channel: slackId, text: message, blocks: [block], @@ -47,14 +88,13 @@ export const replyToMessageInThread = async ( link?: string, linkButtonText?: string ) => { - const { SLACK_BOT_TOKEN } = process.env; - if (!SLACK_BOT_TOKEN) return; + const client = getSlackClient(); + if (!client) return; const block = generateSlackTextBlock(message, link, linkButtonText); try { - await slack.chat.postMessage({ - token: SLACK_BOT_TOKEN, + await client.chat.postMessage({ channel: slackId, thread_ts: parentTimestamp, text: message, @@ -80,14 +120,13 @@ export const editMessage = async ( link?: string, linkButtonText?: string ) => { - const { SLACK_BOT_TOKEN } = process.env; - if (!SLACK_BOT_TOKEN) return; + const client = getSlackClient(); + if (!client) return; const block = generateSlackTextBlock(message, link, linkButtonText); try { - await slack.chat.update({ - token: SLACK_BOT_TOKEN, + await client.chat.update({ channel: slackId, ts: timestamp, text: message, @@ -105,12 +144,11 @@ export const editMessage = async ( * @param emoji - the emoji to react with */ export const reactToMessage = async (slackId: string, parentTimestamp: string, emoji: string) => { - const { SLACK_BOT_TOKEN } = process.env; - if (!SLACK_BOT_TOKEN) return; + const client = getSlackClient(); + if (!client) return; try { - await slack.reactions.add({ - token: SLACK_BOT_TOKEN, + await client.reactions.add({ channel: slackId, timestamp: parentTimestamp, name: emoji @@ -161,12 +199,15 @@ const generateSlackTextBlock = (message: string, link?: string, linkButtonText?: * @returns an array of strings of all the slack ids of the users in the given channel */ export const getUsersInChannel = async (channelId: string) => { + const client = getSlackClient(); + if (!client) return []; + let members: string[] = []; let cursor: string | undefined; try { do { - const response = await slack.conversations.members({ + const response = await client.conversations.members({ channel: channelId, cursor, limit: 200 @@ -192,8 +233,11 @@ export const getUsersInChannel = async (channelId: string) => { * @returns the name of the channel or undefined if it cannot be found */ export const getChannelName = async (channelId: string) => { + const client = getSlackClient(); + if (!client) return undefined; + try { - const channelRes = await slack.conversations.info({ channel: channelId }); + const channelRes = await client.conversations.info({ channel: channelId }); return channelRes.channel?.name; } catch (error) { return undefined; @@ -206,8 +250,11 @@ export const getChannelName = async (channelId: string) => { * @returns the name of the user (real name if no display name), undefined if cannot be found */ export const getUserName = async (userId: string) => { + const client = getSlackClient(); + if (!client) return undefined; + try { - const userRes = await slack.users.info({ user: userId }); + const userRes = await client.users.info({ user: userId }); return userRes.user?.profile?.display_name || userRes.user?.real_name; } catch (error) { return undefined; @@ -219,8 +266,13 @@ export const getUserName = async (userId: string) => { * @returns the id of the workspace */ export const getWorkspaceId = async () => { + const client = getSlackClient(); + if (!client) { + throw new HttpException(500, 'Slack client not configured'); + } + try { - const response = await slack.auth.test(); + const response = await client.auth.test(); if (response.ok) { return response.team_id; } @@ -230,4 +282,57 @@ export const getWorkspaceId = async () => { } }; -export default slack; +/** + * Sends a slack ephemeral message to a user + * @param channelId - the channel id of the channel to send to + * @param threadTs - the timestamp of the thread to send to + * @param userId - the id of the user to send to + * @param text - the text of the message to send (should always be populated in case blocks can't be rendered, but if blocks render text will not) + * @param blocks - the blocks of the message to send + */ +export async function sendEphemeralMessage( + channelId: string, + threadTs: string, + userId: string, + text: string, + blocks: any[] +) { + const client = getSlackClient(); + if (!client) return; + + try { + await client.chat.postEphemeral({ + channel: channelId, + user: userId, + thread_ts: threadTs, + text, + blocks + }); + } catch (err: unknown) { + if (err instanceof Error) { + throw new HttpException(500, `Failed to send slack notifications: ${err.message}`); + } + } +} + +/** + * Get the Slack Bolt app instance (initializes Slack if needed) + * @returns the Slack Bolt App or null if no token is configured + */ +export const getSlackApp = (): App | null => { + initializeSlack(); + return slackApp; +}; + +/** + * Get the Express receiver instance (initializes Slack if needed) + * @returns the ExpressReceiver or null if no token is configured + */ +export const getReceiver = (): ExpressReceiver | null => { + initializeSlack(); + return receiver; +}; + +// Export the getters for any direct usage if needed +export { getSlackClient }; +export default getSlackClient; diff --git a/src/backend/src/routes/slack.routes.ts b/src/backend/src/routes/slack.routes.ts index 6878b176b1..46de14bb3f 100644 --- a/src/backend/src/routes/slack.routes.ts +++ b/src/backend/src/routes/slack.routes.ts @@ -1,8 +1,123 @@ -import { createEventAdapter } from '@slack/events-api'; +import { getSlackApp } from '../integrations/slack'; import SlackController from '../controllers/slack.controllers'; -export const slackEvents = createEventAdapter(process.env.SLACK_SIGNING_SECRET || ''); +// Register Slack event listeners only if the Slack app is configured +const slackApp = getSlackApp(); -slackEvents.on('message', SlackController.processMessageEvent); +if (slackApp) { + // Register message event listener + slackApp.message(async ({ message, logger }: any) => { + try { + await SlackController.processMessageEvent(message); + } catch (error) { + logger.error('Error processing message event:', error); + console.error(error); + } + }); +} -slackEvents.on('error', console.log); +/** + * Validates the general structure of a Slack block action payload. + * This validation is action-agnostic and only checks that the required fields exist. + * Action-specific validation (like action_id and value format) happens in the controller. + * + * @param body The Slack action body to validate + * @returns true if valid, false otherwise + */ +function validateSlackActionBody(body: any): boolean { + // Check required top-level fields + if (!body || typeof body !== 'object') { + console.error('Invalid body: not an object'); + return false; + } + + if (body.type !== 'block_actions') { + console.error('Invalid body type:', body.type); + return false; + } + + // Validate user object + if (!body.user || typeof body.user !== 'object') { + console.error('Invalid or missing user object'); + return false; + } + + if (!body.user.id || typeof body.user.id !== 'string') { + console.error('Invalid or missing user.id'); + return false; + } + + if (!body.user.team_id || typeof body.user.team_id !== 'string') { + console.error('Invalid or missing user.team_id'); + return false; + } + + // Validate actions array exists and has at least one action + if (!Array.isArray(body.actions) || body.actions.length === 0) { + console.error('Invalid or empty actions array'); + return false; + } + + const [action] = body.actions; + if (!action.action_id || typeof action.action_id !== 'string') { + console.error('Invalid or missing action_id'); + return false; + } + + if (!action.value || typeof action.value !== 'string') { + console.error('Invalid or missing action value'); + return false; + } + + // Validate container object (for message timestamp and channel) + if (!body.container || typeof body.container !== 'object') { + console.error('Invalid or missing container object'); + return false; + } + + if (!body.container.message_ts || typeof body.container.message_ts !== 'string') { + console.error('Invalid or missing container.message_ts'); + return false; + } + + if (!body.container.channel_id || typeof body.container.channel_id !== 'string') { + console.error('Invalid or missing container.channel_id'); + return false; + } + + // Validate thread_ts if it exists (optional but if present should be string) + if (body.container.thread_ts && typeof body.container.thread_ts !== 'string') { + console.error('Invalid container.thread_ts type'); + return false; + } + + return true; +} + +if (slackApp) { + // Register interactive action handler for SABO submission confirmation + slackApp.action('sabo_submitted_confirmation', async ({ ack, body, logger, respond }: any) => { + await ack(); + + try { + // Validate the incoming action body structure + if (!validateSlackActionBody(body)) { + logger.error('Invalid Slack action body structure'); + return; + } + + await SlackController.handleSaboSubmittedAction(body); + + // If no error, delete the original message + await respond({ delete_original: true }); + } catch (error) { + // Can't pass to normal middleware because not normal request + logger.error('Error handling sabo_submitted_confirmation action:', error); + } + }); + + // Error handler + slackApp.error(async (error: Error) => { + console.error('Slack app error:', error); + }); +} diff --git a/src/backend/src/services/reimbursement-requests.services.ts b/src/backend/src/services/reimbursement-requests.services.ts index 9463ba88a5..0b5459ae20 100644 --- a/src/backend/src/services/reimbursement-requests.services.ts +++ b/src/backend/src/services/reimbursement-requests.services.ts @@ -418,12 +418,26 @@ export default class ReimbursementRequestService { totalCost, accountCodeId: accountCode.accountCodeId, vendorId: vendor.vendorId + }, + include: { + notificationSlackThreads: true } }); //set any deleted receipts with a dateDeleted await removeDeletedReceiptPictures(receiptPictures, oldReimbursementRequest.receiptPictures || [], submitter); + try { + await sendPendingSaboSubmissionNotification( + updatedReimbursementRequest.notificationSlackThreads, + submitter.userId, + updatedReimbursementRequest.recipientId, + updatedReimbursementRequest.reimbursementRequestId + ); + } catch (e: unknown) { + console.error('Error sending pending SABO submission notification:', e); + } + return updatedReimbursementRequest; } @@ -1292,7 +1306,8 @@ export default class ReimbursementRequestService { await sendPendingSaboSubmissionNotification( reimbursementRequest.notificationSlackThreads, submitter.userId, - reimbursementRequest.recipientId + reimbursementRequest.recipientId, + reimbursementRequest.reimbursementRequestId ); } catch (e: unknown) { console.error('Error sending pending SABO submission notification:', e); @@ -2075,7 +2090,7 @@ export default class ReimbursementRequestService { // find all names that have been tagged in the @FirstLast format const taggedNames = [...comment.matchAll(tagRegex)].map((match) => match[1]); - // spliot the tagged names into first and last names + // split the tagged names into first and last names const splitTaggedNames = taggedNames.map((name) => { const match = name.match(/([A-Z][a-z'-]+)([A-Z][a-z'-]+)/); diff --git a/src/backend/src/services/slack.services.ts b/src/backend/src/services/slack.services.ts index a289124013..16e379a98c 100644 --- a/src/backend/src/services/slack.services.ts +++ b/src/backend/src/services/slack.services.ts @@ -1,9 +1,10 @@ import { getChannelName, getUserName } from '../integrations/slack'; import AnnouncementService from './announcement.services'; -import { Announcement } from 'shared'; +import { Announcement, ReimbursementStatusType } from 'shared'; import prisma from '../prisma/prisma'; import { blockToMentionedUsers, blockToString } from '../utils/slack.utils'; -import { NotFoundException } from '../utils/errors.utils'; +import { InvalidOrganizationException, NotFoundException } from '../utils/errors.utils'; +import ReimbursementRequestService from './reimbursement-requests.services'; /** * Represents a slack event for a message in a channel. @@ -66,7 +67,138 @@ export interface SlackRichTextBlock { usergroup_id?: string; } +/** + * Represents a Slack block action body structure. + * The general structure is validated in routes, while action-specific fields + * (action_id and value format) are validated in controllers. + */ +export interface SlackBlockActionBody { + type: 'block_actions'; + user: { + id: string; + username: string; + name: string; + team_id: string; + }; + api_app_id: string; + token: string; + container: { + type: string; + message_ts: string; + channel_id: string; + is_ephemeral: boolean; + thread_ts?: string; // Optional - if present, the message is in a thread + }; + trigger_id: string; + team: { + id: string; + domain: string; + }; + enterprise: null | { + id: string; + name: string; + }; + is_enterprise_install: boolean; + channel: { + id: string; + name: string; + }; + state: { + values: Record; + }; + response_url: string; + actions: Array<{ + action_id: string; // Validated in controller, not routes + block_id: string; + text?: any; + value: string; // Validated for format in controller, not routes + style?: string; + type: string; + action_ts: string; + }>; +} + +/** + * Represents the parsed value from a SABO submission action + */ +export interface SaboSubmissionActionValue { + reimbursementRequestId: string; +} + export default class SlackServices { + /** + * Handles the Slack button click for marking a reimbursement request as SABO submitted. + * This performs the business logic for processing the SABO submission confirmation. + * + * @param userSlackId The Slack user ID of the user who clicked the button + * @param teamSlackId The Slack team ID (workspace ID) where the action occurred + * @param reimbursementRequestId The ID of the reimbursement request to mark as submitted + * @param interactiveMessageTs The timestamp of the interactive message (to delete after processing) + */ + static async handleSaboSubmittedAction(userSlackId: string, reimbursementRequestId: string): Promise { + // Find the user by their slack ID + const user = await prisma.user.findFirst({ + where: { + userSettings: { + slackId: userSlackId + } + } + }); + + if (!user) { + console.error('User not found for slack ID:', userSlackId); + throw new NotFoundException('User', userSlackId); + } + + // Find the reimbursement request + const reimbursementRequest = await prisma.reimbursement_Request.findUnique({ + where: { + reimbursementRequestId + }, + include: { + organization: true, + reimbursementStatuses: true, + notificationSlackThreads: true + } + }); + + if (!reimbursementRequest) { + throw new NotFoundException('Reimbursement Request', reimbursementRequestId); + } + + // Verify that the user's organization matches the reimbursement request's organization + const userOrganization = await prisma.user.findFirst({ + where: { + userId: user.userId + }, + include: { + organizations: true + } + }); + + const hasAccess = userOrganization?.organizations.some( + (org) => org.organizationId === reimbursementRequest.organizationId + ); + + if (!hasAccess) { + throw new InvalidOrganizationException('Reimbursement Request'); + } + + // If the reimbursement request has already been submitted to SABO, just return (message will be deleted by route) + if ( + reimbursementRequest.reimbursementStatuses.some((status) => status.type === ReimbursementStatusType.SABO_SUBMITTED) + ) { + return; + } + + // Call the service function to mark as SABO submitted + await ReimbursementRequestService.markReimbursementRequestAsSaboSubmitted( + reimbursementRequestId, + user, + reimbursementRequest.organization + ); + } + /** * Given a slack event representing a message in a channel, * make the appropriate announcement change in prisma. diff --git a/src/backend/src/utils/auth.utils.ts b/src/backend/src/utils/auth.utils.ts index ed51775e1d..4517b3a88a 100644 --- a/src/backend/src/utils/auth.utils.ts +++ b/src/backend/src/utils/auth.utils.ts @@ -33,7 +33,7 @@ export const requireJwtProd = (req: Request, res: Response, next: NextFunction) req.path === '/users/auth/login' || // logins dont have cookies yet req.path === '/' || // base route is available so aws can listen and check the health req.method === 'OPTIONS' || // this is a pre-flight request and those don't send cookies - req.path === '/slack' // slack http endpoint is only used from slack api + req.path.startsWith('/slack') // slack endpoints (events and interactions) are only used from slack api ) { return next(); } else if ( @@ -65,7 +65,7 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) = req.path === '/' || // base route is available so aws can listen and check the health req.method === 'OPTIONS' || // this is a pre-flight request and those don't send cookies req.path === '/users' || // dev login needs the list of users to log in - req.path === '/slack' // slack http endpoint is only used from slack api + req.path.startsWith('/slack') // slack endpoints (events and interactions) are only used from slack api ) { next(); } else if ( @@ -185,7 +185,7 @@ export const getUserAndOrganization = async (req: Request, res: Response, next: req.path === '/' || // base route is available so aws can listen and check the health req.method === 'OPTIONS' || // this is a pre-flight request and those don't send cookies req.path === '/users' || // dev login needs the list of users to log in - req.path === '/slack' || // slack http endpoint is only used from slack api + req.path.startsWith('/slack') || // slack endpoints (events and interactions) are only used from slack api req.path.startsWith('/notifications') // Notifications route has its own auth, only called from gh ) { return next(); diff --git a/src/backend/src/utils/finance.utils.ts b/src/backend/src/utils/finance.utils.ts index 5c64bf1da4..75c698ade4 100644 --- a/src/backend/src/utils/finance.utils.ts +++ b/src/backend/src/utils/finance.utils.ts @@ -8,43 +8,49 @@ export const getProjectSegmentedWhereInput = ( ): { where: { wbsElement: { - organizationId: string; - dateDeleted: null; - carNumber?: number; - dateCreated?: { gte?: Date; lte?: Date }; + is: { + organizationId: string; + dateDeleted: null; + carNumber?: number; + dateCreated?: { gte?: Date; lte?: Date }; + }; }; }; } => { const baseWhere: { where: { wbsElement: { - organizationId: string; - dateDeleted: null; - carNumber?: number; - dateCreated?: { gte?: Date; lte?: Date }; + is: { + organizationId: string; + dateDeleted: null; + carNumber?: number; + dateCreated?: { gte?: Date; lte?: Date }; + }; }; }; } = Prisma.validator()({ where: { wbsElement: { - organizationId, - dateDeleted: null + is: { + organizationId, + dateDeleted: null + } } } }); - if (startDate) { - baseWhere.where.wbsElement.dateCreated = { + if (startDate !== undefined) { + baseWhere.where.wbsElement.is.dateCreated = { gte: startDate }; } - if (endDate) { - baseWhere.where.wbsElement.dateCreated = { ...baseWhere.where.wbsElement.dateCreated, lte: endDate }; + if (endDate !== undefined) { + baseWhere.where.wbsElement.is.dateCreated = { ...baseWhere.where.wbsElement.is.dateCreated, lte: endDate }; } if (carNumber !== undefined) { - baseWhere.where.wbsElement.carNumber = carNumber; + baseWhere.where.wbsElement.is.carNumber = carNumber; } return baseWhere; @@ -80,7 +86,7 @@ export const getReimbursementRequestWhereInput = ( }) }; - if (carNumber !== undefined) { + if (carNumber) { baseWhere.reimbursementProducts = { some: { OR: [ diff --git a/src/backend/src/utils/slack.utils.ts b/src/backend/src/utils/slack.utils.ts index 91cea13067..e5ed33dbe4 100644 --- a/src/backend/src/utils/slack.utils.ts +++ b/src/backend/src/utils/slack.utils.ts @@ -16,6 +16,7 @@ import { getUsersInChannel, reactToMessage, replyToMessageInThread, + sendEphemeralMessage, sendMessage } from '../integrations/slack'; import { getUserSlackId, getUserSlackMentionOrName } from './users.utils'; @@ -235,12 +236,58 @@ export const sendSubmittedToSaboNotification = async (threads: SlackMessageThrea export const sendPendingSaboSubmissionNotification = async ( threads: SlackMessageThread[], financeUserId: string, - pendingSubmissionFromId: string + pendingSubmissionFromId: string, + reimbursementRequestId: string ) => { await sendThreadResponse( threads, `${await getUserSlackMentionOrName(financeUserId)} has added this reimbursement request to Concur. ${await getUserSlackMentionOrName(pendingSubmissionFromId)}, please check your email to approve the request in Concur and mark it as submitted on Finishline.` ); + const userId = await getUserSlackId(financeUserId); + if (threads && threads.length !== 0 && userId) { + const msgs = threads.map((thread) => + sendEphemeralMessage( + thread.channelId, + thread.timestamp, + userId, + 'Approve the request on concur and then click the button below to mark it as submitted on Finishline.', + [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'Approve the request on concur and then click the button below to mark it as submitted on Finishline.' + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '' + } + }, + { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: "✓ I've approved the request on Concur" + }, + style: 'primary', + action_id: 'sabo_submitted_confirmation', + value: JSON.stringify({ + reimbursementRequestId + }) + } + ] + } + ] + ) + ); + await Promise.all(msgs); + } }; export const sendSlackDesignReviewConfirmNotification = async ( diff --git a/yarn.lock b/yarn.lock index 6e5ea4207e..e0d2d26377 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4816,27 +4816,34 @@ __metadata: languageName: node linkType: hard -"@slack/events-api@npm:^3.0.1": - version: 3.0.1 - resolution: "@slack/events-api@npm:3.0.1" - dependencies: - "@types/debug": ^4.1.4 - "@types/express": ^4.17.0 - "@types/lodash.isstring": ^4.0.6 - "@types/node": ">=12.13.0 < 13" - "@types/yargs": ^15.0.4 - debug: ^2.6.1 - express: ^4.0.0 - lodash.isstring: ^4.0.1 +"@slack/bolt@npm:^3.22.0": + version: 3.22.0 + resolution: "@slack/bolt@npm:3.22.0" + dependencies: + "@slack/logger": ^4.0.0 + "@slack/oauth": ^2.6.3 + "@slack/socket-mode": ^1.3.6 + "@slack/types": ^2.13.0 + "@slack/web-api": ^6.13.0 + "@types/express": ^4.16.1 + "@types/promise.allsettled": ^1.0.3 + "@types/tsscmp": ^1.0.0 + axios: ^1.7.4 + express: ^4.21.0 + path-to-regexp: ^8.1.0 + promise.allsettled: ^1.0.2 raw-body: ^2.3.3 tsscmp: ^1.0.6 - yargs: ^15.3.1 - dependenciesMeta: - express: - optional: true - bin: - slack-verify: dist/verify.js - checksum: ce62dc2ee9dd93b88820e18f88f543228740243dc390caf49b3a7e1ad351b298e3961898bd78f5eb43e9f6acac067458257cd34c9661089f684bb5cf4af468c3 + checksum: edd5c7cf658808effde87c936f19a0cc2b7d49ac97471651f2b1bb3db0074b92dc8ad3c9657577105d93c48df9ba16c382902c0d90082854cbbe86bfc7753827 + languageName: node + linkType: hard + +"@slack/logger@npm:^3.0.0": + version: 3.0.0 + resolution: "@slack/logger@npm:3.0.0" + dependencies: + "@types/node": ">=12.0.0" + checksum: 6512d0e9e4be47ea465705ab9b6e6901f36fa981da0d4a657fde649d452b567b351002049b5ee0a22569b5119bf6c2f61befd5b8022d878addb7a99c91b03389 languageName: node linkType: hard @@ -4849,30 +4856,58 @@ __metadata: languageName: node linkType: hard -"@slack/types@npm:^2.17.0": - version: 2.17.0 - resolution: "@slack/types@npm:2.17.0" - checksum: 57cad4b3153589707fef50c7a231921364d10d8f4a3e4d342c718d4aa69f5b3541fee686e8b2d93d46dd4c9842adebe5d3bae6530e4ad8719c1ec5d46b0ec157 +"@slack/oauth@npm:^2.6.3": + version: 2.6.3 + resolution: "@slack/oauth@npm:2.6.3" + dependencies: + "@slack/logger": ^3.0.0 + "@slack/web-api": ^6.12.1 + "@types/jsonwebtoken": ^8.3.7 + "@types/node": ">=12" + jsonwebtoken: ^9.0.0 + lodash.isstring: ^4.0.1 + checksum: 6b556da01bd2b026177b4074cd44bdeff00165fb4297ef8f350035ca79ababfff0c0993a297a46ab742bb97469c6c1c8f5790c328ecf6370290fe31014ba3c5e languageName: node linkType: hard -"@slack/web-api@npm:^7.8.0": - version: 7.11.0 - resolution: "@slack/web-api@npm:7.11.0" +"@slack/socket-mode@npm:^1.3.6": + version: 1.3.6 + resolution: "@slack/socket-mode@npm:1.3.6" dependencies: - "@slack/logger": ^4.0.0 - "@slack/types": ^2.17.0 - "@types/node": ">=18.0.0" - "@types/retry": 0.12.0 - axios: ^1.11.0 - eventemitter3: ^5.0.1 - form-data: ^4.0.4 + "@slack/logger": ^3.0.0 + "@slack/web-api": ^6.12.1 + "@types/node": ">=12.0.0" + "@types/ws": ^7.4.7 + eventemitter3: ^5 + finity: ^0.5.4 + ws: ^7.5.3 + checksum: a84c15a6d25a21f76258d1ccebeec1d78b0a0dac0b02ffdfcb3596e7acda5459e4b99a42207eab7e57bed7a2a1d85ac173adf5e07aa66949eac9cc9df3b43947 + languageName: node + linkType: hard + +"@slack/types@npm:^2.11.0, @slack/types@npm:^2.13.0": + version: 2.18.0 + resolution: "@slack/types@npm:2.18.0" + checksum: c425b528924be74fb8e8de0e1883199b088ce427a8d7217998ee77c0cce817464be102383801e142ab531430b7fd1a1f9b9deed2b7a9e3df1c67a24a1d7348a1 + languageName: node + linkType: hard + +"@slack/web-api@npm:^6.12.1, @slack/web-api@npm:^6.13.0": + version: 6.13.0 + resolution: "@slack/web-api@npm:6.13.0" + dependencies: + "@slack/logger": ^3.0.0 + "@slack/types": ^2.11.0 + "@types/is-stream": ^1.1.0 + "@types/node": ">=12.0.0" + axios: ^1.7.4 + eventemitter3: ^3.1.0 + form-data: ^2.5.0 is-electron: 2.2.2 - is-stream: ^2 - p-queue: ^6 - p-retry: ^4 - retry: ^0.13.1 - checksum: 52c26b169111d15a6ef0701b947291ee97a8f4e795f9ee509ec90c56a4ef72f9776a3d35e54d7fa329b0479e7f193539e2e095b3ee0704a775bf66d960dfacf0 + is-stream: ^1.1.0 + p-queue: ^6.6.1 + p-retry: ^4.0.0 + checksum: 77f0d506bbb011ae43d322e5152e8b1ec2b88aa01256da6b3c9ff8ce106d2284f887cad2d9f044e0fe34dc865d60f2bce1c6bb5c4117150ff71a7ef341f5dfeb languageName: node linkType: hard @@ -5853,7 +5888,7 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.4": +"@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" dependencies: @@ -5976,7 +6011,19 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:^4.17.0, @types/express@npm:^4.17.13": +"@types/express@npm:^4.16.1": + version: 4.17.25 + resolution: "@types/express@npm:4.17.25" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.33 + "@types/qs": "*" + "@types/serve-static": ^1 + checksum: 285d16008489d37b2be03e2e050bcf201d5d6ed9278ca13619d9029efd2055b192b2445f769116f716cfcf53d9d799a03f4e76199af9cea0ea3dee3d88595931 + languageName: node + linkType: hard + +"@types/express@npm:^4.17.13": version: 4.17.23 resolution: "@types/express@npm:4.17.23" dependencies: @@ -6043,6 +6090,15 @@ __metadata: languageName: node linkType: hard +"@types/is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "@types/is-stream@npm:1.1.0" + dependencies: + "@types/node": "*" + checksum: 23fcb06cd8adc0124d4c44071bd4b447c41f5e4c2eccb6166789c7fc0992b566e2e8b628a3800ff4472b686d9085adbec203925068bf72e350e085650e83adec + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -6092,7 +6148,7 @@ __metadata: languageName: node linkType: hard -"@types/jsonwebtoken@npm:^8.5.8, @types/jsonwebtoken@npm:^8.5.9": +"@types/jsonwebtoken@npm:^8.3.7, @types/jsonwebtoken@npm:^8.5.8, @types/jsonwebtoken@npm:^8.5.9": version: 8.5.9 resolution: "@types/jsonwebtoken@npm:8.5.9" dependencies: @@ -6101,22 +6157,6 @@ __metadata: languageName: node linkType: hard -"@types/lodash.isstring@npm:^4.0.6": - version: 4.0.9 - resolution: "@types/lodash.isstring@npm:4.0.9" - dependencies: - "@types/lodash": "*" - checksum: ef381be69b459caa42d7c5dc4ff5b3653e6b3c9b2393f6e92848efeafe7690438e058b26f036b11b4e535fc7645ff12d1203847b9a82e9ae0593bdd3b25a971b - languageName: node - linkType: hard - -"@types/lodash@npm:*": - version: 4.17.20 - resolution: "@types/lodash@npm:4.17.20" - checksum: dc7bb4653514dd91117a4c4cec2c37e2b5a163d7643445e4757d76a360fabe064422ec7a42dde7450c5e7e0e7e678d5e6eae6d2a919abcddf581d81e63e63839 - languageName: node - linkType: hard - "@types/mdast@npm:^4.0.0": version: 4.0.4 resolution: "@types/mdast@npm:4.0.4" @@ -6188,10 +6228,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:>=12.13.0 < 13": - version: 12.20.55 - resolution: "@types/node@npm:12.20.55" - checksum: e4f86785f4092706e0d3b0edff8dca5a13b45627e4b36700acd8dfe6ad53db71928c8dee914d4276c7fd3b6ccd829aa919811c9eb708a2c8e4c6eb3701178c37 +"@types/node@npm:>=12, @types/node@npm:>=12.0.0": + version: 24.9.2 + resolution: "@types/node@npm:24.9.2" + dependencies: + undici-types: ~7.16.0 + checksum: 6f1d2c66ce14ef58934c7140b8b7003b3e55fc3b23128bfdabdf59a02f4ff4dbb89a58cd95cc11310cce6c6ffeb5cacc3afaa8753d4a9cd4afdc447a6ab61bee languageName: node linkType: hard @@ -6228,6 +6270,13 @@ __metadata: languageName: node linkType: hard +"@types/promise.allsettled@npm:^1.0.3": + version: 1.0.6 + resolution: "@types/promise.allsettled@npm:1.0.6" + checksum: 07dca8da25b49c0dc323201095552d86159483dc910dc61c345357c9c196b8498e6be4bf260cc2a9a539a725108df61b53db1d82723ed9886bb7c72fedd65f14 + languageName: node + linkType: hard + "@types/prop-types@npm:*, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.14, @types/prop-types@npm:^15.7.15": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" @@ -6385,6 +6434,17 @@ __metadata: languageName: node linkType: hard +"@types/serve-static@npm:^1": + version: 1.15.10 + resolution: "@types/serve-static@npm:1.15.10" + dependencies: + "@types/http-errors": "*" + "@types/node": "*" + "@types/send": <1 + checksum: f216eef2aaf2c8eff09f431c420c5c2989eaf0dfc15d106db9fb64c14577a4059af24fb0ae2eba7984d6360950c8cbc1fb52f65608106477729d251481bc96fe + languageName: node + linkType: hard + "@types/sockjs@npm:^0.3.33": version: 0.3.36 resolution: "@types/sockjs@npm:0.3.36" @@ -6436,6 +6496,13 @@ __metadata: languageName: node linkType: hard +"@types/tsscmp@npm:^1.0.0": + version: 1.0.2 + resolution: "@types/tsscmp@npm:1.0.2" + checksum: c02c0bb9f14f550947fea9fa6f9f3c28e6b2d47a6d049a5450ed466fb0c8a685b6ff37d070d4c43d930a5affc9d828f5e16e35cde1e734de228ffd2df76ac2a8 + languageName: node + linkType: hard + "@types/unist@npm:*, @types/unist@npm:^3.0.0": version: 3.0.3 resolution: "@types/unist@npm:3.0.3" @@ -6464,6 +6531,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^7.4.7": + version: 7.4.7 + resolution: "@types/ws@npm:7.4.7" + dependencies: + "@types/node": "*" + checksum: b4c9b8ad209620c9b21e78314ce4ff07515c0cadab9af101c1651e7bfb992d7fd933bd8b9c99d110738fd6db523ed15f82f29f50b45510288da72e964dedb1a3 + languageName: node + linkType: hard + "@types/ws@npm:^8.5.5": version: 8.18.1 resolution: "@types/ws@npm:8.18.1" @@ -6480,15 +6556,6 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^15.0.4": - version: 15.0.19 - resolution: "@types/yargs@npm:15.0.19" - dependencies: - "@types/yargs-parser": "*" - checksum: 6a509db36304825674f4f00300323dce2b4d850e75819c3db87e9e9f213ac2c4c6ed3247a3e4eed6e8e45b3f191b133a356d3391dd694d9ea27a0507d914ef4c - languageName: node - linkType: hard - "@types/yargs@npm:^16.0.0": version: 16.0.9 resolution: "@types/yargs@npm:16.0.9" @@ -7533,6 +7600,21 @@ __metadata: languageName: node linkType: hard +"array.prototype.map@npm:^1.0.5": + version: 1.0.8 + resolution: "array.prototype.map@npm:1.0.8" + dependencies: + call-bind: ^1.0.8 + call-bound: ^1.0.3 + define-properties: ^1.2.1 + es-abstract: ^1.23.6 + es-array-method-boxes-properly: ^1.0.0 + es-object-atoms: ^1.0.0 + is-string: ^1.1.1 + checksum: df321613636ec8461965d72421569ece78f269460535ced5ec88db9aaa4fc58a9f26e597d72e726f105c55fa4b4b6db0d3156489dc13dfbc7a098b4f1d17b5ab + languageName: node + linkType: hard + "array.prototype.reduce@npm:^1.0.6": version: 1.0.8 resolution: "array.prototype.reduce@npm:1.0.8" @@ -7681,7 +7763,18 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.11.0, axios@npm:^1.7.9": +"axios@npm:^1.7.4": + version: 1.13.1 + resolution: "axios@npm:1.13.1" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.4 + proxy-from-env: ^1.1.0 + checksum: fd34e26d22adaba5ce59b02963ecc4f7a6a4a44950014512f3f86dde10ab30df377dd10260ea9d36aafe9f1f87191a95f5b50c3979485be50f10b465c7b1a164 + languageName: node + linkType: hard + +"axios@npm:^1.7.9": version: 1.12.2 resolution: "axios@npm:1.12.2" dependencies: @@ -7887,8 +7980,7 @@ __metadata: resolution: "backend@workspace:src/backend" dependencies: "@prisma/client": ^6.2.1 - "@slack/events-api": ^3.0.1 - "@slack/web-api": ^7.8.0 + "@slack/bolt": ^3.22.0 "@types/concat-stream": ^2.0.0 "@types/cookie-parser": ^1.4.3 "@types/cors": ^2.8.12 @@ -8284,7 +8376,7 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": +"call-bind@npm:^1.0.2, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": version: 1.0.8 resolution: "call-bind@npm:1.0.8" dependencies: @@ -8330,7 +8422,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": +"camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b @@ -8629,17 +8721,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^6.0.0": - version: 6.0.0 - resolution: "cliui@npm:6.0.0" - dependencies: - string-width: ^4.2.0 - strip-ansi: ^6.0.0 - wrap-ansi: ^6.2.0 - checksum: 4fcfd26d292c9f00238117f39fc797608292ae36bac2168cfee4c85923817d0607fe21b3329a8621e01aedf512c99b7eaa60e363a671ffd378df6649fb48ae42 - languageName: node - linkType: hard - "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -9565,7 +9646,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.1": +"debug@npm:2.6.9, debug@npm:^2.6.0": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -9595,13 +9676,6 @@ __metadata: languageName: node linkType: hard -"decamelize@npm:^1.2.0": - version: 1.2.0 - resolution: "decamelize@npm:1.2.0" - checksum: ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa - languageName: node - linkType: hard - "decimal.js-light@npm:^2.4.1": version: 2.5.1 resolution: "decimal.js-light@npm:2.5.1" @@ -9703,7 +9777,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -10236,7 +10310,7 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.2, es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": +"es-abstract@npm:^1.17.2, es-abstract@npm:^1.17.5, es-abstract@npm:^1.22.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": version: 1.24.0 resolution: "es-abstract@npm:1.24.0" dependencies: @@ -10319,6 +10393,23 @@ __metadata: languageName: node linkType: hard +"es-get-iterator@npm:^1.0.2": + version: 1.1.3 + resolution: "es-get-iterator@npm:1.1.3" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.1.3 + has-symbols: ^1.0.3 + is-arguments: ^1.1.1 + is-map: ^2.0.2 + is-set: ^2.0.2 + is-string: ^1.0.7 + isarray: ^2.0.5 + stop-iteration-iterator: ^1.0.0 + checksum: 8fa118da42667a01a7c7529f8a8cca514feeff243feec1ce0bb73baaa3514560bd09d2b3438873cf8a5aaec5d52da248131de153b28e2638a061b6e4df13267d + languageName: node + linkType: hard + "es-iterator-helpers@npm:^1.2.1": version: 1.2.1 resolution: "es-iterator-helpers@npm:1.2.1" @@ -11299,6 +11390,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^3.1.0": + version: 3.1.2 + resolution: "eventemitter3@npm:3.1.2" + checksum: 81e4e82b8418f5cfd986d2b4a2fa5397ac4eb8134e09bcb47005545e22fdf8e9e61d5c053d34651112245aae411bdfe6d0ad5511da0400743fef5fc38bfcfbe3 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1, eventemitter3@npm:^4.0.4": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -11306,7 +11404,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^5.0.1": +"eventemitter3@npm:^5": version: 5.0.1 resolution: "eventemitter3@npm:5.0.1" checksum: 543d6c858ab699303c3c32e0f0f47fc64d360bf73c3daf0ac0b5079710e340d6fe9f15487f94e66c629f5f82cd1a8678d692f3dbb6f6fcd1190e1b97fcad36f8 @@ -11418,7 +11516,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.0.0, express@npm:^4.17.3": +"express@npm:^4.17.3, express@npm:^4.21.0": version: 4.21.2 resolution: "express@npm:4.21.2" dependencies: @@ -11795,6 +11893,13 @@ __metadata: languageName: unknown linkType: soft +"finity@npm:^0.5.4": + version: 0.5.4 + resolution: "finity@npm:0.5.4" + checksum: eeea74de356ba963231108c3f8e2de44b4114497389121d603f8c3e8316b8d0772ff06b731af08ef5d6ca6b0e3a0fffab452122eca48837a98a2f7e5548b6be2 + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.2.0 resolution: "flat-cache@npm:3.2.0" @@ -11873,6 +11978,20 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^2.5.0": + version: 2.5.5 + resolution: "form-data@npm:2.5.5" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + es-set-tostringtag: ^2.1.0 + hasown: ^2.0.2 + mime-types: ^2.1.35 + safe-buffer: ^5.2.1 + checksum: ba6d8467f959c9bf36a52e423256c1e8055a8e650416760f54fa5db261529c3de698a4ce8378dd4fdb71b44be190906d6b73446556cc74e58de8bda01d09e9e7 + languageName: node + linkType: hard + "form-data@npm:^3.0.0": version: 3.0.4 resolution: "form-data@npm:3.0.4" @@ -12184,14 +12303,14 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": version: 1.3.1 resolution: "get-intrinsic@npm:1.3.1" dependencies: @@ -13144,6 +13263,16 @@ __metadata: languageName: node linkType: hard +"is-arguments@npm:^1.1.1": + version: 1.2.0 + resolution: "is-arguments@npm:1.2.0" + dependencies: + call-bound: ^1.0.2 + has-tostringtag: ^1.0.2 + checksum: aae9307fedfe2e5be14aebd0f48a9eeedf6b8c8f5a0b66257b965146d1e94abdc3f08e3dce3b1d908e1fa23c70039a88810ee1d753905758b9b6eebbab0bafeb + languageName: node + linkType: hard + "is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": version: 3.0.5 resolution: "is-array-buffer@npm:3.0.5" @@ -13322,7 +13451,7 @@ __metadata: languageName: node linkType: hard -"is-map@npm:^2.0.3": +"is-map@npm:^2.0.2, is-map@npm:^2.0.3": version: 2.0.3 resolution: "is-map@npm:2.0.3" checksum: e6ce5f6380f32b141b3153e6ba9074892bbbbd655e92e7ba5ff195239777e767a976dcd4e22f864accaf30e53ebf961ab1995424aef91af68788f0591b7396cc @@ -13435,7 +13564,7 @@ __metadata: languageName: node linkType: hard -"is-set@npm:^2.0.3": +"is-set@npm:^2.0.2, is-set@npm:^2.0.3": version: 2.0.3 resolution: "is-set@npm:2.0.3" checksum: 36e3f8c44bdbe9496c9689762cc4110f6a6a12b767c5d74c0398176aa2678d4467e3bf07595556f2dba897751bde1422480212b97d973c7b08a343100b0c0dfe @@ -13451,14 +13580,21 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2, is-stream@npm:^2.0.0": +"is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 063c6bec9d5647aa6d42108d4c59723d2bd4ae42135a2d4db6eadbd49b7ea05b750fd69d279e5c7c45cf9da753ad2c00d8978be354d65aa9f6bb434969c6a2ae + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 languageName: node linkType: hard -"is-string@npm:^1.1.1": +"is-string@npm:^1.0.7, is-string@npm:^1.1.1": version: 1.1.1 resolution: "is-string@npm:1.1.1" dependencies: @@ -13617,6 +13753,23 @@ __metadata: languageName: node linkType: hard +"iterate-iterator@npm:^1.0.1": + version: 1.0.2 + resolution: "iterate-iterator@npm:1.0.2" + checksum: 97b3ed4f2bebe038be57d03277879e406b2c537ceeeab7f82d4167f9a3cff872cc2cc5da3dc9920ff544ca247329d2a4d44121bb8ef8d0807a72176bdbc17c84 + languageName: node + linkType: hard + +"iterate-value@npm:^1.0.2": + version: 1.0.2 + resolution: "iterate-value@npm:1.0.2" + dependencies: + es-get-iterator: ^1.0.2 + iterate-iterator: ^1.0.1 + checksum: 446a4181657df1872e5020713206806757157db6ab375dee05eb4565b66e1244d7a99cd36ce06862261ad4bd059e66ba8192f62b5d1ff41d788c3b61953af6c3 + languageName: node + linkType: hard + "iterator.prototype@npm:^1.1.4": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" @@ -14555,7 +14708,7 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^9.0.2": +"jsonwebtoken@npm:^9.0.0, jsonwebtoken@npm:^9.0.2": version: 9.0.2 resolution: "jsonwebtoken@npm:9.0.2" dependencies: @@ -16411,7 +16564,7 @@ __metadata: languageName: node linkType: hard -"p-queue@npm:^6": +"p-queue@npm:^6.6.1": version: 6.6.2 resolution: "p-queue@npm:6.6.2" dependencies: @@ -16421,7 +16574,7 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:^4, p-retry@npm:^4.5.0": +"p-retry@npm:^4.0.0, p-retry@npm:^4.5.0": version: 4.6.2 resolution: "p-retry@npm:4.6.2" dependencies: @@ -16619,7 +16772,7 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^8.0.0": +"path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.1.0": version: 8.3.0 resolution: "path-to-regexp@npm:8.3.0" checksum: 73e0d3db449f9899692b10be8480bbcfa294fd575be2d09bce3e63f2f708d1fccd3aaa8591709f8b82062c528df116e118ff9df8f5c52ccc4c2443a90be73e10 @@ -17807,6 +17960,20 @@ __metadata: languageName: node linkType: hard +"promise.allsettled@npm:^1.0.2": + version: 1.0.7 + resolution: "promise.allsettled@npm:1.0.7" + dependencies: + array.prototype.map: ^1.0.5 + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + get-intrinsic: ^1.2.1 + iterate-value: ^1.0.2 + checksum: 96186392286e5ab9aef1a1a725c061c8cf268b6cf141f151daa3834bb8e1680f3b159af6536ce59cf80d4a6a5ad1d8371d05759980cc6c90d58800ddb0a7c119 + languageName: node + linkType: hard + "promise@npm:^8.1.0": version: 8.3.0 resolution: "promise@npm:8.3.0" @@ -18769,13 +18936,6 @@ __metadata: languageName: node linkType: hard -"require-main-filename@npm:^2.0.0": - version: 2.0.0 - resolution: "require-main-filename@npm:2.0.0" - checksum: e9e294695fea08b076457e9ddff854e81bffbe248ed34c1eec348b7abbd22a0d02e8d75506559e2265e96978f3c4720bd77a6dad84755de8162b357eb6c778c7 - languageName: node - linkType: hard - "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -19118,7 +19278,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -19438,13 +19598,6 @@ __metadata: languageName: node linkType: hard -"set-blocking@npm:^2.0.0": - version: 2.0.0 - resolution: "set-blocking@npm:2.0.0" - checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 - languageName: node - linkType: hard - "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -19897,7 +20050,7 @@ __metadata: languageName: node linkType: hard -"stop-iteration-iterator@npm:^1.1.0": +"stop-iteration-iterator@npm:^1.0.0, stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" dependencies: @@ -21220,6 +21373,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 1ef68fc6c5bad200c8b6f17de8e5bc5cfdcadc164ba8d7208cd087cfa8583d922d8316a7fd76c9a658c22b4123d3ff847429185094484fbc65377d695c905857 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.1 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" @@ -22251,13 +22411,6 @@ __metadata: languageName: node linkType: hard -"which-module@npm:^2.0.0": - version: 2.0.1 - resolution: "which-module@npm:2.0.1" - checksum: 1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be - languageName: node - linkType: hard - "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" @@ -22582,7 +22735,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.4.6": +"ws@npm:^7.4.6, ws@npm:^7.5.3": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: @@ -22633,13 +22786,6 @@ __metadata: languageName: node linkType: hard -"y18n@npm:^4.0.0": - version: 4.0.3 - resolution: "y18n@npm:4.0.3" - checksum: 014dfcd9b5f4105c3bb397c1c8c6429a9df004aa560964fb36732bfb999bfe83d45ae40aeda5b55d21b1ee53d8291580a32a756a443e064317953f08025b1aa4 - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -22675,16 +22821,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^18.1.2": - version: 18.1.3 - resolution: "yargs-parser@npm:18.1.3" - dependencies: - camelcase: ^5.0.0 - decamelize: ^1.2.0 - checksum: 60e8c7d1b85814594d3719300ecad4e6ae3796748b0926137bfec1f3042581b8646d67e83c6fc80a692ef08b8390f21ddcacb9464476c39bbdf52e34961dd4d9 - languageName: node - linkType: hard - "yargs-parser@npm:^20.2.2": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -22714,25 +22850,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^15.3.1": - version: 15.4.1 - resolution: "yargs@npm:15.4.1" - dependencies: - cliui: ^6.0.0 - decamelize: ^1.2.0 - find-up: ^4.1.0 - get-caller-file: ^2.0.1 - require-directory: ^2.1.1 - require-main-filename: ^2.0.0 - set-blocking: ^2.0.0 - string-width: ^4.2.0 - which-module: ^2.0.0 - y18n: ^4.0.0 - yargs-parser: ^18.1.2 - checksum: 40b974f508d8aed28598087720e086ecd32a5fd3e945e95ea4457da04ee9bdb8bdd17fd91acff36dc5b7f0595a735929c514c40c402416bbb87c03f6fb782373 - languageName: node - linkType: hard - "yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0"