From 0842559fb2b303ea53ce19e0e2d0848598164ee9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Aug 2023 16:14:53 +0100 Subject: [PATCH] Simplify registration with email validation (#11398) --- cypress/e2e/register/email.spec.ts | 93 +++++++++++++++++++ cypress/plugins/index.ts | 2 + cypress/plugins/mailhog/index.ts | 91 ++++++++++++++++++ cypress/plugins/synapsedocker/index.ts | 6 ++ .../synapsedocker/templates/email/README.md | 1 + .../templates/email/homeserver.yaml | 44 +++++++++ .../synapsedocker/templates/email/log.config | 50 ++++++++++ cypress/support/e2e.ts | 2 + cypress/support/homeserver.ts | 3 + cypress/support/mailhog.ts | 54 +++++++++++ cypress/support/promise.ts | 58 ++++++++++++ package.json | 1 + src/components/structures/MatrixChat.tsx | 10 -- .../structures/auth/Registration.tsx | 26 +----- .../components/structures/MatrixChat-test.tsx | 1 - .../structures/auth/Registration-test.tsx | 1 - yarn.lock | 49 ++++++---- 17 files changed, 437 insertions(+), 55 deletions(-) create mode 100644 cypress/e2e/register/email.spec.ts create mode 100644 cypress/plugins/mailhog/index.ts create mode 100644 cypress/plugins/synapsedocker/templates/email/README.md create mode 100644 cypress/plugins/synapsedocker/templates/email/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/email/log.config create mode 100644 cypress/support/mailhog.ts create mode 100644 cypress/support/promise.ts diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts new file mode 100644 index 0000000000..a93c05c6ee --- /dev/null +++ b/cypress/e2e/register/email.spec.ts @@ -0,0 +1,93 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { Mailhog } from "../../support/mailhog"; + +describe("Email Registration", () => { + let homeserver: HomeserverInstance; + let mailhog: Mailhog; + + beforeEach(() => { + cy.startMailhog().then((_mailhog) => { + mailhog = _mailhog; + cy.startHomeserver({ + template: "email", + variables: { + SMTP_HOST: "host.docker.internal", + SMTP_PORT: _mailhog.instance.smtpPort, + }, + }).then((_homeserver) => { + homeserver = _homeserver; + + cy.intercept( + { method: "GET", pathname: "/config.json" }, + { + body: { + default_server_config: { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + "m.identity_server": { + base_url: "https://server.invalid", + }, + }, + }, + }, + ); + cy.visit("/#/register"); + cy.injectAxe(); + }); + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + cy.stopMailhog(mailhog); + }); + + it("registers an account and lands on the use case selection screen", () => { + cy.findByRole("textbox", { name: "Username" }).should("be.visible"); + // Hide the server text as it contains the randomly allocated Homeserver port + const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }"; + + cy.findByRole("textbox", { name: "Username" }).type("alice"); + cy.findByPlaceholderText("Password").type("totally a great password"); + cy.findByPlaceholderText("Confirm password").type("totally a great password"); + cy.findByPlaceholderText("Email").type("alice@email.com"); + cy.findByRole("button", { name: "Register" }).click(); + + cy.findByText("Check your email to continue").should("be.visible"); + cy.percySnapshot("Registration check your email", { percyCSS }); + cy.checkA11y(); + + cy.findByText("An error was encountered when sending the email").should("not.exist"); + + cy.waitForPromise(async () => { + const messages = await mailhog.api.messages(); + expect(messages.items).to.have.length(1); + expect(messages.items[0].to).to.eq("alice@email.com"); + const [link] = messages.items[0].text.match(/http.+/); + return link; + }).as("emailLink"); + + cy.get("@emailLink").then((link) => cy.request(link)); + + cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist"); + }); +}); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index cb6d819dcd..412057cf54 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -26,6 +26,7 @@ import { webserver } from "./webserver"; import { docker } from "./docker"; import { log } from "./log"; import { oAuthServer } from "./oauth_server"; +import { mailhogDocker } from "./mailhog"; /** * @type {Cypress.PluginConfig} @@ -41,4 +42,5 @@ export default function (on: PluginEvents, config: PluginConfigOptions) { installLogsPrinter(on, { // printLogsToConsole: "always", }); + mailhogDocker(on, config); } diff --git a/cypress/plugins/mailhog/index.ts b/cypress/plugins/mailhog/index.ts new file mode 100644 index 0000000000..a156e93981 --- /dev/null +++ b/cypress/plugins/mailhog/index.ts @@ -0,0 +1,91 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; +import { getFreePort } from "../utils/port"; +import { dockerIp, dockerRun, dockerStop } from "../docker"; + +// A cypress plugins to add command to manage an instance of Mailhog in Docker + +export interface Instance { + host: string; + smtpPort: number; + httpPort: number; + containerId: string; +} + +const instances = new Map(); + +// Start a synapse instance: the template must be the name of +// one of the templates in the cypress/plugins/synapsedocker/templates +// directory +async function mailhogStart(): Promise { + const smtpPort = await getFreePort(); + const httpPort = await getFreePort(); + + console.log(`Starting mailhog...`); + + const containerId = await dockerRun({ + image: "mailhog/mailhog:latest", + containerName: `react-sdk-cypress-mailhog`, + params: ["--rm", "-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`], + }); + + console.log(`Started mailhog on ports smtp=${smtpPort} http=${httpPort}.`); + + const host = await dockerIp({ containerId }); + const instance: Instance = { smtpPort, httpPort, containerId, host }; + instances.set(containerId, instance); + return instance; +} + +async function mailhogStop(id: string): Promise { + const synCfg = instances.get(id); + + if (!synCfg) throw new Error("Unknown mailhog ID"); + + await dockerStop({ + containerId: id, + }); + + instances.delete(id); + + console.log(`Stopped mailhog id ${id}.`); + // cypress deliberately fails if you return 'undefined', so + // return null to signal all is well, and we've handled the task. + return null; +} + +/** + * @type {Cypress.PluginConfig} + */ +export function mailhogDocker(on: PluginEvents, config: PluginConfigOptions) { + on("task", { + mailhogStart, + mailhogStop, + }); + + on("after:spec", async (spec) => { + // Cleans up any remaining instances after a spec run + for (const synId of instances.keys()) { + console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); + await mailhogStop(synId); + } + }); +} diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts index 9773849e03..a802c03939 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/cypress/plugins/synapsedocker/index.ts @@ -65,6 +65,12 @@ async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise" + app_name: my_branded_matrix_server diff --git a/cypress/plugins/synapsedocker/templates/email/log.config b/cypress/plugins/synapsedocker/templates/email/log.config new file mode 100644 index 0000000000..ac232762da --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 2ff0197ba6..11eae401f9 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -40,6 +40,8 @@ import "./network"; import "./composer"; import "./proxy"; import "./axe"; +import "./mailhog"; +import "./promise"; installLogsCollector({ // specify the types of logs to collect (and report to the node console at the end of the test) diff --git a/cypress/support/homeserver.ts b/cypress/support/homeserver.ts index 15fce60350..9f5e46efb0 100644 --- a/cypress/support/homeserver.ts +++ b/cypress/support/homeserver.ts @@ -28,6 +28,9 @@ export interface StartHomeserverOpts { /** Port of an OAuth server to configure the homeserver to use */ oAuthServerPort?: number; + + /** Additional variables to inject into the configuration template **/ + variables?: Record; } declare global { diff --git a/cypress/support/mailhog.ts b/cypress/support/mailhog.ts new file mode 100644 index 0000000000..86efc5e0f6 --- /dev/null +++ b/cypress/support/mailhog.ts @@ -0,0 +1,54 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import mailhog from "mailhog"; + +import Chainable = Cypress.Chainable; +import { Instance } from "../plugins/mailhog"; + +export interface Mailhog { + api: mailhog.API; + instance: Instance; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + startMailhog(): Chainable; + stopMailhog(instance: Mailhog): Chainable; + } + } +} + +Cypress.Commands.add("startMailhog", (): Chainable => { + return cy.task("mailhogStart", { log: false }).then((x) => { + Cypress.log({ name: "startHomeserver", message: `Started mailhog instance ${x.containerId}` }); + return { + api: mailhog({ + host: "localhost", + port: x.httpPort, + }), + instance: x, + }; + }); +}); + +Cypress.Commands.add("stopMailhog", (mailhog: Mailhog): Chainable => { + return cy.task("mailhogStop", mailhog.instance.containerId); +}); diff --git a/cypress/support/promise.ts b/cypress/support/promise.ts new file mode 100644 index 0000000000..4baaf75e8e --- /dev/null +++ b/cypress/support/promise.ts @@ -0,0 +1,58 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import Chainable = Cypress.Chainable; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + /** + * Utility wrapper around promises to help control flow in tests + * Calls `fn` function `tries` times, with a sleep of `interval` between calls. + * Ensure you do not rely on any effects of calling any `cy.*` functions within the body of `fn` + * as the calls will not happen until after waitForPromise returns. + * @param fn the function to retry + * @param tries the number of tries to call it + * @param interval the time interval between tries + */ + waitForPromise(fn: () => Promise, tries?: number, interval?: number): Chainable; + } + } +} + +function waitForPromise(fn: () => Promise, tries = 10, interval = 1000): Chainable { + return cy.then( + () => + new Cypress.Promise(async (resolve, reject) => { + for (let i = 0; i < tries; i++) { + try { + const v = await fn(); + resolve(v); + } catch { + await new Cypress.Promise((resolve) => setTimeout(resolve, interval)); + } + } + reject(); + }), + ); +} + +Cypress.Commands.add("waitForPromise", waitForPromise); + +export {}; diff --git a/package.json b/package.json index 9dfbdbf53b..8c8b3b4b07 100644 --- a/package.json +++ b/package.json @@ -208,6 +208,7 @@ "jest-mock": "^29.2.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", + "mailhog": "^4.16.0", "matrix-mock-request": "^2.5.0", "matrix-web-i18n": "^1.4.0", "mocha-junit-reporter": "^2.2.0", diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 49981b781a..563e228088 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -176,8 +176,6 @@ interface IProps { initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. defaultDeviceDisplayName?: string; - // A function that makes a registration URL - makeRegistrationUrl: (params: QueryDict) => string; } interface IState { @@ -2004,13 +2002,6 @@ export default class MatrixChat extends React.PureComponent { this.setState({ serverConfig }); }; - private makeRegistrationUrl = (params: QueryDict): string => { - if (this.props.startingFragmentQueryParams?.referrer) { - params.referrer = this.props.startingFragmentQueryParams.referrer; - } - return this.props.makeRegistrationUrl(params); - }; - /** * After registration or login, we run various post-auth steps before entering the app * proper, such setting up cross-signing or verifying the new session. @@ -2121,7 +2112,6 @@ export default class MatrixChat extends React.PureComponent { idSid={this.state.register_id_sid} email={email} brand={this.props.config.brand} - makeRegistrationUrl={this.makeRegistrationUrl} onLoggedIn={this.onRegisterFlowComplete} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 2a8f89a5cd..71b34462fd 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -73,15 +73,7 @@ interface IProps { // - The user's password, if available and applicable (may be cached in memory // for a short time so the user is not required to re-enter their password // for operations like uploading cross-signing keys). - onLoggedIn(params: IMatrixClientCreds, password: string): void; - makeRegistrationUrl(params: { - /* eslint-disable camelcase */ - client_secret: string; - hs_url: string; - is_url?: string; - session_id: string; - /* eslint-enable camelcase */ - }): string; + onLoggedIn(params: IMatrixClientCreds, password: string): Promise; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; @@ -302,17 +294,7 @@ export default class Registration extends React.Component { sessionId: string, ): Promise => { if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); - return this.state.matrixClient.requestRegisterEmailToken( - emailAddress, - clientSecret, - sendAttempt, - this.props.makeRegistrationUrl({ - client_secret: clientSecret, - hs_url: this.state.matrixClient.getHomeserverUrl(), - is_url: this.state.matrixClient.getIdentityServerUrl(), - session_id: sessionId, - }), - ); + return this.state.matrixClient.requestRegisterEmailToken(emailAddress, clientSecret, sendAttempt); }; private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { @@ -401,9 +383,7 @@ export default class Registration extends React.Component { const hasAccessToken = Boolean(accessToken); debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken }); // don’t log in if we found a session for a different user - if (!hasEmail && hasAccessToken && !newState.differentLoggedInUserId) { - // we'll only try logging in if we either have no email to verify at all or we're the client that verified - // the email, not the client that started the registration flow + if (hasAccessToken && !newState.differentLoggedInUserId) { await this.props.onLoggedIn( { userId, diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 0dc2fc569d..7684b267bc 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -131,7 +131,6 @@ describe("", () => { }, onNewScreen: jest.fn(), onTokenLoginCompleted: jest.fn(), - makeRegistrationUrl: jest.fn(), realQueryParams: {}, }; const getComponent = (props: Partial> = {}) => diff --git a/test/components/structures/auth/Registration-test.tsx b/test/components/structures/auth/Registration-test.tsx index be5afff827..0123d6867e 100644 --- a/test/components/structures/auth/Registration-test.tsx +++ b/test/components/structures/auth/Registration-test.tsx @@ -75,7 +75,6 @@ describe("Registration", function () { const defaultProps = { defaultDeviceDisplayName: "test-device-display-name", - makeRegistrationUrl: jest.fn(), onLoggedIn: jest.fn(), onLoginClick: jest.fn(), onServerConfigChange: jest.fn(), diff --git a/yarn.lock b/yarn.lock index 3ed1113975..0bcf7d20a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,9 +1389,9 @@ integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g== "@cypress/request@^2.88.11": - version "2.88.11" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.11.tgz#5a4c7399bc2d7e7ed56e92ce5acb620c8b187047" - integrity sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w== + version "2.88.12" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" + integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -1408,7 +1408,7 @@ performance-now "^2.1.0" qs "~6.10.3" safe-buffer "^5.1.2" - tough-cookie "~2.5.0" + tough-cookie "^4.1.3" tunnel-agent "^0.6.0" uuid "^8.3.2" @@ -2615,16 +2615,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== -"@types/node@^14.14.31": - version "14.18.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.54.tgz#fc304bd66419030141fa997dc5a9e0e374029ae8" - integrity sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw== - "@types/node@^16": version "16.18.39" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.39.tgz#aa39a1a87a40ef6098ee69689a1acb0c1b034832" integrity sha512-8q9ZexmdYYyc5/cfujaXb4YOucpQxAV4RMG0himLyDUOEr8Mr79VrqsFI+cQ2M2h89YIuy95lbxuYjxT4Hk4kQ== +"@types/node@^16.18.39": + version "16.18.40" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.40.tgz#968d64746d20cac747a18ca982c0f1fe518c031c" + integrity sha512-+yno3ItTEwGxXiS/75Q/aHaa5srkpnJaH+kdkTVJ3DtJEwv92itpKbxU+FjPoh2m/5G9zmUQfrL4A4C13c+iGA== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -4039,13 +4039,13 @@ cypress-terminal-report@^5.3.2: tv4 "^1.3.0" cypress@^12.0.0: - version "12.17.2" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.2.tgz#040ac55de1e811f6e037d231a2869d5ab8c29c85" - integrity sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg== + version "12.17.3" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.3.tgz#1e2b19037236fc60e4a4b3a14f0846be17a1fc0e" + integrity sha512-/R4+xdIDjUSLYkiQfwJd630S81KIgicmQOLXotFxVXkl+eTeVO+3bHXxdi5KBh/OgC33HWN33kHX+0tQR/ZWpg== dependencies: "@cypress/request" "^2.88.11" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" + "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -5775,7 +5775,7 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3: +iconv-lite@0.6.3, iconv-lite@^0.6: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -7078,6 +7078,13 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== +mailhog@^4.16.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/mailhog/-/mailhog-4.16.0.tgz#1ad4dda104505399f3f17824737a962696e7d240" + integrity sha512-wXrGik+0MaAy4dbYTImxa8niX9a4aRpZTzC/b1GzCvQs09khhs0aKZgHjgScakI4Y18WInDvvF48hhEz9ifN4g== + optionalDependencies: + iconv-lite "^0.6" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -8051,7 +8058,7 @@ proxy-from-env@1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== -psl@^1.1.28, psl@^1.1.33: +psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -9269,13 +9276,15 @@ tough-cookie@^4.1.2: universalify "^0.2.0" url-parse "^1.5.3" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== +tough-cookie@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== dependencies: - psl "^1.1.28" + psl "^1.1.33" punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" tr46@^1.0.1: version "1.0.1"