diff --git a/package.json b/package.json index 326567f044..29f8a0737f 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "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 b4f48ec511..bff378878b 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -58,6 +58,7 @@ 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"; @@ -203,6 +204,7 @@ 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"); @@ -309,6 +311,8 @@ 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 } /** @@ -319,7 +323,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; + let accessToken: string; try { accessToken = await StorageManager.idbLoad("account", "mx_access_token"); } catch (e) { @@ -337,6 +341,43 @@ 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 = @@ -352,7 +393,17 @@ export async function getStoredSessionVars(): Promise { isGuest = localStorage.getItem("matrix-is-guest") === "true"; } - return { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest }; + return { + hsUrl, + isUrl, + hasAccessToken, + accessToken, + accessTokenExpiryTs, + accessTokenRefreshToken, + userId, + deviceId, + isGuest, + }; } // The pickle key is a string of unspecified length and format. For AES, we @@ -391,6 +442,41 @@ 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 // @@ -408,7 +494,16 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): return false; } - const { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest } = await getStoredSessionVars(); + const { + hsUrl, + isUrl, + hasAccessToken, + accessToken, + userId, + deviceId, + isGuest, + accessTokenExpiryTs, + } = await getStoredSessionVars(); if (hasAccessToken && !accessToken) { abortLogin(); @@ -420,18 +515,11 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): return false; } - let decryptedAccessToken = accessToken; 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); - } - } else { - logger.log("No pickle key available"); - } + const { + accessToken: decryptedAccessToken, + accessTokenRefreshToken: decryptedRefreshToken, + } = await getRenewedStoredSessionVars(); const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true"; sessionStorage.removeItem("mx_fresh_login"); @@ -446,6 +534,8 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): guest: isGuest, pickleKey: pickleKey, freshLogin: freshLogin, + accessTokenExpiryTs: accessTokenExpiryTs, + accessTokenRefreshToken: decryptedRefreshToken as string, }, false); return true; } else { @@ -511,12 +601,10 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { +export async function hydrateSession(credentials: IMatrixClientCreds): Promise { const oldUserId = MatrixClientPeg.get().getUserId(); const oldDeviceId = MatrixClientPeg.get().getDeviceId(); @@ -529,9 +617,42 @@ export 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. @@ -554,8 +675,10 @@ async function doSetLoggedIn( " deviceId: " + credentials.deviceId + " guest: " + credentials.guest + " hs: " + credentials.homeserverUrl + - " softLogout: " + softLogout, - " freshLogin: " + credentials.freshLogin, + " softLogout: " + softLogout + + " freshLogin: " + credentials.freshLogin + + " tokenExpires: " + (!!credentials.accessTokenExpiryTs) + + " tokenRenewable: " + (!!credentials.accessTokenRefreshToken), ); // This is dispatched to indicate that the user is still in the process of logging in @@ -583,6 +706,29 @@ 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()) { @@ -605,8 +751,12 @@ async function doSetLoggedIn( if (localStorage) { try { await persistCredentials(credentials); - // make sure we don't think that it's a fresh login any more + // make sure we don't think that it's a fresh login anymore sessionStorage.removeItem("mx_fresh_login"); + + if (didTokenRefresh) { + TokenLifecycle.instance.flagNewCredentialsPersisted(); + } } catch (e) { logger.warn("Error using local storage: can't persist session!", e); } @@ -614,6 +764,9 @@ 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); @@ -640,20 +793,44 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise { @@ -235,6 +242,9 @@ 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 42a54e3ca1..fefba6c117 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -44,6 +44,8 @@ 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; @@ -99,6 +101,14 @@ 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; } /** @@ -164,6 +174,15 @@ 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 { @@ -233,7 +252,15 @@ class MatrixClientPegClass implements IMatrixClientPeg { } public getCredentials(): IMatrixClientCreds { + let copiedCredentials = this.currentClientCreds; + if (this.currentClientCreds?.userId !== this.matrixClient?.credentials?.userId) { + // cached credentials belong to a different user - don't use them + copiedCredentials = null; + } return { + // Copy the cached credentials before overriding what we can. + ...(copiedCredentials ?? {}), + homeserverUrl: this.matrixClient.baseUrl, identityServerUrl: this.matrixClient.idBaseUrl, userId: this.matrixClient.credentials.userId, diff --git a/src/TokenLifecycle.ts b/src/TokenLifecycle.ts new file mode 100644 index 0000000000..e5d3a2d516 --- /dev/null +++ b/src/TokenLifecycle.ts @@ -0,0 +1,233 @@ +/* +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 828ca8d79d..8288198e9a 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -37,6 +37,7 @@ 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; @@ -415,6 +416,7 @@ 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 86e6711359..757240256f 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -33,6 +33,7 @@ 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, @@ -154,6 +155,7 @@ export default class SoftLogout extends React.Component { }, password: this.state.password, device_id: MatrixClientPeg.get().getDeviceId(), + refresh_token: TokenLifecycle.instance.isFeasible, }; let credentials = null; @@ -187,6 +189,7 @@ 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 7d9ce885f7..16fe6bd030 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -25,11 +25,13 @@ const localStorage = window.localStorage; // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. -let indexedDB; +let indexedDB: IDBFactory; 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"; @@ -197,7 +199,7 @@ export function setCryptoInitialised(cryptoInited) { /* Simple wrapper functions around IndexedDB. */ -let idb = null; +let idb: IDBDatabase = null; async function idbInit(): Promise { if (!indexedDB) { diff --git a/yarn.lock b/yarn.lock index ce8543a251..981bac5d75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4750,6 +4750,11 @@ 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"