diff --git a/package.json b/package.json index 29f8a0737f..326567f044 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "glob-to-regexp": "^0.4.1", "highlight.js": "^11.3.1", "html-entities": "^1.4.0", - "idb-mutex": "^0.11.0", "is-ip": "^3.1.0", "jszip": "^3.7.0", "katex": "^0.12.0", diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index bff378878b..de73fcc051 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -58,7 +58,6 @@ import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDis import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; import { setSentryUser } from "./sentry"; -import { IRenewedMatrixClientCreds, TokenLifecycle } from "./TokenLifecycle"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -204,7 +203,6 @@ export function attemptTokenLogin( "m.login.token", { token: queryParams.loginToken as string, initial_device_display_name: defaultDeviceDisplayName, - refresh_token: TokenLifecycle.instance.isFeasible, }, ).then(function(creds) { logger.log("Logged in with token"); @@ -311,8 +309,6 @@ export interface IStoredSession { userId: string; deviceId: string; isGuest: boolean; - accessTokenExpiryTs?: number; // set if the token expires - accessTokenRefreshToken?: string | IEncryptedPayload; // set if the token can be renewed } /** @@ -323,7 +319,7 @@ export interface IStoredSession { export async function getStoredSessionVars(): Promise { const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); const isUrl = localStorage.getItem(ID_SERVER_URL_KEY); - let accessToken: string; + let accessToken; try { accessToken = await StorageManager.idbLoad("account", "mx_access_token"); } catch (e) { @@ -341,43 +337,6 @@ export async function getStoredSessionVars(): Promise { } } } - - let accessTokenExpiryTs: number; - let accessTokenRefreshToken: string; - if (accessToken) { - const expiration = localStorage.getItem("mx_access_token_expires_ts"); - if (expiration) accessTokenExpiryTs = Number(expiration); - - if (localStorage.getItem("mx_has_refresh_token")) { - try { - accessTokenRefreshToken = await StorageManager.idbLoad( - "account", "mx_refresh_token", - ); - } catch (e) { - logger.warn( - "StorageManager.idbLoad failed for account:mx_refresh_token " + - "(presuming no refresh token)", - e, - ); - } - - if (!accessTokenRefreshToken) { - accessTokenRefreshToken = localStorage.getItem("mx_refresh_token"); - if (accessTokenRefreshToken) { - try { - // try to migrate refresh token to IndexedDB if we can - await StorageManager.idbSave( - "account", "mx_refresh_token", accessTokenRefreshToken, - ); - localStorage.removeItem("mx_refresh_token"); - } catch (e) { - logger.error("migration of refresh token to IndexedDB failed", e); - } - } - } - } - } - // if we pre-date storing "mx_has_access_token", but we retrieved an access // token, then we should say we have an access token const hasAccessToken = @@ -393,17 +352,7 @@ export async function getStoredSessionVars(): Promise { isGuest = localStorage.getItem("matrix-is-guest") === "true"; } - return { - hsUrl, - isUrl, - hasAccessToken, - accessToken, - accessTokenExpiryTs, - accessTokenRefreshToken, - userId, - deviceId, - isGuest, - }; + return { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest }; } // The pickle key is a string of unspecified length and format. For AES, we @@ -442,41 +391,6 @@ async function abortLogin() { } } -export async function getRenewedStoredSessionVars(): Promise { - const { - userId, - deviceId, - accessToken, - accessTokenExpiryTs, - accessTokenRefreshToken, - } = await getStoredSessionVars(); - - let decryptedAccessToken = accessToken; - let decryptedRefreshToken = accessTokenRefreshToken; - const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); - if (pickleKey) { - logger.log("Got pickle key"); - if (typeof accessToken !== "string") { - const encrKey = await pickleKeyToAesKey(pickleKey); - decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token"); - encrKey.fill(0); - } - if (accessTokenRefreshToken && typeof accessTokenRefreshToken !== "string") { - const encrKey = await pickleKeyToAesKey(pickleKey); - decryptedRefreshToken = await decryptAES(accessTokenRefreshToken, encrKey, "refresh_token"); - encrKey.fill(0); - } - } else { - logger.log("No pickle key available"); - } - - return { - accessToken: decryptedAccessToken as string, - accessTokenExpiryTs: accessTokenExpiryTs, - accessTokenRefreshToken: decryptedRefreshToken as string, - }; -} - // returns a promise which resolves to true if a session is found in // localstorage // @@ -494,16 +408,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): return false; } - const { - hsUrl, - isUrl, - hasAccessToken, - accessToken, - userId, - deviceId, - isGuest, - accessTokenExpiryTs, - } = await getStoredSessionVars(); + const { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest } = await getStoredSessionVars(); if (hasAccessToken && !accessToken) { abortLogin(); @@ -515,11 +420,18 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): return false; } + let decryptedAccessToken = accessToken; const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); - const { - accessToken: decryptedAccessToken, - accessTokenRefreshToken: decryptedRefreshToken, - } = await getRenewedStoredSessionVars(); + if (pickleKey) { + logger.log("Got pickle key"); + if (typeof accessToken !== "string") { + const encrKey = await pickleKeyToAesKey(pickleKey); + decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token"); + encrKey.fill(0); + } + } else { + logger.log("No pickle key available"); + } const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true"; sessionStorage.removeItem("mx_fresh_login"); @@ -534,8 +446,6 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): guest: isGuest, pickleKey: pickleKey, freshLogin: freshLogin, - accessTokenExpiryTs: accessTokenExpiryTs, - accessTokenRefreshToken: decryptedRefreshToken as string, }, false); return true; } else { @@ -601,7 +511,9 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { @@ -625,34 +537,6 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise { - const oldUserId = MatrixClientPeg.get().getUserId(); - const oldDeviceId = MatrixClientPeg.get().getDeviceId(); - if (credentials.userId !== oldUserId || credentials.deviceId !== oldDeviceId) { - throw new Error("Attempted to hydrate in-place with a different session"); - } - - const cli = MatrixClientPeg.get(); - if (!cli) { - throw new Error("Attempted to hydrate a non-existent MatrixClient"); - } - - logger.info("Lifecycle#hydrateInPlace: Persisting credentials and updating access token"); - await persistCredentials(credentials); - MatrixClientPeg.updateUsingCreds(credentials); - - // reset the token timers - TokenLifecycle.instance.startTimers(credentials); - - return cli; -} - /** * fires on_logging_in, optionally clears localstorage, persists new credentials * to localstorage, starts the new client. @@ -675,10 +559,8 @@ async function doSetLoggedIn( " deviceId: " + credentials.deviceId + " guest: " + credentials.guest + " hs: " + credentials.homeserverUrl + - " softLogout: " + softLogout + - " freshLogin: " + credentials.freshLogin + - " tokenExpires: " + (!!credentials.accessTokenExpiryTs) + - " tokenRenewable: " + (!!credentials.accessTokenRefreshToken), + " softLogout: " + softLogout, + " freshLogin: " + credentials.freshLogin, ); // This is dispatched to indicate that the user is still in the process of logging in @@ -706,29 +588,6 @@ async function doSetLoggedIn( MatrixClientPeg.replaceUsingCreds(credentials); - // Check the token's renewal early so we don't have to undo some of the work down below. - logger.info("Lifecycle#doSetLoggedIn: Trying token refresh in case it is needed"); - let didTokenRefresh = false; - try { - const result = await TokenLifecycle.instance.tryTokenExchangeIfNeeded(credentials, MatrixClientPeg.get()); - if (result) { - logger.info("Lifecycle#doSetLoggedIn: Token refresh successful, using credentials"); - credentials.accessToken = result.accessToken; - credentials.accessTokenExpiryTs = result.accessTokenExpiryTs; - credentials.accessTokenRefreshToken = result.accessTokenRefreshToken; - - // don't forget to replace the client with the new credentials - MatrixClientPeg.replaceUsingCreds(credentials); - - didTokenRefresh = true; - } else { - logger.info("Lifecycle#doSetLoggedIn: Token refresh indicated as not needed"); - } - } catch (e) { - logger.error("Lifecycle#doSetLoggedIn: Failed to exchange token", e); - await abortLogin(); - } - setSentryUser(credentials.userId); if (PosthogAnalytics.instance.isEnabled()) { @@ -751,12 +610,8 @@ async function doSetLoggedIn( if (localStorage) { try { await persistCredentials(credentials); - // make sure we don't think that it's a fresh login anymore + // make sure we don't think that it's a fresh login any more sessionStorage.removeItem("mx_fresh_login"); - - if (didTokenRefresh) { - TokenLifecycle.instance.flagNewCredentialsPersisted(); - } } catch (e) { logger.warn("Error using local storage: can't persist session!", e); } @@ -764,9 +619,6 @@ async function doSetLoggedIn( logger.warn("No local storage available: can't persist session!"); } - // Start the token lifecycle as late as possible in case something above goes wrong - TokenLifecycle.instance.startTimers(credentials); - dis.dispatch({ action: 'on_logged_in' }); await startMatrixClient(/*startSyncing=*/!softLogout); @@ -793,44 +645,20 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise { @@ -242,9 +235,6 @@ export async function sendLoginRequest( userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, - // Use the browser's local time for expiration timestamp - see TokenLifecycle for more info - accessTokenExpiryTs: data.expires_in_ms ? (data.expires_in_ms + Date.now()) : null, - accessTokenRefreshToken: data.refresh_token, }; SecurityCustomisations.examineLoginResponse?.(data, creds); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 4064ca5b33..c8502b0795 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -44,8 +44,6 @@ export interface IMatrixClientCreds { userId: string; deviceId?: string; accessToken: string; - accessTokenExpiryTs?: number; // set if access token expires - accessTokenRefreshToken?: string; // set if access token can be renewed guest?: boolean; pickleKey?: string; freshLogin?: boolean; @@ -101,14 +99,6 @@ export interface IMatrixClientPeg { * @param {IMatrixClientCreds} creds The new credentials to use. */ replaceUsingCreds(creds: IMatrixClientCreds): void; - - /** - * Similar to replaceUsingCreds(), but without the replacement operation. - * Credentials that can be updated in-place will be updated. All others - * will be ignored. - * @param {IMatrixClientCreds} creds The new credentials to use. - */ - updateUsingCreds(creds: IMatrixClientCreds): void; } /** @@ -174,15 +164,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { this.createClient(creds); } - public updateUsingCreds(creds: IMatrixClientCreds): void { - if (creds?.accessToken) { - this.currentClientCreds = creds; - this.matrixClient.setAccessToken(creds.accessToken); - } else { - // ignore, per signature - } - } - public async assign(): Promise { for (const dbType of ['indexeddb', 'memory']) { try { diff --git a/src/TokenLifecycle.ts b/src/TokenLifecycle.ts deleted file mode 100644 index e5d3a2d516..0000000000 --- a/src/TokenLifecycle.ts +++ /dev/null @@ -1,233 +0,0 @@ -/* -Copyright 2022 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 { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient } from "matrix-js-sdk/src"; -import { randomString } from "matrix-js-sdk/src/randomstring"; -import Mutex from "idb-mutex"; -import { Optional } from "matrix-events-sdk"; - -import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; -import { getRenewedStoredSessionVars, hydrateSessionInPlace } from "./Lifecycle"; -import { IDB_SUPPORTED } from "./utils/StorageManager"; - -export interface IRenewedMatrixClientCreds extends Pick {} - -const LOCALSTORAGE_UPDATED_BY_KEY = "mx_token_updated_by"; - -const CLIENT_ID = randomString(64); - -export class TokenLifecycle { - public static readonly instance = new TokenLifecycle(); - - private refreshAtTimerId: number; - private mutex: Mutex; - - protected constructor() { - // we only really want one of these floating around, so private-ish - // constructor. Protected allows for unit tests. - - // Don't try to create a mutex if it'll explode - if (IDB_SUPPORTED) { - this.mutex = new Mutex("token_refresh", null, { - expiry: 120000, // 2 minutes - enough time for the refresh request to time out - }); - } - - // Watch for other tabs causing token refreshes, so we can react to them too. - window.addEventListener("storage", (ev: StorageEvent) => { - if (ev.key === LOCALSTORAGE_UPDATED_BY_KEY) { - const updateBy = localStorage.getItem(LOCALSTORAGE_UPDATED_BY_KEY); - if (!updateBy || updateBy === CLIENT_ID) return; // ignore deletions & echos - - logger.info("TokenLifecycle#storageWatch: Token update received"); - - // noinspection JSIgnoredPromiseFromCall - this.forceHydration(); - } - }); - } - - /** - * Can the client reasonably support token refreshes? - */ - public get isFeasible(): boolean { - return IDB_SUPPORTED; - } - - // noinspection JSMethodCanBeStatic - private get fiveMinutesAgo(): number { - return Date.now() - 300000; - } - - // noinspection JSMethodCanBeStatic - private get fiveMinutesFromNow(): number { - return Date.now() + 300000; - } - - public flagNewCredentialsPersisted() { - logger.info("TokenLifecycle#flagPersisted: Credentials marked as persisted - flagging for other tabs"); - if (localStorage.getItem(LOCALSTORAGE_UPDATED_BY_KEY) !== CLIENT_ID) { - localStorage.setItem(LOCALSTORAGE_UPDATED_BY_KEY, CLIENT_ID); - } - } - - /** - * Attempts a token renewal, if renewal is needed/possible. If renewal is not possible - * then this will return falsy. Otherwise, the new token's details (credentials) will - * be returned or an error if something went wrong. - * @param {IMatrixClientCreds} credentials The input credentials. - * @param {MatrixClient} client A client set up with those credentials. - * @returns {Promise>} Resolves to the new credentials, - * or falsy if renewal not possible/needed. Throws on error. - */ - public async tryTokenExchangeIfNeeded( - credentials: IMatrixClientCreds, - client: MatrixClient, - ): Promise> { - if (!credentials.accessTokenExpiryTs && credentials.accessTokenRefreshToken) { - logger.warn( - "TokenLifecycle#tryExchange: Got a refresh token, but no expiration time. The server is " + - "not compliant with the specification and might result in unexpected logouts.", - ); - } - - if (!this.isFeasible) { - logger.warn("TokenLifecycle#tryExchange: Client cannot do token refreshes reliably"); - return; - } - - if (credentials.accessTokenExpiryTs && credentials.accessTokenRefreshToken) { - if (this.fiveMinutesAgo >= credentials.accessTokenExpiryTs) { - logger.info("TokenLifecycle#tryExchange: Token has or will expire soon, refreshing"); - return await this.doTokenRefresh(credentials, client); - } - } - } - - // noinspection JSMethodCanBeStatic - private async doTokenRefresh( - credentials: IMatrixClientCreds, - client: MatrixClient, - ): Promise> { - try { - logger.info("TokenLifecycle#doRefresh: Acquiring lock"); - await this.mutex.lock(); - logger.info("TokenLifecycle#doRefresh: Lock acquired"); - - logger.info("TokenLifecycle#doRefresh: Performing refresh"); - localStorage.removeItem(LOCALSTORAGE_UPDATED_BY_KEY); - const newCreds = await client.refreshToken(credentials.accessTokenRefreshToken); - return { - // We use the browser's local time to do two things: - // 1. Avoid having to write code that counts down and stores a "time left" variable - // 2. Work around any time drift weirdness by assuming the user's local machine will - // drift consistently with itself. - // We additionally add our own safety buffer when renewing tokens to avoid cases where - // the time drift is accelerating. - accessTokenExpiryTs: Date.now() + newCreds.expires_in_ms, - accessToken: newCreds.access_token, - accessTokenRefreshToken: newCreds.refresh_token, - }; - } catch (e) { - logger.error("TokenLifecycle#doRefresh: Error refreshing token: ", e); - if (e.errcode === "M_UNKNOWN_TOKEN") { - // Emit the logout manually because the function inhibits it. - client.emit("Session.logged_out", e); - } else { - throw e; // we can't do anything with it, so re-throw - } - } finally { - logger.info("TokenLifecycle#doRefresh: Releasing lock"); - await this.mutex.unlock(); - } - } - - public startTimers(credentials: IMatrixClientCreds) { - this.stopTimers(); - - if (!credentials.accessTokenExpiryTs && credentials.accessTokenRefreshToken) { - logger.warn( - "TokenLifecycle#start: Got a refresh token, but no expiration time. The server is " + - "not compliant with the specification and might result in unexpected logouts.", - ); - } - - if (!this.isFeasible) { - logger.warn("TokenLifecycle#start: Not starting refresh timers - browser unsupported"); - } - - if (credentials.accessTokenExpiryTs && credentials.accessTokenRefreshToken) { - // We schedule the refresh task for 5 minutes before the expiration timestamp as - // a safety buffer. We assume/hope that servers won't be expiring tokens faster - // than every 5 minutes, but we do need to consider cases where the expiration is - // fairly quick (<10 minutes, for example). - let relativeTime = credentials.accessTokenExpiryTs - this.fiveMinutesFromNow; - if (relativeTime <= 0) { - logger.warn(`TokenLifecycle#start: Refresh was set for ${relativeTime}ms - readjusting`); - relativeTime = Math.floor(Math.random() * 5000) + 30000; // 30 seconds + 5s jitter - } - this.refreshAtTimerId = setTimeout(() => { - // noinspection JSIgnoredPromiseFromCall - this.forceTokenExchange(); - }, relativeTime); - logger.info(`TokenLifecycle#start: Refresh timer set for ${relativeTime}ms from now`); - } else { - logger.info("TokenLifecycle#start: Not setting a refresh timer - token not renewable"); - } - } - - public stopTimers() { - clearTimeout(this.refreshAtTimerId); - logger.info("TokenLifecycle#stop: Stopped refresh timer (if it was running)"); - } - - private async forceTokenExchange() { - const credentials = MatrixClientPeg.getCredentials(); - await this.rehydrate(await this.doTokenRefresh(credentials, MatrixClientPeg.get())); - this.flagNewCredentialsPersisted(); - } - - private async forceHydration() { - const { - accessToken, - accessTokenRefreshToken, - accessTokenExpiryTs, - } = await getRenewedStoredSessionVars(); - return this.rehydrate({ accessToken, accessTokenRefreshToken, accessTokenExpiryTs }); - } - - private async rehydrate(newCreds: IRenewedMatrixClientCreds) { - const credentials = MatrixClientPeg.getCredentials(); - try { - if (!newCreds) { - logger.error("TokenLifecycle#expireExchange: Expecting new credentials, got nothing. Rescheduling."); - this.startTimers(credentials); - } else { - logger.info("TokenLifecycle#expireExchange: Updating client credentials using rehydration"); - await hydrateSessionInPlace({ - ...credentials, - ...newCreds, // override from credentials - }); - // hydrateSessionInPlace will ultimately call back to startTimers() for us, so no need to do it here. - } - } catch (e) { - logger.error("TokenLifecycle#expireExchange: Error getting new credentials. Rescheduling.", e); - this.startTimers(credentials); - } - } -} diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 8288198e9a..828ca8d79d 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -37,7 +37,6 @@ import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; import InteractiveAuth from "../InteractiveAuth"; import Spinner from "../../views/elements/Spinner"; -import { TokenLifecycle } from "../../../TokenLifecycle"; interface IProps { serverConfig: ValidatedServerConfig; @@ -416,7 +415,6 @@ export default class Registration extends React.Component { initial_device_display_name: this.props.defaultDeviceDisplayName, auth: undefined, inhibit_login: undefined, - refresh_token: TokenLifecycle.instance.isFeasible, }; if (auth) registerParams.auth = auth; if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin; diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 757240256f..86e6711359 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -33,7 +33,6 @@ import AccessibleButton from '../../views/elements/AccessibleButton'; import Spinner from "../../views/elements/Spinner"; import AuthHeader from "../../views/auth/AuthHeader"; import AuthBody from "../../views/auth/AuthBody"; -import { TokenLifecycle } from "../../../TokenLifecycle"; const LOGIN_VIEW = { LOADING: 1, @@ -155,7 +154,6 @@ export default class SoftLogout extends React.Component { }, password: this.state.password, device_id: MatrixClientPeg.get().getDeviceId(), - refresh_token: TokenLifecycle.instance.isFeasible, }; let credentials = null; @@ -189,7 +187,6 @@ export default class SoftLogout extends React.Component { const loginParams = { token: this.props.realQueryParams['loginToken'], device_id: MatrixClientPeg.get().getDeviceId(), - refresh_token: TokenLifecycle.instance.isFeasible, }; let credentials = null; diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts index 16fe6bd030..7d9ce885f7 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -25,13 +25,11 @@ const localStorage = window.localStorage; // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. -let indexedDB: IDBFactory; +let indexedDB; try { indexedDB = window.indexedDB; } catch (e) {} -export const IDB_SUPPORTED = !!indexedDB; - // The JS SDK will add a prefix of "matrix-js-sdk:" to the sync store name. const SYNC_STORE_NAME = "riot-web-sync"; const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto"; @@ -199,7 +197,7 @@ export function setCryptoInitialised(cryptoInited) { /* Simple wrapper functions around IndexedDB. */ -let idb: IDBDatabase = null; +let idb = null; async function idbInit(): Promise { if (!indexedDB) { diff --git a/yarn.lock b/yarn.lock index 981bac5d75..ce8543a251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4750,11 +4750,6 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -idb-mutex@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/idb-mutex/-/idb-mutex-0.11.0.tgz#1573321f74ab83c12c3d200c7cf22ee7c6800d2d" - integrity sha512-jirzMahSlkvNpq9MXzr5uBKjxQrA9gdPYhOJkQXhDW7MvP6RuJpSbog50HYOugkmZWfJ0WmHVhhX0/lG39qOZQ== - ieee754@^1.1.12, ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"