From 2313300a1708a72c5dbe988f65e7fb2e9d4cefbf Mon Sep 17 00:00:00 2001 From: Egor Iakovlev Date: Tue, 8 Oct 2024 22:55:46 +0300 Subject: [PATCH] Use LocalStack for local testing. --- .env.example | 41 +-- backend/vaa-strapi/.env.example | 43 +-- .../config/env/development/plugins.ts | 21 -- backend/vaa-strapi/config/middlewares.ts | 33 ++- backend/vaa-strapi/config/plugins.ts | 58 ++-- backend/vaa-strapi/docker-compose.dev.yml | 26 ++ backend/vaa-strapi/localstack-init-aws.sh | 6 + .../vaa-strapi/localstack-s3-cors-policy.json | 11 + backend/vaa-strapi/package.json | 2 +- .../1.0.0/full_documentation.json | 2 +- .../types/generated/components.d.ts | 253 +++++++++++++++--- .../types/generated/contentTypes.d.ts | 201 ++++++-------- docker-compose.dev.yml | 31 ++- docs/candidate-app/candidates.md | 2 +- package.json | 6 +- tests/candidateApp-advanced.spec.ts | 52 ++-- yarn.lock | 226 +++++++++++++++- 17 files changed, 745 insertions(+), 269 deletions(-) delete mode 100644 backend/vaa-strapi/config/env/development/plugins.ts create mode 100755 backend/vaa-strapi/localstack-init-aws.sh create mode 100755 backend/vaa-strapi/localstack-s3-cors-policy.json diff --git a/.env.example b/.env.example index 39c673cf7..585750afe 100644 --- a/.env.example +++ b/.env.example @@ -15,27 +15,36 @@ DATABASE_SCHEMA=public DATABASE_SSL_SELF=false PUBLIC_FRONTEND_URL="http://localhost:5173" # Used for emails linking to the frontend -# AWS SES Settings -AWS_ACCESS_KEY_ID="EXAMPLE_KEY" -AWS_SECRET_ACCESS_KEY="EXAMPLE_SECRET" -AWS_REGION="eu-north-1" +## AWS LocalStack development endpoint +LOCALSTACK_ENDPOINT=http://127.0.0.1:4566 + +## AWS SES Settings + +## These settings correspond to LocalStack system defaults +## https://docs.localstack.cloud/references/configuration/ +AWS_ACCESS_KEY_ID="test" +AWS_SECRET_ACCESS_KEY="test" +AWS_REGION=us-east-1 + # `MAIL_FROM` and `MAIL_REPLY_TO` variables will not take effect for emails sent by `user-permissions` Strapi plugin # (f.e. reset password emails) as they are configured separately via Strapi UI in `Settings > Email Templates`. -MAIL_FROM="OpenVAA Voting Advice Application " -MAIL_REPLY_TO="OpenVAA Admin " +MAIL_FROM="no-reply@example.com" +MAIL_REPLY_TO="contact@example.com" + +## AWS S3 settings -# Maildev settings (used only in dev) -MAILDEV_PORT=1080 +AWS_S3_BUCKET=static.example.com -## AWS S3 settings for static content storage -AWS_S3_ACCESS_KEY_ID="EXAMPLE_KEY" -AWS_S3_ACCESS_SECRET="EXAMPLE_SECRET" -AWS_S3_REGION=eu-north-1 -AWS_S3_BUCKET=static-example-com +## These settings correspond to LocalStack system defaults +## https://docs.localstack.cloud/references/configuration/ +AWS_S3_ACCESS_KEY_ID="test" +AWS_S3_ACCESS_SECRET="test" +AWS_S3_REGION=us-east-1 -## Will be used as the base URL for media content uploaded via Strapi's UI. -## Requires an existing CNAME DNS record for the corresponding subdomain which points to AWS S3 bucket where the content is stored. -STATIC_CONTENT_BASE_URL=https://static.example.com +## The base URL is used to access static content uploaded via Strapi's UI to AWS S3: +## - on production it uses a dedicated subdomain which is linked to an eponymous AWS S3 bucket via a CNAME DNS record +## - in development it points directly to LocalStack host and is appended by the S3 bucket name in Strapi's `plugin.ts` +STATIC_CONTENT_BASE_URL=http://localhost:4566 STATIC_MEDIA_CONTENT_PATH=public/media # Point to a folder (relative to /backend/vaa-strapi) if you want to load data on initialise. This will only take place if the database contains no Election diff --git a/backend/vaa-strapi/.env.example b/backend/vaa-strapi/.env.example index fdd19c970..ede7043d3 100644 --- a/backend/vaa-strapi/.env.example +++ b/backend/vaa-strapi/.env.example @@ -20,21 +20,34 @@ PUBLIC_FRONTEND_URL="http://localhost:5173" # Used for emails linking to the fro GENERATE_MOCK_DATA_ON_INITIALISE=true # Set to true to enable mock data on an empty database GENERATE_MOCK_DATA_ON_RESTART=false # Used only in development builds -AWS_ACCESS_KEY_ID="EXAMPLE_KEY" -AWS_SECRET_ACCESS_KEY="EXAMPLE_SECRET" -AWS_REGION="eu-north-1" +## AWS LocalStack development endpoint +LOCALSTACK_ENDPOINT=http://127.0.0.1:4566 + +## AWS SES Settings + +## These settings correspond to LocalStack system defaults +## https://docs.localstack.cloud/references/configuration/ +AWS_ACCESS_KEY_ID="test" +AWS_SECRET_ACCESS_KEY="test" +AWS_REGION=us-east-1 + # `MAIL_FROM` and `MAIL_REPLY_TO` variables will not take effect for emails sent by `user-permissions` Strapi plugin # (f.e. reset password emails) as they are configured separately via Strapi UI in `Settings > Email Templates`. -MAIL_FROM="OpenVAA Voting Advice Application " -MAIL_REPLY_TO="OpenVAA Admin " - -## AWS S3 settings for static content storage -AWS_S3_ACCESS_KEY_ID="EXAMPLE_KEY" -AWS_S3_ACCESS_SECRET="EXAMPLE_SECRET" -AWS_S3_REGION=eu-north-1 -AWS_S3_BUCKET=static-example-com - -## Will be used as the base URL for media content uploaded via Strapi's UI. -## Requires an existing CNAME DNS record for the corresponding subdomain which points to AWS S3 bucket where the content is stored. -STATIC_CONTENT_BASE_URL=https://static.example.com +MAIL_FROM="no-reply@example.com" +MAIL_REPLY_TO="contact@example.com" + +## AWS S3 settings + +AWS_S3_BUCKET=static.example.com + +## These settings correspond to LocalStack system defaults +## https://docs.localstack.cloud/references/configuration/ +AWS_S3_ACCESS_KEY_ID="test" +AWS_S3_ACCESS_SECRET="test" +AWS_S3_REGION=us-east-1 + +## The base URL is used to access static content uploaded via Strapi's UI to AWS S3: +## - on production it uses a dedicated subdomain which is linked to an eponymous AWS S3 bucket via a CNAME DNS record +## - in development it points directly to LocalStack host and is appended by the S3 bucket name in Strapi's `plugin.ts` +STATIC_CONTENT_BASE_URL=http://localhost:4566 STATIC_MEDIA_CONTENT_PATH=public/media diff --git a/backend/vaa-strapi/config/env/development/plugins.ts b/backend/vaa-strapi/config/env/development/plugins.ts deleted file mode 100644 index dd0c8ebd0..000000000 --- a/backend/vaa-strapi/config/env/development/plugins.ts +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = ({env}) => ({ - email: { - config: { - // Use the bundled maildev instance part of docker-compose - provider: 'nodemailer', - providerOptions: { - host: 'maildev', - port: 1025, - ignoreTLS: true - } - } - }, - upload: { - config: { - provider: 'local', - providerOptions: { - sizeLimit: 100000 - } - } - } -}); diff --git a/backend/vaa-strapi/config/middlewares.ts b/backend/vaa-strapi/config/middlewares.ts index 2884fcc44..13b059400 100644 --- a/backend/vaa-strapi/config/middlewares.ts +++ b/backend/vaa-strapi/config/middlewares.ts @@ -1,7 +1,36 @@ -module.exports = [ +module.exports = ({env}) => [ 'strapi::logger', 'strapi::errors', - 'strapi::security', + /* + * To enable AWS S3 support: + * https://github.com/strapi/strapi/tree/main/packages/providers/upload-aws-s3 + **/ + { + name: 'strapi::security', + config: { + contentSecurityPolicy: { + useDefaults: true, + directives: { + 'connect-src': ["'self'", env('NODE_ENV') === 'development' ? 'http:' : 'https:'], + 'img-src': [ + "'self'", + 'data:', + 'blob:', + 'market-assets.strapi.io', + `${env('STATIC_CONTENT_BASE_URL')}` + ], + 'media-src': [ + "'self'", + 'data:', + 'blob:', + 'market-assets.strapi.io', + `${env('STATIC_CONTENT_BASE_URL')}` + ], + upgradeInsecureRequests: null + } + } + } + }, 'strapi::cors', 'strapi::poweredBy', 'strapi::query', diff --git a/backend/vaa-strapi/config/plugins.ts b/backend/vaa-strapi/config/plugins.ts index 8888e14a5..bb8024713 100644 --- a/backend/vaa-strapi/config/plugins.ts +++ b/backend/vaa-strapi/config/plugins.ts @@ -1,29 +1,8 @@ const aws = require('@aws-sdk/client-ses'); export default ({env}) => { - /** - * Strapi initilises nodemailer transporter only once using main plugin config. - * It allows to override only transporter specific (SMPT or SES) settings using [env]/plugins.ts, not the transporter type itself. - * So we have to make sure that for development we initilise the transporter as SMTP to be able to use maildev. - */ - const emailProviderOptions = - env('NODE_ENV') === 'development' - ? undefined - : { - SES: { - ses: new aws.SES({ - apiVersion: '2010-12-01', - region: env('AWS_REGION'), - credentials: { - accessKeyId: env('AWS_ACCESS_KEY_ID'), - secretAccessKey: env('AWS_SECRET_ACCESS_KEY') - } - }), - aws - }, - // max 14 messages per second to comply with AWS SES - sendingRate: 14 - }; + const isDev = env('NODE_ENV') === 'development'; + return { 'users-permissions': { config: { @@ -39,7 +18,20 @@ export default ({env}) => { config: { provider: 'nodemailer', providerOptions: { - ...emailProviderOptions + SES: { + ses: new aws.SES({ + endpoint: env('LOCALSTACK_ENDPOINT'), + apiVersion: '2010-12-01', + region: env('AWS_REGION'), + credentials: { + accessKeyId: env('AWS_ACCESS_KEY_ID'), + secretAccessKey: env('AWS_SECRET_ACCESS_KEY') + } + }), + aws + }, + // max 14 messages per second to comply with AWS SES + sendingRate: 14 }, settings: { defaultFrom: env('MAIL_FROM'), @@ -51,9 +43,25 @@ export default ({env}) => { config: { provider: 'aws-s3', providerOptions: { - baseUrl: env('STATIC_CONTENT_BASE_URL'), + /* + * The base URL on production uses a dedicated subdomain which is linked to AWS S3 bucket via CNAME DNS record. + **/ + baseUrl: isDev + ? `${env('STATIC_CONTENT_BASE_URL')}/${env('AWS_S3_BUCKET')}` + : env('STATIC_CONTENT_BASE_URL'), rootPath: env('STATIC_MEDIA_CONTENT_PATH'), s3Options: { + /* + * In development we use local AWS - LocalStack. + **/ + endpoint: isDev ? env('LOCALSTACK_ENDPOINT') : undefined, + /* + * In development we want to use "Path" style S3 URLs, since + * Docker services run locally are unable to resolve "Virtual-Hosted" style S3 URLs. + * https://docs.localstack.cloud/user-guide/aws/s3/#path-style-and-virtual-hosted-style-requests + * + **/ + forcePathStyle: isDev, credentials: { accessKeyId: env('AWS_S3_ACCESS_KEY_ID'), secretAccessKey: env('AWS_S3_ACCESS_SECRET') diff --git a/backend/vaa-strapi/docker-compose.dev.yml b/backend/vaa-strapi/docker-compose.dev.yml index 0c46db1a9..9e4bf6277 100644 --- a/backend/vaa-strapi/docker-compose.dev.yml +++ b/backend/vaa-strapi/docker-compose.dev.yml @@ -1,4 +1,26 @@ services: + # LocalStack configuration: https://docs.localstack.cloud/references/configuration/ + awslocal: + image: localstack/localstack + ports: + - '127.0.0.1:4566:4566' + environment: + DEBUG: 1 + AWS_REGION: ${AWS_REGION} + MAIL_FROM: ${MAIL_FROM} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_ACCESS_KEY_ID: ${AWS_S3_ACCESS_KEY_ID} + AWS_S3_ACCESS_SECRET: ${AWS_S3_ACCESS_SECRET} + AWS_S3_REGION: ${AWS_S3_REGION} + AWS_S3_BUCKET: ${AWS_S3_BUCKET} + volumes: + - awslocal:/var/lib/localstack + - ./localstack-init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh + - ./localstack-s3-cors-policy.json:/etc/localstack/s3-cors-policy.json + # Mounting the Docker socket /var/run/docker.sock as a volume is required for some services + # that use Docker to provide the emulation, such as AWS Lambda. + - '/var/run/docker.sock:/var/run/docker.sock' strapi: build: # Context includes the root of the repository so that we can expose shared/ module. @@ -7,6 +29,8 @@ services: target: development ports: - ${STRAPI_PORT}:${STRAPI_PORT} + depends_on: + - 'awslocal' restart: always environment: STRAPI_HOST: ${STRAPI_HOST} @@ -36,6 +60,7 @@ services: AWS_S3_BUCKET: ${AWS_S3_BUCKET} STATIC_CONTENT_BASE_URL: ${STATIC_CONTENT_BASE_URL} STATIC_MEDIA_CONTENT_PATH: ${STATIC_MEDIA_CONTENT_PATH} + LOCALSTACK_ENDPOINT: http://awslocal:4566 volumes: # Creating this volume will make Docker update changes from local automatically # Note that this only applies to src/, where all the source code is anyway. @@ -68,3 +93,4 @@ services: volumes: postgres: strapi-uploads: + awslocal: diff --git a/backend/vaa-strapi/localstack-init-aws.sh b/backend/vaa-strapi/localstack-init-aws.sh new file mode 100755 index 000000000..404466ca9 --- /dev/null +++ b/backend/vaa-strapi/localstack-init-aws.sh @@ -0,0 +1,6 @@ +#!/bin/bash +AWS_ACCESS_KEY_ID=$AWS_S3_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=$AWS_S3_ACCESS_SECRET awslocal s3api create-bucket --bucket $AWS_S3_BUCKET --region $AWS_S3_REGION + +AWS_ACCESS_KEY_ID=$AWS_S3_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=$AWS_S3_ACCESS_SECRET awslocal s3api put-bucket-cors --bucket $AWS_S3_BUCKET --region $AWS_S3_REGION --cors-configuration file:///etc/localstack/s3-cors-policy.json + +AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY awslocal ses verify-email-identity --email-address $MAIL_FROM --region $AWS_REGION diff --git a/backend/vaa-strapi/localstack-s3-cors-policy.json b/backend/vaa-strapi/localstack-s3-cors-policy.json new file mode 100755 index 000000000..aa62733a1 --- /dev/null +++ b/backend/vaa-strapi/localstack-s3-cors-policy.json @@ -0,0 +1,11 @@ +{ + "CORSRules": [ + { + "AllowedHeaders": ["*"], + "AllowedMethods": ["GET", "HEAD"], + "AllowedOrigins": ["*"], + "ExposeHeaders": [], + "MaxAgeSeconds": 3000 + } + ] +} diff --git a/backend/vaa-strapi/package.json b/backend/vaa-strapi/package.json index cfec29977..ea1d46b12 100644 --- a/backend/vaa-strapi/package.json +++ b/backend/vaa-strapi/package.json @@ -25,7 +25,7 @@ "@strapi/plugin-documentation": "^4.25.1", "@strapi/plugin-i18n": "^4.25.1", "@strapi/plugin-users-permissions": "^4.25.1", - "@strapi/provider-email-nodemailer": "^4.25.1", + "@strapi/provider-email-nodemailer": "^5.0.2", "@strapi/provider-upload-aws-s3": "^5.0.1", "@strapi/strapi": "^4.25.1", "pg": "^8.12.0", diff --git a/backend/vaa-strapi/src/extensions/documentation/documentation/1.0.0/full_documentation.json b/backend/vaa-strapi/src/extensions/documentation/documentation/1.0.0/full_documentation.json index 3bbf4083a..a4c7208ef 100644 --- a/backend/vaa-strapi/src/extensions/documentation/documentation/1.0.0/full_documentation.json +++ b/backend/vaa-strapi/src/extensions/documentation/documentation/1.0.0/full_documentation.json @@ -14,7 +14,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-generation-date": "2024-09-10T08:41:53.329Z" + "x-generation-date": "2024-10-08T22:09:53.144Z" }, "x-strapi-config": { "path": "/documentation", diff --git a/backend/vaa-strapi/types/generated/components.d.ts b/backend/vaa-strapi/types/generated/components.d.ts index e9b352e03..942d2f686 100644 --- a/backend/vaa-strapi/types/generated/components.d.ts +++ b/backend/vaa-strapi/types/generated/components.d.ts @@ -1,61 +1,244 @@ import type {Schema, Attribute} from '@strapi/strapi'; -export interface LabelsActionLabels extends Schema.Component { - collectionName: 'components_labels_action_labels'; +export interface SettingsSurvey extends Schema.Component { + collectionName: 'components_settings_surveys'; info: { - displayName: 'ActionLabels'; + displayName: 'Survey'; description: ''; }; attributes: { - startButton: Attribute.String & + linkTemplate: Attribute.String & Attribute.Required; + showIn: Attribute.JSON & Attribute.Required & - Attribute.DefaultTo<'Start Finding Candidates!'>; - electionInfo: Attribute.Text & - Attribute.Required & - Attribute.DefaultTo<'Information About the Elections'>; - howItWorks: Attribute.Text & - Attribute.Required & - Attribute.DefaultTo<'How Does This App Work?'>; - help: Attribute.String & Attribute.Required & Attribute.DefaultTo<'Help'>; - startQuestions: Attribute.String & + Attribute.CustomField< + 'plugin::multi-select.multi-select', + ['frontpage', 'entityDetails', 'navigation', 'resultsPopup'] + >; + }; +} + +export interface SettingsResults extends Schema.Component { + collectionName: 'components_settings_results'; + info: { + displayName: 'Results'; + description: ''; + }; + attributes: { + sections: Attribute.JSON & Attribute.Required & - Attribute.DefaultTo<'Start the Questionnaire'>; - home: Attribute.String & Attribute.Required & Attribute.DefaultTo<'home'>; - opinions: Attribute.String & Attribute.Required & Attribute.DefaultTo<'Opinions'>; - results: Attribute.String & Attribute.Required & Attribute.DefaultTo<'Results'>; - yourList: Attribute.String & Attribute.Required & Attribute.DefaultTo<'Your List'>; + Attribute.CustomField<'plugin::multi-select.multi-select', ['candidate', 'party']>; + showFeedbackPopup: Attribute.Integer; + showSurveyPopup: Attribute.Integer; + candidateCardContents: Attribute.Component<'settings.results-candidate-card-contents', true> & + Attribute.Required; + partyCardContents: Attribute.Component<'settings.results-party-card-contents', true> & + Attribute.Required; + }; +} + +export interface SettingsResultsPartyCardContents extends Schema.Component { + collectionName: 'components_settings_results_party_card_contents'; + info: { + displayName: 'Results - Candidate Card Contents'; + }; + attributes: { + content: Attribute.Enumeration<['submatches', 'candidates', 'question']> & Attribute.Required; + question_id: Attribute.String; + question_hideLabel: Attribute.Boolean; + question_format: Attribute.Enumeration<['default', 'tag']>; + }; +} + +export interface SettingsResultsCandidateCardContents extends Schema.Component { + collectionName: 'components_settings_results_candidate_card_contents'; + info: { + displayName: 'Results - Candidate Card Contents'; + }; + attributes: { + content: Attribute.Enumeration<['submatches', 'question']> & Attribute.Required; + question_id: Attribute.String; + question_hideLabel: Attribute.Boolean; + question_format: Attribute.Enumeration<['default', 'tag']>; + }; +} + +export interface SettingsQuestions extends Schema.Component { + collectionName: 'components_settings_questions'; + info: { + displayName: 'Questions'; + description: ''; + }; + attributes: { + categoryIntros: Attribute.Component<'settings.questions-category-intros'>; + questionsIntro: Attribute.Component<'settings.questions-intro'> & Attribute.Required; + showCategoryTags: Attribute.Boolean & Attribute.Required; + showResultsLink: Attribute.Boolean; + }; +} + +export interface SettingsQuestionsIntro extends Schema.Component { + collectionName: 'components_settings_questions_intros'; + info: { + displayName: 'Questions - Intro'; + description: ''; + }; + attributes: { + allowCategorySelection: Attribute.Boolean; + show: Attribute.Boolean & Attribute.Required; + }; +} + +export interface SettingsQuestionsCategoryIntros extends Schema.Component { + collectionName: 'components_settings_questions_category_intros'; + info: { + displayName: 'Questions - Category Intros'; + description: ''; + }; + attributes: { + allowSkip: Attribute.Boolean; + show: Attribute.Boolean & Attribute.Required; + }; +} + +export interface SettingsMatching extends Schema.Component { + collectionName: 'components_settings_matchings'; + info: { + displayName: 'Matching'; + description: ''; + }; + attributes: { + minimumAnswers: Attribute.Integer & Attribute.Required; + partyMatching: Attribute.Enumeration<['none', 'answersOnly', 'mean', 'median']> & + Attribute.Required; }; } -export interface LabelsViewTexts extends Schema.Component { - collectionName: 'components_labels_view_texts'; +export interface SettingsHeader extends Schema.Component { + collectionName: 'components_settings_headers'; info: { - displayName: 'ViewTexts'; + displayName: 'Header'; description: ''; }; attributes: { - publishedBy: Attribute.String & + showFeedback: Attribute.Boolean & Attribute.Required; + showHelp: Attribute.Boolean & Attribute.Required; + }; +} + +export interface SettingsEntityDetails extends Schema.Component { + collectionName: 'components_settings_entity_details'; + info: { + displayName: 'Entity Details'; + description: ''; + }; + attributes: { + contents: Attribute.Component<'settings.entity-details-contents'> & Attribute.Required; + showMissingElectionSymbol: Attribute.Component<'settings.entity-details-show-missing-elsmbl'> & + Attribute.Required; + showMissingAnswers: Attribute.Component<'settings.entity-details-show-missing-answers'> & + Attribute.Required; + }; +} + +export interface SettingsEntityDetailsShowMissingElsmbl extends Schema.Component { + collectionName: 'components_settings_entity_details_show_missing_elsmbls'; + info: { + displayName: 'Entity Details - Show Missing Election Symbol'; + description: ''; + }; + attributes: { + candidate: Attribute.Boolean & Attribute.Required; + party: Attribute.Boolean & Attribute.Required; + }; +} + +export interface SettingsEntityDetailsShowMissingAnswers extends Schema.Component { + collectionName: 'components_settings_entity_details_show_missing_answers'; + info: { + displayName: 'Entity Details - Show Missing Answers'; + description: ''; + }; + attributes: { + candidate: Attribute.Boolean & Attribute.Required; + party: Attribute.Boolean & Attribute.Required; + }; +} + +export interface SettingsEntityDetailsContents extends Schema.Component { + collectionName: 'components_settings_entity_details_contents'; + info: { + displayName: 'Entity Details - Contents'; + description: ''; + }; + attributes: { + candidate: Attribute.JSON & Attribute.Required & - Attribute.DefaultTo<'Published by {publisher}'>; - madeWith: Attribute.String & Attribute.Required & Attribute.DefaultTo<'Made with '>; - yourOpinionsTitle: Attribute.String & + Attribute.CustomField<'plugin::multi-select.multi-select', ['info', 'opinions']>; + party: Attribute.JSON & Attribute.Required & - Attribute.DefaultTo<'Tell Your Opinions'>; - questionsTip: Attribute.Text & - Attribute.DefaultTo<'Tip: If you don\u2019t care about an issue, you can skip it.'>; - appTitle: Attribute.String & Attribute.Required & Attribute.DefaultTo<'Election Compass'>; - frontpageIngress: Attribute.Text & - Attribute.DefaultTo<'With this application you can compare candidates in the elections on {electionDate, date, ::yyyyMMdd} based on their opinions, parties and other data.'>; - yourOpinionsIngress: Attribute.Text & - Attribute.DefaultTo<'Next, the app will ask your opinions on {numStatements} statements about political issues and values, which the candidates have also answered. After you\u2019ve answered them, the app will find the candidates that best agree with your opinions. The statements are grouped into {numCategories} categories. You can answer all of them or only select those you find important.'>; + Attribute.CustomField< + 'plugin::multi-select.multi-select', + ['candidates', 'info', 'opinions'] + >; + }; +} + +export interface CustomizationTranslationOverride extends Schema.Component { + collectionName: 'components_customization_translation_overrides'; + info: { + displayName: 'Translation Override'; + description: ''; + }; + attributes: { + translationKey: Attribute.String & Attribute.Required; + translations: Attribute.Component<'customization.translation-override-translation', true>; + }; +} + +export interface CustomizationTranslationOverrideTranslation extends Schema.Component { + collectionName: 'components_customization_translation_override_translations'; + info: { + displayName: 'Translation Override - Translation'; + description: ''; + }; + attributes: { + locale: Attribute.String & Attribute.Required; + translation: Attribute.Text; + }; +} + +export interface CustomizationCandidateAppFaq extends Schema.Component { + collectionName: 'components_customization_candidate_app_faqs'; + info: { + displayName: 'Candidate App FAQ'; + description: ''; + }; + attributes: { + question: Attribute.Text & Attribute.Required; + answer: Attribute.Text & Attribute.Required; + locale: Attribute.String & Attribute.Required; }; } declare module '@strapi/types' { export module Shared { export interface Components { - 'labels.action-labels': LabelsActionLabels; - 'labels.view-texts': LabelsViewTexts; + 'settings.survey': SettingsSurvey; + 'settings.results': SettingsResults; + 'settings.results-party-card-contents': SettingsResultsPartyCardContents; + 'settings.results-candidate-card-contents': SettingsResultsCandidateCardContents; + 'settings.questions': SettingsQuestions; + 'settings.questions-intro': SettingsQuestionsIntro; + 'settings.questions-category-intros': SettingsQuestionsCategoryIntros; + 'settings.matching': SettingsMatching; + 'settings.header': SettingsHeader; + 'settings.entity-details': SettingsEntityDetails; + 'settings.entity-details-show-missing-elsmbl': SettingsEntityDetailsShowMissingElsmbl; + 'settings.entity-details-show-missing-answers': SettingsEntityDetailsShowMissingAnswers; + 'settings.entity-details-contents': SettingsEntityDetailsContents; + 'customization.translation-override': CustomizationTranslationOverride; + 'customization.translation-override-translation': CustomizationTranslationOverrideTranslation; + 'customization.candidate-app-faq': CustomizationCandidateAppFaq; } } } diff --git a/backend/vaa-strapi/types/generated/contentTypes.d.ts b/backend/vaa-strapi/types/generated/contentTypes.d.ts index bcdf0bed2..24c60e2c2 100644 --- a/backend/vaa-strapi/types/generated/contentTypes.d.ts +++ b/backend/vaa-strapi/types/generated/contentTypes.d.ts @@ -485,45 +485,6 @@ export interface PluginContentReleasesReleaseAction extends Schema.CollectionTyp }; } -export interface PluginI18NLocale extends Schema.CollectionType { - collectionName: 'i18n_locale'; - info: { - singularName: 'locale'; - pluralName: 'locales'; - collectionName: 'locales'; - displayName: 'Locale'; - description: ''; - }; - options: { - draftAndPublish: false; - }; - pluginOptions: { - 'content-manager': { - visible: false; - }; - 'content-type-builder': { - visible: false; - }; - }; - attributes: { - name: Attribute.String & - Attribute.SetMinMax< - { - min: 1; - max: 50; - }, - number - >; - code: Attribute.String & Attribute.Unique; - createdAt: Attribute.DateTime; - updatedAt: Attribute.DateTime; - createdBy: Attribute.Relation<'plugin::i18n.locale', 'oneToOne', 'admin::user'> & - Attribute.Private; - updatedBy: Attribute.Relation<'plugin::i18n.locale', 'oneToOne', 'admin::user'> & - Attribute.Private; - }; -} - export interface PluginUsersPermissionsPermission extends Schema.CollectionType { collectionName: 'up_permissions'; info: { @@ -662,6 +623,45 @@ export interface PluginUsersPermissionsUser extends Schema.CollectionType { }; } +export interface PluginI18NLocale extends Schema.CollectionType { + collectionName: 'i18n_locale'; + info: { + singularName: 'locale'; + pluralName: 'locales'; + collectionName: 'locales'; + displayName: 'Locale'; + description: ''; + }; + options: { + draftAndPublish: false; + }; + pluginOptions: { + 'content-manager': { + visible: false; + }; + 'content-type-builder': { + visible: false; + }; + }; + attributes: { + name: Attribute.String & + Attribute.SetMinMax< + { + min: 1; + max: 50; + }, + number + >; + code: Attribute.String & Attribute.Unique; + createdAt: Attribute.DateTime; + updatedAt: Attribute.DateTime; + createdBy: Attribute.Relation<'plugin::i18n.locale', 'oneToOne', 'admin::user'> & + Attribute.Private; + updatedBy: Attribute.Relation<'plugin::i18n.locale', 'oneToOne', 'admin::user'> & + Attribute.Private; + }; +} + export interface ApiAnswerAnswer extends Schema.CollectionType { collectionName: 'answers'; info: { @@ -689,6 +689,42 @@ export interface ApiAnswerAnswer extends Schema.CollectionType { }; } +export interface ApiAppCustomizationAppCustomization extends Schema.SingleType { + collectionName: 'app_customizations'; + info: { + singularName: 'app-customization'; + pluralName: 'app-customizations'; + displayName: 'App Customization'; + description: ''; + }; + options: { + draftAndPublish: false; + }; + attributes: { + publisherName: Attribute.JSON; + publisherLogo: Attribute.Media<'images'>; + publisherLogoDark: Attribute.Media<'images'>; + poster: Attribute.Media<'images'>; + posterCandidateApp: Attribute.Media<'images'>; + candidateAppFAQ: Attribute.Component<'customization.candidate-app-faq', true>; + translationOverrides: Attribute.Component<'customization.translation-override', true>; + createdAt: Attribute.DateTime; + updatedAt: Attribute.DateTime; + createdBy: Attribute.Relation< + 'api::app-customization.app-customization', + 'oneToOne', + 'admin::user' + > & + Attribute.Private; + updatedBy: Attribute.Relation< + 'api::app-customization.app-customization', + 'oneToOne', + 'admin::user' + > & + Attribute.Private; + }; +} + export interface ApiAppSettingAppSetting extends Schema.CollectionType { collectionName: 'app_settings'; info: { @@ -701,14 +737,14 @@ export interface ApiAppSettingAppSetting extends Schema.CollectionType { draftAndPublish: false; }; attributes: { - publisherName: Attribute.JSON; - publisherLogo: Attribute.Media; - publisherLogoDark: Attribute.Media; - customData: Attribute.JSON; - poster: Attribute.Media; - posterCandidateApp: Attribute.Media; underMaintenance: Attribute.Boolean & Attribute.DefaultTo; allowOverwrite: Attribute.Boolean & Attribute.DefaultTo; + header: Attribute.Component<'settings.header'> & Attribute.Required; + matching: Attribute.Component<'settings.matching'> & Attribute.Required; + survey: Attribute.Component<'settings.survey'>; + entityDetails: Attribute.Component<'settings.entity-details'> & Attribute.Required; + questions: Attribute.Component<'settings.questions'> & Attribute.Required; + results: Attribute.Component<'settings.results'> & Attribute.Required; createdAt: Attribute.DateTime; updatedAt: Attribute.DateTime; createdBy: Attribute.Relation<'api::app-setting.app-setting', 'oneToOne', 'admin::user'> & @@ -740,8 +776,7 @@ export interface ApiCandidateCandidate extends Schema.CollectionType { Attribute.Private; firstName: Attribute.String & Attribute.Required; lastName: Attribute.String & Attribute.Required; - photo: Attribute.Media; - manifesto: Attribute.JSON; + photo: Attribute.Media<'images'>; party: Attribute.Relation<'api::candidate.candidate', 'manyToOne', 'api::party.party'>; answers: Attribute.Relation<'api::candidate.candidate', 'oneToMany', 'api::answer.answer'>; nomination: Attribute.Relation< @@ -815,11 +850,6 @@ export interface ApiElectionElection extends Schema.CollectionType { electionStartDate: Attribute.Date & Attribute.Required; electionDate: Attribute.Date & Attribute.Required; electionType: Attribute.Enumeration<['local', 'presidential', 'congress']>; - electionAppLabel: Attribute.Relation< - 'api::election.election', - 'manyToOne', - 'api::election-app-label.election-app-label' - >; constituencies: Attribute.Relation< 'api::election.election', 'manyToMany', @@ -850,64 +880,6 @@ export interface ApiElectionElection extends Schema.CollectionType { }; } -export interface ApiElectionAppLabelElectionAppLabel extends Schema.CollectionType { - collectionName: 'election_app_label'; - info: { - singularName: 'election-app-label'; - pluralName: 'election-app-labels'; - displayName: 'Election App Labels'; - description: ''; - }; - options: { - draftAndPublish: true; - }; - pluginOptions: { - i18n: { - localized: true; - }; - }; - attributes: { - elections: Attribute.Relation< - 'api::election-app-label.election-app-label', - 'oneToMany', - 'api::election.election' - >; - actionLabels: Attribute.Component<'labels.action-labels'> & - Attribute.SetPluginOptions<{ - i18n: { - localized: true; - }; - }>; - viewTexts: Attribute.Component<'labels.view-texts'> & - Attribute.SetPluginOptions<{ - i18n: { - localized: true; - }; - }>; - createdAt: Attribute.DateTime; - updatedAt: Attribute.DateTime; - publishedAt: Attribute.DateTime; - createdBy: Attribute.Relation< - 'api::election-app-label.election-app-label', - 'oneToOne', - 'admin::user' - > & - Attribute.Private; - updatedBy: Attribute.Relation< - 'api::election-app-label.election-app-label', - 'oneToOne', - 'admin::user' - > & - Attribute.Private; - localizations: Attribute.Relation< - 'api::election-app-label.election-app-label', - 'oneToMany', - 'api::election-app-label.election-app-label' - >; - locale: Attribute.String; - }; -} - export interface ApiFeedbackFeedback extends Schema.CollectionType { collectionName: 'feedbacks'; info: { @@ -1018,7 +990,7 @@ export interface ApiPartyParty extends Schema.CollectionType { draftAndPublish: true; }; attributes: { - logo: Attribute.Media; + logo: Attribute.Media<'images'>; candidates: Attribute.Relation<'api::party.party', 'oneToMany', 'api::candidate.candidate'>; answers: Attribute.Relation<'api::party.party', 'oneToMany', 'api::answer.answer'>; nominations: Attribute.Relation<'api::party.party', 'oneToMany', 'api::nomination.nomination'>; @@ -1068,6 +1040,7 @@ export interface ApiQuestionQuestion extends Schema.CollectionType { filterable: Attribute.Boolean & Attribute.DefaultTo; customData: Attribute.JSON; order: Attribute.Integer & Attribute.DefaultTo<0>; + required: Attribute.Boolean & Attribute.DefaultTo; entityType: Attribute.Enumeration<['all', 'candidate', 'party']> & Attribute.DefaultTo<'all'>; createdAt: Attribute.DateTime; updatedAt: Attribute.DateTime; @@ -1092,7 +1065,7 @@ export interface ApiQuestionCategoryQuestionCategory extends Schema.CollectionTy }; attributes: { order: Attribute.Integer & Attribute.Required & Attribute.DefaultTo<0>; - elections: Attribute.Relation< + election: Attribute.Relation< 'api::question-category.question-category', 'manyToOne', 'api::election.election' @@ -1171,16 +1144,16 @@ declare module '@strapi/types' { 'plugin::upload.folder': PluginUploadFolder; 'plugin::content-releases.release': PluginContentReleasesRelease; 'plugin::content-releases.release-action': PluginContentReleasesReleaseAction; - 'plugin::i18n.locale': PluginI18NLocale; 'plugin::users-permissions.permission': PluginUsersPermissionsPermission; 'plugin::users-permissions.role': PluginUsersPermissionsRole; 'plugin::users-permissions.user': PluginUsersPermissionsUser; + 'plugin::i18n.locale': PluginI18NLocale; 'api::answer.answer': ApiAnswerAnswer; + 'api::app-customization.app-customization': ApiAppCustomizationAppCustomization; 'api::app-setting.app-setting': ApiAppSettingAppSetting; 'api::candidate.candidate': ApiCandidateCandidate; 'api::constituency.constituency': ApiConstituencyConstituency; 'api::election.election': ApiElectionElection; - 'api::election-app-label.election-app-label': ApiElectionAppLabelElectionAppLabel; 'api::feedback.feedback': ApiFeedbackFeedback; 'api::language.language': ApiLanguageLanguage; 'api::nomination.nomination': ApiNominationNomination; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c0d264e4d..f3d9bdb27 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,10 +8,26 @@ services: environment: - VITE_BACKEND_URL=${VITE_BACKEND_URL} - PUBLIC_BACKEND_URL=${PUBLIC_BACKEND_URL} + awslocal: + extends: + file: ./backend/vaa-strapi/docker-compose.dev.yml + service: awslocal + environment: + DEBUG: 1 + AWS_REGION: ${AWS_REGION} + MAIL_FROM: ${MAIL_FROM} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_ACCESS_KEY_ID: ${AWS_S3_ACCESS_KEY_ID} + AWS_S3_ACCESS_SECRET: ${AWS_S3_ACCESS_SECRET} + AWS_S3_REGION: ${AWS_S3_REGION} + AWS_S3_BUCKET: ${AWS_S3_BUCKET} strapi: extends: file: ./backend/vaa-strapi/docker-compose.dev.yml service: strapi + depends_on: + - "awslocal" environment: STRAPI_HOST: ${STRAPI_HOST} STRAPI_PORT: ${STRAPI_PORT} @@ -40,6 +56,7 @@ services: AWS_S3_BUCKET: ${AWS_S3_BUCKET} STATIC_CONTENT_BASE_URL: ${STATIC_CONTENT_BASE_URL} STATIC_MEDIA_CONTENT_PATH: ${STATIC_MEDIA_CONTENT_PATH} + LOCALSTACK_ENDPOINT: http://awslocal:4566 postgres: extends: file: ./backend/vaa-strapi/docker-compose.dev.yml @@ -48,20 +65,8 @@ services: POSTGRES_DB: ${DATABASE_NAME} POSTGRES_USER: ${DATABASE_USERNAME} POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - maildev: - image: maildev/maildev:2.1.0 - ports: - - "${MAILDEV_PORT}:${MAILDEV_PORT}" - environment: - - MAILDEV_IP=0.0.0.0 - - MAILDEV_WEB_PORT=${MAILDEV_PORT} - healthcheck: - test: wget --no-verbose --tries=1 --spider http://127.0.0.1:${MAILDEV_PORT}/healthz || exit 1 - interval: 30s - timeout: 30s - retries: 3 - start_period: 15s volumes: postgres: strapi-uploads: + awslocal: diff --git a/docs/candidate-app/candidates.md b/docs/candidate-app/candidates.md index 73b74c560..1936fac60 100644 --- a/docs/candidate-app/candidates.md +++ b/docs/candidate-app/candidates.md @@ -35,7 +35,7 @@ The user gets an email with a link to reset their password using the forgot pass `MAIL_FROM` and `MAIL_REPLY_TO` variables will not take effect for emails sent by `user-permissions` Strapi plugin (f.e. reset password emails) as they are configured separately via Strapi UI in `Settings > Email Templates`. -In a development environment, there is a local instance of [maildev](https://github.com/maildev/maildev) running so that you do not need to set up SMTP. The development configuration automatically enforces the use of maildev through the `backend/vaa-strapi/config/env/development/plugins.ts` file. The user interface of maildev can be found at [http://localhost:1080](http://localhost:1080), where you'll find any emails sent by Strapi. +You can use a local instance of AWS SES via [LocalStack](https://docs.localstack.cloud/user-guide/aws/ses/) for development. To enforce the use of LocalStack set `LOCALSTACK_ENDPOINT` to `http://localhost.localstack.cloud:4566` in `.env` file. You could use the project's Docker compose setup to spin up `awslocal` service or install and run it [yourself](https://docs.localstack.cloud/getting-started/installation/). The LocalStack's AWS SES mailbox can be checked at [http://localhost:4566/_aws/ses](http://localhost:4566/_aws/ses), where you'll find any emails sent by Strapi. ## Technical Documentation diff --git a/package.json b/package.json index 80c503e4e..c06f8aad0 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,13 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "@playwright/test": "^1.45.0", + "@types/cheerio": "^0.22.35", + "@types/mailparser": "^3.4.5", "@types/node": "^20.14.8", + "cheerio": "^1.0.0", "dotenv": "^16.4.5", - "husky": "^9.1.3" + "husky": "^9.1.3", + "mailparser": "^3.7.1" }, "workspaces": [ "frontend", diff --git a/tests/candidateApp-advanced.spec.ts b/tests/candidateApp-advanced.spec.ts index 3b14af44c..02620d0ed 100644 --- a/tests/candidateApp-advanced.spec.ts +++ b/tests/candidateApp-advanced.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, request } from "@playwright/test"; import { faker } from "@faker-js/faker"; import path from "path"; import { TRANSLATIONS as T } from './utils/translations'; @@ -6,11 +6,24 @@ import { Route } from "../frontend/src/lib/utils/navigation/route"; import mockInfoQuestions from "../backend/vaa-strapi/src/functions/mockData/mockInfoQuestions.json"; import mockQuestionTypes from "../backend/vaa-strapi/src/functions/mockData/mockQuestionTypes.json"; import mockQuestions from "../backend/vaa-strapi/src/functions/mockData/mockQuestions.json"; +import { simpleParser } from 'mailparser'; +import { load }from 'cheerio'; + +type Email = { + Id: string + Region: string + Source: string + RawData: string + Timestamp: string +} + +type Mailbox = { + messages: Email[] +} const strapiPort = process.env.STRAPI_PORT || "1337"; const strapiURL = `http://localhost:${strapiPort}`; -const maildevPort = process.env.MAILDEV_PORT || "1080"; -const maildevURL = `http://localhost:${maildevPort}/#/`; +const awsSesInboxURL = `${process.env.LOCALSTACK_ENDPOINT}/_aws/ses`; const LOCALE = 'en'; const userFirstName = faker.person.firstName(); @@ -162,18 +175,27 @@ test("should log into Strapi and import candidates", async ({ page, baseURL }) = // Wait for 5 seconds to allow the email to be sent await page.waitForTimeout(5000); - // Navigate to maildev and open registration link - await page.goto(maildevURL); - await page - .getByRole("link", { name: `Subject To: ${userEmail}` }) - .first() - .click(); - const link = await page - .frameLocator("iframe >> nth=0") - .getByRole("link", { name: `${baseURL}/${LOCALE}` }) - .getAttribute("href"); - if (!link) throw new Error("Link not found"); - await page.goto(link); + const req = await request.newContext(); + + // Fetch emails from the local AWS SES mailbox + const response = await req.fetch(awsSesInboxURL) + const emails = await response.json().then((parsed: Mailbox) => parsed.messages) + + // Get HTML from the latest email + const latestEmailData = emails.at(-1)?.RawData + const htmlContent = latestEmailData && (await simpleParser(latestEmailData)).textAsHtml + + // Extract registration link from the HTML + let registrationLink: string | undefined; + + if (htmlContent) { + const $ = load(htmlContent) + registrationLink = $($('a')[0]).attr('href'); + } + + if (!registrationLink) throw new Error("Link not found"); + + await page.goto(registrationLink); // Complete the registration process // Check that the candidate name is correct diff --git a/yarn.lock b/yarn.lock index 8c7cff7f4..eaae7f009 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3421,6 +3421,14 @@ argparse "~1.0.9" string-argv "~0.3.1" +"@selderee/plugin-htmlparser2@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517" + integrity sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ== + dependencies: + domhandler "^5.0.3" + selderee "^0.11.0" + "@sentry/core@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785" @@ -5006,10 +5014,10 @@ resolved "https://registry.yarnpkg.com/@strapi/provider-audit-logs-local/-/provider-audit-logs-local-4.25.7.tgz#fc9b0b4463c343a11476685099f0a2cadd9b98d9" integrity sha512-pvyj5jvvQxfyek4DByPC25tHg3EmDLa98c7uLTOAnG27Zon2NpE3VIMBqhB5LqoCiRE5QAopxlHt77ptffiDjg== -"@strapi/provider-email-nodemailer@^4.25.1": - version "4.25.7" - resolved "https://registry.yarnpkg.com/@strapi/provider-email-nodemailer/-/provider-email-nodemailer-4.25.7.tgz#7b359b34f42896b45e6cfe1b41a560aa011b2a6f" - integrity sha512-FCfE0nOgUsy4+FTOp7Vz5+vPBdecI/OBp+jn7wrBZCZQixcd42l6FaGLumKW2DcOqUutSb1kMIC6c8HH4Pq+Zw== +"@strapi/provider-email-nodemailer@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@strapi/provider-email-nodemailer/-/provider-email-nodemailer-5.0.2.tgz#98ac0603708a7ca153885ad4efb22a80c713aed7" + integrity sha512-qriAhWSPTB1I4r6GqbFuDUJdvHfTCqDPHkZCIH+FHxOecZI2gcDFXqvFByczTaHwFsFbE4rBr5sG+TqH6eq5rQ== dependencies: lodash "4.17.21" nodemailer "6.9.1" @@ -5445,6 +5453,13 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@types/cheerio@^0.22.35": + version "0.22.35" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.35.tgz#0d16dc1f24d426231c181b9c31847f673867595f" + integrity sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA== + dependencies: + "@types/node" "*" + "@types/connect@*": version "3.4.38" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" @@ -5634,6 +5649,14 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== +"@types/mailparser@^3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@types/mailparser/-/mailparser-3.4.5.tgz#2c8fc00cd899680a9de5c3ce2ece705cbee9b6c4" + integrity sha512-EPERBp7fLeFZh7tS2X36MF7jawUx3Y6/0rXciZah3CTYgwLi3e0kpGUJ6FOmUabgzis/U1g+3/JzrVWbWIOGjg== + dependencies: + "@types/node" "*" + iconv-lite "^0.6.3" + "@types/mime@^1": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" @@ -7189,6 +7212,23 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" +cheerio@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81" + integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.1.0" + encoding-sniffer "^0.2.0" + htmlparser2 "^9.1.0" + parse5 "^7.1.2" + parse5-htmlparser2-tree-adapter "^7.0.0" + parse5-parser-stream "^7.1.2" + undici "^6.19.5" + whatwg-mimetype "^4.0.0" + cheerio@^1.0.0-rc.12: version "1.0.0-rc.12" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" @@ -8354,7 +8394,7 @@ domutils@^2.5.2, domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" -domutils@^3.0.1: +domutils@^3.0.1, domutils@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== @@ -8482,6 +8522,24 @@ encodeurl@^1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encoding-japanese@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.0.0.tgz#fa0226e5469e7b5b69a04fea7d5481bd1fa56936" + integrity sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ== + +encoding-japanese@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.1.0.tgz#5d3c2b652c84ca563783b86907bf5cdfe9a597e2" + integrity sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w== + +encoding-sniffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5" + integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg== + dependencies: + iconv-lite "^0.6.3" + whatwg-encoding "^3.1.1" + encoding@^0.1.12: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -8509,7 +8567,7 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.2.0, entities@^4.4.0: +entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -9889,7 +9947,7 @@ hasown@^2.0.0, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -he@^1.2.0: +he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -9987,6 +10045,17 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" +html-to-text@9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.5.tgz#6149a0f618ae7a0db8085dca9bbf96d32bb8368d" + integrity sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg== + dependencies: + "@selderee/plugin-htmlparser2" "^0.11.0" + deepmerge "^4.3.1" + dom-serializer "^2.0.0" + htmlparser2 "^8.0.2" + selderee "^0.11.0" + html-webpack-plugin@5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" @@ -10008,7 +10077,7 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" -htmlparser2@^8.0.0, htmlparser2@^8.0.1: +htmlparser2@^8.0.0, htmlparser2@^8.0.1, htmlparser2@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== @@ -10018,6 +10087,16 @@ htmlparser2@^8.0.0, htmlparser2@^8.0.1: domutils "^3.0.1" entities "^4.4.0" +htmlparser2@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23" + integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.1.0" + entities "^4.5.0" + http-assert@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" @@ -10148,7 +10227,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3, iconv-lite@^0.6.2: +iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -11607,6 +11686,11 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +leac@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912" + integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -11625,6 +11709,16 @@ libbase64@0.1.0: resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-0.1.0.tgz#62351a839563ac5ff5bd26f12f60e9830bb751e6" integrity sha512-B91jifmFw1DKEqEWstSpg1PbtUbBzR4yQAPT86kCQXBtud1AJVA+Z6RSklSrqmKe4q2eiEufgnhqJKPgozzfIQ== +libbase64@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-1.2.1.tgz#fb93bf4cb6d730f29b92155b6408d1bd2176a8c8" + integrity sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew== + +libbase64@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-1.3.0.tgz#053314755a05d2e5f08bbfc48d0290e9322f4406" + integrity sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg== + libmime@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/libmime/-/libmime-2.1.0.tgz#51bc76de2283161eb9051c4bc80aed713e4fd1cd" @@ -11634,6 +11728,26 @@ libmime@2.1.0: libbase64 "0.1.0" libqp "1.1.0" +libmime@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-5.2.0.tgz#c4ed5cbd2d9fdd27534543a68bb8d17c658d51d8" + integrity sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw== + dependencies: + encoding-japanese "2.0.0" + iconv-lite "0.6.3" + libbase64 "1.2.1" + libqp "2.0.1" + +libmime@5.3.5: + version "5.3.5" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-5.3.5.tgz#acd95a32f58dab55c8a9d269c5b4509e7ad6ae31" + integrity sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg== + dependencies: + encoding-japanese "2.1.0" + iconv-lite "0.6.3" + libbase64 "1.3.0" + libqp "2.1.0" + libmime@^2.0.3: version "2.1.3" resolved "https://registry.yarnpkg.com/libmime/-/libmime-2.1.3.tgz#25017ca5ab5a1e98aadbe2725017cf1d48a42a0c" @@ -11648,6 +11762,16 @@ libqp@1.1.0: resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" integrity sha512-4Rgfa0hZpG++t1Vi2IiqXG9Ad1ig4QTmtuZF946QJP4bPqOYC78ixUXgz5TW/wE7lNaNKlplSYTxQ+fR2KZ0EA== +libqp@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/libqp/-/libqp-2.0.1.tgz#b8fed76cc1ea6c9ceff8888169e4e0de70cd5cf2" + integrity sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg== + +libqp@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/libqp/-/libqp-2.1.0.tgz#ce84bffd86b76029032093bd866d316e12a3d3f5" + integrity sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A== + liftoff@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.5.0.tgz#2009291bb31cea861bbf10a7c15a28caf75c31ec" @@ -11682,6 +11806,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + linkify-it@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" @@ -11950,6 +12081,31 @@ mailcomposer@3.12.0: buildmail "3.10.0" libmime "2.1.0" +mailparser@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/mailparser/-/mailparser-3.7.1.tgz#4d0ea2eeb50a73dd10854a71ef1d4553bdce01cb" + integrity sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw== + dependencies: + encoding-japanese "2.1.0" + he "1.2.0" + html-to-text "9.0.5" + iconv-lite "0.6.3" + libmime "5.3.5" + linkify-it "5.0.0" + mailsplit "5.4.0" + nodemailer "6.9.13" + punycode.js "2.3.1" + tlds "1.252.0" + +mailsplit@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/mailsplit/-/mailsplit-5.4.0.tgz#9f4692fadd9013e9ce632147d996931d2abac6ba" + integrity sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA== + dependencies: + libbase64 "1.2.1" + libmime "5.2.0" + libqp "2.0.1" + make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -12633,6 +12789,11 @@ nodemailer@6.9.1: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.1.tgz#8249d928a43ed85fec17b13d2870c8f758a126ed" integrity sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA== +nodemailer@6.9.13: + version "6.9.13" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.13.tgz#5b292bf1e92645f4852ca872c56a6ba6c4a3d3d6" + integrity sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA== + nodemon@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.0.2.tgz#222dd0de79fc7b7b3eedba422d2b9e5fc678621e" @@ -13110,6 +13271,13 @@ parse5-htmlparser2-tree-adapter@^7.0.0: domhandler "^5.0.2" parse5 "^7.0.0" +parse5-parser-stream@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1" + integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== + dependencies: + parse5 "^7.0.0" + parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -13117,6 +13285,14 @@ parse5@^7.0.0, parse5@^7.1.2: dependencies: entities "^4.4.0" +parseley@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef" + integrity sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw== + dependencies: + leac "^0.6.0" + peberminta "^0.9.0" + parseurl@^1.3.2: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -13263,6 +13439,11 @@ pause@0.0.1: resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== +peberminta@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352" + integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ== + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -13708,6 +13889,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode.js@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@2.3.1, punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -14539,6 +14725,13 @@ scroll-into-view-if-needed@^2.2.20: dependencies: compute-scroll-into-view "^1.0.20" +selderee@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a" + integrity sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA== + dependencies: + parseley "^0.12.0" + "semver@2 || 3 || 4 || 5": version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -15746,6 +15939,11 @@ titleize@^3.0.0: resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== +tlds@1.252.0: + version "1.252.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.252.0.tgz#71d9617f4ef4cc7347843bee72428e71b8b0f419" + integrity sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -15975,6 +16173,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== +uc.micro@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + ufo@^1.5.3: version "1.5.4" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" @@ -16017,6 +16220,11 @@ undici-types@~6.13.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.13.0.tgz#e3e79220ab8c81ed1496b5812471afd7cf075ea5" integrity sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg== +undici@^6.19.5: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" + integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"