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 <richard@matrix.org>
pull/28217/head
Hubert Chathi 2024-05-20 10:53:50 -04:00 committed by GitHub
parent 3e103941d6
commit 1bb70c5b3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 197 additions and 73 deletions

View File

@ -81,6 +81,7 @@
"@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2", "@zxcvbn-ts/language-en": "^3.0.2",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"bloom-filters": "^3.0.1",
"blurhash": "^2.0.3", "blurhash": "^2.0.3",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"commonmark": "^0.31.0", "commonmark": "^0.31.0",
@ -182,6 +183,7 @@
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.11.0", "@types/sanitize-html": "2.11.0",
"@types/sdp-transform": "^2.4.6", "@types/sdp-transform": "^2.4.6",
"@types/seedrandom": "<3.0.5",
"@types/tar-js": "^0.3.2", "@types/tar-js": "^0.3.2",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2",

View File

@ -14,12 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ScalableBloomFilter } from "bloom-filters";
import { CryptoEvent, HttpApiEvent, MatrixClient, MatrixEventEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; 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 { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescript/Error";
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
import { PosthogAnalytics } from "./PosthogAnalytics"; 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 { export class DecryptionFailure {
/** /**
* The time between our initial failure to decrypt and our successful * The time between our initial failure to decrypt and our successful
@ -104,8 +108,8 @@ export class DecryptionFailureTracker {
*/ */
public visibleEvents: Set<string> = new Set(); public visibleEvents: Set<string> = new Set();
/** Event IDs of failures that were reported previously */ /** Bloom filter tracking event IDs of failures that were reported previously */
private reportedEvents: Set<string> = new Set(); private reportedEvents: ScalableBloomFilter = new ScalableBloomFilter();
/** Set to an interval ID when `start` is called */ /** Set to an interval ID when `start` is called */
public checkInterval: number | null = null; public checkInterval: number | null = null;
@ -172,13 +176,18 @@ export class DecryptionFailureTracker {
return DecryptionFailureTracker.internalInstance; return DecryptionFailureTracker.internalInstance;
} }
// loadReportedEvents() { private loadReportedEvents(): void {
// this.reportedEvents = new Set(JSON.parse(localStorage.getItem('mx-decryption-failure-event-ids')) || []); const storedFailures = localStorage.getItem(DECRYPTION_FAILURE_STORAGE_KEY);
// } if (storedFailures) {
this.reportedEvents = ScalableBloomFilter.fromJSON(JSON.parse(storedFailures));
} else {
this.reportedEvents = new ScalableBloomFilter();
}
}
// saveReportedEvents() { private saveReportedEvents(): void {
// localStorage.setItem('mx-decryption-failure-event-ids', JSON.stringify([...this.reportedEvents])); localStorage.setItem(DECRYPTION_FAILURE_STORAGE_KEY, JSON.stringify(this.reportedEvents.saveAsJSON()));
// } }
/** Callback for when an event is decrypted. /** Callback for when an event is decrypted.
* *
@ -290,6 +299,7 @@ export class DecryptionFailureTracker {
* Start checking for and tracking failures. * Start checking for and tracking failures.
*/ */
public async start(client: MatrixClient): Promise<void> { public async start(client: MatrixClient): Promise<void> {
this.loadReportedEvents();
await this.calculateClientProperties(client); await this.calculateClientProperties(client);
this.registerHandlers(client); this.registerHandlers(client);
this.checkInterval = window.setInterval( this.checkInterval = window.setInterval(
@ -385,9 +395,7 @@ export class DecryptionFailureTracker {
} }
this.failures = failuresNotReady; this.failures = failuresNotReady;
// Commented out for now for expediency, we need to consider unbound nature of storing this.saveReportedEvents();
// this in localStorage
// this.saveReportedEvents();
} }
/** /**

View File

@ -23,8 +23,8 @@ import {
MatrixClient, MatrixClient,
MatrixEvent, MatrixEvent,
RoomType, RoomType,
SyncStateData,
SyncState, SyncState,
SyncStateData,
TimelineEvents, TimelineEvents,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils"; 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 { UseCaseSelection } from "../views/elements/UseCaseSelection";
import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; 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 { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
import GenericToast from "../views/toasts/GenericToast"; import GenericToast from "../views/toasts/GenericToast";
@ -1585,13 +1585,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
); );
}); });
const dft = DecryptionFailureTracker.instance; DecryptionFailureTracker.instance
.start(cli)
// Shelved for later date when we have time to think about persisting history of .catch((e) => logger.error("Unable to start DecryptionFailureTracker", e));
// tracked events across sessions.
// dft.loadTrackedEventHashMap();
dft.start(cli).catch((e) => logger.error("Unable to start DecryptionFailureTracker", e));
cli.on(ClientEvent.Room, (room) => { cli.on(ClientEvent.Room, (room) => {
if (cli.isCryptoEnabled()) { if (cli.isCryptoEnabled()) {

View File

@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { mocked, Mocked } from "jest-mock"; import { mocked, Mocked, MockedObject } from "jest-mock";
import { CryptoEvent, HttpApiEvent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; import { CryptoEvent, HttpApiEvent, MatrixClient, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing"; import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing";
import { CryptoApi, DecryptionFailureCode, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { CryptoApi, DecryptionFailureCode, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { DecryptionFailureTracker, ErrorProperties } from "../src/DecryptionFailureTracker"; import { DecryptionFailureTracker, ErrorProperties } from "../src/DecryptionFailureTracker";
import { stubClient } from "./test-utils"; import { stubClient } from "./test-utils";
import * as Lifecycle from "../src/Lifecycle";
async function createFailedDecryptionEvent(opts: { sender?: string; code?: DecryptionFailureCode } = {}) { async function createFailedDecryptionEvent(opts: { sender?: string; code?: DecryptionFailureCode } = {}) {
return await mkDecryptionFailureMatrixEvent({ return await mkDecryptionFailureMatrixEvent({
@ -39,6 +40,10 @@ function eventDecrypted(tracker: DecryptionFailureTracker, e: MatrixEvent, nowTs
} }
describe("DecryptionFailureTracker", function () { describe("DecryptionFailureTracker", function () {
afterEach(() => {
localStorage.clear();
});
it("tracks a failed decryption for a visible event", async function () { it("tracks a failed decryption for a visible event", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent(); const failedDecryptionEvent = await createFailedDecryptionEvent();
@ -247,6 +252,7 @@ describe("DecryptionFailureTracker", function () {
() => count++, () => count++,
() => "UnknownError", () => "UnknownError",
); );
await tracker.start(mockClient());
tracker.addVisibleEvent(decryptedEvent); tracker.addVisibleEvent(decryptedEvent);
@ -264,10 +270,7 @@ describe("DecryptionFailureTracker", function () {
expect(count).toBe(1); expect(count).toBe(1);
}); });
it.skip("should not track a failure for an event that was tracked in a previous session", async () => { it("should not report a failure for an event that was reported in a previous session", async () => {
// This test uses localStorage, clear it beforehand
localStorage.clear();
const decryptedEvent = await createFailedDecryptionEvent(); const decryptedEvent = await createFailedDecryptionEvent();
let count = 0; let count = 0;
@ -276,6 +279,7 @@ describe("DecryptionFailureTracker", function () {
() => count++, () => count++,
() => "UnknownError", () => "UnknownError",
); );
await tracker.start(mockClient());
tracker.addVisibleEvent(decryptedEvent); tracker.addVisibleEvent(decryptedEvent);
@ -289,14 +293,13 @@ describe("DecryptionFailureTracker", function () {
// Simulate the browser refreshing by destroying tracker and creating a new tracker // Simulate the browser refreshing by destroying tracker and creating a new tracker
// @ts-ignore access to private constructor // @ts-ignore access to private constructor
const secondTracker = new DecryptionFailureTracker( const secondTracker = new DecryptionFailureTracker(
(total: number) => (count += total), () => count++,
() => "UnknownError", () => "UnknownError",
); );
await secondTracker.start(mockClient());
secondTracker.addVisibleEvent(decryptedEvent); secondTracker.addVisibleEvent(decryptedEvent);
//secondTracker.loadTrackedEvents();
eventDecrypted(secondTracker, decryptedEvent, Date.now()); eventDecrypted(secondTracker, decryptedEvent, Date.now());
secondTracker.checkFailures(Infinity); secondTracker.checkFailures(Infinity);
@ -304,6 +307,70 @@ describe("DecryptionFailureTracker", function () {
expect(count).toBe(1); 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 () => { it("should count different error codes separately for multiple failures with different error codes", async () => {
const counts: Record<string, number> = {}; const counts: Record<string, number> = {};
@ -521,12 +588,7 @@ describe("DecryptionFailureTracker", function () {
it("listens for client events", async () => { it("listens for client events", async () => {
// Test that the decryption failure tracker registers the right event // Test that the decryption failure tracker registers the right event
// handlers on start, and unregisters them when the client logs out. // handlers on start, and unregisters them when the client logs out.
const client = mocked(stubClient()); const client = mockClient();
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<CryptoApi>;
client.getCrypto.mockReturnValue(mockCrypto);
let errorCount: number = 0; let errorCount: number = 0;
// @ts-ignore access to private constructor // @ts-ignore access to private constructor
@ -568,13 +630,7 @@ describe("DecryptionFailureTracker", function () {
}); });
it("tracks client information", async () => { it("tracks client information", async () => {
const client = mocked(stubClient()); const client = mockClient();
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<CryptoApi>;
client.getCrypto.mockReturnValue(mockCrypto);
const propertiesByErrorCode: Record<string, ErrorProperties> = {}; const propertiesByErrorCode: Record<string, ErrorProperties> = {};
// @ts-ignore access to private constructor // @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker( const tracker = new DecryptionFailureTracker(
@ -610,7 +666,9 @@ describe("DecryptionFailureTracker", function () {
const now = Date.now(); const now = Date.now();
eventDecrypted(tracker, federatedDecryption, 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, {}); client.emit(CryptoEvent.KeysChanged, {});
await sleep(100); await sleep(100);
eventDecrypted(tracker, localDecryption, now); eventDecrypted(tracker, localDecryption, now);
@ -628,7 +686,7 @@ describe("DecryptionFailureTracker", function () {
// change client params, and make sure the reports the right values // change client params, and make sure the reports the right values
client.getDomain.mockReturnValue("example.com"); 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 // @ts-ignore access to private method
await tracker.calculateClientProperties(client); await tracker.calculateClientProperties(client);
@ -673,3 +731,21 @@ describe("DecryptionFailureTracker", function () {
expect(failure?.timeToDecryptMillis).toEqual(50000); expect(failure?.timeToDecryptMillis).toEqual(50000);
}); });
}); });
function mockClient(): MockedObject<MatrixClient> {
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<CryptoApi>;
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;
}

100
yarn.lock
View File

@ -2907,6 +2907,11 @@
resolved "https://registry.yarnpkg.com/@types/sdp-transform/-/sdp-transform-2.4.9.tgz#26ef39f487a6909b0512f580b80920a366b27f52" resolved "https://registry.yarnpkg.com/@types/sdp-transform/-/sdp-transform-2.4.9.tgz#26ef39f487a6909b0512f580b80920a366b27f52"
integrity sha512-bVr+/OoZZy7wrHlNcEAAa6PAgKA4BoXPYVN2EijMC5WnGgQ4ZEuixmKnVs2roiAvr7RhIFVH17QD27cojgIZCg== 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": "@types/semver@^7.5.8":
version "7.5.8" version "7.5.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" 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" resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== 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: big-integer@^1.6.48:
version "1.6.51" version "1.6.51"
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" 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" resolved "https://registry.yarnpkg.com/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz#38bf5e046c41a21bb13654d9d19f303233b8218c"
integrity sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ== 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: blurhash@^2.0.3:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.5.tgz#efde729fc14a2f03571a6aa91b49cba80d1abe4b" 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" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== 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: damerau-levenshtein@^1.0.8:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" 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" call-bind "^1.0.2"
has-tostringtag "^1.0.0" 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: is-buffer@~1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 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" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== 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: lodash.isequal@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" 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" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== 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" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -6991,6 +7036,11 @@ loglevel@^1.7.1:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4"
integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== 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: loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 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: dependencies:
"@babel/runtime" "^7.9.2" "@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: reflect.getprototypeof@^1.0.4:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" 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" resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.1.tgz#2bb443583d478dee217df4caa284c46b870d5827"
integrity sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw== 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: "semver@2 || 3 || 4 || 5", semver@^5.6.0:
version "5.7.2" version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" 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" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0": "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==
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:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -8832,14 +8883,7 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
"strip-ansi-cjs@npm: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==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -9632,7 +9676,7 @@ which@^2.0.1:
dependencies: dependencies:
isexe "^2.0.0" 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" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -9650,15 +9694,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.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: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" 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" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== 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: y18n@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"