From 1bb70c5b3bf372a0ad0010ba1725085593fd5761 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 20 May 2024 10:53:50 -0400 Subject: [PATCH] Only report undecryptable events once (#12501) * persist previously-reported event IDs as a bloom filter * Pin to older `@types/seedrandom` ... to work around https://github.com/Callidon/bloom-filters/issues/72 * Inline `DecryptionFailureTracker.addDecryptionFailure` * Remove redundant TRACK_INTERVAL There really doesn't seem to be much point to this batching up of decryption failure reports. We still call the analytics callback the same number of times. * Rename `trackedEvents` to `reportedEvents` * Fix incorrect documentation on `visibleEvents` This *does* overlap with `failures`. * Combine `addFailure` and `reportFailure` * Calculate client properties before starting reporting * Clear localstorage after each test ... otherwise they interfere * Remove redundant comment * Ensure that reports are cleared on a logout/login cycle * make private const private and const --------- Co-authored-by: Richard van der Hoff --- package.json | 2 + src/DecryptionFailureTracker.ts | 30 ++++-- src/components/structures/MatrixChat.tsx | 14 +-- test/DecryptionFailureTracker-test.ts | 124 ++++++++++++++++++----- yarn.lock | 100 ++++++++++++------ 5 files changed, 197 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 74914b522a..84aff16103 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", "await-lock": "^2.1.0", + "bloom-filters": "^3.0.1", "blurhash": "^2.0.3", "classnames": "^2.2.6", "commonmark": "^0.31.0", @@ -182,6 +183,7 @@ "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.11.0", "@types/sdp-transform": "^2.4.6", + "@types/seedrandom": "<3.0.5", "@types/tar-js": "^0.3.2", "@types/ua-parser-js": "^0.7.36", "@types/uuid": "^9.0.2", diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index e8628f5d66..cc336cc5ce 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -14,12 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ScalableBloomFilter } from "bloom-filters"; import { CryptoEvent, HttpApiEvent, MatrixClient, MatrixEventEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescript/Error"; import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { PosthogAnalytics } from "./PosthogAnalytics"; +/** The key that we use to store the `reportedEvents` bloom filter in localstorage */ +const DECRYPTION_FAILURE_STORAGE_KEY = "mx_decryption_failure_event_ids"; + export class DecryptionFailure { /** * The time between our initial failure to decrypt and our successful @@ -104,8 +108,8 @@ export class DecryptionFailureTracker { */ public visibleEvents: Set = new Set(); - /** Event IDs of failures that were reported previously */ - private reportedEvents: Set = new Set(); + /** Bloom filter tracking event IDs of failures that were reported previously */ + private reportedEvents: ScalableBloomFilter = new ScalableBloomFilter(); /** Set to an interval ID when `start` is called */ public checkInterval: number | null = null; @@ -172,13 +176,18 @@ export class DecryptionFailureTracker { return DecryptionFailureTracker.internalInstance; } - // loadReportedEvents() { - // this.reportedEvents = new Set(JSON.parse(localStorage.getItem('mx-decryption-failure-event-ids')) || []); - // } + private loadReportedEvents(): void { + const storedFailures = localStorage.getItem(DECRYPTION_FAILURE_STORAGE_KEY); + if (storedFailures) { + this.reportedEvents = ScalableBloomFilter.fromJSON(JSON.parse(storedFailures)); + } else { + this.reportedEvents = new ScalableBloomFilter(); + } + } - // saveReportedEvents() { - // localStorage.setItem('mx-decryption-failure-event-ids', JSON.stringify([...this.reportedEvents])); - // } + private saveReportedEvents(): void { + localStorage.setItem(DECRYPTION_FAILURE_STORAGE_KEY, JSON.stringify(this.reportedEvents.saveAsJSON())); + } /** Callback for when an event is decrypted. * @@ -290,6 +299,7 @@ export class DecryptionFailureTracker { * Start checking for and tracking failures. */ public async start(client: MatrixClient): Promise { + this.loadReportedEvents(); await this.calculateClientProperties(client); this.registerHandlers(client); this.checkInterval = window.setInterval( @@ -385,9 +395,7 @@ export class DecryptionFailureTracker { } this.failures = failuresNotReady; - // Commented out for now for expediency, we need to consider unbound nature of storing - // this in localStorage - // this.saveReportedEvents(); + this.saveReportedEvents(); } /** diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 29a3a2a779..5e50f68b48 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -23,8 +23,8 @@ import { MatrixClient, MatrixEvent, RoomType, - SyncStateData, SyncState, + SyncStateData, TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils"; @@ -128,7 +128,7 @@ import { TimelineRenderingType } from "../../contexts/RoomContext"; import { UseCaseSelection } from "../views/elements/UseCaseSelection"; import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; -import { SdkContextClass, SDKContext } from "../../contexts/SDKContext"; +import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import GenericToast from "../views/toasts/GenericToast"; @@ -1585,13 +1585,9 @@ export default class MatrixChat extends React.PureComponent { ); }); - const dft = DecryptionFailureTracker.instance; - - // Shelved for later date when we have time to think about persisting history of - // tracked events across sessions. - // dft.loadTrackedEventHashMap(); - - dft.start(cli).catch((e) => logger.error("Unable to start DecryptionFailureTracker", e)); + DecryptionFailureTracker.instance + .start(cli) + .catch((e) => logger.error("Unable to start DecryptionFailureTracker", e)); cli.on(ClientEvent.Room, (room) => { if (cli.isCryptoEnabled()) { diff --git a/test/DecryptionFailureTracker-test.ts b/test/DecryptionFailureTracker-test.ts index ef5e01719f..06fe22ce54 100644 --- a/test/DecryptionFailureTracker-test.ts +++ b/test/DecryptionFailureTracker-test.ts @@ -14,14 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { mocked, Mocked } from "jest-mock"; -import { CryptoEvent, HttpApiEvent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; +import { mocked, Mocked, MockedObject } from "jest-mock"; +import { CryptoEvent, HttpApiEvent, MatrixClient, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing"; import { CryptoApi, DecryptionFailureCode, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { sleep } from "matrix-js-sdk/src/utils"; import { DecryptionFailureTracker, ErrorProperties } from "../src/DecryptionFailureTracker"; import { stubClient } from "./test-utils"; +import * as Lifecycle from "../src/Lifecycle"; async function createFailedDecryptionEvent(opts: { sender?: string; code?: DecryptionFailureCode } = {}) { return await mkDecryptionFailureMatrixEvent({ @@ -39,6 +40,10 @@ function eventDecrypted(tracker: DecryptionFailureTracker, e: MatrixEvent, nowTs } describe("DecryptionFailureTracker", function () { + afterEach(() => { + localStorage.clear(); + }); + it("tracks a failed decryption for a visible event", async function () { const failedDecryptionEvent = await createFailedDecryptionEvent(); @@ -247,6 +252,7 @@ describe("DecryptionFailureTracker", function () { () => count++, () => "UnknownError", ); + await tracker.start(mockClient()); tracker.addVisibleEvent(decryptedEvent); @@ -264,10 +270,7 @@ describe("DecryptionFailureTracker", function () { expect(count).toBe(1); }); - it.skip("should not track a failure for an event that was tracked in a previous session", async () => { - // This test uses localStorage, clear it beforehand - localStorage.clear(); - + it("should not report a failure for an event that was reported in a previous session", async () => { const decryptedEvent = await createFailedDecryptionEvent(); let count = 0; @@ -276,6 +279,7 @@ describe("DecryptionFailureTracker", function () { () => count++, () => "UnknownError", ); + await tracker.start(mockClient()); tracker.addVisibleEvent(decryptedEvent); @@ -289,14 +293,13 @@ describe("DecryptionFailureTracker", function () { // Simulate the browser refreshing by destroying tracker and creating a new tracker // @ts-ignore access to private constructor const secondTracker = new DecryptionFailureTracker( - (total: number) => (count += total), + () => count++, () => "UnknownError", ); + await secondTracker.start(mockClient()); secondTracker.addVisibleEvent(decryptedEvent); - //secondTracker.loadTrackedEvents(); - eventDecrypted(secondTracker, decryptedEvent, Date.now()); secondTracker.checkFailures(Infinity); @@ -304,6 +307,70 @@ describe("DecryptionFailureTracker", function () { expect(count).toBe(1); }); + it("should report a failure for an event that was tracked but not reported in a previous session", async () => { + const decryptedEvent = await createFailedDecryptionEvent(); + + let count = 0; + + // @ts-ignore access to private constructor + const tracker = new DecryptionFailureTracker( + () => count++, + () => "UnknownError", + ); + await tracker.start(mockClient()); + + tracker.addVisibleEvent(decryptedEvent); + + // Indicate decryption + eventDecrypted(tracker, decryptedEvent, Date.now()); + + // we do *not* call `checkFailures` here + expect(count).toBe(0); + + // Simulate the browser refreshing by destroying tracker and creating a new tracker + // @ts-ignore access to private constructor + const secondTracker = new DecryptionFailureTracker( + () => count++, + () => "UnknownError", + ); + await secondTracker.start(mockClient()); + + secondTracker.addVisibleEvent(decryptedEvent); + + eventDecrypted(secondTracker, decryptedEvent, Date.now()); + secondTracker.checkFailures(Infinity); + expect(count).toBe(1); + }); + + it("should report a failure for an event that was reported before a logout/login cycle", async () => { + const decryptedEvent = await createFailedDecryptionEvent(); + + let count = 0; + + // @ts-ignore access to private constructor + const tracker = new DecryptionFailureTracker( + () => count++, + () => "UnknownError", + ); + await tracker.start(mockClient()); + + tracker.addVisibleEvent(decryptedEvent); + + // Indicate decryption + eventDecrypted(tracker, decryptedEvent, Date.now()); + tracker.checkFailures(Infinity); + expect(count).toBe(1); + + // Simulate a logout/login cycle + await Lifecycle.onLoggedOut(); + await tracker.start(mockClient()); + + tracker.addVisibleEvent(decryptedEvent); + eventDecrypted(tracker, decryptedEvent, Date.now()); + tracker.checkFailures(Infinity); + expect(count).toBe(2); + }); + it("should count different error codes separately for multiple failures with different error codes", async () => { const counts: Record = {}; @@ -521,12 +588,7 @@ describe("DecryptionFailureTracker", function () { it("listens for client events", async () => { // Test that the decryption failure tracker registers the right event // handlers on start, and unregisters them when the client logs out. - const client = mocked(stubClient()); - const mockCrypto = { - getVersion: jest.fn().mockReturnValue("Rust SDK 0.7.0 (61b175b), Vodozemac 0.5.1"), - getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)), - } as unknown as Mocked; - client.getCrypto.mockReturnValue(mockCrypto); + const client = mockClient(); let errorCount: number = 0; // @ts-ignore access to private constructor @@ -568,13 +630,7 @@ describe("DecryptionFailureTracker", function () { }); it("tracks client information", async () => { - const client = mocked(stubClient()); - const mockCrypto = { - getVersion: jest.fn().mockReturnValue("Rust SDK 0.7.0 (61b175b), Vodozemac 0.5.1"), - getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)), - } as unknown as Mocked; - client.getCrypto.mockReturnValue(mockCrypto); - + const client = mockClient(); const propertiesByErrorCode: Record = {}; // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( @@ -610,7 +666,9 @@ describe("DecryptionFailureTracker", function () { const now = Date.now(); eventDecrypted(tracker, federatedDecryption, now); - mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, false)); + mocked(client.getCrypto()!.getUserVerificationStatus).mockResolvedValue( + new UserVerificationStatus(true, true, false), + ); client.emit(CryptoEvent.KeysChanged, {}); await sleep(100); eventDecrypted(tracker, localDecryption, now); @@ -628,7 +686,7 @@ describe("DecryptionFailureTracker", function () { // change client params, and make sure the reports the right values client.getDomain.mockReturnValue("example.com"); - mockCrypto.getVersion.mockReturnValue("Olm 0.0.0"); + mocked(client.getCrypto()!.getVersion).mockReturnValue("Olm 0.0.0"); // @ts-ignore access to private method await tracker.calculateClientProperties(client); @@ -673,3 +731,21 @@ describe("DecryptionFailureTracker", function () { expect(failure?.timeToDecryptMillis).toEqual(50000); }); }); + +function mockClient(): MockedObject { + const client = mocked(stubClient()); + const mockCrypto = { + getVersion: jest.fn().mockReturnValue("Rust SDK 0.7.0 (61b175b), Vodozemac 0.5.1"), + getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)), + } as unknown as Mocked; + client.getCrypto.mockReturnValue(mockCrypto); + + // @ts-ignore + client.stopClient = jest.fn(() => {}); + // @ts-ignore + client.removeAllListeners = jest.fn(() => {}); + + client.store = { destroy: jest.fn(() => {}) } as any; + + return client; +} diff --git a/yarn.lock b/yarn.lock index 86f30bb6fb..695c5deabb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2907,6 +2907,11 @@ resolved "https://registry.yarnpkg.com/@types/sdp-transform/-/sdp-transform-2.4.9.tgz#26ef39f487a6909b0512f580b80920a366b27f52" integrity sha512-bVr+/OoZZy7wrHlNcEAAa6PAgKA4BoXPYVN2EijMC5WnGgQ4ZEuixmKnVs2roiAvr7RhIFVH17QD27cojgIZCg== +"@types/seedrandom@<3.0.5": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.4.tgz#e4a8d0fca0168cacc7dba2af0e4a4ea645d3a190" + integrity sha512-/rWdxeiuZenlawrHU+XV6ZHMTKOqrC2hMfeDfLTIWJhDZP5aVqXRysduYHBbhD7CeJO6FJr/D2uBVXB7GT6v7w== + "@types/semver@^7.5.8": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -3594,6 +3599,11 @@ base-x@^4.0.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + big-integer@^1.6.48: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" @@ -3614,6 +3624,21 @@ blob-polyfill@^7.0.0: resolved "https://registry.yarnpkg.com/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz#38bf5e046c41a21bb13654d9d19f303233b8218c" integrity sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ== +bloom-filters@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/bloom-filters/-/bloom-filters-3.0.1.tgz#13e28ed22febe2489cd00ba5bd98fdc90e820180" + integrity sha512-rU9IU6bgZ1jmqcLWhlKSidrFjbIGjB89CJBsQqUj1+3/11tAJDwn+f7iRu4bbQ2srTjGgNeoWNwcnelumqdi0g== + dependencies: + base64-arraybuffer "^1.0.2" + is-buffer "^2.0.5" + lodash "^4.17.15" + lodash.eq "^4.0.0" + lodash.indexof "^4.0.5" + long "^5.2.0" + reflect-metadata "^0.1.13" + seedrandom "^3.0.5" + xxhashjs "^0.2.2" + blurhash@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.5.tgz#efde729fc14a2f03571a6aa91b49cba80d1abe4b" @@ -4155,6 +4180,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +cuint@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" + integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw== + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -5976,6 +6006,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -6961,6 +6996,16 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.eq@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lodash.eq/-/lodash.eq-4.0.0.tgz#a39f06779e72f9c0d1f310c90cd292c1661d5035" + integrity sha512-vbrJpXL6kQNG6TkInxX12DZRfuYVllSxhwYqjYB78g2zF3UI15nFO/0AgmZnZRnaQ38sZtjCiVjGr2rnKt4v0g== + +lodash.indexof@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/lodash.indexof/-/lodash.indexof-4.0.5.tgz#53714adc2cddd6ed87638f893aa9b6c24e31ef3c" + integrity sha512-t9wLWMQsawdVmf6/IcAgVGqAJkNzYVcn4BHYZKTPW//l7N5Oq7Bq138BaVk19agcsPZePcidSgTTw4NqS1nUAw== + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -6981,7 +7026,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6991,6 +7036,11 @@ loglevel@^1.7.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== +long@^5.2.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -8172,6 +8222,11 @@ redux@^4.0.0, redux@^4.0.4: dependencies: "@babel/runtime" "^7.9.2" +reflect-metadata@^0.1.13: + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" @@ -8472,6 +8527,11 @@ sdp-transform@^2.14.1: resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.1.tgz#2bb443583d478dee217df4caa284c46b870d5827" integrity sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw== +seedrandom@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" + integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== + "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -8729,16 +8789,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8832,14 +8883,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9632,7 +9676,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9650,15 +9694,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -9709,6 +9744,13 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xxhashjs@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8" + integrity sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw== + dependencies: + cuint "^0.2.2" + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"