diff --git a/.eslintrc b/.eslintrc index ad26071a37..97378f5557 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,6 +24,7 @@ } }, "plugins": ["@typescript-eslint", "import", "simple-import-sort"], + "extends": ["plugin:@typescript-eslint/recommended"], "rules": { // Rules for auto sort of imports "simple-import-sort/sort": [ @@ -49,12 +50,10 @@ "import/order": "off", "import/first": "error", "import/newline-after-import": "error", - "import/no-duplicates": "error", - "@typescript-eslint/no-var-requires": "warn", - "@typescript-eslint/no-unused-vars": "error", - "no-unused-vars": "off" + "import/no-duplicates": "error" } - } + }, + { "files": ["*.spec.ts"], "extends": ["plugin:jest/recommended"] } ], "rules": { "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], diff --git a/.template-env b/.template-env index 70603da9f3..8ef7241b96 100644 --- a/.template-env +++ b/.template-env @@ -25,7 +25,7 @@ AWS_ACCESS_KEY_ID= FORMSG_SDK_MODE= -#### Optional variables, some have defaults defined here, as well as in `config/defaults` +#### Optional variables, some have defaults defined here, as well as in `config/schema` ## App Config # APP_NAME=FormSG diff --git a/docker-compose.yml b/docker-compose.yml index 1a7e6064aa..8ac35513e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,9 @@ services: - LOGO_S3_BUCKET=local-logo-bucket - FORMSG_SDK_MODE=development - BOUNCE_LIFE_SPAN=1800000 + - AWS_ACCESS_KEY_ID=fakeKey + - AWS_SECRET_ACCESS_KEY=fakeSecret + - SESSION_SECRET=thisisasecret - GA_TRACKING_ID - SENTRY_CONFIG_URL - TWILIO_ACCOUNT_SID @@ -61,6 +64,7 @@ services: - IS_SP_MAINTENANCE - IS_CP_MAINTENANCE - AGGREGATE_COLLECTION + - AWS_ENDPOINT=http://localhost:4572 mockpass: build: https://github.com/opengovsg/mockpass.git @@ -88,7 +92,7 @@ services: - '27017:27017' localstack: - image: localstack/localstack:0.8.0 + image: localstack/localstack:latest container_name: formsg-localstack depends_on: - formsg diff --git a/docs/DEPLOYMENT_SETUP.md b/docs/DEPLOYMENT_SETUP.md index 7e4e1d35f6..db64c833dd 100644 --- a/docs/DEPLOYMENT_SETUP.md +++ b/docs/DEPLOYMENT_SETUP.md @@ -2,10 +2,10 @@ This document details what is needed to create an environment to run FormSG in AWS. -### Build and run your nodejs app +## Build and run your NodeJS app -``` -$ npm install +```bash +npm install $ npm run build $ npm start ``` @@ -25,7 +25,7 @@ As a prerequisite for EB deployment, make sure you have already created your AWS ### Dockerrun.aws.json -``` +```json { "AWSEBDockerrunVersion": "1", "Image": { @@ -52,7 +52,7 @@ FormSG supports storing of users' Twilio API credentials using AWS Secret Manage Firstly, name the secret with a unique secret name and store the secret value in the following format: -``` +```json { "accountSid": "", "apiKey": "", @@ -96,7 +96,7 @@ The following env variables are set in Travis: ### Core Features -#### App and Database +#### App Config | Variable | Description | | :----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -105,25 +105,36 @@ The following env variables are set in Travis: | `APP_URL` | Defaults to `'https://form.gov.sg'`. | | `APP_KEYWORDS` | Defaults to `'forms, formbuilder, nodejs'`. | | `APP_IMAGES` | Defaults to `'/public/modules/core/img/og/img_metatag.png,/public/modules/core/img/og/logo-vertical-color.png'`. | -| `APP_TWITTER_IMAGE` | ath to Twitter image. Defaults to `'/public/modules/core/img/og/logo-vertical-color.png'`. | +| `APP_TWITTER_IMAGE` | Path to Twitter image. Defaults to `'/public/modules/core/img/og/logo-vertical-color.png'`. | + +#### App and Database + +| Variable | Description | +| :----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DB_HOST` | A MongoDB URI. | | `OTP_LIFE_SPAN` | Time in milliseconds that admin login OTP is valid for. Defaults to 900000ms or 15 minutes. | -| `BOUNCE_LIFE_SPAN` | Time in milliseconds that bounces are tracked for each form. Defaults to 1800000ms or 30 minutes. Only relevant if you have set up AWS to send bounce and delivery notifications to the /emailnotifications endpoint. | | `PORT` | Server port. Defaults to `5000`. | | `NODE_ENV` | [Express environment mode](https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production). Defaults to `'development'`. This should always be set to a production environment | | `SESSION_SECRET` | Secret for `express-session`. Defaults to `'sandcrawler-138577'`. This should always be set in a production environment. | +| `SUBMISSIONS_TOP_UP` | Use this to inflate the number of submissions displayed on the landing page. Defaults to `0`. | + +#### Banners + +| Variable | Description | +| :----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SITE_BANNER_CONTENT` | If set, displays a banner message on both private routes that `ADMIN_BANNER_CONTENT` covers **and** public form routes that `IS_GENERAL_MAINTENANCE` covers. Overrides all other banner environment variables | | `ADMIN_BANNER_CONTENT` | If set, displays a banner message on private admin routes such as the form list page as well as form builder pages. | +| `IS_LOGIN_BANNER` | If set, displays a banner message on the login page | | `IS_GENERAL_MAINTENANCE` | If set, displays a banner message on all forms. Overrides `IS_SP_MAINTENANCE` and `IS_CP_MAINTENANCE`. | -| `IS_SP_MAINTENANCE` | If set, displays a banner message on SingPass forms. Overrides `IS_CP_MAINTENANCE`. | -| `IS_CP_MAINTENANCE` | If set, displays a banner message on SingPass forms. | -| `SUBMISSIONS_TOP_UP` | Use this to inflate the number of submissions displayed on the landing page. Defaults to `0`. | #### AWS services | Variable | Description | | :---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `AWS_REGION` | AWS region. | +| `AWS_ACCESS_KEY_ID` | AWS IAM access key ID used to access S3. | +| `AWS_SECRET_ACCESS_KEY` | AWS IAM access secret used to access S3. | +| `AWS_ENDPOINT` | AWS S3 bucket endpoint. | | `IMAGE_S3_BUCKET` | Name of S3 bucket for image field uploads. | | `LOGO_S3_BUCKET` | Name of S3 bucket for form logo uploads. | | `LOGO_S3_BUCKET` | Name of S3 bucket for form logo uploads. | @@ -145,13 +156,12 @@ The following env variables are set in Travis: | `SES_PASS` | SMTP password. | | `SES_MAX_MESSAGES` | Nodemailer configuration. Connection removed and new one created when this limit is reached. This helps to keep the connection up-to-date for long-running email messaging. Defaults to `100`. | | `SES_POOL` | Connection pool to send email in parallel to the SMTP server. Defaults to `38`. | -| `SES_RATE` | Maximum email to send per second, or per `rateDelta` if supplied. | -| `SES_RATEDELTA` | Defines the time measuring period in milliseconds for rate limiting. Defaults to `1000`. | | `MAIL_FROM` | Sender email address. Defaults to `'donotreply@mail.form.gov.sg'`. | | | `MAIL_SOCKET_TIMEOUT` | Milliseconds of inactivity to allow before killing a connection. This helps to keep the connection up-to-date for long-running email messaging. Defaults to `600000`. | | `MAIL_LOGGER` | If set to true then logs to console. If value is not set or is false then nothing is logged. | | `MAIL_DEBUG` | If set to `true`, then logs SMTP traffic, otherwise logs only transaction events. | | `CHROMIUM_BIN` | Filepath to chromium binary. Required for email autoreply PDF generation with Puppeteer. | +| `BOUNCE_LIFE_SPAN` | Time in milliseconds that bounces are tracked for each form. Defaults to 10800000ms or 3 hours. Only relevant if you have set up AWS to send bounce and delivery notifications to the /emailnotifications endpoint. | ### Additional Features @@ -180,6 +190,7 @@ If this feature is enabled, client-side error events will be piped to [sentry.io | Variable | Description | | :------------------ | ----------------------------------------------------------------------------------------------------- | +| `CSP_REPORT_URI` | Reporting URL for Content Security Policy violdations. Can be configured to use a Sentry.io endpoint. | | `SENTRY_CONFIG_URL` | Sentry.io URL for configuring the Raven SDK. | | `CSP_REPORT_URI` | Reporting URL for Content Security Policy violdations. Can be configured to use a Sentry.io endpoint. | @@ -235,6 +246,8 @@ Note that MyInfo is currently not supported for storage mode forms and enabling | `MYINFO_CLIENT_CONFIG` | Configures [MyInfoGovClient](https://github.com/opengovsg/myinfo-gov-client). Set this to either`stg` or `prod` to fetch MyInfo data from the corresponding endpoints. | | `MYINFO_FORMSG_KEY_PATH` | Filepath to MyInfo private key, which is used to decrypt returned responses. | | `MYINFO_APP_KEY` | (deprecated) Directly specify contents of the MyInfo FormSG private key. Only works if `NODE_ENV` is set to `development`. | +| `IS_SP_MAINTENANCE` | If set, displays a banner message on SingPass forms. Overrides `IS_CP_MAINTENANCE`. | +| `IS_CP_MAINTENANCE` | If set, displays a banner message on CorpPass forms. | #### Verified Emails/SMSes @@ -263,4 +276,4 @@ If this feature is enabled, storage mode forms will also support authentication | `MONGO_BINARY_VERSION` | Version of the Mongo binary used. Defaults to `'latest'` according to [MongoMemoryServer](https://github.com/nodkz/mongodb-memory-server) docs. | | `PWD` | Path of working directory. | | `MOCK_WEBHOOK_CONFIG_FILE` | Path of configuration file for mock webhook server | -| `MOCK_WEBHOOK_PORT` | Port of mock webhook server | \ No newline at end of file +| `MOCK_WEBHOOK_PORT` | Port of mock webhook server | diff --git a/init-localstack.sh b/init-localstack.sh index 8878e3ea69..c0d162faaf 100644 --- a/init-localstack.sh +++ b/init-localstack.sh @@ -1,10 +1,10 @@ #!/bin/bash set -x -until $(curl --output /dev/null --silent --head --fail http://localhost:4572); do +until $(curl --output /dev/null --silent --head --fail $AWS_ENDPOINT); do printf 'Waiting for Localstack to be ready...' sleep 5 done -awslocal s3 mb s3://$IMAGE_S3_BUCKET -awslocal s3 mb s3://$LOGO_S3_BUCKET -awslocal s3 mb s3://$ATTACHMENT_S3_BUCKET +awslocal --endpoint-url=$AWS_ENDPOINT s3 mb s3://$IMAGE_S3_BUCKET +awslocal --endpoint-url=$AWS_ENDPOINT s3 mb s3://$LOGO_S3_BUCKET +awslocal --endpoint-url=$AWS_ENDPOINT s3 mb s3://$ATTACHMENT_S3_BUCKET set +x \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 9e0ce3ef05..3e2e02efb7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,7 @@ module.exports = { // Needed to use @shelf/jest-mongodb preset. ...tsJestPreset.transform, }, - collectCoverageFrom: ['./src/**/*.{ts,js}'], + collectCoverageFrom: ['./src/**/*.{ts,js}', '!**/__tests__/**'], coveragePathIgnorePatterns: ['./node_modules/', './tests'], coverageThreshold: { global: { diff --git a/package-lock.json b/package-lock.json index e28c2df037..8bcc000ecd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FormSG", - "version": "4.33.0", + "version": "4.34.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -25,29 +25,140 @@ } }, "@babel/core": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.10.2.tgz", - "integrity": "sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", + "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.1", - "@babel/generator": "^7.10.2", - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helpers": "^7.10.1", - "@babel/parser": "^7.10.2", - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.2", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.6", + "@babel/helper-module-transforms": "^7.11.0", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.11.5", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.11.5", + "@babel/types": "^7.11.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", "json5": "^2.1.2", - "lodash": "^4.17.13", + "lodash": "^4.17.19", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", + "dev": true, + "requires": { + "@babel/types": "^7.11.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -106,21 +217,15 @@ "dev": true }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -161,12 +266,12 @@ } }, "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { - "@babel/types": "^7.11.0", + "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -254,9 +359,9 @@ } }, "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { @@ -271,26 +376,26 @@ } }, "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", + "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -306,12 +411,6 @@ "requires": { "ms": "^2.1.1" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -342,21 +441,15 @@ "dev": true }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -372,14 +465,132 @@ } }, "@babel/helper-explode-assignable-expression": { + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", + "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-function-name": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", + "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.1", + "@babel/template": "^7.10.1", + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", + "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", + "dev": true, + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-hoist-variables": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz", - "integrity": "sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A==", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz", + "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==", + "dev": true, + "requires": { + "@babel/types": "^7.10.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-module-transforms": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" + }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", @@ -391,12 +602,12 @@ } }, "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { - "@babel/types": "^7.11.0", + "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -421,6 +632,36 @@ "@babel/types": "^7.10.4" } }, + "@babel/helper-member-expression-to-functions": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, "@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", @@ -448,9 +689,9 @@ } }, "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { @@ -465,26 +706,26 @@ } }, "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", + "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -500,102 +741,9 @@ "requires": { "ms": "^2.1.1" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, - "@babel/helper-function-name": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz", - "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", - "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", - "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true - } - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz", - "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz", - "integrity": "sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" - } - }, - "@babel/helper-module-transforms": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz", - "integrity": "sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.1", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-simple-access": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1", - "lodash": "^4.17.13" - } - }, "@babel/helper-optimise-call-expression": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz", @@ -618,26 +766,17 @@ "dev": true, "requires": { "lodash": "^4.17.19" - }, - "dependencies": { - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true - } } }, "@babel/helper-remap-async-to-generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz", - "integrity": "sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", + "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-wrap-function": "^7.10.4", "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" }, "dependencies": { @@ -651,12 +790,12 @@ } }, "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { - "@babel/types": "^7.11.0", + "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -729,9 +868,9 @@ } }, "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { @@ -746,26 +885,26 @@ } }, "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", + "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -781,12 +920,6 @@ "requires": { "ms": "^2.1.1" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -803,13 +936,69 @@ } }, "@babel/helper-simple-access": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz", - "integrity": "sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "dev": true, "requires": { - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-skip-transparent-expression-wrappers": { @@ -828,21 +1017,15 @@ "dev": true }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -874,106 +1057,228 @@ } }, "@babel/helpers": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.1.tgz", - "integrity": "sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw==", - "dev": true, - "requires": { - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" - } - }, - "@babel/highlight": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", - "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.1", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", - "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==", - "dev": true - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", - "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.10.4", - "@babel/plugin-syntax-async-generators": "^7.8.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", - "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } - } - }, - "@babel/plugin-proposal-dynamic-import": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", - "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-dynamic-import": "^7.8.0" + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { - "@babel/helper-plugin-utils": { + "@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", - "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "dependencies": { - "@babel/helper-plugin-utils": { + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", + "dev": true, + "requires": { + "@babel/types": "^7.11.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } - } - }, + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@babel/highlight": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", + "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", + "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4", + "@babel/plugin-syntax-async-generators": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", + "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", + "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", + "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-proposal-function-sent": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.10.1.tgz", @@ -1018,15 +1323,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } } } }, @@ -1063,15 +1359,6 @@ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } } } }, @@ -1362,43 +1649,11 @@ "@babel/helper-remap-async-to-generator": "^7.10.4" }, "dependencies": { - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, "@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -1557,880 +1812,35 @@ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", - "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", - "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", - "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.10.5", - "@babel/helper-plugin-utils": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", - "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", - "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/template": "^7.10.4", - "@babel/types": "^7.11.0", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", - "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", - "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", - "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/template": "^7.10.4", - "@babel/types": "^7.11.0", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", - "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.10.4", - "@babel/helper-module-transforms": "^7.10.5", - "@babel/helper-plugin-utils": "^7.10.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", - "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", - "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/template": "^7.10.4", - "@babel/types": "^7.11.0", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", - "dev": true - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, - "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", - "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", - "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", - "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/template": "^7.10.4", - "@babel/types": "^7.11.0", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/highlight": "^7.10.4" } }, - "@babel/helper-simple-access": { + "@babel/helper-function-name": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { + "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.11.0" + "@babel/types": "^7.10.4" } }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, "@babel/helper-validator-identifier": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", @@ -2449,9 +1859,9 @@ } }, "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { @@ -2465,47 +1875,126 @@ "@babel/types": "^7.10.4" } }, - "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - } - }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + } + } + }, + "@babel/plugin-transform-literals": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", + "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", + "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", + "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", + "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", + "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", + "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true } } @@ -2556,12 +2045,12 @@ } }, "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { - "@babel/types": "^7.11.0", + "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -2649,9 +2138,9 @@ } }, "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { @@ -2666,26 +2155,26 @@ } }, "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", + "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -2701,12 +2190,6 @@ "requires": { "ms": "^2.1.1" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -2742,21 +2225,15 @@ "dev": true }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -2888,21 +2365,15 @@ "dev": true }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, @@ -2959,9 +2430,9 @@ } }, "@babel/preset-env": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz", - "integrity": "sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.5.tgz", + "integrity": "sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA==", "dev": true, "requires": { "@babel/compat-data": "^7.11.0", @@ -3026,7 +2497,7 @@ "@babel/plugin-transform-unicode-escapes": "^7.10.4", "@babel/plugin-transform-unicode-regex": "^7.10.4", "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.11.0", + "@babel/types": "^7.11.5", "browserslist": "^4.12.0", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", @@ -3044,12 +2515,12 @@ } }, "@babel/generator": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", - "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { - "@babel/types": "^7.11.0", + "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -3103,15 +2574,6 @@ "@babel/types": "^7.11.0" } }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, "@babel/helper-optimise-call-expression": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", @@ -3166,38 +2628,11 @@ } }, "@babel/parser": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.1.tgz", - "integrity": "sha512-u9QMIRdKVF7hfEkb3nu2LgZDIzCQPv+yHD9Eg6ruoJLjkrQ9fFz4IBSlF/9XwoNri9+2F1IY+dYuOfZrXq8t3w==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, - "@babel/plugin-syntax-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", - "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, "@babel/plugin-transform-arrow-functions": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", @@ -3253,26 +2688,26 @@ } }, "@babel/traverse": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", - "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.0", + "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.0", - "@babel/types": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", - "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -3288,19 +2723,13 @@ "requires": { "ms": "^2.1.1" } - }, - "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true } } }, "@babel/preset-modules": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz", - "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", @@ -3385,6 +2814,56 @@ "minimist": "^1.2.0" } }, + "@eslint/eslintrc": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", + "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, "@hapi/address": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.1.0.tgz", @@ -3505,32 +2984,41 @@ "dev": true }, "@jest/console": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.2.0.tgz", - "integrity": "sha512-mXQfx3nSLwiHm1i7jbu+uvi+vvpVjNGzIQYLCfsat9rapC+MJkS4zBseNrgJE0vU921b3P67bQzhduphjY3Tig==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.3.0.tgz", + "integrity": "sha512-/5Pn6sJev0nPUcAdpJHMVIsA8sKizL2ZkcKPE5+dJrCccks7tcM7c9wbgHudBJbxXLoTbqsHkG1Dofoem4F09w==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^26.2.0", - "jest-util": "^26.2.0", + "jest-message-util": "^26.3.0", + "jest-util": "^26.3.0", "slash": "^3.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -3572,10 +3060,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -3584,34 +3086,34 @@ } }, "@jest/core": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.2.2.tgz", - "integrity": "sha512-UwA8gNI8aeV4FHGfGAUfO/DHjrFVvlBravF1Tm9Kt6qFE+6YHR47kFhgdepOFpADEKstyO+MVdPvkV6/dyt9sA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.4.2.tgz", + "integrity": "sha512-sDva7YkeNprxJfepOctzS8cAk9TOekldh+5FhVuXS40+94SHbiicRO1VV2tSoRtgIo+POs/Cdyf8p76vPTd6dg==", "dev": true, "requires": { - "@jest/console": "^26.2.0", - "@jest/reporters": "^26.2.2", - "@jest/test-result": "^26.2.0", - "@jest/transform": "^26.2.2", - "@jest/types": "^26.2.0", + "@jest/console": "^26.3.0", + "@jest/reporters": "^26.4.1", + "@jest/test-result": "^26.3.0", + "@jest/transform": "^26.3.0", + "@jest/types": "^26.3.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.4", - "jest-changed-files": "^26.2.0", - "jest-config": "^26.2.2", - "jest-haste-map": "^26.2.2", - "jest-message-util": "^26.2.0", + "jest-changed-files": "^26.3.0", + "jest-config": "^26.4.2", + "jest-haste-map": "^26.3.0", + "jest-message-util": "^26.3.0", "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.2.2", - "jest-resolve-dependencies": "^26.2.2", - "jest-runner": "^26.2.2", - "jest-runtime": "^26.2.2", - "jest-snapshot": "^26.2.2", - "jest-util": "^26.2.0", - "jest-validate": "^26.2.0", - "jest-watcher": "^26.2.0", + "jest-resolve": "^26.4.0", + "jest-resolve-dependencies": "^26.4.2", + "jest-runner": "^26.4.2", + "jest-runtime": "^26.4.2", + "jest-snapshot": "^26.4.2", + "jest-util": "^26.3.0", + "jest-validate": "^26.4.2", + "jest-watcher": "^26.3.0", "micromatch": "^4.0.2", "p-each-series": "^2.1.0", "rimraf": "^3.0.0", @@ -3620,18 +3122,27 @@ }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -3679,6 +3190,20 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -3689,9 +3214,9 @@ } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -3700,30 +3225,39 @@ } }, "@jest/environment": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.2.0.tgz", - "integrity": "sha512-oCgp9NmEiJ5rbq9VI/v/yYLDpladAAVvFxZgNsnJxOETuzPZ0ZcKKHYjKYwCtPOP1WCrM5nmyuOhMStXFGHn+g==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.3.0.tgz", + "integrity": "sha512-EW+MFEo0DGHahf83RAaiqQx688qpXgl99wdb8Fy67ybyzHwR1a58LHcO376xQJHfmoXTu89M09dH3J509cx2AA==", "dev": true, "requires": { - "@jest/fake-timers": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/fake-timers": "^26.3.0", + "@jest/types": "^26.3.0", "@types/node": "*", - "jest-mock": "^26.2.0" + "jest-mock": "^26.3.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -3766,9 +3300,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -3777,32 +3311,41 @@ } }, "@jest/fake-timers": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.2.0.tgz", - "integrity": "sha512-45Gfe7YzYTKqTayBrEdAF0qYyAsNRBzfkV0IyVUm3cx7AsCWlnjilBM4T40w7IXT5VspOgMPikQlV0M6gHwy/g==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.3.0.tgz", + "integrity": "sha512-ZL9ytUiRwVP8ujfRepffokBvD2KbxbqMhrXSBhSdAhISCw3gOkuntisiSFv+A6HN0n0fF4cxzICEKZENLmW+1A==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "@sinonjs/fake-timers": "^6.0.1", "@types/node": "*", - "jest-message-util": "^26.2.0", - "jest-mock": "^26.2.0", - "jest-util": "^26.2.0" + "jest-message-util": "^26.3.0", + "jest-mock": "^26.3.0", + "jest-util": "^26.3.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -3844,10 +3387,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -3856,29 +3413,38 @@ } }, "@jest/globals": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.2.0.tgz", - "integrity": "sha512-Hoc6ScEIPaym7RNytIL2ILSUWIGKlwEv+JNFof9dGYOdvPjb2evEURSslvCMkNuNg1ECEClTE8PH7ULlMJntYA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.4.2.tgz", + "integrity": "sha512-Ot5ouAlehhHLRhc+sDz2/9bmNv9p5ZWZ9LE1pXGGTCXBasmi5jnYjlgYcYt03FBwLmZXCZ7GrL29c33/XRQiow==", "dev": true, "requires": { - "@jest/environment": "^26.2.0", - "@jest/types": "^26.2.0", - "expect": "^26.2.0" + "@jest/environment": "^26.3.0", + "@jest/types": "^26.3.0", + "expect": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -3921,9 +3487,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -3932,16 +3498,16 @@ } }, "@jest/reporters": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.2.2.tgz", - "integrity": "sha512-7854GPbdFTAorWVh+RNHyPO9waRIN6TcvCezKVxI1khvFq9YjINTW7J3WU+tbR038Ynn6WjYred6vtT0YmIWVQ==", + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.4.1.tgz", + "integrity": "sha512-aROTkCLU8++yiRGVxLsuDmZsQEKO6LprlrxtAuzvtpbIFl3eIjgIf3EUxDKgomkS25R9ZzwGEdB5weCcBZlrpQ==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^26.2.0", - "@jest/test-result": "^26.2.0", - "@jest/transform": "^26.2.2", - "@jest/types": "^26.2.0", + "@jest/console": "^26.3.0", + "@jest/test-result": "^26.3.0", + "@jest/transform": "^26.3.0", + "@jest/types": "^26.3.0", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", @@ -3952,31 +3518,40 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.0.2", - "jest-haste-map": "^26.2.2", - "jest-resolve": "^26.2.2", - "jest-util": "^26.2.0", - "jest-worker": "^26.2.1", - "node-notifier": "^7.0.0", + "jest-haste-map": "^26.3.0", + "jest-resolve": "^26.4.0", + "jest-util": "^26.3.0", + "jest-worker": "^26.3.0", + "node-notifier": "^8.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.1.3" + "v8-to-istanbul": "^5.0.1" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -4018,6 +3593,20 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4025,9 +3614,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -4036,9 +3625,9 @@ } }, "@jest/source-map": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.1.0.tgz", - "integrity": "sha512-XYRPYx4eEVX15cMT9mstnO7hkHP3krNtKfxUYd8L7gbtia8JvZZ6bMzSwa6IQJENbudTwKMw5R1BePRD+bkEmA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.3.0.tgz", + "integrity": "sha512-hWX5IHmMDWe1kyrKl7IhFwqOuAreIwHhbe44+XH2ZRHjrKIh0LO5eLQ/vxHFeAfRwJapmxuqlGAEYLadDq6ZGQ==", "dev": true, "requires": { "callsites": "^3.0.0", @@ -4055,30 +3644,39 @@ } }, "@jest/test-result": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.2.0.tgz", - "integrity": "sha512-kgPlmcVafpmfyQEu36HClK+CWI6wIaAWDHNxfQtGuKsgoa2uQAYdlxjMDBEa3CvI40+2U3v36gQF6oZBkoKatw==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.3.0.tgz", + "integrity": "sha512-a8rbLqzW/q7HWheFVMtghXV79Xk+GWwOK1FrtimpI5n1la2SY0qHri3/b0/1F0Ve0/yJmV8pEhxDfVwiUBGtgg==", "dev": true, "requires": { - "@jest/console": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/console": "^26.3.0", + "@jest/types": "^26.3.0", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -4121,9 +3719,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -4132,34 +3730,34 @@ } }, "@jest/test-sequencer": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.2.2.tgz", - "integrity": "sha512-SliZWon5LNqV/lVXkeowSU6L8++FGOu3f43T01L1Gv6wnFDP00ER0utV9jyK9dVNdXqfMNCN66sfcyar/o7BNw==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.4.2.tgz", + "integrity": "sha512-83DRD8N3M0tOhz9h0bn6Kl6dSp+US6DazuVF8J9m21WAp5x7CqSMaNycMP0aemC/SH/pDQQddbsfHRTBXVUgog==", "dev": true, "requires": { - "@jest/test-result": "^26.2.0", + "@jest/test-result": "^26.3.0", "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.2.2", - "jest-runner": "^26.2.2", - "jest-runtime": "^26.2.2" + "jest-haste-map": "^26.3.0", + "jest-runner": "^26.4.2", + "jest-runtime": "^26.4.2" } }, "@jest/transform": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.2.2.tgz", - "integrity": "sha512-c1snhvi5wRVre1XyoO3Eef5SEWpuBCH/cEbntBUd9tI5sNYiBDmO0My/lc5IuuGYKp/HFIHV1eZpSx5yjdkhKw==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.3.0.tgz", + "integrity": "sha512-Isj6NB68QorGoFWvcOjlUhpkT56PqNIsXKR7XfvoDlCANn/IANlh8DrKAA2l2JKC3yWSMH5wS0GwuQM20w3b2A==", "dev": true, "requires": { "@babel/core": "^7.1.0", - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "babel-plugin-istanbul": "^6.0.0", "chalk": "^4.0.0", "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.0.0", "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.2.2", + "jest-haste-map": "^26.3.0", "jest-regex-util": "^26.0.0", - "jest-util": "^26.2.0", + "jest-util": "^26.3.0", "micromatch": "^4.0.2", "pirates": "^4.0.1", "slash": "^3.0.0", @@ -4168,18 +3766,27 @@ }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -4221,6 +3828,20 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4228,9 +3849,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -4470,6 +4091,74 @@ "xpath": "0.0.27" } }, + "@sentry/browser": { + "version": "5.22.3", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.22.3.tgz", + "integrity": "sha512-2TzE/CoBa5ZkvxJizDdi1Iz1ldmXSJpFQ1mL07PIXBjCt0Wxf+WOuFSj5IP4L40XHfJE5gU8wEvSH0VDR8nXtA==", + "requires": { + "@sentry/core": "5.22.3", + "@sentry/types": "5.22.3", + "@sentry/utils": "5.22.3", + "tslib": "^1.9.3" + } + }, + "@sentry/core": { + "version": "5.22.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.22.3.tgz", + "integrity": "sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw==", + "requires": { + "@sentry/hub": "5.22.3", + "@sentry/minimal": "5.22.3", + "@sentry/types": "5.22.3", + "@sentry/utils": "5.22.3", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "5.22.3", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.22.3.tgz", + "integrity": "sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw==", + "requires": { + "@sentry/types": "5.22.3", + "@sentry/utils": "5.22.3", + "tslib": "^1.9.3" + } + }, + "@sentry/integrations": { + "version": "5.22.3", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.22.3.tgz", + "integrity": "sha512-Fx6h8DTDvUpEOymx8Wi49LBdVcNYHwaI6NqApm1qVU9qn/I50Q29KWoZTCGBjBwmkJud+DOAHWYWoU2qRrIvcQ==", + "requires": { + "@sentry/types": "5.22.3", + "@sentry/utils": "5.22.3", + "localforage": "1.8.1", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "5.22.3", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.22.3.tgz", + "integrity": "sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA==", + "requires": { + "@sentry/hub": "5.22.3", + "@sentry/types": "5.22.3", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "5.22.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.22.3.tgz", + "integrity": "sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw==" + }, + "@sentry/utils": { + "version": "5.22.3", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.22.3.tgz", + "integrity": "sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw==", + "requires": { + "@sentry/types": "5.22.3", + "tslib": "^1.9.3" + } + }, "@shelf/jest-mongodb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@shelf/jest-mongodb/-/jest-mongodb-1.2.2.tgz", @@ -4677,6 +4366,12 @@ "@types/express": "*" } }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, "@types/cross-spawn": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", @@ -4710,12 +4405,6 @@ "integrity": "sha1-4ByfjIXKg7YQMgxiJYsMkCat4Pc=", "dev": true }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -4769,9 +4458,9 @@ } }, "@types/glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { "@types/minimatch": "*", @@ -4869,9 +4558,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.155", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.155.tgz", - "integrity": "sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg==", + "version": "4.14.161", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz", + "integrity": "sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==", "dev": true }, "@types/md5-file": { @@ -5027,9 +4716,9 @@ "dev": true }, "@types/prettier": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.0.2.tgz", - "integrity": "sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.0.tgz", + "integrity": "sha512-hiYA88aHiEIgDmeKlsyVsuQdcFn3Z2VuFd/Xm/HCnGnPD8UFU5BM128uzzRVVGEzKDKYUrRsRH9S2o+NUy/3IA==", "dev": true }, "@types/promise-retry": { @@ -5133,6 +4822,25 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "@types/superagent": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.10.tgz", + "integrity": "sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.10.tgz", + "integrity": "sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/tmp": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz", @@ -5200,12 +4908,13 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.9.1.tgz", - "integrity": "sha512-XIr+Mfv7i4paEdBf0JFdIl9/tVxyj+rlilWIfZ97Be0lZ7hPvUbS5iHt9Glc8kRI53dsr0PcAEudbf8rO2wGgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.1.tgz", + "integrity": "sha512-pQZtXupCn11O4AwpYVUX4PDFfmIJl90ZgrEBg0CEcqlwvPiG0uY81fimr1oMFblZnpKAq6prrT9a59pj1x58rw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "3.9.1", + "@typescript-eslint/experimental-utils": "4.0.1", + "@typescript-eslint/scope-manager": "4.0.1", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -5213,35 +4922,6 @@ "tsutils": "^3.17.1" }, "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.9.1.tgz", - "integrity": "sha512-lkiZ8iBBaYoyEKhCkkw4SAeatXyBq9Ece5bZXdLe1LWBUwTszGbmbiqmQbwWA8cSYDnjWXp9eDbXpf9Sn0hLAg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.9.1", - "@typescript-eslint/typescript-estree": "3.9.1", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.9.1.tgz", - "integrity": "sha512-IqM0gfGxOmIKPhiHW/iyAEXwSVqMmR2wJ9uXHNdFpqVvPaQ3dWg302vW127sBpAiqM9SfHhyS40NKLsoMpN2KA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "3.9.1", - "@typescript-eslint/visitor-keys": "3.9.1", - "debug": "^4.1.1", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -5260,44 +4940,68 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.3.0.tgz", - "integrity": "sha512-d4pGIAbu/tYsrPrdHCQ5xfadJGvlkUxbeBB56nO/VGmEDi/sKmfa5fGty5t5veL1OyJBrUmSiRn1R1qfVDydrg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.0.1.tgz", + "integrity": "sha512-gAqOjLiHoED79iYTt3F4uSHrYmg/GPz/zGezdB0jAdr6S6gwNiR/j7cTZ8nREKVzMVKLd9G3xbg1sV9GClW3sw==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "3.3.0", + "@typescript-eslint/scope-manager": "4.0.1", + "@typescript-eslint/types": "4.0.1", + "@typescript-eslint/typescript-estree": "4.0.1", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.3.0.tgz", - "integrity": "sha512-a7S0Sqn/+RpOOWTcaLw6RD4obsharzxmgMfdK24l364VxuBODXjuJM7ImCkSXEN7oz52aiZbXSbc76+2EsE91w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.0.1.tgz", + "integrity": "sha512-1+qLmXHNAWSQ7RB6fdSQszAiA7JTwzakj5cNYjBTUmpH2cqilxMZEIV+DRKjVZs8NzP3ALmKexB0w/ExjcK9Iw==", "dev": true, "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.3.0", - "@typescript-eslint/typescript-estree": "3.3.0", - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/scope-manager": "4.0.1", + "@typescript-eslint/types": "4.0.1", + "@typescript-eslint/typescript-estree": "4.0.1", + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.0.1.tgz", + "integrity": "sha512-u3YEXVJ8jsj7QCJk3om0Y457fy2euEOkkzxIB/LKU3MdyI+FJ2gI0M4aKEaXzwCSfNDiZ13a3lDo5DVozc+XLQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.0.1", + "@typescript-eslint/visitor-keys": "4.0.1" } }, "@typescript-eslint/types": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.9.1.tgz", - "integrity": "sha512-15JcTlNQE1BsYy5NBhctnEhEoctjXOjOK+Q+rk8ugC+WXU9rAcS2BYhoh6X4rOaXJEpIYDl+p7ix+A5U0BqPTw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.0.1.tgz", + "integrity": "sha512-S+gD3fgbkZYW2rnbjugNMqibm9HpEjqZBZkTiI3PwbbNGWmAcxolWIUwZ0SKeG4Dy2ktpKKaI/6+HGYVH8Qrlg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.3.0.tgz", - "integrity": "sha512-3SqxylENltEvJsjjMSDCUx/edZNSC7wAqifUU1Ywp//0OWEZwMZJfecJud9XxJ/40rAKEbJMKBOQzeOjrLJFzQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.0.1.tgz", + "integrity": "sha512-zGzleORFXrRWRJAMLTB2iJD1IZbCPkg4hsI8mGdpYlKaqzvKYSEWVAYh14eauaR+qIoZVWrXgYSXqLtTlxotiw==", "dev": true, "requires": { + "@typescript-eslint/types": "4.0.1", + "@typescript-eslint/visitor-keys": "4.0.1", "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", + "globby": "^11.0.1", "is-glob": "^4.0.1", "lodash": "^4.17.15", "semver": "^7.3.2", @@ -5322,12 +5026,21 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.9.1.tgz", - "integrity": "sha512-zxdtUjeoSh+prCpogswMwVUJfEFmCOjdzK9rpNjNBfm6EyPt99x3RrJoBOGZO23FCt0WPKUCOL5mb/9D5LjdwQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.0.1.tgz", + "integrity": "sha512-yBSqd6FjnTzbg5RUy9J+9kJEyQjTI34JdGMJz+9ttlJzLCnGkBikxw+N5n2VDcc3CesbIEJ0MnZc5uRYnrEnCw==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/types": "4.0.1", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } } }, "@uirouter/core": { @@ -5552,9 +5265,9 @@ } }, "acorn": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", - "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true }, "acorn-globals": { @@ -5616,26 +5329,15 @@ } }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - }, - "dependencies": { - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - } + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ajv-errors": { @@ -6084,6 +5786,11 @@ "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-2.1.1.tgz", "integrity": "sha512-CHBC6gQGCIzjZ09tJ+XmpQoZOn4GdWePB4qUweCaKNJ0D3f115YdhmYVTZ4rMVpiJ3cFzZcTYK1VMYEICV4YXw==" }, + "aws-info": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/aws-info/-/aws-info-1.1.0.tgz", + "integrity": "sha512-ZUWFfYy9Mu5ppoDJr3TxY0UWc4peP3tS5sStgnKkRh3urYalQaZWHewhbDbz40I84PO0xhVdBGut1xQtjum3mw==" + }, "aws-sdk": { "version": "2.734.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.734.0.tgz", @@ -6457,34 +6164,43 @@ } }, "babel-jest": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.2.2.tgz", - "integrity": "sha512-JmLuePHgA+DSOdOL8lPxCgD2LhPPm+rdw1vnxR73PpIrnmKCS2/aBhtkAcxQWuUcW2hBrH8MJ3LKXE7aWpNZyA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.3.0.tgz", + "integrity": "sha512-sxPnQGEyHAOPF8NcUsD0g7hDCnvLL2XyblRBcgrzTWBB/mAIpWow3n1bEL+VghnnZfreLhFSBsFluRoK2tRK4g==", "dev": true, "requires": { - "@jest/transform": "^26.2.2", - "@jest/types": "^26.2.0", + "@jest/transform": "^26.3.0", + "@jest/types": "^26.3.0", "@types/babel__core": "^7.1.7", "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^26.2.0", + "babel-preset-jest": "^26.3.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "slash": "^3.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -6526,10 +6242,10 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -7174,13 +6890,13 @@ } }, "babel-preset-jest": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.2.0.tgz", - "integrity": "sha512-R1k8kdP3R9phYQugXeNnK/nvCGlBzG4m3EoIIukC80GXb6wCv2XiwPhK6K9MAkQcMszWBYvl2Wm+yigyXFQqXg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.3.0.tgz", + "integrity": "sha512-5WPdf7nyYi2/eRxCbVrE1kKCWxgWY4RsPEbdJWFm7QsesFGqjdkyLeu1zRkwM1cxK6EPIlNd6d2AxLk7J+t4pw==", "dev": true, "requires": { "babel-plugin-jest-hoist": "^26.2.0", - "babel-preset-current-node-syntax": "^0.1.2" + "babel-preset-current-node-syntax": "^0.1.3" } }, "babel-preset-react": { @@ -11029,12 +10745,13 @@ } }, "eslint": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", - "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz", + "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.1.3", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -11044,7 +10761,7 @@ "eslint-scope": "^5.1.0", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^1.3.0", - "espree": "^7.2.0", + "espree": "^7.3.0", "esquery": "^1.2.0", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", @@ -11072,18 +10789,6 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "ajv": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", - "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -11245,9 +10950,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -11559,9 +11264,9 @@ "dev": true }, "esotope-hammerhead": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.5.3.tgz", - "integrity": "sha512-EMZvx+2MXsAZxqa+bOJZp+5qWzKZ6jx/tYung2dOalujGWW5WKb52UhXR8rb60XyW/WbmoVBjOB1WMPkaSjEzw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.5.5.tgz", + "integrity": "sha512-EuSYJDtF8gLMB24lzjHw2KotauPsVJybFrtGfQyMm48oC7sTkspA26DqcqcbnRl4GC6sPVKWEx+ex72eqopX9Q==", "dev": true, "requires": { "@types/estree": "^0.0.39" @@ -11760,32 +11465,41 @@ } }, "expect": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.2.0.tgz", - "integrity": "sha512-8AMBQ9UVcoUXt0B7v+5/U5H6yiUR87L6eKCfjE3spx7Ya5lF+ebUo37MCFBML2OiLfkX1sxmQOZhIDonyVTkcw==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.4.2.tgz", + "integrity": "sha512-IlJ3X52Z0lDHm7gjEp+m76uX46ldH5VpqmU0006vqDju/285twh7zaWMRhs67VpQhBwjjMchk+p5aA0VkERCAA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-styles": "^4.0.0", - "jest-get-type": "^26.0.0", - "jest-matcher-utils": "^26.2.0", - "jest-message-util": "^26.2.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.4.2", + "jest-message-util": "^26.3.0", "jest-regex-util": "^26.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -11828,15 +11542,15 @@ "dev": true }, "jest-get-type": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", - "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -14506,9 +14220,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -14747,29 +14461,38 @@ } }, "jest": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-26.2.2.tgz", - "integrity": "sha512-EkJNyHiAG1+A8pqSz7cXttoVa34hOEzN/MrnJhYnfp5VHxflVcf2pu3oJSrhiy6LfIutLdWo+n6q63tjcoIeig==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.4.2.tgz", + "integrity": "sha512-LLCjPrUh98Ik8CzW8LLVnSCfLaiY+wbK53U7VxnFSX7Q+kWC4noVeDvGWIFw0Amfq1lq2VfGm7YHWSLBV62MJw==", "dev": true, "requires": { - "@jest/core": "^26.2.2", + "@jest/core": "^26.4.2", "import-local": "^3.0.2", - "jest-cli": "^26.2.2" + "jest-cli": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -14812,30 +14535,44 @@ "dev": true }, "jest-cli": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.2.2.tgz", - "integrity": "sha512-vVcly0n/ijZvdy6gPQiQt0YANwX2hLTPQZHtW7Vi3gcFdKTtif7YpI85F8R8JYy5DFSWz4x1OW0arnxlziu5Lw==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.4.2.tgz", + "integrity": "sha512-zb+lGd/SfrPvoRSC/0LWdaWCnscXc1mGYW//NP4/tmBvRPT3VntZ2jtKUONsRi59zc5JqmsSajA9ewJKFYp8Cw==", "dev": true, "requires": { - "@jest/core": "^26.2.2", - "@jest/test-result": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/core": "^26.4.2", + "@jest/test-result": "^26.3.0", + "@jest/types": "^26.3.0", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.4", "import-local": "^3.0.2", "is-ci": "^2.0.0", - "jest-config": "^26.2.2", - "jest-util": "^26.2.0", - "jest-validate": "^26.2.0", + "jest-config": "^26.4.2", + "jest-util": "^26.3.0", + "jest-validate": "^26.4.2", "prompts": "^2.0.1", "yargs": "^15.3.1" } }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -14844,29 +14581,38 @@ } }, "jest-changed-files": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.2.0.tgz", - "integrity": "sha512-+RyJb+F1K/XBLIYiL449vo5D+CvlHv29QveJUWNPXuUicyZcq+tf1wNxmmFeRvAU1+TzhwqczSjxnCCFt7+8iA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.3.0.tgz", + "integrity": "sha512-1C4R4nijgPltX6fugKxM4oQ18zimS7LqQ+zTTY8lMCMFPrxqBFb7KJH0Z2fRQJvw2Slbaipsqq7s1mgX5Iot+g==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "execa": "^4.0.0", "throat": "^5.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -14973,9 +14719,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -14993,44 +14739,53 @@ } }, "jest-config": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.2.2.tgz", - "integrity": "sha512-2lhxH0y4YFOijMJ65usuf78m7+9/8+hAb1PZQtdRdgnQpAb4zP6KcVDDktpHEkspBKnc2lmFu+RQdHukUUbiTg==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.4.2.tgz", + "integrity": "sha512-QBf7YGLuToiM8PmTnJEdRxyYy3mHWLh24LJZKVdXZ2PNdizSe1B/E8bVm+HYcjbEzGuVXDv/di+EzdO/6Gq80A==", "dev": true, "requires": { "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^26.2.2", - "@jest/types": "^26.2.0", - "babel-jest": "^26.2.2", + "@jest/test-sequencer": "^26.4.2", + "@jest/types": "^26.3.0", + "babel-jest": "^26.3.0", "chalk": "^4.0.0", "deepmerge": "^4.2.2", "glob": "^7.1.1", "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^26.2.0", - "jest-environment-node": "^26.2.0", - "jest-get-type": "^26.0.0", - "jest-jasmine2": "^26.2.2", + "jest-environment-jsdom": "^26.3.0", + "jest-environment-node": "^26.3.0", + "jest-get-type": "^26.3.0", + "jest-jasmine2": "^26.4.2", "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.2.2", - "jest-util": "^26.2.0", - "jest-validate": "^26.2.0", + "jest-resolve": "^26.4.0", + "jest-util": "^26.3.0", + "jest-validate": "^26.4.2", "micromatch": "^4.0.2", - "pretty-format": "^26.2.0" + "pretty-format": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -15079,27 +14834,41 @@ "dev": true }, "jest-get-type": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", - "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "pretty-format": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.2.0.tgz", - "integrity": "sha512-qi/8IuBu2clY9G7qCXgCdD1Bf9w+sXakdHTRToknzMtVy0g7c4MBWaZy7MfB7ndKZovRO6XRwJiAYqq+MC7SDA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15181,31 +14950,40 @@ } }, "jest-each": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.2.0.tgz", - "integrity": "sha512-gHPCaho1twWHB5bpcfnozlc6mrMi+VAewVPNgmwf81x2Gzr6XO4dl+eOrwPWxbkYlgjgrYjWK2xgKnixbzH3Ew==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.4.2.tgz", + "integrity": "sha512-p15rt8r8cUcRY0Mvo1fpkOGYm7iI8S6ySxgIdfh3oOIv+gHwrHTy5VWCGOecWUhDsit4Nz8avJWdT07WLpbwDA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "chalk": "^4.0.0", - "jest-get-type": "^26.0.0", - "jest-util": "^26.2.0", - "pretty-format": "^26.2.0" + "jest-get-type": "^26.3.0", + "jest-util": "^26.3.0", + "pretty-format": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -15254,27 +15032,41 @@ "dev": true }, "jest-get-type": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", - "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "pretty-format": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.2.0.tgz", - "integrity": "sha512-qi/8IuBu2clY9G7qCXgCdD1Bf9w+sXakdHTRToknzMtVy0g7c4MBWaZy7MfB7ndKZovRO6XRwJiAYqq+MC7SDA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15283,33 +15075,42 @@ } }, "jest-environment-jsdom": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.2.0.tgz", - "integrity": "sha512-sDG24+5M4NuIGzkI3rJW8XUlrpkvIdE9Zz4jhD8OBnVxAw+Y1jUk9X+lAOD48nlfUTlnt3lbAI3k2Ox+WF3S0g==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.3.0.tgz", + "integrity": "sha512-zra8He2btIMJkAzvLaiZ9QwEPGEetbxqmjEBQwhH3CA+Hhhu0jSiEJxnJMbX28TGUvPLxBt/zyaTLrOPF4yMJA==", "dev": true, "requires": { - "@jest/environment": "^26.2.0", - "@jest/fake-timers": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/environment": "^26.3.0", + "@jest/fake-timers": "^26.3.0", + "@jest/types": "^26.3.0", "@types/node": "*", - "jest-mock": "^26.2.0", - "jest-util": "^26.2.0", + "jest-mock": "^26.3.0", + "jest-util": "^26.3.0", "jsdom": "^16.2.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -15351,10 +15152,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15363,32 +15178,41 @@ } }, "jest-environment-node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.2.0.tgz", - "integrity": "sha512-4M5ExTYkJ19efBzkiXtBi74JqKLDciEk4CEsp5tTjWGYMrlKFQFtwIVG3tW1OGE0AlXhZjuHPwubuRYY4j4uOw==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.3.0.tgz", + "integrity": "sha512-c9BvYoo+FGcMj5FunbBgtBnbR5qk3uky8PKyRVpSfe2/8+LrNQMiXX53z6q2kY+j15SkjQCOSL/6LHnCPLVHNw==", "dev": true, "requires": { - "@jest/environment": "^26.2.0", - "@jest/fake-timers": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/environment": "^26.3.0", + "@jest/fake-timers": "^26.3.0", + "@jest/types": "^26.3.0", "@types/node": "*", - "jest-mock": "^26.2.0", - "jest-util": "^26.2.0" + "jest-mock": "^26.3.0", + "jest-util": "^26.3.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -15430,10 +15254,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15448,12 +15286,12 @@ "dev": true }, "jest-haste-map": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.2.2.tgz", - "integrity": "sha512-3sJlMSt+NHnzCB+0KhJ1Ut4zKJBiJOlbrqEYNdRQGlXTv8kqzZWjUKQRY3pkjmlf+7rYjAV++MQ4D6g4DhAyOg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.3.0.tgz", + "integrity": "sha512-DHWBpTJgJhLLGwE5Z1ZaqLTYqeODQIZpby0zMBsCU9iRFHYyhklYqP4EiG73j5dkbaAdSZhgB938mL51Q5LeZA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "@types/graceful-fs": "^4.1.2", "@types/node": "*", "anymatch": "^3.0.3", @@ -15461,27 +15299,36 @@ "fsevents": "^2.1.2", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", - "jest-serializer": "^26.2.0", - "jest-util": "^26.2.0", - "jest-worker": "^26.2.1", + "jest-serializer": "^26.3.0", + "jest-util": "^26.3.0", + "jest-worker": "^26.3.0", "micromatch": "^4.0.2", "sane": "^4.0.3", "walker": "^1.0.7" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -15523,10 +15370,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15535,44 +15396,53 @@ } }, "jest-jasmine2": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.2.2.tgz", - "integrity": "sha512-Q8AAHpbiZMVMy4Hz9j1j1bg2yUmPa1W9StBvcHqRaKa9PHaDUMwds8LwaDyzP/2fkybcTQE4+pTMDOG9826tEw==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.4.2.tgz", + "integrity": "sha512-z7H4EpCldHN1J8fNgsja58QftxBSL+JcwZmaXIvV9WKIM+x49F4GLHu/+BQh2kzRKHAgaN/E82od+8rTOBPyPA==", "dev": true, "requires": { "@babel/traverse": "^7.1.0", - "@jest/environment": "^26.2.0", - "@jest/source-map": "^26.1.0", - "@jest/test-result": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/environment": "^26.3.0", + "@jest/source-map": "^26.3.0", + "@jest/test-result": "^26.3.0", + "@jest/types": "^26.3.0", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "expect": "^26.2.0", + "expect": "^26.4.2", "is-generator-fn": "^2.0.0", - "jest-each": "^26.2.0", - "jest-matcher-utils": "^26.2.0", - "jest-message-util": "^26.2.0", - "jest-runtime": "^26.2.2", - "jest-snapshot": "^26.2.2", - "jest-util": "^26.2.0", - "pretty-format": "^26.2.0", + "jest-each": "^26.4.2", + "jest-matcher-utils": "^26.4.2", + "jest-message-util": "^26.3.0", + "jest-runtime": "^26.4.2", + "jest-snapshot": "^26.4.2", + "jest-util": "^26.3.0", + "pretty-format": "^26.4.2", "throat": "^5.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -15620,22 +15490,36 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "pretty-format": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.2.0.tgz", - "integrity": "sha512-qi/8IuBu2clY9G7qCXgCdD1Bf9w+sXakdHTRToknzMtVy0g7c4MBWaZy7MfB7ndKZovRO6XRwJiAYqq+MC7SDA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15644,28 +15528,37 @@ } }, "jest-leak-detector": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.2.0.tgz", - "integrity": "sha512-aQdzTX1YiufkXA1teXZu5xXOJgy7wZQw6OJ0iH5CtQlOETe6gTSocaYKUNui1SzQ91xmqEUZ/WRavg9FD82rtQ==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.4.2.tgz", + "integrity": "sha512-akzGcxwxtE+9ZJZRW+M2o+nTNnmQZxrHJxX/HjgDaU5+PLmY1qnQPnMjgADPGCRPhB+Yawe1iij0REe+k/aHoA==", "dev": true, "requires": { - "jest-get-type": "^26.0.0", - "pretty-format": "^26.2.0" + "jest-get-type": "^26.3.0", + "pretty-format": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -15714,27 +15607,27 @@ "dev": true }, "jest-get-type": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", - "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, "pretty-format": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.2.0.tgz", - "integrity": "sha512-qi/8IuBu2clY9G7qCXgCdD1Bf9w+sXakdHTRToknzMtVy0g7c4MBWaZy7MfB7ndKZovRO6XRwJiAYqq+MC7SDA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15743,30 +15636,39 @@ } }, "jest-matcher-utils": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.2.0.tgz", - "integrity": "sha512-2cf/LW2VFb3ayPHrH36ZDjp9+CAeAe/pWBAwsV8t3dKcrINzXPVxq8qMWOxwt5BaeBCx4ZupVGH7VIgB8v66vQ==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.4.2.tgz", + "integrity": "sha512-KcbNqWfWUG24R7tu9WcAOKKdiXiXCbMvQYT6iodZ9k1f7065k0keUOW6XpJMMvah+hTfqkhJhRXmA3r3zMAg0Q==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^26.2.0", - "jest-get-type": "^26.0.0", - "pretty-format": "^26.2.0" + "jest-diff": "^26.4.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -15809,9 +15711,9 @@ "dev": true }, "diff-sequences": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.0.0.tgz", - "integrity": "sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.3.0.tgz", + "integrity": "sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig==", "dev": true }, "has-flag": { @@ -15821,39 +15723,39 @@ "dev": true }, "jest-diff": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.2.0.tgz", - "integrity": "sha512-Wu4Aopi2nzCsHWLBlD48TgRy3Z7OsxlwvHNd1YSnHc7q1NJfrmyCPoUXrTIrydQOG5ApaYpsAsdfnMbJqV1/wQ==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.4.2.tgz", + "integrity": "sha512-6T1XQY8U28WH0Z5rGpQ+VqZSZz8EN8rZcBtfvXaOkbwxIEeRre6qnuZQlbY1AJ4MKDxQF8EkrCvK+hL/VkyYLQ==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^26.0.0", - "jest-get-type": "^26.0.0", - "pretty-format": "^26.2.0" + "diff-sequences": "^26.3.0", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.4.2" } }, "jest-get-type": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", - "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, "pretty-format": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.2.0.tgz", - "integrity": "sha512-qi/8IuBu2clY9G7qCXgCdD1Bf9w+sXakdHTRToknzMtVy0g7c4MBWaZy7MfB7ndKZovRO6XRwJiAYqq+MC7SDA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15862,13 +15764,13 @@ } }, "jest-message-util": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.2.0.tgz", - "integrity": "sha512-g362RhZaJuqeqG108n1sthz5vNpzTNy926eNDszo4ncRbmmcMRIUAZibnd6s5v2XSBCChAxQtCoN25gnzp7JbQ==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.3.0.tgz", + "integrity": "sha512-xIavRYqr4/otGOiLxLZGj3ieMmjcNE73Ui+LdSW/Y790j5acqCsAdDiLIbzHCZMpN07JOENRWX5DcU+OQ+TjTA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "@types/stack-utils": "^1.0.1", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", @@ -15878,18 +15780,27 @@ }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -15932,9 +15843,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15943,28 +15854,37 @@ } }, "jest-mock": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.2.0.tgz", - "integrity": "sha512-XeC7yWtWmWByoyVOHSsE7NYsbXJLtJNgmhD7z4MKumKm6ET0si81bsSLbQ64L5saK3TgsHo2B/UqG5KNZ1Sp/Q==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.3.0.tgz", + "integrity": "sha512-PeaRrg8Dc6mnS35gOo/CbZovoDPKAeB1FICZiuagAgGvbWdNNyjQjkOaGUa/3N3JtpQ/Mh9P4A2D4Fv51NnP8Q==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "@types/node": "*" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -16007,9 +15927,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16030,34 +15950,43 @@ "dev": true }, "jest-resolve": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.2.2.tgz", - "integrity": "sha512-ye9Tj/ILn/0OgFPE/3dGpQPUqt4dHwIocxt5qSBkyzxQD8PbL0bVxBogX2FHxsd3zJA7V2H/cHXnBnNyyT9YoQ==", + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.4.0.tgz", + "integrity": "sha512-bn/JoZTEXRSlEx3+SfgZcJAVuTMOksYq9xe9O6s4Ekg84aKBObEaVXKOEilULRqviSLAYJldnoWV9c07kwtiCg==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^26.2.0", + "jest-util": "^26.3.0", "read-pkg-up": "^7.0.1", "resolve": "^1.17.0", "slash": "^3.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -16099,10 +16028,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16111,29 +16054,38 @@ } }, "jest-resolve-dependencies": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.2.2.tgz", - "integrity": "sha512-S5vufDmVbQXnpP7435gr710xeBGUFcKNpNswke7RmFvDQtmqPjPVU/rCeMlEU0p6vfpnjhwMYeaVjKZAy5QYJA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.4.2.tgz", + "integrity": "sha512-ADHaOwqEcVc71uTfySzSowA/RdxUpCxhxa2FNLiin9vWLB1uLPad3we+JSSROq5+SrL9iYPdZZF8bdKM7XABTQ==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "jest-regex-util": "^26.0.0", - "jest-snapshot": "^26.2.2" + "jest-snapshot": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -16176,9 +16128,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16187,46 +16139,55 @@ } }, "jest-runner": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.2.2.tgz", - "integrity": "sha512-/qb6ptgX+KQ+aNMohJf1We695kaAfuu3u3ouh66TWfhTpLd9WbqcF6163d/tMoEY8GqPztXPLuyG0rHRVDLxCA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.4.2.tgz", + "integrity": "sha512-FgjDHeVknDjw1gRAYaoUoShe1K3XUuFMkIaXbdhEys+1O4bEJS8Avmn4lBwoMfL8O5oFTdWYKcf3tEJyyYyk8g==", "dev": true, "requires": { - "@jest/console": "^26.2.0", - "@jest/environment": "^26.2.0", - "@jest/test-result": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/console": "^26.3.0", + "@jest/environment": "^26.3.0", + "@jest/test-result": "^26.3.0", + "@jest/types": "^26.3.0", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.7.1", "exit": "^0.1.2", "graceful-fs": "^4.2.4", - "jest-config": "^26.2.2", + "jest-config": "^26.4.2", "jest-docblock": "^26.0.0", - "jest-haste-map": "^26.2.2", - "jest-leak-detector": "^26.2.0", - "jest-message-util": "^26.2.0", - "jest-resolve": "^26.2.2", - "jest-runtime": "^26.2.2", - "jest-util": "^26.2.0", - "jest-worker": "^26.2.1", + "jest-haste-map": "^26.3.0", + "jest-leak-detector": "^26.4.2", + "jest-message-util": "^26.3.0", + "jest-resolve": "^26.4.0", + "jest-runtime": "^26.4.2", + "jest-util": "^26.3.0", + "jest-worker": "^26.3.0", "source-map-support": "^0.5.6", "throat": "^5.0.0" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -16274,10 +16235,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16286,52 +16261,61 @@ } }, "jest-runtime": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.2.2.tgz", - "integrity": "sha512-a8VXM3DxCDnCIdl9+QucWFfQ28KdqmyVFqeKLigHdErtsx56O2ZIdQkhFSuP1XtVrG9nTNHbKxjh5XL1UaFDVQ==", - "dev": true, - "requires": { - "@jest/console": "^26.2.0", - "@jest/environment": "^26.2.0", - "@jest/fake-timers": "^26.2.0", - "@jest/globals": "^26.2.0", - "@jest/source-map": "^26.1.0", - "@jest/test-result": "^26.2.0", - "@jest/transform": "^26.2.2", - "@jest/types": "^26.2.0", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.4.2.tgz", + "integrity": "sha512-4Pe7Uk5a80FnbHwSOk7ojNCJvz3Ks2CNQWT5Z7MJo4tX0jb3V/LThKvD9tKPNVNyeMH98J/nzGlcwc00R2dSHQ==", + "dev": true, + "requires": { + "@jest/console": "^26.3.0", + "@jest/environment": "^26.3.0", + "@jest/fake-timers": "^26.3.0", + "@jest/globals": "^26.4.2", + "@jest/source-map": "^26.3.0", + "@jest/test-result": "^26.3.0", + "@jest/transform": "^26.3.0", + "@jest/types": "^26.3.0", "@types/yargs": "^15.0.0", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.4", - "jest-config": "^26.2.2", - "jest-haste-map": "^26.2.2", - "jest-message-util": "^26.2.0", - "jest-mock": "^26.2.0", + "jest-config": "^26.4.2", + "jest-haste-map": "^26.3.0", + "jest-message-util": "^26.3.0", + "jest-mock": "^26.3.0", "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.2.2", - "jest-snapshot": "^26.2.2", - "jest-util": "^26.2.0", - "jest-validate": "^26.2.0", + "jest-resolve": "^26.4.0", + "jest-snapshot": "^26.4.2", + "jest-util": "^26.3.0", + "jest-validate": "^26.4.2", "slash": "^3.0.0", "strip-bom": "^4.0.0", "yargs": "^15.3.1" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -16373,6 +16357,20 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -16380,9 +16378,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16391,9 +16389,9 @@ } }, "jest-serializer": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.2.0.tgz", - "integrity": "sha512-V7snZI9IVmyJEu0Qy0inmuXgnMWDtrsbV2p9CRAcmlmPVwpC2ZM8wXyYpiugDQnwLHx0V4+Pnog9Exb3UO8M6Q==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.3.0.tgz", + "integrity": "sha512-IDRBQBLPlKa4flg77fqg0n/pH87tcRKwe8zxOVTWISxGpPHYkRZ1dXKyh04JOja7gppc60+soKVZ791mruVdow==", "dev": true, "requires": { "@types/node": "*", @@ -16401,41 +16399,50 @@ } }, "jest-snapshot": { - "version": "26.2.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.2.2.tgz", - "integrity": "sha512-NdjD8aJS7ePu268Wy/n/aR1TUisG0BOY+QOW4f6h46UHEKOgYmmkvJhh2BqdVZQ0BHSxTMt04WpCf9njzx8KtA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.4.2.tgz", + "integrity": "sha512-N6Uub8FccKlf5SBFnL2Ri/xofbaA68Cc3MGjP/NuwgnsvWh+9hLIR/DhrxbSiKXMY9vUW5dI6EW1eHaDHqe9sg==", "dev": true, "requires": { "@babel/types": "^7.0.0", - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "@types/prettier": "^2.0.0", "chalk": "^4.0.0", - "expect": "^26.2.0", + "expect": "^26.4.2", "graceful-fs": "^4.2.4", - "jest-diff": "^26.2.0", - "jest-get-type": "^26.0.0", - "jest-haste-map": "^26.2.2", - "jest-matcher-utils": "^26.2.0", - "jest-message-util": "^26.2.0", - "jest-resolve": "^26.2.2", + "jest-diff": "^26.4.2", + "jest-get-type": "^26.3.0", + "jest-haste-map": "^26.3.0", + "jest-matcher-utils": "^26.4.2", + "jest-message-util": "^26.3.0", + "jest-resolve": "^26.4.0", "natural-compare": "^1.4.0", - "pretty-format": "^26.2.0", + "pretty-format": "^26.4.2", "semver": "^7.3.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -16478,9 +16485,9 @@ "dev": true }, "diff-sequences": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.0.0.tgz", - "integrity": "sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.3.0.tgz", + "integrity": "sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig==", "dev": true }, "has-flag": { @@ -16490,30 +16497,30 @@ "dev": true }, "jest-diff": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.2.0.tgz", - "integrity": "sha512-Wu4Aopi2nzCsHWLBlD48TgRy3Z7OsxlwvHNd1YSnHc7q1NJfrmyCPoUXrTIrydQOG5ApaYpsAsdfnMbJqV1/wQ==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.4.2.tgz", + "integrity": "sha512-6T1XQY8U28WH0Z5rGpQ+VqZSZz8EN8rZcBtfvXaOkbwxIEeRre6qnuZQlbY1AJ4MKDxQF8EkrCvK+hL/VkyYLQ==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^26.0.0", - "jest-get-type": "^26.0.0", - "pretty-format": "^26.2.0" + "diff-sequences": "^26.3.0", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.4.2" } }, "jest-get-type": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", - "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, "pretty-format": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.2.0.tgz", - "integrity": "sha512-qi/8IuBu2clY9G7qCXgCdD1Bf9w+sXakdHTRToknzMtVy0g7c4MBWaZy7MfB7ndKZovRO6XRwJiAYqq+MC7SDA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" @@ -16526,9 +16533,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16616,32 +16623,41 @@ } }, "jest-validate": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.2.0.tgz", - "integrity": "sha512-8XKn3hM6VIVmLNuyzYLCPsRCT83o8jMZYhbieh4dAyKLc4Ypr36rVKC+c8WMpWkfHHpGnEkvWUjjIAyobEIY/Q==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.4.2.tgz", + "integrity": "sha512-blft+xDX7XXghfhY0mrsBCYhX365n8K5wNDC4XAcNKqqjEzsRUSXP44m6PL0QJEW2crxQFLLztVnJ4j7oPlQrQ==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "camelcase": "^6.0.0", "chalk": "^4.0.0", - "jest-get-type": "^26.0.0", + "jest-get-type": "^26.3.0", "leven": "^3.1.0", - "pretty-format": "^26.2.0" + "pretty-format": "^26.4.2" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -16696,27 +16712,27 @@ "dev": true }, "jest-get-type": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", - "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, "pretty-format": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.2.0.tgz", - "integrity": "sha512-qi/8IuBu2clY9G7qCXgCdD1Bf9w+sXakdHTRToknzMtVy0g7c4MBWaZy7MfB7ndKZovRO6XRwJiAYqq+MC7SDA==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", "dev": true, "requires": { - "@jest/types": "^26.2.0", + "@jest/types": "^26.3.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16725,33 +16741,42 @@ } }, "jest-watcher": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.2.0.tgz", - "integrity": "sha512-674Boco4Joe0CzgKPL6K4Z9LgyLx+ZvW2GilbpYb8rFEUkmDGgsZdv1Hv5rxsRpb1HLgKUOL/JfbttRCuFdZXQ==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.3.0.tgz", + "integrity": "sha512-XnLdKmyCGJ3VoF6G/p5ohbJ04q/vv5aH9ENI+i6BL0uu9WWB6Z7Z2lhQQk0d2AVZcRGp1yW+/TsoToMhBFPRdQ==", "dev": true, "requires": { - "@jest/test-result": "^26.2.0", - "@jest/types": "^26.2.0", + "@jest/test-result": "^26.3.0", + "@jest/types": "^26.3.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "jest-util": "^26.2.0", + "jest-util": "^26.3.0", "string-length": "^4.0.1" }, "dependencies": { "@jest/types": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.2.0.tgz", - "integrity": "sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -16793,10 +16818,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16805,9 +16844,9 @@ } }, "jest-worker": { - "version": "26.2.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.2.1.tgz", - "integrity": "sha512-+XcGMMJDTeEGncRb5M5Zq9P7K4sQ1sirhjdOxsN1462h6lFo9w59bl2LVQmdGEEeU3m+maZCkS2Tcc9SfCHO4A==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz", + "integrity": "sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==", "dev": true, "requires": { "@types/node": "*", @@ -16822,9 +16861,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -16881,9 +16920,9 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.3.0.tgz", - "integrity": "sha512-zggeX5UuEknpdZzv15+MS1dPYG0J/TftiiNunOeNxSl3qr8Z6cIlQpN0IdJa44z9aFxZRIVqRncvEhQ7X5DtZg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", + "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==", "dev": true, "requires": { "abab": "^2.0.3", @@ -17417,6 +17456,24 @@ "json5": "^1.0.1" } }, + "localforage": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.8.1.tgz", + "integrity": "sha512-azSSJJfc7h4bVpi0PGi+SmLQKJl2/8NErI+LhJsrORNikMZnhaQ7rv9fHj+ofwgSHrKRlsDCL/639a6nECIKuQ==", + "requires": { + "lie": "3.1.1" + }, + "dependencies": { + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "requires": { + "immediate": "~3.0.5" + } + } + } + }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -17437,9 +17494,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.assign": { "version": "4.2.0", @@ -18816,33 +18873,46 @@ } } }, + "mongodb-uri": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/mongodb-uri/-/mongodb-uri-0.9.7.tgz", + "integrity": "sha1-D3ca0W9IOuZfQoeWlCjp+8SqYYE=" + }, "mongoose": { - "version": "5.9.19", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.19.tgz", - "integrity": "sha512-wJ5FR2ykvyd17MRHA6sku/N1CMaC/kf4CnN357htD48RpzJhW60YDkxPSPLbkLg8Woa+i7jYi0glhzC0EcBcRQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.10.0.tgz", + "integrity": "sha512-5itAvBMVDG4+zTDtuLg/IyoTxEMgvpOSHnigQ9Cyh8LR4BEgMAChJj7JSaGkg+tr1AjCSY9DgSdU8bHqCOoxXg==", "requires": { "bson": "^1.1.4", "kareem": "2.3.1", - "mongodb": "3.5.9", + "mongodb": "3.6.0", "mongoose-legacy-pluralize": "1.0.2", "mpath": "0.7.0", "mquery": "3.2.2", "ms": "2.1.2", "regexp-clone": "1.0.0", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "sift": "7.0.1", "sliced": "1.0.1" }, "dependencies": { "bson": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.4.tgz", - "integrity": "sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", + "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "mongodb": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.0.tgz", + "integrity": "sha512-/XWWub1mHZVoqEsUppE0GV7u9kanLvHxho6EvBxQbShXTKYF9trhZC2NzbulRGeG7xMJHD8IOWRcdKx5LPjAjQ==", + "requires": { + "bl": "^2.2.0", + "bson": "^1.1.4", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } } } }, @@ -19296,9 +19366,9 @@ "dev": true }, "node-notifier": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-7.0.2.tgz", - "integrity": "sha512-ux+n4hPVETuTL8+daJXTOC6uKLgMsl1RYfFv7DKRzyvzBapqco0rZZ9g72ZN8VS6V+gvNYHYa/ofcCY8fkJWsA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz", + "integrity": "sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA==", "dev": true, "optional": true, "requires": { @@ -19306,7 +19376,7 @@ "is-wsl": "^2.2.0", "semver": "^7.3.2", "shellwords": "^0.1.1", - "uuid": "^8.2.0", + "uuid": "^8.3.0", "which": "^2.0.2" }, "dependencies": { @@ -20831,9 +20901,9 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz", + "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==", "dev": true }, "prettier-linter-helpers": { @@ -21261,11 +21331,6 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, - "raven-js": { - "version": "3.27.2", - "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.27.2.tgz", - "integrity": "sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==" - }, "raw-body": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", @@ -23430,9 +23495,9 @@ } }, "stylelint-config-prettier": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-8.0.1.tgz", - "integrity": "sha512-RcjNW7MUaNVqONhJH4+rtlAE3ow/9SsAM0YWV0Lgu3dbTKdWTa/pQXRdFWgoHWpzUKn+9oBKR5x8JdH+20wmgw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-8.0.2.tgz", + "integrity": "sha512-TN1l93iVTXpF9NJstlvP7nOu9zY2k+mN0NSFQ/VEGz15ZIP9ohdDZTtCWHs5LjctAhSAzaILULGbgiM0ItId3A==", "dev": true }, "stylelint-config-recommended": { @@ -23521,9 +23586,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -23852,9 +23917,9 @@ } }, "testcafe": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.8.6.tgz", - "integrity": "sha512-h4cpvyBZqBXAxCqjaf3iswWFWJ4ZCtNP64+BniWj+bU0MZbTD64DOsyF92zQg09x+odQhU8Hm2zyCxKgAMCbtg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.9.2.tgz", + "integrity": "sha512-85du0zDvzFWleVqRTTsAr8Lo+3gn4yETs9qFZBIEufk6oN1fLzgv6Q14GeaH3/IaKi+/smv55umTce/OXX0mbA==", "dev": true, "requires": { "@types/node": "^10.12.19", @@ -23881,10 +23946,12 @@ "dedent": "^0.4.0", "del": "^3.0.0", "device-specs": "^1.0.0", + "diff": "^4.0.2", "elegant-spinner": "^1.0.1", "emittery": "^0.4.1", "endpoint-utils": "^1.0.2", "error-stack-parser": "^1.3.6", + "execa": "^4.0.3", "globby": "^9.2.0", "graceful-fs": "^4.1.11", "graphlib": "^2.1.5", @@ -23919,8 +23986,8 @@ "sanitize-filename": "^1.6.0", "source-map-support": "^0.5.16", "strip-bom": "^2.0.0", - "testcafe-browser-tools": "2.0.12", - "testcafe-hammerhead": "17.1.2", + "testcafe-browser-tools": "2.0.13", + "testcafe-hammerhead": "17.1.15", "testcafe-legacy-api": "4.0.0", "testcafe-reporter-json": "^2.1.0", "testcafe-reporter-list": "^2.1.0", @@ -23940,9 +24007,9 @@ "dev": true }, "@types/node": { - "version": "10.17.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.26.tgz", - "integrity": "sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw==", + "version": "10.17.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.29.tgz", + "integrity": "sha512-zLo9rjUeQ5+QVhOufDwrb3XKyso31fJBJnk9wUUQIBDExF/O4LryvpOfozfUaxgqifTnlt7FyqsAPXUq5yFZSA==", "dev": true }, "array-union": { @@ -23995,6 +24062,17 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -24019,6 +24097,31 @@ "path-type": "^3.0.0" } }, + "execa": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } + } + }, "fast-glob": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", @@ -24244,12 +24347,27 @@ "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "parse5": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", "dev": true }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -24294,6 +24412,21 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -24333,13 +24466,22 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } } }, "testcafe-browser-tools": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/testcafe-browser-tools/-/testcafe-browser-tools-2.0.12.tgz", - "integrity": "sha512-5oNNYlcZiDspqJB6L8CfI4vxjzkvARSZv3pa+JrFAoqYmEA3VPiAvzrn+P0zi0D5jEPkKngW2KTpq6r3GfdDNw==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/testcafe-browser-tools/-/testcafe-browser-tools-2.0.13.tgz", + "integrity": "sha512-r0AfCNsOJWXHAR+KADumfCffsH3LYoEbJXfmGOG47uEt1FBEw8cWTSWRBp5As+DaAmh9pi75P+ZF6K09WudM/g==", "dev": true, "requires": { "array-find": "^1.0.0", @@ -24513,9 +24655,9 @@ } }, "testcafe-hammerhead": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-17.1.2.tgz", - "integrity": "sha512-WkTRrZoMYIZkB0NGiT+xrlD71hcMDX/a34iHNdrUBwcw9o/xi7ym4YzyZO0LXrG5b1pzuLS3ll/o5OqkhfRZnw==", + "version": "17.1.15", + "resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-17.1.15.tgz", + "integrity": "sha512-FvuaODbFUkoqyzH0OapiMO3/ISCB/v+0kKi0+5DhcIz1tRBishzyfLbHFyKXUf6rm0IhGJlME93uKFOM9NNOMw==", "dev": true, "requires": { "acorn-hammerhead": "^0.3.0", @@ -24525,18 +24667,18 @@ "crypto-md5": "^1.0.0", "css": "2.2.3", "debug": "4.1.1", - "esotope-hammerhead": "0.5.3", + "esotope-hammerhead": "0.5.5", "iconv-lite": "0.5.1", - "lodash": "^4.17.13", + "lodash": "^4.17.19", "lru-cache": "2.6.3", "match-url-wildcard": "0.0.4", "merge-stream": "^1.0.1", "mime": "~1.4.1", "mustache": "^2.1.1", - "nanoid": "^0.2.2", + "nanoid": "^3.1.12", "os-family": "^1.0.0", "parse5": "2.2.3", - "pinkie": "1.0.0", + "pinkie": "2.0.4", "read-file-relative": "^1.2.0", "semver": "5.5.0", "tough-cookie": "2.3.3", @@ -24596,9 +24738,9 @@ "dev": true }, "nanoid": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-0.2.2.tgz", - "integrity": "sha512-GHoRrvNEKiwdkwQ/enKL8AhQkkrBC/2KxMZkDvQzp8OtkpX8ZAmoYJWFVl7l8F2+HcEJUfdg21Ab2wXXfrvACQ==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", "dev": true }, "parse5": { @@ -24607,12 +24749,6 @@ "integrity": "sha1-DE/EHBAAxea5PUiwP4CDg3g06fY=", "dev": true }, - "pinkie": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-1.0.0.tgz", - "integrity": "sha1-Wkfyi6EBXQIBvae/DzWOR77Ix+Q=", - "dev": true - }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -25205,8 +25341,7 @@ "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", - "dev": true + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" }, "tsutils": { "version": "3.17.1", @@ -25764,9 +25899,9 @@ "dev": true }, "v8-to-istanbul": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", - "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-5.0.1.tgz", + "integrity": "sha512-mbDNjuDajqYe3TXFk5qxcQy8L1msXNE37WTlLoqqpBfRsimbNcrlhQlDPntmECEcUvdC+AQ8CyMMf6EUx1r74Q==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.1", @@ -26576,22 +26711,14 @@ "dev": true }, "whatwg-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz", - "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.2.1.tgz", + "integrity": "sha512-ZmVCr6nfBeaMxEHALLEGy0LszYjpJqf6PVNQUQ1qd9Et+q7Jpygd4rGGDXgHjD8e99yLFseD69msHDM4YwPZ4A==", "dev": true, "requires": { "lodash.sortby": "^4.7.0", "tr46": "^2.0.2", - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } + "webidl-conversions": "^6.1.0" } }, "which": { diff --git a/package.json b/package.json index 3a22ffd27d..aa0303e22d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "4.33.0", + "version": "4.34.1", "homepage": "https://form.gov.sg", "authors": [ "FormSG " @@ -67,9 +67,10 @@ "@opengovsg/myinfo-gov-client": "^1.0.4", "@opengovsg/ng-file-upload": "^12.2.14", "@opengovsg/spcp-auth-client": "^1.3.0", + "@sentry/browser": "^5.22.3", + "@sentry/integrations": "^5.22.3", "@stablelib/base64": "^1.0.0", "JSONStream": "^1.3.5", - "ajv": "^5.2.3", "angular": "~1.8.0", "angular-animate": "^1.8.0", "angular-aria": "^1.8.0", @@ -86,6 +87,7 @@ "angular-ui-router": "~1.0.22", "async": "~1.5.2", "await-to-js": "^2.1.1", + "aws-info": "^1.1.0", "aws-sdk": "^2.734.0", "axios": "^0.20.0", "bcrypt": "^5.0.0", @@ -127,10 +129,11 @@ "jszip": "^3.2.2", "jwt-decode": "^2.2.0", "libphonenumber-js": "^1.7.55", - "lodash": "^4.17.19", + "lodash": "^4.17.20", "mobile-detect": "^1.4.2", "moment-timezone": "0.5.31", - "mongoose": "^5.9.10", + "mongodb-uri": "^0.9.7", + "mongoose": "^5.10.0", "multiparty": ">=4.1.3", "ng-infinite-scroll": "^1.3.0", "ng-table": "^3.0.1", @@ -143,7 +146,6 @@ "promise-retry": "^2.0.1", "proxyquire": "^2.1.0", "puppeteer-core": "^5.2.1", - "raven-js": "^3.27.0", "rimraf": "^3.0.2", "selectize": "0.12.4", "slick-carousel": "1.8.1", @@ -163,8 +165,8 @@ "winston-cloudwatch": "^2.3.2" }, "devDependencies": { - "@babel/core": "^7.4.3", - "@babel/preset-env": "^7.11.0", + "@babel/core": "^7.11.5", + "@babel/preset-env": "^7.11.5", "@opengovsg/mockpass": "^2.4.6", "@shelf/jest-mongodb": "^1.2.2", "@types/bcrypt": "^3.0.0", @@ -184,12 +186,13 @@ "@types/nodemailer-direct-transport": "^1.0.31", "@types/promise-retry": "^1.1.3", "@types/puppeteer-core": "^2.0.0", + "@types/supertest": "^2.0.10", "@types/triple-beam": "^1.3.2", "@types/uid-generator": "^2.0.2", "@types/uuid": "^8.0.0", "@types/validator": "^13.0.0", - "@typescript-eslint/eslint-plugin": "^3.9.1", - "@typescript-eslint/parser": "^3.3.0", + "@typescript-eslint/eslint-plugin": "^4.0.1", + "@typescript-eslint/parser": "^4.0.0", "axios-mock-adapter": "^1.18.1", "babel-loader": "^8.0.5", "concurrently": "^3.6.1", @@ -198,7 +201,7 @@ "coveralls": "^3.1.0", "css-loader": "^2.1.1", "env-cmd": "^10.1.0", - "eslint": "^7.7.0", + "eslint": "^7.8.1", "eslint-config-prettier": "^6.11.0", "eslint-plugin-angular": "^4.0.1", "eslint-plugin-html": "^6.0.2", @@ -214,23 +217,23 @@ "jasmine-core": "^3.1.0", "jasmine-sinon": "^0.4.0", "jasmine-spec-reporter": "^5.0.2", - "jest": "^26.2.2", + "jest": "^26.4.2", "lint-staged": "^10.2.2", "maildev": "^1.1.0", "mini-css-extract-plugin": "^0.5.0", "mongodb-memory-server-core": "^5.1.5", "ngrok": "^3.2.7", "optimize-css-assets-webpack-plugin": "^5.0.1", - "prettier": "^2.0.5", + "prettier": "^2.1.1", "regenerator": "^0.14.4", "sinon": "^9.0.3", "stylelint": "^13.3.3", - "stylelint-config-prettier": "^8.0.1", + "stylelint-config-prettier": "^8.0.2", "stylelint-config-standard": "^20.0.0", "stylelint-prettier": "^1.1.2", "supertest": "^3.3.0", "terser-webpack-plugin": "^1.2.3", - "testcafe": "^1.8.0", + "testcafe": "^1.9.1", "ts-jest": "^26.1.4", "ts-loader": "^7.0.5", "ts-mock-imports": "^1.3.0", diff --git a/scripts/date-logic/date-logic.js b/scripts/date-logic/date-logic.js index f151512296..2b4f01b317 100644 --- a/scripts/date-logic/date-logic.js +++ b/scripts/date-logic/date-logic.js @@ -54,5 +54,4 @@ { '$count': 'numFormFields' } ] db.getCollection('forms').aggregate(afterScriptFormFieldsWithIsFutureOnlyTrue) - - \ No newline at end of file + diff --git a/scripts/date-logic/remove-isfutureonly-key.js b/scripts/date-logic/remove-isfutureonly-key.js new file mode 100644 index 0000000000..4f3cb25d83 --- /dev/null +++ b/scripts/date-logic/remove-isfutureonly-key.js @@ -0,0 +1,31 @@ +/* eslint-disable */ + +// Number of forms with isFutureOnly key - expect 0 after running update + +db.forms.count({ + form_fields: { $elemMatch: { isFutureOnly: { $exists: true } } } +}) + +// Number of form fields with isFutureOnly key - expect 0 after running update + +const formFieldsWithIsFutureOnlyExist = [ + { + $match: { + form_fields: { $elemMatch: { isFutureOnly: { $exists: true } } } + }, + }, + { $project: { form_fields: 1 } }, + { $unwind: '$form_fields' }, + { $match: { 'form_fields.isFutureOnly': { $exists: true } } }, + { $count: 'numFormFields' } +] + +db.getCollection('forms').aggregate(formFieldsWithIsFutureOnlyExist) + +// Update - Remove isFutureOnly key + +db.forms.update( + { 'form_fields.isFutureOnly': { $exists: true } }, + { $unset: { 'form_fields.$[elem].isFutureOnly': '' } }, + { arrayFilters: [{ 'elem.isFutureOnly': { $exists: true } }], multi: true } +) diff --git a/src/app/constants/mail.ts b/src/app/constants/mail.ts index e10a0a8f6b..8d5c9f01ff 100644 --- a/src/app/constants/mail.ts +++ b/src/app/constants/mail.ts @@ -14,10 +14,10 @@ export const EMAIL_HEADERS = { } // Types of emails we send -export const EMAIL_TYPES = { - adminResponse: 'Admin (response)', - loginOtp: 'Login OTP', - verificationOtp: 'Verification OTP', - emailConfirmation: 'Email confirmation', - adminBounce: 'Admin (bounce notification)', +export enum EmailType { + AdminResponse = 'Admin (response)', + LoginOtp = 'Login OTP', + VerificationOtp = 'Verification OTP', + EmailConfirmation = 'Email confirmation', + AdminBounce = 'Admin (bounce notification)', } diff --git a/src/app/controllers/admin-forms.server.controller.js b/src/app/controllers/admin-forms.server.controller.js index 8712c907ec..14d7c8088c 100644 --- a/src/app/controllers/admin-forms.server.controller.js +++ b/src/app/controllers/admin-forms.server.controller.js @@ -8,7 +8,6 @@ const moment = require('moment-timezone') const _ = require('lodash') const JSONStream = require('JSONStream') const { StatusCodes } = require('http-status-codes') -const get = require('lodash/get') const logger = require('../../config/logger').createLoggerWithLabel(module) const errorHandler = require('./errors.server.controller') @@ -159,26 +158,7 @@ function makeModule(connection) { (f) => f.globalId === field.globalId, ) - // To keep date schema in sync (isFutureOnly and dateValidation) const actionNameString = String(action.name) - if (field.fieldType === 'date') { - // TODO: Remove after 31 Jul 2020 (#2437) - if (field.isNewClient === true) { - // If form is edited by admin using new client - field.isFutureOnly = - get(field.dateValidation, 'selectedDateValidation') === - 'Disallow past dates' - } else { - // If form is edited by admin using old client - field.dateValidation = { - selectedDateValidation: field.isFutureOnly - ? 'Disallow past dates' - : null, - customMinDate: null, - customMaxDate: null, - } - } - } switch (actionNameString) { case EditFieldActions.Create: diff --git a/src/app/controllers/authentication.server.controller.js b/src/app/controllers/authentication.server.controller.js index 1902b1ca8d..5787ddc46c 100755 --- a/src/app/controllers/authentication.server.controller.js +++ b/src/app/controllers/authentication.server.controller.js @@ -3,27 +3,9 @@ /** * Module dependencies. */ -const mongoose = require('mongoose') -const getUserModel = require('../models/user.server.model').default -const getTokenModel = require('../models/token.server.model').default -const getAgencyModel = require('../models/agency.server.model').default - -const User = getUserModel(mongoose) -const Token = getTokenModel(mongoose) -const Agency = getAgencyModel(mongoose) -const bcrypt = require('bcrypt') -const validator = require('validator') const { StatusCodes } = require('http-status-codes') -const config = require('../../config/config') -const defaults = require('../../config/defaults').default const PERMISSIONS = require('../utils/permission-levels').default -const { getRequestIp } = require('../utils/request') -const logger = require('../../config/logger').createLoggerWithLabel(module) -const { generateOtp } = require('../utils/otp') -const MailService = require('../services/mail.service').default - -const MAX_OTP_ATTEMPTS = 10 /** * Middleware that authenticates admin-user @@ -41,50 +23,6 @@ exports.authenticateUser = function (req, res, next) { } } -/** - * Checks if domain is from a whitelisted agency - * @param {Object} req - Express request object - * @param {Object} res - Express response object - */ -exports.validateDomain = function (req, res, next) { - let email = req.body.email - let emailDomain = String(validator.isEmail(email) && email.split('@').pop()) - Agency.findOne({ emailDomain }, function (err, agency) { - // Database issues - if (err) { - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - `Unable to validate email domain. If this issue persists, please submit a Support Form (${defaults.links.supportFormLink}).`, - ) - } - // Agency not found - if (!agency) { - logger.error({ - message: 'Agency not found', - meta: { - action: 'validateDomain', - email, - emailDomain, - ip: getRequestIp(req), - }, - error: err, - }) - return res - .status(StatusCodes.UNAUTHORIZED) - .send( - 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', - ) - } - // Agency whitelisted - res.locals = { - agency, - email, - } - return next() - }) -} - /** * Returns a middleware function that ensures that only users with the requiredPermission will pass. * @param {String} requiredPermission - Either 'write' or 'delete', indicating what level of authorization @@ -139,341 +77,3 @@ exports.verifyPermission = (requiredPermission) => }) } } - -/** - * Create OTP using bcrypt and save to DB - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Object} next - Express next middleware function - */ -exports.createOtp = function (req, res, next) { - // 1. Create 6 digit OTP using crypto - // 2. Hash OTP using bcrypt. OTP expires in 15 mins - // 3. Save OTP to DB - - let email = res.locals.email - let otp = generateOtp() - bcrypt.hash(otp, 10, function (bcryptErr, hashedOtp) { - if (bcryptErr) { - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - 'Error generating OTP. Please try again later and if the problem persists, contact us.', - ) - } - - let record = { - email: email, - hashedOtp: hashedOtp, - numOtpAttempts: 0, - expireAt: new Date(Date.now() + config.otpLifeSpan), - } - - Token.findOneAndUpdate( - { email: email }, - { - $set: record, - }, - { w: 1, upsert: true, new: true }, - function (updateErr, updatedRecord) { - if (updateErr) { - logger.error({ - message: 'Token update error', - meta: { - action: 'createOtp', - ip: getRequestIp(req), - url: req.url, - headers: req.headers, - }, - error: updateErr, - }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - 'Error saving OTP. Please try again later and if the problem persists, contact us.', - ) - } - - res.locals.otp = otp - res.locals.expireAt = updatedRecord.expireAt - return next() - }, - ) - }) -} - -/** - * Sends OTP using mail transport - * @param {Object} req - Express request object - * @param {Object} res - Express response object - */ -exports.sendOtp = async function (req, res) { - // 1. Configure email with OTP to be sent to user email - // 2. Return success statement to front end - - let otp = res.locals.otp - let recipient = res.locals.email - - try { - await MailService.sendLoginOtp({ - recipient, - otp, - ipAddress: getRequestIp(req), - }) - logger.info({ - message: 'Login OTP sent', - meta: { - action: 'sendOtp', - ip: getRequestIp(req), - email: recipient, - }, - }) - return res.status(StatusCodes.OK).send(`OTP sent to ${recipient}!`) - } catch (err) { - logger.error({ - message: 'Mail otp error', - meta: { - action: 'sendOtp', - ip: getRequestIp(req), - url: req.url, - headers: req.headers, - }, - error: err, - }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - 'Error sending OTP. Please try again later and if the problem persists, contact us.', - ) - } -} - -/** - * Verify OTP using bcrypt - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Object} next - Express next middleware function - */ -exports.verifyOtp = function (req, res, next) { - // 1. Increment the number of times this particular OTP has been attempted. - // * Upsert is false as we do not want OTP to be added to DB if not already present - // * We read and write to numOtpAttempts in findOneAndUpdate command to prevent concurrency issues - // 2. If number of attempts is more than max allowed, block user - // 3. Compare OTP given to hashed OTP in db - // 4. If OTP is correct, remove OTP from token table and call next() - let otp = req.body.otp - let email = res.locals.email - - let upsertData = { - $inc: { numOtpAttempts: 1 }, - } - - Token.findOneAndUpdate( - { email: email }, - upsertData, - { w: 1, upsert: false, new: true }, - function (updateErr, updatedRecord) { - if (updateErr) { - logger.error({ - message: 'Error updating Token in database', - meta: { - action: 'verifyOtp', - email, - ip: getRequestIp(req), - }, - error: updateErr, - }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - `Unable to login at this time. Please submit a Support Form (${defaults.links.supportFormLink}).`, - ) - } - if (!updatedRecord) { - logger.info({ - message: 'Expired OTP', - meta: { - action: 'verifyOtp', - ip: getRequestIp(req), - email, - }, - }) - return res - .status(StatusCodes.UNPROCESSABLE_ENTITY) - .send('OTP has expired. Click Resend to receive a new OTP.') - } - if (updatedRecord.numOtpAttempts > MAX_OTP_ATTEMPTS) { - logger.info({ - message: 'Exceeded max OTP attempts', - meta: { - action: 'verifyOtp', - ip: getRequestIp(req), - email, - }, - }) - return res - .status(StatusCodes.UNPROCESSABLE_ENTITY) - .send( - 'You have hit the max number of attempts for this OTP. Click Resend to receive a new OTP.', - ) - } - bcrypt.compare(otp, updatedRecord.hashedOtp, function ( - bcryptErr, - isCorrect, - ) { - if (bcryptErr) { - logger.error({ - message: 'Malformed OTP', - meta: { - action: 'verifyOtp', - ip: getRequestIp(req), - }, - error: bcryptErr, - }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - 'Malformed OTP. Please try again later and if the problem persists, contact us.', - ) - } - if (isCorrect) { - Token.findOneAndRemove({ email: email }, function (removeErr) { - if (removeErr) { - logger.error({ - message: 'Error removing Token in database', - meta: { - action: 'verifyOtp', - email, - ip: getRequestIp(req), - }, - error: removeErr, - }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - 'Failed to validate OTP. Please try again later and if the problem persists, contact us.', - ) - } else { - return next() - } - }) - } else { - logger.info({ - message: 'Invalid OTP', - meta: { - action: 'verifyOtp', - email, - ip: getRequestIp(req), - }, - }) - return res - .status(StatusCodes.UNAUTHORIZED) - .send('OTP is invalid. Please try again.') - } - }) - }, - ) -} - -/** - * Creates/updates user object and returns obj to client - * @param {Object} req - Express request object - * @param {Object} res - Express response object - */ -exports.signIn = function (req, res) { - // 1. Save user information to DB. Set agency id for new users - // 2. Start session by adding user and agency information to session object. Return user object to front end - - let email = res.locals.email - let agency = res.locals.agency - - let record = { - email: email, - agency: agency._id, - } - let upsertData = { - $set: record, - $setOnInsert: { - created: new Date(), - }, - } - User.findOneAndUpdate( - { email: email }, - upsertData, - { - w: 1, - upsert: true, - new: true, - runValidators: true, // Update validators are off by default - need to specify the runValidators option. - setDefaultsOnInsert: true, // Otherwise, defaults will not be set on update and findOneAndUpdate - }, - function (updateErr, user) { - if (updateErr || !user) { - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send( - `User signin failed. Please try again later and if the problem persists, submit our Support Form (${defaults.links.supportFormLink}).`, - ) - } - let userObj = { - agency: agency, - email: user.email, - contact: user.contact, - _id: user._id, - betaFlags: user.betaFlags, - } - - // Add user info to session - req.session.user = userObj - logger.info({ - message: 'Successful login', - meta: { - action: 'signIn', - email, - ip: getRequestIp(req), - }, - }) - return res.status(StatusCodes.OK).send(userObj) - }, - ) -} - -/** - * Sign out from session - * @param {Object} req - Express request object - * @param {Object} res - Express response object - */ -exports.signOut = function (req, res) { - req.session.destroy(function (err) { - if (err) { - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .send('Sign out failed') - } else { - res.clearCookie('connect.sid') - return res.status(StatusCodes.OK).send('Sign out successful') - } - }) -} - -/** - * Verifies if a request was sent by a user that has the beta flag enabled for a betaType - * @param {String} betaType - beta flag in User schema - * @param {Object} req - Express request object - * @param {Object} req.session - session from express-session - * @param {Object} [req.session.user] - user object with properties - * @param {Object} res - Express response object - * @param {Object} next - Express next object - */ - -exports.doesUserBeta = (betaType) => (req, res, next) => { - const { session } = req - const { user } = session - if (user && user.betaFlags && user.betaFlags[betaType]) { - return next() - } else { - return res.status(StatusCodes.FORBIDDEN).send({ - message: `User is not authorized to access beta feature: ${betaType}`, - }) - } -} diff --git a/src/app/controllers/webhooks.server.controller.js b/src/app/controllers/webhooks.server.controller.js deleted file mode 100644 index 9b08b94d42..0000000000 --- a/src/app/controllers/webhooks.server.controller.js +++ /dev/null @@ -1,73 +0,0 @@ -const formsgSdk = require('../../config/formsg-sdk') -const { validateWebhookUrl } = require('../../shared/util/webhook-validation') -const { - postWebhook, - handleWebhookSuccess, - handleWebhookFailure, - logWebhookFailure, -} = require('../services/webhooks.service') -const { WebhookValidationError } = require('../modules/webhooks/webhook.errors') - -/** - * POST submission to a specified URL. Only works for encrypted submissions. - * The webhook is fired on a best-effort basis, so the next middleware - * is always called. - * @param {Express.Request} req Express request object - * @param {Object} req.form The form object containing the webhook URL - * @param {Object} req.submission The submission saved to the database - * @param {function} next Next middleware - */ -function post(req, _res, next) { - const { form, submission } = req - if (form.webhook.url) { - const webhookUrl = form.webhook.url - const now = Date.now() - const submissionWebhookView = submission.getWebhookView() - - // Log and return, this should not happen. - if (!submissionWebhookView) { - logWebhookFailure( - new WebhookValidationError('submissionWebhookView was null'), - { - webhookUrl, - submissionWebhookView, - now, - }, - ) - return next() - } - - const { submissionId, formId } = submissionWebhookView.data - - const signature = formsgSdk.webhooks.generateSignature({ - uri: form.webhook.url, - submissionId, - formId, - epoch: now, - }) - - const webhookParams = { - webhookUrl, - submissionWebhookView, - submissionId, - formId, - now, - signature, - } - // Use promises instead of await to prevent the user from having to await on the - // webhook before they receive acknowledgement that their submission was successful. - validateWebhookUrl(webhookParams.webhookUrl) - .then(() => postWebhook(webhookParams)) - .then((response) => { - handleWebhookSuccess(response, webhookParams) - }) - .catch((error) => { - handleWebhookFailure(error, webhookParams) - }) - } - return next() -} - -module.exports = { - post, -} diff --git a/src/app/factories/spcp-myinfo.factory.js b/src/app/factories/spcp-myinfo.factory.js index d845c28148..dc2d672d79 100644 --- a/src/app/factories/spcp-myinfo.factory.js +++ b/src/app/factories/spcp-myinfo.factory.js @@ -56,6 +56,8 @@ const spcpFactory = ({ isEnabled, props }) => { singpassEserviceId: props.spEsrvcId, } let myInfoGovClient + // TODO: These env vars should move to spcp-myinfo.config and be validated + // as part of convict (Issue #255) if (config.nodeEnv === 'production') { let myInfoPrefix = process.env.MYINFO_CLIENT_CONFIG === 'stg' ? 'STG2-' : 'PROD2-' diff --git a/src/app/factories/webhook-verified-content.factory.js b/src/app/factories/webhook-verified-content.factory.js index 82a3cf77ff..46b5fb058b 100644 --- a/src/app/factories/webhook-verified-content.factory.js +++ b/src/app/factories/webhook-verified-content.factory.js @@ -1,4 +1,4 @@ -const webhooks = require('../controllers/webhooks.server.controller') +const webhook = require('../modules/webhook/webhook.controller') const spcp = require('../controllers/spcp.server.controller') const featureManager = require('../../config/feature-manager').default @@ -8,7 +8,7 @@ const webhookVerifiedContentFactory = ({ isEnabled, props }) => { encryptedVerifiedFields: spcp.encryptedVerifiedFields( props.signingSecretKey, ), - post: webhooks.post, + post: webhook.post, } } else { return { diff --git a/src/app/models/field/dateField.ts b/src/app/models/field/dateField.ts index 9df689be5e..b8a87b7eb8 100644 --- a/src/app/models/field/dateField.ts +++ b/src/app/models/field/dateField.ts @@ -7,13 +7,6 @@ import { MyInfoSchema } from './baseField' const createDateFieldSchema = () => { return new Schema({ myInfo: MyInfoSchema, - - // TODO((#2437): Remove isFutureOnly key after 31 Aug 2020 - isFutureOnly: { - type: Boolean, - default: false, - }, - dateValidation: { customMinDate: { type: Date, diff --git a/src/app/models/field/tableField.ts b/src/app/models/field/tableField.ts index 4a22dc25c4..aa5c8cfe12 100644 --- a/src/app/models/field/tableField.ts +++ b/src/app/models/field/tableField.ts @@ -23,8 +23,8 @@ const createColumnSchema = () => { ) ColumnSchema.pre('validate', function (next) { - let columnTypes = [BasicField.ShortText, BasicField.Dropdown] - let index = columnTypes.indexOf(this.columnType) + const columnTypes = [BasicField.ShortText, BasicField.Dropdown] + const index = columnTypes.indexOf(this.columnType) if (index > -1) { return next() } else { diff --git a/src/app/models/form.server.model.ts b/src/app/models/form.server.model.ts index 5880a6059e..41d0459105 100644 --- a/src/app/models/form.server.model.ts +++ b/src/app/models/form.server.model.ts @@ -4,7 +4,6 @@ import { Model, Mongoose, Schema, SchemaOptions } from 'mongoose' import validator from 'validator' import { FORM_DUPLICATE_KEYS } from '../../shared/constants' -import { validateWebhookUrl } from '../../shared/util/webhook-validation' import { AuthType, BasicField, @@ -13,6 +12,7 @@ import { FormOtpData, IEmailFormSchema, IEncryptedFormSchema, + IForm, IFormSchema, IPopulatedForm, LogicType, @@ -21,6 +21,7 @@ import { Status, } from '../../types' import { MB } from '../constants/filesize' +import { validateWebhookUrl } from '../modules/webhook/webhook.utils' import getAgencyModel from './agency.server.model' import { @@ -376,7 +377,7 @@ const compileFormModel = (db: Mongoose): IFormModel => { // Methods FormSchema.methods.getMainFields = function (this: IFormSchema) { - let form = { + const form = { _id: this._id, title: this.title, status: this.status, @@ -397,7 +398,7 @@ const compileFormModel = (db: Mongoose): IFormModel => { // Return a duplicate form object with the given properties FormSchema.methods.duplicate = function ( this: IFormSchema, - overrideProps: object, + overrideProps: Partial, ) { const newForm = pick(this, FORM_DUPLICATE_KEYS) Object.assign(newForm, overrideProps) @@ -493,42 +494,24 @@ const compileFormModel = (db: Mongoose): IFormModel => { return FormModel } -const compileEmailFormModel = (db: Mongoose) => { - return db.model( - ResponseMode.Email, - EmailFormSchema, - ) -} - -export const getEmailFormModel = (db: Mongoose) => { +const getFormModel = (db: Mongoose): IFormModel => { try { - return db.model(ResponseMode.Email) as IEmailFormModel + return db.model(FORM_SCHEMA_ID) as IFormModel } catch { - return compileEmailFormModel(db) + return compileFormModel(db) } } -const compileEncryptedFormModel = (db: Mongoose) => { - return db.model( - ResponseMode.Encrypt, - EncryptedFormSchema, - ) -} - -export const getEncryptedFormModel = (db: Mongoose) => { - try { - return db.model(ResponseMode.Encrypt) as IEncryptedFormModel - } catch { - return compileEncryptedFormModel(db) - } +export const getEmailFormModel = (db: Mongoose): IEmailFormModel => { + // Load or build base model first + getFormModel(db) + return db.model(ResponseMode.Email) as IEmailFormModel } -const getFormModel = (db: Mongoose) => { - try { - return db.model(FORM_SCHEMA_ID) as IFormModel - } catch { - return compileFormModel(db) - } +export const getEncryptedFormModel = (db: Mongoose): IEncryptedFormModel => { + // Load or build base model first + getFormModel(db) + return db.model(ResponseMode.Encrypt) as IEncryptedFormModel } export default getFormModel diff --git a/src/app/models/form_statistics_total.server.model.ts b/src/app/models/form_statistics_total.server.model.ts index 32bd665f63..d16081e965 100644 --- a/src/app/models/form_statistics_total.server.model.ts +++ b/src/app/models/form_statistics_total.server.model.ts @@ -5,7 +5,7 @@ import { IFormStatisticsTotalSchema } from '../../types' const FORM_STATS_TOTAL_SCHEMA_ID = 'FormStatisticsTotal' const FORM_STATS_COLLECTION_NAME = 'formStatisticsTotal' -interface IFormStatisticsTotalModel extends Model {} +type IFormStatisticsTotalModel = Model const compileFormStatisticsTotalModel = (db: Mongoose) => { const FormStatisticsTotalSchema = new Schema( diff --git a/src/app/models/submission.server.model.ts b/src/app/models/submission.server.model.ts index bc5cb243dc..ffe0a2b50e 100644 --- a/src/app/models/submission.server.model.ts +++ b/src/app/models/submission.server.model.ts @@ -152,34 +152,6 @@ encryptSubmissionSchema.methods.getWebhookView = function ( } } -export const getEmailSubmissionModel = (db: Mongoose) => { - try { - return db.model(SubmissionType.Email) as IEmailSubmissionModel - } catch { - return db.model( - SubmissionType.Email, - emailSubmissionSchema, - ) - } -} - -export const getEncryptSubmissionModel = (db: Mongoose) => { - try { - return db.model(SubmissionType.Encrypt) as IEncryptSubmissionModel - } catch { - return db.model( - SubmissionType.Encrypt, - encryptSubmissionSchema, - ) - } -} - -/** - * Form Submission Schema - * @param {Object} db - Active DB Connection - * @return {Object} Mongoose Model - */ - const compileSubmissionModel = (db: Mongoose) => { const Submission = db.model('Submission', SubmissionSchema) Submission.discriminator(SubmissionType.Email, emailSubmissionSchema) @@ -195,4 +167,14 @@ const getSubmissionModel = (db: Mongoose) => { } } +export const getEmailSubmissionModel = (db: Mongoose) => { + getSubmissionModel(db) + return db.model(SubmissionType.Email) as IEmailSubmissionModel +} + +export const getEncryptSubmissionModel = (db: Mongoose) => { + getSubmissionModel(db) + return db.model(SubmissionType.Encrypt) as IEncryptSubmissionModel +} + export default getSubmissionModel diff --git a/src/app/models/token.server.model.ts b/src/app/models/token.server.model.ts index 7b2da1b432..f4d2538956 100644 --- a/src/app/models/token.server.model.ts +++ b/src/app/models/token.server.model.ts @@ -1,18 +1,21 @@ -import { Model, Mongoose, Schema } from 'mongoose' +import { Mongoose, Schema } from 'mongoose' -import { ITokenSchema } from '../../types' +import { IToken, ITokenModel, ITokenSchema } from '../../types' export const TOKEN_SCHEMA_ID = 'Token' const TokenSchema = new Schema({ email: { type: String, + required: true, }, hashedOtp: { type: String, + required: true, }, expireAt: { type: Date, + required: true, }, numOtpAttempts: { type: Number, @@ -25,6 +28,40 @@ const TokenSchema = new Schema({ }) TokenSchema.index({ expireAt: 1 }, { expireAfterSeconds: 0 }) +// Statics +/** + * Upserts given OTP into Token collection. + */ +TokenSchema.statics.upsertOtp = async function ( + this: ITokenModel, + upsertParams: Omit, +) { + return this.findOneAndUpdate( + { email: upsertParams.email }, + { + $set: { ...upsertParams, numOtpAttempts: 0 }, + $inc: { numOtpSent: 1 }, + }, + { upsert: true, new: true, setDefaultsOnInsert: true, runValidators: true }, + ) +} + +/** + * Increments the number of login attempts for the document associated with the + * given email. + * @param email the email to retrieve the related Token document + */ +TokenSchema.statics.incrementAttemptsByEmail = async function ( + this: ITokenModel, + email: string, +) { + return this.findOneAndUpdate( + { email: email }, + { $inc: { numOtpAttempts: 1 } }, + { new: true }, + ) +} + /** * Token Schema * @param db - Active DB Connection @@ -32,9 +69,9 @@ TokenSchema.index({ expireAt: 1 }, { expireAfterSeconds: 0 }) */ const getTokenModel = (db: Mongoose) => { try { - return db.model(TOKEN_SCHEMA_ID) as Model + return db.model(TOKEN_SCHEMA_ID) as ITokenModel } catch { - return db.model(TOKEN_SCHEMA_ID, TokenSchema) + return db.model(TOKEN_SCHEMA_ID, TokenSchema) } } diff --git a/src/app/models/user.server.model.ts b/src/app/models/user.server.model.ts index 3efa2e0046..047e6634eb 100644 --- a/src/app/models/user.server.model.ts +++ b/src/app/models/user.server.model.ts @@ -1,15 +1,13 @@ import { parsePhoneNumberFromString } from 'libphonenumber-js/mobile' -import { Model, Mongoose, Schema } from 'mongoose' +import { Mongoose, Schema } from 'mongoose' import validator from 'validator' -import { IUserSchema } from '../../types' +import { IUser, IUserModel, IUserSchema } from '../../types' import getAgencyModel, { AGENCY_SCHEMA_ID } from './agency.server.model' export const USER_SCHEMA_ID = 'User' -export interface IUserModel extends Model {} - const compileUserModel = (db: Mongoose) => { const Agency = getAgencyModel(db) @@ -79,7 +77,27 @@ const compileUserModel = (db: Mongoose) => { } }) - return db.model(USER_SCHEMA_ID, UserSchema) + // Statics + /** + * Upserts given user details into User collection. + */ + UserSchema.statics.upsertUser = async function ( + this: IUserModel, + upsertParams: Pick, + ) { + return this.findOneAndUpdate( + { email: upsertParams.email }, + { $set: upsertParams }, + { + upsert: true, + new: true, + runValidators: true, + setDefaultsOnInsert: true, + }, + ) + } + + return db.model(USER_SCHEMA_ID, UserSchema) } /** diff --git a/src/app/modules/auth/__tests__/auth.controller.spec.ts b/src/app/modules/auth/__tests__/auth.controller.spec.ts new file mode 100644 index 0000000000..21ba412ef6 --- /dev/null +++ b/src/app/modules/auth/__tests__/auth.controller.spec.ts @@ -0,0 +1,285 @@ +import expressHandler from 'tests/unit/backend/helpers/jest-express' +import { mocked } from 'ts-jest/utils' + +import MailService from 'src/app/services/mail.service' +import { IAgencySchema, IUserSchema } from 'src/types' + +import * as UserService from '../../user/user.service' +import * as AuthController from '../auth.controller' +import { InvalidOtpError } from '../auth.errors' +import * as AuthService from '../auth.service' + +const VALID_EMAIL = 'test@example.com' + +// Mock services invoked by AuthController +jest.mock('../auth.service') +jest.mock('../../user/user.service') +jest.mock('src/app/services/mail.service') +const MockAuthService = mocked(AuthService) +const MockMailService = mocked(MailService) +const MockUserService = mocked(UserService) + +describe('auth.controller', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('handleCheckUser', () => { + it('should return 200', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + // Act + await AuthController.handleCheckUser( + expressHandler.mockRequest(), + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.sendStatus).toBeCalledWith(200) + }) + }) + + describe('handleLoginSendOtp', () => { + const MOCK_OTP = '123456' + const MOCK_REQ = expressHandler.mockRequest({ + body: { email: VALID_EMAIL }, + }) + + it('should return 200 when login OTP is generated and sent to recipient successfully', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock AuthService and MailService to return without errors + MockAuthService.createLoginOtp.mockResolvedValueOnce(MOCK_OTP) + MockMailService.sendLoginOtp.mockResolvedValueOnce(true) + + // Act + await AuthController.handleLoginSendOtp(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(200) + expect(mockRes.send).toBeCalledWith(`OTP sent to ${VALID_EMAIL}!`) + // Services should have been invoked. + expect(MockAuthService.createLoginOtp).toHaveBeenCalledTimes(1) + expect(MockMailService.sendLoginOtp).toHaveBeenCalledTimes(1) + }) + + it('should return 500 when there is an error generating login OTP', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock createLoginOtp failure + MockAuthService.createLoginOtp.mockRejectedValueOnce( + new Error('otp creation error'), + ) + + // Act + await AuthController.handleLoginSendOtp(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(500) + expect(mockRes.send).toBeCalledWith( + 'Failed to send login OTP. Please try again later and if the problem persists, contact us.', + ) + // Sending login OTP should not have been called. + expect(MockAuthService.createLoginOtp).toHaveBeenCalledTimes(1) + expect(MockMailService.sendLoginOtp).not.toHaveBeenCalled() + }) + + it('should return 500 when there is an error sending login OTP', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock createLoginOtp success but sendLoginOtp failure. + MockAuthService.createLoginOtp.mockResolvedValueOnce(MOCK_OTP) + MockMailService.sendLoginOtp.mockRejectedValueOnce( + new Error('send error'), + ) + + // Act + await AuthController.handleLoginSendOtp(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(500) + expect(mockRes.send).toBeCalledWith( + 'Error sending OTP. Please try again later and if the problem persists, contact us.', + ) + // Services should have been invoked. + expect(MockAuthService.createLoginOtp).toHaveBeenCalledTimes(1) + expect(MockMailService.sendLoginOtp).toHaveBeenCalledTimes(1) + }) + }) + + describe('handleLoginVerifyOtp', () => { + const MOCK_OTP = '123456' + const MOCK_REQ = expressHandler.mockRequest({ + body: { email: VALID_EMAIL, otp: MOCK_OTP }, + }) + const MOCK_AGENCY = { id: 'mock agency id' } as IAgencySchema + + it('should return 200 with the user when verification succeeds', async () => { + // Arrange + // Mock bare minimum mongo documents. + const mockUser = { + toObject: () => ({ id: 'imagine this is a user document from the db' }), + } as IUserSchema + // Add agency into locals due to precondition. + const mockRes = expressHandler.mockResponse({ + locals: { agency: MOCK_AGENCY }, + }) + + // Mock all service success. + MockAuthService.verifyLoginOtp.mockResolvedValueOnce(true) + MockUserService.retrieveUser.mockResolvedValueOnce(mockUser) + + // Act + await AuthController.handleLoginVerifyOtp(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(200) + expect(mockRes.send).toBeCalledWith({ + ...mockUser.toObject(), + agency: MOCK_AGENCY, + }) + }) + + it('should return 422 when verifying login OTP throws an InvalidOtpError', async () => { + // Arrange + // Add agency into locals due to precondition. + const mockRes = expressHandler.mockResponse({ + locals: { agency: MOCK_AGENCY }, + }) + const expectedInvalidOtpError = new InvalidOtpError() + // Mock error from verifyLoginOtp. + MockAuthService.verifyLoginOtp.mockRejectedValueOnce( + expectedInvalidOtpError, + ) + + // Act + await AuthController.handleLoginVerifyOtp(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(422) + expect(mockRes.send).toBeCalledWith(expectedInvalidOtpError.message) + // Check that the correct services have been called or not called. + expect(MockAuthService.verifyLoginOtp).toHaveBeenCalledTimes(1) + expect(MockUserService.retrieveUser).not.toHaveBeenCalled() + }) + + it('should return 500 when verifying login OTP throws a non-InvalidOtpError', async () => { + // Arrange + // Add agency into locals due to precondition. + const mockRes = expressHandler.mockResponse({ + locals: { agency: MOCK_AGENCY }, + }) + // Mock generic error from verifyLoginOtp. + MockAuthService.verifyLoginOtp.mockRejectedValueOnce( + new Error('generic error'), + ) + + // Act + await AuthController.handleLoginVerifyOtp(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(500) + expect(mockRes.send).toBeCalledWith( + 'Failed to validate OTP. Please try again later and if the problem persists, contact us.', + ) + // Check that the correct services have been called or not called. + expect(MockAuthService.verifyLoginOtp).toHaveBeenCalledTimes(1) + expect(MockUserService.retrieveUser).not.toHaveBeenCalled() + }) + + it('should return 500 when an error is thrown while upserting user', async () => { + // Arrange + // Add agency into locals due to precondition. + const mockRes = expressHandler.mockResponse({ + locals: { agency: MOCK_AGENCY }, + }) + MockAuthService.verifyLoginOtp.mockResolvedValueOnce(true) + MockUserService.retrieveUser.mockRejectedValueOnce( + new Error('upsert error'), + ) + + // Act + await AuthController.handleLoginVerifyOtp(MOCK_REQ, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(500) + expect(mockRes.send).toBeCalledWith( + // Use stringContaining here due to dynamic text and out of test scope. + expect.stringContaining( + 'User signin failed. Please try again later and if the problem persists', + ), + ) + // Check that the correct services have been called or not called. + expect(MockAuthService.verifyLoginOtp).toHaveBeenCalledTimes(1) + expect(MockUserService.retrieveUser).toHaveBeenCalledTimes(1) + }) + }) + + describe('handleSignout', () => { + it('should return 200 when session is successfully destroyed', async () => { + // Arrange + const mockDestroy = jest.fn().mockImplementation((fn) => fn(false)) + const mockClearCookie = jest.fn() + const mockReq = expressHandler.mockRequest({ + session: { + destroy: mockDestroy, + }, + }) + const mockRes = expressHandler.mockResponse({ + clearCookie: mockClearCookie, + }) + + // Act + await AuthController.handleSignout(mockReq, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(200) + expect(mockRes.send).toBeCalledWith('Sign out successful') + expect(mockClearCookie).toBeCalledTimes(1) + expect(mockDestroy).toBeCalledTimes(1) + }) + + it('should return 400 when session does not exist in request', async () => { + // Arrange + const mockReqWithoutSession = expressHandler.mockRequest() + const mockRes = expressHandler.mockResponse() + + // Act + await AuthController.handleSignout( + mockReqWithoutSession, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.sendStatus).toBeCalledWith(400) + }) + + it('should return 500 when error is thrown when destroying session', async () => { + // Arrange + const mockDestroyWithErr = jest + .fn() + .mockImplementation((fn) => fn(new Error('some error'))) + const mockClearCookie = jest.fn() + const mockReq = expressHandler.mockRequest({ + session: { + destroy: mockDestroyWithErr, + }, + }) + const mockRes = expressHandler.mockResponse({ + clearCookie: mockClearCookie, + }) + + // Act + await AuthController.handleSignout(mockReq, mockRes, jest.fn()) + + // Assert + expect(mockRes.status).toBeCalledWith(500) + expect(mockRes.send).toBeCalledWith('Sign out failed') + expect(mockDestroyWithErr).toBeCalledTimes(1) + expect(mockClearCookie).not.toBeCalled() + }) + }) +}) diff --git a/src/app/modules/auth/__tests__/auth.middlewares.spec.ts b/src/app/modules/auth/__tests__/auth.middlewares.spec.ts new file mode 100644 index 0000000000..4c3bb824ba --- /dev/null +++ b/src/app/modules/auth/__tests__/auth.middlewares.spec.ts @@ -0,0 +1,71 @@ +import expressHandler from 'tests/unit/backend/helpers/jest-express' +import { mocked } from 'ts-jest/utils' + +import { IAgencySchema } from 'src/types' + +import { InvalidDomainError } from '../auth.errors' +import * as AuthMiddleware from '../auth.middlewares' +import * as AuthService from '../auth.service' + +jest.mock('../auth.service') +const MockAuthService = mocked(AuthService) + +describe('auth.middleware', () => { + describe('validateDomain', () => { + const MOCK_REQ = expressHandler.mockRequest({ + body: { email: 'test@example.com' }, + }) + + it('should continue without error when domain is valid', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + const mockNext = jest.fn() + MockAuthService.getAgencyWithEmail.mockResolvedValueOnce( + {} as IAgencySchema, + ) + + // Act + await AuthMiddleware.validateDomain(MOCK_REQ, mockRes, mockNext) + + // Assert + expect(mockNext).toBeCalled() + }) + + it('should return 500 when retrieving agency throws non ApplicationError', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + const mockNext = jest.fn() + MockAuthService.getAgencyWithEmail.mockRejectedValueOnce( + new Error('some error'), + ) + + // Act + await AuthMiddleware.validateDomain(MOCK_REQ, mockRes, mockNext) + + // Assert + expect(mockRes.status).toBeCalledWith(500) + expect(mockRes.send).toBeCalledWith( + expect.stringContaining( + 'Unable to validate email domain. If this issue persists, please submit a Support Form', + ), + ) + expect(mockNext).not.toBeCalled() + }) + + it('should return with ApplicationError status and message when retrieving agency throws ApplicationError', async () => { + // Arrange + const expectedError = new InvalidDomainError() + const mockRes = expressHandler.mockResponse() + const mockNext = jest.fn() + MockAuthService.getAgencyWithEmail.mockRejectedValueOnce(expectedError) + + // Act + await AuthMiddleware.validateDomain(MOCK_REQ, mockRes, mockNext) + + // Assert + expect(mockRes.status).toBeCalledWith(expectedError.status) + expect(mockRes.send).toBeCalledWith(expectedError.message) + expect(mockNext).not.toBeCalled() + }) + }) +}) diff --git a/src/app/modules/auth/__tests__/auth.routes.spec.ts b/src/app/modules/auth/__tests__/auth.routes.spec.ts new file mode 100644 index 0000000000..b6606de02e --- /dev/null +++ b/src/app/modules/auth/__tests__/auth.routes.spec.ts @@ -0,0 +1,530 @@ +import { pick } from 'lodash' +import supertest from 'supertest' +import { CookieStore, setupApp } from 'tests/integration/helpers/express-setup' +import dbHandler from 'tests/unit/backend/helpers/jest-db' +import validator from 'validator' + +import MailService from 'src/app/services/mail.service' +import * as OtpUtils from 'src/app/utils/otp' +import { IAgencySchema } from 'src/types' + +import * as UserService from '../../user/user.service' +import { AuthRouter } from '../auth.routes' +import * as AuthService from '../auth.service' + +const app = setupApp('/auth', AuthRouter) +const cookieStore = new CookieStore() +const request = supertest(app) + +describe('auth.routes', () => { + beforeAll(async () => await dbHandler.connect()) + afterEach(async () => { + await dbHandler.clearDatabase() + jest.restoreAllMocks() + cookieStore.clear() + }) + afterAll(async () => await dbHandler.closeDatabase()) + + describe('POST /auth/checkuser', () => { + it('should return 400 when body.email is not provided as a param', async () => { + // Act + const response = await request.post('/auth/checkuser') + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('"email" is required') + }) + + it('should return 400 when body.email is invalid', async () => { + // Arrange + const invalidEmail = 'not an email' + + // Act + const response = await request + .post('/auth/checkuser') + .send({ email: invalidEmail }) + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('Please enter a valid email') + }) + + it('should return 401 when domain of body.email does not exist in Agency collection', async () => { + // Arrange + const validEmailWithInvalidDomain = 'test@example.com' + + // Act + const response = await request + .post('/auth/checkuser') + .send({ email: validEmailWithInvalidDomain }) + + // Assert + expect(response.status).toEqual(401) + expect(response.text).toEqual( + 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', + ) + }) + + it('should return 200 when domain of body.email exists in Agency collection', async () => { + // Arrange + // Insert agency + const validDomain = 'example.com' + const validEmail = `test@${validDomain}` + await dbHandler.insertDefaultAgency({ mailDomain: validDomain }) + + // Act + const response = await request + .post('/auth/checkuser') + .send({ email: validEmail }) + + // Assert + expect(response.status).toEqual(200) + expect(response.text).toEqual('OK') + }) + + it('should return 500 when validating domain throws an unknown error', async () => { + // Arrange + // Insert agency + const validDomain = 'example.com' + const validEmail = `test@${validDomain}` + await dbHandler.insertDefaultAgency({ mailDomain: validDomain }) + + const getAgencySpy = jest + .spyOn(AuthService, 'getAgencyWithEmail') + .mockRejectedValueOnce(new Error('some error occured')) + + // Act + const response = await request + .post('/auth/checkuser') + .send({ email: validEmail }) + + // Assert + expect(getAgencySpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.text).toEqual( + expect.stringContaining('Unable to validate email domain.'), + ) + }) + }) + + describe('POST /auth/sendotp', () => { + const VALID_DOMAIN = 'example.com' + const VALID_EMAIL = `test@${VALID_DOMAIN}` + const INVALID_DOMAIN = 'example.org' + + beforeEach(async () => + dbHandler.insertDefaultAgency({ mailDomain: VALID_DOMAIN }), + ) + + it('should return 400 when body.email is not provided as a param', async () => { + // Act + const response = await request.post('/auth/sendotp') + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('"email" is required') + }) + + it('should return 400 when body.email is invalid', async () => { + // Arrange + const invalidEmail = 'not an email' + + // Act + const response = await request + .post('/auth/sendotp') + .send({ email: invalidEmail }) + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('Please enter a valid email') + }) + + it('should return 401 when domain of body.email does not exist in Agency collection', async () => { + // Arrange + const validEmailWithInvalidDomain = `test@${INVALID_DOMAIN}` + expect(validator.isEmail(validEmailWithInvalidDomain)).toEqual(true) + + // Act + const response = await request + .post('/auth/sendotp') + .send({ email: validEmailWithInvalidDomain }) + + // Assert + expect(response.status).toEqual(401) + expect(response.text).toEqual( + 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', + ) + }) + + it('should return 500 when error occurs whilst creating OTP', async () => { + // Arrange + const createLoginOtpSpy = jest + .spyOn(AuthService, 'createLoginOtp') + .mockRejectedValueOnce(new Error('some error')) + + // Act + const response = await request + .post('/auth/sendotp') + .send({ email: VALID_EMAIL }) + + // Assert + expect(createLoginOtpSpy).toHaveBeenCalled() + expect(response.status).toEqual(500) + expect(response.text).toEqual( + 'Failed to send login OTP. Please try again later and if the problem persists, contact us.', + ) + }) + + it('should return 500 when error occurs whilst sending login OTP', async () => { + // Arrange + const sendLoginOtpSpy = jest + .spyOn(MailService, 'sendLoginOtp') + .mockRejectedValueOnce(new Error('some error')) + + // Act + const response = await request + .post('/auth/sendotp') + .send({ email: VALID_EMAIL }) + + // Assert + expect(sendLoginOtpSpy).toHaveBeenCalled() + expect(response.status).toEqual(500) + expect(response.text).toEqual( + 'Error sending OTP. Please try again later and if the problem persists, contact us.', + ) + }) + + it('should return 500 when validating domain throws an unknown error', async () => { + // Arrange + const getAgencySpy = jest + .spyOn(AuthService, 'getAgencyWithEmail') + .mockRejectedValueOnce(new Error('some error occured')) + + // Act + const response = await request + .post('/auth/sendotp') + .send({ email: VALID_EMAIL }) + + // Assert + expect(getAgencySpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.text).toEqual( + expect.stringContaining('Unable to validate email domain.'), + ) + }) + + it('should return 200 when otp is sent successfully', async () => { + // Arrange + const sendLoginOtpSpy = jest + .spyOn(MailService, 'sendLoginOtp') + .mockResolvedValueOnce(true) + + // Act + const response = await request + .post('/auth/sendotp') + .send({ email: VALID_EMAIL }) + + // Assert + expect(sendLoginOtpSpy).toHaveBeenCalled() + expect(response.status).toEqual(200) + expect(response.text).toEqual(`OTP sent to ${VALID_EMAIL}!`) + }) + }) + + describe('POST /auth/verifyotp', () => { + const MOCK_VALID_OTP = '123456' + const VALID_DOMAIN = 'example.com' + const VALID_EMAIL = `test@${VALID_DOMAIN}` + const INVALID_DOMAIN = 'example.org' + + let defaultAgency: IAgencySchema + + beforeEach(async () => { + defaultAgency = await dbHandler.insertDefaultAgency({ + mailDomain: VALID_DOMAIN, + }) + jest.spyOn(OtpUtils, 'generateOtp').mockReturnValue(MOCK_VALID_OTP) + }) + + it('should return 400 when body.email is not provided as a param', async () => { + // Act + const response = await request.post('/auth/verifyotp').send({ + otp: MOCK_VALID_OTP, + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('"email" is required') + }) + + it('should return 400 when body.otp is not provided as a param', async () => { + // Act + const response = await request.post('/auth/verifyotp').send({ + email: VALID_EMAIL, + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('"otp" is required') + }) + + it('should return 400 when body.email is invalid', async () => { + // Arrange + const invalidEmail = 'not an email' + + // Act + const response = await request + .post('/auth/verifyotp') + .send({ email: invalidEmail, otp: MOCK_VALID_OTP }) + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('Please enter a valid email') + }) + + it('should return 400 when body.otp is less than 6 digits', async () => { + // Act + const response = await request.post('/auth/verifyotp').send({ + email: VALID_EMAIL, + otp: '12345', + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('Please enter a valid otp') + }) + + it('should return 400 when body.otp is 6 characters but does not consist purely of digits', async () => { + // Act + const response = await request.post('/auth/verifyotp').send({ + email: VALID_EMAIL, + otp: '123abc', + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.text).toEqual('Please enter a valid otp') + }) + + it('should return 401 when domain of body.email does not exist in Agency collection', async () => { + // Arrange + const validEmailWithInvalidDomain = `test@${INVALID_DOMAIN}` + expect(validator.isEmail(validEmailWithInvalidDomain)).toEqual(true) + + // Act + const response = await request + .post('/auth/verifyotp') + .send({ email: validEmailWithInvalidDomain, otp: MOCK_VALID_OTP }) + + // Assert + expect(response.status).toEqual(401) + expect(response.text).toEqual( + 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', + ) + }) + + it('should return 500 when validating domain throws an unknown error', async () => { + // Arrange + const getAgencySpy = jest + .spyOn(AuthService, 'getAgencyWithEmail') + .mockRejectedValueOnce(new Error('some error occured')) + + // Act + const response = await request + .post('/auth/verifyotp') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + expect(getAgencySpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.text).toEqual( + expect.stringContaining('Unable to validate email domain.'), + ) + }) + + it('should return 422 when hash does not exist for body.otp', async () => { + // Act + const response = await request + .post('/auth/verifyotp') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + expect(response.status).toEqual(422) + expect(response.text).toEqual( + expect.stringContaining( + 'OTP has expired. Please request for a new OTP.', + ), + ) + }) + + it('should return 422 when body.otp is invalid', async () => { + // Arrange + const invalidOtp = '654321' + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Act + const response = await request + .post('/auth/verifyotp') + .send({ email: VALID_EMAIL, otp: invalidOtp }) + + // Assert + expect(response.status).toEqual(422) + expect(response.text).toEqual('OTP is invalid. Please try again.') + }) + + it('should should return 422 when invalid body.otp has been attempted too many times', async () => { + // Arrange + const invalidOtp = '654321' + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Act + // Attempt invalid OTP for MAX_OTP_ATTEMPTS. + const verifyPromises = [] + for (let i = 0; i < AuthService.MAX_OTP_ATTEMPTS; i++) { + verifyPromises.push( + request + .post('/auth/verifyotp') + .send({ email: VALID_EMAIL, otp: invalidOtp }), + ) + } + const results = (await Promise.all(verifyPromises)).map((resolve) => + pick(resolve, ['status', 'text']), + ) + // Should be all invalid OTP responses. + expect(results).toEqual( + Array(AuthService.MAX_OTP_ATTEMPTS).fill({ + status: 422, + text: 'OTP is invalid. Please try again.', + }), + ) + + // Act again, this time with a valid OTP. + const response = await request + .post('/auth/verifyotp') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + // Should still reject with max OTP attempts error. + expect(response.status).toEqual(422) + expect(response.text).toEqual( + 'You have hit the max number of attempts. Please request for a new OTP.', + ) + }) + + it('should return 200 with user object when body.otp is a valid OTP', async () => { + // Arrange + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Act + const response = await request + .post('/auth/verifyotp') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + cookieStore.handleCookie(response) + + // Assert + expect(response.status).toEqual(200) + // Body should be an user object. + expect(response.body).toMatchObject({ + // Required since that's how the data is sent out from the application. + agency: JSON.parse(JSON.stringify(defaultAgency.toObject())), + _id: expect.any(String), + created: expect.any(String), + email: VALID_EMAIL, + }) + // Should have session cookie returned. + expect(cookieStore.get()).toEqual(expect.stringContaining('connect.sid')) + }) + + it('should return 500 when upserting user document fails', async () => { + // Arrange + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Mock error thrown when creating user + const upsertSpy = jest + .spyOn(UserService, 'retrieveUser') + .mockRejectedValueOnce(new Error('some error')) + + // Act + const response = await request + .post('/auth/verifyotp') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + // Should have reached this spy. + expect(upsertSpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.text).toEqual( + expect.stringContaining('User signin failed. Please try again later'), + ) + }) + }) + + describe('GET /auth/signout', () => { + const MOCK_VALID_OTP = '123456' + const VALID_DOMAIN = 'example.com' + const VALID_EMAIL = `test@${VALID_DOMAIN}` + + beforeEach(async () => { + await dbHandler.insertDefaultAgency({ + mailDomain: VALID_DOMAIN, + }) + jest.spyOn(OtpUtils, 'generateOtp').mockReturnValue(MOCK_VALID_OTP) + }) + + it('should return 200 and clear cookies when signout is successful', async () => { + // Act + // Sign in user + await signInUser(VALID_EMAIL, MOCK_VALID_OTP) + + // Arrange + const response = await request + .get('/auth/signout') + .set('Cookie', cookieStore.get()) + + // Assert + expect(response.status).toEqual(200) + expect(response.text).toEqual('Sign out successful') + // connect.sid should now be empty. + expect(response.header['set-cookie'][0]).toEqual( + expect.stringContaining('connect.sid=;'), + ) + }) + + it('should return 200 even when user has not signed in before', async () => { + // Arrange + // Should have no cookies. + expect(cookieStore.get()).toEqual('') + + // Act + const response = await request.get('/auth/signout') + + // Assert + expect(response.status).toEqual(200) + expect(response.text).toEqual('Sign out successful') + }) + }) +}) + +// Helper functions +const requestForOtp = async (email: string) => { + // Set that so no real mail is sent. + jest.spyOn(MailService, 'sendLoginOtp').mockResolvedValue(true) + + const response = await request.post('/auth/sendotp').send({ email }) + expect(response.text).toEqual(`OTP sent to ${email}!`) +} + +const signInUser = async (email: string, otp: string) => { + await requestForOtp(email) + const response = await request.post('/auth/verifyotp').send({ email, otp }) + cookieStore.handleCookie(response) + + // Assert + // Should have session cookie returned. + expect(cookieStore.get()).toEqual(expect.stringContaining('connect.sid')) + return response.body +} diff --git a/src/app/modules/auth/__tests__/auth.service.spec.ts b/src/app/modules/auth/__tests__/auth.service.spec.ts new file mode 100644 index 0000000000..540e0ed17d --- /dev/null +++ b/src/app/modules/auth/__tests__/auth.service.spec.ts @@ -0,0 +1,166 @@ +import mongoose from 'mongoose' +import dbHandler from 'tests/unit/backend/helpers/jest-db' +import { ImportMock } from 'ts-mock-imports' + +import getTokenModel from 'src/app/models/token.server.model' +import * as OtpUtils from 'src/app/utils/otp' +import { IAgencySchema } from 'src/types' + +import { InvalidDomainError, InvalidOtpError } from '../auth.errors' +import * as AuthService from '../auth.service' + +const TokenModel = getTokenModel(mongoose) + +const VALID_EMAIL_DOMAIN = 'test.gov.sg' +const VALID_EMAIL = `valid@${VALID_EMAIL_DOMAIN}` +const MOCK_OTP = '123456' + +// All calls to generateOtp will return MOCK_OTP. +ImportMock.mockFunction(OtpUtils, 'generateOtp', MOCK_OTP) + +describe('auth.service', () => { + let defaultAgency: IAgencySchema + + beforeAll(async () => { + await dbHandler.connect() + defaultAgency = await dbHandler.insertDefaultAgency({ + mailDomain: VALID_EMAIL_DOMAIN, + }) + }) + + // Only need to clear Token collection, and ignore other collections. + beforeEach( + async () => + await dbHandler.clearCollection(TokenModel.collection.collectionName), + ) + + afterAll(async () => await dbHandler.closeDatabase()) + + describe('getAgencyWithEmail', () => { + it('should retrieve agency successfully when email is valid and domain is in Agency collection', async () => { + // Act + const actual = await AuthService.getAgencyWithEmail(VALID_EMAIL) + + // Assert + expect(actual.toObject()).toEqual(defaultAgency.toObject()) + }) + + it('should throw InvalidDomainError when email is invalid', async () => { + // Arrange + const notAnEmail = 'not an email' + + // Act + const actualPromise = AuthService.getAgencyWithEmail(notAnEmail) + + // Assert + await expect(actualPromise).rejects.toThrowError(InvalidDomainError) + }) + + it('should throw InvalidDomainError when valid email domain is not in Agency collection', async () => { + // Arrange + const invalidEmail = 'invalid@example.com' + + // Act + const actualPromise = AuthService.getAgencyWithEmail(invalidEmail) + + // Assert + await expect(actualPromise).rejects.toThrowError(InvalidDomainError) + }) + }) + + describe('createLoginOtp', () => { + it('should create login otp successfully when email is valid', async () => { + // Arrange + // Should have no documents prior to this. + await expect(TokenModel.countDocuments()).resolves.toEqual(0) + + // Act + const actualOtp = await AuthService.createLoginOtp(VALID_EMAIL) + + // Assert + expect(actualOtp).toEqual(MOCK_OTP) + // Should have new token document inserted. + await expect(TokenModel.countDocuments()).resolves.toEqual(1) + }) + + it('should throw InvalidDomainError when email is invalid', async () => { + // Arrange + const notAnEmail = 'not an email' + + // Act + const actualPromise = AuthService.createLoginOtp(notAnEmail) + + // Assert + await expect(actualPromise).rejects.toThrowError(InvalidDomainError) + }) + }) + + describe('verifyLoginOtp', () => { + it('should successfully return true and delete Token document when OTP hash matches', async () => { + // Arrange + // Add a Token document to verify against. + await AuthService.createLoginOtp(VALID_EMAIL) + await expect(TokenModel.countDocuments()).resolves.toEqual(1) + + // Act + const actual = await AuthService.verifyLoginOtp(MOCK_OTP, VALID_EMAIL) + + // Assert + // Resolves successfully. + expect(actual).toEqual(true) + // Token document should be removed. + await expect(TokenModel.countDocuments()).resolves.toEqual(0) + }) + + it('should throw InvalidOtpError when Token document cannot be retrieved', async () => { + // Arrange + // No OTP requested; should have no documents prior to acting. + await expect(TokenModel.countDocuments()).resolves.toEqual(0) + + // Act + const verifyPromise = AuthService.verifyLoginOtp(MOCK_OTP, VALID_EMAIL) + + // Assert + const expectedError = new InvalidOtpError( + 'OTP has expired. Please request for a new OTP.', + ) + await expect(verifyPromise).rejects.toThrowError(expectedError) + }) + + it('should throw InvalidOtpError when verification has been attempted too many times', async () => { + // Arrange + // Add a Token document to verify against. + await AuthService.createLoginOtp(VALID_EMAIL) + // Update Token to already have MAX_OTP_ATTEMPTS. + await TokenModel.findOneAndUpdate( + { email: VALID_EMAIL }, + { $inc: { numOtpAttempts: AuthService.MAX_OTP_ATTEMPTS } }, + ) + + // Act + const verifyPromise = AuthService.verifyLoginOtp(MOCK_OTP, VALID_EMAIL) + + // Assert + const expectedError = new InvalidOtpError( + 'You have hit the max number of attempts. Please request for a new OTP.', + ) + await expect(verifyPromise).rejects.toThrowError(expectedError) + }) + + it('should throw InvalidOtpError when the OTP hash does not match', async () => { + // Arrange + // Add a Token document to verify against. + await AuthService.createLoginOtp(VALID_EMAIL) + const invalidOtp = '654321' + + // Act + const verifyPromise = AuthService.verifyLoginOtp(invalidOtp, VALID_EMAIL) + + // Assert + const expectedError = new InvalidOtpError( + 'OTP is invalid. Please try again.', + ) + await expect(verifyPromise).rejects.toThrowError(expectedError) + }) + }) +}) diff --git a/src/app/modules/auth/auth.controller.ts b/src/app/modules/auth/auth.controller.ts new file mode 100644 index 0000000000..6bee99769a --- /dev/null +++ b/src/app/modules/auth/auth.controller.ts @@ -0,0 +1,202 @@ +import to from 'await-to-js' +import { Request, RequestHandler } from 'express' +import { ParamsDictionary } from 'express-serve-static-core' +import { StatusCodes } from 'http-status-codes' +import { isEmpty } from 'lodash' + +import { createLoggerWithLabel } from '../../../config/logger' +import { LINKS } from '../../../shared/constants' +import MailService from '../../services/mail.service' +import { getRequestIp } from '../../utils/request' +import { ApplicationError } from '../core/core.errors' +import * as UserService from '../user/user.service' + +import * as AuthService from './auth.service' +import { ResponseAfter, SessionUser } from './auth.types' + +const logger = createLoggerWithLabel(module) + +/** + * Precondition: AuthMiddlewares.validateDomain must precede this handler. + * @returns 200 regardless, assumed to have passed domain validation. + */ +export const handleCheckUser: RequestHandler = async ( + _req: Request, + res: ResponseAfter['validateDomain'], +) => { + return res.sendStatus(StatusCodes.OK) +} + +/** + * Precondition: AuthMiddlewares.validateDomain must precede this handler. + */ +export const handleLoginSendOtp: RequestHandler = async ( + req: Request, + res: ResponseAfter['validateDomain'], +) => { + // Joi validation ensures existence. + const { email } = req.body + const requestIp = getRequestIp(req) + const logMeta = { + action: 'handleSendLoginOtp', + email, + ip: requestIp, + } + + // Create OTP. + const [otpErr, otp] = await to(AuthService.createLoginOtp(email)) + + if (otpErr) { + logger.error({ + message: 'Error generating OTP', + meta: logMeta, + error: otpErr, + }) + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send( + 'Failed to send login OTP. Please try again later and if the problem persists, contact us.', + ) + } + + // Send OTP. + const [sendErr] = await to( + MailService.sendLoginOtp({ + recipient: email, + otp, + ipAddress: requestIp, + }), + ) + if (sendErr) { + logger.error({ + message: 'Error mailing OTP', + meta: logMeta, + error: sendErr, + }) + + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send( + 'Error sending OTP. Please try again later and if the problem persists, contact us.', + ) + } + + // Successfully sent login otp. + logger.info({ + message: 'Login OTP sent successfully', + meta: logMeta, + }) + + return res.status(StatusCodes.OK).send(`OTP sent to ${email}!`) +} + +/** + * Precondition: AuthMiddlewares.validateDomain must precede this handler. + */ +export const handleLoginVerifyOtp: RequestHandler = async ( + req: Request< + ParamsDictionary, + string | SessionUser, + { email: string; otp: string } + >, + res: ResponseAfter['validateDomain'], +) => { + // Joi validation ensures existence. + const { email, otp } = req.body + // validateDomain middleware will populate agency. + const { agency } = res.locals + + const logMeta = { + action: 'handleLoginVerifyOtp', + email, + ip: getRequestIp(req), + } + + const [verifyErr] = await to(AuthService.verifyLoginOtp(otp, email)) + + if (verifyErr) { + logger.warn({ + message: + verifyErr instanceof ApplicationError + ? 'Login OTP is invalid' + : 'Error occurred when trying to validate login OTP', + meta: logMeta, + error: verifyErr, + }) + + if (verifyErr instanceof ApplicationError) { + return res.status(verifyErr.status).send(verifyErr.message) + } + + // Unknown error, return generic error response. + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send( + 'Failed to validate OTP. Please try again later and if the problem persists, contact us.', + ) + } + + // OTP is valid, proceed to login user. + try { + const user = await UserService.retrieveUser(email, agency) + // Create user object to return to frontend. + const userObj = { ...user.toObject(), agency } + + if (!req.session) { + throw new Error('req.session not found') + } + + // TODO(#212): Should store only userId in session. + // Add user info to session. + req.session.user = userObj as SessionUser + logger.info({ + message: `Successfully logged in user ${user.email}`, + meta: logMeta, + }) + + return res.status(StatusCodes.OK).send(userObj) + } catch (err) { + logger.error({ + message: 'Error logging in user', + meta: logMeta, + error: err, + }) + + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send( + `User signin failed. Please try again later and if the problem persists, submit our Support Form (${LINKS.supportFormLink}).`, + ) + } +} + +export const handleSignout: RequestHandler = async (req, res) => { + if (isEmpty(req.session)) { + logger.error({ + message: 'Attempted to sign out without a session', + meta: { + action: 'handleSignout', + }, + }) + return res.sendStatus(StatusCodes.BAD_REQUEST) + } + + req.session.destroy((error) => { + if (error) { + logger.error({ + message: 'Failed to destroy session', + meta: { + action: 'handleSignout', + }, + error, + }) + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send('Sign out failed') + } + + // No error. + res.clearCookie('connect.sid') + return res.status(StatusCodes.OK).send('Sign out successful') + }) +} diff --git a/src/app/modules/auth/auth.errors.ts b/src/app/modules/auth/auth.errors.ts new file mode 100644 index 0000000000..3f6ae71de2 --- /dev/null +++ b/src/app/modules/auth/auth.errors.ts @@ -0,0 +1,17 @@ +import { StatusCodes } from 'http-status-codes' + +import { ApplicationError } from '../core/core.errors' + +export class InvalidDomainError extends ApplicationError { + constructor( + message = 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', + ) { + super(message, StatusCodes.UNAUTHORIZED) + } +} + +export class InvalidOtpError extends ApplicationError { + constructor(message = 'OTP has expired. Please request for a new OTP.') { + super(message, StatusCodes.UNPROCESSABLE_ENTITY) + } +} diff --git a/src/app/modules/auth/auth.middlewares.ts b/src/app/modules/auth/auth.middlewares.ts new file mode 100644 index 0000000000..7a19f652e8 --- /dev/null +++ b/src/app/modules/auth/auth.middlewares.ts @@ -0,0 +1,58 @@ +import to from 'await-to-js' +import { RequestHandler } from 'express' +import { ParamsDictionary } from 'express-serve-static-core' +import { StatusCodes } from 'http-status-codes' + +import { createLoggerWithLabel } from '../../../config/logger' +import { LINKS } from '../../../shared/constants' +import { getRequestIp } from '../../utils/request' +import { ApplicationError } from '../core/core.errors' + +import * as AuthService from './auth.service' + +const logger = createLoggerWithLabel(module) + +/** + * Middleware to check if domain of email in the body is from a whitelisted + * agency. + * @returns 500 when there was an error validating email + * @returns 401 when email domain is invalid + * @returns sets retrieved agency in `res.locals.agency` and calls next when domain is valid + */ +export const validateDomain: RequestHandler< + ParamsDictionary, + string, + { email: string } +> = async (req, res, next) => { + // Joi validation ensures existence. + const { email } = req.body + + const [validationError, agency] = await to( + AuthService.getAgencyWithEmail(email), + ) + + if (validationError) { + logger.error({ + message: 'Domain validation error', + meta: { + action: 'validateDomain', + ip: getRequestIp(req), + email, + }, + error: validationError, + }) + if (validationError instanceof ApplicationError) { + return res.status(validationError.status).send(validationError.message) + } + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send( + `Unable to validate email domain. If this issue persists, please submit a Support Form at (${LINKS.supportFormLink}).`, + ) + } + + // Pass down agency to next handler. + res.locals.agency = agency + + return next() +} diff --git a/src/app/modules/auth/auth.routes.ts b/src/app/modules/auth/auth.routes.ts new file mode 100644 index 0000000000..8584040a04 --- /dev/null +++ b/src/app/modules/auth/auth.routes.ts @@ -0,0 +1,97 @@ +import { celebrate, Joi, Segments } from 'celebrate' +import { Router } from 'express' + +import * as AuthController from './auth.controller' +import * as AuthMiddlewares from './auth.middlewares' + +export const AuthRouter = Router() + +/** + * Check if email domain is a valid agency + * @route POST /auth/checkuser + * @group admin + * @param body.email the user's email to validate domain for + * @return 200 when email domain is valid + * @return 401 when email domain is invalid + */ +AuthRouter.post( + '/checkuser', + celebrate({ + [Segments.BODY]: Joi.object().keys({ + email: Joi.string() + .required() + .email() + .message('Please enter a valid email'), + }), + }), + AuthMiddlewares.validateDomain, + AuthController.handleCheckUser, +) + +/** + * Send a one-time password (OTP) to the specified email address + * as part of the login procedure. + * @route POST /auth/sendotp + * @group admin + * @param body.email the user's email to validate domain for + * @produces application/json + * @consumes application/json + * @return 200 when OTP has been been successfully sent + * @return 401 when email domain is invalid + * @return 500 when FormSG was unable to generate the OTP, or create/send the email that delivers the OTP to the user's email address + */ +AuthRouter.post( + '/sendotp', + celebrate({ + [Segments.BODY]: Joi.object().keys({ + email: Joi.string() + .required() + .email() + .message('Please enter a valid email'), + }), + }), + AuthMiddlewares.validateDomain, + AuthController.handleLoginSendOtp, +) + +/** + * Verify the one-time password (OTP) for the specified email address + * as part of the login procedure. + * @route POST /auth/verifyotp + * @group admin + * @param body.email the user's email + * @param body.otp the otp to verify + * @headers 200.set-cookie contains the session cookie upon login + * @returns 200 when user has successfully logged in, with session cookie set + * @returns 401 when the email domain is invalid + * @returns 422 when the OTP is invalid + * @returns 500 when error occurred whilst verifying the OTP + */ +AuthRouter.post( + '/verifyotp', + celebrate({ + [Segments.BODY]: Joi.object().keys({ + email: Joi.string() + .required() + .email() + .message('Please enter a valid email'), + otp: Joi.string() + .required() + .regex(/^\d{6}$/) + .message('Please enter a valid otp'), + }), + }), + AuthMiddlewares.validateDomain, + AuthController.handleLoginVerifyOtp, +) + +/** + * Sign the user out of the session by clearing the relevant session cookie + * @route GET /auth/signout + * @group admin + * @headers 200.clear-cookie clears cookie upon signout + * @returns 200 when user has signed out successfully + * @returns 400 when the request does not contain a session + * @returns 500 when the session fails to be destroyed + */ +AuthRouter.get('/signout', AuthController.handleSignout) diff --git a/src/app/modules/auth/auth.service.ts b/src/app/modules/auth/auth.service.ts new file mode 100644 index 0000000000..ac6d34f4b6 --- /dev/null +++ b/src/app/modules/auth/auth.service.ts @@ -0,0 +1,105 @@ +import bcrypt from 'bcrypt' +import mongoose from 'mongoose' +import validator from 'validator' + +import config from '../../../config/config' +import getAgencyModel from '../../models/agency.server.model' +import getTokenModel from '../../models/token.server.model' +import { generateOtp } from '../../utils/otp' + +import { InvalidDomainError, InvalidOtpError } from './auth.errors' + +const TokenModel = getTokenModel(mongoose) +const AgencyModel = getAgencyModel(mongoose) + +const DEFAULT_SALT_ROUNDS = 10 +export const MAX_OTP_ATTEMPTS = 10 + +/** + * Validates the domain of the given email. A domain is valid if it exists in + * the Agency collection in the database. + * @param email the email to validate the domain for + * @returns the agency document with the domain of the email only if it is valid. + * @throws error if database query fails or if agency cannot be found. + */ +export const getAgencyWithEmail = async (email: string) => { + // Extra guard even if Joi validation has already checked. + if (!validator.isEmail(email)) { + throw new InvalidDomainError() + } + + const emailDomain = email.split('@').pop() + const agency = await AgencyModel.findOne({ emailDomain }) + if (!agency) { + throw new InvalidDomainError() + } + + return agency +} + +/** + * Creates a login OTP and saves its hash into the Token collection. + * @param email the email to link the generated otp to + * @returns the generated OTP if saving into DB is successful + * @throws {InvalidDomainError} the given email is invalid + * @throws {Error} if any error occur whilst creating the OTP or insertion of OTP into the database. + */ +export const createLoginOtp = async (email: string) => { + if (!validator.isEmail(email)) { + throw new InvalidDomainError() + } + + const otp = generateOtp() + const hashedOtp = await bcrypt.hash(otp, DEFAULT_SALT_ROUNDS) + + await TokenModel.upsertOtp({ + email, + hashedOtp, + expireAt: new Date(Date.now() + config.otpLifeSpan), + }) + + return otp +} + +/** + * Compares the given otp with their hashed counterparts in the database to be + * retrieved with the given email. + * + * If the hash matches, the saved document in the database is removed and + * returned. Else, the document is kept in the database and an error is thrown. + * @param otpToVerify the OTP to verify with the hashed counterpart + * @param email the email used to retrieve the document from the database + * @returns true on success + * @throws {InvalidOtpError} if the OTP is invalid or expired. + * @throws {Error} if any errors occur whilst retrieving from database or comparing hashes. + */ +export const verifyLoginOtp = async (otpToVerify: string, email: string) => { + const updatedDocument = await TokenModel.incrementAttemptsByEmail(email) + + // Does not exist, return expired error message. + if (!updatedDocument) { + throw new InvalidOtpError('OTP has expired. Please request for a new OTP.') + } + + // Too many attempts. + if (updatedDocument.numOtpAttempts! > MAX_OTP_ATTEMPTS) { + throw new InvalidOtpError( + 'You have hit the max number of attempts. Please request for a new OTP.', + ) + } + + // Compare otp with saved hash. + const isOtpMatch = await bcrypt.compare( + otpToVerify, + updatedDocument.hashedOtp, + ) + + if (!isOtpMatch) { + throw new InvalidOtpError('OTP is invalid. Please try again.') + } + + // Hashed OTP matches, remove from collection. + await TokenModel.findOneAndRemove({ email }) + // Finally return true (as success). + return true +} diff --git a/src/app/modules/auth/auth.types.ts b/src/app/modules/auth/auth.types.ts new file mode 100644 index 0000000000..861c0e7a98 --- /dev/null +++ b/src/app/modules/auth/auth.types.ts @@ -0,0 +1,13 @@ +import { IAgencySchema, IPopulatedUser } from 'src/types' + +import { ResponseWithLocals } from '../core/core.types' + +/** + * Meta typing for the shape of the Express.Response object after various + * middlewares for /auth. + */ +export type ResponseAfter = { + validateDomain: ResponseWithLocals<{ agency?: IAgencySchema }> +} + +export type SessionUser = IPopulatedUser diff --git a/tests/unit/backend/helpers/bounce.ts b/src/app/modules/bounce/__tests__/bounce-test-helpers.ts similarity index 83% rename from tests/unit/backend/helpers/bounce.ts rename to src/app/modules/bounce/__tests__/bounce-test-helpers.ts index 4b69b3a71a..2ea9aba172 100644 --- a/tests/unit/backend/helpers/bounce.ts +++ b/src/app/modules/bounce/__tests__/bounce-test-helpers.ts @@ -26,6 +26,7 @@ const makeEmailNotification = ( formId: ObjectId, submissionId: ObjectId, recipientList: string[], + emailType: 'Admin (response)' | 'Email confirmation', ): IEmailNotification => { return { notificationType, @@ -43,7 +44,7 @@ const makeEmailNotification = ( }, { name: 'X-Formsg-Email-Type', - value: 'Admin (response)', + value: emailType, }, ], commonHeaders: { @@ -61,9 +62,16 @@ export const makeBounceNotification = ( recipientList: string[] = [], bouncedList: string[] = [], bounceType: 'Transient' | 'Permanent' = 'Permanent', + emailType: 'Admin (response)' | 'Email confirmation' = 'Admin (response)', ): ISnsNotification => { const Message = merge( - makeEmailNotification('Bounce', formId, submissionId, recipientList), + makeEmailNotification( + 'Bounce', + formId, + submissionId, + recipientList, + emailType, + ), { bounce: { bounceType, @@ -83,9 +91,16 @@ export const makeDeliveryNotification = ( submissionId: ObjectId = new ObjectId(), recipientList: string[] = [], deliveredList: string[] = [], + emailType: 'Admin (response)' | 'Email confirmation' = 'Admin (response)', ): ISnsNotification => { const Message = merge( - makeEmailNotification('Delivery', formId, submissionId, recipientList), + makeEmailNotification( + 'Delivery', + formId, + submissionId, + recipientList, + emailType, + ), { delivery: { recipients: deliveredList, diff --git a/tests/unit/backend/modules/bounce/bounce.controller.spec.ts b/src/app/modules/bounce/__tests__/bounce.controller.spec.ts similarity index 89% rename from tests/unit/backend/modules/bounce/bounce.controller.spec.ts rename to src/app/modules/bounce/__tests__/bounce.controller.spec.ts index e76d0b3700..ac73b3cdb2 100644 --- a/tests/unit/backend/modules/bounce/bounce.controller.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.controller.spec.ts @@ -2,7 +2,7 @@ import { StatusCodes } from 'http-status-codes' import expressHandler from 'tests/unit/backend/helpers/jest-express' import { mocked } from 'ts-jest/utils' -import handleSns from 'src/app/modules/bounce/bounce.controller' +import { handleSns } from 'src/app/modules/bounce/bounce.controller' import * as BounceService from 'src/app/modules/bounce/bounce.service' jest.mock('src/app/modules/bounce/bounce.service') @@ -20,7 +20,7 @@ describe('handleSns', () => { MockBounceService.isValidSnsRequest.mockReturnValueOnce( Promise.resolve(false), ) - await handleSns(MOCK_REQ, MOCK_RES) + await handleSns(MOCK_REQ, MOCK_RES, jest.fn()) expect(MockBounceService.isValidSnsRequest).toHaveBeenCalledWith( MOCK_REQ.body, ) @@ -32,7 +32,7 @@ describe('handleSns', () => { MockBounceService.isValidSnsRequest.mockReturnValueOnce( Promise.resolve(true), ) - await handleSns(MOCK_REQ, MOCK_RES) + await handleSns(MOCK_REQ, MOCK_RES, jest.fn()) expect(MockBounceService.isValidSnsRequest).toHaveBeenCalledWith( MOCK_REQ.body, ) @@ -44,7 +44,7 @@ describe('handleSns', () => { MockBounceService.isValidSnsRequest.mockImplementation(() => { throw new Error() }) - await handleSns(MOCK_REQ, MOCK_RES) + await handleSns(MOCK_REQ, MOCK_RES, jest.fn()) expect(MockBounceService.isValidSnsRequest).toHaveBeenCalledWith( MOCK_REQ.body, ) @@ -59,7 +59,7 @@ describe('handleSns', () => { MockBounceService.updateBounces.mockImplementation(() => { throw new Error() }) - await handleSns(MOCK_REQ, MOCK_RES) + await handleSns(MOCK_REQ, MOCK_RES, jest.fn()) expect(MockBounceService.isValidSnsRequest).toHaveBeenCalledWith( MOCK_REQ.body, ) diff --git a/tests/unit/backend/models/bounce.server.model.spec.ts b/src/app/modules/bounce/__tests__/bounce.model.spec.ts similarity index 97% rename from tests/unit/backend/models/bounce.server.model.spec.ts rename to src/app/modules/bounce/__tests__/bounce.model.spec.ts index 25a81a6f91..0475c12b0d 100644 --- a/tests/unit/backend/models/bounce.server.model.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.model.spec.ts @@ -1,15 +1,15 @@ import { ObjectId } from 'bson' import { omit, pick } from 'lodash' import mongoose from 'mongoose' +import dbHandler from 'tests/unit/backend/helpers/jest-db' -import getBounceModel from 'src/app/models/bounce.server.model' +import getBounceModel from 'src/app/modules/bounce/bounce.model' import { extractBounceObject, makeBounceNotification, makeDeliveryNotification, -} from '../helpers/bounce' -import dbHandler from '../helpers/jest-db' +} from './bounce-test-helpers' const Bounce = getBounceModel(mongoose) diff --git a/tests/unit/backend/modules/bounce/bounce.service.spec.ts b/src/app/modules/bounce/__tests__/bounce.service.spec.ts similarity index 73% rename from tests/unit/backend/modules/bounce/bounce.service.spec.ts rename to src/app/modules/bounce/__tests__/bounce.service.spec.ts index b6806163f8..90df58a7a2 100644 --- a/tests/unit/backend/modules/bounce/bounce.service.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.service.spec.ts @@ -4,31 +4,34 @@ import crypto from 'crypto' import dedent from 'dedent' import { cloneDeep, omit } from 'lodash' import mongoose from 'mongoose' +import dbHandler from 'tests/unit/backend/helpers/jest-db' +import getMockLogger, { + resetMockLogger, +} from 'tests/unit/backend/helpers/jest-logger' import { mocked } from 'ts-jest/utils' import * as LoggerModule from 'src/config/logger' -import { ISnsNotification } from 'src/types' +import { IBounceNotification, ISnsNotification } from 'src/types' import { extractBounceObject, makeBounceNotification, makeDeliveryNotification, MOCK_SNS_BODY, -} from '../../helpers/bounce' -import dbHandler from '../../helpers/jest-db' -import getMockLogger, { resetMockLogger } from '../../helpers/jest-logger' +} from './bounce-test-helpers' jest.mock('axios') const mockAxios = mocked(axios, true) jest.mock('src/config/logger') const MockLoggerModule = mocked(LoggerModule, true) +const mockShortTermLogger = getMockLogger() const mockLogger = getMockLogger() -MockLoggerModule.createCloudWatchLogger.mockReturnValue(mockLogger) -MockLoggerModule.createLoggerWithLabel.mockReturnValue(getMockLogger()) +MockLoggerModule.createCloudWatchLogger.mockReturnValue(mockShortTermLogger) +MockLoggerModule.createLoggerWithLabel.mockReturnValue(mockLogger) // Import modules which depend on config last so that mocks get imported correctly // eslint-disable-next-line import/first -import getBounceModel from 'src/app/models/bounce.server.model' +import getBounceModel from 'src/app/modules/bounce/bounce.model' // eslint-disable-next-line import/first import { isValidSnsRequest, @@ -111,11 +114,17 @@ describe('updateBounces', () => { 'email3@example.com', ] - beforeAll(async () => await dbHandler.connect()) + beforeAll(async () => { + await dbHandler.connect() + // Avoid being affected by other modules + resetMockLogger(mockLogger) + resetMockLogger(mockShortTermLogger) + }) afterEach(async () => { await dbHandler.clearDatabase() resetMockLogger(mockLogger) + resetMockLogger(mockShortTermLogger) }) afterAll(async () => await dbHandler.closeDatabase()) @@ -136,9 +145,11 @@ describe('updateBounces', () => { email, hasBounced: false, })) - expect(mockLogger.info).toHaveBeenCalledWith( - JSON.parse(notification.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { + ...JSON.parse(notification.Message), + }, + }) expect(mockLogger.warn).not.toHaveBeenCalled() expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -169,9 +180,11 @@ describe('updateBounces', () => { email, hasBounced: bounces[email], })) - expect(mockLogger.info).toHaveBeenCalledWith( - JSON.parse(notification.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { + ...JSON.parse(notification.Message), + }, + }) expect(mockLogger.warn).not.toHaveBeenCalled() expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -190,6 +203,9 @@ describe('updateBounces', () => { recipientList, recipientList, ) + const parsedNotification: IBounceNotification = JSON.parse( + notification.Message, + ) await updateBounces(notification) const actualBounceDoc = await Bounce.findOne({ formId }) const actualBounce = extractBounceObject(actualBounceDoc) @@ -197,11 +213,20 @@ describe('updateBounces', () => { email, hasBounced: true, })) - expect(mockLogger.info).toHaveBeenCalledWith( - JSON.parse(notification.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { + ...parsedNotification, + }, + }) expect(mockLogger.warn.mock.calls[0][0]).toMatchObject({ - type: 'CRITICAL BOUNCE', + message: 'Critical bounce', + meta: { + action: 'updateBounces', + hasAlarmed: false, + formId: formId.toHexString(), + submissionId: submissionId.toHexString(), + bounceInfo: parsedNotification.bounce, + }, }) expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -236,12 +261,16 @@ describe('updateBounces', () => { })) // There should only be one document after 2 notifications expect(actualBounceCursor.length).toBe(1) - expect(mockLogger.info.mock.calls[0][0]).toEqual( - JSON.parse(notification1.Message), - ) - expect(mockLogger.info.mock.calls[1][0]).toEqual( - JSON.parse(notification2.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { + ...JSON.parse(notification1.Message), + }, + }) + expect(mockLogger.info.mock.calls[1][0]).toMatchObject({ + meta: { + ...JSON.parse(notification2.Message), + }, + }) expect(mockLogger.warn).not.toHaveBeenCalled() expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -281,12 +310,16 @@ describe('updateBounces', () => { })) // There should only be one document after 2 notifications expect(actualBounceCursor.length).toBe(1) - expect(mockLogger.info.mock.calls[0][0]).toEqual( - JSON.parse(notification1.Message), - ) - expect(mockLogger.info.mock.calls[1][0]).toEqual( - JSON.parse(notification2.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { + ...JSON.parse(notification1.Message), + }, + }) + expect(mockLogger.info.mock.calls[1][0]).toMatchObject({ + meta: { + ...JSON.parse(notification2.Message), + }, + }) expect(mockLogger.warn).not.toHaveBeenCalled() expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -298,19 +331,26 @@ describe('updateBounces', () => { it('should save correctly when there are consecutive critical bounce notifications', async () => { const formId = new ObjectId() - const submissionId = new ObjectId() + const submissionId1 = new ObjectId() + const submissionId2 = new ObjectId() const notification1 = makeBounceNotification( formId, - submissionId, + submissionId1, recipientList, recipientList.slice(0, 1), // First email bounced ) + const parsedNotification1: IBounceNotification = JSON.parse( + notification1.Message, + ) const notification2 = makeBounceNotification( formId, - submissionId, + submissionId2, recipientList, recipientList.slice(1, 3), // Second and third email bounced ) + const parsedNotification2: IBounceNotification = JSON.parse( + notification2.Message, + ) await updateBounces(notification1) await updateBounces(notification2) const actualBounceCursor = await Bounce.find({ formId }) @@ -321,14 +361,25 @@ describe('updateBounces', () => { })) // There should only be one document after 2 notifications expect(actualBounceCursor.length).toBe(1) - expect(mockLogger.info.mock.calls[0][0]).toEqual( - JSON.parse(notification1.Message), - ) - expect(mockLogger.info.mock.calls[1][0]).toEqual( - JSON.parse(notification2.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { + ...parsedNotification1, + }, + }) + expect(mockLogger.info.mock.calls[1][0]).toMatchObject({ + meta: { + ...parsedNotification2, + }, + }) expect(mockLogger.warn.mock.calls[0][0]).toMatchObject({ - type: 'CRITICAL BOUNCE', + message: 'Critical bounce', + meta: { + action: 'updateBounces', + hasAlarmed: false, + formId: formId.toHexString(), + submissionId: submissionId2.toHexString(), + bounceInfo: parsedNotification2.bounce, + }, }) expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -368,12 +419,12 @@ describe('updateBounces', () => { })) // There should only be one document after 2 notifications expect(actualBounceCursor.length).toBe(1) - expect(mockLogger.info.mock.calls[0][0]).toEqual( - JSON.parse(notification1.Message), - ) - expect(mockLogger.info.mock.calls[1][0]).toEqual( - JSON.parse(notification2.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { ...JSON.parse(notification1.Message) }, + }) + expect(mockLogger.info.mock.calls[1][0]).toMatchObject({ + meta: { ...JSON.parse(notification2.Message) }, + }) expect(mockLogger.warn).not.toHaveBeenCalled() expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -413,12 +464,12 @@ describe('updateBounces', () => { })) // There should only be one document after 2 notifications expect(actualBounceCursor.length).toBe(1) - expect(mockLogger.info.mock.calls[0][0]).toEqual( - JSON.parse(notification1.Message), - ) - expect(mockLogger.info.mock.calls[1][0]).toEqual( - JSON.parse(notification2.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { ...JSON.parse(notification1.Message) }, + }) + expect(mockLogger.info.mock.calls[1][0]).toMatchObject({ + meta: { ...JSON.parse(notification2.Message) }, + }) expect(mockLogger.warn).not.toHaveBeenCalled() expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -453,12 +504,12 @@ describe('updateBounces', () => { })) // There should only be one document after 2 notifications expect(actualBounceCursor.length).toBe(1) - expect(mockLogger.info.mock.calls[0][0]).toEqual( - JSON.parse(notification1.Message), - ) - expect(mockLogger.info.mock.calls[1][0]).toEqual( - JSON.parse(notification2.Message), - ) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { ...JSON.parse(notification1.Message) }, + }) + expect(mockLogger.info.mock.calls[1][0]).toMatchObject({ + meta: { ...JSON.parse(notification2.Message) }, + }) expect(mockLogger.warn).not.toHaveBeenCalled() expect(omit(actualBounce, 'expireAt')).toEqual({ formId, @@ -468,7 +519,7 @@ describe('updateBounces', () => { expect(actualBounce.expireAt).toBeInstanceOf(Date) }) - it('should not log critical bounces when hasAlarmed is true', async () => { + it('should log a second critical bounce with hasAlarmed true', async () => { const formId = new ObjectId() const submissionId1 = new ObjectId() const submissionId2 = new ObjectId() @@ -478,12 +529,18 @@ describe('updateBounces', () => { recipientList, recipientList, ) + const parsedNotification1: IBounceNotification = JSON.parse( + notification1.Message, + ) const notification2 = makeBounceNotification( formId, submissionId2, recipientList, recipientList, ) + const parsedNotification2: IBounceNotification = JSON.parse( + notification2.Message, + ) await updateBounces(notification1) await updateBounces(notification2) const actualBounceCursor = await Bounce.find({ formId }) @@ -494,14 +551,32 @@ describe('updateBounces', () => { })) // There should only be one document after 2 notifications expect(actualBounceCursor.length).toBe(1) - expect(mockLogger.info.mock.calls[0][0]).toEqual( - JSON.parse(notification1.Message), - ) - expect(mockLogger.info.mock.calls[1][0]).toEqual( - JSON.parse(notification2.Message), - ) - // Expect only 1 call to logger.warn - expect(mockLogger.warn.mock.calls.length).toBe(1) + expect(mockLogger.info.mock.calls[0][0]).toMatchObject({ + meta: { ...parsedNotification1 }, + }) + expect(mockLogger.info.mock.calls[1][0]).toMatchObject({ + meta: { ...parsedNotification2 }, + }) + expect(mockLogger.warn.mock.calls[0][0]).toMatchObject({ + message: 'Critical bounce', + meta: { + action: 'updateBounces', + hasAlarmed: false, + formId: formId.toHexString(), + submissionId: submissionId1.toHexString(), + bounceInfo: parsedNotification1.bounce, + }, + }) + expect(mockLogger.warn.mock.calls[1][0]).toMatchObject({ + message: 'Critical bounce', + meta: { + action: 'updateBounces', + hasAlarmed: true, + formId: formId.toHexString(), + submissionId: submissionId2.toHexString(), + bounceInfo: parsedNotification2.bounce, + }, + }) expect(omit(actualBounce, 'expireAt')).toEqual({ formId, hasAlarmed: true, @@ -509,4 +584,23 @@ describe('updateBounces', () => { }) expect(actualBounce.expireAt).toBeInstanceOf(Date) }) + + it('should log email confirmations to short-term logs', async () => { + const formId = new ObjectId() + const submissionId = new ObjectId() + const notification = makeBounceNotification( + formId, + submissionId, + recipientList, + recipientList, + 'Transient', + 'Email confirmation', + ) + await updateBounces(notification) + expect(mockLogger.info).not.toHaveBeenCalled() + expect(mockLogger.warn).not.toHaveBeenCalled() + expect(mockShortTermLogger.info).toHaveBeenCalledWith( + JSON.parse(notification.Message), + ) + }) }) diff --git a/src/app/modules/bounce/bounce.controller.ts b/src/app/modules/bounce/bounce.controller.ts index f983462f6b..b8ba7fad70 100644 --- a/src/app/modules/bounce/bounce.controller.ts +++ b/src/app/modules/bounce/bounce.controller.ts @@ -1,4 +1,5 @@ -import { Request, Response } from 'express' +import { RequestHandler } from 'express' +import { ParamsDictionary } from 'express-serve-static-core' import { StatusCodes } from 'http-status-codes' import { createLoggerWithLabel } from '../../../config/logger' @@ -13,10 +14,11 @@ const logger = createLoggerWithLabel(module) * @param req Express request object * @param res - Express response object */ -const handleSns = async ( - req: Request<{}, {}, ISnsNotification>, - res: Response, -) => { +export const handleSns: RequestHandler< + ParamsDictionary, + never, + ISnsNotification +> = async (req, res) => { // Since this function is for a public endpoint, catch all possible errors // so we never fail on malformed input. The response code is meaningless since // it is meant to go back to AWS. @@ -38,5 +40,3 @@ const handleSns = async ( return res.sendStatus(StatusCodes.BAD_REQUEST) } } - -export default handleSns diff --git a/src/app/models/bounce.server.model.ts b/src/app/modules/bounce/bounce.model.ts similarity index 75% rename from src/app/models/bounce.server.model.ts rename to src/app/modules/bounce/bounce.model.ts index 05effe1264..affb7c9f45 100644 --- a/src/app/models/bounce.server.model.ts +++ b/src/app/modules/bounce/bounce.model.ts @@ -2,18 +2,22 @@ import { get } from 'lodash' import { Model, Mongoose, Schema } from 'mongoose' import validator from 'validator' -import { bounceLifeSpan } from '../../config/config' +import { bounceLifeSpan } from '../../../config/config' import { IBounceNotification, IBounceSchema, IEmailNotification, - isBounceNotification, - isDeliveryNotification, ISingleBounce, -} from '../../types' -import { EMAIL_HEADERS, EMAIL_TYPES } from '../constants/mail' +} from '../../../types' +import { EMAIL_HEADERS, EmailType } from '../../constants/mail' +import { FORM_SCHEMA_ID } from '../../models/form.server.model' -import { FORM_SCHEMA_ID } from './form.server.model' +import { + extractHeader, + hasEmailBounced, + isBounceNotification, + isDeliveryNotification, +} from './bounce.util' export const BOUNCE_SCHEMA_ID = 'Bounce' @@ -58,26 +62,6 @@ const BounceSchema = new Schema({ }) BounceSchema.index({ expireAt: 1 }, { expireAfterSeconds: 0 }) -// Helper function for methods. -// Extracts custom headers which we send with all emails, such as form ID, submission ID -// and email type (admin response, email confirmation OTP etc). -const extractHeader = (body: IEmailNotification, header: string): string => { - return get(body, 'mail.headers').find( - (mailHeader) => mailHeader.name.toLowerCase() === header.toLowerCase(), - )?.value -} - -// Helper function for methods. -// Whether a bounce notification says a given email has bounced -const hasEmailBounced = ( - bounceInfo: IBounceNotification, - email: string, -): boolean => { - return get(bounceInfo, 'bounce.bouncedRecipients').some( - (emailInfo) => emailInfo.emailAddress === email, - ) -} - // Create a new Bounce document from an SNS notification. // More info on format of SNS notifications: // https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html @@ -88,7 +72,7 @@ BounceSchema.statics.fromSnsNotification = function ( const emailType = extractHeader(snsInfo, EMAIL_HEADERS.emailType) const formId = extractHeader(snsInfo, EMAIL_HEADERS.formId) // We only care about admin emails - if (emailType !== EMAIL_TYPES.adminResponse || !formId) { + if (emailType !== EmailType.AdminResponse || !formId) { return null } const isBounce = isBounceNotification(snsInfo) @@ -117,7 +101,6 @@ BounceSchema.methods.merge = function ( latestBounces: IBounceSchema, snsInfo: IEmailNotification, ): void { - const isDelivery = isDeliveryNotification(snsInfo) this.bounces.forEach((oldBounce) => { // If we were previously notified that a given email has bounced, // we want to retain that information @@ -131,8 +114,8 @@ BounceSchema.methods.merge = function ( // a false in latestBounces doesn't guarantee that the email was // delivered, only that the email has not bounced yet. const hasSubsequentlySucceeded = - isDelivery && - get(snsInfo, 'delivery.recipients').includes(oldBounce.email) + isDeliveryNotification(snsInfo) && + snsInfo.delivery.recipients.includes(oldBounce.email) if (matchedLatestBounce) { // Set the latest bounce status based on the latest notification matchedLatestBounce.hasBounced = !hasSubsequentlySucceeded @@ -142,14 +125,17 @@ BounceSchema.methods.merge = function ( this.bounces = latestBounces.bounces } -const getBounceModel = (db: Mongoose) => { +BounceSchema.methods.isCriticalBounce = function ( + this: IBounceSchema, +): boolean { + return this.bounces.every((emailInfo) => emailInfo.hasBounced) +} + +const getBounceModel = (db: Mongoose): IBounceModel => { try { return db.model(BOUNCE_SCHEMA_ID) as IBounceModel } catch { - return db.model( - BOUNCE_SCHEMA_ID, - BounceSchema, - ) as IBounceModel + return db.model(BOUNCE_SCHEMA_ID, BounceSchema) } } diff --git a/src/app/modules/bounce/bounce.routes.ts b/src/app/modules/bounce/bounce.routes.ts index ca74a29eef..96bf3d4f57 100644 --- a/src/app/modules/bounce/bounce.routes.ts +++ b/src/app/modules/bounce/bounce.routes.ts @@ -1,6 +1,6 @@ import { Router } from 'express' -import handleSns from './bounce.controller' +import { handleSns } from './bounce.controller' export const BounceRouter = Router() diff --git a/src/app/modules/bounce/bounce.service.ts b/src/app/modules/bounce/bounce.service.ts index 082b3238e7..288884b504 100644 --- a/src/app/modules/bounce/bounce.service.ts +++ b/src/app/modules/bounce/bounce.service.ts @@ -3,16 +3,23 @@ import crypto from 'crypto' import { isEmpty } from 'lodash' import mongoose from 'mongoose' -import { createCloudWatchLogger } from '../../../config/logger' +import { + createCloudWatchLogger, + createLoggerWithLabel, +} from '../../../config/logger' import { IBounceNotification, IBounceSchema, IEmailNotification, ISnsNotification, } from '../../../types' -import getBounceModel from '../../models/bounce.server.model' +import { EMAIL_HEADERS, EmailType } from '../../constants/mail' + +import getBounceModel from './bounce.model' +import { extractHeader, isBounceNotification } from './bounce.util' -const logger = createCloudWatchLogger('email') +const logger = createLoggerWithLabel(module) +const shortTermLogger = createCloudWatchLogger('email') const Bounce = getBounceModel(mongoose) // Note that these need to be ordered in order to generate @@ -101,24 +108,48 @@ export const isValidSnsRequest = async ( // Writes a log message if all recipients have bounced const logCriticalBounce = ( bounceDoc: IBounceSchema, - formId: string, - notification: IEmailNotification, + submissionId: string, + bounceInfo: IBounceNotification['bounce'] | undefined, ): void => { - if ( - !bounceDoc.hasAlarmed && - bounceDoc.bounces.every((emailInfo) => emailInfo.hasBounced) - ) { + if (bounceDoc.isCriticalBounce()) { logger.warn({ - type: 'CRITICAL BOUNCE', - formId, - recipients: bounceDoc.bounces.map((emailInfo) => emailInfo.email), - // We know for sure that critical bounces can only happen because of bounce - // notifications, so this casting is okay - bounceInfo: (notification as IBounceNotification).bounce, + message: 'Critical bounce', + meta: { + action: 'updateBounces', + hasAlarmed: bounceDoc.hasAlarmed, + formId: String(bounceDoc.formId), + submissionId, + recipients: bounceDoc.bounces.map((emailInfo) => emailInfo.email), + // We know for sure that critical bounces can only happen because of bounce + // notifications, so we don't expect this to be undefined + bounceInfo: bounceInfo, + }, }) - // We don't want a flood of logs and alarms, so we use this to limit the rate of - // critical bounce logs for each form ID + // TODO (private #31): autoemail and set hasAlarmed to true. Currently + // hasAlarmed is a dangling key. bounceDoc.hasAlarmed = true + // TODO (private #31): convert bounceType to enum. + } +} + +/** + * Logs the raw notification to the relevant log group. + * @param notification The parsed SNS notification + */ +const logEmailNotification = (notification: IEmailNotification): void => { + if ( + extractHeader(notification, EMAIL_HEADERS.emailType) === + EmailType.EmailConfirmation + ) { + shortTermLogger.info(notification) + } else { + logger.info({ + message: 'Email notification', + meta: { + action: 'updateBounces', + ...notification, + }, + }) } } @@ -129,18 +160,21 @@ const logCriticalBounce = ( export const updateBounces = async (body: ISnsNotification): Promise => { const notification: IEmailNotification = JSON.parse(body.Message) // This is the crucial log statement which allows us to debug bounce-related - // issues, as it logs all the details about deliveries and bounces - logger.info(notification) + // issues, as it logs all the details about deliveries and bounces. Email + // confirmation info goes to the short-term log group so we do not store + // form fillers' information for too long, and everything else goes into the + // main log group. + logEmailNotification(notification) const latestBounces = Bounce.fromSnsNotification(notification) if (!latestBounces) return const formId = latestBounces.formId + const submissionId = extractHeader(notification, EMAIL_HEADERS.submissionId) + const bounceInfo = isBounceNotification(notification) && notification.bounce const oldBounces = await Bounce.findOne({ formId }) if (oldBounces) { oldBounces.merge(latestBounces, notification) - logCriticalBounce(oldBounces, formId, notification) - await oldBounces.save() - } else { - logCriticalBounce(latestBounces, formId, notification) - await latestBounces.save() } + const bounce = oldBounces ?? latestBounces + logCriticalBounce(bounce, submissionId, bounceInfo) + await bounce.save() } diff --git a/src/app/modules/bounce/bounce.util.ts b/src/app/modules/bounce/bounce.util.ts new file mode 100644 index 0000000000..36e2eb3cf7 --- /dev/null +++ b/src/app/modules/bounce/bounce.util.ts @@ -0,0 +1,43 @@ +import { + IBounceNotification, + IDeliveryNotification, + IEmailNotification, +} from 'src/types' +/** + * Extracts custom headers which we send with all emails, such as form ID, submission ID + * and email type (admin response, email confirmation OTP etc). + * @param body Body of SNS notification + * @param header Key of header to extract + */ +export const extractHeader = ( + body: IEmailNotification, + header: string, +): string => { + return body.mail.headers.find( + (mailHeader) => mailHeader.name.toLowerCase() === header.toLowerCase(), + )?.value +} + +/** + * Whether a bounce notification says a given email has bounced. + * @param bounceInfo Bounce notification from SNS + * @param email Email address to check + */ +export const hasEmailBounced = ( + bounceInfo: IBounceNotification, + email: string, +): boolean => { + return bounceInfo.bounce.bouncedRecipients.some( + (emailInfo) => emailInfo.emailAddress === email, + ) +} + +// If an email notification is for bounces +export const isBounceNotification = ( + body: IEmailNotification, +): body is IBounceNotification => body.notificationType === 'Bounce' + +// If an email notification is for successful delivery +export const isDeliveryNotification = ( + body: IEmailNotification, +): body is IDeliveryNotification => body.notificationType === 'Delivery' diff --git a/src/app/modules/core/core.errors.ts b/src/app/modules/core/core.errors.ts index 9f43d02939..5c66fdfacb 100644 --- a/src/app/modules/core/core.errors.ts +++ b/src/app/modules/core/core.errors.ts @@ -2,7 +2,7 @@ * A custom base error class that encapsulates the name, message, status code, * and logging meta string (if any) for the error. */ -export class ApplicationError extends Error { +export abstract class ApplicationError extends Error { /** * Http status code for the error to be returned in the response. */ diff --git a/src/app/modules/core/core.types.ts b/src/app/modules/core/core.types.ts new file mode 100644 index 0000000000..9c1011c90c --- /dev/null +++ b/src/app/modules/core/core.types.ts @@ -0,0 +1,3 @@ +import { Response } from 'express' + +export type ResponseWithLocals = Omit & { locals: T } diff --git a/src/app/modules/user/user.controller.ts b/src/app/modules/user/user.controller.ts index 7953a6bf38..8a941ad0c3 100644 --- a/src/app/modules/user/user.controller.ts +++ b/src/app/modules/user/user.controller.ts @@ -1,8 +1,10 @@ import to from 'await-to-js' import { RequestHandler } from 'express' +import { ParamsDictionary } from 'express-serve-static-core' import { StatusCodes } from 'http-status-codes' import { createLoggerWithLabel } from '../../../config/logger' +import { IPopulatedUser } from '../../../types' import SmsFactory from '../../factories/sms.factory' import { ApplicationError } from '../core/core.errors' @@ -22,8 +24,8 @@ const logger = createLoggerWithLabel(module) * @returns 400 on OTP creation or SMS send failure */ export const handleContactSendOtp: RequestHandler< - {}, - {}, + ParamsDictionary, + string, { contact: string; userId: string } > = async (req, res) => { // Joi validation ensures existence. @@ -56,8 +58,8 @@ export const handleContactSendOtp: RequestHandler< * @returns 500 when OTP is malformed or for unknown errors */ export const handleContactVerifyOtp: RequestHandler< - {}, - {}, + ParamsDictionary, + string | IPopulatedUser, { userId: string otp: string @@ -77,7 +79,14 @@ export const handleContactVerifyOtp: RequestHandler< try { await verifyContactOtp(otp, contact, userId) } catch (err) { - logger.warn(err.meta ?? err) + logger.warn({ + message: 'Error occurred whilst verifying contact OTP', + meta: { + action: 'handleContactVerifyOtp', + userId, + }, + error: err, + }) if (err instanceof ApplicationError) { return res.status(err.status).send(err.message) } else { @@ -91,7 +100,14 @@ export const handleContactVerifyOtp: RequestHandler< return res.status(StatusCodes.OK).send(updatedUser) } catch (updateErr) { // Handle update error. - logger.warn(updateErr) + logger.warn({ + message: 'Error occurred whilst updating user contact', + meta: { + action: 'handleContactVerifyOtp', + userId, + }, + error: updateErr, + }) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(updateErr.message) } } @@ -110,6 +126,7 @@ export const handleFetchUser: RequestHandler = async (req, res) => { message: `Unable to retrieve user ${sessionUserId}`, meta: { action: 'handleFetchUser', + userId: sessionUserId, }, error: dbErr, }) diff --git a/src/app/modules/user/user.errors.ts b/src/app/modules/user/user.errors.ts index 6df7c6d8a8..a57239cdeb 100644 --- a/src/app/modules/user/user.errors.ts +++ b/src/app/modules/user/user.errors.ts @@ -10,7 +10,7 @@ export class InvalidOtpError extends ApplicationError { export class MalformedOtpError extends ApplicationError { constructor( - message: string = 'Malformed OTP. Please try again later. If the problem persists, contact us.', + message = 'Malformed OTP. Please try again later. If the problem persists, contact us.', meta?: string, ) { super(message, StatusCodes.INTERNAL_SERVER_ERROR, meta) diff --git a/src/app/modules/user/user.service.ts b/src/app/modules/user/user.service.ts index 955a935017..7bad460194 100644 --- a/src/app/modules/user/user.service.ts +++ b/src/app/modules/user/user.service.ts @@ -1,18 +1,20 @@ import to from 'await-to-js' import bcrypt from 'bcrypt' import mongoose from 'mongoose' +import validator from 'validator' import getAdminVerificationModel from '../../../app/models/admin_verification.server.model' import { AGENCY_SCHEMA_ID } from '../../../app/models/agency.server.model' import getUserModel from '../../../app/models/user.server.model' import { generateOtp } from '../../../app/utils/otp' import config from '../../../config/config' -import { IPopulatedUser, IUserSchema } from '../../../types' +import { IAgency, IPopulatedUser, IUserSchema } from '../../../types' +import { InvalidDomainError } from '../auth/auth.errors' import { InvalidOtpError, MalformedOtpError } from './user.errors' -const AdminVerification = getAdminVerificationModel(mongoose) -const User = getUserModel(mongoose) +const AdminVerificationModel = getAdminVerificationModel(mongoose) +const UserModel = getUserModel(mongoose) const DEFAULT_SALT_ROUNDS = 10 export const MAX_OTP_ATTEMPTS = 10 @@ -29,7 +31,7 @@ export const createContactOtp = async ( contact: string, ): Promise => { // Verify existence of userId - const admin = await User.findById(userId) + const admin = await UserModel.findById(userId) if (!admin) { throw new Error('User id is invalid') } @@ -38,7 +40,7 @@ export const createContactOtp = async ( const hashedOtp = await bcrypt.hash(otp, DEFAULT_SALT_ROUNDS) const hashedContact = await bcrypt.hash(contact, DEFAULT_SALT_ROUNDS) - await AdminVerification.upsertOtp({ + await AdminVerificationModel.upsertOtp({ admin: userId, expireAt: new Date(Date.now() + config.otpLifeSpan), hashedContact, @@ -65,7 +67,7 @@ export const verifyContactOtp = async ( contactToVerify: string, userId: IUserSchema['_id'], ) => { - const updatedDocument = await AdminVerification.incrementAttemptsByAdminId( + const updatedDocument = await AdminVerificationModel.incrementAttemptsByAdminId( userId, ) @@ -114,7 +116,7 @@ export const verifyContactOtp = async ( } // Hashed OTP matches, remove from collection. - await AdminVerification.findOneAndRemove({ admin: userId }) + await AdminVerificationModel.findOneAndRemove({ admin: userId }) // Finally return true (as success). return true } @@ -133,7 +135,7 @@ export const updateUserContact = async ( ): Promise => { // Retrieve user from database. // Update user's contact details. - const admin = await User.findById(userId).populate({ + const admin = await UserModel.findById(userId).populate({ path: 'agency', model: AGENCY_SCHEMA_ID, }) @@ -148,8 +150,34 @@ export const updateUserContact = async ( export const getPopulatedUserById = async ( userId: IUserSchema['_id'], ): Promise => { - return User.findById(userId).populate({ + return UserModel.findById(userId).populate({ path: 'agency', model: AGENCY_SCHEMA_ID, }) } + +/** + * Retrieves the user with the given email. If the user does not yet exist, a + * new user document is created and returned. + * @param email the email of the user to retrieve + * @param agency the agency document to associate with the user + * @returns the upserted user document + * @throws {InvalidDomainError} on invalid email + * @throws {Error} on upsert failure + */ +export const retrieveUser = async (email: string, agency: IAgency) => { + if (!validator.isEmail(email)) { + throw new InvalidDomainError() + } + + const admin = await UserModel.upsertUser({ + email, + agency: agency._id, + }) + + if (!admin) { + throw new Error('Failed to upsert user') + } + + return admin +} diff --git a/src/app/modules/verification/verification.controller.ts b/src/app/modules/verification/verification.controller.ts index afa51beedd..0cf45ef0ab 100644 --- a/src/app/modules/verification/verification.controller.ts +++ b/src/app/modules/verification/verification.controller.ts @@ -4,7 +4,8 @@ import { StatusCodes } from 'http-status-codes' import { createLoggerWithLabel } from '../../../config/logger' import { VfnErrors } from '../../../shared/util/verification' -import * as verificationService from './verification.service' +import * as VerificationService from './verification.service' +import { ITransaction } from './verification.types' const logger = createLoggerWithLabel(module) /** @@ -16,13 +17,13 @@ const logger = createLoggerWithLabel(module) * @returns 200 - transaction was not created as no fields were verifiable for the form */ export const createTransaction: RequestHandler< - {}, - {}, + Record, + ITransaction, { formId: string } > = async (req, res) => { try { const { formId } = req.body - const transaction = await verificationService.createTransaction(formId) + const transaction = await VerificationService.createTransaction(formId) return transaction ? res.status(StatusCodes.CREATED).json(transaction) : res.sendStatus(StatusCodes.OK) @@ -47,7 +48,7 @@ export const getTransactionMetadata: RequestHandler<{ }> = async (req, res) => { try { const { transactionId } = req.params - const transaction = await verificationService.getTransactionMetadata( + const transaction = await VerificationService.getTransactionMetadata( transactionId, ) return res.status(StatusCodes.OK).json(transaction) @@ -70,14 +71,14 @@ export const getTransactionMetadata: RequestHandler<{ */ export const resetFieldInTransaction: RequestHandler< { transactionId: string }, - {}, + string, { fieldId: string } > = async (req, res) => { try { const { transactionId } = req.params const { fieldId } = req.body - const transaction = await verificationService.getTransaction(transactionId) - await verificationService.resetFieldInTransaction(transaction, fieldId) + const transaction = await VerificationService.getTransaction(transactionId) + await VerificationService.resetFieldInTransaction(transaction, fieldId) return res.sendStatus(StatusCodes.OK) } catch (error) { logger.error({ @@ -98,14 +99,14 @@ export const resetFieldInTransaction: RequestHandler< */ export const getNewOtp: RequestHandler< { transactionId: string }, - {}, + string, { answer: string; fieldId: string } > = async (req, res) => { try { const { transactionId } = req.params const { answer, fieldId } = req.body - const transaction = await verificationService.getTransaction(transactionId) - await verificationService.getNewOtp(transaction, fieldId, answer) + const transaction = await VerificationService.getTransaction(transactionId) + await VerificationService.getNewOtp(transaction, fieldId, answer) return res.sendStatus(StatusCodes.CREATED) } catch (error) { logger.error({ @@ -127,14 +128,14 @@ export const getNewOtp: RequestHandler< */ export const verifyOtp: RequestHandler< { transactionId: string }, - {}, + string, { otp: string; fieldId: string } > = async (req, res) => { try { const { transactionId } = req.params const { fieldId, otp } = req.body - const transaction = await verificationService.getTransaction(transactionId) - const data = await verificationService.verifyOtp(transaction, fieldId, otp) + const transaction = await VerificationService.getTransaction(transactionId) + const data = await VerificationService.verifyOtp(transaction, fieldId, otp) return res.status(StatusCodes.OK).json(data) } catch (error) { logger.error({ diff --git a/src/app/modules/verification/verification.service.ts b/src/app/modules/verification/verification.service.ts index c298895592..0e3d456ffc 100644 --- a/src/app/modules/verification/verification.service.ts +++ b/src/app/modules/verification/verification.service.ts @@ -3,7 +3,7 @@ import _ from 'lodash' import mongoose from 'mongoose' import formsgSdk from '../../../config/formsg-sdk' -import * as vfnUtil from '../../../shared/util/verification' +import * as VfnUtils from '../../../shared/util/verification' import { IEmailFieldSchema, IFieldSchema, @@ -18,8 +18,11 @@ import getVerificationModel from '../../models/verification.server.model' import MailService from '../../services/mail.service' import { generateOtp } from '../../utils/otp' +import { ITransaction } from './verification.types' + const Form = getFormModel(mongoose) const Verification = getVerificationModel(mongoose) + const { HASH_EXPIRE_AFTER_SECONDS, NUM_OTP_RETRIES, @@ -27,12 +30,7 @@ const { VERIFIED_FIELDTYPES, VfnErrors, WAIT_FOR_OTP_SECONDS, -} = vfnUtil - -interface ITransaction { - transactionId: IVerificationSchema['_id'] - expireAt: IVerificationSchema['expireAt'] -} +} = VfnUtils /** * Creates a transaction for a form that has verifiable fields @@ -282,7 +280,7 @@ const isTransactionExpired = (expireAt: Date): boolean => { */ const isHashedOtpExpired = (hashCreatedAt: Date): boolean => { const currentDate = new Date() - const expireAt = vfnUtil.getExpiryDate( + const expireAt = VfnUtils.getExpiryDate( HASH_EXPIRE_AFTER_SECONDS, hashCreatedAt, ) @@ -298,9 +296,10 @@ const waitToResendOtpSeconds = (hashCreatedAt: Date): number => { // Hash has not been created return 0 } - const expireAtMs = vfnUtil - .getExpiryDate(WAIT_FOR_OTP_SECONDS, hashCreatedAt) - .getTime() + const expireAtMs = VfnUtils.getExpiryDate( + WAIT_FOR_OTP_SECONDS, + hashCreatedAt, + ).getTime() const currentMs = Date.now() return Math.ceil((expireAtMs - currentMs) / 1000) } @@ -324,7 +323,7 @@ const getFieldFromTransaction = ( * @param name */ const throwError = (message: string, name?: string): never => { - let error = new Error(message) + const error = new Error(message) error.name = name || message throw error } diff --git a/src/app/modules/verification/verification.types.ts b/src/app/modules/verification/verification.types.ts new file mode 100644 index 0000000000..d7bd6b14f5 --- /dev/null +++ b/src/app/modules/verification/verification.types.ts @@ -0,0 +1,6 @@ +import { IVerificationSchema } from '../../../types' + +export interface ITransaction { + transactionId: IVerificationSchema['_id'] + expireAt: IVerificationSchema['expireAt'] +} diff --git a/src/app/modules/webhook/webhook.controller.ts b/src/app/modules/webhook/webhook.controller.ts new file mode 100644 index 0000000000..d8b199c65f --- /dev/null +++ b/src/app/modules/webhook/webhook.controller.ts @@ -0,0 +1,33 @@ +import { NextFunction, Request, Response } from 'express' + +import { pushData } from './webhook.service' +import { WebhookRequestLocals } from './webhook.types' + +/** + * POST submission to a specified URL. Only works for encrypted submissions. + * The webhook is fired on a best-effort basis, so the next middleware + * is always called. + * @param {Express.Request} req Express request object + * @param {Object} req.form The form object containing the webhook URL + * @param {Object} req.submission The submission saved to the database + * @param {Express.Response} res Express response object + * @param {function} next Next middleware + */ +export const post = ( + req: Request & WebhookRequestLocals, + res: Response, + next: NextFunction, +) => { + // TODO: Once we move away from the middleware pattern, there should not be a webhook controller + // There should only be a webhook service, which is called within the submission controller + // This will also remove the need for retrieval of form/submission from req. + const { form, submission } = req + const webhookUrl = form.webhook.url + const submissionWebhookView = submission.getWebhookView() + if (webhookUrl) { + // Note that we push data to webhook endpoints on a best effort basis + // As such, we should not await on these post requests + pushData(webhookUrl, submissionWebhookView) + } + return next() +} diff --git a/src/app/modules/webhooks/webhook.errors.ts b/src/app/modules/webhook/webhook.errors.ts similarity index 100% rename from src/app/modules/webhooks/webhook.errors.ts rename to src/app/modules/webhook/webhook.errors.ts diff --git a/src/app/modules/webhook/webhook.service.ts b/src/app/modules/webhook/webhook.service.ts new file mode 100644 index 0000000000..636a282566 --- /dev/null +++ b/src/app/modules/webhook/webhook.service.ts @@ -0,0 +1,317 @@ +import axios, { AxiosError, AxiosResponse } from 'axios' +import { get } from 'lodash' +import mongoose from 'mongoose' + +import formsgSdk from '../../../config/formsg-sdk' +import { createLoggerWithLabel } from '../../../config/logger' +// Prevents JSON.stringify error for circular JSONs and BigInts +import { stringifySafe } from '../../../shared/util/stringify-safe' +import { + IFormSchema, + ISubmissionSchema, + IWebhookResponse, + WebhookView, +} from '../../../types' +import { getEncryptSubmissionModel } from '../../models/submission.server.model' + +import { WebhookValidationError } from './webhook.errors' +import { WebhookParams } from './webhook.types' +import { validateWebhookUrl } from './webhook.utils' + +const logger = createLoggerWithLabel(module) +const EncryptSubmission = getEncryptSubmissionModel(mongoose) + +/** + * Logs webhook failure in console and database. + * @param {error} error Error object returned by axios + * @param {Object} webhookParams Parameters which fully specify webhook + * @param {string} webhookParams.webhookUrl URL to POST to + * @param {Object} webhookParams.submissionWebhookView POST body + * @param {string} webhookParams.submissionId + * @param {string} webhookParams.formId + * @param {string} webhookParams.now Epoch for POST header + * @param {string} webhookParams.signature Signature generated by FormSG SDK + */ +const handleWebhookFailure = async ( + error: Error | AxiosError, + webhookParams: WebhookParams, +): Promise => { + logWebhookFailure(error, webhookParams) + return updateSubmissionsDb( + webhookParams.formId, + webhookParams.submissionId, + getFailureDbUpdate(error, webhookParams), + ) +} + +/** + * Logs webhook success in console and database. + * @param {response} response Response object returned by axios + * @param {Object} webhookParams Parameters which fully specify webhook + * @param {string} webhookParams.webhookUrl URL to POST to + * @param {Object} webhookParams.submissionWebhookView POST body + * @param {string} webhookParams.submissionId + * @param {string} webhookParams.formId + * @param {string} webhookParams.now Epoch for POST header + * @param {string} webhookParams.signature Signature generated by FormSG SDK + */ +const handleWebhookSuccess = async ( + response: AxiosResponse, + webhookParams: WebhookParams, +): Promise => { + logWebhookSuccess(response, webhookParams) + return updateSubmissionsDb( + webhookParams.formId, + webhookParams.submissionId, + getSuccessDbUpdate(response, webhookParams), + ) +} + +/** + * Sends webhook POST. + * Note that the arguments are the same as those in webhookParams + * for handleWebhookSuccess and handleWebhookFailure, just destructured. + * @param {Object} webhookParams Parameters which fully specify webhook + * @param {string} webhookParams.webhookUrl URL to POST to + * @param {Object} webhookParams.submissionWebhookView POST body + * @param {string} webhookParams.submissionId + * @param {string} webhookParams.formId + * @param {string} webhookParams.now Epoch for POST header + * @param {string} webhookParams.signature Signature generated by FormSG SDK + */ +const postWebhook = ({ + webhookUrl, + submissionWebhookView, + submissionId, + formId, + now, + signature, +}: WebhookParams): Promise => { + return axios.post(webhookUrl, submissionWebhookView, { + headers: { + 'X-FormSG-Signature': formsgSdk.webhooks.constructHeader({ + epoch: now, + submissionId, + formId, + signature, + }), + }, + maxRedirects: 0, + }) +} + +/** + * Logging for webhook success + * @param {response} response Response object returned by axios + * @param {Object} webhookParams Parameters which fully specify webhook + * @param {string} webhookParams.webhookUrl URL to POST to + * @param {Object} webhookParams.submissionWebhookView POST body + * @param {string} webhookParams.submissionId + * @param {string} webhookParams.formId + * @param {string} webhookParams.now Epoch for POST header + * @param {string} webhookParams.signature Signature generated by FormSG SDK + */ +const logWebhookSuccess = ( + response: AxiosResponse, + { webhookUrl, submissionId, formId, now, signature }: WebhookParams, +): void => { + const status = get(response, 'status') + + logger.info({ + message: 'Webhook POST succeeded', + meta: { + action: 'logWebhookSuccess', + status, + submissionId, + formId, + now, + webhookUrl, + signature, + }, + }) +} + +/** + * Logging for webhook failure + * @param {error} error Error object returned by axios + * @param {Object} webhookParams Parameters which fully specify webhook + * @param {string} webhookParams.webhookUrl URL to POST to + * @param {Object} webhookParams.submissionWebhookView POST body + * @param {string} webhookParams.submissionId + * @param {string} webhookParams.formId + * @param {string} webhookParams.now Epoch for POST header + * @param {string} webhookParams.signature Signature generated by FormSG SDK + */ +const logWebhookFailure = ( + error: Error | AxiosError, + { webhookUrl, submissionId, formId, now, signature }: Partial, +): void => { + const logMeta = { + action: 'logWebhookFailure', + submissionId, + formId, + now, + webhookUrl, + signature, + } + + if (error instanceof WebhookValidationError) { + logger.error({ + message: 'Webhook not attempted', + meta: logMeta, + error, + }) + } else { + logger.error({ + message: 'Webhook POST failed', + meta: { + ...logMeta, + status: get(error, 'response.status'), + }, + error, + }) + } +} + +/** + * Updates the submission in the database with the webhook response + * @param {ObjectId} formId Form that submission to update belongs to + * @param {ObjectId} submissionId Submission to update with webhook response + * @param {Object} updateObj Webhook response to update submission document with + * @param {number} updateObj.status status code received from webhook endpoint + * @param {string} updateObj.statusText status text received from webhook endpoint + * @param {string} updateObj.headers stringified headers received from webhook endpoint + * @param {string} updateObj.data stringified data received from webhook endpoint + */ +const updateSubmissionsDb = async ( + formId: IFormSchema['_id'], + submissionId: ISubmissionSchema['_id'], + updateObj: IWebhookResponse, +): Promise => { + try { + const { nModified } = await EncryptSubmission.updateOne( + { _id: submissionId }, + { $push: { webhookResponses: updateObj } }, + ) + if (nModified !== 1) { + // Pass on to catch block + throw new Error('Submission not found in database.') + } + } catch (error) { + logger.error({ + message: 'Database update for webhook status failed', + meta: { + action: 'updateSubmissionsDb', + formId, + submissionId, + updateObj: stringifySafe(updateObj), + }, + error, + }) + } +} + +/** + * Formats webhook success info into an object to update Submissions collection + * @param {response} response Response object returned by axios + * @param {Object} webhookParams Parameters which fully specify webhook + * @param {string} webhookParams.webhookUrl URL to POST to + * @param {string} webhookParams.signature Signature generated by FormSG SDK + */ +const getSuccessDbUpdate = ( + response: AxiosResponse, + { webhookUrl, signature }: Pick, +): IWebhookResponse => { + return { webhookUrl, signature, ...getFormattedResponse(response) } +} + +/** + * Formats webhook failure info into an object to update Submissions collection + * @param {error} error Error object returned by axios + * @param {Object} webhookParams Parameters which fully specify webhook + * @param {string} webhookParams.webhookUrl URL to POST to + * @param {string} webhookParams.signature Signature generated by FormSG SDK + */ +const getFailureDbUpdate = ( + error: Error | AxiosError, + { webhookUrl, signature }: Pick, +): IWebhookResponse => { + const errorMessage = get(error, 'message') + const update: IWebhookResponse = { + webhookUrl, + signature, + errorMessage, + } + if (!(error instanceof WebhookValidationError)) { + const { response } = getFormattedResponse(get(error, 'response')) + update.response = response + } + return update +} + +/** + * Formats a response object for update in the Submissions collection + * @param {response} response Response object returned by axios + */ +const getFormattedResponse = ( + response: AxiosResponse, +): Pick => { + return { + response: { + status: get(response, 'status'), + statusText: get(response, 'statusText'), + headers: stringifySafe(get(response, 'headers')), + data: stringifySafe(get(response, 'data')), + }, + } +} + +/** + * Validates webhook url, posts data to it and updates submission document with response + * @param {string} webhookUrl Endpoint to push data to + * @param {Object} submissionWebhookView Metadata containing form information and crucial submission data + */ +export const pushData = async ( + webhookUrl: WebhookParams['webhookUrl'], + submissionWebhookView: WebhookView, +): Promise => { + const now = Date.now() + // Log and return, this should not happen. + if (!submissionWebhookView) { + logWebhookFailure( + new WebhookValidationError('submissionWebhookView was null'), + { + webhookUrl, + submissionWebhookView, + now, + }, + ) + return + } + + const { submissionId, formId } = submissionWebhookView.data + + const signature = formsgSdk.webhooks.generateSignature({ + uri: webhookUrl, + submissionId, + formId, + epoch: now, + }) as string + + const webhookParams = { + webhookUrl, + submissionWebhookView, + submissionId, + formId, + now, + signature, + } + + try { + await validateWebhookUrl(webhookParams.webhookUrl) + const response = await postWebhook(webhookParams) + return handleWebhookSuccess(response, webhookParams) + } catch (error) { + return handleWebhookFailure(error, webhookParams) + } +} diff --git a/src/app/modules/webhook/webhook.types.ts b/src/app/modules/webhook/webhook.types.ts new file mode 100644 index 0000000000..4e3a1bb2c6 --- /dev/null +++ b/src/app/modules/webhook/webhook.types.ts @@ -0,0 +1,21 @@ +import { + IEncryptedSubmission, + IForm, + ISubmissionSchema, + WebhookView, +} from '../../../types' +import { IFormSchema } from '../../../types/form' + +export interface WebhookParams { + webhookUrl: string + submissionWebhookView: WebhookView + submissionId: ISubmissionSchema['_id'] + formId: IFormSchema['_id'] + now: number + signature: string +} + +export interface WebhookRequestLocals { + form: IForm + submission: IEncryptedSubmission +} diff --git a/src/shared/util/webhook-validation.ts b/src/app/modules/webhook/webhook.utils.ts similarity index 91% rename from src/shared/util/webhook-validation.ts rename to src/app/modules/webhook/webhook.utils.ts index 9cf34d2819..a3c24dee2e 100644 --- a/src/shared/util/webhook-validation.ts +++ b/src/app/modules/webhook/webhook.utils.ts @@ -1,9 +1,9 @@ import { promises as dns } from 'dns' import ip from 'ip' -import { WebhookValidationError } from '../../app/modules/webhooks/webhook.errors' +import { isValidHttpsUrl } from '../../../shared/util/url-validation' -import { isValidHttpsUrl } from './url-validation' +import { WebhookValidationError } from './webhook.errors' /** * Checks that a URL is valid for use in webhooks. diff --git a/src/app/routes/authentication.server.routes.js b/src/app/routes/authentication.server.routes.js deleted file mode 100755 index e8884701b0..0000000000 --- a/src/app/routes/authentication.server.routes.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict' - -/** - * Module dependencies. - */ -const { StatusCodes } = require('http-status-codes') -const { celebrate, Joi } = require('celebrate') - -let auth = require('../../app/controllers/authentication.server.controller') -const emailValOpts = { - minDomainSegments: 2, // Number of segments required for the domain - tlds: true, // TLD (top level domain) validation - multiple: false, // Disallow multiple emails -} - -module.exports = function (app) { - /** - * @typedef Email - * @property {string} email.required - the user's email - */ - - /** - * Check if email domain is a valid agency - * @route POST /auth/checkuser - * @group admin - form administration - * @param {Email.model} email.body.required - the user's email - * @produces application/json - * @consumes application/json - * @returns {Boolean} 200 - indicates if user has logged in before - * @returns {string} 400 - a message indicating either a bad email address - */ - app.route('/auth/checkuser').post( - celebrate({ - body: Joi.object().keys({ - email: Joi.string() - .required() - .email(emailValOpts) - .error(() => 'Please enter a valid email'), - }), - }), - auth.validateDomain, - (_, res) => res.sendStatus(StatusCodes.OK), - ) - - /** - * Send a one-time password (OTP) to the specified email address - * as part of login procedure - * @route POST /auth/sendotp - * @group admin - form administration - * @param {Email.model} email.body.required - the user's email - * @produces application/json - * @consumes application/json - * @returns {string} 200 - OTP has been been successfully sent - * @returns {string} 400 - a message indicating either a bad email address, or that - * the agency indicated in the email address has not been onboarded to FormSG - * @returns {string} 500 - FormSG was unable to generate the OTP, or create or send - * the email that delivers the OTP to the user's email address - */ - app.route('/auth/sendotp').post( - celebrate({ - body: Joi.object().keys({ - email: Joi.string() - .required() - .email(emailValOpts) - .error(() => 'Please enter a valid email'), - }), - }), - auth.validateDomain, - auth.createOtp, - auth.sendOtp, - ) - - /** - * @typedef EmailAndOtp - * @property {string} email.required - the user's email - * @property {string} otp.required - the OTP provided by the user - */ - - /** - * Verify the one-time password (OTP) for the specified email address - * as part of login procedure - * @route POST /auth/verifyotp - * @group admin - form administration - * @param {EmailAndOtp.model} email.body.required - the user's email and otp - * @produces application/json - * @consumes application/json - * @returns {string} 200 - user has successfully logged in, with session cookie set - * @returns {string} 400 - the OTP is invalid or has expired, or the email is invalid - * @returns {string} 500 - FormSG was unable to verify the OTP - * @headers {string} 200.set-cookie - contains the session cookie upon login - */ - app.route('/auth/verifyotp').post( - celebrate({ - body: Joi.object().keys({ - email: Joi.string() - .required() - .email(emailValOpts) - .error(() => 'Please enter a valid email'), - otp: Joi.string() - .required() - .regex(/^\d{6}$/) - .error(() => 'Please enter a valid otp'), - }), - }), - auth.validateDomain, - auth.verifyOtp, - auth.signIn, - ) - - /** - * Sign the user out of the session by clearing the relevant session cookie - * @route GET /auth/signout - * @group admin - form administration - * @produces application/json - * @returns {string} 200 - user has signed out - * @returns {string} 400 - the signout failed for one reason or another - */ - app.route('/auth/signout').get(auth.signOut) -} diff --git a/src/app/routes/index.js b/src/app/routes/index.js index 3dacdafc3a..dde5caee35 100644 --- a/src/app/routes/index.js +++ b/src/app/routes/index.js @@ -1,7 +1,6 @@ module.exports = [ require('./admin-console.server.routes.js'), require('./admin-forms.server.routes.js'), - require('./authentication.server.routes.js'), require('./core.server.routes.js'), require('./frontend.server.routes.js'), require('./public-forms.server.routes.js'), diff --git a/src/app/services/mail.service.ts b/src/app/services/mail.service.ts index 2f0afaf0a0..c319649eea 100644 --- a/src/app/services/mail.service.ts +++ b/src/app/services/mail.service.ts @@ -15,7 +15,7 @@ import { IPopulatedForm, ISubmissionSchema, } from '../../types' -import { EMAIL_HEADERS, EMAIL_TYPES } from '../constants/mail' +import { EMAIL_HEADERS, EmailType } from '../constants/mail' import { generateAutoreplyHtml, generateAutoreplyPdf, @@ -257,7 +257,7 @@ export class MailService { headers: { [EMAIL_HEADERS.formId]: String(form._id), [EMAIL_HEADERS.submissionId]: submission.id, - [EMAIL_HEADERS.emailType]: EMAIL_TYPES.emailConfirmation, + [EMAIL_HEADERS.emailType]: EmailType.EmailConfirmation, }, } @@ -291,7 +291,7 @@ export class MailService { otp, }), headers: { - [EMAIL_HEADERS.emailType]: EMAIL_TYPES.verificationOtp, + [EMAIL_HEADERS.emailType]: EmailType.VerificationOtp, }, } // Error gets caught in getNewOtp @@ -324,7 +324,7 @@ export class MailService { otp, }), headers: { - [EMAIL_HEADERS.emailType]: EMAIL_TYPES.loginOtp, + [EMAIL_HEADERS.emailType]: EmailType.LoginOtp, }, } @@ -399,7 +399,7 @@ export class MailService { headers: { [EMAIL_HEADERS.formId]: String(form._id), [EMAIL_HEADERS.submissionId]: refNo, - [EMAIL_HEADERS.emailType]: EMAIL_TYPES.adminResponse, + [EMAIL_HEADERS.emailType]: EmailType.AdminResponse, }, // replyTo options only allow string format. replyTo: replyToEmails?.join(', '), diff --git a/src/app/services/webhooks.service.ts b/src/app/services/webhooks.service.ts deleted file mode 100644 index 53e2f978eb..0000000000 --- a/src/app/services/webhooks.service.ts +++ /dev/null @@ -1,219 +0,0 @@ -import axios, { AxiosError, AxiosResponse } from 'axios' -import { get } from 'lodash' -import mongoose from 'mongoose' - -import formsgSdk from '../../config/formsg-sdk' -import { createLoggerWithLabel } from '../../config/logger' -// Prevents JSON.stringify error for circular JSONs and BigInts -import { stringifySafe } from '../../shared/util/stringify-safe' -import { - IFormSchema, - ISubmissionSchema, - IWebhookResponse, - WebhookParams, -} from '../../types' -import { getEncryptSubmissionModel } from '../models/submission.server.model' -import { WebhookValidationError } from '../modules/webhooks/webhook.errors' - -const logger = createLoggerWithLabel(module) -const EncryptSubmission = getEncryptSubmissionModel(mongoose) - -/** - * Logs webhook failure in console and database. - * @param {error} error Error object returned by axios - * @param {Object} webhookParams Parameters which fully specify webhook - * @param {string} webhookParams.webhookUrl URL to POST to - * @param {Object} webhookParams.submissionWebhookView POST body - * @param {string} webhookParams.submissionId - * @param {string} webhookParams.formId - * @param {string} webhookParams.now Epoch for POST header - * @param {string} webhookParams.signature Signature generated by FormSG SDK - */ -export const handleWebhookFailure = async ( - error: Error | AxiosError, - webhookParams: WebhookParams, -): Promise => { - logWebhookFailure(error, webhookParams) - await updateSubmissionsDb( - webhookParams.formId, - webhookParams.submissionId, - getFailureDbUpdate(error, webhookParams), - ) -} - -/** - * Logs webhook success in console and database. - * @param {response} response Response object returned by axios - * @param {Object} webhookParams Parameters which fully specify webhook - * @param {string} webhookParams.webhookUrl URL to POST to - * @param {Object} webhookParams.submissionWebhookView POST body - * @param {string} webhookParams.submissionId - * @param {string} webhookParams.formId - * @param {string} webhookParams.now Epoch for POST header - * @param {string} webhookParams.signature Signature generated by FormSG SDK - */ -export const handleWebhookSuccess = async ( - response: AxiosResponse, - webhookParams: WebhookParams, -): Promise => { - logWebhookSuccess(response, webhookParams) - await updateSubmissionsDb( - webhookParams.formId, - webhookParams.submissionId, - getSuccessDbUpdate(response, webhookParams), - ) -} - -/** - * Sends webhook POST. - * Note that the arguments are the same as those in webhookParams - * for handleWebhookSuccess and handleWebhookFailure, just destructured. - * @param {string} webhookUrl URL to POST to - * @param {Object} submissionWebhookView POST body - * @param {string} submissionId - * @param {string} formId - * @param {string} now Epoch for POST header - * @param {string} signature Signature generated by FormSG SDK - */ -export const postWebhook = ({ - webhookUrl, - submissionWebhookView, - submissionId, - formId, - now, - signature, -}: WebhookParams): Promise => { - return axios.post(webhookUrl, submissionWebhookView, { - headers: { - 'X-FormSG-Signature': formsgSdk.webhooks.constructHeader({ - epoch: now, - submissionId, - formId, - signature, - }), - }, - maxRedirects: 0, - }) -} - -// Logging for webhook success -const logWebhookSuccess = ( - response: AxiosResponse, - { webhookUrl, submissionId, formId, now, signature }: WebhookParams, -): void => { - const status = get(response, 'status') - - logger.info({ - message: 'Webhook POST succeeded', - meta: { - action: 'logWebhookSuccess', - status, - submissionId, - formId, - now, - webhookUrl, - signature, - }, - }) -} - -// Logging for webhook failure -export const logWebhookFailure = ( - error: Error | AxiosError, - { webhookUrl, submissionId, formId, now, signature }: WebhookParams, -): void => { - let logMeta = { - action: 'logWebhookFailure', - submissionId, - formId, - now, - webhookUrl, - signature, - } - - if (error instanceof WebhookValidationError) { - logger.error({ - message: 'Webhook not attempted', - meta: logMeta, - error, - }) - } else { - logger.error({ - message: 'Webhook POST failed', - meta: { - ...logMeta, - status: get(error, 'response.status'), - }, - error, - }) - } -} - -// Updates the submission in the database with the webhook response -const updateSubmissionsDb = async ( - formId: IFormSchema['_id'], - submissionId: ISubmissionSchema['_id'], - updateObj: IWebhookResponse, -): Promise => { - try { - const { nModified } = await EncryptSubmission.updateOne( - { _id: submissionId }, - { $push: { webhookResponses: updateObj } }, - ) - if (nModified !== 1) { - // Pass on to catch block - throw new Error('Submission not found in database.') - } - } catch (error) { - logger.error({ - message: 'Database update for webhook status failed', - meta: { - action: 'updateSubmissionsDb', - formId, - submissionId, - updateObj: stringifySafe(updateObj), - }, - error, - }) - } -} - -// Formats webhook success info into an object to update Submissions collection -const getSuccessDbUpdate = ( - response: AxiosResponse, - { webhookUrl, signature }: WebhookParams, -): IWebhookResponse => { - return { webhookUrl, signature, ...getFormattedResponse(response) } -} - -// Formats webhook failure info into an object to update Submissions collection -const getFailureDbUpdate = ( - error: Error | AxiosError, - { webhookUrl, signature }: WebhookParams, -): IWebhookResponse => { - const errorMessage = get(error, 'message') - let update: IWebhookResponse = { - webhookUrl, - signature, - errorMessage, - } - if (!(error instanceof WebhookValidationError)) { - const { response } = getFormattedResponse(get(error, 'response')) - update.response = response - } - return update -} - -// Formats a response object for update in the Submissions collection -const getFormattedResponse = ( - response: AxiosResponse, -): Pick => { - return { - response: { - status: get(response, 'status'), - statusText: get(response, 'statusText'), - headers: stringifySafe(get(response, 'headers')), - data: stringifySafe(get(response, 'data')), - }, - } -} diff --git a/src/app/utils/date.ts b/src/app/utils/date.ts index 46410f4211..947a72be84 100644 --- a/src/app/utils/date.ts +++ b/src/app/utils/date.ts @@ -1,6 +1,6 @@ import moment from 'moment-timezone' -export const isMalformedDate = (date?: string): Boolean => { +export const isMalformedDate = (date?: string): boolean => { return Boolean(date) && !moment(date, 'YYYY-MM-DD', true).isValid() } diff --git a/src/app/utils/mail.ts b/src/app/utils/mail.ts index 3f02ce0b4a..87e25f420d 100644 --- a/src/app/utils/mail.ts +++ b/src/app/utils/mail.ts @@ -13,7 +13,7 @@ export const generateLoginOtpHtml = (htmlData: { appName: string appUrl: string ipAddress: string -}) => { +}): Promise => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/otp-email.server.view.html` return ejs.renderFile(pathToTemplate, htmlData) } @@ -26,7 +26,7 @@ export const generateVerificationOtpHtml = ({ otp: string appName: string minutesToExpiry: number -}) => { +}): string => { return dedent`

You are currently submitting a form on ${appName}.

@@ -56,7 +56,7 @@ type SubmissionToAdminHtmlData = { export const generateSubmissionToAdminHtml = async ( htmlData: SubmissionToAdminHtmlData, -) => { +): Promise => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/submit-form-email.server.view.html` return ejs.renderFile(pathToTemplate, htmlData) } @@ -65,13 +65,14 @@ type AutoreplySummaryRenderData = { refNo: ISubmissionSchema['_id'] formTitle: IFormSchema['title'] submissionTime: string + // TODO (#42): Add proper types once the type is determined. formData: any formUrl: string } export const generateAutoreplyPdf = async ( renderData: AutoreplySummaryRenderData, -) => { +): Promise => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/submit-form-summary-pdf.server.view.html` const summaryHtml = await ejs.renderFile(pathToTemplate, renderData) @@ -96,16 +97,18 @@ export const generateAutoreplyPdf = async ( return pdfBuffer } -type AutoreplyHtmlData = { - autoReplyBody: string[] -} & (AutoreplySummaryRenderData | {}) +type AutoreplyHtmlData = + | ({ autoReplyBody: string[] } & AutoreplySummaryRenderData) + | { autoReplyBody: string[] } -export const generateAutoreplyHtml = async (htmlData: AutoreplyHtmlData) => { +export const generateAutoreplyHtml = async ( + htmlData: AutoreplyHtmlData, +): Promise => { const pathToTemplate = `${process.cwd()}/src/app/views/templates/submit-form-autoreply.server.view.html` return ejs.renderFile(pathToTemplate, htmlData) } -export const isToFieldValid = (addresses: string | string[]) => { +export const isToFieldValid = (addresses: string | string[]): boolean => { // Retrieve all emails from each address. // As addresses can be strings or a string array, cast given addresses param // into an array regardless and flatten deep. diff --git a/src/config/config.ts b/src/config/config.ts index aa09b3c805..c498e0d166 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,209 +1,105 @@ -import { PackageMode } from '@opengovsg/formsg-sdk/dist/types' import aws from 'aws-sdk' +import convict from 'convict' import { SessionOptions } from 'express-session' -import { ConnectionOptions } from 'mongoose' +import { merge } from 'lodash' import nodemailer from 'nodemailer' import directTransport from 'nodemailer-direct-transport' import Mail from 'nodemailer/lib/mailer' import SMTPPool from 'nodemailer/lib/smtp-pool' -import defaults from './defaults' -import { createLoggerWithLabel } from './logger' +import { AwsConfig, Config, DbConfig, Environment, MailConfig } from '../types' -const logger = createLoggerWithLabel(module) +import { + compulsoryVarsSchema, + loadS3BucketUrlSchema, + optionalVarsSchema, + prodOnlyVarsSchema, +} from './schema' -// Typings -type AppConfig = { - title: string - description: string - appUrl: string - keywords: string - images: string[] - twitterImage: string -} - -type DbConfig = { - uri: string - options: ConnectionOptions -} - -type AwsConfig = { - imageS3Bucket: string - logoS3Bucket: string - attachmentS3Bucket: string - region: string - - logoBucketUrl: string - imageBucketUrl: string - attachmentBucketUrl: string - s3: aws.S3 -} +// Load and validate optional configuration values +// If environment variables are not present, defaults are loaded +const optionalVars = convict(optionalVarsSchema) + .validate({ allowed: 'strict' }) + .getProperties() -type MailConfig = { - mailFrom: string - mailer: { - from: string - } - transporter: Mail -} +// Load and validate compulsory configuration values +// If environment variables are not present, an error will be thrown +const compulsoryVars = convict(compulsoryVarsSchema) + .validate({ allowed: 'strict' }) + .getProperties() -type Config = { - app: AppConfig - db: DbConfig - aws: AwsConfig - mail: MailConfig +// Deep merge nested objects optionalVars and compulsoryVars +const basicVars = merge(optionalVars, compulsoryVars) - cookieSettings: SessionOptions['cookie'] - // Consts - isDev: boolean - nodeEnv: Environment - port: number - sessionSecret: string - cspReportUri: string - chromiumBin: string - otpLifeSpan: number - bounceLifeSpan: number - formsgSdkMode: PackageMode - submissionsTopUp: number - customCloudWatchGroup?: string - isGeneralMaintenance?: string - isLoginBanner?: string - siteBannerContent?: string - adminBannerContent?: string - - // Functions - configureAws: () => Promise -} - -// Enums -enum Environment { - Dev = 'development', - Prod = 'production', - Test = 'test', -} - -// Environment variables with defaults const isDev = - process.env.NODE_ENV === Environment.Dev || - process.env.NODE_ENV === Environment.Test + basicVars.core.nodeEnv === Environment.Dev || + basicVars.core.nodeEnv === Environment.Test const nodeEnv = isDev ? Environment.Dev : Environment.Prod -const port = parseInt(process.env.PORT, 10) || defaults.app.port -const sessionSecret = process.env.SESSION_SECRET || defaults.app.sessionSecret - -/** - * OTP Life Span for Login. (Should be in miliseconds, e.g. 1000 * 60 * 15 = 15 - * mins). - */ -const otpLifeSpan = - parseInt(process.env.OTP_LIFE_SPAN, 10) || defaults.login.otpLifeSpan - -/** - * TTL of bounce documents in milliseconds. - */ -const bounceLifeSpan = - parseInt(process.env.BOUNCE_LIFE_SPAN, 10) || defaults.bounce.bounceLifeSpan -/** - * Number of submissions to top up submissions statistic by - */ -const submissionsTopUp = parseInt(process.env.SUBMISSIONS_TOP_UP, 10) || 0 - -/** - * Content Security Policy reporting - * HelmetJS reportUri param requires non-empty string, so 'undefined' is - * declared. - */ -const cspReportUri = process.env.CSP_REPORT_URI || 'undefined' -if (!process.env.CSP_REPORT_URI) { - logger.warn({ - message: 'Content Security Policy reporting is not configured.', - meta: { - action: 'init', - }, - }) +// Load and validate configuration values which are compulsory only in production +// If environment variables are not present, an error will be thrown +// They may still be referenced in development +let prodOnlyVars +if (isDev) { + prodOnlyVars = convict(prodOnlyVarsSchema).getProperties() +} else { + // Perform validation before accessing ses config + prodOnlyVars = convict(prodOnlyVarsSchema) + .validate({ allowed: 'strict' }) + .getProperties() } -/** - * Chromium executable for PDF generation - */ -const chromiumBin = process.env.CHROMIUM_BIN -if (!chromiumBin) { - const errMsg = - 'Path to Chromium executable missing - please specify in CHROMIUM_BIN environment variable' - logger.error({ - message: errMsg, - meta: { - action: 'init', - }, - }) - throw new Error(errMsg) -} +// Construct bucket URLs depending on node environment +// If in development env, endpoint communicates with localstack, a fully +// functional local AWS cloud stack for hosting images/logos/attachments. +// Else, the environment variables to instantiate S3 are used. -/** - * FormSG SDK mode. - * Needed due to the SDK having multiple modes - staging, production, - * development, and test, whilst the backend only has 2 modes (prod and dev). - * - * The SDK modes are required to be set properly so that the correct public keys - * are used to sign, encrypt, or decrypt data that is passed into the SDK. - */ -const formsgSdkMode = process.env.FORMSG_SDK_MODE as PackageMode -if ( - !formsgSdkMode || - !['staging', 'production', 'development', 'test'].includes(formsgSdkMode) -) { - const errMsg = - 'FORMSG_SDK_MODE not found or invalid. Please specify one of: "staging" | "production" | "development" | "test" in the environment variable.' - - logger.error({ - message: errMsg, - meta: { - action: 'init', - }, +// Perform validation before accessing s3 Bucket Urls +const s3BucketUrlSchema = loadS3BucketUrlSchema({ + isDev, + region: basicVars.awsConfig.region, +}) +const awsEndpoint = convict(s3BucketUrlSchema).getProperties().endPoint +const s3BucketUrlVars = convict(s3BucketUrlSchema) + .load({ + logoBucketUrl: `${awsEndpoint}/${basicVars.awsConfig.logoS3Bucket}`, + imageBucketUrl: `${awsEndpoint}/${basicVars.awsConfig.imageS3Bucket}`, + // NOTE THE TRAILING / AT THE END OF THIS URL! This is only for attachments! + attachmentBucketUrl: `${awsEndpoint}/${basicVars.awsConfig.attachmentS3Bucket}/`, }) - throw new Error(errMsg) + .validate({ allowed: 'strict' }) + .getProperties() + +const s3 = new aws.S3({ + region: basicVars.awsConfig.region, + // Unset and use default if not in development mode + // Endpoint and path style overrides are needed only in development mode for + // localstack to work. + endpoint: isDev ? s3BucketUrlVars.endPoint : undefined, + s3ForcePathStyle: isDev ? true : undefined, +}) + +const awsConfig: AwsConfig = { + ...s3BucketUrlVars, + ...basicVars.awsConfig, + s3, } -// Optional environment variables -/** - * Name of CloudWatch log group to store short-term logs. Log streams are - * separated by date. - */ -const customCloudWatchGroup = process.env.CUSTOM_CLOUDWATCH_LOG_GROUP - -/** - * Load env variable with General Maintenance banner text. - */ -const isGeneralMaintenance = process.env.IS_GENERAL_MAINTENANCE - -/** - * The banner message on login page. Allows for HTML. - */ -const isLoginBanner = process.env.IS_LOGIN_BANNER - -/** - * The banner message to show on all pages. Allows for HTML. Will supersede - * all other banner content if it exists. - */ -const siteBannerContent = process.env.SITE_BANNER_CONTENT - -/** - * The banner message to show on on admin pages. Allows for HTML. - */ -const adminBannerContent = process.env.ADMIN_BANNER_CONTENT - -// Configs -const appConfig: AppConfig = { - title: process.env.APP_NAME || defaults.app.name, - description: process.env.APP_DESC || defaults.app.desc, - appUrl: process.env.APP_URL || defaults.app.url, - keywords: process.env.APP_KEYWORDS || defaults.app.keywords, - images: (process.env.APP_IMAGES || defaults.app.images).split(','), - twitterImage: process.env.APP_TWITTER_IMAGE || defaults.app.twitterImage, +let dbUri +if (isDev) { + if (basicVars.core.nodeEnv === Environment.Dev && prodOnlyVars.dbHost) { + dbUri = prodOnlyVars.dbHost + } else if (basicVars.core.nodeEnv === Environment.Test) { + dbUri = undefined + } else { + throw new Error('Database configuration missing') + } +} else { + dbUri = prodOnlyVars.dbHost } const dbConfig: DbConfig = { - uri: process.env.DB_HOST || undefined, + uri: dbUri, options: { user: '', pass: '', @@ -222,77 +118,50 @@ const dbConfig: DbConfig = { } const mailConfig: MailConfig = (function () { - const mailFrom = process.env.MAIL_FROM || defaults.mail.mailFrom + const mailFrom = basicVars.mail.from const mailer = { - from: `${appConfig.title} <${mailFrom}>`, + from: `${basicVars.appConfig.title} <${mailFrom}>`, } // Creating mail transport - const hasAllSesCredentials = - process.env.SES_HOST && - process.env.SES_PORT && - process.env.SES_USER && - process.env.SES_PASS - let transporter: Mail - - if (hasAllSesCredentials) { + if (!isDev) { const options: SMTPPool.Options = { - host: process.env.SES_HOST, + host: prodOnlyVars.host, auth: { - user: process.env.SES_USER, - pass: process.env.SES_PASS, + user: prodOnlyVars.user, + pass: prodOnlyVars.pass, }, - port: Number(process.env.SES_PORT), + port: prodOnlyVars.port, // Options as advised from https://nodemailer.com/usage/bulk-mail/ // pool connections instead of creating fresh one for each email pool: true, - maxMessages: - Number(process.env.SES_MAX_MESSAGES) || defaults.ses.maxMessages, - maxConnections: - Number(process.env.SES_POOL) || defaults.ses.maxConnections, - socketTimeout: - Number(process.env.MAIL_SOCKET_TIMEOUT) || defaults.ses.socketTimeout, + maxMessages: basicVars.mail.maxMessages, + maxConnections: basicVars.mail.maxConnections, + socketTimeout: basicVars.mail.socketTimeout, // If set to true then logs to console. If value is not set or is false // then nothing is logged. - logger: String(process.env.MAIL_LOGGER).toLowerCase() === 'true', + logger: basicVars.mail.logger, // If set to true, then logs SMTP traffic, otherwise logs only transaction // events. - debug: String(process.env.MAIL_DEBUG).toLowerCase() === 'true', - // Per second, or per rateDelta if supplied. - rateLimit: process.env.SES_RATE - ? Number(process.env.SES_RATE) - : undefined, - // Defines the time measuring period in milliseconds (defaults to 1000) - // for rate limiting. - rateDelta: process.env.SES_RATEDELTA - ? Number(process.env.SES_RATEDELTA) - : undefined, + debug: basicVars.mail.debug, } - transporter = nodemailer.createTransport(options) - } else if (process.env.SES_PORT) { - logger.warn({ - message: - '\n!!! WARNING !!!\nNo SES credentials detected.\nUsing Nodemailer to send to local SMTP server instead.\nThis should NEVER be seen in production.', - meta: { - action: 'init.mailConfig', - }, - }) - transporter = nodemailer.createTransport({ - port: Number(process.env.SES_PORT), - ignoreTLS: true, - }) } else { - logger.warn({ - message: - '\n!!! WARNING !!!\nNo SES credentials detected.\nUsing Nodemailer Direct Transport instead.\nThis should NEVER be seen in production.', - meta: { - action: 'init.mailConfig', - }, - }) - // Falls back to direct transport - transporter = nodemailer.createTransport(directTransport({})) + if (basicVars.core.nodeEnv === Environment.Dev) { + // Falls back to direct transport + transporter = nodemailer.createTransport(directTransport({})) + } else if ( + basicVars.core.nodeEnv === Environment.Test && + prodOnlyVars.port + ) { + transporter = nodemailer.createTransport({ + port: prodOnlyVars.port, + ignoreTLS: true, + }) + } else { + throw new Error('Nodemailer configuration is missing') + } } return { @@ -302,73 +171,7 @@ const mailConfig: MailConfig = (function () { } })() -// Run function instead of a constant so errors can be thrown. -const awsConfig: AwsConfig = (function () { - const imageS3Bucket = process.env.IMAGE_S3_BUCKET - if (!imageS3Bucket) { - throw new Error( - 'Image S3 Bucket configuration missing - please specify in IMAGE_S3_BUCKET environment variable', - ) - } - /** - * S3 Bucket to upload logos to - */ - const logoS3Bucket = process.env.LOGO_S3_BUCKET - if (!logoS3Bucket) { - throw new Error( - 'Logo S3 Bucket configuration missing - please specify in LOGO_S3_BUCKET environment variable', - ) - } - - /** - * S3 Bucket to upload encrypted attachments to - */ - const attachmentS3Bucket = process.env.ATTACHMENT_S3_BUCKET - if (!attachmentS3Bucket) { - throw new Error( - 'Attachment S3 Bucket configuration missing - please specify in ATTACHMENT_S3_BUCKET environment variable', - ) - } - - /** - * Region that S3 bucket is located in - */ - const region = process.env.AWS_REGION || defaults.aws.region - - // Construct bucket URLs depending on node environment - // If in development env, endpoint communicates with localstack, a fully - // functional local AWS cloud stack for hosting images/logos/attachments. - // Else, the environment variables to instantiate S3 are used. - const awsEndpoint = isDev - ? defaults.aws.endpoint - : `https://s3.${region}.amazonaws.com` // NOTE NO TRAILING / AT THE END OF THIS URL! - - const logoBucketUrl = `${awsEndpoint}/${logoS3Bucket}` - const imageBucketUrl = `${awsEndpoint}/${imageS3Bucket}` - // NOTE THE TRAILING / AT THE END OF THIS URL! This is only for attachments! - const attachmentBucketUrl = `${awsEndpoint}/${attachmentS3Bucket}/` - - const s3 = new aws.S3({ - region, - // Unset and use default if not in development mode - // Endpoint and path style overrides are needed only in development mode for - // localstack to work. - endpoint: isDev ? defaults.aws.endpoint : undefined, - s3ForcePathStyle: isDev ? true : undefined, - }) - - return { - imageS3Bucket, - logoS3Bucket, - attachmentS3Bucket, - region, - logoBucketUrl, - imageBucketUrl, - attachmentBucketUrl, - s3, - } -})() - +// Cookie settings needed for express-session configuration const cookieSettings: SessionOptions['cookie'] = { httpOnly: true, // JavaScript will not be able to read the cookie in case of XSS exploitation secure: !isDev, // true prevents cookie from being accessed over http @@ -376,7 +179,9 @@ const cookieSettings: SessionOptions['cookie'] = { sameSite: 'strict', // Cookie will not be sent if navigating from another domain } -// Functions +/** + * Fetches AWS credentials + */ const configureAws = async () => { if (!isDev) { const getCredentials = () => { @@ -390,7 +195,6 @@ const configureAws = async () => { }) }) } - await getCredentials() if (!aws.config.credentials.accessKeyId) { throw new Error(`AWS Access Key Id is missing`) @@ -402,26 +206,25 @@ const configureAws = async () => { } const config: Config = { - app: appConfig, + app: basicVars.appConfig, db: dbConfig, aws: awsConfig, mail: mailConfig, cookieSettings, isDev, nodeEnv, - port, - customCloudWatchGroup, - sessionSecret, - otpLifeSpan, - bounceLifeSpan, - formsgSdkMode, - chromiumBin, - cspReportUri, - submissionsTopUp, - isGeneralMaintenance, - isLoginBanner, - siteBannerContent, - adminBannerContent, + formsgSdkMode: basicVars.formsgSdkMode, + customCloudWatchGroup: basicVars.awsConfig.customCloudWatchGroup, + bounceLifeSpan: basicVars.mail.bounceLifeSpan, + chromiumBin: basicVars.mail.chromiumBin, + port: basicVars.core.port, + sessionSecret: basicVars.core.sessionSecret, + otpLifeSpan: basicVars.core.otpLifeSpan, + submissionsTopUp: basicVars.core.submissionsTopUp, + isGeneralMaintenance: basicVars.banner.isGeneralMaintenance, + isLoginBanner: basicVars.banner.isLoginBanner, + siteBannerContent: basicVars.banner.siteBannerContent, + adminBannerContent: basicVars.banner.adminBannerContent, configureAws, } diff --git a/src/config/defaults.ts b/src/config/defaults.ts deleted file mode 100644 index d2bbc78ac5..0000000000 --- a/src/config/defaults.ts +++ /dev/null @@ -1,63 +0,0 @@ -// This file contains defaults used throughout the app that does not really need -// to be kept in a `.env` file, or can be used as a fallback when the `.env` key -// does not exist. - -// Config for express application. -const APP_CONFIG = { - name: 'FormSG', - url: 'https://form.gov.sg', - desc: 'Form Manager for Government', - keywords: 'forms, formbuilder, nodejs', - images: - '/public/modules/core/img/og/img_metatag.png,/public/modules/core/img/og/logo-vertical-color.png', - twitterImage: '/public/modules/core/img/og/logo-vertical-color.png', - port: 5000, - sessionSecret: 'sandcrawler-138577', -} - -// Config for login constants. -const LOGIN_CONFIG = { - // Number is in miliseconds. - otpLifeSpan: 900000, -} - -// Config for email sending. -const MAIL_CONFIG = { - // The sender email to display on mail sent. - mailFrom: 'donotreply@mail.form.gov.sg', -} - -// Config for AWS. -const AWS_CONFIG = { - region: 'ap-southeast-1', - endpoint: 'http://localhost:4572', -} - -// Config for AWS SES service. -const SES_CONFIG = { - // Connection removed and new one created when this limit is reached. - maxMessages: 100, - // Send email in parallel to SMTP server. - maxConnections: 38, - // How many milliseconds of inactivity to allow. - socketTimeout: 600000, -} - -const LINKS = { - supportFormLink: 'https://go.gov.sg/formsg-support', -} - -const BOUNCE_CONFIG = { - // TTL of Bounce document in milliseconds - bounceLifeSpan: 1800000, -} - -export default { - app: APP_CONFIG, - login: LOGIN_CONFIG, - mail: MAIL_CONFIG, - aws: AWS_CONFIG, - ses: SES_CONFIG, - links: LINKS, - bounce: BOUNCE_CONFIG, -} diff --git a/src/config/feature-manager/sentry.config.ts b/src/config/feature-manager/sentry.config.ts index 48134a910a..a9bfc6e30d 100644 --- a/src/config/feature-manager/sentry.config.ts +++ b/src/config/feature-manager/sentry.config.ts @@ -4,11 +4,17 @@ const sentryFeature: RegisterableFeature = { name: FeatureNames.Sentry, schema: { sentryConfigUrl: { - doc: 'Sentry.io URL for configuring the Raven SDK', + doc: 'Sentry.io URL for configuring the Sentry SDK', format: 'url', default: null, env: 'SENTRY_CONFIG_URL', }, + cspReportUri: { + doc: 'Endpoint for content security policy reporting', + format: 'url', + default: null, + env: 'CSP_REPORT_URI', + }, }, } diff --git a/src/config/feature-manager/types.ts b/src/config/feature-manager/types.ts index c49ea5bf31..d9e6a98c75 100644 --- a/src/config/feature-manager/types.ts +++ b/src/config/feature-manager/types.ts @@ -26,6 +26,7 @@ export interface IGoogleAnalytics { export interface ISentry { sentryConfigUrl: string + cspReportUri: string } export interface ISms { diff --git a/src/config/feature-manager/util/FeatureManager.class.ts b/src/config/feature-manager/util/FeatureManager.class.ts index 0a3326b7ed..bb7ff7bd35 100644 --- a/src/config/feature-manager/util/FeatureManager.class.ts +++ b/src/config/feature-manager/util/FeatureManager.class.ts @@ -43,9 +43,9 @@ export default class FeatureManager { const config = convict(schema) const properties = config.getProperties() const isEnabled = Object.keys(schema).every((variable) => { - let val = _.get(properties, variable, null) + const val = _.get(properties, variable, null) // empty strings (i.e. '') are considered defined - let isDefined = !_.isNil(val) + const isDefined = !_.isNil(val) return isDefined }) diff --git a/src/config/logger.ts b/src/config/logger.ts index b361f48449..c1753217f0 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -1,4 +1,5 @@ import hasAnsi from 'has-ansi' +import { isEmpty } from 'lodash' import omit from 'lodash/omit' import logform from 'logform' import path from 'path' @@ -8,7 +9,9 @@ import { v4 as uuidv4 } from 'uuid' import { format, Logger, LoggerOptions, loggers, transports } from 'winston' import WinstonCloudWatch from 'winston-cloudwatch' -import defaults from './defaults' +import { Environment } from '../types' + +import { aws, customCloudWatchGroup, isDev, nodeEnv } from './config' // Params to enforce the logging format. type CustomLoggerParams = { @@ -20,12 +23,6 @@ type CustomLoggerParams = { error?: Error } -// Cannot use config due to logger being instantiated first, and -// having circular dependencies. -const isDev = ['development', 'test'].includes(process.env.NODE_ENV) -const customCloudWatchGroup = process.env.CUSTOM_CLOUDWATCH_LOG_GROUP -const awsRegion = process.env.AWS_REGION || defaults.aws.region - // A variety of helper functions to make winston logging like console logging, // allowing multiple arguments. // Retrieved from @@ -167,7 +164,7 @@ const createLoggerOptions = (label: string): LoggerOptions => { ), transports: [ new transports.Console({ - silent: process.env.NODE_ENV === 'test', + silent: nodeEnv === Environment.Test, }), ], exitOnError: false, @@ -189,23 +186,52 @@ const getModuleLabel = (callingModule: NodeModule) => { /** * Overrides the given winston logger with a new signature, so as to enforce a * log format. + * TODO(#42): Remove try catch blocks when application is 100% TypeScript. * @param logger the logger to override */ const createCustomLogger = (logger: Logger) => { return { - info: ({ message, meta }: Omit) => - logger.info(message, { meta }), - warn: ({ message, meta, error }: CustomLoggerParams) => { - if (error) { - return logger.warn(message, { meta }, error) + info: (params: Omit) => { + try { + const { message, meta } = params + // Not the expected shape, throw to catch block. + if (!message || isEmpty(meta)) { + throw new Error('Wrong shape') + } + return logger.info(message, { meta }) + } catch { + return logger.info(params) } - return logger.warn(message, { meta }) }, - error: ({ message, meta, error }: CustomLoggerParams) => { - if (error) { - return logger.error(message, { meta }, error) + warn: (params: CustomLoggerParams) => { + try { + const { message, meta, error } = params + // Not the expected shape, throw to catch block. + if (!message || isEmpty(meta)) { + throw new Error('Wrong shape') + } + if (error) { + return logger.warn(message, { meta }, error) + } + return logger.warn(message, { meta }) + } catch { + return logger.warn(params) + } + }, + error: (params: CustomLoggerParams) => { + try { + const { message, meta, error } = params + // Not the expected shape, throw to catch block. + if (!message || isEmpty(meta)) { + throw new Error('Wrong shape') + } + if (error) { + return logger.error(message, { meta }, error) + } + return logger.error(message, { meta }) + } catch { + return logger.error(params) } - return logger.error(message, { meta }) }, } } @@ -238,7 +264,7 @@ export const createCloudWatchLogger = (label: string) => { // not share sequence tokens. Hence generate a unique ID for each instance // of the logger. logStreamName: uuidv4(), - awsRegion, + awsRegion: aws.region, jsonMessage: true, }), ] diff --git a/src/config/schema.ts b/src/config/schema.ts new file mode 100644 index 0000000000..3e58364030 --- /dev/null +++ b/src/config/schema.ts @@ -0,0 +1,363 @@ +import { PackageMode } from '@opengovsg/formsg-sdk/dist/types' +import awsInfo from 'aws-info' +import convict, { Schema } from 'convict' +import { email, url } from 'convict-format-with-validator' +import { isNil } from 'lodash' +import mongodbUri from 'mongodb-uri' +import validator from 'validator' + +import { + Environment, + IBucketUrlSchema, + ICompulsoryVarsSchema, + IOptionalVarsSchema, + IProdOnlyVarsSchema, +} from '../types' + +convict.addFormat(url) +convict.addFormat(email) +convict.addFormat({ + name: 'string[]', + validate: (val: string[]) => { + if (!Array.isArray(val)) { + throw new Error('must be of type Array') + } + if (val.some((i) => typeof i !== 'string')) { + throw new Error('Elements must be of type string') + } + }, + coerce: (val: string): string[] => { + return val.split(',') + }, +}) + +/** + * Verifies that S3 bucket url is a valid url with or without trailing slash + */ +const validateBucketUrl = ( + val: string, + { + isDev, + hasTrailingSlash, + region, + }: { isDev: boolean; hasTrailingSlash: boolean; region: string }, +) => { + if (!validator.isURL(val, { require_tld: !isDev })) { + throw new Error('must be a url') + } + if (hasTrailingSlash) { + if (!/[/]$/.test(val)) { + throw new Error('must end with a slash') + } + } else { + if (/[/]$/.test(val)) { + throw new Error('must not end with a slash') + } + } + // Region should be specified correctly in production + const isRegionCorrect = new RegExp(`^https://s3.${region}.amazonaws.com`, 'i') + if (!isDev && !isRegionCorrect.test(val)) { + throw new Error(`region should be ${region}`) + } +} + +// If the default value does not match the format specified, the configuration built from this schema +// will throw an error upon validation (i.e. All env vars with default null have to be specified) +export const compulsoryVarsSchema: Schema = { + awsConfig: { + imageS3Bucket: { + doc: 'S3 Bucket to upload images to', + format: String, + default: null, + env: 'IMAGE_S3_BUCKET', + }, + logoS3Bucket: { + doc: 'S3 Bucket to upload logos to', + format: String, + default: null, + env: 'LOGO_S3_BUCKET', + }, + attachmentS3Bucket: { + doc: 'S3 Bucket to upload encrypted attachments to', + format: String, + default: null, + env: 'ATTACHMENT_S3_BUCKET', + }, + }, + core: { + sessionSecret: { + doc: 'Session Secret', + format: String, + default: null, + env: 'SESSION_SECRET', + sensitive: true, + }, + }, +} + +// If the following environment variables are not specified, we will fall back to the defaults provided +export const optionalVarsSchema: Schema = { + appConfig: { + title: { + doc: 'Application name in window title', + format: String, + default: 'FormSG', + env: 'APP_NAME', + }, + description: { + doc: 'Application description in meta tag', + format: String, + default: 'Form Manager for Government', + env: 'APP_DESC', + }, + appUrl: { + doc: 'Application url in meta tag', + format: 'url', + default: 'https://form.gov.sg', + env: 'APP_URL', + }, + keywords: { + doc: 'Application keywords in meta tag', + format: String, + default: 'forms, formbuilder, nodejs', + env: 'APP_KEYWORDS', + }, + twitterImage: { + doc: 'Application image in twitter meta tag', + format: String, + default: '/public/modules/core/img/og/logo-vertical-color.png', + env: 'APP_TWITTER_IMAGE', + }, + images: { + doc: 'Application images in meta tag', + format: 'string[]', + default: [ + '/public/modules/core/img/og/img_metatag.png', + '/public/modules/core/img/og/logo-vertical-color.png', + ], + env: 'APP_IMAGES', + }, + }, + banner: { + isGeneralMaintenance: { + doc: 'Load env variable with General Maintenance banner text.', + format: String, + default: '', + env: 'IS_GENERAL_MAINTENANCE', + }, + isLoginBanner: { + doc: 'The banner message on login page. Allows for HTML.', + format: String, + default: '', + env: 'IS_LOGIN_BANNER', + }, + siteBannerContent: { + doc: + 'The banner message to show on all pages. Allows for HTML. Will supersede all other banner content if it exists.', + format: String, + default: '', + env: 'SITE_BANNER_CONTENT', + }, + adminBannerContent: { + doc: 'The banner message to show on on admin pages. Allows for HTML.', + format: String, + default: '', + env: 'ADMIN_BANNER_CONTENT', + }, + }, + formsgSdkMode: { + doc: + 'Inform SDK which public keys are to be used to sign, encrypt, or decrypt data that is passed to it', + format: ['staging', 'production', 'development', 'test'], + default: 'production' as PackageMode, + env: 'FORMSG_SDK_MODE', + }, + mail: { + from: { + doc: 'Sender email address', + format: 'email', + default: 'donotreply@mail.form.gov.sg', + env: 'MAIL_FROM', + }, + logger: { + doc: 'If set to true then logs to console', + format: 'Boolean', + default: false, + env: 'MAIL_LOGGER', + }, + debug: { + doc: + 'If set to true, then logs SMTP traffic, otherwise logs only transaction events.', + format: 'Boolean', + default: false, + env: 'MAIL_DEBUG', + }, + bounceLifeSpan: { + doc: 'TTL of bounce documents in milliseconds', + format: 'int', + default: 10800000, + env: 'BOUNCE_LIFE_SPAN', + }, + chromiumBin: { + doc: 'Path to chromium executable for PDF generation', + format: String, + default: '/usr/bin/chromium-browser', + env: 'CHROMIUM_BIN', + }, + maxMessages: { + doc: + 'Nodemailer config to help to keep the connection up-to-date for long-running messaging', + format: 'int', + default: 100, + env: 'SES_MAX_MESSAGES', + }, + maxConnections: { + doc: 'Connection pool to send email in parallel to the SMTP server', + format: 'int', + default: 38, + env: 'SES_POOL', + }, + socketTimeout: { + doc: 'Milliseconds of inactivity to allow before killing a connection', + format: 'int', + default: 600000, + env: 'MAIL_SOCKET_TIMEOUT', + }, + }, + awsConfig: { + region: { + doc: 'Region that S3 bucket is located in', + format: Object.keys(awsInfo.data.regions), + default: 'ap-southeast-1', + env: 'AWS_REGION', + }, + customCloudWatchGroup: { + doc: + 'Name of CloudWatch log group to store short-term logs. Log streams are separated by date.', + format: String, + default: '', + env: 'CUSTOM_CLOUDWATCH_LOG_GROUP', + }, + }, + core: { + port: { + doc: 'Application Port', + format: 'port', + default: 5000, + env: 'PORT', + }, + otpLifeSpan: { + doc: + 'OTP Life Span for Login. (Should be in miliseconds, e.g. 1000 * 60 * 15 = 15 mins)', + format: 'int', + default: 900000, + env: 'OTP_LIFE_SPAN', + }, + submissionsTopUp: { + doc: 'Number of submissions to top up submissions statistic by', + format: 'int', + default: 0, + env: 'SUBMISSIONS_TOP_UP', + }, + nodeEnv: { + doc: 'Express environment mode', + format: [Environment.Prod, Environment.Dev, Environment.Test], + default: Environment.Prod, + env: 'NODE_ENV', + }, + }, +} + +export const prodOnlyVarsSchema: Schema = { + port: { + doc: 'SMTP port number', + format: 'port', + default: null, + env: 'SES_PORT', + }, + host: { + doc: 'SMTP hostname', + format: String, + default: null, + env: 'SES_HOST', + }, + user: { + doc: 'SMTP username', + format: String, + default: null, + env: 'SES_USER', + }, + pass: { + doc: 'SMTP password', + format: String, + default: null, + env: 'SES_PASS', + sensitive: true, + }, + dbHost: { + doc: 'Database URI', + format: (val) => { + // Will throw error if scheme and hosts are not present + const uriObject = mongodbUri.parse(val) + /* + e.g. mongodb://database:27017/formsg will be parsed into: + { + scheme: 'mongodb', + database: 'formsg', + hosts: [ { host: 'database', port: 27017 } ] + } + e.g. https://form.gov.sg will be parsed into: + { + scheme: 'https', + hosts: [ { host: 'form.gov.sg' } ] + } + */ + if (uriObject.scheme !== 'mongodb') { + throw new Error('Scheme must be mongodb') + } + if (isNil(uriObject.database)) { + throw new Error('Database must be specified') + } + }, + default: null, + env: 'DB_HOST', + sensitive: true, + }, +} + +export const loadS3BucketUrlSchema = ({ + isDev, + region, +}: { + isDev: boolean + region: string +}): Schema => { + return { + endPoint: { + doc: 'Endpoint for S3 buckets', + format: (val) => + validateBucketUrl(val, { isDev, hasTrailingSlash: false, region }), + default: 'https://s3.ap-southeast-1.amazonaws.com', // NOTE NO TRAILING / AT THE END OF THIS URL! + env: 'AWS_ENDPOINT', + }, + attachmentBucketUrl: { + doc: + 'Url of attachment S3 bucket derived from S3 endpoint and bucket name', + format: (val) => + validateBucketUrl(val, { isDev, hasTrailingSlash: true, region }), + default: null, + }, + logoBucketUrl: { + doc: 'Url of logo S3 bucket derived from S3 endpoint and bucket name', + format: (val) => + validateBucketUrl(val, { isDev, hasTrailingSlash: false, region }), + default: null, + }, + imageBucketUrl: { + doc: 'Url of images S3 bucket derived from S3 endpoint and bucket name', + format: (val) => + validateBucketUrl(val, { isDev, hasTrailingSlash: false, region }), + default: null, + }, + } +} diff --git a/src/loaders/express/error-handler.ts b/src/loaders/express/error-handler.ts index 8ce0a80746..5f7630d524 100644 --- a/src/loaders/express/error-handler.ts +++ b/src/loaders/express/error-handler.ts @@ -28,13 +28,13 @@ const errorHandlerMiddlewares = () => { 'Apologies, something odd happened. Please try again later!' // Error page if (isCelebrate(err)) { - let errorMessage = get( + const errorMessage = get( err, 'joi.details[0].message', genericErrorMessage, ) // formId is only present for Joi validated routes that require it - let formId = get(req, 'form._id', null) + const formId = get(req, 'form._id', null) logger.error({ message: 'Joi validation error', meta: { @@ -45,7 +45,14 @@ const errorHandlerMiddlewares = () => { }) return res.status(StatusCodes.BAD_REQUEST).send(errorMessage) } - logger.error(err) + + logger.error({ + message: 'Unknown error', + meta: { + action: 'genericErrorHandlerMiddleware', + }, + error: err, + }) return res .status(StatusCodes.INTERNAL_SERVER_ERROR) .send({ message: genericErrorMessage }) diff --git a/src/loaders/express/helmet.ts b/src/loaders/express/helmet.ts index f30884ec3b..32a20d05e2 100644 --- a/src/loaders/express/helmet.ts +++ b/src/loaders/express/helmet.ts @@ -1,7 +1,10 @@ import { RequestHandler } from 'express' import helmet from 'helmet' +import { get } from 'lodash' import config from '../../config/config' +import featureManager from '../../config/feature-manager' +import { FeatureNames } from '../../config/feature-manager/types' const helmetMiddlewares = () => { // Only add the "Strict-Transport-Security" header if request is https. @@ -24,57 +27,68 @@ const helmetMiddlewares = () => { policy: 'strict-origin-when-cross-origin', }) + const cspCoreDirectives = { + defaultSrc: ["'self'"], + imgSrc: [ + "'self'", + 'data:', + 'https://www.googletagmanager.com/', + 'https://www.google-analytics.com/', + `https://s3-${config.aws.region}.amazonaws.com/agency.form.sg/`, // Agency logos + config.aws.imageBucketUrl, // Image field + config.aws.logoBucketUrl, // Form logo + '*', // TODO: Remove when we host our own images for Image field and Form Logo + ], + fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com/'], + scriptSrc: [ + "'self'", + 'https://www.googletagmanager.com/', + 'https://ssl.google-analytics.com/', + 'https://www.google-analytics.com/', + 'https://www.tagmanager.google.com/', + 'https://www.google.com/recaptcha/', + 'https://www.recaptcha.net/recaptcha/', + 'https://www.gstatic.com/recaptcha/', + 'https://www.gstatic.cn/', + 'https://www.google-analytics.com/', + ], + connectSrc: [ + "'self'", + 'https://www.google-analytics.com/', + 'https://ssl.google-analytics.com/', + 'https://sentry.io/api/', + config.aws.attachmentBucketUrl, // Attachment downloads + config.aws.imageBucketUrl, // Image field + config.aws.logoBucketUrl, // Form logo + ], + frameSrc: [ + "'self'", + 'https://www.google.com/recaptcha/', + 'https://www.recaptcha.net/recaptcha/', + ], + objectSrc: ["'none'"], + styleSrc: [ + "'self'", + 'https://www.google.com/recaptcha/', + 'https://www.recaptcha.net/recaptcha/', + 'https://www.gstatic.com/recaptcha/', + 'https://www.gstatic.cn/', + ], + formAction: ["'self'"], + upgradeInsecureRequests: !config.isDev, + } + + const reportUri = get( + featureManager.props(FeatureNames.Sentry), + 'cspReportUri', + undefined, + ) + const cspOptionalDirectives = reportUri ? { reportUri } : {} + const contentSecurityPolicyMiddleware = helmet.contentSecurityPolicy({ directives: { - defaultSrc: ["'self'"], - imgSrc: [ - "'self'", - 'data:', - 'https://www.googletagmanager.com/', - 'https://www.google-analytics.com/', - `https://s3-${config.aws.region}.amazonaws.com/agency.form.sg/`, // Agency logos - config.aws.imageBucketUrl, // Image field - config.aws.logoBucketUrl, // Form logo - '*', // TODO: Remove when we host our own images for Image field and Form Logo - ], - fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com/'], - scriptSrc: [ - "'self'", - 'https://www.googletagmanager.com/', - 'https://ssl.google-analytics.com/', - 'https://www.google-analytics.com/', - 'https://www.tagmanager.google.com/', - 'https://www.google.com/recaptcha/', - 'https://www.recaptcha.net/recaptcha/', - 'https://www.gstatic.com/recaptcha/', - 'https://www.gstatic.cn/', - 'https://www.google-analytics.com/', - ], - connectSrc: [ - "'self'", - 'https://www.google-analytics.com/', - 'https://ssl.google-analytics.com/', - 'https://sentry.io/api/', - config.aws.attachmentBucketUrl, // Attachment downloads - config.aws.imageBucketUrl, // Image field - config.aws.logoBucketUrl, // Form logo - ], - frameSrc: [ - "'self'", - 'https://www.google.com/recaptcha/', - 'https://www.recaptcha.net/recaptcha/', - ], - objectSrc: ["'none'"], - styleSrc: [ - "'self'", - 'https://www.google.com/recaptcha/', - 'https://www.recaptcha.net/recaptcha/', - 'https://www.gstatic.com/recaptcha/', - 'https://www.gstatic.cn/', - ], - formAction: ["'self'"], - upgradeInsecureRequests: !config.isDev, - reportUri: config.cspReportUri, + ...cspCoreDirectives, + ...cspOptionalDirectives, }, }) diff --git a/src/loaders/express/index.ts b/src/loaders/express/index.ts index 96a816e8a1..38b517e1c3 100644 --- a/src/loaders/express/index.ts +++ b/src/loaders/express/index.ts @@ -7,6 +7,7 @@ import { Connection } from 'mongoose' import path from 'path' import url from 'url' +import { AuthRouter } from '../../app/modules/auth/auth.routes' import { BounceRouter } from '../../app/modules/bounce/bounce.routes' import UserRouter from '../../app/modules/user/user.routes' import { VfnRouter } from '../../app/modules/verification/verification.routes' @@ -45,7 +46,7 @@ const loadExpressApp = async (connection: Connection) => { } app.use(function (req, res, next) { - let urlPath = url.parse(req.url).path.split('/') + const urlPath = url.parse(req.url).path.split('/') if ( urlPath.indexOf('static') > -1 && urlPath.indexOf('view') === urlPath.indexOf('static') - 1 @@ -123,6 +124,8 @@ const loadExpressApp = async (connection: Connection) => { apiRoutes.forEach(function (routeFunction) { routeFunction(app) }) + + app.use('/auth', AuthRouter) app.use('/user', UserRouter) app.use('/emailnotifications', BounceRouter) app.use('/transaction', VfnRouter) diff --git a/src/loaders/mongoose.ts b/src/loaders/mongoose.ts index e1eea03469..53190820e2 100644 --- a/src/loaders/mongoose.ts +++ b/src/loaders/mongoose.ts @@ -7,7 +7,7 @@ import { createLoggerWithLabel } from '../config/logger' const logger = createLoggerWithLabel(module) export default async (): Promise => { - let usingMockedDb = config.db.uri === undefined + const usingMockedDb = config.db.uri === undefined // Mock out the database if we're developing locally if (usingMockedDb) { logger.warn({ @@ -48,7 +48,13 @@ export default async (): Promise => { // Only required for initial connection errors, reconnect on error. connect().catch((err) => { - logger.error(err) + logger.error({ + message: '@MongoDB: Error caught while connecting', + meta: { + action: 'init', + }, + error: err, + }) return connect() }) @@ -83,8 +89,8 @@ export default async (): Promise => { // Seed the db with govtech agency if using the mocked db if (usingMockedDb) { - let Agency = mongoose.model('Agency') - let agency = new Agency({ + const Agency = mongoose.model('Agency') + const agency = new Agency({ shortName: 'govtech', fullName: 'Government Technology Agency', emailDomain: 'data.gov.sg', diff --git a/src/public/main.js b/src/public/main.js index 69d0f14e4f..a647a19cdc 100644 --- a/src/public/main.js +++ b/src/public/main.js @@ -1,6 +1,8 @@ 'use strict' const textEncoding = require('text-encoding') +const Sentry = require('@sentry/browser') +const { Angular: AngularIntegration } = require('@sentry/integrations') if (!window['TextDecoder']) { window['TextDecoder'] = textEncoding.TextDecoder @@ -10,7 +12,7 @@ if (!window['TextEncoder']) { window['TextEncoder'] = textEncoding.TextEncoder } -// Define module dependencies (without ngRaven) +// Define module dependencies (without ngSentry) const moduleDependencies = [ 'ui.select', 'ngAnimate', @@ -41,12 +43,13 @@ window.$ = window.jQuery const angular = require('angular') // Sentry.io SDK -const Raven = require('raven-js') +// Docs: https://docs.sentry.io/platforms/javascript/guides/angular/angular1/ if (window.sentryConfigUrl) { - Raven.config(window.sentryConfigUrl) - .addPlugin(require('raven-js/plugins/angular'), angular) - .install() - moduleDependencies.push('ngRaven') + Sentry.init({ + dsn: window.sentryConfigUrl, + integrations: [new AngularIntegration()], + }) + moduleDependencies.push('ngSentry') } require('angular-translate') @@ -141,7 +144,6 @@ app.requires.push('users') require('./modules/core/services/gtag.client.service.js') require('./modules/core/services/analytics.client.service.js') require('./modules/core/services/formsgSdk.client.factory') -require('./modules/core/services/feature.client.factory') // Core controllers require('./modules/core/controllers/landing.client.controller.js') diff --git a/src/public/modules/core/componentViews/avatar-dropdown.html b/src/public/modules/core/componentViews/avatar-dropdown.html index 4f92c89fdb..874653b06a 100644 --- a/src/public/modules/core/componentViews/avatar-dropdown.html +++ b/src/public/modules/core/componentViews/avatar-dropdown.html @@ -10,9 +10,7 @@ ng-mouseenter="vm.isDropdownHover=true" ng-mouseleave="vm.isDropdownHover=false" > - +