diff --git a/.github/mergify.yml b/.github/mergify.yml index b47bba2590..0a53bc3d3d 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -6,8 +6,6 @@ pull_request_rules: actions: review: type: APPROVE - merge: - method: squash - name: Approve and merge non-major Snyk.io upgrades conditions: - author=snyk-bot diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba57033877..481b6bfd10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -176,9 +176,10 @@ jobs: NODE_OPTIONS: --max-old-space-size=4096 -r ${{ env.DD_TRACE_PACKAGE }} AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE: 1 - name: Coveralls - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} + allow-empty: false backend_lint: needs: [changes, install] diff --git a/CHANGELOG.md b/CHANGELOG.md index e5edc35c8f..12fb983bed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,41 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v6.149.0](https://github.com/opengovsg/FormSG/compare/v6.148.0...v6.149.0) + +- build: merge release v6.148.0 to develop [`#7724`](https://github.com/opengovsg/FormSG/pull/7724) +- feat: approvals for mrf [`#7636`](https://github.com/opengovsg/FormSG/pull/7636) +- fix: coverall allow empty lcov [`#7722`](https://github.com/opengovsg/FormSG/pull/7722) +- chore: remove p-queue from be [`#7693`](https://github.com/opengovsg/FormSG/pull/7693) +- chore: add comments clarifying potentially confusing terms [`#7694`](https://github.com/opengovsg/FormSG/pull/7694) +- ci(Mergify): configuration update [`#7720`](https://github.com/opengovsg/FormSG/pull/7720) +- chore: Revert "chore(deps-dev): bump jest-environment-jsdom from 29.5.0 to 29.7.0" [`#7719`](https://github.com/opengovsg/FormSG/pull/7719) +- feat(tracking): mrf workflow + creation [`#7704`](https://github.com/opengovsg/FormSG/pull/7704) +- chore(deps-dev): bump @types/lodash from 4.17.7 to 4.17.9 in /shared [`#7717`](https://github.com/opengovsg/FormSG/pull/7717) +- fix(deps): bump libphonenumber-js from 1.11.8 to 1.11.9 in /shared [`#7713`](https://github.com/opengovsg/FormSG/pull/7713) +- chore(deps-dev): bump @babel/preset-env from 7.25.3 to 7.25.4 [`#7711`](https://github.com/opengovsg/FormSG/pull/7711) +- fix(deps): bump express-rate-limit from 7.2.0 to 7.4.0 [`#7710`](https://github.com/opengovsg/FormSG/pull/7710) +- chore(deps-dev): bump @types/busboy from 1.5.3 to 1.5.4 [`#7709`](https://github.com/opengovsg/FormSG/pull/7709) +- chore(deps-dev): bump @stoplight/prism-cli from 5.5.4 to 5.10.0 [`#7708`](https://github.com/opengovsg/FormSG/pull/7708) +- chore(deps-dev): bump jest-environment-jsdom from 29.5.0 to 29.7.0 [`#7707`](https://github.com/opengovsg/FormSG/pull/7707) +- chore(deps-dev): bump eslint-plugin-prettier from 5.1.3 to 5.2.1 [`#7706`](https://github.com/opengovsg/FormSG/pull/7706) +- fix(deps): bump aws-sdk from 2.1659.0 to 2.1691.0 [`#7703`](https://github.com/opengovsg/FormSG/pull/7703) +- fix(deps): jest extended upgrade to 4.0.2 [`#7698`](https://github.com/opengovsg/FormSG/pull/7698) +- fix(deps): bump dotenv from 16.0.3 to 16.4.5 [`#7702`](https://github.com/opengovsg/FormSG/pull/7702) +- fix(deps): bump @aws-sdk/client-lambda from 3.414.0 to 3.654.0 [`#7701`](https://github.com/opengovsg/FormSG/pull/7701) +- fix(deps): bump axios from 1.7.4 to 1.7.7 [`#7699`](https://github.com/opengovsg/FormSG/pull/7699) +- fix(deps): bump opossum and @types/opossum [`#7593`](https://github.com/opengovsg/FormSG/pull/7593) +- fix(deps): bump neverthrow from 6.1.0 to 8.0.0 [`#7697`](https://github.com/opengovsg/FormSG/pull/7697) +- chore(deps-dev): bump @types/dedent from 0.7.0 to 0.7.2 [`#7696`](https://github.com/opengovsg/FormSG/pull/7696) +- fix(deps): bump @opengovsg/sgid-client from 2.0.0 to 2.2.0 [`#7687`](https://github.com/opengovsg/FormSG/pull/7687) +- chore: remove unused deps [`#7685`](https://github.com/opengovsg/FormSG/pull/7685) +- fix: chromatic build failure [`#7684`](https://github.com/opengovsg/FormSG/pull/7684) +- build: release v6.148.0 [`#7683`](https://github.com/opengovsg/FormSG/pull/7683) + #### [v6.148.0](https://github.com/opengovsg/FormSG/compare/v6.147.1...v6.148.0) +> 16 September 2024 + - build: merge release v6.147.1 to develop [`#7681`](https://github.com/opengovsg/FormSG/pull/7681) - chore: remove unused deps [`#7676`](https://github.com/opengovsg/FormSG/pull/7676) - feat: add betaflag on submission key [`#7677`](https://github.com/opengovsg/FormSG/pull/7677) @@ -15,6 +48,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps): bump axios from 1.6.5 to 1.7.7 in /frontend [`#7674`](https://github.com/opengovsg/FormSG/pull/7674) - chore(deps-dev): bump husky from 8.0.3 to 9.1.6 [`#7673`](https://github.com/opengovsg/FormSG/pull/7673) - build: release 6.174.1 [`#7672`](https://github.com/opengovsg/FormSG/pull/7672) +- chore: bump version to v6.148.0 [`ae76291`](https://github.com/opengovsg/FormSG/commit/ae762918dffa3f0bcba0e0d25dd5e5c5623e435c) #### [v6.147.1](https://github.com/opengovsg/FormSG/compare/v6.147.0...v6.147.1) diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 35274e4b01..9a3a5a228d 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -37,6 +37,32 @@ module.exports = { test: /\.mjs$/, include: /node_modules/, }, + { + // Added to fix ticket: + // https://linear.app/ogp/issue/FRM-1857/fix-chromatic-build-failure-due-es2018-features-not-supported + test: /\.(?:jsx?|tsx?|vue)$/, + use: [ + { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + [ + '@babel/preset-react', + { + runtime: 'automatic', + }, + ], + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-transform-async-to-generator', + '@babel/plugin-proposal-async-generator-functions', + ], + }, + }, + ], + }, ], }, } diff --git a/frontend/datadog-chunk.ts b/frontend/datadog-chunk.ts index dc85cc3781..b757636654 100644 --- a/frontend/datadog-chunk.ts +++ b/frontend/datadog-chunk.ts @@ -6,14 +6,9 @@ import { datadogRum, RumInitConfiguration } from '@datadog/browser-rum' // Discard benign RUM errors. -export const ddBeforeSend: RumInitConfiguration['beforeSend'] = (event) => { - // TODO(#4279): Might want to remove this once we are fully React, since then we will not need to check auth state. - // Discard unauth'd errors - if (event.type === 'resource' && event.resource.status_code === 404) { - return false - } - - if (event.type !== 'error') return +// Ensure that beforeSend returns true to keep the event and false to discard it. +const ddBeforeSend: RumInitConfiguration['beforeSend'] = (event) => { + if (event.type !== 'error') return true // Caused by @chakra-ui/react@latest-v1 -> @chakra-ui/modal@1.11.1 -> react-remove-scroll@2.4.1 // Already fixed in @chakra-ui/react@latest, but we cannot upgrade until we upgrade to React 18. @@ -27,6 +22,8 @@ export const ddBeforeSend: RumInitConfiguration['beforeSend'] = (event) => { if (event.error.message.includes('ResizeObserver loop limit exceeded')) { return false } + + return true } // Init Datadog RUM @@ -41,10 +38,9 @@ datadogRum.init({ // Specify a version number to identify the deployed version of your application in Datadog version: '@REACT_APP_VERSION', - // TODO/RUM: Update these RUM percentages as we increase the rollout percentage! - sampleRate: Number('@REACT_APP_DD_SAMPLE_RATE') || 5, - replaySampleRate: 100, - trackInteractions: true, + sessionSampleRate: Number('@REACT_APP_DD_SAMPLE_RATE') || 5, + sessionReplaySampleRate: 100, + trackUserInteractions: true, defaultPrivacyLevel: 'mask-user-input', beforeSend: ddBeforeSend, }) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e177fdc8ac..1684666bf9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,17 +1,17 @@ { "name": "form-frontend", - "version": "6.148.0", + "version": "6.149.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "form-frontend", - "version": "6.148.0", + "version": "6.149.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^1.8.6", "@datadog/browser-logs": "^4.50.1", - "@datadog/browser-rum": "^4.34.1", + "@datadog/browser-rum": "^5.27.0", "@emotion/react": "^11.7.0", "@emotion/styled": "^11.6.0", "@floating-ui/react-dom-interactions": "^0.9.3", @@ -3272,9 +3272,9 @@ } }, "node_modules/@datadog/browser-core": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-4.34.1.tgz", - "integrity": "sha512-xZUMxSEUlzXVGU8fvIG5J4xGGBShrxgTTmCd9p7Ju/rUPnWV0hTqhPIXwSgbMvHgmX1vNwoNvydmlzhNVpKr3g==" + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-5.27.0.tgz", + "integrity": "sha512-lgc2cTZFUNWaeTLIK6laXoTRIycSHcQWMDWhAkdROsqbXK5DEHMagKqn3WGBCFN7uEnnohLPBSzmbZP4wwQfJA==" }, "node_modules/@datadog/browser-logs": { "version": "4.50.1", @@ -3298,15 +3298,15 @@ "integrity": "sha512-2ypS19XngsMu6W4qUBtDwvImFz886Im+PziOnEycO1w41TVS5LH8/vWBMvjSf8Suer+CeRjRN9IOu0ocRx9BVw==" }, "node_modules/@datadog/browser-rum": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-4.34.1.tgz", - "integrity": "sha512-fKw1EUwlPNliUJnOftsIgNGbW70+pZGwt2BbiUsWxp+CBHsw+MRFENUJybAK2l4PqNm+AZKVkvultiCDGvo3gg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-5.27.0.tgz", + "integrity": "sha512-8tCHJ6yU6O91fcJmQL29nRj9yEqxIMcJdP6eMziXRJhCrpQqz9c34FwVyrY9KfJG4KBnvakNV53aVE6AOOfhSA==", "dependencies": { - "@datadog/browser-core": "4.34.1", - "@datadog/browser-rum-core": "4.34.1" + "@datadog/browser-core": "5.27.0", + "@datadog/browser-rum-core": "5.27.0" }, "peerDependencies": { - "@datadog/browser-logs": "4.34.1" + "@datadog/browser-logs": "5.27.0" }, "peerDependenciesMeta": { "@datadog/browser-logs": { @@ -3315,11 +3315,11 @@ } }, "node_modules/@datadog/browser-rum-core": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-4.34.1.tgz", - "integrity": "sha512-s1nHAXREKRlTl2EOn+J/786D7pvQ2lg9MunhmC5Juh9DeNrYrmAooxbrXCaCy0gKRBHbm68HM4h59FXuA5iNGg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-5.27.0.tgz", + "integrity": "sha512-YFKnoglFnujAGgefXl+7NUmnpmlox9bJ3wYK7AFje2mh6O6MtquyLHHkNJI9lGLEmvdjubipgF0Fh45S8bIUHw==", "dependencies": { - "@datadog/browser-core": "4.34.1" + "@datadog/browser-core": "5.27.0" } }, "node_modules/@design-systems/utils": { @@ -52165,9 +52165,9 @@ "integrity": "sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==" }, "@datadog/browser-core": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-4.34.1.tgz", - "integrity": "sha512-xZUMxSEUlzXVGU8fvIG5J4xGGBShrxgTTmCd9p7Ju/rUPnWV0hTqhPIXwSgbMvHgmX1vNwoNvydmlzhNVpKr3g==" + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-5.27.0.tgz", + "integrity": "sha512-lgc2cTZFUNWaeTLIK6laXoTRIycSHcQWMDWhAkdROsqbXK5DEHMagKqn3WGBCFN7uEnnohLPBSzmbZP4wwQfJA==" }, "@datadog/browser-logs": { "version": "4.50.1", @@ -52185,20 +52185,20 @@ } }, "@datadog/browser-rum": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-4.34.1.tgz", - "integrity": "sha512-fKw1EUwlPNliUJnOftsIgNGbW70+pZGwt2BbiUsWxp+CBHsw+MRFENUJybAK2l4PqNm+AZKVkvultiCDGvo3gg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-5.27.0.tgz", + "integrity": "sha512-8tCHJ6yU6O91fcJmQL29nRj9yEqxIMcJdP6eMziXRJhCrpQqz9c34FwVyrY9KfJG4KBnvakNV53aVE6AOOfhSA==", "requires": { - "@datadog/browser-core": "4.34.1", - "@datadog/browser-rum-core": "4.34.1" + "@datadog/browser-core": "5.27.0", + "@datadog/browser-rum-core": "5.27.0" } }, "@datadog/browser-rum-core": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-4.34.1.tgz", - "integrity": "sha512-s1nHAXREKRlTl2EOn+J/786D7pvQ2lg9MunhmC5Juh9DeNrYrmAooxbrXCaCy0gKRBHbm68HM4h59FXuA5iNGg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-5.27.0.tgz", + "integrity": "sha512-YFKnoglFnujAGgefXl+7NUmnpmlox9bJ3wYK7AFje2mh6O6MtquyLHHkNJI9lGLEmvdjubipgF0Fh45S8bIUHw==", "requires": { - "@datadog/browser-core": "4.34.1" + "@datadog/browser-core": "5.27.0" } }, "@design-systems/utils": { diff --git a/frontend/package.json b/frontend/package.json index 8ce3452e86..c421a5b003 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,12 +1,12 @@ { "name": "form-frontend", - "version": "6.148.0", + "version": "6.149.0", "homepage": ".", "private": true, "dependencies": { "@chakra-ui/react": "^1.8.6", "@datadog/browser-logs": "^4.50.1", - "@datadog/browser-rum": "^4.34.1", + "@datadog/browser-rum": "^5.27.0", "@emotion/react": "^11.7.0", "@emotion/styled": "^11.6.0", "@floating-ui/react-dom-interactions": "^0.9.3", diff --git a/frontend/src/components/Badge/Badge.tsx b/frontend/src/components/Badge/Badge.tsx index 0630059efb..29467673b3 100644 --- a/frontend/src/components/Badge/Badge.tsx +++ b/frontend/src/components/Badge/Badge.tsx @@ -10,6 +10,7 @@ export interface BadgeProps extends ChakraBadgeProps { * The theme of the tag to display */ variant?: BadgeVariants + colorScheme?: string } export const Badge = (props: BadgeProps): JSX.Element => { diff --git a/frontend/src/components/FormControl/FormLabel/FormLabel.tsx b/frontend/src/components/FormControl/FormLabel/FormLabel.tsx index 7c8e3a702b..30cd2c39d3 100644 --- a/frontend/src/components/FormControl/FormLabel/FormLabel.tsx +++ b/frontend/src/components/FormControl/FormLabel/FormLabel.tsx @@ -95,6 +95,7 @@ export const FormLabel = ({ > ( spacing = '0.5rem', children, isFullWidth, + isLabelFullWidth = false, ...rest } = omitThemingProps(props) @@ -203,6 +208,7 @@ export const Radio = forwardRef( } const labelStyles: SystemStyleObject = { + width: isLabelFullWidth ? 'full' : undefined, userSelect: 'none', marginStart: spacing, ...styles.label, diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListDrawer.stories.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListDrawer.stories.tsx new file mode 100644 index 0000000000..17400f0b03 --- /dev/null +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListDrawer.stories.tsx @@ -0,0 +1,52 @@ +import { DragDropContext } from 'react-beautiful-dnd' +import { StoryFn } from '@storybook/react' + +import { FormResponseMode } from '~shared/types' + +import { getAdminFormView } from '~/mocks/msw/handlers/admin-form' + +import { StoryRouter } from '~utils/storybook' + +import { CreatePageSidebarProvider } from '~features/admin-form/create/common' + +import { FieldListDrawer } from '..' + +export default { + component: FieldListDrawer, + title: + 'Features/AdminForm/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer', + parameters: { + msw: [getAdminFormView({ mode: FormResponseMode.Encrypt })], + }, + decorators: [ + StoryRouter({ initialEntries: ['/12345'], path: '/:formId' }), + (Story: StoryFn) => ( + // eslint-disable-next-line @typescript-eslint/no-empty-function + {}}> + + + + + ), + ], +} + +const encryptModeHandlers = [ + getAdminFormView({ mode: FormResponseMode.Encrypt }), +] + +const mrfModeHandlers = [ + getAdminFormView({ mode: FormResponseMode.Multirespondent }), +] + +export const EncryptMode = { + parameters: { + msw: encryptModeHandlers, + }, +} + +export const MrfMode = { + parameters: { + msw: mrfModeHandlers, + }, +} diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListOption.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListOption.tsx index dac4f6002e..e3534d4495 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListOption.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListOption.tsx @@ -6,14 +6,17 @@ import { } from 'react-beautiful-dnd' import { Box, BoxProps, forwardRef, Icon, Stack, Text } from '@chakra-ui/react' +import { FormResponseMode } from '~shared/types' import { BasicField, MyInfoAttribute } from '~shared/types/field' import { useIsMobile } from '~hooks/useIsMobile' +import Badge from '~components/Badge' import { BASICFIELD_TO_DRAWER_META, MYINFO_FIELD_TO_DRAWER_META, } from '~features/admin-form/create/constants' +import { useUser } from '~features/user/queries' import { useCreateTabForm } from '../../useCreateTabForm' import { @@ -68,7 +71,6 @@ interface DraggableMyInfoFieldOptionProps export const DraggableBasicFieldListOption = ({ fieldType, index, - children, isDisabled, ...props }: DraggableBasicFieldOptionProps): JSX.Element => { @@ -111,7 +113,6 @@ export const DraggableBasicFieldListOption = ({ export const DraggableMyInfoFieldListOption = ({ fieldType, index, - children, isDisabled, ...props }: DraggableMyInfoFieldOptionProps): JSX.Element => ( @@ -149,12 +150,16 @@ export const DraggableMyInfoFieldListOption = ({ export const BasicFieldOption = forwardRef( ({ fieldType, isDisabled, ...props }, ref) => { + // TODO: (MRF-email-notif) Remove isTest and useUser when approvals is out of beta + const isTest = process.env.NODE_ENV === 'test' + const { user } = useUser() const meta = useMemo( () => BASICFIELD_TO_DRAWER_META[fieldType], [fieldType], ) const { data: form } = useCreateTabForm() - const numFields = useMemo(() => form?.form_fields?.length ?? 0, [form]) + const isMrf = form?.responseMode === FormResponseMode.Multirespondent + const numFields = form?.form_fields?.length ?? 0 const newFieldMeta = useMemo( () => getFieldCreationMeta(fieldType), @@ -178,6 +183,14 @@ export const BasicFieldOption = forwardRef( > {meta.label} + {/* TODO: (MRF-email-notif) Remove isTest and betaFlag check when approvals is out of beta */} + {isTest || user?.betaFlags?.mrfEmailNotifications ? ( + isMrf && fieldType === BasicField.YesNo ? ( + + Use for approvals + + ) : null + ) : null} ) }, diff --git a/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx b/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx index 99a681405b..52430925bc 100644 --- a/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx +++ b/frontend/src/features/admin-form/create/common/CreatePageSidebar/CreatePageSidebar.tsx @@ -110,6 +110,7 @@ export const CreatePageSidebar = (): JSX.Element | null => { } onClick={handleDrawerBuilderClick} isActive={activeTab === DrawerTabs.Builder} @@ -117,6 +118,7 @@ export const CreatePageSidebar = (): JSX.Element | null => { /> } onClick={handleDrawerDesignClick} isActive={activeTab === DrawerTabs.Design} @@ -124,6 +126,7 @@ export const CreatePageSidebar = (): JSX.Element | null => { /> } onClick={handleDrawerLogicClick} isActive={activeTab === DrawerTabs.Logic} @@ -131,6 +134,7 @@ export const CreatePageSidebar = (): JSX.Element | null => { /> } onClick={handleDrawerEndpageClick} isActive={activeTab === DrawerTabs.EndPage} @@ -141,6 +145,7 @@ export const CreatePageSidebar = (): JSX.Element | null => { } onClick={handleDrawerWorkflowClick} isActive={activeTab === DrawerTabs.Workflow} @@ -157,6 +162,7 @@ export const CreatePageSidebar = (): JSX.Element | null => { icon={} borderRadius="full" aria-label="Help" + data-dd-action-name="create_builder.drawer_tab.help" onClick={(e) => { e.preventDefault() window.open(FORM_GUIDE) diff --git a/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx b/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx index 9fb300aa90..b99b049991 100644 --- a/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx +++ b/frontend/src/features/admin-form/create/common/CreatePageSidebar/DrawerTabIcon.tsx @@ -11,6 +11,7 @@ interface DrawerTabIconProps { isActive: boolean id?: string showRedDot?: boolean + trackingLabel?: string } export const DrawerTabIcon = ({ @@ -20,6 +21,7 @@ export const DrawerTabIcon = ({ isActive, id, showRedDot, + trackingLabel, }: DrawerTabIconProps): JSX.Element => { return ( @@ -27,6 +29,7 @@ export const DrawerTabIcon = ({ {handleDelete ? ( diff --git a/frontend/src/mocks/msw/handlers/admin-form/settings.ts b/frontend/src/mocks/msw/handlers/admin-form/settings.ts index c06d0bbf47..3278200e3a 100644 --- a/frontend/src/mocks/msw/handlers/admin-form/settings.ts +++ b/frontend/src/mocks/msw/handlers/admin-form/settings.ts @@ -21,7 +21,7 @@ export const getAdminFormView = ({ mode = FormResponseMode.Email, }: { delay?: number | 'infinite' - overrides?: Partial + overrides?: Partial mode?: FormResponseMode } = {}) => { return rest.get( diff --git a/frontend/src/typings/window.d.ts b/frontend/src/typings/window.d.ts new file mode 100644 index 0000000000..326ea1ca65 --- /dev/null +++ b/frontend/src/typings/window.d.ts @@ -0,0 +1,7 @@ +import type { RumGlobal } from '@datadog/browser-rum' + +declare global { + interface Window { + DD_RUM: RumGlobal | undefined + } +} diff --git a/frontend/src/utils/__tests__/datadog.tests.ts b/frontend/src/utils/__tests__/datadog.tests.ts new file mode 100644 index 0000000000..1f0f8a3840 --- /dev/null +++ b/frontend/src/utils/__tests__/datadog.tests.ts @@ -0,0 +1,49 @@ +describe('datadogRum', () => { + describe('DD_RUM is undefined', () => { + beforeEach(() => { + window.DD_RUM = undefined + }) + + afterEach(() => { + jest.resetModules() + }) + + it('should return a noop function for addAction', () => { + // Arrange + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const datadogRum = require('../datadog').datadogRum + // Assert + expect(window.DD_RUM).not.toBeDefined() + expect(datadogRum.addAction).not.toThrow() + }) + }) + + describe('DD_RUM is defined', () => { + let addActionSpy: jest.Mock + beforeEach(() => { + addActionSpy = jest.fn() + // @ts-expect-error mocking undefined DD_RUM + window.DD_RUM = { + addAction: addActionSpy, + } + }) + + afterEach(() => { + jest.resetModules() + }) + + it('should call addAction without throwing', () => { + // Arrange + // eslint-disable-next-line @typescript-eslint/no-var-requires + const datadogRum = require('../datadog').datadogRum + + // Assert + expect(window.DD_RUM).toBeDefined() + expect(datadogRum.addAction).not.toThrow() + expect(addActionSpy).toBeCalledTimes(1) + }) + }) +}) + +export {} diff --git a/frontend/src/utils/datadog.ts b/frontend/src/utils/datadog.ts new file mode 100644 index 0000000000..4f3eeea68b --- /dev/null +++ b/frontend/src/utils/datadog.ts @@ -0,0 +1,22 @@ +import type { RumGlobal } from '@datadog/browser-rum' + +/** + * Retrieves the datadogRum instance from the window object. + * + * `datadogRum` imported from `'@datadog/browser-rum'` refers to a different datadog instance as the chunk is loaded separately from the js bundle. + * Instead, we have to extract the global variable from the window object. + * */ +const _datadogRum = window.DD_RUM as RumGlobal | undefined + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {} +const handler = { + get: (target: RumGlobal, prop: keyof RumGlobal) => { + if (Object.keys(target).length === 0) { + return noop + } + return target[prop] + }, +} + +export const datadogRum = new Proxy(_datadogRum || {}, handler) diff --git a/package-lock.json b/package-lock.json index 4a79f97d4f..5d0a6ffe04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,29 @@ { "name": "FormSG", - "version": "6.148.0", + "version": "6.149.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "FormSG", - "version": "6.148.0", + "version": "6.149.0", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.536.0", - "@aws-sdk/client-lambda": "^3.414.0", + "@aws-sdk/client-lambda": "^3.654.0", "@babel/runtime": "^7.24.7", "@faker-js/faker": "^8.4.1", "@growthbook/growthbook": "^1.1.0", "@joi/date": "^2.1.0", "@opengovsg/formsg-sdk": "^0.12.0-alpha.1", "@opengovsg/myinfo-gov-client": "^4.1.2", - "@opengovsg/sgid-client": "^2.0.0", + "@opengovsg/sgid-client": "^2.2.0", "@react-email/components": "^0.0.15", "@react-email/render": "^0.0.12", "@stablelib/base64": "^1.0.1", "aws-info": "^1.2.0", - "aws-sdk": "^2.1659.0", - "axios": "^1.7.4", + "aws-sdk": "^2.1691.0", + "axios": "^1.7.7", "bcrypt": "^5.1.1", "bluebird": "^3.5.2", "body-parser": "^1.20.3", @@ -42,10 +42,10 @@ "date-fns": "^2.30.0", "dd-trace": "^5.22.0", "dedent-js": "~1.0.1", - "dotenv": "^16.0.3", + "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.20.0", - "express-rate-limit": "^7.2.0", + "express-rate-limit": "^7.4.0", "express-request-id": "^1.4.1", "express-session": "^1.18.0", "express-winston": "^4.2.0", @@ -75,21 +75,18 @@ "multer": "^1.4.5-lts.1", "multiparty": ">=4.2.3", "nan": "^2.19.0", - "neverthrow": "^6.1.0", + "neverthrow": "^8.0.0", "nocache": "^3.0.4", "node-cache": "^5.1.2", "nodemailer": "^6.9.13", "openid-client": "^5.3.1", - "opossum": "^7.1.0", - "p-queue": "^6.6.2", + "opossum": "^8.1.4", "promise-retry": "^2.0.1", "promise-timeout": "^1.3.0", "puppeteer-core": "22.6.3", "react-email": "^2.1.3", - "selectize": "0.12.6", "slick-carousel": "1.8.1", "sns-validator": "^0.3.5", - "sortablejs": "~1.14.0", "spark-md5": "^3.0.2", "sqs-consumer": "^5.7.0", "sqs-producer": "^2.1.0", @@ -111,19 +108,18 @@ "devDependencies": { "@babel/core": "^7.24.3", "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/preset-env": "^7.25.3", - "@opengovsg/credits-generator": "^1.0.6", + "@babel/preset-env": "^7.25.4", "@opengovsg/mockpass": "^4.3.4", "@playwright/test": "^1.45.1", - "@stoplight/prism-cli": "^5.5.4", + "@stoplight/prism-cli": "^5.10.0", "@types/bcrypt": "^5.0.0", "@types/bluebird": "^3.5.42", - "@types/busboy": "^1.5.3", + "@types/busboy": "^1.5.4", "@types/compression": "^1.7.5", "@types/connect-datadog": "0.0.6", "@types/convict": "^6.1.6", "@types/cookie-parser": "^1.4.7", - "@types/dedent": "^0.7.0", + "@types/dedent": "^0.7.2", "@types/ejs": "^3.1.5", "@types/express": "^4.17.21", "@types/express-request-id": "^1.4.3", @@ -142,7 +138,7 @@ "@types/multer": "^1.4.11", "@types/node": "^14.18.23", "@types/nodemailer": "^6.4.15", - "@types/opossum": "^6.2.3", + "@types/opossum": "^8.1.7", "@types/promise-retry": "^1.1.3", "@types/promise-timeout": "^1.3.0", "@types/puppeteer-core": "^5.4.0", @@ -159,14 +155,13 @@ "axios-mock-adapter": "^1.22.0", "concurrently": "^7.6.0", "copyfiles": "^2.4.1", - "coveralls": "^3.1.1", "env-cmd": "^10.1.0", "eslint": "^8.57.0", "eslint-config-prettier": "^8.10.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-playwright": "^1.6.0", - "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-typesafe": "^0.5.2", "form-data": "^4.0.0", @@ -174,7 +169,7 @@ "husky": "^9.1.6", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "jest-extended": "^3.2.4", + "jest-extended": "^4.0.2", "jest-localstorage-mock": "^2.4.26", "jest-mock-axios": "^4.7.2", "lint-staged": "^15.2.7", @@ -843,443 +838,1223 @@ "optional": true }, "node_modules/@aws-sdk/client-lambda": { - "version": "3.414.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.414.0.tgz", - "integrity": "sha512-Nt2ktmFWKlL19NWcaG9fS2cxjiJvGDIp8Irt1NZngIOfmqm4XsY1AcUjUcdZRED/VjdfM0ziHa9Oj4VVVTdYZA==", + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.654.0.tgz", + "integrity": "sha512-/ITRFpQeutTNsyZ5AjLCnNKM08rPq5WSEtNoJ2zLOAinOUnPpLFZ3p6w+5AVc2FYTeFY5gxby84Ejs4gzS8/Mg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.654.0", + "@aws-sdk/client-sts": "3.654.0", + "@aws-sdk/core": "3.654.0", + "@aws-sdk/credential-provider-node": "3.654.0", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.3", + "@smithy/eventstream-serde-browser": "^3.0.9", + "@smithy/eventstream-serde-config-resolver": "^3.0.6", + "@smithy/eventstream-serde-node": "^3.0.8", + "@smithy/fetch-http-handler": "^3.2.7", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.18", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.2", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.18", + "@smithy/util-defaults-mode-node": "^3.0.18", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-stream": "^3.1.6", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.414.0", - "@aws-sdk/credential-provider-node": "3.414.0", - "@aws-sdk/middleware-host-header": "3.413.0", - "@aws-sdk/middleware-logger": "3.413.0", - "@aws-sdk/middleware-recursion-detection": "3.413.0", - "@aws-sdk/middleware-signing": "3.413.0", - "@aws-sdk/middleware-user-agent": "3.413.0", - "@aws-sdk/region-config-resolver": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@aws-sdk/util-endpoints": "3.413.0", - "@aws-sdk/util-user-agent-browser": "3.413.0", - "@aws-sdk/util-user-agent-node": "3.413.0", - "@smithy/config-resolver": "^2.0.8", - "@smithy/eventstream-serde-browser": "^2.0.7", - "@smithy/eventstream-serde-config-resolver": "^2.0.7", - "@smithy/eventstream-serde-node": "^2.0.7", - "@smithy/fetch-http-handler": "^2.1.3", - "@smithy/hash-node": "^2.0.7", - "@smithy/invalid-dependency": "^2.0.7", - "@smithy/middleware-content-length": "^2.0.9", - "@smithy/middleware-endpoint": "^2.0.7", - "@smithy/middleware-retry": "^2.0.10", - "@smithy/middleware-serde": "^2.0.7", - "@smithy/middleware-stack": "^2.0.0", - "@smithy/node-config-provider": "^2.0.10", - "@smithy/node-http-handler": "^2.1.3", - "@smithy/protocol-http": "^3.0.3", - "@smithy/smithy-client": "^2.1.4", - "@smithy/types": "^2.3.1", - "@smithy/url-parser": "^2.0.7", - "@smithy/util-base64": "^2.0.0", - "@smithy/util-body-length-browser": "^2.0.0", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.8", - "@smithy/util-defaults-mode-node": "^2.0.10", - "@smithy/util-retry": "^2.0.0", - "@smithy/util-stream": "^2.0.10", - "@smithy/util-utf8": "^2.0.0", - "@smithy/util-waiter": "^2.0.7", - "tslib": "^2.5.0" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/client-sso": { - "version": "3.414.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.414.0.tgz", - "integrity": "sha512-GvRwQ7wA3edzsQEKS70ZPhkOUZ62PAiXasjp6GxrsADEb8sV1z4FxXNl9Un/7fQxKkh9QYaK1Wu1PmhLi9MLMg==", + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/middleware-host-header": "3.413.0", - "@aws-sdk/middleware-logger": "3.413.0", - "@aws-sdk/middleware-recursion-detection": "3.413.0", - "@aws-sdk/middleware-user-agent": "3.413.0", - "@aws-sdk/region-config-resolver": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@aws-sdk/util-endpoints": "3.413.0", - "@aws-sdk/util-user-agent-browser": "3.413.0", - "@aws-sdk/util-user-agent-node": "3.413.0", - "@smithy/config-resolver": "^2.0.8", - "@smithy/fetch-http-handler": "^2.1.3", - "@smithy/hash-node": "^2.0.7", - "@smithy/invalid-dependency": "^2.0.7", - "@smithy/middleware-content-length": "^2.0.9", - "@smithy/middleware-endpoint": "^2.0.7", - "@smithy/middleware-retry": "^2.0.10", - "@smithy/middleware-serde": "^2.0.7", - "@smithy/middleware-stack": "^2.0.0", - "@smithy/node-config-provider": "^2.0.10", - "@smithy/node-http-handler": "^2.1.3", - "@smithy/protocol-http": "^3.0.3", - "@smithy/smithy-client": "^2.1.4", - "@smithy/types": "^2.3.1", - "@smithy/url-parser": "^2.0.7", - "@smithy/util-base64": "^2.0.0", - "@smithy/util-body-length-browser": "^2.0.0", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.8", - "@smithy/util-defaults-mode-node": "^2.0.10", - "@smithy/util-retry": "^2.0.0", + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/client-sts": { - "version": "3.414.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.414.0.tgz", - "integrity": "sha512-xeYH3si6Imp1EWolWn1zuxJJu2AXKwXl1HDftQULwC5AWkm1mNFbXYSJN4hQul1IM+kn+JTRB0XRHByQkKhe+Q==", + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/credential-provider-node": "3.414.0", - "@aws-sdk/middleware-host-header": "3.413.0", - "@aws-sdk/middleware-logger": "3.413.0", - "@aws-sdk/middleware-recursion-detection": "3.413.0", - "@aws-sdk/middleware-sdk-sts": "3.413.0", - "@aws-sdk/middleware-signing": "3.413.0", - "@aws-sdk/middleware-user-agent": "3.413.0", - "@aws-sdk/region-config-resolver": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@aws-sdk/util-endpoints": "3.413.0", - "@aws-sdk/util-user-agent-browser": "3.413.0", - "@aws-sdk/util-user-agent-node": "3.413.0", - "@smithy/config-resolver": "^2.0.8", - "@smithy/fetch-http-handler": "^2.1.3", - "@smithy/hash-node": "^2.0.7", - "@smithy/invalid-dependency": "^2.0.7", - "@smithy/middleware-content-length": "^2.0.9", - "@smithy/middleware-endpoint": "^2.0.7", - "@smithy/middleware-retry": "^2.0.10", - "@smithy/middleware-serde": "^2.0.7", - "@smithy/middleware-stack": "^2.0.0", - "@smithy/node-config-provider": "^2.0.10", - "@smithy/node-http-handler": "^2.1.3", - "@smithy/protocol-http": "^3.0.3", - "@smithy/smithy-client": "^2.1.4", - "@smithy/types": "^2.3.1", - "@smithy/url-parser": "^2.0.7", - "@smithy/util-base64": "^2.0.0", - "@smithy/util-body-length-browser": "^2.0.0", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.8", - "@smithy/util-defaults-mode-node": "^2.0.10", - "@smithy/util-retry": "^2.0.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", - "fast-xml-parser": "4.2.5", - "tslib": "^2.5.0" + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/client-sso": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.654.0.tgz", + "integrity": "sha512-4kBxs2IzCDtj6a6lRXa/lXK5wWpMGzwKtb+HMXf/rJYVM6x7wYRzc1hYrOd3DYkFQ/sR3dUFj+0mTP0os3aAbA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.654.0", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.3", + "@smithy/fetch-http-handler": "^3.2.7", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.18", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.2", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.18", + "@smithy/util-defaults-mode-node": "^3.0.18", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.654.0.tgz", + "integrity": "sha512-gbHrKsEnaAtmkNCVQzLyiqMzpDaThV/bWl/ODEklI+t6stW3Pe3oDMstEHLfJ6JU5g8sYnx4VLuxlnJMtUkvPw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.654.0", + "@aws-sdk/credential-provider-node": "3.654.0", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.3", + "@smithy/fetch-http-handler": "^3.2.7", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.18", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.2", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.18", + "@smithy/util-defaults-mode-node": "^3.0.18", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.654.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/client-sts": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.654.0.tgz", + "integrity": "sha512-tyHa8jsBy+/NQZFHm6Q2Q09Vi9p3EH4yPy6PU8yPewpi2klreObtrUd0anJa6nzjS9SSuqnlZWsRic3cQ4QwCg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.654.0", + "@aws-sdk/core": "3.654.0", + "@aws-sdk/credential-provider-node": "3.654.0", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.3", + "@smithy/fetch-http-handler": "^3.2.7", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.18", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.2", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.18", + "@smithy/util-defaults-mode-node": "^3.0.18", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/core": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.654.0.tgz", + "integrity": "sha512-4Rwx7BVaNaFqmXBDmnOkMbyuIFFbpZ+ru4lr660p45zY1QoNNSalechfoRffcokLFOZO+VWEJkdcorPUUU993w==", + "dependencies": { + "@smithy/core": "^2.4.3", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/property-provider": "^3.1.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/signature-v4": "^4.1.3", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/util-middleware": "^3.0.6", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.413.0.tgz", - "integrity": "sha512-yeMOkfG20/RlzfPMtQuDB647AcPEvFEVYOWZzAWVJfldYQ5ybKr0d7sBkgG9sdAzGkK3Aw9dE4rigYI8EIqc1Q==", + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.654.0.tgz", + "integrity": "sha512-kogsx3Ql81JouHS7DkheCDU9MYAvK0AokxjcshDveGmf7BbgbWCA8Fnb9wjQyNDaOXNvkZu8Z8rgkX91z324/w==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.654.0.tgz", + "integrity": "sha512-tgmAH4MBi/aDR882lfw48+tDV95ZH3GWc1Eoe6DpNLiM3GN2VfU/cZwuHmi6aq+vAbdIlswBHJ/+va0fOvlyjw==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/fetch-http-handler": "^3.2.7", + "@smithy/node-http-handler": "^3.2.2", + "@smithy/property-provider": "^3.1.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/util-stream": "^3.1.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.414.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.414.0.tgz", - "integrity": "sha512-rlpLLx70roJL/t40opWC96LbIASejdMbRlgSCRpK8b/hKngYDe5A7SRVacaw08vYrAywxRiybxpQOwOt9b++rA==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.413.0", - "@aws-sdk/credential-provider-process": "3.413.0", - "@aws-sdk/credential-provider-sso": "3.414.0", - "@aws-sdk/credential-provider-web-identity": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@smithy/credential-provider-imds": "^2.0.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.654.0.tgz", + "integrity": "sha512-DKSdaNu2hwdmuvnm9KnA0NLqMWxxmxSOLWjSUSoFIm++wGXUjPrRMFYKvMktaXnPuyf5my8gF/yGbwzPZ8wlTg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.654.0", + "@aws-sdk/credential-provider-http": "3.654.0", + "@aws-sdk/credential-provider-process": "3.654.0", + "@aws-sdk/credential-provider-sso": "3.654.0", + "@aws-sdk/credential-provider-web-identity": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.654.0" } }, "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.414.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.414.0.tgz", - "integrity": "sha512-xlkcOUKeGHInxWKKrZKIPSBCUL/ozyCldJBjmMKEj7ZmBAEiDcjpMe3pZ//LibMkCSy0b/7jtyQBE/eaIT2o0A==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.413.0", - "@aws-sdk/credential-provider-ini": "3.414.0", - "@aws-sdk/credential-provider-process": "3.413.0", - "@aws-sdk/credential-provider-sso": "3.414.0", - "@aws-sdk/credential-provider-web-identity": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@smithy/credential-provider-imds": "^2.0.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.654.0.tgz", + "integrity": "sha512-wPV7CNYaXDEc+SS+3R0v8SZwkHRUE1z2k2j1d49tH5QBDT4tb/k2V/biXWkwSk3hbR+IMWXmuhJDv/5lybhIvg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.654.0", + "@aws-sdk/credential-provider-http": "3.654.0", + "@aws-sdk/credential-provider-ini": "3.654.0", + "@aws-sdk/credential-provider-process": "3.654.0", + "@aws-sdk/credential-provider-sso": "3.654.0", + "@aws-sdk/credential-provider-web-identity": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.413.0.tgz", - "integrity": "sha512-GFJdgS14GzJ1wc2DEnS44Z/34iBZ05CAkvDsLN2CMwcDgH4eZuif9/x0lwzIJBK3xVFHzYUeVvEzsqRPbCHRsw==", - "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.654.0.tgz", + "integrity": "sha512-PmQoo8sZ9Q2Ow8OMzK++Z9lI7MsRUG7sNq3E72DVA215dhtTICTDQwGlXH2AAmIp7n+G9LLRds+4wo2ehG4mkg==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.414.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.414.0.tgz", - "integrity": "sha512-w9g2hlkZn7WekWICRqk+L33py7KrjYMFryVpkKXOx2pjDchCfZDr6pL1ml782GZ0L3qsob4SbNpbtp13JprnWQ==", + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.654.0.tgz", + "integrity": "sha512-7GFme6fWEdA/XYKzZPOAdj/jS6fMBy1NdSIZsDXikS0v9jU+ZzHrAaWt13YLzHyjgxB9Sg9id9ncdY1IiubQXQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.654.0", + "@aws-sdk/token-providers": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.654.0.tgz", + "integrity": "sha512-6a2g9gMtZToqSu+CusjNK5zvbLJahQ9di7buO3iXgbizXpLXU1rnawCpWxwslMpT5fLgMSKDnKDrr6wdEk7jSw==", "dependencies": { - "@aws-sdk/client-sso": "3.414.0", - "@aws-sdk/token-providers": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.654.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.654.0.tgz", + "integrity": "sha512-rxGgVHWKp8U2ubMv+t+vlIk7QYUaRCHaVpmUlJv0Wv6Q0KeO9a42T9FxHphjOTlCGQOLcjCreL9CF8Qhtb4mdQ==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-logger": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.654.0.tgz", + "integrity": "sha512-OQYb+nWlmASyXfRb989pwkJ9EVUMP1CrKn2eyTk3usl20JZmKo2Vjis6I0tLUkMSxMhnBJJlQKyWkRpD/u1FVg==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.654.0.tgz", + "integrity": "sha512-gKSomgltKVmsT8sC6W7CrADZ4GHwX9epk3GcH6QhebVO3LA9LRbkL3TwOPUXakxxOLLUTYdOZLIOtFf7iH00lg==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.654.0.tgz", + "integrity": "sha512-liCcqPAyRsr53cy2tYu4qeH4MMN0eh9g6k56XzI5xd4SghXH5YWh4qOYAlQ8T66ZV4nPMtD8GLtLXGzsH8moFg==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/token-providers": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.654.0.tgz", + "integrity": "sha512-D8GeJYmvbfWkQDtTB4owmIobSMexZel0fOoetwvgCQ/7L8VPph3Q2bn1TRRIXvH7wdt6DcDxA3tKMHPBkT3GlA==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.654.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/types": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.654.0.tgz", + "integrity": "sha512-VWvbED3SV+10QJIcmU/PKjsKilsTV16d1I7/on4bvD/jo1qGeMXqLDBSen3ks/tuvXZF/mFc7ZW/W2DiLVtO7A==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-endpoints": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.654.0.tgz", + "integrity": "sha512-i902fcBknHs0Irgdpi62+QMvzxE+bczvILXigYrlHL4+PiEnlMVpni5L5W1qCkNZXf8AaMrSBuR1NZAGp6UOUw==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "@smithy/util-endpoints": "^2.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.654.0.tgz", + "integrity": "sha512-ykYAJqvnxLt7wfrqya28wuH3/7NdrwzfiFd7NqEVQf7dXVxL5RPEpD7DxjcyQo3DsHvvdUvGZVaQhozycn1pzA==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.654.0.tgz", + "integrity": "sha512-a0ojjdBN6pqv6gB4H/QPPSfhs7mFtlVwnmKCM/QrTaFzN0U810PJ1BST3lBx5sa23I5jWHGaoFY+5q65C3clLQ==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/abort-controller": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.4.tgz", + "integrity": "sha512-VupaALAQlXViW3/enTf/f5l5JZYSAxoJL7f0nanhNNKnww6DGCg1oYIuNP78KDugnkwthBO6iEcym16HhWV8RQ==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/config-resolver": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.8.tgz", + "integrity": "sha512-Tv1obAC18XOd2OnDAjSWmmthzx6Pdeh63FbLin8MlPiuJ2ATpKkq0NcNOJFr0dO+JmZXnwu8FQxKJ3TKJ3Hulw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/core": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.3.tgz", + "integrity": "sha512-4LTusLqFMRVQUfC3RNuTg6IzYTeJNpydRdTKq7J5wdEyIRQSu3rGIa3s80mgG2hhe6WOZl9IqTSo1pgbn6EHhA==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.18", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/credential-provider-imds": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.3.tgz", + "integrity": "sha512-VoxMzSzdvkkjMJNE38yQgx4CfnmT+Z+5EUXkg4x7yag93eQkVQgZvN3XBSHC/ylfBbLbAtdu7flTCChX9I+mVg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.7", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/eventstream-codec": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.5.tgz", + "integrity": "sha512-6pu+PT2r+5ZnWEV3vLV1DzyrpJ0TmehQlniIDCSpZg6+Ji2SfOI38EqUyQ+O8lotVElCrfVc9chKtSMe9cmCZQ==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.4.2", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.9.tgz", + "integrity": "sha512-PiQLo6OQmZAotJweIcObL1H44gkvuJACKMNqpBBe5Rf2Ax1DOcGi/28+feZI7yTe1ERHlQQaGnm8sSkyDUgsMg==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.8", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.6.tgz", + "integrity": "sha512-iew15It+c7WfnVowWkt2a7cdPp533LFJnpjDQgfZQcxv2QiOcyEcea31mnrk5PVbgo0nNH3VbYGq7myw2q/F6A==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.8.tgz", + "integrity": "sha512-6m+wI+fT0na+6oao6UqALVA38fsScCpoG5UO/A8ZSyGLnPM2i4MS1cFUhpuALgvLMxfYoTCh7qSeJa0aG4IWpQ==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.8", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.8.tgz", + "integrity": "sha512-09tqzIQ6e+7jLqGvRji1yJoDbL/zob0OFhq75edgStWErGLf16+yI5hRc/o9/YAybOhUZs/swpW2SPn892G5Gg==", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.5", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.7.tgz", + "integrity": "sha512-Ra6IPI1spYLO+t62/3jQbodjOwAbto9wlpJdHZwkycm0Kit+GVpzHW/NMmSgY4rK1bjJ4qLAmCnaBzePO5Nkkg==", + "dependencies": { + "@smithy/protocol-http": "^4.1.3", + "@smithy/querystring-builder": "^3.0.6", + "@smithy/types": "^3.4.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/hash-node": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.6.tgz", + "integrity": "sha512-c/FHEdKK/7DU2z6ZE91L36ahyXWayR3B+FzELjnYq7wH5YqIseM24V+pWCS9kFn1Ln8OFGTf+pyYPiHZuX0s/Q==", + "dependencies": { + "@smithy/types": "^3.4.2", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/hash-node/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/invalid-dependency": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.6.tgz", + "integrity": "sha512-czM7Ioq3s8pIXht7oD+vmgy4Wfb4XavU/k/irO8NdXFFOx7YAlsCCcKOh/lJD1mJSYQqiR7NmpZ9JviryD/7AQ==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/middleware-content-length": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.8.tgz", + "integrity": "sha512-VuyszlSO49WKh3H9/kIO2kf07VUwGV80QRiaDxUfP8P8UKlokz381ETJvwLhwuypBYhLymCYyNhB3fLAGBX2og==", + "dependencies": { + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/middleware-endpoint": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.3.tgz", + "integrity": "sha512-KeM/OrK8MVFUsoJsmCN0MZMVPjKKLudn13xpgwIMpGTYpA8QZB2Xq5tJ+RE6iu3A6NhOI4VajDTwBsm8pwwrhg==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-middleware": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/middleware-retry": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.18.tgz", + "integrity": "sha512-YU1o/vYob6vlqZdd97MN8cSXRToknLXhFBL3r+c9CZcnxkO/rgNZ++CfgX2vsmnEKvlqdi26+SRtSzlVp5z6Mg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.3", + "@smithy/service-error-classification": "^3.0.6", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/middleware-serde": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.6.tgz", + "integrity": "sha512-KKTUSl1MzOM0MAjGbudeaVNtIDo+PpekTBkCNwvfZlKndodrnvRo+00USatiyLOc0ujjO9UydMRu3O9dYML7ag==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/middleware-stack": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.6.tgz", + "integrity": "sha512-2c0eSYhTQ8xQqHMcRxLMpadFbTXg6Zla5l0mwNftFCZMQmuhI7EbAJMx6R5eqfuV3YbJ3QGyS3d5uSmrHV8Khg==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/node-config-provider": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.7.tgz", + "integrity": "sha512-g3mfnC3Oo8pOI0dYuPXLtdW1WGVb3bR2tkV21GNkm0ZvQjLTtamXAwCWt/FCb0HGvKt3gHHmF1XerG0ICfalOg==", + "dependencies": { + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/node-http-handler": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.2.tgz", + "integrity": "sha512-42Cy4/oT2O+00aiG1iQ7Kd7rE6q8j7vI0gFfnMlUiATvyo8vefJkhb7O10qZY0jAqo5WZdUzfl9IV6wQ3iMBCg==", + "dependencies": { + "@smithy/abort-controller": "^3.1.4", + "@smithy/protocol-http": "^4.1.3", + "@smithy/querystring-builder": "^3.0.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/property-provider": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.6.tgz", + "integrity": "sha512-NK3y/T7Q/Bw+Z8vsVs9MYIQ5v7gOX7clyrXcwhhIBQhbPgRl6JDrZbusO9qWDhcEus75Tg+VCxtIRfo3H76fpw==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/protocol-http": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.3.tgz", + "integrity": "sha512-GcbMmOYpH9iRqtC05RbRnc/0FssxSTHlmaNhYBTgSgNCYpdR3Kt88u5GAZTBmouzv+Zlj/VRv92J9ruuDeJuEw==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/querystring-builder": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.6.tgz", + "integrity": "sha512-sQe08RunoObe+Usujn9+R2zrLuQERi3CWvRO3BvnoWSYUaIrLKuAIeY7cMeDax6xGyfIP3x/yFWbEKSXvOnvVg==", + "dependencies": { + "@smithy/types": "^3.4.2", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/querystring-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.6.tgz", + "integrity": "sha512-UJKw4LlEkytzz2Wq+uIdHf6qOtFfee/o7ruH0jF5I6UAuU+19r9QV7nU3P/uI0l6+oElRHmG/5cBBcGJrD7Ozg==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/service-error-classification": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.6.tgz", + "integrity": "sha512-53SpchU3+DUZrN7J6sBx9tBiCVGzsib2e4sc512Q7K9fpC5zkJKs6Z9s+qbMxSYrkEkle6hnMtrts7XNkMJJMg==", + "dependencies": { + "@smithy/types": "^3.4.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.7.tgz", + "integrity": "sha512-IA4K2qTJYXkF5OfVN4vsY1hfnUZjaslEE8Fsr/gGFza4TAC2A9NfnZuSY2srQIbt9bwtjHiAayrRVgKse4Q7fA==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/signature-v4": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.3.tgz", + "integrity": "sha512-YD2KYSCEEeFHcWZ1E3mLdAaHl8T/TANh6XwmocQ6nPcTdBfh4N5fusgnblnWDlnlU1/cUqEq3PiGi22GmT2Lkg==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/smithy-client": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.3.2.tgz", + "integrity": "sha512-RKDfhF2MTwXl7jan5d7QfS9eCC6XJbO3H+EZAvLQN8A5in4ib2Ml4zoeLo57w9QrqFekBPcsoC2hW3Ekw4vQ9Q==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "@smithy/util-stream": "^3.1.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/types": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.4.2.tgz", + "integrity": "sha512-tHiFcfcVedVBHpmHUEUHOCCih8iZbIAYn9NvPsNzaPm/237I3imdDdZoOC8c87H5HBAVEa06tTgb+OcSWV9g5w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/url-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.6.tgz", + "integrity": "sha512-47Op/NU8Opt49KyGpHtVdnmmJMsp2hEwBdyjuFB9M2V5QVOwA7pBhhxKN5z6ztKGrMw76gd8MlbPuzzvaAncuQ==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-base64/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.413.0.tgz", - "integrity": "sha512-5cdA1Iq9JeEHtg59ERV9fdMQ7cS0JF6gH/BWA7HYEUGdSVPXCuwyEggPtG64QgpNU7SmxH+QdDG+Ldxz09ycIA==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.413.0.tgz", - "integrity": "sha512-r9PQx468EzPHo9wRzZLfgROpKtVdbkteMrdhsuM12bifVHjU1OHr7yfhc1OdWv39X8Xiv6F8n5r+RBQEM0S6+g==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.18.tgz", + "integrity": "sha512-/eveCzU6Z6Yw8dlYQLA4rcK30XY0E4L3lD3QFHm59mzDaWYelrXE1rlynuT3J6qxv+5yNy3a1JuzhG5hk5hcmw==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/protocol-http": "^3.0.3", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "@smithy/property-provider": "^3.1.6", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">= 10.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-logger": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.413.0.tgz", - "integrity": "sha512-jqcXDubcKvoqBy+kkEa0WoNjG6SveDeyNy+gdGnTV+DEtYjkcHrHJei4q0W5zFl0mzc+dP+z8tJF44rv95ZY3Q==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.18.tgz", + "integrity": "sha512-9cfzRjArtOFPlTYRREJk00suUxVXTgbrzVncOyMRTUeMKnecG/YentLF3cORa+R6mUOMSrMSnT18jos1PKqK6Q==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "@smithy/config-resolver": "^3.0.8", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/property-provider": "^3.1.6", + "@smithy/smithy-client": "^3.3.2", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">= 10.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.413.0.tgz", - "integrity": "sha512-C6k0IKJk/A4/VBGwUjxEPG+WOjjnmWAZVRBUzaeM7PqRh+g5rLcuIV356ntV3pREVxyiSTePTYVYIHU9YXkLKQ==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-endpoints": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.2.tgz", + "integrity": "sha512-FEISzffb4H8DLzGq1g4MuDpcv6CIG15fXoQzDH9SjpRJv6h7J++1STFWWinilG0tQh9H1v2UKWG19Jjr2B16zQ==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/protocol-http": "^3.0.3", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-sdk-sts": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.413.0.tgz", - "integrity": "sha512-t0u//JUyaEZRVnH5q+Ur3tWnuyIsTdwA0XOdDCZXcSlLYzGp2MI/tScLjn9IydRrceIFpFfmbjk4Nf/Q6TeBTQ==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", "dependencies": { - "@aws-sdk/middleware-signing": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-signing": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.413.0.tgz", - "integrity": "sha512-QFEnVvIKYPCermM+ESxEztgUgXzGSKpnPnohMYNvSZySqmOLu/4VvxiZbRO/BX9J3ZHcUgaw4vKm5VBZRrycxw==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-middleware": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.6.tgz", + "integrity": "sha512-BxbX4aBhI1O9p87/xM+zWy0GzT3CEVcXFPBRDoHAM+pV0eSW156pR+PSYEz0DQHDMYDsYAflC2bQNz2uaDBUZQ==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/protocol-http": "^3.0.3", - "@smithy/signature-v4": "^2.0.0", - "@smithy/types": "^2.3.1", - "@smithy/util-middleware": "^2.0.0", - "tslib": "^2.5.0" + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.413.0.tgz", - "integrity": "sha512-eVMJyeWxNBqerhfD+sE9sTjDtwQiECrfU6wpUQP5fGPhJD2cVVZPxuTuJGDZCu/4k/V61dF85IYlsPUNLdVQ6w==", - "dependencies": { - "@aws-sdk/types": "3.413.0", - "@aws-sdk/util-endpoints": "3.413.0", - "@smithy/protocol-http": "^3.0.3", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-retry": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.6.tgz", + "integrity": "sha512-BRZiuF7IwDntAbevqMco67an0Sr9oLQJqqRCsSPZZHYRnehS0LHDAkJk/pSmI7Z8c/1Vet294H7fY2fWUgB+Rg==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/token-providers": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.413.0.tgz", - "integrity": "sha512-NfP1Ib9LAWVLMTOa/1aJwt4TRrlRrNyukCpVZGfNaMnNNEoP5Rakdbcs8KFVHe/MJzU+GdKVzxQ4TgRkLOGTrA==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.6.tgz", + "integrity": "sha512-lQEUfTx1ht5CRdvIjdAN/gUL6vQt2wSARGGLaBHNe+iJSkRHlWzY+DOn0mFTmTgyU3jcI5n9DkT5gTzYuSOo6A==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/middleware-host-header": "3.413.0", - "@aws-sdk/middleware-logger": "3.413.0", - "@aws-sdk/middleware-recursion-detection": "3.413.0", - "@aws-sdk/middleware-user-agent": "3.413.0", - "@aws-sdk/types": "3.413.0", - "@aws-sdk/util-endpoints": "3.413.0", - "@aws-sdk/util-user-agent-browser": "3.413.0", - "@aws-sdk/util-user-agent-node": "3.413.0", - "@smithy/config-resolver": "^2.0.8", - "@smithy/fetch-http-handler": "^2.1.3", - "@smithy/hash-node": "^2.0.7", - "@smithy/invalid-dependency": "^2.0.7", - "@smithy/middleware-content-length": "^2.0.9", - "@smithy/middleware-endpoint": "^2.0.7", - "@smithy/middleware-retry": "^2.0.10", - "@smithy/middleware-serde": "^2.0.7", - "@smithy/middleware-stack": "^2.0.0", - "@smithy/node-config-provider": "^2.0.10", - "@smithy/node-http-handler": "^2.1.3", - "@smithy/property-provider": "^2.0.0", - "@smithy/protocol-http": "^3.0.3", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/smithy-client": "^2.1.4", - "@smithy/types": "^2.3.1", - "@smithy/url-parser": "^2.0.7", - "@smithy/util-base64": "^2.0.0", - "@smithy/util-body-length-browser": "^2.0.0", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.8", - "@smithy/util-defaults-mode-node": "^2.0.10", - "@smithy/util-retry": "^2.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.5.0" + "@smithy/fetch-http-handler": "^3.2.7", + "@smithy/node-http-handler": "^3.2.2", + "@smithy/types": "^3.4.2", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/types": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.413.0.tgz", - "integrity": "sha512-j1xib0f/TazIFc5ySIKOlT1ujntRbaoG4LJFeEezz4ji03/wSJMI8Vi4KjzpBp8J1tTu0oRDnsxRIGixsUBeYQ==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-stream/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "dependencies": { - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-endpoints": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.413.0.tgz", - "integrity": "sha512-VAwr7cITNb1L6/2XUPIbCOuhKGm0VtKCRblurrfUF2bxqG/wtuw/2Fm4ahYJPyxklOSXAMSq+RHdFWcir0YB/g==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.413.0.tgz", - "integrity": "sha512-7j/qWcRO2OBZBre2fC6V6M0PAS9n7k6i+VtofPkkhxC2DZszLJElqnooF9hGmVGYK3zR47Np4WjURXKIEZclWg==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/types": "^2.3.1", - "bowser": "^2.11.0", - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.413.0.tgz", - "integrity": "sha512-vHm9TVZIzfWMeDvdmoOky6VarqOt8Pr68CESHN0jyuO6XbhCDnr9rpaXiBhbSR+N1Qm7R/AfJgAhQyTMu2G1OA==", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "dependencies": { - "@aws-sdk/types": "3.413.0", - "@smithy/node-config-provider": "^2.0.10", - "@smithy/types": "^2.3.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" } }, "node_modules/@aws-sdk/client-lambda/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/@aws-sdk/client-lambda/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } }, "node_modules/@aws-sdk/client-sso": { "version": "3.535.0", @@ -2270,24 +3045,109 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.413.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.413.0.tgz", - "integrity": "sha512-h90e6yyOhvoc+1F5vFk3C5mxwB8RSDEMKTO/fxexyur94seczZ1yxyYkTMZv30oc9RUiToABlHNrh/wxL7TZPQ==", + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.654.0.tgz", + "integrity": "sha512-ydGOrXJxj3x0sJhsXyTmvJVLAE0xxuTWFJihTl67RtaO7VRNtd82I3P3bwoMMaDn5WpmV5mPo8fEUDRlBm3fPg==", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@aws-sdk/types": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.654.0.tgz", + "integrity": "sha512-VWvbED3SV+10QJIcmU/PKjsKilsTV16d1I7/on4bvD/jo1qGeMXqLDBSen3ks/tuvXZF/mFc7ZW/W2DiLVtO7A==", "dependencies": { - "@smithy/node-config-provider": "^2.0.10", - "@smithy/types": "^2.3.1", - "@smithy/util-config-provider": "^2.0.0", - "@smithy/util-middleware": "^2.0.0", - "tslib": "^2.5.0" + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/node-config-provider": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.7.tgz", + "integrity": "sha512-g3mfnC3Oo8pOI0dYuPXLtdW1WGVb3bR2tkV21GNkm0ZvQjLTtamXAwCWt/FCb0HGvKt3gHHmF1XerG0ICfalOg==", + "dependencies": { + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/property-provider": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.6.tgz", + "integrity": "sha512-NK3y/T7Q/Bw+Z8vsVs9MYIQ5v7gOX7clyrXcwhhIBQhbPgRl6JDrZbusO9qWDhcEus75Tg+VCxtIRfo3H76fpw==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.7.tgz", + "integrity": "sha512-IA4K2qTJYXkF5OfVN4vsY1hfnUZjaslEE8Fsr/gGFza4TAC2A9NfnZuSY2srQIbt9bwtjHiAayrRVgKse4Q7fA==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/types": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.4.2.tgz", + "integrity": "sha512-tHiFcfcVedVBHpmHUEUHOCCih8iZbIAYn9NvPsNzaPm/237I3imdDdZoOC8c87H5HBAVEa06tTgb+OcSWV9g5w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/util-middleware": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.6.tgz", + "integrity": "sha512-BxbX4aBhI1O9p87/xM+zWy0GzT3CEVcXFPBRDoHAM+pV0eSW156pR+PSYEz0DQHDMYDsYAflC2bQNz2uaDBUZQ==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/@aws-sdk/region-config-resolver/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/@aws-sdk/token-providers": { "version": "3.535.0", @@ -2428,9 +3288,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "engines": { "node": ">=6.9.0" } @@ -2501,11 +3361,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dependencies": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -2584,9 +3444,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", - "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", @@ -2594,7 +3454,7 @@ "@babel/helper-optimise-call-expression": "^7.24.7", "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "semver": "^6.3.1" }, "engines": { @@ -3258,15 +4118,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", - "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz", + "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-remap-async-to-generator": "^7.25.0", "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/traverse": "^7.25.0" + "@babel/traverse": "^7.25.4" }, "engines": { "node": ">=6.9.0" @@ -3323,13 +4183,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", + "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -3356,16 +4216,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", - "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", + "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "globals": "^11.1.0" }, "engines": { @@ -3809,13 +4669,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", + "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -4041,13 +4901,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", + "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -4057,12 +4917,12 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", - "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", + "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.25.2", + "@babel/compat-data": "^7.25.4", "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", @@ -4091,13 +4951,13 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoped-functions": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", @@ -4125,7 +4985,7 @@ "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.25.4", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-property-literals": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", @@ -4138,10 +4998,10 @@ "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.37.1", "semver": "^6.3.1" @@ -4238,15 +5098,15 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -4267,11 +5127,11 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -4281,9 +5141,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dependencies": { "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", @@ -6145,18 +7005,6 @@ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" }, - "node_modules/@opengovsg/credits-generator": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@opengovsg/credits-generator/-/credits-generator-1.0.6.tgz", - "integrity": "sha512-c0M9pKpn0J9D3Rdx4gcL9p7JGp0mfAbUijXF3FUfzWKeTjw60wYkh+vRR8DrezDLGP2hMtUU0K0PpaMO7TmveA==", - "dev": true, - "dependencies": { - "crawler": "^1.2.1" - }, - "bin": { - "credits-generator": "index.js" - } - }, "node_modules/@opengovsg/formsg-sdk": { "version": "0.12.0-alpha.1", "resolved": "https://registry.npmjs.org/@opengovsg/formsg-sdk/-/formsg-sdk-0.12.0-alpha.1.tgz", @@ -6261,9 +7109,9 @@ } }, "node_modules/@opengovsg/sgid-client": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@opengovsg/sgid-client/-/sgid-client-2.0.0.tgz", - "integrity": "sha512-zqcVQz03zB7dAwWh2MJVRAmHYjK1EryqOPnbBgrkr8Jx8BjtcjFa4cCrHstwWP1kVkGomhi0C7e3TRvf1qYSFQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opengovsg/sgid-client/-/sgid-client-2.2.0.tgz", + "integrity": "sha512-xx/duuvncxp3vLzz+A0CW5kD/+zq19M23H7ueCh1ySPcB1fixUqsYKJAukk2En6FTI62jUizeJVjXOsd7rhl+A==", "dependencies": { "jose": "4.9.2", "node-rsa": "1.1.1", @@ -8213,22 +9061,45 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@smithy/util-waiter": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.0.8.tgz", - "integrity": "sha512-t9yaoofNhdEhNlyDeV5al/JJEFJ62HIQBGktgCUE63MvKn6imnbkh1qISsYMyMYVLwhWCpZ3Xa3R1LA+SnWcng==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.5.tgz", + "integrity": "sha512-jYOSvM3H6sZe3CHjzD2VQNCjWBJs+4DbtwBMvUp9y5EnnwNa7NQxTeYeQw0CKCAdGGZ3QvVkyJmvbvs5M/B10A==", "dependencies": { - "@smithy/abort-controller": "^2.0.8", - "@smithy/types": "^2.3.2", - "tslib": "^2.5.0" + "@smithy/abort-controller": "^3.1.4", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-waiter/node_modules/@smithy/abort-controller": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.4.tgz", + "integrity": "sha512-VupaALAQlXViW3/enTf/f5l5JZYSAxoJL7f0nanhNNKnww6DGCg1oYIuNP78KDugnkwthBO6iEcym16HhWV8RQ==", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-waiter/node_modules/@smithy/types": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.4.2.tgz", + "integrity": "sha512-tHiFcfcVedVBHpmHUEUHOCCih8iZbIAYn9NvPsNzaPm/237I3imdDdZoOC8c87H5HBAVEa06tTgb+OcSWV9g5w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/@smithy/util-waiter/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", @@ -8240,9 +9111,9 @@ "license": "MIT" }, "node_modules/@stoplight/http-spec": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@stoplight/http-spec/-/http-spec-7.0.2.tgz", - "integrity": "sha512-4DvT0w5goAhLxVbHfdzkMqGcTdi9bU4LmBrYNrZBOCFV4JPAHRERSBdI7F7n/MfgVvzxWb3Vftrh6pCgTd/+Jg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@stoplight/http-spec/-/http-spec-7.1.0.tgz", + "integrity": "sha512-Z2XqKX2SV8a1rrgSzFqccX2TolfcblT+l4pNvUU+THaLl50tKDoeidwWWZTzYUzqU0+UV97ponvqEbWWN3PaXg==", "dev": true, "dependencies": { "@stoplight/json": "^3.18.1", @@ -8254,7 +9125,7 @@ "fnv-plus": "^1.3.1", "lodash": "^4.17.21", "openapi3-ts": "^2.0.2", - "postman-collection": "^4.1.2", + "postman-collection": "^4.1.3", "tslib": "^2.6.2", "type-is": "^1.6.18" }, @@ -8276,15 +9147,15 @@ } }, "node_modules/@stoplight/http-spec/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, "node_modules/@stoplight/json": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.0.tgz", - "integrity": "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g==", + "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", "dev": true, "dependencies": { "@stoplight/ordered-object-literal": "^1.0.3", @@ -8388,22 +9259,21 @@ } }, "node_modules/@stoplight/prism-cli": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/@stoplight/prism-cli/-/prism-cli-5.5.4.tgz", - "integrity": "sha512-MvJUQcd8Fb3cuJuggjWinclz/JlHBeqZd5cYJtyJYs1Cz/woXjYF+0KeDviTdUTXEcTLhFguXS8vUK9vxqZ01g==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stoplight/prism-cli/-/prism-cli-5.10.0.tgz", + "integrity": "sha512-U7XMCAlXZFh2GPvzpZeMJ4u5V47ncYxgIxbLy4Y8rjsBCKWKQqqUCFoENXDub2O6ibZKSQctIvO0HiMZlIWERw==", "dev": true, "dependencies": { - "@stoplight/http-spec": "^7.0.2", - "@stoplight/json": "^3.18.1", + "@stoplight/json": "3.21.7", "@stoplight/json-schema-ref-parser": "9.2.7", - "@stoplight/prism-core": "^5.5.4", - "@stoplight/prism-http": "^5.5.4", - "@stoplight/prism-http-server": "^5.5.4", + "@stoplight/prism-core": "^5.8.0", + "@stoplight/prism-http": "5.10.0", + "@stoplight/prism-http-server": "^5.10.0", "@stoplight/types": "^14.1.0", "chalk": "^4.1.2", "chokidar": "^3.5.2", "fp-ts": "^2.11.5", - "json-schema-faker": "0.5.3", + "json-schema-faker": "0.5.6", "lodash": "^4.17.21", "node-fetch": "^2.6.5", "pino": "^6.13.3", @@ -8418,7 +9288,7 @@ "prism": "dist/index.js" }, "engines": { - "node": ">=16" + "node": ">=18.20.1" } }, "node_modules/@stoplight/prism-cli/node_modules/ansi-styles": { @@ -8545,9 +9415,9 @@ } }, "node_modules/@stoplight/prism-core": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/@stoplight/prism-core/-/prism-core-5.5.4.tgz", - "integrity": "sha512-P/I1UD2HwP02EocTRFtjRJe3BeQbJASIGMbE1/rT5fHlYeOiMb+IerFfrTp+/AGO5a193UEDut3XBXR3dBuQcw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@stoplight/prism-core/-/prism-core-5.8.0.tgz", + "integrity": "sha512-fmH7n6e0thzOGcD5uZBu/Xx1iFNfpc9ACTxPie+lFD54SJ214M2FIFXD7kV+NCFlC+w5OFw+lJRaYM859uMnAg==", "dev": true, "dependencies": { "fp-ts": "^2.11.5", @@ -8556,26 +9426,28 @@ "tslib": "^2.3.1" }, "engines": { - "node": ">=16" + "node": ">=18.20.1" } }, "node_modules/@stoplight/prism-core/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, "node_modules/@stoplight/prism-http": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/@stoplight/prism-http/-/prism-http-5.5.4.tgz", - "integrity": "sha512-W7NV3W09iAbAWj4ft6tqcBC2kdXpgArWSHO15glxQwRu1Y7I/U1Nr6EUhqQwyBgF5DWB5WFW/mnj30fZuyxGzw==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stoplight/prism-http/-/prism-http-5.10.0.tgz", + "integrity": "sha512-OlWSv9XobxkNu4D1xfDfwjMSETrw4wNFkWcbKzQmepNERqJ6uuWDDjkIR2f1F7SYU61ElwhaXf1bA9dlze6U6g==", "dev": true, "dependencies": { "@faker-js/faker": "^6.0.0", - "@stoplight/json": "^3.18.1", + "@stoplight/http-spec": "^7.0.3", + "@stoplight/json": "3.21.7", "@stoplight/json-schema-merge-allof": "0.7.8", + "@stoplight/json-schema-ref-parser": "9.2.7", "@stoplight/json-schema-sampler": "0.3.0", - "@stoplight/prism-core": "^5.5.4", + "@stoplight/prism-core": "^5.8.0", "@stoplight/types": "^14.1.0", "@stoplight/yaml": "^4.2.3", "abstract-logging": "^2.0.1", @@ -8588,7 +9460,7 @@ "fp-ts": "^2.11.5", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "json-schema-faker": "0.5.3", + "json-schema-faker": "0.5.6", "lodash": "^4.17.21", "node-fetch": "^2.6.5", "parse-multipart-data": "^1.5.0", @@ -8599,17 +9471,17 @@ "whatwg-mimetype": "^3.0.0" }, "engines": { - "node": ">=16" + "node": ">=18.20.1" } }, "node_modules/@stoplight/prism-http-server": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/@stoplight/prism-http-server/-/prism-http-server-5.5.4.tgz", - "integrity": "sha512-2k/hWfxq+P7HQkoFpB9Bb18s+JDiiIZADXIQLYvk/bx6LO/cVxoZst9kqtM7UxugYrfkemYPP9/7/z0v73+ifg==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stoplight/prism-http-server/-/prism-http-server-5.10.0.tgz", + "integrity": "sha512-7sdMNzBY4+/96fLQn7/LR6ItDfNwcvCY9XCWrpruWeq5fHJHrb5OynpHvZs9vHcTpZNITBs/sgmukXn35wB5Sw==", "dev": true, "dependencies": { - "@stoplight/prism-core": "^5.5.4", - "@stoplight/prism-http": "^5.5.4", + "@stoplight/prism-core": "^5.8.0", + "@stoplight/prism-http": "^5.10.0", "@stoplight/types": "^14.1.0", "fast-xml-parser": "^4.2.0", "fp-ts": "^2.11.5", @@ -8622,13 +9494,13 @@ "type-is": "^1.6.18" }, "engines": { - "node": ">=16" + "node": ">=18.20.1" } }, "node_modules/@stoplight/prism-http-server/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, "node_modules/@stoplight/prism-http/node_modules/@faker-js/faker": { @@ -8642,15 +9514,15 @@ } }, "node_modules/@stoplight/prism-http/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -8748,9 +9620,9 @@ } }, "node_modules/@stoplight/prism-http/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, "node_modules/@stoplight/types": { @@ -9119,9 +9991,9 @@ } }, "node_modules/@types/busboy": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.3.tgz", - "integrity": "sha512-YMBLFN/xBD8bnqywIlGyYqsNFXu6bsiY7h3Ae0kO17qEuTjsqeyYMRPSUDacIKIquws2Y6KjmxAyNx8xB3xQbw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", "dev": true, "dependencies": { "@types/node": "*" @@ -9201,9 +10073,10 @@ } }, "node_modules/@types/dedent": { - "version": "0.7.0", - "dev": true, - "license": "MIT" + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@types/dedent/-/dedent-0.7.2.tgz", + "integrity": "sha512-kRiitIeUg1mPV9yH4VUJ/1uk2XjyANfeL8/7rH1tsjvHeO9PJLBHJIYsFWmAvmGj5u8rj+1TZx7PZzW2qLw3Lw==", + "dev": true }, "node_modules/@types/ejs": { "version": "3.1.5", @@ -9435,9 +10308,9 @@ } }, "node_modules/@types/opossum": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@types/opossum/-/opossum-6.2.3.tgz", - "integrity": "sha512-KcbU4O7GydLbRLZbKRdFikNHHAsQESYIPlA6HEm+CCaZfEk+/NTGlvY8g5t8pg/ks3U80oEf8a21lUAjW311Og==", + "version": "8.1.7", + "resolved": "https://registry.npmjs.org/@types/opossum/-/opossum-8.1.7.tgz", + "integrity": "sha512-nIihJLNA412EioGg7mHTwDY7nfmSb4TpnSMSsftZYQ+vvKf1GYz3WSWnKrxGyFaeWpoTD5FTnZabkhR8ql50Iw==", "dev": true, "dependencies": { "@types/node": "*" @@ -10586,15 +11459,15 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -10666,10 +11539,6 @@ "node": ">=4" } }, - "node_modules/ansicolors": { - "version": "0.2.1", - "license": "MIT" - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -10940,14 +11809,6 @@ "util": "0.10.3" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assert/node_modules/inherits": { "version": "2.0.1", "dev": true, @@ -11132,9 +11993,9 @@ "license": "MIT" }, "node_modules/aws-sdk": { - "version": "2.1659.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1659.0.tgz", - "integrity": "sha512-WOoy5DdWW4kpQuxjWiQdoSDR+dT/HeAUwjb6b+8taEMZzvUzp3fmdDwdryUTlLWGxrnb7ru2yu5pryjhPOzANg==", + "version": "2.1691.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1691.0.tgz", + "integrity": "sha512-/F2YC+DlsY3UBM2Bdnh5RLHOPNibS/+IcjUuhP8XuctyrN+MlL+fWDAiela32LTDk7hMy4rx8MTgvbJ+0blO5g==", "hasInstallScript": true, "dependencies": { "buffer": "4.9.2", @@ -11168,23 +12029,10 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.10.0", - "dev": true, - "license": "MIT" - }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -11396,13 +12244,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -11627,19 +12475,6 @@ "node": ">= 10.0.0" } }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { - "version": "0.14.5", - "dev": true, - "license": "Unlicense" - }, "node_modules/big.js": { "version": "5.2.2", "dev": true, @@ -11752,18 +12587,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/bottleneckp": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/bottleneckp/-/bottleneckp-1.1.3.tgz", - "integrity": "sha512-f3XqkhYX2xuSxgZNtk/XqT1CHgYLTHK90SVQZjEZqOr+F6ryZA3xEsMQWqWFpRItTbc4X/dnjsE8p+gnr16qgA==", - "dev": true - }, "node_modules/bowser": { "version": "2.11.0", "license": "MIT" @@ -12094,21 +12917,11 @@ } ] }, - "node_modules/cardinal": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "ansicolors": "~0.2.1", - "redeyed": "~1.0.0" - }, - "bin": { - "cdl": "bin/cdl.js" - } - }, "node_modules/caseless": { "version": "0.12.0", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true }, "node_modules/celebrate": { "version": "15.0.3", @@ -12150,33 +12963,6 @@ "node": ">=4.0.0" } }, - "node_modules/cheerio": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", - "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", - "dev": true, - "dependencies": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash.assignin": "^4.0.9", - "lodash.bind": "^4.1.4", - "lodash.defaults": "^4.0.1", - "lodash.filter": "^4.4.0", - "lodash.flatten": "^4.2.0", - "lodash.foreach": "^4.3.0", - "lodash.map": "^4.4.0", - "lodash.merge": "^4.4.0", - "lodash.pick": "^4.2.1", - "lodash.reduce": "^4.4.0", - "lodash.reject": "^4.4.0", - "lodash.some": "^4.4.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/chokidar": { "version": "3.5.3", "funding": [ @@ -13051,12 +13837,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "dev": true, "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.23.3" }, "funding": { "type": "opencollective", @@ -13079,39 +13865,6 @@ "node": ">= 0.10" } }, - "node_modules/coveralls": { - "version": "3.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "js-yaml": "^3.13.1", - "lcov-parse": "^1.0.0", - "log-driver": "^1.2.7", - "minimist": "^1.2.5", - "request": "^2.88.2" - }, - "bin": { - "coveralls": "bin/coveralls.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/crawler": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/crawler/-/crawler-1.4.0.tgz", - "integrity": "sha512-rFg8/AtpQ5c8SItxwamlP4fGClnDd/n5GOdFakq0KtCdTzXL2HqzROyrAfEWPYuFzX2n+avafe3hQiFGKKOLXw==", - "dev": true, - "dependencies": { - "bottleneckp": "~1.1.3", - "cheerio": "^0.22.0", - "iconv-lite": "^0.4.8", - "lodash": "^4.17.10", - "request": "~2.88.0", - "seenreq": "^3.0.0", - "type-is": "^1.6.14" - } - }, "node_modules/create-ecdh": { "version": "4.0.4", "dev": true, @@ -13212,27 +13965,6 @@ "version": "1.0.0", "license": "MIT" }, - "node_modules/css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", - "dev": true, - "dependencies": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "node_modules/css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -13281,17 +14013,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dashdash": { - "version": "1.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -13889,16 +14610,6 @@ "node": ">=10" } }, - "node_modules/dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "dev": true, - "dependencies": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, "node_modules/domain-browser": { "version": "1.2.0", "dev": true, @@ -13908,41 +14619,20 @@ "npm": ">=1.2" } }, - "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "node_modules/domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "dependencies": { - "domelementtype": "1" - } - }, "node_modules/dompurify": { "version": "2.3.9", "dev": true, "license": "(MPL-2.0 OR Apache-2.0)" }, - "node_modules/domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", - "dev": true, - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, "node_modules/dotenv": { - "version": "16.0.3", - "license": "BSD-2-Clause", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/duplexify": { @@ -13968,15 +14658,6 @@ "version": "0.2.0", "license": "MIT" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "license": "Apache-2.0", @@ -14185,12 +14866,6 @@ "node": ">=8.6" } }, - "node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, "node_modules/env-cmd": { "version": "10.1.0", "dev": true, @@ -14760,13 +15435,13 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -14809,6 +15484,14 @@ "eslint": ">6.6.0" } }, + "node_modules/eslint-plugin-turbo/node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/eslint-plugin-typesafe": { "version": "0.5.2", "dev": true, @@ -15410,17 +16093,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "3.0.0", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -15487,10 +16159,6 @@ "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz", "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==" }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "license": "MIT" - }, "node_modules/events": { "version": "1.1.1", "license": "MIT", @@ -15681,9 +16349,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.2.0.tgz", - "integrity": "sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.0.tgz", + "integrity": "sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg==", "engines": { "node": ">= 16" }, @@ -15954,14 +16622,6 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/extsprintf": { - "version": "1.3.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -16001,9 +16661,9 @@ "license": "MIT" }, "node_modules/fast-redact": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", - "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", "dev": true, "engines": { "node": ">=6" @@ -16013,6 +16673,12 @@ "version": "2.1.1", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, "node_modules/fast-xml-parser": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", @@ -16358,14 +17024,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/form-data": { "version": "4.0.0", "license": "MIT", @@ -16741,14 +17399,6 @@ "node": ">=0.10.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "7.1.6", "license": "ISC", @@ -16885,26 +17535,6 @@ "integrity": "sha512-cUduQxa5p3TFtGmb55mrRbkk/3EJCsLSeFrCIuTakQHQlYVWXeW2L9IUQUHyoHLI4UgpBNaN2JrZ0He1jPu+vg==", "dev": true }, - "node_modules/har-schema": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -17360,34 +17990,6 @@ "node": ">=8" } }, - "node_modules/htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "dependencies": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, - "node_modules/htmlparser2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/http-errors": { "version": "2.0.0", "license": "MIT", @@ -17431,20 +18033,6 @@ "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", "dev": true }, - "node_modules/http-signature": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", @@ -17475,12 +18063,6 @@ "node": ">=16.17.0" } }, - "node_modules/humanize": { - "version": "0.0.9", - "engines": { - "node": "*" - } - }, "node_modules/husky": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", @@ -18089,14 +18671,9 @@ "engines": { "node": ">= 0.4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -18155,11 +18732,6 @@ "whatwg-fetch": "^3.4.1" } }, - "node_modules/isstream": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -19267,9 +19839,9 @@ } }, "node_modules/jest-extended": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.2.4.tgz", - "integrity": "sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", "dev": true, "dependencies": { "jest-diff": "^29.0.0", @@ -20554,11 +21126,6 @@ "node": ">=4" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "dev": true, - "license": "MIT" - }, "node_modules/jsdom": { "version": "24.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", @@ -20759,10 +21326,6 @@ "bluebird": "*" } }, - "node_modules/json-schema": { - "version": "0.2.3", - "dev": true - }, "node_modules/json-schema-compare": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", @@ -20773,9 +21336,9 @@ } }, "node_modules/json-schema-faker": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.3.tgz", - "integrity": "sha512-BeIrR0+YSrTbAR9dOMnjbFl1MvHyXnq+Wpdw1FpWZDHWKLzK229hZ5huyPcmzFUfVq1ODwf40WdGVoE266UBUg==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.6.tgz", + "integrity": "sha512-u/cFC26/GDxh2vPiAC8B8xVvpXAW+QYtG2mijEbKrimCk8IHtiwQBjCE8TwvowdhALWq9IcdIWZ+/8ocXvdL3Q==", "dev": true, "dependencies": { "json-schema-ref-parser": "^6.1.0", @@ -20912,20 +21475,6 @@ "node": ">=10" } }, - "node_modules/jsprim": { - "version": "1.4.1", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, "node_modules/jszip": { "version": "3.10.1", "license": "(MIT OR GPL-3.0-or-later)", @@ -21016,14 +21565,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/lcov-parse": { - "version": "1.0.0", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "lcov-parse": "bin/cli.js" - } - }, "node_modules/leac": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", @@ -21438,18 +21979,6 @@ "version": "4.2.0", "license": "MIT" }, - "node_modules/lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", - "dev": true - }, - "node_modules/lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", - "dev": true - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -21465,34 +21994,10 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true - }, - "node_modules/lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", - "dev": true - }, "node_modules/lodash.find": { "version": "4.6.0", "license": "MIT" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, - "node_modules/lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", - "dev": true - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -21531,12 +22036,6 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, - "node_modules/lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "dev": true, @@ -21552,29 +22051,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", - "dev": true - }, - "node_modules/lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", - "dev": true - }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", - "dev": true - }, "node_modules/lodash.sortby": { "version": "4.7.0", "license": "MIT" @@ -21584,14 +22060,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-driver": { - "version": "1.2.7", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=0.8.6" - } - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -22318,12 +22786,6 @@ "node": ">=8.6" } }, - "node_modules/microplugin": { - "version": "0.0.3", - "engines": { - "node": "*" - } - }, "node_modules/miller-rabin": { "version": "4.0.1", "dev": true, @@ -22987,9 +23449,12 @@ } }, "node_modules/neverthrow": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-6.1.0.tgz", - "integrity": "sha512-xNbNjp/6M5vUV+mststgneJN9eJeJCDSYSBTaf3vxgvcKooP+8L0ATFpM8DGfmH7UWKJeoa24Qi33tBP9Ya3zA==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-8.0.0.tgz", + "integrity": "sha512-SX2Z50+U27I+CF3NwHE9J8MB6+bYRRub3U+1nAKxnL6c+2vW2l/WsYEC0e3Wqg8DwiJvrquqE0YhxlVTzGJGsg==", + "engines": { + "node": ">=18" + } }, "node_modules/new-find-package-json": { "version": "2.0.0", @@ -23327,15 +23792,6 @@ "asn1": "^0.2.4" } }, - "node_modules/node-url-utils": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-url-utils/-/node-url-utils-0.4.0.tgz", - "integrity": "sha512-cg9J2VQ0nNvrZR+edoDaAoInvGRgi2r/LCSwOwNCgVASBo0rQiDbSwa4s0MpEcpPf1p4qokC14BXMx5TPxSU5w==", - "dev": true, - "engines": { - "node": ">=0.2.x" - } - }, "node_modules/nodemailer": { "version": "6.9.13", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", @@ -23426,28 +23882,11 @@ "set-blocking": "^2.0.0" } }, - "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "dependencies": { - "boolbase": "~1.0.0" - } - }, "node_modules/nwsapi": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==" }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -23701,25 +24140,13 @@ } }, "node_modules/opossum": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/opossum/-/opossum-7.1.0.tgz", - "integrity": "sha512-u3KZa2JTVsewFILe2NIebLMii/zzszTTBxRnM9USxVNcq2R2me40uP38/B39GueeOABXgWGtClPZPg4NAwHU4g==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-8.1.4.tgz", + "integrity": "sha512-ktDDCD2MKX7yx8ZLtt57JrUE0IyYJthmTyXtaF4dEdPYHvX0kkWRiKkQkhEjtZnTNN5y1ErMriKNqOLqgpYXtQ==", "engines": { - "node": "^19 || ^18 || ^16 || ^14" - } - }, - "node_modules/optimist": { - "version": "0.6.1", - "license": "MIT/X11", - "dependencies": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "node": "^22 || ^21 || ^20 || ^18 || ^16" } }, - "node_modules/optimist/node_modules/minimist": { - "version": "0.0.10", - "license": "MIT" - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -23836,13 +24263,6 @@ "node": ">=4" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "2.3.0", "license": "MIT", @@ -23866,30 +24286,6 @@ "node": ">=8" } }, - "node_modules/p-queue": { - "version": "6.6.2", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-try": { "version": "2.2.0", "license": "MIT", @@ -24177,11 +24573,6 @@ "version": "1.2.0", "license": "MIT" }, - "node_modules/performance-now": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -24553,9 +24944,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/postman-collection": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.3.0.tgz", - "integrity": "sha512-QpmNOw1JhAVQTFWRz443/qpKs4/3T1MFrKqDZ84RS1akxOzhXXr15kD8+/+jeA877qyy9rfMsrFgLe2W7aCPjw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.5.0.tgz", + "integrity": "sha512-152JSW9pdbaoJihwjc7Q8lc3nPg/PC9lPTHdMk7SHnHhu/GBJB7b2yb9zG7Qua578+3PxkQ/HYBuXpDSvsf7GQ==", "dev": true, "dependencies": { "@faker-js/faker": "5.5.3", @@ -24567,7 +24958,7 @@ "mime-format": "2.0.1", "mime-types": "2.1.35", "postman-url-encoder": "3.0.5", - "semver": "7.5.4", + "semver": "7.6.3", "uuid": "8.3.2" }, "engines": { @@ -24593,13 +24984,10 @@ } }, "node_modules/postman-collection/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -24996,15 +25384,6 @@ } ] }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/querystring": { "version": "0.2.0", "engines": { @@ -25550,13 +25929,6 @@ "node": ">= 0.10" } }, - "node_modules/redeyed": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "esprima": "~3.0.0" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -25770,57 +26142,6 @@ "node": ">=0.10" } }, - "node_modules/request": { - "version": "2.88.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", @@ -26167,15 +26488,6 @@ "version": "2.1.0", "license": "BSD-3-Clause" }, - "node_modules/seenreq": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/seenreq/-/seenreq-3.0.0.tgz", - "integrity": "sha512-wSe7hb83TKkyweL8Jq5a1xuStmqfwxiJn2SXjA/Wns42aUJjlWzPzj/jWaomOCRY5ZpIRkiyh/+5pNz/20363A==", - "dev": true, - "dependencies": { - "node-url-utils": "^0.4.0" - } - }, "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", @@ -26187,20 +26499,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/selectize": { - "version": "0.12.6", - "license": "Apache-2.0", - "dependencies": { - "microplugin": "0.0.3", - "sifter": "^0.5.1" - }, - "engines": { - "node": "*" - }, - "peerDependencies": { - "jquery": "^1.7.0, ^2, ^3" - } - }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -26511,31 +26809,6 @@ "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" }, - "node_modules/sifter": { - "version": "0.5.4", - "license": "Apache-2.0", - "dependencies": { - "async": "^2.6.0", - "cardinal": "^1.0.0", - "csv-parse": "^4.6.5", - "humanize": "^0.0.9", - "optimist": "^0.6.1" - }, - "bin": { - "sifter": "bin/sifter.js" - } - }, - "node_modules/sifter/node_modules/async": { - "version": "2.6.4", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/sifter/node_modules/csv-parse": { - "version": "4.16.3", - "license": "MIT" - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -26914,10 +27187,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/sortablejs": { - "version": "1.14.0", - "license": "MIT" - }, "node_modules/source-list-map": { "version": "2.0.1", "dev": true, @@ -27048,35 +27317,6 @@ "node": ">=12.0.0" } }, - "node_modules/sshpk": { - "version": "1.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sshpk/node_modules/tweetnacl": { - "version": "0.14.5", - "dev": true, - "license": "Unlicense" - }, "node_modules/stack-trace": { "version": "0.0.10", "license": "MIT", @@ -27634,9 +27874,9 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "dependencies": { "@pkgr/core": "^0.1.0", @@ -27650,9 +27890,9 @@ } }, "node_modules/synckit/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, "node_modules/table": { @@ -28120,18 +28360,6 @@ "node": ">=0.6" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -28535,17 +28763,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/tweetnacl": { "version": "1.0.3", "license": "Unlicense" @@ -29242,19 +29459,6 @@ "node": ">= 0.8" } }, - "node_modules/verror": { - "version": "1.10.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "dev": true, @@ -30133,13 +30337,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "0.0.3", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/worker-farm": { "version": "1.7.0", "dev": true, diff --git a/package.json b/package.json index 2cda4336a4..143e32d9f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "6.148.0", + "version": "6.149.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " @@ -56,20 +56,20 @@ }, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.536.0", - "@aws-sdk/client-lambda": "^3.414.0", + "@aws-sdk/client-lambda": "^3.654.0", "@babel/runtime": "^7.24.7", "@faker-js/faker": "^8.4.1", "@growthbook/growthbook": "^1.1.0", "@joi/date": "^2.1.0", "@opengovsg/formsg-sdk": "^0.12.0-alpha.1", "@opengovsg/myinfo-gov-client": "^4.1.2", - "@opengovsg/sgid-client": "^2.0.0", + "@opengovsg/sgid-client": "^2.2.0", "@react-email/components": "^0.0.15", "@react-email/render": "^0.0.12", "@stablelib/base64": "^1.0.1", "aws-info": "^1.2.0", - "aws-sdk": "^2.1659.0", - "axios": "^1.7.4", + "aws-sdk": "^2.1691.0", + "axios": "^1.7.7", "bcrypt": "^5.1.1", "bluebird": "^3.5.2", "body-parser": "^1.20.3", @@ -88,10 +88,10 @@ "date-fns": "^2.30.0", "dd-trace": "^5.22.0", "dedent-js": "~1.0.1", - "dotenv": "^16.0.3", + "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.20.0", - "express-rate-limit": "^7.2.0", + "express-rate-limit": "^7.4.0", "express-request-id": "^1.4.1", "express-session": "^1.18.0", "express-winston": "^4.2.0", @@ -121,21 +121,18 @@ "multer": "^1.4.5-lts.1", "multiparty": ">=4.2.3", "nan": "^2.19.0", - "neverthrow": "^6.1.0", + "neverthrow": "^8.0.0", "nocache": "^3.0.4", "node-cache": "^5.1.2", "nodemailer": "^6.9.13", "openid-client": "^5.3.1", - "opossum": "^7.1.0", - "p-queue": "^6.6.2", + "opossum": "^8.1.4", "promise-retry": "^2.0.1", "promise-timeout": "^1.3.0", "puppeteer-core": "22.6.3", "react-email": "^2.1.3", - "selectize": "0.12.6", "slick-carousel": "1.8.1", "sns-validator": "^0.3.5", - "sortablejs": "~1.14.0", "spark-md5": "^3.0.2", "sqs-consumer": "^5.7.0", "sqs-producer": "^2.1.0", @@ -157,19 +154,18 @@ "devDependencies": { "@babel/core": "^7.24.3", "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/preset-env": "^7.25.3", - "@opengovsg/credits-generator": "^1.0.6", + "@babel/preset-env": "^7.25.4", "@opengovsg/mockpass": "^4.3.4", "@playwright/test": "^1.45.1", - "@stoplight/prism-cli": "^5.5.4", + "@stoplight/prism-cli": "^5.10.0", "@types/bcrypt": "^5.0.0", "@types/bluebird": "^3.5.42", - "@types/busboy": "^1.5.3", + "@types/busboy": "^1.5.4", "@types/compression": "^1.7.5", "@types/connect-datadog": "0.0.6", "@types/convict": "^6.1.6", "@types/cookie-parser": "^1.4.7", - "@types/dedent": "^0.7.0", + "@types/dedent": "^0.7.2", "@types/ejs": "^3.1.5", "@types/express": "^4.17.21", "@types/express-request-id": "^1.4.3", @@ -188,7 +184,7 @@ "@types/multer": "^1.4.11", "@types/node": "^14.18.23", "@types/nodemailer": "^6.4.15", - "@types/opossum": "^6.2.3", + "@types/opossum": "^8.1.7", "@types/promise-retry": "^1.1.3", "@types/promise-timeout": "^1.3.0", "@types/puppeteer-core": "^5.4.0", @@ -205,14 +201,13 @@ "axios-mock-adapter": "^1.22.0", "concurrently": "^7.6.0", "copyfiles": "^2.4.1", - "coveralls": "^3.1.1", "env-cmd": "^10.1.0", "eslint": "^8.57.0", "eslint-config-prettier": "^8.10.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-playwright": "^1.6.0", - "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-typesafe": "^0.5.2", "form-data": "^4.0.0", @@ -220,7 +215,7 @@ "husky": "^9.1.6", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "jest-extended": "^3.2.4", + "jest-extended": "^4.0.2", "jest-localstorage-mock": "^2.4.26", "jest-mock-axios": "^4.7.2", "lint-staged": "^15.2.7", diff --git a/shared/constants/form.ts b/shared/constants/form.ts index 9d4b8331f8..29900e2049 100644 --- a/shared/constants/form.ts +++ b/shared/constants/form.ts @@ -61,7 +61,6 @@ export const STORAGE_FORM_SETTINGS_FIELDS = [ export const MULTIRESPONDENT_FORM_SETTINGS_FIELDS = [ ...FORM_SETTINGS_FIELDS, 'publicKey', - 'workflow', 'emails', 'stepsToNotify', ] diff --git a/shared/package-lock.json b/shared/package-lock.json index d478633318..62867becb6 100644 --- a/shared/package-lock.json +++ b/shared/package-lock.json @@ -109,9 +109,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz", + "integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -590,9 +590,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.8.tgz", - "integrity": "sha512-0fv/YKpJBAgXKy0kaS3fnqoUVN8901vUYAKIGD/MWZaDfhJt1nZjPL3ZzdZBt/G8G8Hw2J1xOIrXWdNHFHPAvg==" + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.9.tgz", + "integrity": "sha512-Zs5wf5HaWzW2/inlupe2tstl0I/Tbqo7lH20ZLr6Is58u7Dz2n+gRFGNlj9/gWxFvNfp9+YyDsiegjNhdixB9A==" }, "node_modules/lie": { "version": "3.3.0", diff --git a/shared/types/form/workflow.ts b/shared/types/form/workflow.ts index 439895bcf2..a317eb24f4 100644 --- a/shared/types/form/workflow.ts +++ b/shared/types/form/workflow.ts @@ -8,6 +8,7 @@ export enum WorkflowType { export interface FormWorkflowStepBase { workflow_type: WorkflowType edit: FormFieldDto['_id'][] + approval_field?: FormFieldDto['_id'] } export interface FormWorkflowStepStatic extends FormWorkflowStepBase { diff --git a/src/app/loaders/express/constants.ts b/src/app/loaders/express/constants.ts index 0bd5830d7a..84090d4082 100644 --- a/src/app/loaders/express/constants.ts +++ b/src/app/loaders/express/constants.ts @@ -41,6 +41,7 @@ export const CSP_CORE_DIRECTIVES = { 'https://www.google-analytics.com/', 'https://ssl.google-analytics.com/', 'https://*.browser-intake-datadoghq.com', + 'https://browser-intake-datadoghq.com', config.aws.attachmentBucketUrl, config.aws.imageBucketUrl, config.aws.logoBucketUrl, diff --git a/src/app/models/form_workflow_step.server.schema.ts b/src/app/models/form_workflow_step.server.schema.ts index 16e029a04b..bf84c557d8 100644 --- a/src/app/models/form_workflow_step.server.schema.ts +++ b/src/app/models/form_workflow_step.server.schema.ts @@ -21,6 +21,9 @@ const WorkflowStepSchema = new Schema( type: [{ type: Schema.Types.ObjectId }], required: true, }, + approval_field: { + type: Schema.Types.ObjectId, + }, }, { discriminatorKey: 'workflow_type', diff --git a/src/app/modules/core/core.errors.ts b/src/app/modules/core/core.errors.ts index 733c533443..c182ddec8b 100644 --- a/src/app/modules/core/core.errors.ts +++ b/src/app/modules/core/core.errors.ts @@ -39,6 +39,7 @@ export enum ErrorCodes { ADMIN_FORM_GOGOV_REQUEST_LIMIT = 100108, ADMIN_FORM_GOGOV_BAD_GATEWAY = 100109, ADMIN_FORM_GOGOV_SERVER = 100110, + ADMIN_FORM_INVALID_APPROVAL_FIELD_TYPE = 100111, // [100200 - 100299] Submission Errors (/modules/submission) SUBMISSION_CONFLICT = 100200, SUBMISSION_NOT_FOUND = 100201, @@ -63,6 +64,8 @@ export enum ErrorCodes { SUBMISSION_MALICIOUS_FILE_DETECTED = 100220, SUBMISSION_INVALID_WORKFLOW_TYPE = 100221, SUBMISSION_ATTACHMENT_UPLOAD = 100222, + SUBMISSION_EXPECTED_RESPONSE_NOT_FOUND = 100224, + SUBMISSION_SAVE_FAILURE = 100225, // Email Submission Errors (email mode deprecated soon) EMAIL_SUBMISSION_HASH = 100223, // [100300 - 100399] Receiver Errors (/modules/submission/receiver) diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index 1b85a96d76..5ce6a1940f 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -32,6 +32,8 @@ import { FormSettings, FormWebhookResponseModeSettings, FormWebhookSettings, + FormWorkflowDto, + FormWorkflowStepDto, LogicConditionState, LogicDto, LogicIfValue, @@ -99,9 +101,11 @@ import { } from './admin-form.constants' import { EditFieldError, GoGovServerError } from './admin-form.errors' import { + createWorkflowStepValidator, getWebhookSettingsValidator, updateSettingsValidator, updateWebhookSettingsValidator, + updateWorkflowStepValidator, } from './admin-form.middlewares' import * as AdminFormService from './admin-form.service' import { PermissionLevel } from './admin-form.types' @@ -1410,6 +1414,26 @@ export const _handleUpdateSettings: ControllerHandler< }) } +/** + * Handler for PATCH /forms/:formId/settings. + * @security session + * + * @returns 200 with updated form settings + * @returns 400 when body is malformed + * @returns 403 when current user does not have permissions to update form settings + * @returns 404 when form to update settings for cannot be found + * @returns 409 when saving form settings incurs a conflict in the database + * @returns 410 when updating settings for archived form + * @returns 413 when updating settings causes form to be too large to be saved in the database + * @returns 422 when an invalid settings update is attempted on the form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const handleUpdateSettings = [ + updateSettingsValidator, + _handleUpdateSettings, +] as ControllerHandler[] + export const _handleUpdateWebhookSettings: ControllerHandler< { formId: string }, FormWebhookSettings | ErrorDto, @@ -1468,25 +1492,173 @@ export const _handleUpdateWebhookSettings: ControllerHandler< } /** - * Handler for PATCH /forms/:formId/settings. + * Handler for PATCH api/public/v1/admin/forms/:formId/webhooksettings. * @security session * * @returns 200 with updated form settings * @returns 400 when body is malformed - * @returns 403 when current user does not have permissions to update form settings + * @returns 403 when user email does not have permissions to update form settings * @returns 404 when form to update settings for cannot be found * @returns 409 when saving form settings incurs a conflict in the database * @returns 410 when updating settings for archived form * @returns 413 when updating settings causes form to be too large to be saved in the database * @returns 422 when an invalid settings update is attempted on the form - * @returns 422 when user in session cannot be retrieved from the database + * @returns 422 when user from user email cannot be retrieved from the database * @returns 500 when database error occurs */ -export const handleUpdateSettings = [ - updateSettingsValidator, - _handleUpdateSettings, +export const handleUpdateWebhookSettings = [ + updateWebhookSettingsValidator, + _handleUpdateWebhookSettings, ] as ControllerHandler[] +export const _handleCreateWorkflowStep: ControllerHandler< + { formId: string }, + FormWorkflowDto | ErrorDto, + FormWorkflowStepDto +> = (req, res) => { + const { formId } = req.params + const workflowStepToCreate = req.body + const sessionUserId = (req.session as AuthedSessionData).user._id + + // Step 1: Retrieve currently logged in user. + return ( + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Retrieve form with write permission check. + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Write, + }), + ) + // Step 3: User has permissions, proceed to create form field with provided body. + .andThen((form) => + AdminFormService.createWorkflowStep(form, workflowStepToCreate), + ) + .map((updatedWorkflow) => + res.status(StatusCodes.OK).json(updatedWorkflow), + ) + .mapErr((error) => { + logger.error({ + message: 'Error occurred when creating form field', + meta: { + action: 'handleCreateFormField', + ...createReqMeta(req), + userId: sessionUserId, + formId, + workflowStepToCreate, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + +export const handleCreateWorkflowStep = [ + createWorkflowStepValidator, + _handleCreateWorkflowStep, +] + +const _handleUpdateWorkflowStep: ControllerHandler< + { + formId: string + stepNumber: number + }, + FormWorkflowDto | ErrorDto, + FormWorkflowStepDto +> = (req, res) => { + const { formId, stepNumber } = req.params + const sessionUserId = (req.session as AuthedSessionData).user._id + const updatedWorkflowStep = req.body + + // Step 1: Retrieve currently logged in user. + return UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Retrieve form with write permission check. + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Write, + }), + ) + .andThen((retrievedForm) => + AdminFormService.updateFormWorkflowStep( + retrievedForm, + stepNumber, + updatedWorkflowStep, + ), + ) + .map((updatedWorkflow) => res.status(StatusCodes.OK).json(updatedWorkflow)) + .mapErr((error) => { + logger.error({ + message: 'Error occurred when updating form workflow step', + meta: { + action: 'handleUpdateWorkflowStep', + ...createReqMeta(req), + userId: sessionUserId, + formId, + updatedWorkflowStep, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) +} + +export const handleUpdateWorkflowStep = [ + updateWorkflowStepValidator, + _handleUpdateWorkflowStep, +] as ControllerHandler[] + +export const handleDeleteWorkflowStep: ControllerHandler< + { + formId: string + stepNumber: number + }, + FormWorkflowDto | ErrorDto +> = (req, res) => { + const { formId, stepNumber } = req.params + const sessionUserId = (req.session as AuthedSessionData).user._id + + // Step 1: Retrieve currently logged in user. + return ( + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Retrieve form with write permission check. + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Write, + }), + ) + // Step 3: Delete workflow step. + .andThen((retrievedForm) => + AdminFormService.deleteFormWorkflowStep(retrievedForm, stepNumber), + ) + .map((updatedWorkflow) => + res.status(StatusCodes.OK).json(updatedWorkflow), + ) + .mapErr((error) => { + logger.error({ + message: 'Error occurred when deleting form workflow step', + meta: { + action: 'handleDeleteWorkflowStep', + ...createReqMeta(req), + userId: sessionUserId, + formId, + stepNumber, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + const TWENTY_MB_IN_BYTES = 20 * 1024 * 1024 const handleWhitelistSettingMultipartBody = multer({ limits: { @@ -1628,26 +1800,6 @@ export const handleUpdateWhitelistSetting = [ _handleUpdateWhitelistSetting, ] as ControllerHandler[] -/** - * Handler for PATCH api/public/v1/admin/forms/:formId/webhooksettings. - * @security session - * - * @returns 200 with updated form settings - * @returns 400 when body is malformed - * @returns 403 when user email does not have permissions to update form settings - * @returns 404 when form to update settings for cannot be found - * @returns 409 when saving form settings incurs a conflict in the database - * @returns 410 when updating settings for archived form - * @returns 413 when updating settings causes form to be too large to be saved in the database - * @returns 422 when an invalid settings update is attempted on the form - * @returns 422 when user from user email cannot be retrieved from the database - * @returns 500 when database error occurs - */ -export const handleUpdateWebhookSettings = [ - updateWebhookSettingsValidator, - _handleUpdateWebhookSettings, -] as ControllerHandler[] - /** * NOTE: Exported for testing. * Private handler for PUT /forms/:formId/fields/:fieldId diff --git a/src/app/modules/form/admin-form/admin-form.middlewares.ts b/src/app/modules/form/admin-form/admin-form.middlewares.ts index 094e0c72e4..ccac631259 100644 --- a/src/app/modules/form/admin-form/admin-form.middlewares.ts +++ b/src/app/modules/form/admin-form/admin-form.middlewares.ts @@ -41,24 +41,6 @@ export const updateSettingsValidator = celebrate({ gstRegNo: Joi.string().allow(''), }), payments_field: Joi.object({ gst_enabled: Joi.boolean() }), - workflow: Joi.array() - .items( - Joi.object({ - _id: Joi.string(), - workflow_type: Joi.string().valid(...Object.values(WorkflowType)), - emails: Joi.when('workflow_type', { - is: WorkflowType.Static, - then: Joi.array().items(Joi.string().email()).required(), - }), - // TODO: Add regex validation that these are valid mongo IDs - field: Joi.when('workflow_type', { - is: WorkflowType.Dynamic, - then: Joi.string().required(), - }), - edit: Joi.array().items(Joi.string()).required(), - }), - ) - .optional(), }) .min(1) .custom((value, helpers) => verifyValidUnicodeString(value, helpers)), @@ -82,3 +64,49 @@ export const getWebhookSettingsValidator = celebrate({ userEmail: Joi.string().email().optional(), }), }) + +/** + * Joi validator for POST /forms/:formId/workflow/ route. + */ +export const createWorkflowStepValidator = celebrate({ + [Segments.BODY]: Joi.object({ + workflow_type: Joi.string().valid(...Object.values(WorkflowType)), + emails: Joi.when('workflow_type', { + is: WorkflowType.Static, + then: Joi.array().items(Joi.string().email()).required(), + }), + field: Joi.when('workflow_type', { + is: WorkflowType.Dynamic, + then: Joi.string().required(), + }), + edit: Joi.array().items(Joi.string()).required(), + approval_field: Joi.string().optional(), + }), + [Segments.PARAMS]: Joi.object({ + formId: Joi.string().required(), + }), +}) + +/** + * Joi validator for PUT /forms/:formId/workflow/:stepNumber route. + */ +export const updateWorkflowStepValidator = celebrate({ + [Segments.BODY]: Joi.object({ + _id: Joi.string().required(), + workflow_type: Joi.string().valid(...Object.values(WorkflowType)), + emails: Joi.when('workflow_type', { + is: WorkflowType.Static, + then: Joi.array().items(Joi.string().email()).required(), + }), + field: Joi.when('workflow_type', { + is: WorkflowType.Dynamic, + then: Joi.string().required(), + }), + edit: Joi.array().items(Joi.string().hex().length(24)).required(), + approval_field: Joi.string().optional(), + }), + [Segments.PARAMS]: Joi.object({ + formId: Joi.string().required(), + stepNumber: Joi.number().integer().min(0).required(), + }), +}) diff --git a/src/app/modules/form/admin-form/admin-form.service.ts b/src/app/modules/form/admin-form/admin-form.service.ts index 5398739fd3..f7319d6d05 100644 --- a/src/app/modules/form/admin-form/admin-form.service.ts +++ b/src/app/modules/form/admin-form/admin-form.service.ts @@ -25,6 +25,7 @@ import { import { MYINFO_ATTRIBUTE_MAP } from '../../../../../shared/constants/field/myinfo' import { AdminDashboardFormMetaDto, + BasicField, DuplicateFormOverwriteDto, EndPageUpdateDto, FieldCreateDto, @@ -34,11 +35,14 @@ import { FormPermission, FormResponseMode, FormSettings, + FormWorkflowDto, + FormWorkflowStepDto, LogicDto, PaymentChannel, SettingsUpdateDto, StartPageUpdateDto, StorageFormSettings, + WorkflowType, } from '../../../../../shared/types' import { isMFinSeriesValid, @@ -52,7 +56,11 @@ import { IForm, IFormDocument, IFormSchema, + IMultirespondentForm, + IMultirespondentFormModel, + IMultirespondentFormSchema, IPopulatedForm, + IPopulatedMultirespondentForm, IPopulatedUser, } from '../../../../types' import { EditFormFieldParams, FormUpdateParams } from '../../../../types/api' @@ -484,6 +492,11 @@ export const transferAllFormsOwnership = ( ) } +type MultirespondentFormToCreate = Merge< + IMultirespondentForm, + { admin: string } +> + /** * Creates a form with the given form params * @param formParams parameters for the form to be created. @@ -506,6 +519,78 @@ export const createForm = ( formParams, } + if (formParams.responseMode === FormResponseMode.Multirespondent) { + const workflow = (formParams as MultirespondentFormToCreate).workflow ?? [] + + const emailFieldIds = formParams.form_fields + ?.filter((field) => field.fieldType === BasicField.Email) + .map((field) => field._id.toString()) + const isRespondentFieldEmail = workflow?.every((step) => { + return ( + step.workflow_type !== WorkflowType.Dynamic || + (step.field && emailFieldIds?.includes(step.field)) + ) + }) + if (!isRespondentFieldEmail) { + return errAsync( + new MalformedParametersError( + 'All respondent fields in workflow must be email fields', + ), + ) + } + + const isFirstStepApproval = workflow[0] && workflow[0].approval_field + if (isFirstStepApproval) { + return errAsync( + new MalformedParametersError( + 'First step of workflow cannot be an approval step', + ), + ) + } + + const yesNoFieldIds = formParams.form_fields + ?.filter((field) => field.fieldType === BasicField.YesNo) + .map((field) => field._id.toString()) + const isApprovalFieldYesNo = workflow.every((step) => { + return ( + !step.approval_field || yesNoFieldIds?.includes(step.approval_field) + ) + }) + if (!isApprovalFieldYesNo) { + return errAsync( + new MalformedParametersError( + 'All approval fields must be yes/no fields', + ), + ) + } + + const selectedApprovalFields = workflow + .map((step) => step.approval_field) + .filter(Boolean) + const isApprovalFieldUnique = + new Set(selectedApprovalFields).size === selectedApprovalFields.length + if (!isApprovalFieldUnique) { + return errAsync( + new MalformedParametersError( + 'Each yes/no field cannot be used in more than one approval step', + ), + ) + } + + const isApprovalFieldInEditFields = workflow.every( + (step) => + !step.approval_field || + (step.edit && step.edit.includes(step.approval_field)), + ) + if (!isApprovalFieldInEditFields) { + return errAsync( + new MalformedParametersError( + 'Approval fields must be included in edit fields.', + ), + ) + } + } + if (workspaceId) return ResultAsync.fromPromise( createFormInWorkspaceTransaction(formParams, workspaceId), @@ -1244,6 +1329,316 @@ export const updateFormWhitelistSetting = ( }) } +export const createWorkflowStep = ( + originalForm: IPopulatedForm, + newWorkflowStep: FormWorkflowStepDto, +): ResultAsync => { + if (originalForm.responseMode !== FormResponseMode.Multirespondent) { + return errAsync( + new MalformedParametersError( + 'Cannot update workflow step for non-multirespondent mode forms', + ), + ) + } + + if ( + newWorkflowStep.workflow_type === WorkflowType.Dynamic && + newWorkflowStep.field + ) { + const emailFieldIds = originalForm.form_fields + .filter((field) => field.fieldType === BasicField.Email) + .map((field) => field._id.toString()) + const isEmailField = emailFieldIds.includes(newWorkflowStep.field) + if (!isEmailField) { + return errAsync( + new MalformedParametersError( + 'Respondent field must be a valid email field', + ), + ) + } + } + + const isFirstStep = + (originalForm as IPopulatedMultirespondentForm).workflow.length === 0 + if (isFirstStep && newWorkflowStep.approval_field) { + return errAsync( + new MalformedParametersError( + 'First step of workflow cannot be an approval step', + ), + ) + } + + const selectedApprovalField = newWorkflowStep.approval_field + if (selectedApprovalField) { + const yesNoFieldIds = originalForm.form_fields + .filter((field) => field.fieldType === BasicField.YesNo) + .map((field) => field._id.toString()) + const isYesNoField = yesNoFieldIds.includes(selectedApprovalField) + if (!isYesNoField) { + return errAsync( + new MalformedParametersError( + 'Approval field must be a valid yes/no field', + ), + ) + } + + const otherApprovalFields = ( + originalForm as IPopulatedMultirespondentForm + ).workflow + .map((step) => step.approval_field) + .filter(Boolean) + + const isAlreadyUsed = otherApprovalFields.includes(selectedApprovalField) + if (isAlreadyUsed) { + return errAsync( + new MalformedParametersError( + 'Approval field has already been used in another step', + ), + ) + } + + const editFields = newWorkflowStep.edit ?? [] + const isApprovalFieldInEditFields = editFields.includes( + selectedApprovalField, + ) + if (!isApprovalFieldInEditFields) { + return errAsync( + new MalformedParametersError( + "Approval field must also be in the same step's edit fields", + ), + ) + } + } + + const originalMrfForm = originalForm as IPopulatedMultirespondentForm + const originalWorkflow = originalMrfForm.workflow ?? [] + + // Create new workflow step + const updatedWorkflow = originalWorkflow.concat(newWorkflowStep) + + const MultirespondentFormModel = getFormModelByResponseMode( + originalForm.responseMode, + ) as IMultirespondentFormModel + + return ResultAsync.fromPromise( + MultirespondentFormModel.findByIdAndUpdate( + originalMrfForm._id, + { workflow: updatedWorkflow }, + { + new: true, + runValidators: true, + }, + ).exec(), + (error) => { + logger.error({ + message: + 'Error encountered while creating new form workflow step in database', + meta: { + action: 'createWorkflowStep', + formId: originalMrfForm._id, + newWorkflowStep, + }, + error, + }) + return transformMongoError(error) + }, + ).andThen((updatedForm) => { + if (!updatedForm) { + return errAsync(new FormNotFoundError()) + } + return okAsync((updatedForm as IMultirespondentFormSchema).workflow) + }) +} + +export const updateFormWorkflowStep = ( + originalForm: IPopulatedForm, + stepNumber: number, + updatedWorkflowStep: FormWorkflowStepDto, +): ResultAsync => { + if (originalForm.responseMode !== FormResponseMode.Multirespondent) { + return errAsync( + new MalformedParametersError( + 'Cannot update workflow step for non-multirespondent mode forms', + ), + ) + } + + if ( + updatedWorkflowStep.workflow_type === WorkflowType.Dynamic && + updatedWorkflowStep.field + ) { + const emailFieldIds = originalForm.form_fields + .filter((field) => field.fieldType === BasicField.Email) + .map((field) => field._id.toString()) + const isEmailField = emailFieldIds.includes(updatedWorkflowStep.field) + if (!isEmailField) { + return errAsync( + new MalformedParametersError( + 'Respondent field must be a valid email field', + ), + ) + } + } + + const isFirstStep = stepNumber === 0 + if (isFirstStep && updatedWorkflowStep.approval_field) { + return errAsync( + new MalformedParametersError( + 'First step of workflow cannot be an approval step', + ), + ) + } + + const selectedApprovalField = updatedWorkflowStep.approval_field + if (selectedApprovalField) { + const yesNoFieldIds = originalForm.form_fields + .filter((field) => field.fieldType === BasicField.YesNo) + .map((field) => field._id.toString()) + const isYesNoField = yesNoFieldIds.includes(selectedApprovalField) + if (!isYesNoField) { + return errAsync( + new MalformedParametersError( + 'Approval field must be a valid yes/no field', + ), + ) + } + + const otherApprovalFields = ( + originalForm as IPopulatedMultirespondentForm + ).workflow + .map((step, index) => (index !== stepNumber ? step.approval_field : null)) + .filter(Boolean) + + const isAlreadyUsed = otherApprovalFields.includes(selectedApprovalField) + if (isAlreadyUsed) { + return errAsync( + new MalformedParametersError( + 'Approval field has already been used in another step', + ), + ) + } + + const editFields = updatedWorkflowStep.edit ?? [] + const isApprovalFieldInEditFields = editFields.includes( + selectedApprovalField, + ) + if (!isApprovalFieldInEditFields) { + return errAsync( + new MalformedParametersError( + "Approval field must also be in the same step's edit fields", + ), + ) + } + } + + const originalMrfForm = originalForm as IPopulatedMultirespondentForm + const originalWorkflow = originalMrfForm.workflow ?? [] + + const isStepNumberValid = + stepNumber >= 0 && stepNumber < originalWorkflow.length + if (!isStepNumberValid) { + return errAsync(new MalformedParametersError('Invalid step number')) + } + + const updatedWorkflow = originalMrfForm.workflow.map((step, index) => + index === stepNumber ? updatedWorkflowStep : step, + ) + + const MultirespondentFormModel = getFormModelByResponseMode( + originalForm.responseMode, + ) as IMultirespondentFormModel + + return ResultAsync.fromPromise( + MultirespondentFormModel.findByIdAndUpdate( + originalMrfForm._id, + { workflow: updatedWorkflow }, + { + new: true, + runValidators: true, + }, + ).exec(), + (error) => { + logger.error({ + message: + 'Error encountered while updating form workflow step in database', + meta: { + action: 'updateFormWorkflowStep', + formId: originalMrfForm._id, + stepNumber, + updatedWorkflowStep, + }, + error, + }) + return transformMongoError(error) + }, + ).andThen((updatedForm) => { + if (!updatedForm) { + return errAsync(new FormNotFoundError()) + } + + return okAsync((updatedForm as IMultirespondentFormSchema).workflow) + }) +} + +export const deleteFormWorkflowStep = ( + originalForm: IPopulatedForm, + stepNumber: number, +): ResultAsync => { + if (originalForm.responseMode !== FormResponseMode.Multirespondent) { + return errAsync( + new MalformedParametersError( + 'Cannot update workflow step for non-multirespondent mode forms', + ), + ) + } + + const originalMrfForm = originalForm as IPopulatedMultirespondentForm + const originalWorkflow = originalMrfForm.workflow ?? [] + + const isStepNumberValid = + stepNumber >= 0 && stepNumber < originalWorkflow.length + if (!isStepNumberValid) { + return errAsync(new MalformedParametersError('Invalid step number')) + } + + // Remove step with stepNumber from workflow + const updatedWorkflow = originalWorkflow + updatedWorkflow.splice(stepNumber, 1) + + const MultirespondentFormModel = getFormModelByResponseMode( + originalForm.responseMode, + ) as IMultirespondentFormModel + + return ResultAsync.fromPromise( + MultirespondentFormModel.findByIdAndUpdate( + originalMrfForm._id, + { workflow: updatedWorkflow }, + { + new: true, + runValidators: true, + }, + ).exec(), + (error) => { + logger.error({ + message: + 'Error encountered while deleting form workflow step in database', + meta: { + action: 'deleteFormWorkflowStep', + formId: originalMrfForm._id, + stepNumber, + }, + error, + }) + return transformMongoError(error) + }, + ).andThen((updatedForm) => { + if (!updatedForm) { + return errAsync(new FormNotFoundError()) + } + return okAsync((updatedForm as IMultirespondentFormSchema).workflow) + }) +} + /** * Updates form settings. * @param originalForm The original form to update settings for diff --git a/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts b/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts index e96359efa0..09f4f23b3e 100644 --- a/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts +++ b/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts @@ -798,7 +798,7 @@ describe('public-form.controller', () => { expect((mockRes.json as jest.Mock).mock.calls[0][0]).not.toContainKey( 'spcpSession', ) - expect(mockRes.clearCookie).toHaveBeenCalledOnceWith( + expect(mockRes.clearCookie).toHaveBeenCalledExactlyOnceWith( getCookieNameByAuthType(FormAuthType.SP), ) }) diff --git a/src/app/modules/payments/__tests__/stripe.controller.spec.ts b/src/app/modules/payments/__tests__/stripe.controller.spec.ts index b316e83124..c9fea3ca46 100644 --- a/src/app/modules/payments/__tests__/stripe.controller.spec.ts +++ b/src/app/modules/payments/__tests__/stripe.controller.spec.ts @@ -146,15 +146,15 @@ describe('stripe.controller', () => { expect(mockRes.redirect).toHaveBeenCalledWith( `${config.app.appUrl}/admin/form/${MOCK_FORM_ID}/settings/payments`, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledOnceWith( - MOCK_FORM_ID, - ) + expect( + MockFormService.retrieveFullFormById, + ).toHaveBeenCalledExactlyOnceWith(MOCK_FORM_ID) expect( MockStripeService.exchangeCodeForAccessToken, - ).toHaveBeenCalledOnceWith('someCode') + ).toHaveBeenCalledExactlyOnceWith('someCode') expect( MockStripeService.linkStripeAccountToForm, - ).toHaveBeenCalledOnceWith(mockForm, { + ).toHaveBeenCalledExactlyOnceWith(mockForm, { accountId: mockStripeToken.stripe_user_id, publishableKey: mockStripeToken.stripe_publishable_key, }) @@ -183,9 +183,9 @@ describe('stripe.controller', () => { expect(mockRes.redirect).toHaveBeenCalledWith( `${config.app.appUrl}/admin/form/formId/settings/payments`, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledOnceWith( - 'formId', - ) + expect( + MockFormService.retrieveFullFormById, + ).toHaveBeenCalledExactlyOnceWith('formId') expect( MockStripeService.exchangeCodeForAccessToken, ).not.toHaveBeenCalledWith() diff --git a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts index e06245e022..022285be5e 100644 --- a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts +++ b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts @@ -645,7 +645,7 @@ describe('encrypt-submission.controller', () => { // Assert email notification not sent since submission not allowed expect(MockMailService.sendSubmissionToAdmin).toHaveBeenCalledTimes(0) - expect(mockRes.status).toHaveBeenCalledOnceWith(403) + expect(mockRes.status).toHaveBeenCalledExactlyOnceWith(403) expect((mockRes.json as jest.Mock).mock.calls[0][0].message).toEqual( FORM_RESPONDENT_NOT_WHITELISTED_ERROR_MESSAGE, diff --git a/src/app/modules/submission/multirespondent-submission/__tests__/multirespondent-submission.controller.spec.ts b/src/app/modules/submission/multirespondent-submission/__tests__/multirespondent-submission.controller.spec.ts index c7d597a5d3..33c40537a6 100644 --- a/src/app/modules/submission/multirespondent-submission/__tests__/multirespondent-submission.controller.spec.ts +++ b/src/app/modules/submission/multirespondent-submission/__tests__/multirespondent-submission.controller.spec.ts @@ -1,81 +1,189 @@ -import dbHandler from '__tests__/unit/backend/helpers/jest-db' import expressHandler from '__tests__/unit/backend/helpers/jest-express' import { ObjectId } from 'bson' -import { merge } from 'lodash' -import mongoose from 'mongoose' -import { ok, okAsync } from 'neverthrow' -import { FormAuthType, FormWorkflowStepDto, WorkflowType } from 'shared/types' +import { merge, omit } from 'lodash' +import { Types } from 'mongoose' +import { errAsync, ok, okAsync } from 'neverthrow' +import { FormAuthType } from 'shared/types' -import { getMultirespondentSubmissionModel } from 'src/app/models/submission.server.model' import * as FormService from 'src/app/modules/form/form.service' -import MailService from 'src/app/services/mail/mail.service' +import { MailSendError } from 'src/app/services/mail/mail.errors' +import { IMultirespondentSubmissionSchema } from 'src/types' +import { + AttachmentUploadError, + InvalidWorkflowTypeError, + SubmissionNotFoundError, + SubmissionSaveError, +} from '../../submission.errors' import { submitMultirespondentFormForTest, updateMultirespondentSubmissionForTest, } from '../multirespondent-submission.controller' +import * as MultiRespondentSubmissionService from '../multirespondent-submission.service' jest.mock('src/app/modules/datadog/datadog.utils') -const MultiRespondentSubmission = getMultirespondentSubmissionModel(mongoose) - +jest.mock('src/app/modules/form/form.service') const MockFormService = jest.mocked(FormService) - -describe('multirespondent-submission.controller', () => { - beforeAll(async () => { - await dbHandler.connect() +jest.mock( + 'src/app/modules/submission/multirespondent-submission/multirespondent-submission.service', +) +const MockMultiRespondentSubmissionService = jest.mocked( + MultiRespondentSubmissionService, +) + +const mockFormId = new ObjectId().toHexString() +const mockMrfForm = { + _id: mockFormId, + workflow: [], +} +const mockSubmissionId = new ObjectId().toHexString() +const mockMrfSubmission = { + _id: mockSubmissionId, +} as IMultirespondentSubmissionSchema & { _id: Types.ObjectId } + +describe('multiresponodent-submision.controller', () => { + beforeEach(() => { + MockFormService.isFormPublic = jest.fn().mockReturnValue(ok(true)) + MockFormService.checkFormSubmissionLimitAndDeactivateForm = jest + .fn() + .mockReturnValue(okAsync(mockMrfForm)) + + MockMultiRespondentSubmissionService.createMultiRespondentFormSubmission = + jest.fn().mockReturnValue(okAsync(mockMrfSubmission)) + MockMultiRespondentSubmissionService.updateMultiRespondentFormSubmission = + jest.fn().mockReturnValue(okAsync(mockMrfSubmission)) + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionCreateActions = + jest.fn().mockReturnValue(okAsync(true)) + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionUpdateActions = + jest.fn().mockReturnValue(okAsync(true)) }) - afterEach(async () => { + afterEach(() => { jest.clearAllMocks() - await dbHandler.clearDatabase() }) - afterAll(async () => { - await dbHandler.closeDatabase() - }) + describe('submitMultirespondentForm', () => { + it('returns 200 ok when form validation passes and invokes createMultiRespondentFormSubmission and performMultiRespondentPostSubmissionCreateActions', async () => { + // Arrange + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, + }, + body: {} as any, + }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), + }, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 0, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() + + // Act + await submitMultirespondentFormForTest(mockSubmitMrfReq, mockRes) + + // Assert + // save submission is invoked with correct args + expect( + MockMultiRespondentSubmissionService.createMultiRespondentFormSubmission, + ).toHaveBeenCalledOnce() + expect( + omit( + MockMultiRespondentSubmissionService + .createMultiRespondentFormSubmission.mock.calls[0][0], + 'logMeta', + ), + ).toEqual({ + encryptedPayload: mockSubmitMrfReq.formsg.encryptedPayload, + form: mockSubmitMrfReq.formsg.formDef, + }) - const mockFormId = new ObjectId().toHexString() - const mockMrfForm = { - _id: mockFormId, - workflow: [], - } - - const mockSubmissionId = new ObjectId().toHexString() - - describe('mrf completion email notification', () => { - beforeAll(() => { - MockFormService.isFormPublic = jest.fn().mockReturnValue(ok(true)) - MockFormService.checkIsIntranetFormAccess = jest - .fn() - .mockReturnValue(false) - MockFormService.checkFormSubmissionLimitAndDeactivateForm = jest - .fn() - .mockReturnValue(okAsync(mockMrfForm)) + // Assert post save actions are invoked with correct args + expect( + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionCreateActions, + ).toHaveBeenCalledOnce() + expect( + omit( + MockMultiRespondentSubmissionService + .performMultiRespondentPostSubmissionCreateActions.mock.calls[0][0], + 'logMeta', + ), + ).toEqual({ + form: mockSubmitMrfReq.formsg.formDef, + encryptedPayload: mockSubmitMrfReq.formsg.encryptedPayload, + submissionId: mockSubmissionId, + }) + // Expect 200 ok + expect(mockRes.status).not.toHaveBeenCalled() // default is 200 ok }) - it('sends completion email when single step mrf is completed', async () => { + it('returns 400 bad request if attachment upload error occurs when createMultiRespondentFormSubmission', async () => { // Arrange - const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( - MailService, - 'sendMrfWorkflowCompletionEmail', - ) - - const singleStepWorkflow: FormWorkflowStepDto[] = [ - { - _id: new ObjectId().toHexString(), - workflow_type: WorkflowType.Static, - emails: [], - edit: [], + const attachmentUploadError = new AttachmentUploadError() + MockMultiRespondentSubmissionService.createMultiRespondentFormSubmission = + jest.fn().mockReturnValue(errAsync(attachmentUploadError)) + + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, }, - ] + body: {} as any, + }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), + }, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 0, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() + + // Act + await submitMultirespondentFormForTest(mockSubmitMrfReq, mockRes) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(400) + expect(mockRes.json).toHaveBeenCalledWith({ + message: + 'Could not upload attachments for submission. For assistance, please contact the person who asked you to fill in this form.', + }) + }) - MockFormService.checkFormSubmissionLimitAndDeactivateForm = jest - .fn() - .mockReturnValue(okAsync(mockMrfForm)) + it('returns 500 internal server error when submission fails to save', async () => { + // Arrange + const submissionSaveError = new SubmissionSaveError() + MockMultiRespondentSubmissionService.createMultiRespondentFormSubmission = + jest.fn().mockReturnValue(errAsync(submissionSaveError)) const mockReq = expressHandler.mockRequest({ - params: { formId: mockFormId }, + params: { + formId: mockFormId, + submissionId: mockSubmissionId, + }, body: {} as any, }) const mockSubmitMrfReq = merge(mockReq, { @@ -84,104 +192,111 @@ describe('multirespondent-submission.controller', () => { _id: mockFormId, authType: FormAuthType.NIL, getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), - workflow: singleStepWorkflow, - emails: ['email1@example.com'], }, encryptedPayload: { encryptedContent: 'encryptedContent', version: 1, submissionPublicKey: 'submissionPublicKey', encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 0, }, } as any, }) - const mockRes = expressHandler.mockResponse({}) + const mockRes = expressHandler.mockResponse() // Act await submitMultirespondentFormForTest(mockSubmitMrfReq, mockRes) // Assert - expect(sendMrfWorkflowCompletionEmailSpy).toHaveBeenCalledTimes(1) - expect( - sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails, - ).toContainAllValues(['email1@example.com']) - expect( - sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails.length, - ).toBe(1) + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + message: submissionSaveError.message, + }) }) - it('sends completion email when multi-step mrf is completed and only to specified steps only and also static emails', async () => { + it('returns 200 ok when step has invalid workflow type', async () => { // Arrange - const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( - MailService, - 'sendMrfWorkflowCompletionEmail', - ) - - const expectedEmails = [ - 'expected1@example.com', - 'expected2@example.com', - 'expected3@example.com', - 'expected4@example.com', - ] - - const stepOneId = new ObjectId().toHexString() - const stepTwoId = new ObjectId().toHexString() - const stepThreeId = new ObjectId().toHexString() - const stepFourId = new ObjectId().toHexString() - - const emailFieldId1 = new ObjectId().toHexString() - const emailFieldId2 = new ObjectId().toHexString() - - const submissionResponses = { - [emailFieldId1]: { - fieldType: 'email', - answer: { - value: expectedEmails[0], - }, + const invalidWorkflowTypeError = new InvalidWorkflowTypeError() + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionCreateActions = + jest.fn().mockReturnValue(errAsync(invalidWorkflowTypeError)) + + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, }, - [emailFieldId2]: { - fieldType: 'email', - answer: { - value: 'not_expected_1@example.com', + body: {} as any, + }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), }, - }, - } - - const fourStepWorkflow: FormWorkflowStepDto[] = [ - { - _id: stepOneId, - workflow_type: WorkflowType.Dynamic, - field: emailFieldId1, - edit: [], - }, - { - _id: stepTwoId, - workflow_type: WorkflowType.Static, - emails: ['not_expected_2@example.com'], - edit: [], - }, - { - _id: stepThreeId, - workflow_type: WorkflowType.Dynamic, - field: emailFieldId2, - edit: [], - }, - { - _id: stepFourId, - workflow_type: WorkflowType.Static, - emails: [expectedEmails[1], expectedEmails[2]], - edit: [], - }, - ] + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 0, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() - MockFormService.checkFormSubmissionLimitAndDeactivateForm = jest - .fn() - .mockReturnValue(okAsync(mockMrfForm)) + // Act + await submitMultirespondentFormForTest(mockSubmitMrfReq, mockRes) - MultiRespondentSubmission.findById = jest.fn().mockReturnValue({ - save: () => true, + // Assert + expect(mockRes.status).not.toHaveBeenCalled() // default is 200 ok + }) + + it('returns 200 ok when mail send error occurs', async () => { + // Arrange + const mailSendError = new MailSendError() + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionCreateActions = + jest.fn().mockReturnValue(errAsync(mailSendError)) + + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, + }, + body: {} as any, }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), + }, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 0, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() + + // Act + await submitMultirespondentFormForTest(mockSubmitMrfReq, mockRes) + + // Assert + expect(mockRes.status).not.toHaveBeenCalled() // default is 200 ok + }) + }) + describe('updateMultirespondentSubmission', () => { + it('returns 200 ok when form validation passes and invokes updateMultiRespondentFormSubmission and performMultiRespondentPostSubmissionUpdateActions', async () => { + // Arrange const mockReq = expressHandler.mockRequest({ params: { formId: mockFormId, @@ -195,107 +310,228 @@ describe('multirespondent-submission.controller', () => { _id: mockFormId, authType: FormAuthType.NIL, getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), - workflow: fourStepWorkflow, - emails: [expectedEmails[3]], - stepsToNotify: [stepOneId, stepFourId], }, encryptedPayload: { encryptedContent: 'encryptedContent', version: 1, submissionPublicKey: 'submissionPublicKey', encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', - responses: submissionResponses, - workflowStep: fourStepWorkflow.length - 1, // last step + responses: {}, + workflowStep: 0, }, } as any, }) - const mockRes = expressHandler.mockResponse({}) + const mockRes = expressHandler.mockResponse() // Act await updateMultirespondentSubmissionForTest(mockSubmitMrfReq, mockRes) // Assert - expect(sendMrfWorkflowCompletionEmailSpy).toHaveBeenCalledTimes(1) - // The emails sent to should only be the expected emails exactly + // save submission is invoked with correct args + expect( + MockMultiRespondentSubmissionService.updateMultiRespondentFormSubmission, + ).toHaveBeenCalledOnce() expect( - sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails, - ).toContainAllValues(expectedEmails) + omit( + MockMultiRespondentSubmissionService + .updateMultiRespondentFormSubmission.mock.calls[0][0], + 'logMeta', + ), + ).toEqual({ + formId: mockFormId, + submissionId: mockSubmissionId, + encryptedPayload: mockSubmitMrfReq.formsg.encryptedPayload, + form: mockSubmitMrfReq.formsg.formDef, + }) + + // Assert post save actions are invoked with correct args expect( - sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails.length, - ).toBe(expectedEmails.length) + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionUpdateActions, + ).toHaveBeenCalledOnce() + expect( + omit( + MockMultiRespondentSubmissionService + .performMultiRespondentPostSubmissionUpdateActions.mock.calls[0][0], + 'logMeta', + ), + ).toEqual({ + form: mockSubmitMrfReq.formsg.formDef, + encryptedPayload: mockSubmitMrfReq.formsg.encryptedPayload, + submissionId: mockSubmissionId, + }) + // Expect 200 ok + expect(mockRes.status).not.toHaveBeenCalled() // default is 200 ok }) - it('does not send completion email when step number >0 and mrf not completed', async () => { + it('returns 400 bad request when attachment upload fails when updateMultiRespondentFormSubmission', async () => { // Arrange - const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( - MailService, - 'sendMrfWorkflowCompletionEmail', - ) - - const selectedEmails = [ - 'seelcted1@example.com', - 'seelcted2@example.com', - 'seelcted3@example.com', - 'seelcted4@example.com', - ] - - const stepOneId = new ObjectId().toHexString() - const stepTwoId = new ObjectId().toHexString() - const stepThreeId = new ObjectId().toHexString() - const stepFourId = new ObjectId().toHexString() - - const emailFieldId1 = new ObjectId().toHexString() - const emailFieldId2 = new ObjectId().toHexString() - - const submissionResponses = { - [emailFieldId1]: { - fieldType: 'email', - answer: { - value: selectedEmails[0], - }, + const attachmentUploadError = new AttachmentUploadError() + MockMultiRespondentSubmissionService.updateMultiRespondentFormSubmission = + jest.fn().mockReturnValue(errAsync(attachmentUploadError)) + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, }, - [emailFieldId2]: { - fieldType: 'email', - answer: { - value: 'not_selected_1@example.com', + body: {} as any, + }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), }, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 1, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() + + // Act + await updateMultirespondentSubmissionForTest(mockSubmitMrfReq, mockRes) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(400) + expect(mockRes.json).toHaveBeenCalledWith({ + message: + 'Could not upload attachments for submission. For assistance, please contact the person who asked you to fill in this form.', + }) + }) + + it('returns 500 internal server error when submission fails to save', async () => { + // Arrange + const submissionSaveError = new SubmissionSaveError() + MockMultiRespondentSubmissionService.updateMultiRespondentFormSubmission = + jest.fn().mockReturnValue(errAsync(submissionSaveError)) + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, }, - } - - const fourStepWorkflow: FormWorkflowStepDto[] = [ - { - _id: stepOneId, - workflow_type: WorkflowType.Dynamic, - field: emailFieldId1, - edit: [], - }, - { - _id: stepTwoId, - workflow_type: WorkflowType.Static, - emails: ['not_selected_2@example.com'], - edit: [], - }, - { - _id: stepThreeId, - workflow_type: WorkflowType.Dynamic, - field: emailFieldId2, - edit: [], - }, - { - _id: stepFourId, - workflow_type: WorkflowType.Static, - emails: [selectedEmails[1], selectedEmails[2]], - edit: [], + body: {} as any, + }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), + }, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 1, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() + + // Act + await updateMultirespondentSubmissionForTest(mockSubmitMrfReq, mockRes) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + message: submissionSaveError.message, + submissionId: mockSubmissionId, + }) + }) + + it('returns 404 not found when submission id not found', async () => { + // Arrange + const submissionNotFoundError = new SubmissionNotFoundError() + MockMultiRespondentSubmissionService.updateMultiRespondentFormSubmission = + jest.fn().mockReturnValue(errAsync(submissionNotFoundError)) + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, }, - ] + body: {} as any, + }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), + }, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 1, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() + + // Act + await updateMultirespondentSubmissionForTest(mockSubmitMrfReq, mockRes) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(404) + expect(mockRes.json).toHaveBeenCalledWith({ + message: submissionNotFoundError.message, + }) + }) - MockFormService.checkFormSubmissionLimitAndDeactivateForm = jest - .fn() - .mockReturnValue(okAsync(mockMrfForm)) + it('returns 200 ok when mail send error occurs', async () => { + // Arrange + const mailSendError = new MailSendError() + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionUpdateActions = + jest.fn().mockReturnValue(errAsync(mailSendError)) - MultiRespondentSubmission.findById = jest.fn().mockReturnValue({ - save: () => true, + const mockReq = expressHandler.mockRequest({ + params: { + formId: mockFormId, + submissionId: mockSubmissionId, + }, + body: {} as any, }) + const mockSubmitMrfReq = merge(mockReq, { + formsg: { + formDef: { + _id: mockFormId, + authType: FormAuthType.NIL, + getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), + }, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: {}, + workflowStep: 1, + }, + } as any, + }) + const mockRes = expressHandler.mockResponse() + + // Act + await updateMultirespondentSubmissionForTest(mockSubmitMrfReq, mockRes) + + // Assert + expect(mockRes.status).not.toHaveBeenCalled() // default is 200 ok + }) + + it('returns 200 ok when step has invalid workflow type', async () => { + // Arrange + const invalidWorkflowTypeError = new InvalidWorkflowTypeError() + MockMultiRespondentSubmissionService.performMultiRespondentPostSubmissionUpdateActions = + jest.fn().mockReturnValue(errAsync(invalidWorkflowTypeError)) const mockReq = expressHandler.mockRequest({ params: { @@ -310,27 +546,24 @@ describe('multirespondent-submission.controller', () => { _id: mockFormId, authType: FormAuthType.NIL, getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), - workflow: fourStepWorkflow, - emails: [selectedEmails[3]], - stepsToNotify: [stepOneId, stepFourId], }, encryptedPayload: { encryptedContent: 'encryptedContent', version: 1, submissionPublicKey: 'submissionPublicKey', encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', - responses: submissionResponses, - workflowStep: fourStepWorkflow.length - 2, // not last step + responses: {}, + workflowStep: 1, }, } as any, }) - const mockRes = expressHandler.mockResponse({}) + const mockRes = expressHandler.mockResponse() // Act await updateMultirespondentSubmissionForTest(mockSubmitMrfReq, mockRes) // Assert - expect(sendMrfWorkflowCompletionEmailSpy).not.toHaveBeenCalled() + expect(mockRes.status).not.toHaveBeenCalled() // default is 200 ok }) }) }) diff --git a/src/app/modules/submission/multirespondent-submission/__tests__/multirespondent-submission.service.spec.ts b/src/app/modules/submission/multirespondent-submission/__tests__/multirespondent-submission.service.spec.ts new file mode 100644 index 0000000000..51f4ad9bfb --- /dev/null +++ b/src/app/modules/submission/multirespondent-submission/__tests__/multirespondent-submission.service.spec.ts @@ -0,0 +1,818 @@ +import dbHandler from '__tests__/unit/backend/helpers/jest-db' +import { ObjectId } from 'bson' +import { + BasicField, + FieldResponsesV3, + FormWorkflowStepDto, + WorkflowType, +} from 'shared/types' + +import MailService from 'src/app/services/mail/mail.service' +import { IPopulatedMultirespondentForm } from 'src/types' +import { MultirespondentSubmissionDto } from 'src/types/api' + +import { + performMultiRespondentPostSubmissionCreateActions, + performMultiRespondentPostSubmissionUpdateActions, +} from '../multirespondent-submission.service' + +jest.mock('src/app/modules/datadog/datadog.utils') + +describe('multirespondent-submission.service', () => { + beforeAll(async () => { + await dbHandler.connect() + }) + + afterEach(async () => { + jest.clearAllMocks() + await dbHandler.clearDatabase() + }) + + afterAll(async () => { + await dbHandler.closeDatabase() + }) + + const mockFormId = new ObjectId().toHexString() + const mockSubmissionId = new ObjectId().toHexString() + + describe('mrf approval email notification when approval step exists', () => { + it('workflow continues and does not send approved outcome email when mrf is approved for mid step of multiple step MRF', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + const sendMrfApprovalEmailSpy = jest.spyOn( + MailService, + 'sendMrfApprovalEmail', + ) + const sendMRFWorkflowStepEmailSpy = jest.spyOn( + MailService, + 'sendMRFWorkflowStepEmail', + ) + + const expectedEmails = ['expected1@example.com'] + + const stepOneId = new ObjectId().toHexString() + const stepTwoId = new ObjectId().toHexString() + const stepThreeId = new ObjectId().toHexString() + + const emailFieldId1 = new ObjectId().toHexString() + const emailFieldId2 = new ObjectId().toHexString() + + const yesNoFieldId1 = new ObjectId().toHexString() + const yesNoFieldId2 = new ObjectId().toHexString() + + const submissionResponses = { + [emailFieldId1]: { + fieldType: BasicField.Email, + answer: { + value: 'not_expected_1@example.com', + }, + }, + [emailFieldId2]: { + fieldType: BasicField.Email, + answer: { + value: expectedEmails[0], + }, + }, + [yesNoFieldId1]: { + fieldType: BasicField.YesNo, + answer: 'Yes', + }, + [yesNoFieldId2]: { + fieldType: BasicField.YesNo, + answer: 'No', + }, + } as FieldResponsesV3 + + const threeStepApprovalWorkflow: FormWorkflowStepDto[] = [ + { + _id: stepOneId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId1, + edit: [], + }, + { + _id: stepTwoId, + workflow_type: WorkflowType.Static, + emails: ['not_expected_2@example.com'], + edit: [yesNoFieldId1], + approval_field: yesNoFieldId1, + }, + { + _id: stepThreeId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId2, + edit: [yesNoFieldId2], + // no approval field for last step + }, + ] + + const currentStepNumber = 1 // 2nd step of 3 steps workflow + + // Act + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: threeStepApprovalWorkflow, + emails: [expectedEmails[1]], + stepsToNotify: [stepOneId], + } as IPopulatedMultirespondentForm, + currentStepNumber: currentStepNumber, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: submissionResponses, + workflowStep: currentStepNumber, + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + // Assert + // next workflow step email is sent only + expect(sendMrfApprovalEmailSpy).not.toHaveBeenCalled() + expect(sendMrfWorkflowCompletionEmailSpy).not.toHaveBeenCalled() + expect(sendMRFWorkflowStepEmailSpy).toHaveBeenCalledTimes(1) + // destination emails are correct + expect( + sendMRFWorkflowStepEmailSpy.mock.calls[0][0].emails, + ).toContainAllValues(expectedEmails) + expect(sendMRFWorkflowStepEmailSpy.mock.calls[0][0].emails.length).toBe( + expectedEmails.length, + ) + }) + + it('sends approved outcome email when mrf has approval step earlier but last step is not approval step', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + const sendMrfApprovalEmailSpy = jest.spyOn( + MailService, + 'sendMrfApprovalEmail', + ) + const sendMRFWorkflowStepEmailSpy = jest.spyOn( + MailService, + 'sendMRFWorkflowStepEmail', + ) + + const expectedEmails = ['expected1@example.com', 'expected2@example.com'] + + const stepOneId = new ObjectId().toHexString() + const stepTwoId = new ObjectId().toHexString() + const stepThreeId = new ObjectId().toHexString() + + const emailFieldId1 = new ObjectId().toHexString() + const emailFieldId2 = new ObjectId().toHexString() + + const yesNoFieldId1 = new ObjectId().toHexString() + const yesNoFieldId2 = new ObjectId().toHexString() + + const submissionResponses = { + [emailFieldId1]: { + fieldType: BasicField.Email, + answer: { + value: expectedEmails[0], + }, + }, + [emailFieldId2]: { + fieldType: BasicField.Email, + answer: { + value: 'not_expected_1@example.com', + }, + }, + [yesNoFieldId1]: { + fieldType: BasicField.YesNo, + answer: 'Yes', + }, + [yesNoFieldId2]: { + fieldType: BasicField.YesNo, + answer: 'No', + }, + } as FieldResponsesV3 + + const threeStepApprovalWorkflow: FormWorkflowStepDto[] = [ + { + _id: stepOneId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId1, + edit: [], + }, + { + _id: stepTwoId, + workflow_type: WorkflowType.Static, + emails: ['not_expected_2@example.com'], + edit: [yesNoFieldId1], + approval_field: yesNoFieldId1, + }, + { + _id: stepThreeId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId2, + edit: [yesNoFieldId2], + // no approval field for last step + }, + ] + + const currentWorkflowStep = 2 // last step of 3 step workflow + + // Act + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: threeStepApprovalWorkflow, + emails: [expectedEmails[1]], + stepsToNotify: [stepOneId], + } as IPopulatedMultirespondentForm, + currentStepNumber: currentWorkflowStep, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: submissionResponses, + workflowStep: currentWorkflowStep, + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + // Assert + // approval email is sent instead of completion email + expect(sendMrfApprovalEmailSpy).toHaveBeenCalledTimes(1) + expect(sendMrfWorkflowCompletionEmailSpy).not.toHaveBeenCalled() + expect(sendMRFWorkflowStepEmailSpy).not.toHaveBeenCalled() + // is approve email and destination emails are correct + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].isRejected).toBeFalse() + expect( + sendMrfApprovalEmailSpy.mock.calls[0][0].emails, + ).toContainAllValues(expectedEmails) + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].emails.length).toBe( + expectedEmails.length, + ) + }) + + it('sends approved outcome email when mrf is approved for last step of multiple step MRF', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + const sendMrfApprovalEmailSpy = jest.spyOn( + MailService, + 'sendMrfApprovalEmail', + ) + const sendMRFWorkflowStepEmailSpy = jest.spyOn( + MailService, + 'sendMRFWorkflowStepEmail', + ) + + const expectedEmails = ['expected1@example.com', 'expected2@example.com'] + + const stepOneId = new ObjectId().toHexString() + const stepTwoId = new ObjectId().toHexString() + const stepThreeId = new ObjectId().toHexString() + + const emailFieldId1 = new ObjectId().toHexString() + const emailFieldId2 = new ObjectId().toHexString() + + const yesNoFieldId1 = new ObjectId().toHexString() + const yesNoFieldId2 = new ObjectId().toHexString() + + const submissionResponses = { + [emailFieldId1]: { + fieldType: BasicField.Email, + answer: { + value: expectedEmails[0], + }, + }, + [emailFieldId2]: { + fieldType: BasicField.Email, + answer: { + value: 'not_expected_1@example.com', + }, + }, + [yesNoFieldId1]: { + fieldType: BasicField.YesNo, + answer: 'Yes', + }, + [yesNoFieldId2]: { + fieldType: BasicField.YesNo, + answer: 'Yes', + }, + } + + const threeStepApprovalWorkflow: FormWorkflowStepDto[] = [ + { + _id: stepOneId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId1, + edit: [], + }, + { + _id: stepTwoId, + workflow_type: WorkflowType.Static, + emails: ['not_expected_2@example.com'], + edit: [yesNoFieldId1], + approval_field: yesNoFieldId1, + }, + { + _id: stepThreeId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId2, + edit: [yesNoFieldId2], + approval_field: yesNoFieldId2, + }, + ] + + const currentWorkflowStep = 2 // last step of 3 step workflow + + // Act + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: threeStepApprovalWorkflow, + emails: [expectedEmails[1]], + stepsToNotify: [stepOneId], + } as IPopulatedMultirespondentForm, + currentStepNumber: currentWorkflowStep, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: submissionResponses, + workflowStep: threeStepApprovalWorkflow.length - 1, // last step + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + // Assert + // approval email is sent instead of completion email + expect(sendMrfApprovalEmailSpy).toHaveBeenCalledTimes(1) + expect(sendMrfWorkflowCompletionEmailSpy).not.toHaveBeenCalled() + expect(sendMRFWorkflowStepEmailSpy).not.toHaveBeenCalled() + // is approve email and destination emails are correct + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].isRejected).toBeFalse() + expect( + sendMrfApprovalEmailSpy.mock.calls[0][0].emails, + ).toContainAllValues(expectedEmails) + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].emails.length).toBe( + expectedEmails.length, + ) + }) + + it('workflow terminates and sends not approved outcome email when mrf is rejected for mid step of multiple step MRF', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + const sendMrfApprovalEmailSpy = jest.spyOn( + MailService, + 'sendMrfApprovalEmail', + ) + const sendMRFWorkflowStepEmailSpy = jest.spyOn( + MailService, + 'sendMRFWorkflowStepEmail', + ) + + const expectedEmails = ['expected1@example.com', 'expected2@example.com'] + + const stepOneId = new ObjectId().toHexString() + const stepTwoId = new ObjectId().toHexString() + const stepThreeId = new ObjectId().toHexString() + + const emailFieldId1 = new ObjectId().toHexString() + const emailFieldId2 = new ObjectId().toHexString() + + const yesNoFieldId1 = new ObjectId().toHexString() + const yesNoFieldId2 = new ObjectId().toHexString() + + const submissionResponses = { + [emailFieldId1]: { + fieldType: BasicField.Email, + answer: { + value: expectedEmails[0], + }, + }, + [emailFieldId2]: { + fieldType: BasicField.Email, + answer: { + value: 'not_expected_1@example.com', + }, + }, + [yesNoFieldId1]: { + fieldType: BasicField.YesNo, + answer: 'No', + }, + } as FieldResponsesV3 + + const threeStepApprovalWorkflow: FormWorkflowStepDto[] = [ + { + _id: stepOneId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId1, + edit: [], + }, + { + _id: stepTwoId, + workflow_type: WorkflowType.Static, + emails: ['not_expected_2@example.com'], + edit: [yesNoFieldId1], + approval_field: yesNoFieldId1, + }, + { + _id: stepThreeId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId2, + edit: [yesNoFieldId2], + approval_field: yesNoFieldId2, + }, + ] + + const currentStepNumber = 1 // 2nd step of 3 steps workflow + + // Act + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: threeStepApprovalWorkflow, + emails: [expectedEmails[1]], + stepsToNotify: [stepOneId], + } as IPopulatedMultirespondentForm, + currentStepNumber: currentStepNumber, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: submissionResponses, + workflowStep: currentStepNumber, + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + // Assert + // approval email is sent instead of completion email + expect(sendMrfApprovalEmailSpy).toHaveBeenCalledTimes(1) + expect(sendMrfWorkflowCompletionEmailSpy).not.toHaveBeenCalled() + expect(sendMRFWorkflowStepEmailSpy).not.toHaveBeenCalled() + // is rejected email and destination emails are correct + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].isRejected).toBeTrue() + expect( + sendMrfApprovalEmailSpy.mock.calls[0][0].emails, + ).toContainAllValues(expectedEmails) + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].emails.length).toBe( + expectedEmails.length, + ) + }) + + it('workflow terminates and sends not approved outcome email when mrf is rejected for last step of multiple step MRF', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + const sendMrfApprovalEmailSpy = jest.spyOn( + MailService, + 'sendMrfApprovalEmail', + ) + const sendMRFWorkflowStepEmailSpy = jest.spyOn( + MailService, + 'sendMRFWorkflowStepEmail', + ) + + const expectedEmails = ['expected1@example.com', 'expected2@example.com'] + + const stepOneId = new ObjectId().toHexString() + const stepTwoId = new ObjectId().toHexString() + const stepThreeId = new ObjectId().toHexString() + + const emailFieldId1 = new ObjectId().toHexString() + const emailFieldId2 = new ObjectId().toHexString() + + const yesNoFieldId1 = new ObjectId().toHexString() + const yesNoFieldId2 = new ObjectId().toHexString() + + const submissionResponses = { + [emailFieldId1]: { + fieldType: BasicField.Email, + answer: { + value: expectedEmails[0], + }, + }, + [emailFieldId2]: { + fieldType: BasicField.Email, + answer: { + value: 'not_expected_1@example.com', + }, + }, + [yesNoFieldId1]: { + fieldType: BasicField.YesNo, + answer: 'Yes', + }, + [yesNoFieldId2]: { + fieldType: BasicField.YesNo, + answer: 'No', + }, + } + + const threeStepApprovalWorkflow: FormWorkflowStepDto[] = [ + { + _id: stepOneId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId1, + edit: [], + }, + { + _id: stepTwoId, + workflow_type: WorkflowType.Static, + emails: ['not_expected_2@example.com'], + edit: [yesNoFieldId1], + approval_field: yesNoFieldId1, + }, + { + _id: stepThreeId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId2, + edit: [yesNoFieldId2], + approval_field: yesNoFieldId2, + }, + ] + + const currentStepNumber = 2 // last step of 3 step workflow + + // Act + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: threeStepApprovalWorkflow, + emails: [expectedEmails[1]], + stepsToNotify: [stepOneId], + } as IPopulatedMultirespondentForm, + currentStepNumber, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: submissionResponses, + workflowStep: threeStepApprovalWorkflow.length - 1, // last step + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + // Assert + // approval email is sent instead of completion email + expect(sendMrfApprovalEmailSpy).toHaveBeenCalledTimes(1) + expect(sendMrfWorkflowCompletionEmailSpy).not.toHaveBeenCalled() + expect(sendMRFWorkflowStepEmailSpy).not.toHaveBeenCalled() + // is rejected email and destination emails are correct + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].isRejected).toBeTrue() + expect( + sendMrfApprovalEmailSpy.mock.calls[0][0].emails, + ).toContainAllValues(expectedEmails) + expect(sendMrfApprovalEmailSpy.mock.calls[0][0].emails.length).toBe( + expectedEmails.length, + ) + }) + }) + + describe('mrf completion email notification when no approval step exists', () => { + it('sends completion email when single step mrf is completed', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + + const singleStepWorkflow: FormWorkflowStepDto[] = [ + { + _id: new ObjectId().toHexString(), + workflow_type: WorkflowType.Static, + emails: [], + edit: [], + }, + ] + + // Act + await performMultiRespondentPostSubmissionCreateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: singleStepWorkflow, + emails: ['email1@example.com'], + } as IPopulatedMultirespondentForm, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + + // Assert + expect(sendMrfWorkflowCompletionEmailSpy).toHaveBeenCalledTimes(1) + expect( + sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails, + ).toContainAllValues(['email1@example.com']) + expect( + sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails.length, + ).toBe(1) + }) + + it('sends completion email when multi-step mrf is completed and only to specified steps only and also static emails', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + + const expectedEmails = [ + 'expected1@example.com', + 'expected2@example.com', + 'expected3@example.com', + 'expected4@example.com', + ] + + const stepOneId = new ObjectId().toHexString() + const stepTwoId = new ObjectId().toHexString() + const stepThreeId = new ObjectId().toHexString() + const stepFourId = new ObjectId().toHexString() + + const emailFieldId1 = new ObjectId().toHexString() + const emailFieldId2 = new ObjectId().toHexString() + + const submissionResponses = { + [emailFieldId1]: { + fieldType: 'email', + answer: { + value: expectedEmails[0], + }, + }, + [emailFieldId2]: { + fieldType: 'email', + answer: { + value: 'not_expected_1@example.com', + }, + }, + } as FieldResponsesV3 + + const fourStepWorkflow: FormWorkflowStepDto[] = [ + { + _id: stepOneId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId1, + edit: [], + }, + { + _id: stepTwoId, + workflow_type: WorkflowType.Static, + emails: ['not_expected_2@example.com'], + edit: [], + }, + { + _id: stepThreeId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId2, + edit: [], + }, + { + _id: stepFourId, + workflow_type: WorkflowType.Static, + emails: [expectedEmails[1], expectedEmails[2]], + edit: [], + }, + ] + + const currentStepNumber = fourStepWorkflow.length - 1 // last step of 4 steps workflow + + // Act + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: fourStepWorkflow, + emails: [expectedEmails[3]], + stepsToNotify: [stepOneId, stepFourId], + } as IPopulatedMultirespondentForm, + currentStepNumber, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: submissionResponses, + workflowStep: currentStepNumber, + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + + // Assert + expect(sendMrfWorkflowCompletionEmailSpy).toHaveBeenCalledTimes(1) + // The emails sent to should only be the expected emails exactly + expect( + sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails, + ).toContainAllValues(expectedEmails) + expect( + sendMrfWorkflowCompletionEmailSpy.mock.calls[0][0].emails.length, + ).toBe(expectedEmails.length) + }) + + it('does not send completion email when step number >0 and mrf not completed', async () => { + // Arrange + const sendMrfWorkflowCompletionEmailSpy = jest.spyOn( + MailService, + 'sendMrfWorkflowCompletionEmail', + ) + + const selectedEmails = [ + 'seelcted1@example.com', + 'seelcted2@example.com', + 'seelcted3@example.com', + 'seelcted4@example.com', + ] + + const stepOneId = new ObjectId().toHexString() + const stepTwoId = new ObjectId().toHexString() + const stepThreeId = new ObjectId().toHexString() + const stepFourId = new ObjectId().toHexString() + + const emailFieldId1 = new ObjectId().toHexString() + const emailFieldId2 = new ObjectId().toHexString() + + const submissionResponses = { + [emailFieldId1]: { + fieldType: 'email', + answer: { + value: selectedEmails[0], + }, + }, + [emailFieldId2]: { + fieldType: 'email', + answer: { + value: 'not_selected_1@example.com', + }, + }, + } + + const fourStepWorkflow: FormWorkflowStepDto[] = [ + { + _id: stepOneId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId1, + edit: [], + }, + { + _id: stepTwoId, + workflow_type: WorkflowType.Static, + emails: ['not_selected_2@example.com'], + edit: [], + }, + { + _id: stepThreeId, + workflow_type: WorkflowType.Dynamic, + field: emailFieldId2, + edit: [], + }, + { + _id: stepFourId, + workflow_type: WorkflowType.Static, + emails: [selectedEmails[1], selectedEmails[2]], + edit: [], + }, + ] + + const currentStepNumber = fourStepWorkflow.length - 2 // not last step of 4 steps workflow + + // Act + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId: mockSubmissionId, + form: { + _id: mockFormId, + workflow: fourStepWorkflow, + emails: [selectedEmails[3]], + stepsToNotify: [stepOneId, stepFourId], + } as IPopulatedMultirespondentForm, + currentStepNumber, + encryptedPayload: { + encryptedContent: 'encryptedContent', + version: 1, + submissionPublicKey: 'submissionPublicKey', + encryptedSubmissionSecretKey: 'encryptedSubmissionSecretKey', + responses: submissionResponses, + workflowStep: currentStepNumber, + } as MultirespondentSubmissionDto, + logMeta: {} as any, + }) + + // Assert + expect(sendMrfWorkflowCompletionEmailSpy).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts index 24eade2097..b237bd4672 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts @@ -1,31 +1,14 @@ import { StatusCodes } from 'http-status-codes' -import mongoose from 'mongoose' -import { errAsync, okAsync, ResultAsync } from 'neverthrow' - -import { MailSendError } from 'src/app/services/mail/mail.errors' -import { EncryptSubmissionDto } from 'src/types/api' +import { errAsync } from 'neverthrow' import { ErrorDto, - FieldResponsesV3, FormAuthType, MultirespondentSubmissionDto, SubmissionType, } from '../../../../../shared/types' -import { getMultirespondentSubmissionEditPath } from '../../../../../shared/utils/urls' -import { - Environment, - IPopulatedMultirespondentForm, -} from '../../../../../src/types' -// TODO: (MRF-email-notif) Remove isTest import when MRF email notifications is out of beta -import config, { isTest } from '../../../config/config' -import { - createLoggerWithLabel, - CustomLoggerParams, -} from '../../../config/logger' -import { getMultirespondentSubmissionModel } from '../../../models/submission.server.model' +import { createLoggerWithLabel } from '../../../config/logger' import * as CaptchaMiddleware from '../../../services/captcha/captcha.middleware' -import MailService from '../../../services/mail/mail.service' import * as TurnstileMiddleware from '../../../services/turnstile/turnstile.middleware' import { Pipeline } from '../../../utils/pipeline-middleware' import { createReqMeta } from '../../../utils/request' @@ -42,40 +25,32 @@ import { import * as ReceiverMiddleware from '../receiver/receiver.middleware' import { InvalidSubmissionTypeError, - InvalidWorkflowTypeError, SubmissionFailedError, + SubmissionSaveError, } from '../submission.errors' import { getEncryptedSubmissionData, transformAttachmentMetasToSignedUrls, - uploadAttachments, } from '../submission.service' import { mapRouteError } from '../submission.utils' -import { reportSubmissionResponseTime } from '../submissions.statsd-client' import * as MultirespondentSubmissionMiddleware from './multirespondent-submission.middleware' import { checkFormIsMultirespondent, - sendMrfOutcomeEmails, + createMultiRespondentFormSubmission, + performMultiRespondentPostSubmissionCreateActions, + performMultiRespondentPostSubmissionUpdateActions, + updateMultiRespondentFormSubmission, } from './multirespondent-submission.service' import { - MultirespondentSubmissionContent, SubmitMultirespondentFormHandlerRequest, SubmitMultirespondentFormHandlerType, UpdateMultirespondentSubmissionHandlerRequest, UpdateMultirespondentSubmissionHandlerType, } from './multirespondent-submission.types' -import { - createMultirespondentSubmissionDto, - retrieveWorkflowStepEmailAddresses, -} from './multirespondent-submission.utils' +import { createMultirespondentSubmissionDto } from './multirespondent-submission.utils' const logger = createLoggerWithLabel(module) -const MultirespondentSubmission = getMultirespondentSubmissionModel(mongoose) -const appUrl = - process.env.NODE_ENV === Environment.Dev - ? config.app.feAppUrl - : config.app.appUrl const submitMultirespondentForm = async ( req: SubmitMultirespondentFormHandlerRequest, @@ -130,254 +105,40 @@ const submitMultirespondentForm = async ( return res.status(statusCode).json({ message: errorMessage }) } - // Save Responses to Database const encryptedPayload = req.formsg.encryptedPayload - let attachmentMetadata = new Map() - - if (encryptedPayload.attachments) { - const attachmentUploadResult = await uploadAttachments( - form._id, - encryptedPayload.attachments, - ) - - if (attachmentUploadResult.isErr()) { - const { statusCode, errorMessage } = mapRouteError( - attachmentUploadResult.error, - ) - return res.status(statusCode).json({ - message: errorMessage, - }) - } else { - attachmentMetadata = attachmentUploadResult.value - } - } - - // Create Incoming Submission - const { - submissionPublicKey, - encryptedSubmissionSecretKey, - submissionSecretKey, - encryptedContent, - responseMetadata, - version, - mrfVersion, - } = encryptedPayload - - const submissionContent: MultirespondentSubmissionContent = { - form: form._id, - authType: form.authType, - myInfoFields: form.getUniqueMyInfoAttrs(), - form_fields: form.form_fields, - form_logics: form.form_logics, - workflow: form.workflow, - submissionPublicKey, - encryptedSubmissionSecretKey, - encryptedContent, - attachmentMetadata, - version, - workflowStep: 0, - mrfVersion, - } - - return _createSubmission({ - req, - res, - logMeta, - formId, - responses: encryptedPayload.responses, - responseMetadata, - submissionContent, - submissionSecretKey, - form, - }) -} -export const submitMultirespondentFormForTest = submitMultirespondentForm - -const _createSubmission = async ({ - req, - res, - logMeta, - formId, - responses, - responseMetadata, - submissionContent, - submissionSecretKey, - form, -}: { - req: Parameters[0] - res: Parameters[1] - responseMetadata: EncryptSubmissionDto['responseMetadata'] - responses: FieldResponsesV3 - formId: string - submissionContent: MultirespondentSubmissionContent - logMeta: CustomLoggerParams['meta'] - form: IPopulatedMultirespondentForm - submissionSecretKey: string -}) => { - const submission = new MultirespondentSubmission(submissionContent) - - try { - await submission.save() - } catch (err) { - logger.error({ - message: 'Multirespondent submission save error', - meta: { - action: 'onMultirespondentSubmissionFailure', - ...createReqMeta(req), - }, - error: err, - }) - return res.status(StatusCodes.BAD_REQUEST).json({ - message: - 'Could not send submission. For assistance, please contact the person who asked you to fill in this form.', - submissionId: submission._id, + const createMultiRespondentFormSubmissionResult = + await createMultiRespondentFormSubmission({ + form, + encryptedPayload, + logMeta, }) - } - const submissionId = submission.id - logger.info({ - message: 'Saved submission to MongoDB', - meta: { - ...logMeta, - submissionId, - formId, - responseMetadata, - }, - }) + if (createMultiRespondentFormSubmissionResult.isErr()) { + const error = createMultiRespondentFormSubmissionResult.error - // TODO 6395 make responseMetadata mandatory - if (responseMetadata) { - reportSubmissionResponseTime(responseMetadata, { - mode: 'multirespodent', - payment: 'false', - }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) } + const submission = createMultiRespondentFormSubmissionResult.value + // Send success back to client res.json({ message: 'Form submission successful.', - submissionId, + submissionId: submission._id, timestamp: (submission.created || new Date()).getTime(), }) - // TODO(MRF/FRM-1591): Add post-submission actions handling - // return await performEncryptPostSubmissionActions(submission, responses) - - const currentStepNumber = submissionContent.workflowStep - - try { - await sendNextStepEmail({ - nextStepNumber: currentStepNumber + 1, // we want to send emails to the addresses linked to the next step of the workflow - form, - formTitle: form.title, - responseUrl: `${appUrl}/${getMultirespondentSubmissionEditPath( - form._id, - submissionId, - { key: submissionSecretKey }, - )}`, - formId: form._id, - submissionId, - responses, - }) - } catch (err) { - logger.error({ - message: 'Send multirespondent workflow email error', - meta: { - ...logMeta, - ...createReqMeta(req), - currentWorkflowStep: currentStepNumber, - formId: form._id, - submissionId, - }, - error: err, - }) - } - - // TODO: (MRF-email-notif) Remove isTest and betaFlag check when MRF email notifications is out of beta - if (isTest || form.admin.betaFlags.mrfEmailNotifications) { - try { - await sendMrfOutcomeEmails({ - currentStepNumber, - form, - responses, - submissionId, - }) - } catch (err) { - logger.error({ - message: 'Send mrf outcome email error', - meta: { - ...logMeta, - ...createReqMeta(req), - currentWorkflowStep: currentStepNumber, - formId: form._id, - submissionId, - }, - error: err, - }) - } - } + await performMultiRespondentPostSubmissionCreateActions({ + submissionId: submission._id.toString(), + form, + encryptedPayload, + logMeta, + }) } -const sendNextStepEmail = ({ - nextStepNumber, - form, - formTitle, - responseUrl, - formId, - submissionId, - responses, -}: { - nextStepNumber: number - form: IPopulatedMultirespondentForm - formTitle: string - responseUrl: string - formId: string - submissionId: string - responses: FieldResponsesV3 -}): ResultAsync => { - const logMeta = { - action: 'sendNextStepEmail', - formId, - submissionId, - nextWorkflowStep: nextStepNumber, - } - - const nextStep = form.workflow[nextStepNumber] - if (!nextStep) { - return okAsync(true) - } - - return ( - // Step 1: Retrieve email addresses for current workflow step - retrieveWorkflowStepEmailAddresses(nextStep, responses) - .mapErr((error) => { - logger.error({ - message: 'Failed to retrieve workflow step email addresses', - meta: logMeta, - error, - }) - return error - }) - // Step 2: send out next workflow step email - .asyncAndThen((emails) => { - if (!emails) return okAsync(true) - return MailService.sendMRFWorkflowStepEmail({ - emails, - formTitle, - responseId: submissionId, - responseUrl, - }).orElse((error) => { - logger.error({ - message: 'Failed to send workflow email', - meta: { ...logMeta, emails }, - error, - }) - return errAsync(error) - }) - }) - ) -} +export const submitMultirespondentFormForTest = submitMultirespondentForm const updateMultirespondentSubmission = async ( req: UpdateMultirespondentSubmissionHandlerRequest, @@ -420,83 +181,30 @@ const updateMultirespondentSubmission = async ( const encryptedPayload = req.formsg.encryptedPayload - // Create Incoming Submission - const { - responseMetadata, - submissionPublicKey, - encryptedSubmissionSecretKey, - encryptedContent, - submissionSecretKey, - version, - workflowStep, - responses, - mrfVersion, - } = encryptedPayload - - // Save Responses to Database - let attachmentMetadata = new Map() - - if (encryptedPayload.attachments) { - const attachmentUploadResult = await uploadAttachments( - form._id, - encryptedPayload.attachments, - ) + const updateMultiRespondentFormSubmissionResult = + await updateMultiRespondentFormSubmission({ + formId, + submissionId, + form, + encryptedPayload, + logMeta, + }) - if (attachmentUploadResult.isErr()) { - const { statusCode, errorMessage } = mapRouteError( - attachmentUploadResult.error, - ) - return res.status(statusCode).json({ - message: errorMessage, + if (updateMultiRespondentFormSubmissionResult.isErr()) { + const error = updateMultiRespondentFormSubmissionResult.error + + if (error instanceof SubmissionSaveError) { + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: error.message, + submissionId, }) - } else { - attachmentMetadata = attachmentUploadResult.value } - } - const submission = await MultirespondentSubmission.findById(submissionId) - if (!submission) { - return res.status(StatusCodes.NOT_FOUND).json({ - message: 'Not found', - }) - } - - submission.responseMetadata = responseMetadata - submission.submissionPublicKey = submissionPublicKey - submission.encryptedSubmissionSecretKey = encryptedSubmissionSecretKey - submission.encryptedContent = encryptedContent - submission.version = version - submission.workflowStep = workflowStep - submission.attachmentMetadata = attachmentMetadata - submission.mrfVersion = mrfVersion - - try { - await submission.save() - } catch (err) { - logger.error({ - message: 'Multirespondent submission save error', - meta: { - action: 'onMultirespondentSubmissionFailure', - ...createReqMeta(req), - }, - error: err, - }) - return res.status(StatusCodes.BAD_REQUEST).json({ - message: - 'Could not send submission. For assistance, please contact the person who asked you to fill in this form.', - submissionId, - }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) } - logger.info({ - message: 'Saved submission to MongoDB', - meta: { - ...logMeta, - submissionId, - formId, - responseMetadata, - }, - }) + const submission = updateMultiRespondentFormSubmissionResult.value // Send success back to client res.json({ @@ -505,57 +213,15 @@ const updateMultirespondentSubmission = async ( timestamp: (submission.created || new Date()).getTime(), }) - try { - await sendNextStepEmail({ - nextStepNumber: workflowStep + 1, - form, - formTitle: form.title, - responseUrl: `${appUrl}/${getMultirespondentSubmissionEditPath( - form._id, - submissionId, - { key: submissionSecretKey }, - )}`, - formId: form._id, - submissionId, - responses, - }) - } catch (err) { - logger.error({ - message: 'Send multirespondent workflow email error', - meta: { - ...logMeta, - ...createReqMeta(req), - currentWorkflowStep: workflowStep, - formId: form._id, - submissionId, - }, - error: err, - }) - } + const currentStepNumber = submission.workflowStep - // TODO: (MRF-email-notif) Remove isTest and betaFlag check when MRF email notifications is out of beta - if (isTest || form.admin.betaFlags.mrfEmailNotifications) { - try { - await sendMrfOutcomeEmails({ - currentStepNumber: workflowStep, - form, - responses, - submissionId, - }) - } catch (err) { - logger.error({ - message: 'Send mrf outcome email error', - meta: { - ...logMeta, - ...createReqMeta(req), - currentWorkflowStep: workflowStep, - formId: form._id, - submissionId, - }, - error: err, - }) - } - } + await performMultiRespondentPostSubmissionUpdateActions({ + submissionId, + form, + currentStepNumber, + encryptedPayload, + logMeta, + }) } export const updateMultirespondentSubmissionForTest = diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.service.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.service.ts index b59b179035..6d20ba72d4 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.service.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.service.ts @@ -1,19 +1,27 @@ -import { flatten } from 'lodash' +import { flatten, uniq } from 'lodash' import mongoose from 'mongoose' import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' import { + BasicField, FieldResponsesV3, FormResponseMode, - FormWorkflowDto, FormWorkflowStepDto, } from '../../../../../shared/types' +import { getMultirespondentSubmissionEditPath } from '../../../../../shared/utils/urls' import { + Environment, IMultirespondentSubmissionSchema, IPopulatedForm, IPopulatedMultirespondentForm, } from '../../../../types' -import { createLoggerWithLabel } from '../../../config/logger' +import { MultirespondentSubmissionDto } from '../../../../types/api' +// TODO: (MRF-email-notif) Remove isTest import when MRF email notifications is out of beta +import config, { isTest } from '../../../config/config' +import { + createLoggerWithLabel, + CustomLoggerParams, +} from '../../../config/logger' import { getMultirespondentSubmissionModel } from '../../../models/submission.server.model' import { MailSendError } from '../../../services/mail/mail.errors' import MailService from '../../../services/mail/mail.service' @@ -21,16 +29,27 @@ import { transformMongoError } from '../../../utils/handle-mongo-error' import { DatabaseError } from '../../core/core.errors' import { isFormMultirespondent } from '../../form/form.utils' import { + AttachmentUploadError, + ExpectedResponseNotFoundError, + InvalidApprovalFieldTypeError, InvalidWorkflowTypeError, ResponseModeError, SubmissionNotFoundError, + SubmissionSaveError, } from '../submission.errors' +import { uploadAttachments } from '../submission.service' +import { AttachmentMetadata } from '../submission.types' +import { reportSubmissionResponseTime } from '../submissions.statsd-client' +import { MultirespondentSubmissionContent } from './multirespondent-submission.types' import { retrieveWorkflowStepEmailAddresses } from './multirespondent-submission.utils' const logger = createLoggerWithLabel(module) - const MultirespondentSubmission = getMultirespondentSubmissionModel(mongoose) +const appUrl = + process.env.NODE_ENV === Environment.Dev + ? config.app.feAppUrl + : config.app.appUrl export const checkFormIsMultirespondent = ( form: IPopulatedForm, @@ -45,43 +64,121 @@ export const checkFormIsMultirespondent = ( ) } -export const getMultirespondentSubmission = ( - submissionId: string, -): ResultAsync< - IMultirespondentSubmissionSchema, - DatabaseError | SubmissionNotFoundError -> => - ResultAsync.fromPromise( - MultirespondentSubmission.findById(submissionId).exec(), - (error) => { - logger.error({ - message: - 'Error encountered while retrieving multirespondent submission', - meta: { - action: 'getMultirespondentSubmission', - submissionId, - }, - error, +const checkIsFormApproval = (form: IPopulatedMultirespondentForm): boolean => { + return ( + form.workflow && + form.workflow.map((step) => step.approval_field).filter(Boolean).length > 0 + ) +} + +const checkIsStepRejected = ({ + zeroIndexedStepNumber, + form, + responses, +}: { + zeroIndexedStepNumber: number + form: IPopulatedMultirespondentForm + responses: FieldResponsesV3 +}): Result< + boolean, + ExpectedResponseNotFoundError | InvalidApprovalFieldTypeError +> => { + const currentStep = form.workflow[zeroIndexedStepNumber] + if (!currentStep) { + return ok(false) + } + const approvalFieldId = currentStep.approval_field + const isApprovalStep = !!approvalFieldId + + if (!isApprovalStep) { + return ok(false) + } + + const approvalFieldResponse = responses[approvalFieldId] + if (!approvalFieldResponse || !approvalFieldResponse.answer) { + return err(new ExpectedResponseNotFoundError()) + } + if (approvalFieldResponse.fieldType !== BasicField.YesNo) { + return err(new InvalidApprovalFieldTypeError()) + } + + return ok(approvalFieldResponse.answer === 'No') +} + +const sendNextStepEmail = ({ + nextStepNumber, + form, + formTitle, + responseUrl, + formId, + submissionId, + responses, +}: { + nextStepNumber: number + form: IPopulatedMultirespondentForm + formTitle: string + responseUrl: string + formId: string + submissionId: string + responses: FieldResponsesV3 +}): ResultAsync => { + const logMeta = { + action: 'sendNextStepEmail', + formId, + submissionId, + nextWorkflowStep: nextStepNumber, + } + + const nextStep = form.workflow[nextStepNumber] + if (!nextStep) { + return okAsync(true) + } + + return ( + // Step 1: Retrieve email addresses for current workflow step + retrieveWorkflowStepEmailAddresses(nextStep, responses) + .mapErr((error) => { + logger.error({ + message: 'Failed to retrieve workflow step email addresses', + meta: logMeta, + error, + }) + return error }) - return transformMongoError(error) - }, - ).andThen((submission) => { - if (!submission) { - return errAsync(new SubmissionNotFoundError()) - } - return okAsync(submission) - }) + // Step 2: send out next workflow step email + .asyncAndThen((emails) => { + if (!emails) return okAsync(true) + return MailService.sendMRFWorkflowStepEmail({ + emails, + formTitle, + responseId: submissionId, + responseUrl, + }).orElse((error) => { + logger.error({ + message: 'Failed to send workflow email', + meta: { ...logMeta, emails }, + error, + }) + return errAsync(error) + }) + }) + ) +} -export const sendMrfOutcomeEmails = ({ +const sendMrfOutcomeEmails = ({ currentStepNumber, form, responses, submissionId, + isApproval = false, + isRejected = false, }: { currentStepNumber: number form: IPopulatedMultirespondentForm responses: FieldResponsesV3 submissionId: string + isApproval?: boolean + isRejected?: boolean }): ResultAsync => { const logMeta = { action: 'sendMrfOutcomeEmails', @@ -118,65 +215,463 @@ export const sendMrfOutcomeEmails = ({ }) // Step 2: Combine static emails and workflow step emails that are selected to notify .map((workflowStepEmailsToNotify) => { - return [...workflowStepEmailsToNotify, ...emailsToNotify] + return uniq([...workflowStepEmailsToNotify, ...emailsToNotify]) }) // Step 3: Send outcome emails based on type .asyncAndThen((destinationEmails) => { if (!destinationEmails || destinationEmails.length <= 0) return okAsync(true) - return sendMrfCompletionEmailIfWorkflowCompleted({ - currentStepNumber, - formWorkflow: form.workflow, - destinationEmails, + const lastStepNumber = form.workflow.length - 1 + const isLastStep = currentStepNumber === lastStepNumber + const isWorkflowCompleted = isLastStep + + if (!isWorkflowCompleted && !isRejected) { + return okAsync(true) + } + + if (isApproval) { + return MailService.sendMrfApprovalEmail({ + emails: destinationEmails, + formId: form._id, + formTitle: form.title, + responseId: submissionId, + isRejected, + }).orElse((error) => { + logger.error({ + message: 'Failed to send approval email', + meta: { + action: 'sendMrfApprovalEmail', + formId: form._id, + submissionId, + }, + error, + }) + return errAsync(error) + }) + } + return MailService.sendMrfWorkflowCompletionEmail({ + emails: destinationEmails, formId: form._id, formTitle: form.title, - submissionId, + responseId: submissionId, + }).orElse((error) => { + logger.error({ + message: 'Failed to send workflow completion email', + meta: { + action: 'sendMrfWorkflowCompletionEmail', + formId: form._id, + submissionId, + }, + error, + }) + return errAsync(error) }) }) ) } -const sendMrfCompletionEmailIfWorkflowCompleted = ({ - currentStepNumber, - formWorkflow, - destinationEmails, +const saveAttachmentsToDbIfExists = ({ formId, - formTitle, - submissionId, + attachments, }: { - currentStepNumber: number - formWorkflow: FormWorkflowDto - destinationEmails: string[] formId: string - formTitle: string + attachments: MultirespondentSubmissionDto['attachments'] +}): ResultAsync => { + return attachments + ? uploadAttachments(formId, attachments) + : okAsync(new Map()) +} + +export const createMultiRespondentFormSubmission = ({ + form, + encryptedPayload, + logMeta, +}: { + form: IPopulatedMultirespondentForm + encryptedPayload: MultirespondentSubmissionDto + logMeta: CustomLoggerParams['meta'] +}): ResultAsync< + IMultirespondentSubmissionSchema & { _id: mongoose.Types.ObjectId }, + AttachmentUploadError | SubmissionSaveError +> => { + logMeta = { + ...logMeta, + action: 'createMultiRespondentFormSubmission', + } + + return saveAttachmentsToDbIfExists({ + formId: form._id, + attachments: encryptedPayload.attachments, + }) + .andThen((attachmentMetadata) => { + // Create Incoming Submission + const { + submissionPublicKey, + encryptedSubmissionSecretKey, + encryptedContent, + responseMetadata, + version, + mrfVersion, + } = encryptedPayload + + const submissionContent: MultirespondentSubmissionContent = { + form: form._id, + authType: form.authType, + myInfoFields: form.getUniqueMyInfoAttrs(), + form_fields: form.form_fields, + form_logics: form.form_logics, + workflow: form.workflow, + submissionPublicKey, + encryptedSubmissionSecretKey, + encryptedContent, + attachmentMetadata, + version, + workflowStep: 0, + mrfVersion, + } + + const submission = new MultirespondentSubmission(submissionContent) + + return ResultAsync.fromPromise( + submission.save().then(() => ({ + submission, + responseMetadata, + })), + (error) => { + logger.error({ + message: 'Multirespondent submission save error', + meta: logMeta, + error, + }) + return new SubmissionSaveError() + }, + ) + }) + .map(({ submission, responseMetadata }) => { + const submissionId = submission.id + logger.info({ + message: 'Saved submission to MongoDB', + meta: { ...logMeta, submissionId, responseMetadata }, + }) + + // TODO 6395 make responseMetadata mandatory + if (responseMetadata) { + reportSubmissionResponseTime(responseMetadata, { + mode: 'multirespodent', + payment: 'false', + }) + } + + return submission + }) +} + +export const performMultiRespondentPostSubmissionCreateActions = ({ + submissionId, + form, + encryptedPayload, + logMeta, +}: { submissionId: string -}): ResultAsync => { - const logMeta = { - action: 'sendMrfCompletionEmail', - formId, + form: IPopulatedMultirespondentForm + encryptedPayload: MultirespondentSubmissionDto + logMeta: CustomLoggerParams['meta'] +}): ResultAsync => { + const { submissionSecretKey, responses } = encryptedPayload + const currentStepNumber = 0 + + logMeta = { + ...logMeta, + action: 'performMultiRespondentPostSubmissionCreateActions', + currentWorkflowStep: currentStepNumber, + formId: form._id, submissionId, } - const lastStepNumber = formWorkflow.length - 1 - const isLastStep = currentStepNumber === lastStepNumber - const isWorkflowCompleted = isLastStep - - if (isWorkflowCompleted) { - return MailService.sendMrfWorkflowCompletionEmail({ - emails: destinationEmails, - formId, - formTitle, - responseId: submissionId, - }).orElse((error) => { + return sendNextStepEmail({ + nextStepNumber: currentStepNumber + 1, // we want to send emails to the addresses linked to the next step of the workflow + form, + formTitle: form.title, + responseUrl: `${appUrl}/${getMultirespondentSubmissionEditPath( + form._id, + submissionId, + { key: submissionSecretKey }, + )}`, + formId: form._id, + submissionId, + responses, + }) + .mapErr((error) => { logger.error({ - message: 'Failed to send workflow completion email', - meta: { ...logMeta, destinationEmails }, + message: 'Send multirespondent workflow email error', + meta: logMeta, error, }) - return errAsync(error) + return error }) - } else { - return okAsync(true) + .andThen(() => { + // TODO: (MRF-email-notif) Remove isTest and betaFlag check when MRF email notifications is out of beta + if (isTest || form.admin.betaFlags.mrfEmailNotifications) { + return sendMrfOutcomeEmails({ + currentStepNumber, + form, + responses, + submissionId, + }) + } + return okAsync(true) + }) + .mapErr((error) => { + logger.error({ + message: 'Send mrf outcome email error', + meta: logMeta, + error, + }) + return error + }) +} + +export const updateMultiRespondentFormSubmission = ({ + submissionId, + form, + encryptedPayload, + logMeta, +}: { + formId: string + submissionId: string + form: IPopulatedMultirespondentForm + encryptedPayload: MultirespondentSubmissionDto + logMeta: CustomLoggerParams['meta'] +}): ResultAsync< + IMultirespondentSubmissionSchema & { _id: mongoose.Types.ObjectId }, + AttachmentUploadError | SubmissionSaveError | SubmissionNotFoundError +> => { + logMeta = { + ...logMeta, + action: 'updateMultiRespondentFormSubmission', } + + return saveAttachmentsToDbIfExists({ + formId: form._id, + attachments: encryptedPayload.attachments, + }) + .map(async (attachmentMetadata) => { + const submission = await MultirespondentSubmission.findById(submissionId) + return { submission, attachmentMetadata } + }) + .andThen(({ submission, attachmentMetadata }) => { + if (!submission) { + logger.error({ + message: 'Submission not found', + meta: { ...logMeta, submissionId }, + }) + return errAsync(new SubmissionNotFoundError()) + } + return okAsync({ submission, attachmentMetadata }) + }) + .andThen(({ submission, attachmentMetadata }) => { + const { + responseMetadata, + submissionPublicKey, + encryptedSubmissionSecretKey, + encryptedContent, + version, + workflowStep, + mrfVersion, + } = encryptedPayload + + submission.responseMetadata = responseMetadata + submission.submissionPublicKey = submissionPublicKey + submission.encryptedSubmissionSecretKey = encryptedSubmissionSecretKey + submission.encryptedContent = encryptedContent + submission.version = version + submission.workflowStep = workflowStep + submission.attachmentMetadata = attachmentMetadata + submission.mrfVersion = mrfVersion + + return ResultAsync.fromPromise( + submission.save().then(() => ({ submission, responseMetadata })), + (error) => { + logger.error({ + message: 'Multirespondent submission save error', + meta: logMeta, + error, + }) + return new SubmissionSaveError() + }, + ) + }) + .map(({ submission, responseMetadata }) => { + logger.info({ + message: 'Saved submission to MongoDB', + meta: { ...logMeta, submissionId: submission.id, responseMetadata }, + }) + + return submission + }) } + +export const performMultiRespondentPostSubmissionUpdateActions = ({ + submissionId, + form, + currentStepNumber, + encryptedPayload, + logMeta, +}: { + submissionId: string + form: IPopulatedMultirespondentForm + currentStepNumber: number + encryptedPayload: MultirespondentSubmissionDto + logMeta: CustomLoggerParams['meta'] +}): ResultAsync< + boolean, + | InvalidWorkflowTypeError + | MailSendError + | ExpectedResponseNotFoundError + | InvalidApprovalFieldTypeError +> => { + const { responses, submissionSecretKey } = encryptedPayload + + logMeta = { + ...logMeta, + action: 'performMultiRespondentPostSubmissionUpdateActions', + currentWorkflowStep: currentStepNumber, + formId: form._id, + submissionId, + } + + const isStepRejectedResult = + // TODO: (MRF-email-notif): Remove flag once approvals is out of beta + isTest || form.admin.betaFlags.mrfEmailNotifications + ? checkIsStepRejected({ + zeroIndexedStepNumber: currentStepNumber, + form, + responses, + }).mapErr((error) => { + logger.error({ + message: 'Error checking if step is rejected', + meta: logMeta, + error, + }) + return error + }) + : ok(false) + + if (isStepRejectedResult.isErr()) { + logger.error({ + message: 'Error checking if step is rejected', + meta: logMeta, + error: isStepRejectedResult.error, + }) + return errAsync(isStepRejectedResult.error) + } + + const isStepRejected = isStepRejectedResult.value + + // TODO: (MRF-email-notif): Remove flag once approvals is out of beta + if (isTest || form.admin.betaFlags.mrfEmailNotifications) { + if (isStepRejected) { + return sendMrfOutcomeEmails({ + currentStepNumber, + form, + responses, + submissionId, + isApproval: true, + isRejected: true, + }).mapErr((error) => { + logger.error({ + message: 'Send mrf outcome email error', + meta: logMeta, + error, + }) + return error + }) + } + return sendMrfOutcomeEmails({ + currentStepNumber, + form, + responses, + submissionId, + isApproval: checkIsFormApproval(form), + }) + .mapErr((error) => { + logger.error({ + message: 'Send mrf outcome email error', + meta: logMeta, + error, + }) + return error + }) + .andThen(() => + sendNextStepEmail({ + nextStepNumber: currentStepNumber + 1, + form, + formTitle: form.title, + responseUrl: `${appUrl}/${getMultirespondentSubmissionEditPath( + form._id, + submissionId, + { key: submissionSecretKey }, + )}`, + formId: form._id, + submissionId, + responses, + }).mapErr((error) => { + logger.error({ + message: 'Send multirespondent workflow email error', + meta: logMeta, + error, + }) + return error + }), + ) + } + // TODO: (MRF-email-notif): Remove this case once approvals is out of beta + return sendNextStepEmail({ + nextStepNumber: currentStepNumber + 1, + form, + formTitle: form.title, + responseUrl: `${appUrl}/${getMultirespondentSubmissionEditPath( + form._id, + submissionId, + { key: submissionSecretKey }, + )}`, + formId: form._id, + submissionId, + responses, + }).mapErr((error) => { + logger.error({ + message: 'Send multirespondent workflow email error', + meta: logMeta, + error, + }) + return error + }) +} + +export const getMultirespondentSubmission = ( + submissionId: string, +): ResultAsync< + IMultirespondentSubmissionSchema, + DatabaseError | SubmissionNotFoundError +> => + ResultAsync.fromPromise( + MultirespondentSubmission.findById(submissionId).exec(), + (error) => { + logger.error({ + message: + 'Error encountered while retrieving multirespondent submission', + meta: { + action: 'getMultirespondentSubmission', + submissionId, + }, + error, + }) + return transformMongoError(error) + }, + ).andThen((submission) => { + if (!submission) { + return errAsync(new SubmissionNotFoundError()) + } + return okAsync(submission) + }) diff --git a/src/app/modules/submission/submission.errors.ts b/src/app/modules/submission/submission.errors.ts index ef89c08f15..de444f5d18 100644 --- a/src/app/modules/submission/submission.errors.ts +++ b/src/app/modules/submission/submission.errors.ts @@ -11,6 +11,16 @@ export class ConflictError extends ApplicationError { } } +export class SubmissionSaveError extends ApplicationError { + constructor() { + super( + 'Failed to save submission. Please try again later.', + undefined, + ErrorCodes.SUBMISSION_SAVE_FAILURE, + ) + } +} + export class SubmissionNotFoundError extends ApplicationError { constructor(message = 'Submission not found for given ID') { super(message, undefined, ErrorCodes.SUBMISSION_NOT_FOUND) @@ -203,3 +213,19 @@ export class AttachmentUploadError extends ApplicationError { super(message, undefined, ErrorCodes.SUBMISSION_ATTACHMENT_UPLOAD) } } + +export class InvalidApprovalFieldTypeError extends ApplicationError { + constructor( + message = 'Invalid field type for approval step selected. Please select a Yes/No field', + ) { + super(message, undefined, ErrorCodes.ADMIN_FORM_INVALID_APPROVAL_FIELD_TYPE) + } +} + +export class ExpectedResponseNotFoundError extends ApplicationError { + constructor( + message = 'Response for the Yes/No field for this approval step is not found', + ) { + super(message, undefined, ErrorCodes.SUBMISSION_EXPECTED_RESPONSE_NOT_FOUND) + } +} diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index 8955521f4b..24bc51ee6a 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -132,6 +132,7 @@ import { ResponseModeError, SubmissionFailedError, SubmissionNotFoundError, + SubmissionSaveError, UnsupportedSettingsError, ValidateFieldError, VirusScanFailedError, @@ -171,6 +172,11 @@ const errorMapper: MapRouteError = ( errorMessage: 'Could not upload attachments for submission. For assistance, please contact the person who asked you to fill in this form.', } + case SubmissionSaveError: + return { + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + errorMessage: error.message, + } case CreateRedirectUrlError: return { statusCode: StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts b/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts index 8d059a7613..ec44aaae91 100644 --- a/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts +++ b/src/app/routes/api/v3/admin/forms/admin-forms.form.routes.ts @@ -212,6 +212,16 @@ AdminFormsFormRouter.post( AdminFormController.handleCreateFormField, ) +AdminFormsFormRouter.route('/:formId([a-fA-F0-9]{24})/workflow').post( + AdminFormController.handleCreateWorkflowStep, +) + +AdminFormsFormRouter.route( + '/:formId([a-fA-F0-9]{24})/workflow/:stepNumber(\\d+)', +) + .put(AdminFormController.handleUpdateWorkflowStep) + .delete(AdminFormController.handleDeleteWorkflowStep) + AdminFormsFormRouter.put( '/:formId([a-fA-F0-9]{24})/end-page', AdminFormController.handleUpdateEndPage, diff --git a/src/app/services/mail/mail.service.ts b/src/app/services/mail/mail.service.ts index bc6d5e9a31..b2b0f8b31d 100644 --- a/src/app/services/mail/mail.service.ts +++ b/src/app/services/mail/mail.service.ts @@ -33,6 +33,9 @@ import { getAdminEmails, } from '../../modules/form/form.utils' import { formatAsPercentage } from '../../utils/formatters' +import MrfApprovalOutcomeEmail, { + WorkflowOutcome, +} from '../../views/templates/MrfApprovalOutcomeEmail' import MrfWorkflowCompletionEmail from '../../views/templates/MrfWorkflowCompletionEmail' import MrfWorkflowEmail, { WorkflowEmailData, @@ -1063,7 +1066,7 @@ export class MailService { }): ResultAsync => { const htmlData: WorkflowEmailData = { formTitle, - responseId, + responseId: responseId.toString(), responseUrl, } @@ -1095,7 +1098,7 @@ export class MailService { }) => { const htmlData = { formTitle, - responseId, + responseId: responseId.toString(), } const html = render(MrfWorkflowCompletionEmail(htmlData)) @@ -1112,6 +1115,43 @@ export class MailService { return this.#sendNodeMail(mail, { formId, mailId: 'workflowNotification' }) } + + sendMrfApprovalEmail = ({ + emails, + formId, + formTitle, + responseId, + isRejected, + }: { + emails: string[] + formId: string + formTitle: string + responseId: string + isRejected: boolean + }) => { + const outcome = isRejected + ? WorkflowOutcome.NOT_APPROVED + : WorkflowOutcome.APPROVED + const htmlData = { + formTitle, + responseId: responseId.toString(), + outcome, + } + + const html = render(MrfApprovalOutcomeEmail(htmlData)) + + const mail: MailOptions = { + to: emails, + from: this.#senderFromString, + subject: `${outcome} - ${formTitle} (${responseId})`, + html, + headers: { + [EMAIL_HEADERS.emailType]: EmailType.WorkflowNotification, + }, + } + + return this.#sendNodeMail(mail, { formId, mailId: 'workflowNotification' }) + } } export default new MailService() diff --git a/src/app/views/templates/MrfApprovalOutcomeEmail.tsx b/src/app/views/templates/MrfApprovalOutcomeEmail.tsx new file mode 100644 index 0000000000..ae82b6a790 --- /dev/null +++ b/src/app/views/templates/MrfApprovalOutcomeEmail.tsx @@ -0,0 +1,90 @@ +import { + Body, + Column, + Container, + Head, + Heading, + Hr, + Html, + Img, + Row, + Text, +} from '@react-email/components' +import { FORMSG_LOGO_URL } from '../../constants/formsg-logo' + +import { + headingStyle, + innerContainerStyle, + outerContainerStyle, + textStyle, +} from './styles' + +export enum WorkflowOutcome { + APPROVED = 'Approved', + NOT_APPROVED = 'Not approved' +} + +export type WorkflowOutcomeEmailData = { + formTitle: string + responseId: string + outcome: WorkflowOutcome +} + +export const MrfApprovalOutcomeEmail = ({ + // Defaults are provided only for testing purposes in react-email-preview. + outcome = WorkflowOutcome.APPROVED, + formTitle = 'Test form title', + responseId = '64303c45828035f732088a41' +}: WorkflowOutcomeEmailData): JSX.Element => { + return ( + + + + + + + FormSG + + + + + + The outcome for {formTitle}. + + + + + + Outcome + + + {outcome} + + + + + Response ID + + + {responseId} + + + + +
+
+
+ + + + For more details, please contact the respondent(s) or form administrator. + + + +
+ + + ) +} + +export default MrfApprovalOutcomeEmail diff --git a/src/app/views/templates/MrfWorkflowCompletionEmail.tsx b/src/app/views/templates/MrfWorkflowCompletionEmail.tsx index bc345e4bce..a1c3c6ac70 100644 --- a/src/app/views/templates/MrfWorkflowCompletionEmail.tsx +++ b/src/app/views/templates/MrfWorkflowCompletionEmail.tsx @@ -7,7 +7,6 @@ import { Hr, Html, Img, - Link, Row, Text, } from '@react-email/components'